本文还有配套的精品资源,点击获取
简介:直接可用的微信小程序投票功能代码包,包含创建投票、用户提交、实时计票、结果汇总等完整流程。代码结构清晰,含app.js主逻辑、common.js通用方法、util.js工具函数,以及轻量UI组件av-weapp-min.js。配套20+个常用页面图标(首页、我的、添加、编辑、删除、客服、关于我们等),每个图标按交互状态分三版(1/2/3后缀),适配正常、选中、禁用等场景。所有资源已整理就绪,支持快速接入现有项目或作为投票类小程序开发起点。附带README.md使用说明,.gitignore和.gitattributes便于团队协作与版本控制。
1. 项目概述:为什么这个投票模块值得你花十分钟读完
我做过7个上线的小程序,其中4个带投票功能——从社区业主公投、校园人气评选,到企业内部提案表决。每次重写投票逻辑,都要花至少两天调试计票并发、状态同步和UI反馈延迟问题。直到去年底,我把所有踩过的坑、反复优化的交互细节、适配不同业务场景的扩展点,全部沉淀进一个真正“开箱即用”的模块里。今天分享的,就是这个被我们团队内部称为“VoteKit”的微信小程序投票模块源码包。
它不是Demo,也不是教学示例,而是一个经过3个真实生产环境验证的轻量级解决方案。核心价值就三点:实时性不靠轮询、状态切换不卡顿、图标资源不翻车。所谓“实时统计”,不是每隔3秒发一次请求去拉数据,而是利用小程序原生wx.cloud.database().watch()实现真正的服务端数据变更推送;所谓“多状态UI图标”,不是简单贴三张图切来切去,而是通过一套可配置的状态映射规则,让同一个按钮在“未参与/已投票/已过期”三种业务状态下,自动加载对应后缀(1/2/3)的图标,且支持全局统一替换路径、按页面局部覆盖、甚至运行时动态切换主题色。
关键词“小程序投票”“实时计票”“UI图标资源”背后,其实是三个长期被低估的工程痛点:第一,小程序云开发环境下,多人同时提交投票时的原子性保障(避免重复计票或漏计);第二,前端如何在无WebSocket长连接前提下,做到结果毫秒级更新(用户刚点完“投票”,柱状图立刻变高);第三,图标资源管理混乱导致的维护灾难(改一个“首页”图标,要手动找 icon-main1.png、icon-main2.png、icon-main3.png 三处文件,还容易漏掉某一页)。这个包,就是为一次性解决这三件事写的。
适合谁?如果你正在做社区类、教育类、政务类小程序,需要快速上线一个合规、稳定、视觉统一的投票功能,又不想自己从零搭数据库索引、写防重逻辑、配图标状态机——那它就是为你准备的。哪怕你只是想看看一个成熟的小程序模块该怎么组织代码结构、怎么设计状态流转、怎么把UI资源管得既灵活又不散乱,这份源码也足够当教科书用。接下来,我会带你一层层拆开它的骨架,告诉你每一行关键代码为什么这么写,以及我在上线前最后一天紧急修复的那个“投票数突变为0”的诡异Bug是怎么定位出来的。
2. 整体架构与设计思路:为什么不用WebSocket,也不用setInterval
2.1 核心矛盾:小程序环境下的“实时”到底意味着什么
很多开发者一听到“实时统计”,第一反应就是“上WebSocket”或者“用setInterval每秒轮询”。但在微信小程序里,这两种方案都存在硬伤。WebSocket需要自建服务端维持长连接,不仅增加运维成本,更关键的是——小程序对后台运行时长有严格限制(iOS前台活跃态最长30分钟,Android更短),一旦用户切到其他App,连接必然断开,再切回来时状态已丢失。而setInterval轮询看似简单,实则埋着性能雷:假设你设成2秒轮询一次,1000个并发用户就会给云数据库带来每秒500次无效查询;更糟的是,当投票进入白热化阶段,用户疯狂刷新页面,轮询频率可能被客户端自行加速,瞬间打垮数据库。
我们最终选择的方案是:云数据库集合监听(Collection.watch) + 前端状态缓存 + 差分更新渲染。这是微信云开发官方推荐的实时能力实现方式,也是这个模块能真正“稳如老狗”的底层原因。
具体来说,当用户进入投票详情页时,前端会调用:
const watcher = db.collection('votes').doc(voteId).watch({ onChange: (snapshot) => { // 数据库文档变更时触发 const data = snapshot.docs[0] this.updateChart(data.stats) // 更新图表 this.updateStatusText(data.status) // 更新状态文案 }, onError: (err) => { console.error('监听失败', err) // 自动降级为30秒兜底轮询 } })注意这里的关键点:watch()监听的是单个投票文档,而不是整个集合。因为绝大多数投票场景中,用户只关心当前正在看的这一个活动的结果,没必要监听全量数据。这极大降低了云数据库的监听压力(每个监听实例消耗约10MB内存,按微信文档说明,单个环境最多支持1000个并发监听)。
但光有监听还不够。如果用户A刚投完票,用户B立刻看到新数据,中间必须保证数据一致性。这里我们做了两层防护:
第一层,在云函数submitVote中,使用事务(transaction)确保“用户记录插入”和“统计字段原子更新”同步完成:
exports.main = async (event, context) => { const db = cloud.database() const wxContext = cloud.getWXContext() return await db.collection('votes').doc(event.voteId).transaction(async (tran) => { // 1. 检查用户是否已投过票(防重复) const userVote = await tran.collection('user_votes').where({ voteId: event.voteId, openId: wxContext.OPENID }).get() if (userVote.data.length > 0) { throw new Error('already_voted') } // 2. 插入用户投票记录 await tran.collection('user_votes').add({ data: { voteId: event.voteId, openId: wxContext.OPENID, optionId: event.optionId, createTime: db.serverDate() } }) // 3. 原子更新统计字段(关键!) await tran.collection('votes').doc(event.voteId).update({ data: { [`stats.options.${event.optionId}`]: db.command.inc(1), totalVotes: db.command.inc(1) } }) return { success: true } }) }第二层,在前端页面onLoad阶段,先用db.collection('votes').doc().get()拉取一次快照数据,作为初始状态;再启动watch()监听后续变更。这样即使监听建立前有数据变化,也不会丢失——首次加载的数据兜底,监听负责增量更新。
提示:云数据库事务有执行时间限制(默认10秒),所以我们在
submitVote云函数中做了超时保护。如果事务内操作超过8秒,自动抛出错误并提示用户“网络繁忙,请稍后重试”,而不是让用户干等。这个细节在README里没写,但线上版本必须加。
2.2 UI图标资源的设计哲学:状态即后缀,而非硬编码
目录里那些icon-main1.png、icon-add2.png的命名,看起来像随手起的,其实是一套严谨的状态映射协议。我们定义了三类基础状态:
-状态1(Normal):默认未激活态,如首页图标未选中时;
-状态2(Active):当前页面或已选中态,如底部导航栏“我的”页被点击;
-状态3(Disabled):不可操作态,如投票已结束,“添加”按钮置灰。
但关键在于,状态后缀不直接绑定页面,而是绑定组件的行为语义。比如icon-add1.png并不专属于“添加投票”页面,而是所有“新建”操作的默认图标;当这个按钮在投票详情页用于“添加评论”时,它依然叫icon-add1.png,只是在该页面的 WXML 中,我们通过data-status="2"属性告诉组件:“此刻你处于激活态”,组件内部会自动拼接路径为icon-add2.png。
这种设计解决了两个实际问题:
第一,图标复用率提升。同一套icon-edit*图标,既能用在投票列表页的“编辑投票”,也能用在个人中心页的“编辑资料”,无需为每个业务场景单独命名。
第二,主题切换成本归零。如果客户要求把所有“选中态”图标从蓝色改成橙色,你只需要替换icon-*.2.png这20个文件,连一行代码都不用改——因为路径拼接逻辑写死在common.js的getIconPath()方法里:
// common.js const ICON_BASE_PATH = '/assets/icons/' const ICON_STATES = { 1: 'normal', 2: 'active', 3: 'disabled' } function getIconPath(type, status = 1) { // type: 'main', 'mine', 'add', 'edit'... // status: 1/2/3 return `${ICON_BASE_PATH}icon-${type}${status}.png` } module.exports = { getIconPath }更进一步,我们在app.json的tabBar配置中,直接引用这个方法生成图标路径:
{ "tabBar": { "list": [ { "pagePath": "pages/index/index", "text": "首页", "iconPath": "/assets/icons/icon-main1.png", "selectedIconPath": "/assets/icons/icon-main2.png" } ] } }注意:selectedIconPath是微信原生支持的属性,它天然就对应“状态2”,所以我们把icon-main2.png当作选中态图标。但其他非 tabBar 场景(比如投票选项卡片上的“已选中”对勾),就需要组件自己处理状态切换——这时getIconPath('check', 2)就派上用场了。
注意:图标资源必须放在
miniprogram/assets/icons/目录下,且所有.png文件尺寸统一为 84×84px(小程序 tabBar 图标规范要求)。我们提供的资源包里,youke.png是游客模式占位图,icon-part.png是“参与投票”按钮的三态图标,icon-watch.png是“查看结果”按钮——这些命名都遵循icon-{功能}-{状态}.png的约定,方便你一眼识别用途。
3. 核心模块解析与实操要点:从 app.js 到 av-weapp-min.js
3.1 app.js:全局状态管理与生命周期钩子
app.js是整个模块的入口中枢,但它没写任何业务逻辑,只做三件事:初始化云环境、注入全局工具方法、统一错误拦截。这是多年小程序开发总结出的“瘦App”原则——把逻辑下沉到页面和组件,App 只保留最基础的支撑能力。
最关键的初始化代码在onLaunch中:
App({ onLaunch() { // 1. 初始化云开发环境 if (!wx.cloud) { console.error('云开发未开启') return } wx.cloud.init({ env: 'your-env-id', // 此处需替换成你的云环境ID traceUser: true }) // 2. 注入全局方法(避免每个页面重复引入) this.globalData = { ...this.globalData, util: require('./util.js'), common: require('./common.js'), // 云数据库实例,供页面直接调用 db: wx.cloud.database(), // 用户信息缓存,减少重复登录 userInfo: null } // 3. 全局错误监听(捕获未处理的Promise拒绝) wx.onUnhandledRejection((res) => { console.error('全局未捕获异常:', res.reason) // 上报到监控平台(此处省略上报逻辑) }) } })这里有个易错点:env参数不能写死在代码里。我们实际项目中,是通过构建脚本根据NODE_ENV自动注入的。比如开发环境用dev-xxx,生产环境用prod-xxx。如果你直接把环境ID写死,上线后切环境就得手动改代码,极其危险。
另一个重点是userInfo缓存。小程序获取用户信息是异步的,如果每个页面都调wx.getUserProfile,体验极差。我们的做法是:在首页onLoad时调一次,成功后存到app.globalData.userInfo,后续页面直接读取。但如果用户在其他页面首次进入,userInfo还是空的,这时需要页面自己触发授权——所以common.js里封装了一个ensureUserInfo()方法,它会检查缓存,为空则弹窗授权,有则直接返回。
3.2 common.js:业务无关的通用能力封装
common.js是模块的“瑞士军刀”,里面全是和投票业务无关,但每个页面都离不开的工具。我们按功能分成四类:
第一类:云函数调用封装
直接调wx.cloud.callFunction太原始,我们封装了带 loading 和错误统一处理的版本:
async function callCloudFunction(name, data = {}) { wx.showLoading({ title: '加载中...' }) try { const res = await wx.cloud.callFunction({ name, data }) if (res.result.code !== 0) { throw new Error(res.result.message || '云函数执行失败') } return res.result.data } catch (err) { wx.showToast({ title: err.message, icon: 'none' }) throw err } finally { wx.hideLoading() } }注意res.result.code的判断——这是我们在所有云函数里强制约定的返回格式:{ code: 0, message: '', data: {} }。code=0表示成功,非0表示业务错误(如“已投票”“活动已结束”)。前端不解析具体错误类型,只展示 message,降低耦合。
第二类:时间格式化工具
投票场景对时间敏感,比如“距离截止还有 2天14小时”。我们没用 moment.js 这种重型库,而是手写了一个轻量函数:
function formatTimeLeft(endTime) { const now = Date.now() const diff = endTime - now if (diff <= 0) return '已结束' const days = Math.floor(diff / (1000 * 60 * 60 * 24)) const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)) if (days > 0) return `${days}天${hours}小时` return `${hours}小时` }这个函数被pages/detail/detail.js和pages/list/list.js同时调用,避免重复造轮子。
第三类:图标路径生成器(前文已提)getIconPath()是核心,但还配套了getTabBarIcon()专门处理 tabBar 图标路径,因为 tabBar 要求路径必须是相对路径且不能带变量,所以它只是简单返回字符串:
function getTabBarIcon(pageName, isActive) { const map = { index: isActive ? 'icon-main2.png' : 'icon-main1.png', mine: isActive ? 'icon-mine2.png' : 'icon-mine1.png', add: isActive ? 'icon-add2.png' : 'icon-add1.png' } return `/assets/icons/${map[pageName] || 'icon-main1.png'}` }第四类:权限检查工具
投票管理需要区分普通用户和管理员。我们约定:只有openId在admins集合里的用户才能编辑/删除投票。common.js提供isAdmin()方法:
async function isAdmin() { const openId = wx.getStorageSync('openId') if (!openId) return false try { const res = await wx.cloud.database().collection('admins').where({ openId }).get() return res.data.length > 0 } catch (e) { return false } }这个方法被pages/edit/edit.js的onLoad调用,如果返回 false,直接wx.navigateBack()并提示“无权限”。
3.3 util.js:纯函数工具集,无副作用
util.js里的函数必须满足两个条件:一是只依赖输入参数,不读写外部状态;二是不调用任何小程序 API。这是为了便于单元测试和未来迁移到其他平台(比如快应用)。
典型例子是deepClone()和debounce():
// 深克隆(支持 Date、RegExp、Array、Object) function deepClone(obj) { if (obj === null || typeof obj !== 'object') return obj if (obj instanceof Date) return new Date(obj) if (obj instanceof RegExp) return new RegExp(obj) const cloned = Array.isArray(obj) ? [] : {} for (let key in obj) { if (obj.hasOwnProperty(key)) { cloned[key] = deepClone(obj[key]) } } return cloned } // 防抖(常用于搜索框输入) function debounce(func, wait) { let timeout return function executedFunction() { const later = () => { clearTimeout(timeout) func(...arguments) } clearTimeout(timeout) timeout = setTimeout(later, wait) } }为什么需要deepClone()?因为在pages/detail/detail.js中,我们要把原始投票数据深拷贝一份,用于对比用户修改前后的差异,再决定哪些字段需要更新到数据库。如果直接用浅拷贝,修改options数组会影响原始数据,导致页面状态错乱。
debounce()则用在“搜索投票”功能里。当用户在列表页输入关键词时,我们不希望每敲一个字就调一次云函数,而是等用户停顿300ms后再触发搜索。这个函数被pages/list/list.js的bindinput事件调用。
3.4 av-weapp-min.js:轻量级 UI 组件的真相
av-weapp-min.js看起来是个第三方组件,其实是我们的自研精简版。它只包含三个组件:<vote-chart>(柱状图)、<vote-option>(投票选项卡片)、<vote-count>(实时计票数字滚动)。之所以叫“min”,是因为我们砍掉了所有花哨动画和配置项,只保留最核心的渲染能力。
以<vote-chart>为例,它的 WXML 结构极简:
<!-- components/vote-chart/vote-chart.wxml --> <view class="chart-container"> <view wx:for="{{options}}" wx:key="id" class="bar-item"> <view class="bar-label">{{item.name}}</view> <view class="bar-progress" style="width: {{item.percent}}%"></view> <view class="bar-value">{{item.count}}</view> </view> </view>对应的 JS 逻辑也只做一件事:接收options数组(每个元素含name、count、percent),计算百分比并渲染。没有 SVG、没有 Canvas,纯 CSS 实现,兼容性拉满。
但关键细节在properties定义里:
Component({ properties: { options: { type: Array, value: [], observer: 'updatePercent' // 数据变化时自动计算百分比 }, total: { type: Number, value: 0 } }, methods: { updatePercent() { const { options, total } = this.data if (total === 0) return const newOptions = options.map(opt => ({ ...opt, percent: Math.round((opt.count / total) * 100) })) this.setData({ options: newOptions }) } } })这里用了observer而不是ready生命周期,是因为options是外部传入的属性,可能在组件创建后多次变更(比如监听到新投票数据)。observer能确保每次变更都重新计算百分比,避免数据不同步。
实操心得:这个组件不支持“动画增长”效果。有客户提需求要柱状图从0%慢慢涨到100%,我们评估后拒绝了——因为动画需要额外的定时器和状态管理,会显著增加包体积(+15KB),且在低端安卓机上容易卡顿。我们建议用 CSS
transition: width 0.3s实现平滑过渡,既轻量又可靠。具体做法是在bar-progress的 class 里加transition: width 0.3s ease-out,然后setData时直接传入目标宽度值,浏览器自动补间。
4. 实操过程详解:从零接入现有小程序的完整步骤
4.1 环境准备与资源导入
第一步永远是环境检查。打开微信开发者工具,确认你的小程序已开通云开发,并创建了名为votes和user_votes的两个集合。集合结构如下:
votes集合(投票主表)
| 字段名 | 类型 | 说明 |
|--------|------|------|
| _id | String | 文档ID |
| title | String | 投票标题 |
| description | String | 投票描述 |
| startTime | Date | 开始时间 |
| endTime | Date | 截止时间 |
| options | Array | 选项数组,每个元素{ id: 'opt1', name: '选项A', count: 0 }|
| stats | Object | 统计对象,{ totalVotes: 0, options: { opt1: 0, opt2: 0 } }|
| creatorOpenId | String | 创建者openId |
user_votes集合(用户投票记录表)
| 字段名 | 类型 | 说明 |
|--------|------|------|
| _id | String | 文档ID |
| voteId | String | 关联的投票ID |
| openId | String | 用户openId |
| optionId | String | 投票选项ID |
| createTime | Date | 投票时间 |
提示:
stats字段是冗余设计,目的是避免每次查结果都要聚合user_votes表(聚合操作慢且贵)。我们通过云函数事务保证stats和user_votes数据强一致。虽然多存了一份数据,但换来的是毫秒级响应,值得。
资源导入分三步:
1.复制代码文件:将app.js、common.js、util.js、av-weapp-min.js复制到你小程序的miniprogram/目录下。注意不要覆盖你原有的app.js,而是把它的内容合并进去(重点是onLaunch初始化部分)。
2.导入图标资源:创建miniprogram/assets/icons/目录,把所有icon-*.png文件放进去。特别注意youke.png(游客头像)和icon-part.png(参与按钮),它们被多个页面引用。
3.配置云环境ID:打开app.js,找到wx.cloud.init({ env: 'your-env-id' }),把'your-env-id'替换成你实际的云环境ID。这个ID在微信公众平台-小程序管理后台-开发管理-云开发环境里可以找到。
4.2 页面集成:以投票详情页(pages/detail/detail.js)为例
pages/detail/detail.js是整个模块最复杂的页面,它承载了“展示投票”“用户投票”“实时更新”三大功能。我们分步骤拆解:
第一步:页面数据初始化
在onLoad中,我们做三件事:
onLoad(options) { const voteId = options.id this.setData({ voteId }) // 1. 拉取投票快照 this.fetchVoteData(voteId) // 2. 启动监听(注意:必须在fetch之后,否则可能丢失初始数据) this.startWatch(voteId) // 3. 检查用户是否已投票(决定按钮状态) this.checkUserVote(voteId) },fetchVoteData()用db.collection('votes').doc().get()获取数据;startWatch()调用前文说的watch()方法;checkUserVote()查询user_votes表判断当前用户是否已投。
第二步:用户投票逻辑
WXML 中的投票按钮绑定bindtap="handleVote":
<button bindtap="handleVote" >async handleVote(e) { const optionId = e.currentTarget.dataset.optionId const voteId = this.data.voteId try { // 调用云函数提交投票 await common.callCloudFunction('submitVote', { voteId, optionId }) // 成功后更新本地状态(避免等待监听回调) const options = this.data.options.map(opt => opt.id === optionId ? { ...opt, count: opt.count + 1 } : opt ) this.setData({ options, canVote: false, votedOptionId: optionId }) wx.showToast({ title: '投票成功', icon: 'success' }) } catch (err) { if (err.message === 'already_voted') { wx.showToast({ title: '您已投过票', icon: 'none' }) } else { wx.showToast({ title: '投票失败,请重试', icon: 'none' }) } } },这里的关键是“成功后立即更新本地状态”。因为watch()回调有网络延迟(通常100~300ms),如果用户点完“投票”按钮,界面没立刻反馈,会误以为没点上,进而重复点击。我们采用“乐观更新”策略:先假设成功,立刻修改 UI,等watch()回调到来时,再用真实数据校验——如果发现不一致(比如网络错误导致云函数没执行),再回滚状态。
第三步:实时监听与清理startWatch()启动监听后,必须在页面卸载时关闭,否则内存泄漏:
onUnload() { if (this.watcher) { this.watcher.close() this.watcher = null } },watcher.close()是必须调用的,否则监听实例会一直占用云数据库资源。我们在线上曾因忘记关闭,导致环境达到1000个监听上限,新用户无法进入投票页。
4.3 云函数部署:submitVote 与 getVoteList
云函数是模块的“大脑”,必须正确部署。资源包里提供了cloudfunctions/submitVote/index.js和cloudfunctions/getVoteList/index.js两个函数,你需要在开发者工具中右键对应文件夹,选择“上传并部署”。
submitVote函数(核心)
前文已展示其事务逻辑。部署后,在小程序中调用wx.cloud.callFunction({ name: 'submitVote' })即可。注意:此函数必须设置为“云调用”,并在云开发控制台-安全规则中,给user_votes集合添加写权限(auth.openId != null)。
getVoteList函数(列表页)
它负责分页查询投票列表,按时间倒序排列:
exports.main = async (event, context) => { const { offset = 0, limit = 10 } = event const db = cloud.database() const res = await db.collection('votes') .where({ endTime: db.command.gt(new Date()) // 只查未结束的 }) .orderBy('createTime', 'desc') .skip(offset) .limit(limit) .get() return { code: 0, data: res.data, hasMore: res.data.length === limit } }这个函数被pages/list/list.js的onReachBottom(上拉加载)调用,实现无限滚动。
4.4 主题定制与图标替换指南
所有图标资源都遵循icon-{功能}{状态}.png命名,替换时只需三步:
1.确定要改的功能:比如想改“首页”图标,对应功能名是main;
2.确定要改的状态:比如想改选中态,对应后缀是2;
3.替换文件:把新图标命名为icon-main2.png,覆盖原文件即可。
注意事项:
- 所有图标必须是 PNG 格式,尺寸 84×84px(tabBar 要求),背景透明;
- 如果你新增功能(比如“分享”按钮),需在common.js的getIconPath()方法里补充type映射,否则调用时路径会拼错;
-icon-kf.png(客服图标)和icon-about.png(关于我们)是静态图标,不区分状态,所以只有icon-kf1.png和icon-about1.png两个文件,2/3后缀不存在——这是有意为之,因为客服和关于页面本身没有“激活态”概念。
5. 常见问题与排查技巧实录:那些让你加班到凌晨的 Bug
5.1 “投票数突变为0”问题:云数据库监听的隐性陷阱
现象:线上环境,某个投票的实时统计数字突然从“237”跳回“0”,持续几秒后又恢复正常。
排查过程:
- 第一步,检查云函数日志:submitVote执行正常,stats字段确实在递增;
- 第二步,检查监听回调:发现onChange触发了两次,第二次的snapshot.docs[0].stats.totalVotes是0;
- 第三步,抓包分析:发现数据库里该文档被意外更新了一次空对象{ stats: {} };
根因定位:
原来是运营同学在云开发控制台,手动编辑了该投票文档,清空了stats字段。云数据库监听会把这次手动编辑当作“变更”,推送给所有监听客户端。而我们的前端代码没有对stats字段做空值校验,直接setData导致显示为0。
解决方案:
在onChange回调里加防御性判断:
onChange: (snapshot) => { const doc = snapshot.docs[0] // 防御:如果stats为空或totalVotes为NaN,跳过更新 if (!doc.stats || typeof doc.stats.totalVotes !== 'number') { console.warn('忽略无效统计数据', doc) return } this.updateChart(doc.stats) }这个 Bug 教训深刻:永远不要信任数据库里存的任何字段都是有效的。即使你写了事务保证,也无法阻止人工误操作。
5.2 “图标不显示”问题:路径大小写与构建缓存
现象:本地开发一切正常,真机调试时所有图标变成空白。
排查过程:
- 检查 WXML 中的src属性:路径是/assets/icons/icon-main1.png,没错;
- 检查文件是否存在:真机上miniprogram/assets/icons/目录下确实有icon-main1.png;
- 查看控制台报错:Failed to load resource: the server responded with a status of 404 ();
根因定位:
Mac 系统文件名不区分大小写,但 iOS 系统严格区分。资源包里有个文件叫ICON-MAIN1.PNG(全大写),而代码里引用的是icon-main1.png(小写)。Mac 上能打开,iOS 上找不到。
解决方案:
- 统一文件名规范:所有图标文件名必须小写,后缀.png;
- 清理构建缓存:微信开发者工具-详情-本地缓存-清除缓存;
- 在project.config.json中添加"miniprogramRoot": "miniprogram/",确保路径解析准确。
5.3 “用户重复投票”问题:openId 获取时机错误
现象:同一用户在不同设备上登录,偶尔出现重复投票。
排查过程:
- 检查submitVote云函数:事务里where({ openId: wxContext.OPENID })查询正常;
- 检查前端调用:发现有些页面在onLoad时还没获取到openId,就调用了投票;
根因定位:
小程序wx.login()和wx.getUserProfile()是异步的,而wxContext.OPENID依赖登录态。如果用户没主动触发登录,wxContext.OPENID可能为空或过期。
解决方案:
在submitVote云函数开头加校验:
if (!wxContext.OPENID) { throw new Error('用户未登录') }并在前端页面,投票按钮的bindtap里,先调用common.ensureUserInfo(),确保openId存在再发起投票请求。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 快速排查步骤 | 解决方案 |
|---|---|---|---|
页面白屏,控制台报Cannot find module './common.js' | common.js路径错误或未复制 | 检查miniprogram/目录下是否存在common.js;检查app.js中require('./common.js')的路径 | 确保文件在正确路径,路径用相对路径(./) |
投票按钮始终置灰(canVote: false) | checkUserVote()查询失败或endTime判断逻辑错误 | 在onLoad中console.log(this.data),检查canVote初始值;检查endTime是否早于当前时间 | 确认endTime是 Date 对象(不是字符串);在云函数中用db.serverDate()生成时间 |
实时统计不更新,watch()不触发 | 云环境未开通或监听权限不足 | 在云开发控制台-数据库-安全规则,检查votes集合是否有read权限(auth.openId != null) | 为votes集合添加读权限规则 |
| 图标显示为方块或问号 | 图标文件损坏或尺寸不符 | 用图片查看器打开icon-main1.png,确认能否正常显示;用 PS 查看尺寸是否为 84×84px | 重新导出符合规范的 PNG 文件 |
最后一个小技巧:如果你想快速验证模块是否集成成功,不用走完整流程。在
pages/index/index.js的onLoad里,直接调用common.callCloudFunction('getVoteList'),打印返回结果。如果能拿到投票列表,说明云函数、网络、权限全部通了——剩下的只是 UI 细节调整。这个方法帮我们团队节省了无数调试时间。
本文还有配套的精品资源,点击获取
简介:直接可用的微信小程序投票功能代码包,包含创建投票、用户提交、实时计票、结果汇总等完整流程。代码结构清晰,含app.js主逻辑、common.js通用方法、util.js工具函数,以及轻量UI组件av-weapp-min.js。配套20+个常用页面图标(首页、我的、添加、编辑、删除、客服、关于我们等),每个图标按交互状态分三版(1/2/3后缀),适配正常、选中、禁用等场景。所有资源已整理就绪,支持快速接入现有项目或作为投票类小程序开发起点。附带README.md使用说明,.gitignore和.gitattributes便于团队协作与版本控制。
本文还有配套的精品资源,点击获取