├── cloudfunctions ├── vote_exe │ ├── config.json │ ├── package.json │ └── index.js ├── vote_init │ ├── config.json │ ├── package.json │ └── index.js └── data.json ├── miniprogram ├── pages │ └── vote │ │ ├── vote.json │ │ ├── vote.wxml │ │ ├── vote.wxss │ │ └── vote.js ├── app.js ├── sitemap.json └── app.json ├── readme.md ├── webview └── index.html └── project.config.json /cloudfunctions/vote_exe/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "permissions": { 3 | "openapi": [ 4 | ] 5 | } 6 | } -------------------------------------------------------------------------------- /cloudfunctions/vote_init/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "permissions": { 3 | "openapi": [ 4 | ] 5 | } 6 | } -------------------------------------------------------------------------------- /miniprogram/pages/vote/vote.json: -------------------------------------------------------------------------------- 1 | { 2 | "usingComponents": {}, 3 | "navigationBarTitleText": "云开发投票" 4 | } -------------------------------------------------------------------------------- /miniprogram/app.js: -------------------------------------------------------------------------------- 1 | // app.js 2 | App({ 3 | onLaunch: function () { 4 | wx.cloud.init({ 5 | // env: '云开发环境ID', 6 | traceUser: true 7 | }) 8 | } 9 | }) 10 | -------------------------------------------------------------------------------- /miniprogram/sitemap.json: -------------------------------------------------------------------------------- 1 | { 2 | "desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html", 3 | "rules": [{ 4 | "action": "allow", 5 | "page": "*" 6 | }] 7 | } -------------------------------------------------------------------------------- /cloudfunctions/vote_exe/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vote_exe", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "wx-server-sdk": "~2.3.2" 13 | } 14 | } -------------------------------------------------------------------------------- /cloudfunctions/vote_init/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vote_init", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "wx-server-sdk": "~2.3.2" 13 | } 14 | } -------------------------------------------------------------------------------- /cloudfunctions/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "b45a21d55ff30a39039278534bfe346e", 3 | "open": true, 4 | "options": [ 5 | "小白:从来没有接触", 6 | "入门:正在学习和尝试DEMO", 7 | "了解:懂一些使用,敢于尝试小项目", 8 | "掌握:具备开发线上项目的实力", 9 | "熟悉:专业踩坑达人,多个线上项目" 10 | ], 11 | "title": "你的云开发使用经验如何?", 12 | "code": "SIN", 13 | "one": false 14 | } -------------------------------------------------------------------------------- /miniprogram/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "pages": [ 3 | "pages/vote/vote" 4 | ], 5 | "window": { 6 | "backgroundColor": "#FFFFFF", 7 | "backgroundTextStyle": "light", 8 | "navigationBarBackgroundColor": "#5586ff", 9 | "navigationBarTitleText": "云开发投票", 10 | "navigationBarTextStyle": "white" 11 | }, 12 | "style": "v2", 13 | "sitemapLocation": "sitemap.json" 14 | } -------------------------------------------------------------------------------- /cloudfunctions/vote_init/index.js: -------------------------------------------------------------------------------- 1 | const cloud = require('wx-server-sdk') 2 | cloud.init({ 3 | env: cloud.DYNAMIC_CURRENT_ENV 4 | }) 5 | const db = cloud.database() 6 | 7 | exports.main = async (event, context) => { 8 | const openid = event.userInfo.openId; 9 | const result = (await db.collection('vote_user').where({ 10 | openid:openid 11 | }).get()).data; 12 | if(result.length!=0){ 13 | const code = event.code; 14 | let project = result[0].project[code]; 15 | return project?project:{} 16 | } 17 | else{ 18 | await db.collection('vote_user').add({ 19 | data:{ 20 | openid:openid, 21 | project:{} 22 | } 23 | }) 24 | return {} 25 | } 26 | } -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # 云开发投票小程序 2 | 3 | ## 项目介绍 4 | 5 | 云开发B站趣味项目直播【第1期】完整项目。 6 | - 投票小程序,可以进行单选投票,投票者可以实时查看各个选项的投票数; 7 | - 支持在线变更投票内容,自动更新到所有投票者; 8 | - 支持仅投一次,防止多次投票变更 9 | - 支持项目在线开启和关闭,自动更新到所有投票者; 10 | - 项目理论可以支撑多选投票类型【暂为实现】 11 | - 极简项目,无任何框架纯原生小程序、云开发 12 | - 极简不失典型,各种高效数据库的操作、云函数操作 13 | 14 | ## 项目来源 15 | 16 | 2021年1月6日,B站直播前,由于预定的投票项目太老旧,不能支撑趣味直播。提前花2小时拼出来一个前端虚拟化的投票小程序 17 | 直播时现场构建数据模型,对本地小程序进行改造,全面接入云开发。纯手工打造,热乎! 18 | 19 | 数据设计可以支撑CMS系统的使用。 20 | 21 | ## 部署步骤 22 | 23 | 1. 打开 app.js,替换 env 为自己的云开发环境ID,如果未开通则先开通云开发 24 | 2. 进入控制台,创建 vote_mess 和 vote_user 数据库集合,vote_mess 集合需要变更权限为所有人可读。 25 | 3. 进入 vote_mess 集合,导入 cloudfunctions 中的 data.json 数据。 26 | 3. 将cloudfunctions文件夹中两个云函数上传部署(云端安装)到云开发中,注意选择在第一步中的云开发环境上传。 27 | 4. 在开发者工具中选择编译模式为【有效进入】,即可开始体验之旅 28 | 29 | ## 作者 30 | 31 | - 腾讯云·云开发团队:zira -------------------------------------------------------------------------------- /miniprogram/pages/vote/vote.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{info}} 5 | 关闭小程序 6 | 7 | 8 | 9 | {{project.title}} 10 | 11 | 17 | 18 | 19 | 20 | 21 | 22 | function hidden(user){ 23 | return JSON.stringify(user)=="{}"; 24 | } 25 | module.exports = {hidden:hidden} 26 | -------------------------------------------------------------------------------- /cloudfunctions/vote_exe/index.js: -------------------------------------------------------------------------------- 1 | const cloud = require('wx-server-sdk') 2 | cloud.init({ 3 | env: cloud.DYNAMIC_CURRENT_ENV 4 | }) 5 | const db = cloud.database() 6 | const _ = db.command 7 | 8 | exports.main = async (event, context) => { 9 | const openid = event.userInfo.openId; 10 | const result = (await db.collection('vote_user').where({ 11 | openid:openid 12 | }).get()).data; 13 | if(result.length!=0){ 14 | const code = event.code; 15 | let project = result[0].project[code]; 16 | let newData = event.select; 17 | let diff = {} 18 | for(let i in newData){ 19 | diff[i] = _.inc(1) 20 | } 21 | for(let i in project){ 22 | if(diff[i]) delete diff[i]; 23 | else diff[i] = _.inc(-1) 24 | } 25 | if(Object.keys(diff).length==0){ 26 | return newData; 27 | } 28 | await db.collection('vote_mess').where({ 29 | code:code 30 | }).update({ 31 | data:{ 32 | number:diff 33 | } 34 | }) 35 | await db.collection('vote_user').where({ 36 | openid:openid 37 | }).update({ 38 | data:{ 39 | project:{ 40 | [event.code]:_.set(newData) 41 | } 42 | } 43 | }) 44 | 45 | return newData 46 | } 47 | } -------------------------------------------------------------------------------- /webview/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 投票实时展示 6 | 7 | 8 | 9 | 10 | 11 |
12 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /project.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "miniprogramRoot": "miniprogram/", 3 | "cloudfunctionRoot": "cloudfunctions/", 4 | "setting": { 5 | "urlCheck": true, 6 | "es6": true, 7 | "enhance": true, 8 | "postcss": true, 9 | "preloadBackgroundData": false, 10 | "minified": true, 11 | "newFeature": true, 12 | "coverView": true, 13 | "nodeModules": false, 14 | "autoAudits": false, 15 | "showShadowRootInWxmlPanel": true, 16 | "scopeDataCheck": false, 17 | "uglifyFileName": false, 18 | "checkInvalidKey": true, 19 | "checkSiteMap": true, 20 | "uploadWithSourceMap": true, 21 | "compileHotReLoad": false, 22 | "useMultiFrameRuntime": true, 23 | "useApiHook": true, 24 | "useApiHostProcess": true, 25 | "babelSetting": { 26 | "ignore": [], 27 | "disablePlugins": [], 28 | "outputPath": "" 29 | }, 30 | "enableEngineNative": false, 31 | "bundle": false, 32 | "useIsolateContext": true, 33 | "useCompilerModule": true, 34 | "userConfirmedUseCompilerModuleSwitch": false, 35 | "userConfirmedBundleSwitch": false, 36 | "packNpmManually": false, 37 | "packNpmRelationList": [], 38 | "minifyWXSS": true 39 | }, 40 | "appid": "", 41 | "projectname": "云开发投票小程序", 42 | "libVersion": "2.8.1", 43 | "condition": { 44 | "plugin": { 45 | "list": [] 46 | }, 47 | "game": { 48 | "list": [] 49 | }, 50 | "gamePlugin": { 51 | "list": [] 52 | }, 53 | "miniprogram": { 54 | "list": [ 55 | { 56 | "name": "有效进入", 57 | "pathName": "pages/vote/vote", 58 | "query": "code=SIN", 59 | "scene": null 60 | }, 61 | { 62 | "name": "无效进入", 63 | "pathName": "pages/vote/vote", 64 | "query": "code=SINA", 65 | "scene": null 66 | } 67 | ] 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /miniprogram/pages/vote/vote.wxss: -------------------------------------------------------------------------------- 1 | page{ 2 | display: flex; 3 | background:#F3F4F8; 4 | width: 100%; 5 | height: 100%; 6 | } 7 | .container{ 8 | display: flex; 9 | flex-direction: column; 10 | width: 100%; 11 | height: 100%; 12 | } 13 | .title { 14 | font-size:40rpx; 15 | font-weight: 900; 16 | width: calc(100% - 60rpx); 17 | padding: 80rpx 30rpx 40rpx; 18 | } 19 | .options { 20 | width: 100%; 21 | padding: 30rpx 0; 22 | display: flex; 23 | flex-direction: column; 24 | } 25 | .options button { 26 | display: flex; 27 | width: 100% !important; 28 | margin-bottom: 20rpx; 29 | background: #FFFFFF; 30 | border-radius: 0 !important; 31 | text-align: left; 32 | font-size: 27rpx; 33 | font-weight: 500; 34 | line-height: 70rpx; 35 | padding:15rpx 30rpx; 36 | height: 100rpx; 37 | } 38 | .options button:active{ 39 | background:#ececec; 40 | color:inherit; 41 | } 42 | .options button[type='select']{ 43 | background:#c5d6ff; 44 | } 45 | .options button[type='loading']{ 46 | background:#ffd2d2; 47 | } 48 | .options button text { 49 | width: 80%; 50 | } 51 | .options button span { 52 | width: 20%; 53 | text-align: right; 54 | font-size: 25rpx; 55 | color: #545454; 56 | } 57 | 58 | 59 | 60 | .spinner { 61 | width: 200rpx; 62 | height: 200rpx; 63 | margin: auto; 64 | background-color: #5586ff; 65 | border-radius: 100%; 66 | -webkit-animation: scaleout 1.0s infinite ease-in-out; 67 | animation: scaleout 1.0s infinite ease-in-out; 68 | } 69 | @-webkit-keyframes scaleout { 70 | 0% { -webkit-transform: scale(0.0) } 71 | 100% { 72 | -webkit-transform: scale(1.0); 73 | opacity: 0; 74 | } 75 | } 76 | @keyframes scaleout { 77 | 0% { 78 | transform: scale(0.0); 79 | -webkit-transform: scale(0.0); 80 | } 100% { 81 | transform: scale(1.0); 82 | -webkit-transform: scale(1.0); 83 | opacity: 0; 84 | } 85 | } 86 | 87 | 88 | .showinfo{ 89 | width: 80%; 90 | margin: 30% auto; 91 | text-align: center; 92 | } 93 | .showinfo text { 94 | font-size:30rpx; 95 | color:#343434; 96 | line-height: 50rpx; 97 | letter-spacing: 3rpx; 98 | } 99 | .showinfo navigator { 100 | height: 85rpx; 101 | width: 400rpx; 102 | margin: 80rpx auto; 103 | background: #5586ff; 104 | color:#FFFFFF; 105 | line-height: 85rpx; 106 | font-size: 32rpx; 107 | } -------------------------------------------------------------------------------- /miniprogram/pages/vote/vote.js: -------------------------------------------------------------------------------- 1 | // 项目,通过实时 2 | // 用户,调用云函数 3 | // 投票,调用云函数,自己和项目票数自增 4 | 5 | // JS-SDK : 未登录,匿名登录,邮件登录,手机登录 6 | // WEB-SDK: 未登录、微信登录(服务号) 7 | 8 | //=================虚拟数据源==================== 9 | var tempdata = { 10 | SIN: { 11 | title: "你的云开发使用经验如何?", //投票标题 12 | options: [ //投票选项 13 | "小白:从来没有接触", 14 | "入门:正在学习和尝试DEMO", 15 | "了解:懂一些使用,敢于尝试小项目", 16 | "掌握:具备开发线上项目的实力", 17 | "熟悉:专业踩坑达人,多个线上项目" 18 | ], 19 | code: "SIN", //进入路径 20 | open: true, //允许投票 21 | one: false, //只允许一次 22 | number: { //投票数 23 | 0:32, 24 | 1:56, 25 | 2:34, 26 | 3:78, 27 | 4:40 28 | } 29 | } 30 | } 31 | //=================页面逻辑区==================== 32 | var that = null 33 | 34 | Page({ 35 | data: {}, 36 | onLoad(options) { 37 | that = this; 38 | if (options.code != null) { 39 | that.code = options.code; 40 | that.init_project() 41 | } else { 42 | setInfo('T_OPEN') 43 | } 44 | }, 45 | init_project() { 46 | that.watch = wx.cloud.database().collection('vote_mess').where({ 47 | code:that.code 48 | }).watch({ 49 | onChange(res){ 50 | if (res.docs.length != 0) { 51 | let result = res.docs[0]; 52 | setProject(result) 53 | if (that.data.user == null || result.number == null) { 54 | that.init_user() 55 | } 56 | } else { 57 | setInfo('T_OPEN') 58 | } 59 | }, 60 | onError(err){ 61 | setInfo('T_NET') 62 | } 63 | }) 64 | }, 65 | init_user() { 66 | netCall({ 67 | name:'vote_init', 68 | data:{ 69 | code:that.code 70 | }, 71 | success(res){ 72 | setUser(res.result) 73 | that.one = JSON.stringify(res.result) 74 | } 75 | }) 76 | }, 77 | vote(e) { 78 | if (that.data.project.open != true) { 79 | showModel('S_CLOSE') 80 | return; 81 | } 82 | if (that.data.user[e.currentTarget.dataset.i] == true) { 83 | return; 84 | } 85 | if (that.data.project.one && that.one != '{}') { 86 | showModel('S_ONE') 87 | return; 88 | } 89 | let tempvote = {} 90 | if(e.currentTarget.dataset.mul){ 91 | for(let i in that.data.user){ 92 | if(that.data.user[i]==true){ 93 | tempvote[i] = true 94 | } 95 | } 96 | if(Object.keys(tempvote).length==0){ 97 | showModel('S_EMPTY') 98 | return; 99 | } 100 | } 101 | else{ 102 | tempvote[e.currentTarget.dataset.i] = true 103 | } 104 | that.setData({ 105 | load: tempvote 106 | }) 107 | netCall({ 108 | name:'vote_exe', 109 | data:{ 110 | select:tempvote, 111 | code:that.code 112 | }, 113 | success(res){ 114 | setUser(res.result) 115 | that.one = JSON.stringify(res.result) 116 | } 117 | }) 118 | }, 119 | select(e) { 120 | let key = 'user.'+e.currentTarget.dataset.i 121 | that.setData({ 122 | [key]:!that.data.user[e.currentTarget.dataset.i] 123 | }) 124 | }, 125 | onShareAppMessage() { 126 | return { 127 | title: `投票-${that.data.project.title}`, 128 | path: `/pages/vote/vote?code=${that.code}` 129 | } 130 | } 131 | }) 132 | //=================功能封装区==================== 133 | const INFO = { 134 | T_NET: '网络服务出现异常,请稍后再试\n如有问题请联系管理员处理', 135 | T_OPEN: '无法找到对应的投票项目,请重新尝试\n如有问题请联系管理员处理', 136 | S_ONE: ['项目设置只能投票一次', '提示'], 137 | S_CLOSE: ['当前不能投票,请等待投票开启', '提示'], 138 | S_LOAD: ['请等待本次投票操作完毕后再变更选择', '提示'], 139 | S_FAIL: ['在操作时遇到了一些网络问题,请稍后再试', '网络错误'], 140 | S_EMPTY: ['多选不能为空,请至少选择一个','提示'] 141 | } 142 | function setInfo(info) { 143 | that.setData({ 144 | info: INFO[info] 145 | }) 146 | } 147 | function setUser(user) { 148 | that.setData({ 149 | user, 150 | load: null 151 | }) 152 | } 153 | function setProject(project) { 154 | that.setData({ 155 | project 156 | }) 157 | } 158 | function setLoad(load) { 159 | that.setData({ 160 | load 161 | }) 162 | } 163 | function showModel(info) { 164 | wx.showModal({ 165 | title: INFO[info][1], 166 | content: INFO[info][0], 167 | showCancel: false 168 | }) 169 | } 170 | function netCall(obj) { 171 | if (that.netload != true) { 172 | that.netload = true; 173 | wx.cloud.callFunction({ 174 | name: obj.name, 175 | data: (obj.data ? obj.data : {}), 176 | success: (res) => { 177 | typeof obj.success == "function" ? obj.success(res) : null 178 | }, 179 | fail: (err) => { 180 | showModel('S_FAIL') 181 | console.log(err) 182 | typeof obj.fail == "function" ? obj.fail(err) : null 183 | }, 184 | complete() { 185 | that.netload = false; 186 | } 187 | }) 188 | } else { 189 | showModel('S_LOAD') 190 | } 191 | } --------------------------------------------------------------------------------