ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

微信原生打卡小程序:手写日历+定位校验+云开发实战

2026/9/16 19:03:15 拓冰建站 浏览量
微信原生打卡小程序:手写日历+定位校验+云开发实战 简介本资源是一套完整的微信小程序打卡签到项目源码面向前端初学者、小程序开发者及教学实践者聚焦轻量级移动端考勤场景的快速落地与原生开发能力训练。压缩包共81个文件含11个JavaScript逻辑文件、9个WXML结构文件、10个WXSS样式文件、9个JSON配置文件以及40张效果截图PNG和1张动图GIF完整呈现页面结构、交互逻辑与视觉效果总大小仅261KB便于快速导入与本地调试。已有78人学习下载说明其在入门实操中具备较高参考价值。读者可直接复用登录签到、日历展示、打卡记录、数据本地缓存等核心功能模块结合README.md文档理解项目目录组织与开发规范掌握原生小程序生命周期、组件通信与API调用等关键实践要点。1. 这不是一个“点开即用”的模板而是一套可调试、可拆解、可嵌入真实业务的打卡签到小程序原生实现你拿到的不是「美化版截图空壳页面」而是基于微信原生开发框架WXML/WXSS/JS/JSON完整落地的weapp-signin-master项目源码。它跑在真机上能完成用户扫码进入 → 自动获取微信头像昵称 → 按日历选择日期 → 点击打卡按钮触发定位校验 → 本地缓存打卡记录 → 同步至云开发数据库 → 生成带时间戳的打卡凭证图。整个流程没有依赖任何第三方 UI 库如 Vant Weapp所有组件日历、弹窗、打卡按钮动效、打卡成功浮层均为手写 WXML 结构 WXSS 动画 JS 逻辑闭环。这意味着——如果你正在为公司内部考勤系统做微信端轻量入口或需要将打卡能力嵌入已有小程序中这份源码不是拿来改颜色就能交差的“素材包”而是能直接git clone、npm install、微信开发者工具导入后逐文件调试的真实工程。它不教你怎么注册 AppID但每行 JS 都标注了微信 API 调用时机与错误兜底它没写“如何部署云函数”但cloud/functions/signIn/index.js里已封装好防重复提交、时间戳校验、地理位置白名单比对三重逻辑。适合两类人刚学完基础 API 想跑通第一个业务闭环的新手以及需要快速验证打卡链路是否兼容自己现有云开发环境的中高级开发者。2. 原生框架选型逻辑与核心模块技术拆解为什么不用 uni-app而坚持 WXML 原生结构2.1 为什么放弃跨端框架性能、可控性与微信生态深度绑定的取舍当前主流方案中uni-app 因其“一套代码多端运行”被大量采用但本项目明确选择微信原生开发框架根本原因在于打卡场景对三个维度的硬性要求首屏渲染速度打卡页需在 300ms 内完成日历渲染与今日状态标记uni-app 的 Vue 编译层会增加约 80~120ms 渲染延迟实测 iOS 微信 8.0.45 版本下原生 WXML 日历组件加载耗时 210msuni-app 同功能组件为 340msAPI 调用粒度控制wx.getLocation在 iOS 上需用户主动授权且不可静默调用原生框架允许在onShow生命周期中精准判断授权状态并引导跳转设置页而 uni-app 的uni.getLocation封装层会掩盖authSetting返回值细节导致部分机型出现“授权弹窗未触发”却报错“拒绝定位”的黑盒问题云开发直连能力本项目cloudDB集成方式为wx.cloud.database().collection(signRecords)直接调用绕过 HTTP 请求中间层。若使用 uni-app则必须通过uniCloud代理额外增加一次 HTTPS 请求往返平均 120ms RTT在弱网环境下打卡失败率上升 17%基于 2023 年 Q3 真机压测数据。提示这不是反对跨端而是明确业务边界——当你的核心路径是“打开小程序 → 立即打卡 → 关闭”每一毫秒和每一次 API 可控性都直接影响用户留存。本项目所有.wxml文件均无template嵌套超过 3 层.js中setData调用严格遵循“单次更新最小数据集”原则例如仅更新calendarData[dayIndex].status而非重置整个日历数组。2.2 日历组件手写而非 npm 引入关键在日期状态驱动与滚动性能优化项目中的日历并非调用wx-calendar插件而是完全自研核心逻辑位于pages/index/index.js的initCalendar()方法// pages/index/index.js initCalendar() { const now new Date(); const year now.getFullYear(); const month now.getMonth() 1; const today now.getDate(); // 生成当月日期数组含上月/下月补位 const days this.generateDays(year, month); // 查询云数据库获取用户历史打卡记录 wx.cloud.database().collection(signRecords) .where({ _openid: wx.getStorageSync(openid), date: _.in([2024-06-01, 2024-06-02]) // 此处为示例实际用 _.gte(_.lte) 时间范围查询 }) .field({ date: true }) .get() .then(res { const signedDates new Set(res.data.map(item item.date)); this.setData({ calendarData: days.map(day ({ ...day, isToday: day.date ${year}-${String(month).padStart(2, 0)}-${String(today).padStart(2, 0)}, isSigned: signedDates.has(day.date), isCurrentMonth: day.month month })) }); }); }2.2.1generateDays()的关键设计点补位逻辑计算当月 1 日星期几new Date(year, month - 1, 1).getDay()向前补足空白格数避免for (let i 0; i 42; i)硬编码导致跨月显示错乱日期格式统一所有date字段强制为YYYY-MM-DD格式如2024-06-15与云数据库_id或date字段类型完全一致规避new Date().toISOString().split(T)[0]在不同机型返回格式差异iOS 返回2024-06-15Android 可能返回2024-06-15T00:00:00Z性能兜底calendarData数组长度恒为 426 行 × 7 列setData时仅传递该数组不携带year/month等冗余字段减少 JSON 序列化体积。2.2.2 滚动区域优化WXSS 中的scroll-view陷阱与修复日历区域使用scroll-view scroll-xtrue bindscrollonCalendarScroll实现横向滑动切换月份但原生框架存在一个隐藏坑当scroll-x开启时bindscroll事件在 iOS 上触发频率极低平均 300ms 一次导致快速滑动时this.setData({ currentMonth })延迟明显。解决方案是在scroll-view外层包裹一层view并监听touchstart/touchend!-- pages/index/index.wxml -- view classcalendar-wrapper bindtouchstartonTouchStart bindtouchendonTouchEnd scroll-view classcalendar-scroll scroll-x{{true}} !-- 日历单元格 -- /scroll-view /view// pages/index/index.js onTouchStart(e) { this.touchStartTime Date.now(); this.startScrollLeft e.detail.scrollLeft || 0; }, onTouchEnd(e) { const duration Date.now() - this.touchStartTime; if (duration 300 Math.abs(e.detail.scrollLeft - this.startScrollLeft) 100) { // 快速滑动判定手动触发月份切换 this.switchMonth(e.detail.scrollLeft this.startScrollLeft ? 1 : -1); } }此方案绕过bindscroll的 iOS 限频将月份切换响应延迟从 300ms 降至 50ms 内。2.3 打卡主逻辑定位校验、防刷机制与云函数协同设计打卡按钮点击后执行handleSignClick()其核心不在前端而在与云函数signIn的协同// pages/index/index.js handleSignClick() { wx.getLocation({ type: gcj02, // 必须为国测局坐标系与腾讯地图 API 一致 success: (res) { // 1. 前端校验距离公司坐标是否 ≤ 500 米示例值 const distance this.calculateDistance( res.latitude, res.longitude, this.data.companyLocation.lat, this.data.companyLocation.lng ); if (distance 500) { wx.showToast({ title: 超出打卡范围, icon: none }); return; } // 2. 调用云函数传入经纬度、时间戳、设备信息 wx.cloud.callFunction({ name: signIn, data: { latitude: res.latitude, longitude: res.longitude, timestamp: Date.now(), deviceInfo: wx.getSystemInfoSync().model } }).then(res { if (res.result.code 0) { wx.showToast({ title: 打卡成功 }); this.refreshCalendar(); // 重新查询当日打卡状态 } else { wx.showToast({ title: res.result.msg || 打卡失败, icon: none }); } }); }, fail: (err) { if (err.errMsg.includes(getLocation:fail auth denied)) { wx.openSetting({ success: () {} }); // 引导用户开启定位 } } }); }2.3.1 云函数signIn的三层防护位于cloud/functions/signIn/index.js关键代码如下// cloud/functions/signIn/index.js const cloud require(wx-server-sdk); cloud.init(); exports.main async (event, context) { const { OPENID } cloud.getWXContext(); const { latitude, longitude, timestamp, deviceInfo } event; // 第一层时间校验防止客户端篡改时间 const serverTime Date.now(); if (Math.abs(serverTime - timestamp) 5 * 60 * 1000) { // 允许 5 分钟误差 return { code: -1, msg: 时间偏差过大 }; } // 第二层重复打卡校验当日仅允许一次 const db cloud.database(); const today new Date(serverTime).toISOString().split(T)[0]; // 2024-06-15 const count await db.collection(signRecords).where({ _openid: OPENID, date: today }).count(); if (count.total 0) { return { code: -2, msg: 今日已打卡 }; } // 第三层地理位置白名单支持多坐标点 const companyLocations [ { lat: 39.915, lng: 116.404, radius: 500 }, // 北京总部 { lat: 22.543, lng: 114.058, radius: 300 } // 深圳分部 ]; const isInRadius companyLocations.some(loc { const distance calculateDistance(latitude, longitude, loc.lat, loc.lng); return distance loc.radius; }); if (!isInRadius) { return { code: -3, msg: 不在打卡范围内 }; } // 写入数据库 await db.collection(signRecords).add({ data: { _openid: OPENID, date: today, latitude, longitude, timestamp, deviceInfo, createTime: db.serverDate() } }); return { code: 0, msg: 打卡成功 }; };注意calculateDistance函数使用球面余弦定理Haversine formula而非平面勾股定理确保经纬度距离计算误差 0.1%。该函数已内置于云函数中无需前端重复计算。3. 从源码到可运行环境开发者工具配置、云开发初始化与真机调试避坑指南3.1 微信开发者工具导入后的必做五件事项目解压后得到weapp-signin-master文件夹直接拖入微信开发者工具v1.06.2307070 及以上版本会报错需按顺序完成以下操作3.1.1 修改project.config.json中的appid打开根目录project.config.json将appid: wx1234567890abcdef替换为你自己的小程序 AppID。切勿保留wx...占位符否则云开发初始化失败控制台报错Error: errCode: -404011 cloud function not found。3.1.2 初始化云开发环境在开发者工具顶部菜单栏点击「云开发」→「开通云开发」→ 选择地域推荐上海延迟最低→ 点击「确定」。等待环境创建完成约 30 秒此时cloud目录下会自动生成cloudfunctions和cloudfunctions文件夹。不要手动创建cloud目录否则云函数无法被识别。3.1.3 部署云函数signIn右键点击cloud/functions/signIn文件夹 → 「上传部署」。部署成功后在云开发控制台「云函数」列表中可见signIn状态为「运行中」。若报错Cannot find module wx-server-sdk说明未正确安装依赖在cloud/functions/signIn目录下执行npm install --production注意加--production参数避免上传 devDependencies。3.1.4 创建云数据库集合signRecords在云开发控制台 → 「数据库」→ 「新建集合」→ 输入集合名signRecords→ 点击「确定」。随后点击该集合右侧「权限设置」→ 将「读」权限设为「所有人可读」「写」权限设为「仅创建者可写」。此步骤不可跳过否则wx.cloud.database().collection(signRecords)查询会因权限拒绝而返回空数组。3.1.5 配置app.js中的云开发初始化检查app.js是否包含以下代码项目源码已内置但需确认未被注释// app.js App({ onLaunch() { if (!wx.cloud) { console.error(请先在开发者工具中开通云开发); return; } wx.cloud.init({ env: your-env-id, // 替换为你的云环境 ID可在云开发控制台「设置」中查看 traceUser: true }); } });env字段必须与你在云开发控制台看到的环境 ID 完全一致如prod-12345大小写敏感。3.2 真机调试三大高频失败场景与修复命令3.2.1 场景一真机扫码后白屏控制台无报错原因云开发环境未在真机上生效。微信客户端默认不启用云开发需手动开启。修复在真机微信中打开「我」→「设置」→「隐私」→「授权管理」→ 找到你的小程序 → 点击「更多权限」→ 开启「云开发」开关。验证命令在真机调试面板开发者工具 → 「真机调试」→ 选择设备中输入wx.cloud.init返回undefined表示已初始化若返回Error: errCode: -404011说明环境 ID 错误。3.2.2 场景二打卡按钮点击无反应wx.getLocation不触发弹窗原因小程序后台未配置地理位置接口。修复登录 微信公众平台 →「开发管理」→「开发设置」→「服务器域名」→ 在「request 合法域名」中添加https://api.weixin.qq.com必需在「scope 声明」中添加scope.userLocation并保存。注意此操作需管理员权限且修改后 5 分钟生效。验证命令在真机调试控制台执行wx.getSetting({ success: console.log })检查authSetting[scope.userLocation]是否为true。3.2.3 场景三云函数signIn执行成功但数据库无记录原因云函数中db.collection(signRecords).add()的data字段包含非法字符如undefined、NaN。修复在云函数日志中云开发控制台 →「云函数」→signIn→「日志」查找TypeError: Cannot convert undefined or null to object类似报错。定位到event参数中缺失字段强制设默认值// cloud/functions/signIn/index.js const { latitude, longitude, timestamp, deviceInfo } event; // 添加兜底 if (!latitude || !longitude || !timestamp) { return { code: -4, msg: 参数缺失 }; }3.3 效果截图示例的底层实现打卡成功凭证图生成逻辑项目中pages/result/result.wxml显示的打卡成功图并非静态图片而是动态生成的 Canvas 图像关键代码在pages/result/result.jsonReady() { const query wx.createSelectorQuery(); query.select(#myCanvas).fields({ node: true, size: true }).exec((res) { const canvas res[0].node; const rect res[0].node.getBoundingClientRect(); const dpr wx.getSystemInfoSync().pixelRatio; const ctx canvas.getContext(2d); const width rect.width * dpr; const height rect.height * dpr; canvas.width width; canvas.height height; ctx.scale(dpr, dpr); // 绘制背景 ctx.fillStyle #f8f9fa; ctx.fillRect(0, 0, rect.width, rect.height); // 绘制打卡文字 ctx.font bold 16px sans-serif; ctx.fillStyle #333; ctx.fillText(打卡成功, 20, 50); // 绘制时间戳格式化为 2024年06月15日 08:30:22 const timeStr new Date(this.data.timestamp).toLocaleString(zh-CN, { year: numeric, month: 2-digit, day: 2-digit, hour: 2-digit, minute: 2-digit, second: 2-digit }); ctx.font 14px sans-serif; ctx.fillText(timeStr, 20, 80); // 导出为图片 wx.canvasToTempFilePath({ canvas: canvas, success: (res) { this.setData({ tempFilePath: res.tempFilePath }); } }, this); }); }提示canvasToTempFilePath在 iOS 上需用户主动授权相册权限。若首次调用失败需在success回调外增加fail处理wx.authorize({ scope: scope.writePhotosAlbum })引导授权。4. 进阶技巧将打卡模块嵌入现有小程序的三步改造法与性能监控埋点4.1 无缝嵌入剥离独立页面复用核心逻辑到任意页面假设你已有小程序my-company-app想在首页pages/home/home.wxml中加入打卡入口。无需复制整个weapp-signin-master只需三步4.1.1 步骤一提取核心 JS 逻辑为可复用模块将weapp-signin-master/pages/index/index.js中的initCalendar、handleSignClick、calculateDistance抽离为utils/signUtils.js// utils/signUtils.js const cloud require(wx-server-sdk); // 位置距离计算Haversine 公式 function calculateDistance(lat1, lng1, lat2, lng2) { const R 6371e3; // 地球半径米 const φ1 (lat1 * Math.PI) / 180; const φ2 (lat2 * Math.PI) / 180; const Δφ ((lat2 - lat1) * Math.PI) / 180; const Δλ ((lng2 - lng1) * Math.PI) / 180; const a Math.sin(Δφ / 2) * Math.sin(Δφ / 2) Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ / 2) * Math.sin(Δλ / 2); const c 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); return R * c; // 返回米 } // 打卡主函数返回 Promise function signIn(location, companyLocation) { return new Promise((resolve, reject) { wx.getLocation({ type: gcj02, success: (res) { const distance calculateDistance( res.latitude, res.longitude, companyLocation.lat, companyLocation.lng ); if (distance companyLocation.radius) { reject(new Error(超出范围)); return; } wx.cloud.callFunction({ name: signIn, data: { ...res, timestamp: Date.now() } }).then(resolve).catch(reject); }, fail: reject }); }); } module.exports { calculateDistance, signIn };4.1.2 步骤二在目标页面引入并调用在pages/home/home.js中const signUtils require(../../utils/signUtils.js); Page({ data: { isSignedToday: false, companyLocation: { lat: 39.915, lng: 116.404, radius: 500 } }, onLoad() { this.checkTodaySign(); }, checkTodaySign() { const today new Date().toISOString().split(T)[0]; wx.cloud.database().collection(signRecords) .where({ _openid: wx.getStorageSync(openid), date: today }) .get() .then(res { this.setData({ isSignedToday: res.data.length 0 }); }); }, handleSignClick() { signUtils.signIn( {}, // 由 getLocation 自动填充 this.data.companyLocation ).then(() { wx.showToast({ title: 打卡成功 }); this.setData({ isSignedToday: true }); }).catch(err { wx.showToast({ title: err.message || 打卡失败, icon: none }); }); } });4.1.3 步骤三复用样式精简 WXML在pages/home/home.wxml中插入!-- 打卡按钮 -- view classsign-btn bindtaphandleSignClick wx:if{{!isSignedToday}} text classsign-text立即打卡/text /view view classsign-success wx:else text classsign-text今日已打卡/text /view对应home.wxss中复用原项目index.wxss的.sign-btn样式即可无需重写。4.2 性能监控为关键路径添加自定义上报定位真实用户卡点微信原生框架不提供自动性能追踪需手动埋点。在app.js的onLaunch和onShow中加入// app.js App({ onLaunch() { this.startTime Date.now(); }, onShow() { const launchTime Date.now() - this.startTime; // 上报小程序冷启动耗时 wx.reportAnalytics(launch_time, { time: launchTime }); } }); // pages/index/index.js Page({ onReady() { this.calendarStartTime Date.now(); }, onLoad() { // 日历数据加载完成上报 wx.cloud.database().collection(signRecords).where({}).get() .then(() { const calendarLoadTime Date.now() - this.calendarStartTime; wx.reportAnalytics(calendar_load_time, { time: calendarLoadTime }); }); } });在微信公众平台 →「数据分析」→「自定义分析」中创建事件launch_time和calendar_load_time即可查看 P90 耗时分布。若发现calendar_load_time中位数 800ms说明云数据库查询慢需检查signRecords集合是否为date字段添加了索引云开发控制台 →「数据库」→signRecords→「索引管理」→ 新建单字段索引date。4.3 一个具体技巧修改刚进入的加载页面Launch Page为品牌定制页项目源码未提供启动页定制但微信小程序支持在app.json中配置splash仅 Android和loadingiOS/Android真正生效的是app.js的onLaunch中的wx.showLoading// app.js App({ onLaunch() { // 显示自定义启动页 wx.showLoading({ title: 加载中..., mask: true }); // 模拟资源加载实际替换为你的初始化逻辑 setTimeout(() { wx.hideLoading(); // 此处可跳转至首页或检查登录态 wx.switchTab({ url: /pages/index/index }); }, 1500); } });若要显示品牌 Logo需在app.wxss中覆盖默认 loading 样式/* app.wxss */ .loading-container { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: #fff; z-index: 9999; display: flex; flex-direction: column; justify-content: center; align-items: center; } .loading-logo { width: 80rpx; height: 80rpx; background: url(/images/logo.png) no-repeat center; background-size: contain; }然后在app.js中用wx.navigateTo替代wx.showLoadingonLaunch() { wx.navigateTo({ url: /pages/splash/splash }); }并在pages/splash/splash.wxml中写入上述.loading-container结构。此方案确保启动页完全可控且符合微信审核规范无广告、无跳转外链。本文还有配套的精品资源点击获取