简介面向微信小程序开发者的易打卡签到完整案例源码涵盖了用户签到、历史记录等典型业务场景适合需要实战项目参考的入门与进阶开发者。压缩包内共81个文件包含40张PNG界面图、11个JS逻辑文件、10个WXSS样式表、9个WXML页面结构、9个JSON配置文件整体仅1.51MB便于快速下载与对照学习。源码目录结构清晰包含pages页面目录、全局app.js与app.json、utils工具函数以及request.js网络请求模块可帮助理解小程序生命周期、全局配置和页面间通信。目前已有895人学习该案例通过研读实际签到逻辑能够提升微信小程序开发规范、状态管理及API调用的实操能力。1. 微信小程序签到源码一个能跑的易打卡工程长什么样微信小程序签到类项目真正麻烦的不是那个打卡按钮而是把登录授权、日历渲染、打卡状态写入、连续天数统计这一整串状态串起来还要保证在 iOS 和 Android 上表现一致。这份易打卡签到案例源码对应的是一个很务实的场景培训机构老师每天在微信群里催学员打卡网页版签到率低到可以忽略换成微信小程序里点开即用的工具后完成率肉眼可见地涨了上去。工程不大但结构完整——日历打卡页、记录页、个人中心页、登录授权页齐全数据层用本地缓存模拟后端下载导入就能跑。小程序入门开发者可以拿它通读一遍完整链路做课程设计的人也能直接拿它当骨架改业务。我这就按拆工程、跑链路、踩坑、改规则的顺序带你盘一遍。2. 拆工程结构四个页面、全局配置与启动流程拿到压缩包先别急着点“导入”打开工程后的第一步应该是把目录结构摸清。原生小程序没有复杂的构建层目录即页面看懂 pages 数组就等于看懂了整张产品地图。2.1 页面划分与跳转关系这份源码一共四个页面对应四条业务路径pages/index/index打卡主页面承担日历展示与打卡操作两大职责pages/record/record打卡记录页按周或按月浏览历史记录pages/mine/mine个人中心页展示用户信息、连续打卡天数与设置入口pages/login/login登录授权页处理微信登录与用户信息获取页面跳转关系是一条典型的漏斗小程序冷启动先进 index如果本地 storage 里没有 token就重定向到 login登录完成后跳回 indexindex 上的记录入口跳 record头像入口跳 mine。源码里没有用 tabBar 之外的复杂路由所有跳转都是 wx.navigateTo 或 wx.redirectTo这种简单的控制流适合入门阅读。我拆的时候注意到一个设计细节login 页没有放在 pages 数组第一位说明作者不打算把它当启动页而是用 index 里的 onLoad 生命周期去判断登录态。这个判断逻辑位置放得不错——把登录拦截收敛到页面加载阶段而不是在 app.json 里做 redirect代码更容易调试。2.2 app.json 全局配置与 tabBar 参数app.json 是整个小程序的兜底配置页面注册、窗口样式、底部导航都在这里决定。这份源码的配置大概是这样的结构{ pages: [ pages/index/index, pages/record/record, pages/mine/mine, pages/login/login ], window: { navigationBarTitleText: 易打卡, navigationBarBackgroundColor: #ffffff, navigationBarTextStyle: black, backgroundColor: #f6f6f6 }, tabBar: { color: #999999, selectedColor: #07c160, backgroundColor: #ffffff, list: [ { pagePath: pages/index/index, text: 打卡 }, { pagePath: pages/mine/mine, text: 我的 } ] } }pages 数组第一项决定冷启动页面这里排的是 index意味着无论从分享卡片还是从记录页返回最终都会以 index 为根页面。window 里 navigationBarTitleText 控制顶部标题navigationBarTextStyle 只有 black 和 white 两个选项想用其他颜色必须在自定义导航里做。tabBar 的关键参数是 color 与 selectedColor前者是未选中态文字颜色通常用 #999999 这类低对比灰后者是选中态高亮色绿色系 #07c160 很适合打卡这类“完成”语义的场景。tabBar 的 list 数组最少 2 项、最多 5 项pagePath 必须和 pages 里的路径完全一致否则编译直接报错。如果后面想加一个“统计”入口在 list 里追加一项并保证 pages 里有对应页面即可。2.3 app.js 启动流程设备信息与登录态检查app.js 的 onLaunch 是全局最先执行的逻辑通常用来做两类事收集设备信息供全局使用检查本地登录态。这份源码在 onLaunch 里做了很关键的一步——读取胶囊按钮和状态栏位置App({ onLaunch() { // 胶囊按钮位置在自定义导航时是硬需求提前算好存全局 const windowInfo wx.getWindowInfo(); const menuRect wx.getMenuButtonBoundingClientRect(); this.globalData.statusBarHeight windowInfo.statusBarHeight; this.globalData.menuRect menuRect; // 登录态检查token 存在说明之前已授权 this.globalData.token wx.getStorageSync(token) || ; }, globalData: { token: , statusBarHeight: 0, menuRect: null } });这里用 wx.getWindowInfo() 替代了老代码里常见的 wx.getSystemInfoSync()前者在基础库 2.20.1 之后更干净字段不冗余。statusBarHeight 是状态栏高度menuRect 是胶囊按钮的 boundingClientRect两者配合才能算出自定义导航栏的真实高度这个方法直接用在了 mine 页和自定义导航组件里。token 从本地缓存读取后放进 globalData后续所有页面判断登录态都读这一个来源避免各页面重复查 storage。3. 打卡链路核心实现日历渲染、登录授权与状态存储主链路的三个技术点决定了这个签到项目能不能用日历怎么生成、登录怎么走通、打卡状态怎么存。每一步都有至少一个值得抄下来的通用函数。3.1 日历渲染生成当月日期数组的通用函数小程序的日历不像 Web 可以直接引成熟的日历组件自己渲染反而更可控。核心思路是先算清楚两件事这个月有多少天、第一天是星期几。然后在 js 层生成一个线性数组wxml 里用 wx:for 渲染成 7 列网格function buildMonthGrid(year, month) { // 传入真实月份new Date 内部月份从 0 开始所以减 1 const firstDay new Date(year, month - 1, 1); // 当月天数new Date(year, month, 0) 是上月最后一天 const daysInMonth new Date(year, month, 0).getDate(); // 周一作为一周开头计算首日前面的空格数 const lead (firstDay.getDay() 6) % 7; const cells []; for (let i 0; i lead; i) { cells.push(null); } for (let d 1; d daysInMonth; d) { const dateStr ${year}-${String(month).padStart(2, 0)}-${String(d).padStart(2, 0)}; cells.push({ dateStr, day: d }); } // 补足到整周方便样式统一 while (cells.length % 7 ! 0) { cells.push(null); } return cells; }几个参数容易绕晕year 和 month 要传真实值比如“2025 年 6 月”就传 2025 和 6new Date(year, month - 1, 1) 会帮你把月份偏移纠正过来。计算当月天数用的是 new Date(year, month, 0).getDate()这里的 0 表示 month 的上一个月的最后一天等于拿到了当月总天数。lead 变量的核心是 (getDay() 6) % 7把 JavaScript 里周日的 0 映射到 6从而实现周一开头。生成的 cells 数组里null 代表空格子正常格子带 dateStr形如 2025-06-15和 day。wxml 渲染时用 dateStr 作 key后面存打卡记录也用它。日历翻月时只需要重新调 buildMonthGrid 并 setData数据量撑死 42 个格子不需要做差分优化。3.2 登录授权code 换 session 与用户信息获取签到类小程序一般都要显示用户昵称头像登录流程是固定的两步wx.login 拿 code把 code 发给后端换 openid 和 token然后调 wx.getUserProfile 拿昵称头像。核心代码如下handleLogin() { wx.login({ success: (res) { if (!res.code) return; // code 有效期 5 分钟且一次性后端拿去换 openid wx.request({ url: https://api.example.com/login, method: POST, data: { code: res.code }, success: (resp) { // 后端返回的自定义登录态要存到本地 wx.setStorageSync(token, resp.data.token); this.getUserProfile(); } }); } }); } getUserProfile() { wx.getUserProfile({ desc: 用于展示打卡头像, success: (res) { wx.setStorageSync(userInfo, res.userInfo); this.setData({ userInfo: res.userInfo }); } }); }wx.login 拿到的 code 是一次性的5 分钟内有效不能缓存复用所以每次登录都要重新调。后端拿 code 换 openid 和 session_key 后自己生成一个自定义 token 返回给前端这个 token 才适合长期保存在本地。还有一个现实约束如果后端接口没准备好可以先把 token 写死一个测试值跑通页面交互后再接真接口。需要提醒的是用户信息接口的版本差异。基础库 2.27.1 之后wx.getUserProfile 在真机上的行为已经和老版本不同更稳的方案是用官方推荐的头像昵称填写能力昵称用 input 组件的 typenickname头像用 button 的 open-typechooseAvatar。另外换手机号必须走 getPhoneNumber但那个接口要求小程序为企业主体且已完成认证个人开发者绕不开这道门槛。3.3 打卡状态写入与缓存 key 设计打卡按钮的交互逻辑很简单但状态存储的设计决定了后续统计逻辑的复杂度。这份源码的存储思路是用一个对象存所有打卡记录key 就是日期字符串handleClockIn() { const now new Date(); // 统一用 yyyy-MM-dd 作为存储 key const dateStr formatDate(now); const history wx.getStorageSync(clockRecords) || {}; // 幂等判断今天已打卡就提示不覆盖原记录 if (history[dateStr]) { wx.showToast({ title: 今天已打卡, icon: none }); return; } // 记录格式留出扩展位后续可加定位、备注 history[dateStr] { time: ${String(now.getHours()).padStart(2, 0)}:${String(now.getMinutes()).padStart(2, 0)}, location: }; wx.setStorageSync(clockRecords, history); this.setData({ todayStatus: history[dateStr] }); }用日期字符串做 key 有三个好处。第一按天分区查某天打了没打就是一次属性访问时间复杂度 O(1)。第二计算连续天数时可以拿昨天的 key 去 records 里查逻辑直观。第三不会因为数组顺序变化导致状态串位配合后面要讲的 wx:for key 能避免一类经典翻车。记录值里留了 location 字段这是给后续加“指定地点打卡”留的扩展位不需要 GPS 权限时留空即可。存储容量不用担心wx.setStorageSync 单 key 上限 1MB一天一条这种量级跑十年也到不了上限。需要注意同步 API 会阻塞渲染打卡场景数据量小无所谓如果未来改成批量写入要换 wx.setStorage 的异步版本。4. 易打卡签到避坑登录、日期与真机适配的五个常见问题源码能跑是一回事换台真机跑不跑得稳是另一回事。这五个坑是我在签到类项目里反复撞过的有些现象只在特定基础库版本或特定机型上出现没有真机调试根本发现不了。4.1 用户信息获取失败getUserProfile 的版本差异现象开发者工具里点击登录授权弹窗正常昵称头像都能拿到一上真机弹窗不出现或者拿到的是灰色默认头像和“微信用户”这种匿名昵称。原因基础库 2.27.1 之后wx.getUserProfile 的行为被官方调整不再默认拉起授权弹窗老写法在真机上失效。开发者工具默认基础库版本较新时表现也不稳定所以工具里看不出问题真机才露馅。解决别再依赖 getUserProfile改为官方推荐的头像昵称填写能力。头像用一个 button 加 open-typechooseAvatar昵称用一个 input 加 typenickname用户主动填写后把结果 setData 并提交给后端。这比授权弹窗多一步用户操作但稳定性完全在可控范围内。4.2 日期解析异常iOS 不认带空格的日期字符串现象日历页在开发者工具和 Android 上一切正常iPhone 上打开后日期显示 NaN打卡时间变成“NaN:NaN”。原因iOS 的 JavaScript 引擎对 new Date(2025-06-05 08:30:00) 这种带空格的字符串解析失败返回 Invalid Date。它只认“2025/06/05 08:30:00”和 ISO 8601 的“2025-06-05T08:30:00”格式而开发工具用的 Chromium 核心能宽容处理空格格式。解决写自己的 formatDate 函数用 getFullYear、getMonth、getDate 手动拼接不要依赖字符串解析。或者数据层直接存时间戳显示时再做格式化。我一般两种方案结合存储用时间戳展示用拼接好的 yyyy-MM-dd两头都不踩解析坑。4.3 wx:for 状态串位key 不能是 index现象日历翻月后某几天的打卡状态显示错乱比如把上个月的已打卡状态带到了这个月没打卡的日期上。原因wx:for 渲染列表时如果 key 写成 index小程序会把节点按位置复用。数组内容变化后节点复用的是位置而不是数据视觉上就是“状态跟着格子走”而不是“状态跟着日期走”。解决wx:for 的 key 必须用唯一且稳定的标识。日历场景直接用 dateStr打卡记录遍历用记录 id。这个坑很隐蔽因为只在翻月、增删数据时会闪现新手很容易忽略 key 的作用直接把 index 写上去了事。4.4 自定义导航偏移顶部导航栏高度计算现象自定义导航的标题在 iPhone 15 Pro 上贴进状态栏在 iPhone SE 上又明显偏低左右位置和胶囊按钮对不齐。原因不同机型的状态栏高度和胶囊按钮位置不一样代码里写死 64px 或 44px 是典型的拍脑袋方案。状态栏高度、胶囊按钮 top 和 height 都是由机型决定的运行期数据不能硬编码。解决在 app.js 的 onLaunch 里用 wx.getWindowInfo().statusBarHeight 和 wx.getMenuButtonBoundingClientRect() 算好导航栏高度存进 globalData自定义导航组件从 globalData 读取。公式是导航总高 状态栏高度 胶囊按钮高度 上下留白这个值在真机和开发者工具里都可能不同必须动态计算。4.5 数据丢失风险本地缓存不能当正式存储现象用户清掉微信缓存或换手机后所有打卡记录和连续天数全部归零用户以为系统出 bug实际是数据只存在本机 storage 里。原因wx.setStorageSync 是本地存储作用域是一台设备的某个小程序环境跟服务端数据库有本质区别。源码用本地缓存模拟后端方便演示但不代表这个方案能直接上线。解决理解这份源码的定位是“案例源码”。做正式项目时把第 3 章里的 wx.setStorageSync 替换成 wx.request 提交到服务端读取时先请求接口再用本地缓存做降级。数据闭环里服务端永远是主存储storage 只能当加速缓存。5. 参数化改造连续打卡天数、签到时段与分享参数把源码跑通之后下一步是按照自己的业务规则改参数。签到工具的灵魂不在按钮样式而在规则配置能力——连续天数怎么算、允许什么时间段打卡、分享卡片怎么定位到具体日期。5.1 连续打卡天数从今天往回遍历的算法连续天数统计是签到业务里最容易被产品经理追问的字段算法本身不难难在边界定义清楚。这里给出一个从今天往回遍历的实现getContinuousDays(records) { let count 0; const cursor new Date(); // cursor 先归零到当天 0 点避免时间差影响 key 计算 cursor.setHours(0, 0, 0, 0); while (true) { const key formatDate(cursor); if (records[key]) { count; // 重要用 setDate 往前推一天而不是减 24*60*60*1000 cursor.setDate(cursor.getDate() - 1); } else { break; } } return count; }这个算法的时间复杂度是 O(n)n 是连续天数正常用户撑死几百天性能可以忽略。两个细节值得注意一是 cursor 要先归零否则当天 10 点打卡和 23 点打卡算出来的 key 不一致二是往前推一天用 setDate 而不是时间戳减 86400000后者在夏令时地区会翻车中国时区虽然没有夏令时但这个习惯能避免未来部署到海外。产品边界在这里要提前约定如果今天还没打卡但昨天之前连续打了 10 天连续天数算 10 还是 11源码默认是“从今天开始检查”今天没打就从昨天开始数返回 10。如果你想要“必须包含今天”的语义在函数开头加一个判断今天没记录直接返回 0。5.2 签到时段配置把阈值抽成常量打卡签到的时段规则最忌讳散落在业务代码里。我习惯单独建一个 config.js把可调参数全部收敛进去module.exports { // 允许打卡的时间段24 小时制 clockInRange: { start: 08:00, end: 09:30 }, // 是否需要定位true 时打卡页面会调 wx.getLocation needLocation: false, // 连续打卡满多少天触发奖励展示 rewardDays: 30 };时间判断直接用字符串比较就能工作前提是格式恒定补齐两位“08:00”和“09:30”字典序比较结果和真实时间一致。这个技巧省掉了 new Date 的复杂构造但要注意必须 padStart 补齐否则“8:00”和“09:30”比较会得到错误结果。跨天时段是字符串比较的例外比如“23:00”到“02:00”这类规则必须转时间戳判断不能直接比字符串。如果你要支持“早签”和“晚签”两种打卡类型选择器直接用小程序的 radio-group 加 radio 组件就行原生组件比自绘的选中态少维护一个样式文件。改配置后不用动页面逻辑打卡按钮读取 config.clockInRange 即可生效。5.3 分享卡片参数跳转定位到指定日期签到类小程序最常见的获客场景是微信群老师把打卡卡片发到群里学员点开直接进入今天的日历。这个功能靠 onShareAppMessage 的 path 参数实现onShareAppMessage() { const dateStr formatDate(new Date()); return { title: 今天你打卡了吗, // 关键path 上带 query进入页面后 onLoad 能读到 path: /pages/index/index?date${dateStr} }; }path 必须写成完整页面路径加 query 的形式否则点击者进入后拿不到参数。接收端在 onLoad(options) 里读 options.date如果有值就把日历定位到该日期并高亮对应格子没有值就走默认的今天。这个“参数决定页面初始状态”的模式在分享、扫码、推送通知三类入口里都通用。需要留意 onShareAppMessage 的触发位置它属于页面级方法写在 Page() 配置对象里。小程序菜单里的“转发”会调用它但如果页面没定义这个方法分享出去的卡片就是默认标题和路径date 参数自然也不会带上。6. 上线前验证三件事清缓存、换机型、拉通分享链路源码调试到能跑只是第一步我拿到签到类项目之后上线前会强制走三遍验证流程顺序一次都不能乱否则会漏掉最容易复发的问题。6.1 清缓存跑全流程开发者工具里执行“清缓存 → 清除全部缓存”然后重新编译模拟的是用户首次进入的场景。这一步验证登录态判断是否可靠token 没了页面会不会卡在死循环重定向打卡按钮在没有登录的情况下点击会不会报 undefined 错误清缓存之后还能正常完成“登录 → 打卡 → 看统计”全流程说明页面没有偷偷依赖上一次残留的全局变量。我每次改完数据层代码都会先跑这一遍比点十次按钮都有效。6.2 换机型与分享链路验证第二件事是把开发者工具的机型切到 iPhone SE 和 iPhone 15 Pro 各看一遍重点盯自定义导航栏高度。这一步没法在代码里修复什么但能逼你把 app.json 和 globalData 里的硬编码清干净。如果导航高度在两种机型下的表现一致再上真机预览确认。第三件事是分享链路拉通自己在开发者工具里点开右上角菜单转发到文件传输助手从聊天里点卡片进入确认 onLoad 能读到 date 参数。表格里是我常用的验收清单验证项操作通过标准登录链路清除缓存后重新授权能拿到 userInfotoken 正常写入打卡写入时段内点击打卡当天日期生成记录出现成功提示重复打卡再次点击按钮提示“今天已打卡”记录不被覆盖连续统计手工写入前一天记录连续天数在原先基础上 1分享进入从聊天卡片打开onLoad 能取到 date 参数并定位日历导航布局切换两种机型对比标题不贴状态栏不压胶囊按钮这套清单跑完源码才算真正变成“能上线”的工程。原生框架下主包体积相对宽裕如果你后续想迁移到 uniapp 统一多端记得留意 2MB 的主包上限把静态资源扔到 CDN 或分包里。从那以后我每次拿到签到类源码第一件事永远是先清缓存跑全流程再换两台机型对比导航栏最后才谈 UI 调整。顺序反了你很可能会在一个可复现的布局问题上浪费整个下午而真正危险的登录边界反而被漏掉。希望这次拆解能帮你把这个工程一次跑通。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?