微信小程序计分源码实战:解压导入、数据模型与本地存储全解析

📅 发布时间:2026/9/17 3:02:37
微信小程序计分源码实战:解压导入、数据模型与本地存储全解析
简介这是一份用于牌桌娱乐场景的计分小程序源码主要面向初学小程序开发或需要快速实现多人计分功能的开发者解决牌局中分数记录与统计不便的问题。压缩包共45个文件、约41KB核心代码以TypeScript、SCSS和TSX为主并搭配若干JavaScript、JSON配置及SVG图标资源整体结构轻量便于直接阅读和改造。项目基于poker-score-record-applet-main目录组织涵盖小程序前端页面、样式配置和基础工具模块适合对照学习小程序开发中的数据结构设计、计分逻辑和事件处理。已有1418人浏览学习资源虽小但工程化痕迹明显包含ESLint、EditorConfig和TypeScript配置能帮助初学者理解一个规范的小程序项目从编码到构建的完整骨架。从中可以掌握牌桌计分场景下的数据存储方式、界面交互写法以及基本的异常处理思路动手实践后即可迁移到其他棋牌或聚会场景中。1. 用户牌桌娱乐的计分小程序源码拿到 zip 后先做这三件事周末约局打麻将、打扑克最扫兴的往往不是手气差而是记分的人记到一半忘了上一把谁赢谁输结算时全凭印象一句“你是不是少算我十块”就能让气氛僵住。这套牌桌娱乐的计分小程序源码解决的就是这个场景不需要服务器、打开就能记分、关掉再打开数据还在的微信小程序。它更接近工具类应用和微信小程序游戏开发里那套 Canvas、物理引擎不沾边核心只有三件事——加减分、多局累计、历史记录。网上这类源码版本不少能不能落地取决于动手前有没有想清楚“一局怎么算分、整场怎么汇总、误触了怎么撤销”。太多人把 zip 下载下来就导入开发者工具跑起来才发现计分对不上回头改数据模型比重写还难受。下面按“验包解压 → 数据模型 → 导入跑通 → 交互存储 → 改造打包”的顺序走每一步都能照做zip 环节的坑单独拎出来说。适合刚接触微信小程序项目实例的开发者也适合拿到源码想改规则、想发给牌友用的熟手。2. 计分小程序的数据模型与核心算法牌局、玩家和流水怎么设计拿到源码别急着看页面先把数据层读明白。牌桌计分代码里最核心的约束是“零和”一局结束赢家得分必须等于所有输家失分之和整局分数变化合计为 0。死磕这一条后面无论是算总分、做历史记录还是核对账单都不会出现对不上的情况。2.1 先分清两类场景单局结算与整场累计第一类是按局结算界面展示当前这一把的结果打完即走适合临时凑一桌的场合。第二类是整场累计记录每一局的分数变化量也就是 delta最后按总分结账适合一下午连着打的局面。我一般建议源码里只存 delta不直接存总分。理由是总分是派生值从 delta 序列重算一定能得到一致结果如果同时维护“总分”字段任何一次误触、撤销、回退都会让两个值产生偏差排查起来非常痛苦。常见的计分小程序源码也基本遵循这个思路每条流水记录是一个{playerId, delta}的二元组而不是{playerId, total}。另外要注意一个边界中途有人离席或者换人。流水只认 playerId 的话离席的人回来继续打累计自然带上换人就开新 session不要混在同一个 session 里。很多模板源码在“换人”这件事上直接清空分数这是最省事的做法但代价是整场历史没了牌友如果吵着要查三小时前那把怎么算的就无从谈起。2.2 三张核心表session、player、round_record以本地存储为主的计分小程序没有数据库但设计上仍然要按“表”来思考哪怕实际存的是 wx.Storage 里 JSON 序列化后的数组。下面是三份最主要的数据结构。集合关键字段说明sessionsessionId, name, ruleType, baseScore, status一场牌局元信息status 用 playing / finished 标记playerplayerId, sessionId, displayName, seatNo玩家身份与座位号分离seatNo 只影响展示顺序roundRecordrecordId, sessionId, roundNo, deltas, createdAtdeltas 存序列化后的 [{playerId, delta}] 数组先说 playerId。不要用 1、2、3 这种整数下标当玩家 id原因是座位一换、一重排分数就挂错了人。常见做法是Date.now()加两位随机数拼一个字符串生成一次就不再变。seatNo 只负责“现在坐在第几位”排序 UI 调整时改动 seatNo 不影响分数归属。再说 deltas 字段。一局结束后把这一局所有人的变化量打包成一个数组存进一条记录比“每次写多条独立流水”的存储方式省很多 key读取历史时也只需要一次 getStorageSync。代价是查询单个人所有局的对账逻辑要自己写但这种体量的项目完全可以接受。baseScore 放在 session 而不是写死在前端常量里这一点直接决定后面改造的难度。底分会随玩法、随牌友约定变化放在页面里每次都要改代码重新发版放在 session 里则可以做到一局一设。2.3 计分函数把规则写成纯函数再接到按钮上界面上的每一个加号、减号、倍率按钮最后都调用同一个函数算出这一局的 delta 数组。函数不碰任何页面数据输入玩家列表和规则参数输出流水这样既方便单测也方便以后扩展番型、加倍之类的规则。// utils/scoreEngine.js /** * 计算单局各玩家的分数变化量保证整局总和为 0 * param {Array} players 玩家数组元素形如 { id, name } * param {Number} winnerIndex 赢家在数组中的下标 * param {Number} baseScore 底分 * param {Number} multiplier 倍率1 表示平胡2/4 表示翻倍 * returns {Array} [{ playerId, delta }] */ function calcRound(players, winnerIndex, baseScore, multiplier) { const perLose baseScore * multiplier; // 先把所有输家的扣分加起来作为赢家的得分 const loseSum perLose * (players.length - 1); return players.map((p, i) ({ playerId: p.id, delta: i winnerIndex ? loseSum : -perLose, })); } /** * 把多局流水汇总成每个玩家的总分 * param {Array} rounds [{ deltas: [{ playerId, delta }] }] * returns {Object} { [playerId]: total } */ function sumScores(rounds) { const total {}; for (const r of rounds) { for (const d of r.deltas) { total[d.playerId] (total[d.playerId] || 0) d.delta; } } return total; }calcRound 里先算每名输家扣多少再把“所有输家扣分之和”赋给赢家这样一局的 delta 加总必然是 0。multiplier 参数由调用方传入按钮上一般只暴露 1、2、4、8 这几个档位具体哪些档位可用由玩法配置决定。players 数组长度由页面保证函数内部不依赖下标做任何持久化操作纯粹计算。sumScores 遍历所有 roundRecord 的 deltas按 playerId 累加。返回的对象以 playerId 为 key渲染时再映射回玩家名。这里刻意不用数组下标参与累计因为一旦玩家排序变化、或者中途插进来一个人按位置的累计立刻错位。2.4 一个高频误用在按钮事件里直接改界面数值很多简化版源码是这么写的tap 一个加号按钮直接this.data.scores[i] 1然后 setData 刷新页面。短期看没问题一旦涉及撤销、回退、换座界面上的数值就会和真实流水脱节。撤销不是把值减回去而是删掉上一条 roundRecord 再重新汇总一遍。所以按钮事件的职责只有两件事构造一条 delta 记录追加到当前 session 的记录列表里。显示值永远从汇总函数算出来而不是被点击次数一次次顶上去。这个习惯养成了后面做历史回放、做账单导出都是顺手的事。3. 解压与导入微信开发者工具跑通计分小程序源码 zip 的完整流程标题里的 .zip 不是装饰。微信开发者工具导入的是项目目录不是压缩包本身所以第一关永远是安全解压。系统自带的 unzip 够用图形界面用户用 7-Zip 即可。3.1 解压前先验包EOCD、编码和密码三个坑下载 zip 最常见的失败形态是文件不完整。zip 的目录结构叫中央目录其中最关键的一段 EOCDEnd of Central Directory记录放在文件末尾下载断在这个区间之前解压工具就会报invalid zip archive: could not find eocd。这类报错优先重新下载不要急着怀疑压缩包本身损坏。拿到压缩包先做完整性检查再解压。下面这几条命令分别验证结构、加密方式和编码。# 完整性检测exit code 为 0 才说明文件结构完整 unzip -t score-miniapp.zip # 查看条目加密状态与压缩算法 zipinfo -v score-miniapp.zip | grep -E encryption|compression method # 识别文件类型粗略判断打包平台 file score-miniapp.zipunzip -t 会逐条校验每个文件条目的 CRC输出末尾有No errors detected in compressed data才算通过。如果出现missing end signature之类的提示对应就是 EOCD 缺失或损坏。zipinfo -v 的输出里能看到条目是否加密、用什么算法压缩一般来说只需要确认没有 password required 字样。遇到带密码的 zip正确做法是找交付方要密码而不是第一时间去搜 zip 压缩包密码破解工具。暴力破解对短密码还有可能对长密码基本是无底洞而且很多源码 zip 的密码只是打包人随手设的问一句比跑几个小时省事。编码问题更隐蔽Windows 下用 WinRAR 打包的中文文件名默认 GBK在 macOS 上解压可能变成乱码影响的是目录名而不是代码内容先解到临时目录里核对结构。3.2 目录结构识别app.json 的 pages 数组是第一道关卡解压后先不要急着双击 project.config.json 导入先看一眼目录。一份标准原生微信小程序工程的结构大致如下。score-miniapp/ ├── project.config.json # 开发者工具的项目配置包含 appid 字段 ├── app.js # 小程序逻辑入口 ├── app.json # 全局配置pages、window、tabBar ├── app.wxss # 全局样式 └── pages/ ├── index/ # 计分主页面 │ ├── index.js │ ├── index.json │ ├── index.wxml │ └── index.wxss └── history/ # 历史记录页 ├── history.js └── history.wxmlapp.json 里最值得先读的是 pages 数组它的第一项决定了小程序启动时进入哪个页面。拿到源码先把这个数组和 pages/ 目录对一遍能提前发现路径写错、文件缺失这类导入后才会炸的问题。project.config.json 里的 appid 字段如果是空字符串或明显的占位符导入时会让你重新选择这不影响本地跑通。用 uniapp 开发的人可能想把这套源码改造成 uni-app 工程可以改但没必要。原生小程序源码导入开发者工具直接编译是最短路径真要跨端再考虑用 uni-app 重写页面数据模型和计分函数是完全能照搬的。3.3 导入微信开发者工具AppID 选测试号还是正式号打开微信开发者工具选“导入项目”目录指向解压后包含 app.json 的那一层注意不要多选一层。AppID 这一步有两个选项要分清测试号不需要注册小程序账号就能用本地预览、清缓存、编译调试都正常但没有发布权限正式 AppID 从小程序管理后台获取用于上传代码、真机预览、申请发布。日常开发流程建议先测试号跑通逻辑最后要上线了再切换到正式 AppID。导入完成后第一次编译如果报错多半不是项目本身的问题而是基础库版本不匹配。调试器面板里把调试基础库切到较高版本比如 3.x能减少因为 API 废弃导致的编译失败。计分小程序用到的主要 API 都很稳定但模板源码可能用了一些老接口切换基础库是最快的验证方法。3.4 导入期常见报错对照表把导入阶段出现频率最高的几个问题整理如下遇到哪条直接按对应方式处理。报错 / 现象常见原因处理方式invalid zip archive: could not find eocdzip 下载或传输被截断重新下载再用 unzip -t 验证pages/index/index 未找到app.json 的 pages 路径与目录不符对齐 pages 数组里的路径大小写与层级中文注释或文件名乱码源包用 GBK 编码用 VS Code 重新打开文件并另存为 UTF-8project.config.json 的 appid 为空交付方移除或未填写导入时选择测试号或自行填入 AppID编译显示 sitemap 索引失败app.json 里缺少 sitemapLocation检查根目录是否有 sitemap.json缺失则补上路径大小写是这批报错里最坑的。macOS 默认文件系统大小写不敏感Windows 也不敏感但开发者工具的编译器和线上服务器按精确匹配处理所以 pages 数组里的路径必须和实际目录逐字符一致。中文文件名解出来如果变成了乱码目录直接手动重命名目录再同步改 pages 数组。4. 计分面板、动态标题与本地存储小程序交互层从注册到调通数据模型定清楚以后交互层就只剩三件事把计分函数接到按钮上把牌局名写进头部标题把每一局流水写进本地缓存。这三件事分别对应页面模板、导航栏 API、存储 API做完了这套小程序的核心体验就闭环了。4.1 计分面板用 wx:for 生成事件参数从 dataset 读取玩家列表用 wx:for 渲染每个玩家一行左右各一个加减按钮中间是累计分数。按钮的事件处理函数不需要绑定每个玩家的方法统一走一个 onDelta用>view classscore-row wx:for{{players}} wx:keyid text classname{{item.name}}/text button sizemini>// pages/index/index.js onDelta(e) { const { index, delta } e.currentTarget.dataset; const player this.data.players[index]; if (!player) return; // 只追加一条流水记录界面数值由汇总函数统一重算 this.appendRound({ playerId: player.id, delta: Number(delta), ts: Date.now(), }); }dataset 是 WXML 里>// pages/index/index.js applyTitle() { wx.setNavigationBarTitle({ title: 牌局${this.data.sessionName}, }); }调用时机有两个一个是 onLoad 恢复上一场牌局之后另一个是用户输入牌局名并确认之后。标题对超长文本会做截断牌局名输入框最好限制最大长度比如 20 个汉字避免标题显示成残缺的半句话。页面卸载回到上一页时导航栏标题会自动恢复为页面 json 里配置的默认值不需要手动还原。这套 API 在 iOS 和 Android 上的表现基本一致真机上字体渲染略窄字符串长度判断要比预期的保守一些。4.3 本地持久化wx.setStorageSync 的 key 设计与写入时机小程序本地缓存的单条 key 上限 1MB总上限 10MB。一份计分流水按条算也就几 KB完全够用重点是 key 怎么规划。我一般用两个 keyLAST_SESSION存当前牌局摘要ROUND_${sessionId}存这个牌局的流水列表sessionId 用玩家创建牌局时生成的时间戳字符串。// pages/index/index.js appendRound(round) { const key ROUND_${this.data.sessionId}; const rounds wx.getStorageSync(key) || []; rounds.push(round); wx.setStorageSync(key, rounds); wx.setStorageSync(LAST_SESSION, { sessionId: this.data.sessionId, name: this.data.sessionName, players: this.data.players, status: playing, }); }写入时机是每局结束后立即写而不是退出页面时统一写。小程序在用户切后台后可能被微信回收等到 onHide 再写一旦回收发生在写入前这一局数据就没了。读取时 wx.getStorageSync 在 key 不存在时返回空字符串而不是空数组所以统一用|| []兜底。历史记录页读取流水时只按 session 的 key 前缀遍历渲染时把 delta 按 playerId 映射回玩家名列表。4.4 修改刚进入的加载页面onLoad 恢复上一场牌局模板源码的 onLoad 往往只是初始化空玩家数组每次启动都是全新牌局。对牌桌场景来说更实用的行为是冷启动后直接恢复上一场还没结束的牌局省掉重新录人的步骤。onLoad() { const last wx.getStorageSync(LAST_SESSION); if (last last.status playing) { const rounds wx.getStorageSync(ROUND_${last.sessionId}) || []; this.setData({ sessionId: last.sessionId, sessionName: last.name, players: last.players, displayScores: this.sumScores(rounds), }); this.applyTitle(); } }这里判断 status 是否为 playing 是有意为之。结算时把 LAST_SESSION 的 status 改成 finished下次启动就不会把打完的牌局重新拉起来。displayScores 是从流水重新汇总的不信任任何缓存里的总分快照这样即使上一版代码算错过新版本启动后也会自动纠正。第一次进入没有任何牌局时提供一个“快速开局”的默认界面预置四个空玩家位就够了。5. 改造规则、真机验证与重新打包把这份源码变成自己的小程序计分逻辑跑通只是开始把这套源码变成能发给牌友的东西还需要做参数化改造、完整验证和重新打包。5.1 把规则参数化用 radio-group 切换玩法底分和倍率档位不要散落在页面各个按钮里。常见做法是独立一个 config 文件把每种玩法的参数集中放。// config/rules.js const RULES { mahjong: { label: 麻将, baseScore: 10, multipliers: [1, 2, 4, 8] }, doudizhu: { label: 斗地主, baseScore: 5, multipliers: [1, 2] }, general: { label: 通用, baseScore: 1, multipliers: [1] }, }; module.exports { RULES };倍率按钮根据当前规则配置循环生成规则选择用小程序单选框 radio-group 放在设置页里。以后想改规则只动这个文件不用翻页面代码。这类定制需求如果走外包报价表里通常按“规则数”计价而不是按页面数就是因为参数集中管理之后加一种玩法只是加一段配置。5.2 重新打包 zip排除多余文件并复验 EOCD改完之后重新交付给牌友压缩包内容和从网上拉下来的原包不能一样。原生微信小程序工程里 node_modules、.git、sourcemap 都不该出现在交付包里它们既增大体积又容易被误判为附带其他内容。zip -r score-miniapp-src-release.zip . \ -x node_modules/* .git/* .* *.map # 交付前唯一一道硬性检查 unzip -t score-miniapp-src-release.zip echo zip ok-x排除的规则要写在打包命令里而不是打包后手动删除否则漏掉一个隐藏文件就会把不必要的内容带出去。复验命令 exit code 为 0 才说明 EOCD 完整这一步和用户侧拿到包后第一反应做的事情完全一样。5.3 交付前的五连验证按下面这个顺序跑一遍比对着功能清单逐项点击更能发现问题。清缓存冷启动确认自动恢复的是未结算牌局而不是空页面。连续记 10 局结算页核对总分合计为 0。记完三局立刻杀掉微信进程重新进入确认流水还在。同一台手机换一个微信号进入确认本地缓存仍然能读到不能出现“换号就清空”。结算后重进确认 finished 状态的牌局不被恢复。第 4 条要特别说明微信小程序的本地缓存绑定的是小程序本身和设备不随微信号隔离同一台手机换账号依旧能读到上一份数据。如果这套小程序要在牌友之间流转设置页必须提供“结束牌局并清空数据”的入口否则下一个打牌的人会看到上一场的数据。把上面三条命令存成一个 check.sh每次改完配置先跑一遍再打包。对方拿到 zip 的第一动作大概率就是 unzip -t你这边先行自检就避免了“包发过去打不开”的来回扯皮。本文还有配套的精品资源点击获取