尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
成语小秀才微信小程序源码拆解:答题闯关与题库设计实践
简介这是以成语接龙为主题的微信小程序完整源码面向对小程序开发感兴趣、希望学习游戏化设计与广告变现的开发者。项目实现了无限关卡玩法、横幅/激励视频/插屏广告集成、分享好友获能力等机制可直观看到前端交互与后端接口配合方式。压缩包共九千九百二十八个文件约六十二点九四兆以js逻辑脚本、php服务端接口、png与svg图标素材、css/less样式以及wxml/wxss页面结构为主目录清晰便于按功能模块查阅。目前已有一千五百一十五人学习下载。通过阅读源码可掌握微信开发者工具操作、关卡数据结构设计、广告组件触发时机及社交裂变实现思路同时学习版本管理习惯适合刚入门小程序或想拓展游戏类项目的开发者参考。1. 成语小秀才微信小程序源码能跑起来的不只是答题页拿到这份「成语小秀才微信小程序源码」你最先想做的事估计和我一样把它丢进微信开发者工具点开编译看看成语题库和闯关页面长什么样。这套源码的核心是一条完整的答题闯关闭环——首页选关、成语填空、提示求救、倒计时奖惩、金币累计中间还夹着含 800 多组成语的数据文件足够撑起一个能上架的小体量应用。对刚从小游戏、web 页面转过来的开发者来说它的价值在于把题库结构、页面状态管理和闯关判定都做成了可以直接拆的范式如果你手头正缺一个带完整数据层的小程序练手项目或者想拿成语题库做二次开发这份源码可以在一个晚上内帮你跑通全流程适合当独立小程序开发的起步骨架也适合做教学演示用的最小可运行样例。2. 把项目拆开看源码结构、题库文件与关卡配置2.1 目录结构哪些文件决定了小程序能编译运行微信小程序原生项目打开后第一眼看到的是 app.js / app.json / app.wxss 这老三样接着是 pages 目录、utils 目录以及一份独立存放的成语题库数据文件。这份源码的结构不算复杂但分层比一般 demo 做得清楚app.json 里注册了两个以上页面常见的是首页index和答题页quiz部分版本还会有排行页或设置页pages/index 负责关卡选择和金币展示pages/quiz 承担答题交互utils 目录放公共方法比如随机抽题、洗牌、金币计算这类纯函数题库数据通常以 .js 或 .json 形式放在根目录或 data 目录成语记录每一条包含成语本身、拼音、解释和示例句。打开 app.json你会看到类似这样的页面注册配置{ pages: [ pages/index/index, pages/quiz/quiz ], window: { navigationBarTitleText: 成语闯关, navigationBarBackgroundColor: #F5E6C8, navigationBarTextStyle: black }, style: v2 }这段配置决定了编译后哪些页面可以被路由到其中第一项 pages/index/index 会作为首页加载。navigationBarTitleText 是顶部导航标题和页面上实际展示的标题是两个地方很多新手改了半天标题发现没生效就是改错了层级。navigationBarBackgroundColor 用古风米黄色契合主题但如果你打算发布正式版本建议把颜色统一到品牌主色否则安卓和 iOS 两端顶部栏色差会比较明显。再往下看 utils 里通常有一个工具文件导出公共函数答题页通过 require 引用。值得留意的是数据文件如果在根目录命名为 js 格式它导出的通常是一个数组或对象const idiomList [ { word: 一心一意, pinyin: yī xīn yī yì, explanation: 形容做事专心一意没有杂念。, example: 他学习时总是十分专一一心一意地看书。 } ]; module.exports { idiomList };这种 js 格式的数据文件比 json 格式多一个好处可以在文件内部写注释也可以按需把数据拆分导出在微信开发者工具里调试时不会遇到 json 文件的注释报错问题。但注意如果后续数据量涨到几千条推荐改用独立的 JSON 文件加载因为 js 文件里一次性把全部成语打进代码包会明显增加主包体积影响小程序的包体审核。2.2 题库数据设计字段越全二次开发越省心成语题库每条数据的字段设计直接决定了答题页、排行榜、每日挑战这些功能好不好扩展。这份源码里每条成语记录包含 word、pinyin、explanation、example 四个字段属于最实用的最小集。如果你计划做更复杂的玩法建议至少再补两个字段一个 difficulty 标记难度一个 category 标记主题比如动物、数字、自然、人物。先看不补字段时候的抽题逻辑长什么样function pickQuestions(idiomList, count) { // 把原数组复制一份避免抽题过程改动原始数据 let pool idiomList.slice(); let picked []; for (let i 0; i count pool.length 0; i) { let randomIndex Math.floor(Math.random() * pool.length); picked.push(pool.splice(randomIndex, 1)[0]); } return picked; }这里用 Math.random() 取整生成下标核心是每次抽完就把该条从 pool 中移除以此来保证同一次闯关不会抽到重复成语。math 对象在微信小程序里可以直接用不需要额外引入依赖但在真机上 Math.random() 的随机性表现一般如果你要做抽卡或每日随机挑战这类对随机度要求更高的功能可以考虑把随机种子和本地时间戳结合生成更分散的序列常见做法是在取下标时这样处理let seed Date.now() % 1000; let randomIndex Math.floor(((Math.random() * seed) % pool.length) Math.random() * pool.length * 0.5) % pool.length;这段做法谈不上最优但能有效避免连续几次抽到相邻位置的成语避免用户很快背出题库顺序。真正决定上手体验的其实是题库质量——源码自带的 800 组成语里有不少是让人一眼能答出答案的常用成语这类题目适合前五关到后面关卡需要更生僻的成语拉高区分度就需要你自己扩充了。扩库时最重要的一条红线是不要把拼音和汉字写错一个多音字标错就能让整个判断逻辑失效用户在页面上怎么点都判定错误。2.3 关卡与金币配置从入门到调整参数源码里关卡难度的梯度是靠题库中成语的排列顺序和题目抽取范围控制的。常见做法是给每个成语加一个 mark 标记前 100 条属于第一档难度100 到 300 属于第二档以此类推。与难度贴得最近的是金币奖励参数每次过关在源码里会给 100 金币作为基础奖励答错扣 10 金币连续答对三题以上有加成。金币计算逻辑一般写在过关回调里例function calculateReward(base, streak, isPerfect) { let reward base; if (streak 3) { reward Math.floor(streak * 10); } if (isPerfect streak 5) { reward 50; } return reward; }参数计算逻辑简单但效果直接基础奖励 base 控制经济系统整体水位streak 连胜加成鼓励用户连续作答isPerfect 全对奖励则给了高手额外成就感。我一般会把 base 设为 100连胜加成 10 金币全对再有 50 金币奖励这个参数组合在用户留存上比较温和不至于让金币通货膨胀过猛。要注意金币数据存储在本地缓存里使用 wx.setStorageSync(coins, totalCoins) 同步写入如果你改成异步的 wx.setStorage会导致连续多次写入时出现覆盖时序问题后写的把它前边的值覆盖掉。3. 闯关核心逻辑从渲染到判定的完整链路3.1 答题页的界面与数据绑定答题页是这份源码里信息密度最高的文件它基本上是一个问答交互页面上方显示待填成语的拼音提示和空白格子中间位置列出若干个候选汉字用户按顺序点击汉字填入空格选错可以点击已填汉字将其退回到候选区全部填对后自动进入下一题。页面的 wxml 结构大致是这样的view classquestion-card view classpinyin{{ current.pinyin }}/view view classword-slots view wx:for{{ current.answerArray }} wx:keyindex classslot {{ item.filled ? filled : }} bindtaponSlotTap >startQuestion(question) { let wordArray question.word.split(); // 每个空格初始显示为空字符串 let answerArray wordArray.map(char ({ char: , target: char, filled: false })); // 构造候选字面板把当前成语的每个字拆开放入 let candidateChars wordArray.map(char ({ char: char, used: false })); this.setData({ current: question, answerArray: answerArray, candidateChars: candidateChars, selectedIndex: 0 }); }初始化方法里最关键的一步是给 answerArray 的每一项保留 target 字段target 存的是这个位置本来应该填入的正确汉字而 render 时只渲染 char。这样判定对错就不需要额外查题库直接把用户填入的字符和该位置的 target 对比即可省去了一次遍历查找。selectedIndex 记录当前应该填入第几个格子相当于一个指向下一个空位的指针用户按顺序点击汉字时不需要自己先点空格再选字这个设计大大降低了操作成本但从另一面看也带来一个问题只允许顺序填字用户想从第二个字开始填就做不到了对习惯“按字面顺序答题”的人来说很自然但对先看首尾猜词的用户可能不太顺手。3.2 点击判定与输入状态的边界处理答题的核心事件有两个onCharTap 负责点击候选字onSlotTap 负责点击已填字的格子将其退回候选区。这两个事件一个正向一个反向逻辑处理上最容易出 bug 的是状态同步。先看正向流程的代码onCharTap(e) { let char e.currentTarget.dataset.char; let index e.currentTarget.dataset.index; let candidateChars this.data.candidateChars; // 如果该字已经被使用直接忽略本次点击 if (candidateChars[index].used) { return; } let answerArray this.data.answerArray; let selectedIndex this.data.selectedIndex; // 所有空格都已填满时不再接受新字 if (selectedIndex answerArray.length) { return; } answerArray[selectedIndex].char char; answerArray[selectedIndex].filled true; candidateChars[index].used true; this.setData({ answerArray: answerArray, candidateChars: candidateChars, selectedIndex: selectedIndex 1 }); // 全部填满后自动触发判定 if (selectedIndex 1 answerArray.length) { this.checkAnswer(); } }这段逻辑里有三个必须守住的状态边界第一已经 used 的汉字不能再次被点击否则同一个字会被填进两个格子第二选字指针 selectedIndex 不能越过答案数组末尾否则继续点击会导致数组下标越界渲染层报错第三当最后一个格子刚被填满在 setData 之后立即调用 checkAnswer能保证用户在视觉上看到完整成语后再进入对错反馈。反向操作 onSlotTap 同样有对应的边界条件onSlotTap(e) { let slotIndex e.currentTarget.dataset.index; let answerArray this.data.answerArray; // 只有已填充的格子才能被点击退回 if (!answerArray[slotIndex].filled) { return; } let letter answerArray[slotIndex].char; let candidateChars this.data.candidateChars; // 从右侧尚未使用的位置开始查找找到与退回字相同的候选字 for (let i 0; i candidateChars.length; i) { if (candidateChars[i].char letter candidateChars[i].used) { candidateChars[i].used false; break; } } answerArray[slotIndex].char ; answerArray[slotIndex].filled false; // 只要退回的格子位置比当前指针靠前指针要回退 let newSelectedIndex Math.min(slotIndex, this.data.selectedIndex - 1); this.setData({ answerArray: answerArray, candidateChars: candidateChars, selectedIndex: newSelectedIndex }); }这里比较值得说的一行是 newSelectedIndex 的计算它取 Math.min(slotIndex, this.data.selectedIndex - 1)。仔细想一下用户点击候选字填字时指针是往后走的一般只会点击最后一个已填充的格子来做“后悔药”这时 slotIndex 等于 selectedIndex - 1回退后指针刚好等于原位置但用户也可能在填了四个字后发现第二个字填错了直接点了第二格把字退回此时指针已经指向 4如果不做处理下一次点候选字会直接填到第五个不存在的格子上去。Math.min 的作用就是确保无论退回哪一个位置指针最多回退一格并且永远指向一个有效区间。这种指针与格子状态联动的写法在类似的填字小程序里属于最稳的方案。3.3 判定、过关与本地记录checkAnswer 是闯关闭环的收口函数它拿 answerArray 里每个格子实际填的字符与对应位置的 target 逐一比对。全部一致时触发过关逻辑弹出一个自定义动画层或消息提示然后进入下一题不一致时不直接判定失败而是给出一个“再想想”的提示把填错的字标红并扣除少量金币。判定函数参考写法checkAnswer() { let answerArray this.data.answerArray; let correct answerArray.every(item item.char item.target); if (correct) { this.handlePass(); } else { // 错的格子置为 error 状态呈现红色样式 let updated answerArray.map(item { if (item.char ! item.target item.filled) { return { ...item, error: true }; } return item; }); this.setData({ answerArray: updated }); wx.showToast({ title: 再想想, icon: none }); } }这里用 Array.every 做全量比对比 for 循环简洁且可读性高在 4 个字的规模上性能差距可以忽略。错字标记 error 状态后下一次点击候选字时应该把目标位置的 error 清除否则视觉上会一直保留红色残影在填写新字时清除上一个错误标记是很多没做过此类交互的人容易漏掉的状态清理。过关后的 handlePass 里会做三件事累计金币、更新关卡进度、判断是否全关通关。关卡进度我一般用 wx.setStorageSync(unlockedLevel, level 1) 保存这样用户退出小程序再进来能直接接着上次的关卡继续。这里如果你用的是页面的 data 里的全局变量来记录进度那小程序一关掉数据就清零用户会觉得自己没玩过体验损失很大。3.4 音频与动效提示系统背后的联动细节题库源码里除了答题逻辑还集成了两个辅助系统一是提示道具二是答对答错时的音频反馈。提示道具的实现是拿到当前成语的第一个字作为免费提示再往后用提示需要扣金币音频反馈则通过 wx.createInnerAudioContext 完成。创建音频上下文的典型写法如下const innerAudioContext wx.createInnerAudioContext(); function playCorrectSound() { innerAudioContext.src /assets/audio/correct.mp3; innerAudioContext.play(); }这段代码业务上不难但容易忽略的是 InnerAudioContext 是全局单例如果在页面 onLoad 里重复创建会积累多个音频实例同时播放在低端安卓机上表现成“声音重叠、破音”。更稳的做法是把音频上下文定义在 Page 对象外部用页面级变量保存页面销毁时调用 innerAudioContext.destroy() 释放资源。另一个常踩的点是音频资源路径不能放在条件编译分支里如果用绝对路径必须在路径前加斜杠写成 /assets/audio/correct.mp3否则真机上会报“音频资源不存在”的错。4. 上线前三次想骂人的排坑实录数据格式、倒计时与音频黑匣子4.1 题库字与候选字开关不一致判对时出现“灵异现象”现象答对题目后页面偶尔会弹出“再想想”刷新一次再答又能过关同一题结果却不稳定。原因题库里的成语字面是一个字符串比如“一心一意”拆分成候选字后出现了两个“一”和两个“意”候选数组简单用 indexOf 查找匹配字时会把第一个“一”标记为 used导致用户填第二个空格时点击的“一”已经处于禁用状态最终答案里空格缺失判作错误。解决候选字从一开始就按“独立个体”标记每个元素保留独立 iduse 状态按 id 维护不用字符相等判断。我在改造时给 candidateChars 每一项加了一个唯一索引字段点击和退回都通过 id 定位同字不同个体之间互不干扰。4.2 倒计时归零后页面还在偷偷走秒现象答题页有一个 30 秒倒计时倒计时归零后弹窗提示超时但关掉弹窗发现文字还在走负数。原因倒计时用的是 setInterval 每秒减 1但超时弹窗里没有 clearInterval定时器继续运行界面上的数字就一路负下去。解决在超时分支里先把 timer 清掉并且在 onHide 生命周期里也清一次定时器防止用户切到后台再回来时定时器叠着跑。代码结构上可以把清除定时器的动作抽成一个 stopTimer 方法在超时、答完、页面隐藏三种场景下复用。4.3 真机预览时音频不响开发者工具里却一切正常现象开发者工具里答对答错都有提示音转到真机预览后完全无声。原因一种可能是音频文件格式或尺寸不合规另一种最常见的是音频上下文被销毁后没有重新创建开发者工具里销毁逻辑不会报错真机上则是“无声音”的无声故障。解决把 InnerAudioContext 的创建移到每次播放前播放后不销毁只在 onUnload 时统一释放。如果还不能出声检查音频文件比特率不少工具导出的 mp3 在微信真机上兼容性一般把音源压成 44.1kHz、96kbps 以下基本能解决。4.4 金币数值越攒越少甚至变成负数现象用户答题正确加了金币返回首页再进答题页金币反而比之前少了有时会扣到负数。原因金币初始化和读取时机错乱页面 onLoad 时用默认值覆盖了本地缓存里已有的金币相当于每次进入都被重置了一遍。解决读取金币的代码要放在写入之前并加一层判断——只有本地没有缓存时才初始化默认值已有缓存则直接用缓存值不做覆盖操作。这里我个人的习惯是封装成一个 getCoins 方法内部统一处理读缓存、初始化和类型校验所有页面都从它拿金币。4.5 排行榜数字排对了可页面渲染顺序不对现象排行榜列表里的分数是对的但从高到低排序后页面展示出来并不是按分数高低排列。原因单纯 sort 后直接 setData没有考虑 sort 是按字符串排序还是按数字排序的差异。数字分数被读成字符串sort 方法按 ASCII 比较导致 900 排在 1000 前面。解决排序前先把分数字段用 Number() 转成数字再 sort并且在数据源源头保证分数写入时就是 number 类型。这个地方如果不做防御不同微信基础库版本的表现还不一样最阴的是部分版本表面上排对了换了基础库又塌了。5. 让成语题库从“能玩”变成“耐玩”数据校验与玩法扩容的落地技巧当这套源码的答题流程跑通后下一步需要处理的就是成语数据本身的可维护性。我见过不少人对题库做二次开发时直接把新成语一行行追加到数组尾部结果出现重复条目、拼音缺失、前后空格等问题轻则题目异常重则点击某个成语时页面渲染报错。这里提供一个简单但有效的本地校验脚本思路把题库作为 Node 脚本的可执行模块跑一遍数据完整性检查。校验脚本可以这样写const { idiomList } require(./data/idioms.js); let errors []; idiomList.forEach((item, index) { if (!item.word || item.word.length ! 4) { errors.push(第${index 1}条成语长度异常: ${item.word}); } if (item.word.split().some(char char.trim() )) { errors.push(第${index 1}条成语包含空格: ${item.word}); } if (!item.pinyin || item.pinyin.split( ).length ! 4) { errors.push(第${index 1}条拼音异常: ${item.word}); } }); if (errors.length 0) { console.log(发现问题); errors.forEach(err console.log(err)); } else { console.log(校验通过); }这个脚本覆盖了三个最常翻车的点成语字数必须为四、字面不能有空格、拼音数量必须和字数对应。如果以后要加入题目的难度字段脚本里建议加一条枚举校验防止有人把难度写成 discrete、normal 之外的任意值。玩法的扩容方面在不改动架构的前提下最值得加的是“每日一题”模式。实现思路是取今天日期计算一个固定随机种子从题库里稳定抽出一道题保证当天所有用户拿到的是同一题第二天再自动轮换。这样做的代码量很小但能带来每日回访的理由。固定随机种子可以用年月日的数字求和作为 seed示例逻辑如下function getDailyQuestion() { const now new Date(); const seed now.getFullYear() * 10000 (now.getMonth() 1) * 100 now.getDate(); const index seed % idiomList.length; return idiomList[index]; }每日一题背后真正的产品逻辑是给用户一个轻量的回归钩子而不是靠排行榜那样的重度社交压力。这种设计成本低、效果好我自己的项目里就是靠这一招把七日留存拉高了不少。那次之后我也养成一个习惯任何题库类项目提测前必须强制跑一遍数据校验脚本顺手再检查一次金币缓存读写顺序。小程序端的很多问题不是代码逻辑多深而是数据模样的坑。希望这份拆解能帮你在改成语小秀才源码时少走几段弯路把更多精力留给真正想做的玩法和体验创新。希望帮到你。本文还有配套的精品资源点击获取
RELATED

相关推荐

高数自学复盘:从极限到导数,吃透增量比取极限

高数自学复盘:从极限到导数,吃透增量比取极限

说个实话,8月21日这个日期,是我自己定的死线。暑假一开始就想着要啃下高数第二章,结果从7月中旬拖到8月下旬,再不开始,整个假期就真的交代了。我当时翻开这套经典高数教材,第二章第一节“导数的概念”&…

📅 2026/10/14 11:47:20
用数据挖掘重构工资分配逻辑:从成本科目到业务仪表盘

用数据挖掘重构工资分配逻辑:从成本科目到业务仪表盘

1. 这不是薪酬体系设计,而是用数据挖掘解构“工资为什么这样发”“数据挖掘与工资分配”——看到这个标题,很多人第一反应是:这不就是HR在做薪酬分析?或者企业用算法自动调薪?但实际操作中,我带过的几个模拟…

📅 2026/10/14 11:42:18
风光发电场景削减:基于概率距离的MATLAB高效实现

风光发电场景削减:基于概率距离的MATLAB高效实现

1. 项目背景与核心价值风光发电场景的随机性和波动性给电力系统规划与运行带来巨大挑战。传统蒙特卡洛模拟虽然能生成大量场景,但计算成本高昂且存在大量冗余。我在参与某省级电网新能源消纳项目时,曾遇到需要处理3000个初始场景的案例,直接导…

📅 2026/10/14 11:42:18
MORE NEWS

更多资讯

📰

大比例尺DLG转地理实体:数据体检、映射表与自动化管线

简介:针对1∶500、1∶1 000、1∶2 000比例尺基础地理信息要素数据转换生产基础地理实体数据,这份技术规程面向基础地理信息专业人员和测绘生产管理人员,是新型基础测绘与实景三维中国建设中可实际参照的作业指导文件。内容覆盖范围、引用文件…

📰

IKAnalyzer中文分词器实战:从集成到调优全指南

做中文搜索和内容检索项目时,IKAnalyzer分词器是一个绕不开的名字。如果你的项目对搜索质量有要求,或者说你正在尝试用Lucene、Elasticsearch这类搜索引擎处理中文内容,那这篇文章应该能帮你省掉不少弯路。本文会从一个实际可用、可复现的角度…

📰

Vue登录页如何彻底屏蔽浏览器保存密码提示?五种方案实战对比

1. 登录页密码保存提示的来龙去脉1.1 这个提示到底从哪冒出来的做过前端登录页的人基本都遇到过这个场景:用户输入账号密码,点击登录,页面跳转的一瞬间,浏览器右上角或者地址栏下方弹出一个"是否保存密码"的小气泡。对普…

📰

Knative v1.0 版本发布深度解读:Serving、Eventing、扩展组件与 Operator 的里程碑更新

文档教程云原生 【免费下载链接】docs User documentation for Knative components. 项目地址: https://gitcode.com/gh_mirrors/docs1/docs 点击查看 免费下载 2021 年 11 月 2 日,Knative 正式发布 v1.0 版本,标志着这个构建在 Kubernetes…

📰

sebastian/version 版本演进全解析:从 ChangeLog 看 Git 托管 PHP 项目的版本号管理库

开发工具 【免费下载链接】version Library that helps with managing the version number of Git-hosted PHP projects 项目地址: https://gitcode.com/gh_mirrors/ver/version 点击查看 免费下载 本篇技术指南以 ChangeLog.md 为脉络,完整梳理 sebast…

📰

机械臂建模+双臂蒙特卡洛法工作空间、笛卡尔空间、运动学圆弧轨迹(Matlab代码实现)

💥💥💞💞欢迎来到本博客❤️❤️💥💥 🏆博主优势:🌞🌞🌞博客内容尽量做到思维缜密,逻辑清晰,为了方便读者。 &#x1f381…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬