简介:面向微信小程序开发者的易打卡签到完整案例源码,涵盖了用户签到、历史记录等典型业务场景,适合需要实战项目参考的入门与进阶开发者。压缩包内共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;登录完成后跳回 index;index 上的记录入口跳 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 和 6,new 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 组件的 type="nickname",头像用 button 的 open-type="chooseAvatar"。另外,换手机号必须走 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-type="chooseAvatar",昵称用一个 input 加 type="nickname",用户主动填写后把结果 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 参数。表格里是我常用的验收清单:
| 验证项 | 操作 | 通过标准 |
|---|---|---|
| 登录链路 | 清除缓存后重新授权 | 能拿到 userInfo,token 正常写入 |
| 打卡写入 | 时段内点击打卡 | 当天日期生成记录,出现成功提示 |
| 重复打卡 | 再次点击按钮 | 提示“今天已打卡”,记录不被覆盖 |
| 连续统计 | 手工写入前一天记录 | 连续天数在原先基础上 +1 |
| 分享进入 | 从聊天卡片打开 | onLoad 能取到 date 参数并定位日历 |
| 导航布局 | 切换两种机型对比 | 标题不贴状态栏,不压胶囊按钮 |
这套清单跑完,源码才算真正变成“能上线”的工程。原生框架下主包体积相对宽裕,如果你后续想迁移到 uniapp 统一多端,记得留意 2MB 的主包上限,把静态资源扔到 CDN 或分包里。
从那以后,我每次拿到签到类源码,第一件事永远是先清缓存跑全流程,再换两台机型对比导航栏,最后才谈 UI 调整。顺序反了,你很可能会在一个可复现的布局问题上浪费整个下午,而真正危险的登录边界反而被漏掉。希望这次拆解能帮你把这个工程一次跑通。
本文还有配套的精品资源,点击获取