简介:这份资源是微信小程序投票系统的完整源码包,面向希望学习小程序开发或快速搭建投票应用的开发者,可用于意见收集、评选活动与市场调研等场景。解压后包含创建投票、发布投票、结果展示等核心页面,以及全局配置、自定义网络请求模块、数据模型、可复用组件与模拟数据,便于理解小程序的架构、页面生命周期、网络通信与数据可视化实现。资源共90个文件,以49个wxss样式、15个png图片、8个js脚本、8个json配置、6个wxml结构为主,另含php接口与sql建表文件,压缩包约153KB,目录结构清晰,适合按模块检索学习。目前已有3644人学习下载,读者可据此掌握组件化开发、请求封装与数据管理思路,并在此基础上扩展登录、权限控制与防刷机制。
1. 从一份能跑通的投票 Demo 说起:它到底解决了什么问题
如果你正在找一个能直接跑起来的微信小程序投票系统,大概率不是为了研究投票算法本身,而是想快速验证一个想法:能不能在微信里发起一次投票,让用户点几下就完成选择,后台还能看到结果。这份「微信小程序投票系统创建投票发布demo完整源码下载.zip」就是冲着这个场景去的——它把创建投票、发布投票、参与投票、查看结果这条最短链路完整地做了一遍,前端用小程序原生框架,页面结构以 HTML 风格的 WXML 为主,配合 WXSS 做样式,逻辑层走 JavaScript,数据交互按小程序常见的wx.request方式对接后端接口。
它适合两类人:一类是刚接触小程序开发、想拿一个完整项目练手的新手,照着跑一遍就能理解小程序从页面到接口的完整流程;另一类是有二次开发需求的前端或全栈开发者,想在这个 Demo 基础上改成自己业务里的投票模块,比如活动评选、意见收集、课程分组选择等。源码包里没有过度封装,目录结构清晰,改起来不会像拆黑匣子一样无从下手。
2. 项目结构拆解:从目录到页面,先看清骨架再动手
拿到一个压缩包,最忌讳的就是直接双击打开然后乱翻。我一般会先看目录层级,判断这个项目是「单页堆逻辑」还是「按功能分模块」。这份投票 Demo 的结构属于后者,虽然体量不大,但该分的目录都分了,对二次开发比较友好。
2.1 根目录与小程序配置文件
解压后你会看到类似下面的结构(不同版本可能略有差异,但核心文件不会少):
vote-demo/ ├── app.js ├── app.json ├── app.wxss ├── project.config.json ├── sitemap.json ├── pages/ │ ├── index/ │ ├── create/ │ ├── detail/ │ └── result/ ├── utils/ │ └── request.js └── images/app.json是小程序的全局配置,里面注册了所有页面路径,也决定了底部导航栏(tabBar)长什么样。这份 Demo 里通常会把「首页」和「创建投票」放进 tabBar,方便快速切换。project.config.json是开发者工具的配置,里面记录了 appid 占位、编译模式等信息。你导入项目时如果提示 appid 不合法,改成自己的测试号或者直接选「不使用云服务」的普通模式即可。
utils/request.js是我比较在意的一个文件。很多 Demo 会把wx.request直接写在页面里,改起来要满项目搜。这份源码把它抽了一层,统一处理请求头、baseUrl 和错误提示,二次开发时只需要改这一个文件就能切换后端地址。
2.2 页面目录与职责划分
pages下每个文件夹对应一个页面,通常包含四个文件:.wxml、.wxss、.js、.json。以create页面为例:
create.wxml负责表单结构:投票标题输入框、选项列表、添加选项按钮、发布时间选择器。create.js负责收集表单数据、校验必填项、调用创建接口。create.json一般只写navigationBarTitleText,用来设置页面标题。create.wxss处理表单样式,输入框间距、按钮圆角这些。
detail页面是投票详情页,用户在这里看到选项并点击投票。result页面展示票数统计,通常用进度条或百分比条来呈现。index是列表页,展示已发布的投票,点击进入详情。
这种「一个功能一个页面」的划分方式,好处是逻辑边界清楚。你要加一个「我参与的投票」页面,只需要在pages下新建目录,然后在app.json的pages数组里注册一行,不会影响到现有页面。
2.3 数据流与接口约定
小程序本身不提供数据库,所以这份 Demo 的投票数据一定依赖后端接口。源码里通常会在utils/request.js里定义一个baseUrl,比如http://localhost:3000/api,然后页面里调用类似request('/vote/create', 'POST', data)的方法。
接口大致分四类:
| 接口路径 | 方法 | 作用 | 关键参数 |
|---|---|---|---|
/vote/list | GET | 获取投票列表 | page, pageSize |
/vote/create | POST | 创建投票 | title, options, endTime |
/vote/detail | GET | 获取投票详情 | voteId |
/vote/submit | POST | 提交投票 | voteId, optionId |
如果你没有现成后端,可以用微信云开发的云函数替代,或者用 Mock 数据先把前端跑通。我一般会先用mockjs或者直接在request.js里写死返回数据,确认页面渲染没问题后再接真实接口。
提示:小程序正式环境要求请求域名必须备案并配置在后台白名单里,本地调试可以在开发者工具里勾选「不校验合法域名」。
3. 核心功能实现:创建投票与发布投票的代码链路
这一章是整份源码里最值得逐行看的部分。创建投票和发布投票看似简单,但涉及表单状态管理、选项动态增删、时间格式处理、接口提交后的页面跳转,每一环都有容易翻车的地方。
3.1 创建投票页的表单状态管理
先看create.js里数据初始化的部分:
Page({ data: { title: '', options: ['', ''], endTime: '', isSubmitting: false }, onTitleInput(e) { this.setData({ title: e.detail.value }); }, onOptionInput(e) { const index = e.currentTarget.dataset.index; const options = this.data.options; options[index] = e.detail.value; this.setData({ options }); }, addOption() { if (this.data.options.length >= 10) { wx.showToast({ title: '最多10个选项', icon: 'none' }); return; } this.setData({ options: [...this.data.options, ''] }); }, removeOption(e) { const index = e.currentTarget.dataset.index; if (this.data.options.length <= 2) { wx.showToast({ title: '至少保留2个选项', icon: 'none' }); return; } const options = this.data.options.filter((_, i) => i !== index); this.setData({ options }); } });这里有几个细节值得说。options初始给两个空字符串,保证用户一进页面就能看到两个输入框,而不是一个空白区域。addOption里做了上限判断,防止用户无限添加导致页面卡顿或后端字段超长。removeOption用filter而不是splice,是因为setData对数组的更新需要新引用才能触发视图刷新,直接改原数组有时候会出现「数据变了但页面没变」的玄学问题。
onOptionInput里用dataset.index来定位当前编辑的是第几个选项,这是小程序列表输入的常见做法。注意setData时传的是整个options数组,而不是options[index],因为小程序对路径更新的支持有限,整体替换更稳妥。
3.2 发布投票的校验与提交
表单填完后,点击「发布」按钮会触发提交逻辑:
submitVote() { const { title, options, endTime } = this.data; if (!title.trim()) { wx.showToast({ title: '请输入投票标题', icon: 'none' }); return; } const validOptions = options.filter(item => item.trim() !== ''); if (validOptions.length < 2) { wx.showToast({ title: '至少填写两个有效选项', icon: 'none' }); return; } if (this.data.isSubmitting) return; this.setData({ isSubmitting: true }); request('/vote/create', 'POST', { title: title.trim(), options: validOptions, endTime: endTime || null }).then(res => { wx.showToast({ title: '发布成功', icon: 'success' }); setTimeout(() => { wx.navigateTo({ url: `/pages/detail/detail?id=${res.data.voteId}` }); }, 800); }).catch(err => { wx.showToast({ title: err.message || '发布失败', icon: 'none' }); }).finally(() => { this.setData({ isSubmitting: false }); }); }这段代码里,isSubmitting是一个防重复提交的锁。用户手快连点两下,如果没有这个标记,可能会创建出两条一模一样的投票记录。validOptions过滤掉空选项,避免后端收到空字符串。提交成功后用setTimeout延迟跳转,是为了让showToast的提示能完整显示出来,否则页面一换提示就没了。
request方法来自utils/request.js,它内部封装了wx.request,统一拼接baseUrl、设置header、处理 HTTP 状态码。你如果换成云开发,这里可以改成wx.cloud.callFunction,页面逻辑基本不用动。
3.3 投票详情页的选项渲染与提交
detail.wxml里用wx:for渲染选项列表:
<view class="vote-detail"> <view class="vote-title">{{vote.title}}</view> <view class="option-item" wx:for="{{vote.options}}" wx:key="id">const total = options.reduce((sum, item) => sum + item.count, 0); const list = options.map(item => ({ ...item, percent: total === 0 ? 0 : Math.round((item.count / total) * 100) })); this.setData({ list, total });百分比用Math.round取整,避免出现33.33333%这种不美观的数字。进度条宽度直接用percent拼成style="width: {{item.percent}}%",简单直接。如果总票数为零,要单独处理,否则会出现除以零得到NaN的情况,页面上显示NaN%就很尴尬。
4. 跑起来之前:环境准备与常见报错排查
源码能不能跑起来,一半看代码,一半看环境。这一章把导入、配置、调试过程中最容易卡住的地方列出来,每条都按「现象 → 原因 → 解决」写,方便你对号入座。
4.1 开发者工具导入与 appid 配置
现象:导入项目后,开发者工具提示「appid 不合法」或「无法编译」。
原因:源码里的project.config.json通常填的是一个占位 appid,或者作者自己的测试号,你本地没有权限。
解决:打开project.config.json,把appid字段改成你自己的小程序测试号,或者直接在开发者工具导入时选择「测试号」。如果你只是本地跑通,不涉及真机预览,测试号完全够用。
4.2 接口请求失败与域名校验
现象:页面能打开,但列表为空,控制台报request:fail url not in domain list。
原因:小程序正式环境要求所有请求域名在后台配置白名单,本地开发时如果没勾选「不校验合法域名」,请求会被拦截。
解决:在开发者工具右上角「详情」→「本地设置」里勾选「不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书」。同时确认utils/request.js里的baseUrl指向的后端服务已经启动,端口没有被占用。
4.3 数据渲染为空或 setData 不生效
现象:接口返回了数据,但页面上选项列表是空的,或者修改数组后视图没更新。
原因:小程序setData对数组和对象的更新需要传新引用,直接修改原数组的某一项,视图层可能检测不到变化。
解决:统一用扩展运算符或concat、filter生成新数组再setData。比如this.setData({ options: [...this.data.options, newItem] }),而不是this.data.options.push(newItem)。
4.4 投票提交后重复记录
现象:用户快速点击「确认投票」,后端收到多条相同记录。
原因:没有做防重复提交处理,按钮点击后没有立即禁用。
解决:在data里加isSubmitting标记,提交前设为true,请求结束后在finally里设回false。同时按钮的disabled属性绑定这个标记,双保险。
4.5 时间格式与后端不一致
现象:创建投票时选择了截止时间,但后端收到的格式不对,或者详情页显示Invalid Date。
原因:小程序picker组件返回的是2025-01-01这样的字符串,后端可能期望时间戳或ISO 8601格式。
解决:在提交前统一转换。如果后端要时间戳,用new Date(endTime).getTime();如果要完整 ISO 格式,用new Date(endTime).toISOString()。转换逻辑放在create.js的submitVote里,不要散落在多个页面。
注意:真机调试时,本地
localhost接口是访问不到的,需要用局域网 IP 或者部署到可访问的测试服务器上。
5. 二次开发怎么改:从 Demo 到可用投票模块的几条路径
这份源码最大的价值不是它现在能做什么,而是它留出了哪些扩展点。你拿到手之后,大概率不会直接上线,而是要根据自己的业务改几处。下面是我在类似项目里常用的改造思路,按改动量从小到大排。
5.1 替换数据源:从本地接口到云开发
如果你不想维护后端服务器,最省事的改造是把utils/request.js里的wx.request换成微信云开发的数据库调用。具体做法是:在app.js的onLaunch里初始化云环境wx.cloud.init({ env: '你的环境ID' }),然后把创建投票的逻辑改成wx.cloud.database().collection('votes').add({ data: {...} })。查询列表用.get(),提交投票用.doc(voteId).update()配合原子操作_.inc(1)来增加票数。
这样改完之后,你不需要自己买服务器、配域名、处理 HTTPS 证书,对于小型投票场景足够用。代价是云开发的免费额度有限,投票量特别大的时候要留意配额。
5.2 增加投票类型:单选改多选
现在的 Demo 是单选投票,selectedId是一个字符串。要改成多选,需要把selectedId换成selectedIds数组,点击选项时判断是否已选,已选则移除,未选则追加。提交接口的optionId也要改成optionIds数组。后端对应的存储结构从「一条记录一个选项」改成「一条记录多个选项」,统计票数时要注意去重逻辑。
这个改动涉及前端交互、接口参数、后端存储三处,建议先在纸上把数据流画清楚再动手,否则很容易出现「前端传了数组但后端还在按字符串解析」的翻车现场。
5.3 增加登录与权限控制
Demo 里通常没有用户体系,任何人都能创建和投票。要防止刷票,至少需要拿到用户的openid。小程序里可以通过wx.login获取code,传给后端换openid,然后在投票记录里存一份,同一个openid对同一个投票只能投一次。
前端需要在app.js里做一次静默登录,把openid存在globalData里。创建投票时带上openid作为创建者标识,投票时带上openid做去重判断。这部分改动不大,但能挡住大部分重复投票的情况。
5.4 样式与交互的定制化
app.wxss里通常定义了一些全局样式变量,比如主色调、圆角大小、按钮高度。你要换成自己品牌的视觉风格,改这里比逐个页面改.wxss高效得多。我一般会把颜色、间距、字号抽成 CSS 变量放在app.wxss的page选择器下,页面里用var(--primary-color)引用。这样换主题只需要改一处。
另外,投票选项的选中态、结果页的进度条动画,这些交互细节可以根据业务场景调整。比如活动评选类投票,结果页可以加一个「实时更新」的轮询;意见收集类投票,结果页可以默认隐藏票数,只给管理员看。
5.5 一个容易忽略的细节:投票截止后的状态处理
Demo 里可能只做了「创建时设置截止时间」,但没有处理「截止后禁止投票」的逻辑。你需要在详情页的onLoad里判断当前时间是否超过endTime,如果已过期,把选项区域替换成「投票已结束」的提示,同时禁用提交按钮。后端接口也要做同样的校验,防止有人绕过前端直接调接口。
这个逻辑不复杂,但漏掉的话,用户会在一个已经结束的投票里继续点选,体验很差。我一般会在utils里加一个isVoteExpired(endTime)的工具函数,前端后端各调一次,保证一致性。
从那以后我每次拿到类似的小程序 Demo,都会先跑通主流程,再按「数据源 → 用户体系 → 业务规则 → 样式」的顺序逐层改造,而不是一上来就大改页面结构。希望这份拆解能帮你少走点弯路,顺利把投票功能跑起来、改下去。
本文还有配套的精品资源,点击获取