├── .gitignore ├── LICENSE ├── README.md ├── app ├── app.js ├── app.json ├── app.wxss ├── config.js ├── images │ ├── loading.gif │ ├── logo.png │ ├── play.png │ └── qr.png ├── lib │ ├── api.js │ ├── request.js │ └── util.js └── pages │ ├── detail │ ├── detail.js │ ├── detail.wxml │ └── detail.wxss │ ├── index │ ├── index.js │ ├── index.wxml │ └── index.wxss │ └── video │ ├── video.js │ ├── video.wxml │ └── video.wxss └── server ├── app.js ├── common └── routerbase.js ├── config.js ├── globals.js ├── middlewares └── route_dispatcher.js ├── package.json ├── process.json └── routes ├── index.js └── video ├── handlers ├── comment.js ├── commentlist.js └── list.js └── routehub.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | LICENSE - "MIT License" 2 | 3 | Copyright (c) 2016 by Tencent Cloud 4 | 5 | Permission is hereby granted, free of charge, to any person 6 | obtaining a copy of this software and associated documentation 7 | files (the "Software"), to deal in the Software without 8 | restriction, including without limitation the rights to use, 9 | copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the 11 | Software is furnished to do so, subject to the following 12 | conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 19 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 22 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 24 | OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 微信小程序示例 - 新片预告 2 | 3 | 新片预告是结合腾讯云[点播 VOD](https://www.qcloud.com/product/vod.html?utm_source=jiaocheng&utm_medium=vod-introduction&utm_campaign=qcloud)和[云数据库 MySQL](https://www.qcloud.com/product/cdb.html?utm_source=jiaocheng&utm_medium=cdb-introduction&utm_campaign=qcloud)制作的一个微信小程序示例。在代码结构上包含如下两部分: 4 | 5 | - `app`: 新片预告应用包代码,可直接在微信开发者工具中作为项目打开 6 | - `server`: 搭建的Node服务端代码,作为服务器和`app`通信,提供 CGI 接口示例用于拉取云数据库上的视频列表、评论列表,将评论数据提交到云数据库 7 | 8 | 新片预告主要功能如下: 9 | * 支持分页滚动加载视频列表 10 | * 点击海报跳转至详情页播放视频 11 | * 对视频进行评论 12 | * 展示视频的评论列表 13 | 14 | ![运行截图](https://share-10039692.file.myqcloud.com/app4.png) 15 | 16 | 17 | ## 部署和运行 18 | 19 | 拿到了本小程序源码的朋友可以尝试自己运行起来。 20 | 21 | ### 整体架构 22 | 23 | ![整体架构](https://share-10039692.file.myqcloud.com/app3.png) 24 | 25 | ### 1. 准备域名和证书 26 | 27 | 在微信小程序中,所有的网路请求受到严格限制,不满足条件的域名和协议无法请求,具体包括: 28 | 29 | * 只允许和在 MP 中配置好的域名进行通信,如果还没有域名,需要注册一个。 30 | * 网络请求必须走 HTTPS 协议,所以你还需要为你的域名申请一个 SSL 证书。 31 | 32 | > 腾讯云提供[域名注册](https://www.qcloud.com/product/dm.html?utm_source=jiaocheng&utm_medium=domain2&utm_campaign=qcloud)和[证书申请](https://console.qcloud.com/ssl?utm_source=jiaocheng&utm_medium=ssl2&utm_campaign=qcloud)服务,还没有域名或者证书的可以去使用 33 | 34 | 域名注册好之后,可以登录[微信公众平台](https://mp.weixin.qq.com)配置通信域名了。 35 | 36 | ![配置通信域名](https://easyimage-10028115.file.myqcloud.com/internal/tjzpgjrz.y5a.jpg) 37 | 38 | 注意:需要将 `www.qcloud.la` 设置为上面申请的域名 39 | 40 | ### 2. Nginx 和 Node 代码部署 41 | 42 | 小程序服务要运行,需要进行以下几步: 43 | 44 | * 部署 Nginx,Nginx 的安装和部署请大家自行搜索(注意需要把 SSL 模块也编译进去) 45 | * 配置 Nginx 反向代理到 `http://127.0.0.1:9994` 46 | * Node 运行环境,可以安装 [Node V6.6.0](https://nodejs.org/) 47 | * 部署 `server` 目录的代码到服务器上,如 `/data/release/qcloud-applet-video` 48 | * 使用 `npm install` 安装依赖模块 49 | * 使用 `npm install pm2 -g` 安装 pm2 50 | 51 | > 上述环境配置比较麻烦,新片预告的服务器运行代码和配置已经打包成[腾讯云 CVM 镜像](https://buy.qcloud.com/cvm?marketImgId=371?utm_source=jiaocheng&utm_medium=cvm2&utm_campaign=qcloud),推荐大家直接使用。 52 | > * 镜像部署完成之后,云主机上就有运行 WebSocket 服务的基本环境、代码和配置了。 53 | > * 腾讯云用户可以[免费领取礼包](https://www.qcloud.com/act/event/yingyonghao.html#section-voucher),体验腾讯云小程序解决方案。 54 | > * 镜像已包含所有小程序的服务器环境与代码,需要体验小程序的朋友无需重复部署 55 | 56 | ### 3. 配置 HTTPS 57 | 58 | 镜像中已经部署了 nginx,需要在 `/etc/nginx/conf.d` 下修改配置中的域名、证书、私钥。 59 | 60 | ![证书 Nginx 配置](https://easyimage-10028115.file.myqcloud.com/internal/agfty0fn.gfi.jpg) 61 | 62 | 63 | 配置完成后,即可启动 nginx。 64 | 65 | ```sh 66 | nginx 67 | ``` 68 | 69 | ### 4. 域名解析 70 | 71 | 我们还需要添加域名记录解析到我们的云服务器上,这样才可以使用域名进行 HTTPS 服务。 72 | 73 | 在腾讯云注册的域名,可以直接使用[云解析控制台](https://console.qcloud.com/cns/domains?utm_source=jiaocheng&utm_medium=cns&utm_campaign=qcloud)来添加主机记录,直接选择上面购买的 CVM。 74 | 75 | ![添加域名解析](https://easyimage-10028115.file.myqcloud.com/internal/uw25hdj2.k1u.jpg) 76 | 77 | 解析生效后,我们在浏览器使用域名就可以进行 HTTPS 访问。 78 | 79 | ![HTTPS 访问效果图](https://easyimage-10028115.file.myqcloud.com/internal/bxfkmjea.g41.jpg) 80 | 81 | ### 5. 开通 点播服务 82 | 83 | 新片预告示例的播放资源是存储在 腾讯云点播 上的mp4文件,要使用 点播 服务,需要登录 [点播 管理控制台](http://console.qcloud.com/video?utm_source=jiaocheng&utm_medium=vod-console&utm_campaign=qcloud),然后在其中完成以下操作: 84 | 85 | - 上传视频资源,点播几乎支持所有主流的[视频格式](https://www.qcloud.com/doc/product/266/2846)上传 86 | - 转码成功后获取mp4或m3u8源地址 87 | 88 | ![上传转码](https://share-10039692.file.myqcloud.com/app5.png) 89 | 90 | > 目前微信小程序`video`组件经测试支持`mp4`和`m3u8`格式,其中 m3u8 格式只能在手机上使用,开发者可以使用腾讯云点播控制台将视频源转码成 mp4 或 m3u8 格式,并且腾讯云点播会对播放的资源进行CDN加速。 91 | 92 | ### 6. 准备 云数据库MySQL 93 | 示例中拉取的视频和评论列表都是存储在 云数据库 上,要使用 [云数据库](https://www.qcloud.com/product/cdb.html?utm_source=jiaocheng&utm_medium=cdb-introduction&utm_campaign=qcloud) 服务需要完成以下操作 94 | 95 | - [购买](https://buy.qcloud.com/cdb?utm_source=jiaocheng&utm_medium=cdb-purchase&utm_campaign=qcloud),注意购买的云数据库需要与云服务器同在一个地域分区 96 | - [初始化流程](https://www.qcloud.com/doc/product/236/3128),本示例选用的是`utf8`编码 97 | - 点击[云数据库 控制台](https://console.qcloud.com/cdb?utm_source=jiaocheng&utm_medium=cdb-console&utm_campaign=qcloud)操作栏的`登录`按钮,登录到phpMyAdmin`创建数据库`并在当前数据库中导入本示例中的[SQL文件](https://share-10039692.file.myqcloud.com/wechat_app.sql) 98 | 99 | > 注意:导入SQL文件中包含了 点播 上传的视频列表,开发者可以基于云数据库自行开发维护一个视频发布管理系统,因为此内容跟本示例暂不相关,所以不再详述。 100 | 101 | ### 7. 启动新片预告示例 Node 服务 102 | 103 | 在镜像中,新片预告示例的 Node 服务代码已部署在目录`/data/release/qcloud-applet-video`下: 104 | 105 | 进入该目录: 106 | 107 | ```bash 108 | cd /data/release/qcloud-applet-video 109 | ``` 110 | 111 | 在该目录下有个名为`config.js`的配置文件(如下所示),按注释修改对应的 MySQL 配置: 112 | 113 | ```js 114 | module.exports = { 115 | // Node 监听的端口号 116 | port: '9994', 117 | ROUTE_BASE_PATH: '/applet', 118 | 119 | host: '填写开通 MySQL 时分配的内网IP', 120 | user: '填写MySQL用户名', 121 | password: '填写MySQL密码', 122 | database: '填写上一步中创建的MySQL数据名', 123 | }; 124 | ``` 125 | 126 | 示例使用`pm2`管理 Node 进程,执行以下命令启动 node 服务: 127 | 128 | ```bash 129 | pm2 start process.json 130 | ``` 131 | 132 | ### 8. 启动新片预告 Demo 133 | 134 | 在微信开发者工具将新片预告应用包源码添加为项目,并把源文件`config.js`中的通讯域名修改成上面申请的域名。 135 | 136 | ![修改配置文件](https://share-10039692.file.myqcloud.com/app1.png) 137 | 138 | 然后点击调试即可打开新片预告Demo开始体验。 139 | 140 | ![调试](https://share-10039692.file.myqcloud.com/app2.png) 141 | 142 | 143 | ## 主要功能实现 144 | 145 | ### 获取视频列表、展示评论、提交评论 146 | 通过node的mysql模块连接mysql,进行查询,插入操作 147 | 以下是查询评论列表的示例代码 148 | 149 | ```js 150 | const mysql = require('mysql'); 151 | const config = require('../../../config'); 152 | 153 | let vid = this.req.query.vid; 154 | if (!vid) { 155 | this.res.json({ code: -1, msg: 'failed', data: {} }); 156 | return; 157 | } 158 | 159 | //CDB Mysql配置 160 | let connection = mysql.createConnection({ 161 | host: config.host, 162 | password: config.password, 163 | user: config.user, 164 | database: config.database 165 | }); 166 | 167 | //开启数据库连接 168 | connection.connect((err) => { 169 | if (err) { 170 | this.res.json({ code: -1, msg: 'failed', data: {} }); 171 | } 172 | }); 173 | 174 | //查询列表 175 | connection.query('SELECT * from comment where vid = ? order by id desc', [vid], (err, result) => { 176 | if (err) { 177 | this.res.json({ code: -1, msg: 'failed', data: {} }); 178 | return; 179 | } 180 | 181 | this.res.json({ 182 | code: 0, 183 | msg: 'ok', 184 | data: result, 185 | }); 186 | }); 187 | 188 | //查询完后关闭连接 189 | connection.end(); 190 | ``` 191 | 192 | ### 播放视频 193 | ```js 194 | 195 | ``` 196 | 197 | | 属性名 | 类型 | 说明 | 198 | | :------: | :------: | :------------: | 199 | | src | String | 要播放视频的资源地址 | 200 | | binderror| EventHandle | 当发生错误时触发error事件,event.detail = {errMsg: 'something wrong'} | 201 | 202 | 播放视频使用的是video标签,目前官方文档上只给出了两个参数说明,笔者测试了src支持加载`mp4`和`m3u8`格式视频, 203 | video标签的控制条暂时没办法自定义样式以及隐藏 204 | 205 | ## LICENSE 206 | 207 | [MIT](LICENSE) 208 | -------------------------------------------------------------------------------- /app/app.js: -------------------------------------------------------------------------------- 1 | App({ 2 | 3 | }); -------------------------------------------------------------------------------- /app/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "pages": [ 3 | "pages/index/index", 4 | "pages/video/video", 5 | "pages/detail/detail" 6 | ], 7 | 8 | "window": { 9 | "backgroundTextStyle": "dark", 10 | "navigationBarBackgroundColor": "#202020", 11 | "navigationBarTitleText": "新片预告", 12 | "windowBackground": "white", 13 | "navigationBarTextStyle": "white" 14 | } 15 | } -------------------------------------------------------------------------------- /app/app.wxss: -------------------------------------------------------------------------------- 1 | page { 2 | background-color: #1c1b16; 3 | } -------------------------------------------------------------------------------- /app/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | /** 通讯域名 */ 3 | host: 'www.qcloud.la', 4 | basePath: '/applet/video', 5 | }; -------------------------------------------------------------------------------- /app/images/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CFETeam/weapp-demo-video/da6180572efd6f4c4e32b2213dd96a39574bb6d5/app/images/loading.gif -------------------------------------------------------------------------------- /app/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CFETeam/weapp-demo-video/da6180572efd6f4c4e32b2213dd96a39574bb6d5/app/images/logo.png -------------------------------------------------------------------------------- /app/images/play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CFETeam/weapp-demo-video/da6180572efd6f4c4e32b2213dd96a39574bb6d5/app/images/play.png -------------------------------------------------------------------------------- /app/images/qr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CFETeam/weapp-demo-video/da6180572efd6f4c4e32b2213dd96a39574bb6d5/app/images/qr.png -------------------------------------------------------------------------------- /app/lib/api.js: -------------------------------------------------------------------------------- 1 | var config = require('../config.js'); 2 | 3 | module.exports = { 4 | getUrl(route) { 5 | return `https://${config.host}${config.basePath}${route}`; 6 | }, 7 | }; -------------------------------------------------------------------------------- /app/lib/request.js: -------------------------------------------------------------------------------- 1 | module.exports = (options) => { 2 | return new Promise((resolve, reject) => { 3 | options = Object.assign(options, { 4 | success(result) { 5 | if (result.statusCode === 200) { 6 | resolve(result.data); 7 | } else { 8 | reject(result); 9 | } 10 | }, 11 | 12 | fail: reject, 13 | }); 14 | 15 | wx.request(options); 16 | }); 17 | }; -------------------------------------------------------------------------------- /app/lib/util.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // 一维数组转二维数组 3 | listToMatrix(list, elementsPerSubArray) { 4 | let matrix = [], i, k; 5 | 6 | for (i = 0, k = -1; i < list.length; i += 1) { 7 | if (i % elementsPerSubArray === 0) { 8 | k += 1; 9 | matrix[k] = []; 10 | } 11 | 12 | matrix[k].push(list[i]); 13 | } 14 | 15 | return matrix; 16 | }, 17 | 18 | // 为promise设置简单回调(无论成功或失败都执行) 19 | always(promise, callback) { 20 | promise.then(callback, callback); 21 | return promise; 22 | }, 23 | }; -------------------------------------------------------------------------------- /app/pages/detail/detail.js: -------------------------------------------------------------------------------- 1 | const request = require('../../lib/request.js'); 2 | const api = require('../../lib/api.js'); 3 | 4 | Page({ 5 | data: { 6 | nick: '', 7 | avatar: '', 8 | vid: 0, 9 | commentList: [], 10 | showSubmitLoading: false, 11 | showSubmitSuccessToast: false, 12 | }, 13 | 14 | onReady() { 15 | let app = getApp(); 16 | wx.setNavigationBarTitle({ title: '播放-' + app.currentVideoTitle}); 17 | }, 18 | 19 | onLoad() { 20 | let app = getApp(); 21 | let self = this; 22 | this.setData({ videoUrl: app.currentVideoUrl, vid: +app.vid }); 23 | 24 | this.getUserInfo().then((userInfo) => { 25 | self.setData({ 26 | nick: userInfo.nickName, 27 | avatar: userInfo.avatarUrl 28 | }); 29 | }) 30 | 31 | this.getCommentList().then((resp) => { 32 | if (resp.code !== 0) { 33 | // 视频列表加载失败 34 | return; 35 | } 36 | 37 | this.setData({ commentList: resp.data }); 38 | }); 39 | }, 40 | 41 | getUserInfo() { 42 | return new Promise((resolve, reject) => { 43 | wx.getUserInfo({ success: (resp) => { 44 | resolve(resp.userInfo); 45 | }, fail: reject }) 46 | }) 47 | }, 48 | 49 | // 获取评论列表 50 | getCommentList() { 51 | let promise = request({ 52 | method: 'GET', 53 | data: {vid: this.data.vid}, 54 | url: api.getUrl('/commentList'), 55 | }); 56 | return promise; 57 | }, 58 | 59 | commentInputChange(e) { 60 | this.data.commentContent = e.detail.value.trim(); 61 | }, 62 | 63 | submitComment() { 64 | if (!this.data.commentContent || this.data.isSubmiting) return; 65 | 66 | let params = { 67 | vid: this.data.vid, 68 | nick: this.data.nick, 69 | avatar: this.data.avatar, 70 | content: this.data.commentContent 71 | }; 72 | 73 | this.setData({isSubmiting: true, showSubmitLoading: true}); 74 | request({ 75 | method: 'POST', 76 | url: api.getUrl('/comment'), 77 | data: params 78 | }).then((resp) => { 79 | this.setData({isSubmiting: false, showSubmitLoading: false}); 80 | 81 | if (resp.code == 0) { 82 | this.setData({ 83 | commentList: [params].concat(this.data.commentList), 84 | showSubmitSuccessToast: true, 85 | }); 86 | 87 | setTimeout(() => { 88 | this.setData( {showSubmitSuccessToast: false} ); 89 | }, 2000) 90 | } 91 | }); 92 | } 93 | }); -------------------------------------------------------------------------------- /app/pages/detail/detail.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | {{item.nick}} 18 | {{item.content}} 19 | 20 | 21 | 22 | 23 | 24 | 25 | 28 | 29 | -------------------------------------------------------------------------------- /app/pages/detail/detail.wxss: -------------------------------------------------------------------------------- 1 | .container { 2 | width: 100%; 3 | height: 100%; 4 | background: #f0f0f0; 5 | } 6 | 7 | .container video { 8 | width: 100%; 9 | height: 15rem; 10 | background: black; 11 | } 12 | 13 | .comment { 14 | background: white; 15 | height: auto; 16 | margin-top: 1rem; 17 | } 18 | 19 | .list{ 20 | padding: 20px; 21 | } 22 | .item{ 23 | display: flex; 24 | margin-bottom: 20px; 25 | } 26 | .item.first-child{ 27 | border-bottom: #eee 1px solid; 28 | padding-bottom: 15px; 29 | margin-bottom: 30px; 30 | } 31 | .photo{ 32 | display: inline-block; 33 | width:45px; 34 | height: 45px; 35 | border: #eee 1px solid; 36 | } 37 | .photo image{ 38 | display: block; 39 | width: 100%; 40 | height: 100%; 41 | } 42 | .content{ 43 | flex: 1; 44 | padding-left: 20px; 45 | font-size: 14px; 46 | text-align: left; 47 | } 48 | .input{ 49 | box-sizing: border-box; 50 | border: #eee 1px solid; 51 | width: 100%; 52 | height: 45px; 53 | font-size: 14px; 54 | padding:0 10px; 55 | } 56 | .top{ 57 | color: #666; 58 | display: block; 59 | margin-bottom: 10px; 60 | } 61 | .text{ 62 | color: #333; 63 | display: block; 64 | margin-bottom: 10px; 65 | } 66 | .bottom{ 67 | color: #999; 68 | font-size: 12px; 69 | display: block; 70 | } 71 | 72 | .button{ 73 | margin-top:10px; 74 | width: 120px; 75 | height: 35px; 76 | font-size: 12px; 77 | line-height: 35px; 78 | } 79 | -------------------------------------------------------------------------------- /app/pages/index/index.js: -------------------------------------------------------------------------------- 1 | Page({ 2 | // 前往视频列表 3 | gotoVideo() { 4 | wx.navigateTo({ url: '../video/video' }); 5 | }, 6 | }); 7 | -------------------------------------------------------------------------------- /app/pages/index/index.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 恭喜你 4 | 成功地搭建了一个微信小程序 5 | 6 | 7 | 8 | 9 | 10 | 分享二维码邀请好友结伴一起写小程序! 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/pages/index/index.wxss: -------------------------------------------------------------------------------- 1 | .page-top { 2 | width: 750rpx; 3 | height: 594rpx; 4 | background-image: url(data:image/png;base64,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); 5 | background-repeat: no-repeat; 6 | background-size: 750rpx 694rpx; 7 | background-position:0 -40rpx; 8 | position: relative; 9 | z-index: 2; 10 | } 11 | .username,.text-info { 12 | position: absolute; 13 | left:50%; 14 | transform: translateX(-50%); 15 | white-space: nowrap; 16 | } 17 | .username { 18 | font-size: 40rpx; 19 | color: #fff; 20 | top:339rpx; 21 | } 22 | .text-info { 23 | font-size: 32rpx; 24 | color:#bdd0ee; 25 | top:400rpx; 26 | } 27 | .page-btn-wrap { 28 | position: absolute; 29 | top: 470rpx; 30 | width: 100%; 31 | text-align: center; 32 | } 33 | .page-btn { 34 | position:relative; 35 | margin:0 20rpx; 36 | padding:0; 37 | box-sizing:border-box; 38 | font-size:32rpx; 39 | text-decoration:none; 40 | -webkit-tap-highlight-color:transparent; 41 | overflow:hidden; 42 | display: inline-block; 43 | width: 300rpx; 44 | height: 85rpx; 45 | background-color: #fff; 46 | color: #2277da; 47 | line-height: 85rpx; 48 | } 49 | .page-bottom { 50 | background-color: #fff; 51 | width: 100%; 52 | height: 100%; 53 | position: absolute; 54 | top: 0; 55 | left: 0; 56 | padding: 624rpx 0 0; 57 | z-index: 1; 58 | box-sizing: border-box; 59 | } 60 | .qr-img { 61 | display: block; 62 | width: 300rpx; 63 | height: 300rpx; 64 | margin: 40rpx auto; 65 | } 66 | .qr-txt { 67 | display: block; 68 | color: #666; 69 | font-size: 32rpx; 70 | margin: 20rpx auto 0; 71 | text-align: center; 72 | } 73 | .page-logo { 74 | display: block; 75 | width: 200rpx; 76 | height: 54rpx; 77 | margin: 40rpx auto 40rpx; 78 | } -------------------------------------------------------------------------------- /app/pages/video/video.js: -------------------------------------------------------------------------------- 1 | const config = require('../../config.js'); 2 | const util = require('../../lib/util.js'); 3 | const request = require('../../lib/request.js'); 4 | const api = require('../../lib/api.js'); 5 | 6 | Page({ 7 | data: { 8 | // 视频列表数据 9 | videoList: [], 10 | 11 | // 列表布局数据, 12 | layerList: [], 13 | 14 | //一行几个 15 | layoutColumnSize: 2, 16 | 17 | //当前加载页 18 | curPage: 1, 19 | 20 | //总页数 21 | totalPage: 1, 22 | 23 | //是否显示加载图标 24 | showLoding: true 25 | }, 26 | 27 | onLoad() { 28 | this.getVideoList(); 29 | }, 30 | 31 | // 获取相册列表 32 | getVideoList() { 33 | request({ 34 | data: {pageNo: this.data.curPage}, 35 | method: 'GET', 36 | url: api.getUrl('/list'), 37 | }).then((resp) => { 38 | if (resp.code !== 0) { 39 | // 视频列表加载失败 40 | return; 41 | } 42 | this.setData({totalPage: resp.data.totalPage}); 43 | 44 | //对vid进行初始化,如果有的话就保留,如果没有的话,就默认时间戳 45 | for( let i = 0 ; i < resp.data.list.length ; i++ ){ 46 | let cur = resp.data.list[i]; 47 | if( cur ){ 48 | cur.id = cur.id || Date.now(); 49 | } 50 | } 51 | 52 | this.renderVideoList(resp.data.list); 53 | }); 54 | }, 55 | 56 | // 渲染影片列表 57 | renderVideoList(videoList) { 58 | let layoutColumnSize = this.data.layoutColumnSize; 59 | videoList = this.data.videoList.concat(videoList); 60 | this.setData({ videoList: videoList }); 61 | 62 | let layoutList = []; 63 | if (videoList.length) { 64 | layoutList = util.listToMatrix(videoList, layoutColumnSize); 65 | } 66 | 67 | setTimeout(() => { 68 | this.setData({ layoutList: layoutList, showLoding: false}); 69 | }, 500) 70 | }, 71 | 72 | //滚动到底部时触发 73 | scrollToLower(e) { 74 | if (this.data.showLoding || this.data.curPage == this.data.totalPage) return; 75 | 76 | this.setData( {curPage: ++this.data.curPage, showLoding: true} ) 77 | this.getVideoList(); 78 | }, 79 | 80 | //播放影片 81 | gotoPlay(event) { 82 | let currentVideoUrl = event.currentTarget.dataset.src; 83 | let currentVideoTitle = event.currentTarget.dataset.title; 84 | let vid = event.currentTarget.dataset.vid; 85 | if (!currentVideoUrl || !currentVideoTitle || !vid) return; 86 | 87 | let app = getApp(); 88 | app.currentVideoUrl = currentVideoUrl; 89 | app.currentVideoTitle = currentVideoTitle; 90 | app.vid = vid; 91 | 92 | wx.navigateTo({ url: '../detail/detail' }); 93 | }, 94 | }); 95 | -------------------------------------------------------------------------------- /app/pages/video/video.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | 12 | {{item.video_title}} 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /app/pages/video/video.wxss: -------------------------------------------------------------------------------- 1 | .container { 2 | width: 100%; 3 | height: 100%; 4 | background: white; 5 | } 6 | 7 | .item-group { 8 | display: flex; 9 | height: 15rem; 10 | margin-bottom: 0.5rem; 11 | } 12 | 13 | .video-item { 14 | flex: 1; 15 | margin: 0.1rem; 16 | text-align: center; 17 | width: 9.7rem; 18 | height: 15rem; 19 | border: 0.1rem solid #ebebeb; 20 | position:relative; 21 | } 22 | 23 | .video-image { 24 | text-align: center; 25 | width: 9.7rem; 26 | height: 13.5rem; 27 | } 28 | 29 | .play-image { 30 | height: 3rem; 31 | width: 3rem; 32 | position:absolute; 33 | z-index: 2; 34 | top: 5.5rem; 35 | left: 3.8rem; 36 | } 37 | 38 | .video-title { 39 | position: absolute; 40 | color: #333; 41 | width: 9.7rem; 42 | top: 13.7rem; 43 | left: 0.1rem; 44 | font-size: 0.8rem; 45 | } 46 | 47 | .loading{ 48 | width: 100%; 49 | text-align: center; 50 | } 51 | 52 | .loading image { 53 | width: 0.9rem; 54 | height: 0.9rem; 55 | } 56 | -------------------------------------------------------------------------------- /server/app.js: -------------------------------------------------------------------------------- 1 | require('./globals'); 2 | 3 | const http = require('http'); 4 | const express = require('express'); 5 | const bodyParser = require('body-parser'); 6 | const morgan = require('morgan'); 7 | const config = require('./config'); 8 | 9 | const app = express(); 10 | 11 | app.set('query parser', 'simple'); 12 | app.set('case sensitive routing', true); 13 | app.set('jsonp callback name', 'callback'); 14 | app.set('strict routing', true); 15 | app.set('trust proxy', true); 16 | 17 | app.disable('x-powered-by'); 18 | 19 | // 记录请求日志 20 | app.use(morgan('tiny')); 21 | 22 | // parse `application/x-www-form-urlencoded` 23 | app.use(bodyParser.urlencoded({ extended: true })); 24 | 25 | // parse `application/json` 26 | app.use(bodyParser.json()); 27 | 28 | app.use(require('./middlewares/route_dispatcher')); 29 | 30 | // 打印异常日志 31 | process.on('uncaughtException', error => { 32 | console.log(error); 33 | }); 34 | 35 | // 启动server 36 | http.createServer(app).listen(config.port, () => { 37 | console.log('Express server listening on port: %s', config.port); 38 | }); 39 | -------------------------------------------------------------------------------- /server/common/routerbase.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 封装的路由公共基类,用于添加公用方法,不能直接实例化 3 | */ 4 | 5 | class RouterBase { 6 | constructor(req, res, next) { 7 | Object.assign(this, { req, res, next }); 8 | } 9 | 10 | /** 11 | * 静态工厂方法:创建用以响应路由的回调函数 12 | */ 13 | static makeRouteHandler() { 14 | return (req, res, next) => new this(req, res, next).handle(); 15 | } 16 | 17 | /** 18 | * 子类实现该方法处理请求 19 | */ 20 | handle() { 21 | throw new Error(`Please implement instance method \`${this.constructor.name}::handle\`.`); 22 | } 23 | } 24 | 25 | module.exports = RouterBase; -------------------------------------------------------------------------------- /server/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | port: '9994', 3 | ROUTE_BASE_PATH: '/applet', 4 | 5 | host: '填写开通 MySQL 时分配的内网IP', 6 | user: '填写MySQL用户名', 7 | password: '填写MySQL密码', 8 | database: '填写上一步中创建的MySQL数据名', 9 | }; -------------------------------------------------------------------------------- /server/globals.js: -------------------------------------------------------------------------------- 1 | global.SERVER_ROOT = __dirname; -------------------------------------------------------------------------------- /server/middlewares/route_dispatcher.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 通用路由分发器 3 | */ 4 | 5 | const express = require('express'); 6 | const path = require('path'); 7 | const _ = require('lodash'); 8 | const config = require('../config'); 9 | const routes = require('../routes'); 10 | 11 | const routeOptions = { 'caseSensitive': true, 'strict': true }; 12 | const routeDispatcher = express.Router(routeOptions); 13 | 14 | _.each(routes, (route, subpath) => { 15 | const router = express.Router(routeOptions); 16 | 17 | let routePath; 18 | 19 | // ignore `config.ROUTE_BASE_PATH` if `subpath` begin with `~` 20 | if (subpath[0] === '~') { 21 | routePath = subpath.slice(1); 22 | } else { 23 | routePath = config.ROUTE_BASE_PATH + subpath; 24 | } 25 | 26 | require(path.join(global.SERVER_ROOT, 'routes', route))(router); 27 | 28 | routeDispatcher.use(routePath, router, (err, req, res, next) => { 29 | // mute `URIError` error 30 | if (err instanceof URIError) { 31 | return next(); 32 | } 33 | 34 | throw err; 35 | }); 36 | }); 37 | 38 | module.exports = routeDispatcher; -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "video-server", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start-dev": "nodemon app.js", 8 | "start": "pm2 start process.json" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "body-parser": "^1.15.2", 15 | "express": "^4.14.0", 16 | "morgan": "^1.7.0", 17 | "lodash": "^4.16.1", 18 | "mysql": "^2.11.1" 19 | }, 20 | "devDependencies": { 21 | "nodemon": "^1.10.2" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /server/process.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "video", 3 | "script": "app.js", 4 | "cwd": "./", 5 | "exec_mode": "fork", 6 | "watch": true, 7 | "env": { 8 | "NODE_ENV": "production" 9 | } 10 | } -------------------------------------------------------------------------------- /server/routes/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | '/video': 'video/routehub', 3 | }; 4 | -------------------------------------------------------------------------------- /server/routes/video/handlers/comment.js: -------------------------------------------------------------------------------- 1 | const RouterBase = require('../../../common/routerbase'); 2 | const config = require('../../../config'); 3 | const mysql = require('mysql'); 4 | 5 | class Comment extends RouterBase { 6 | handle() { 7 | let req = this.req; 8 | let comment = Object.assign({}, { 9 | vid: req.body.vid, 10 | nick: req.body.nick, 11 | avatar: req.body.avatar, 12 | content: req.body. content 13 | }); 14 | 15 | //CDB Mysql配置 16 | let connection = mysql.createConnection({ 17 | host: config.host, 18 | password: config.password, 19 | user: config.user, 20 | database: config.database 21 | }); 22 | 23 | //开启数据库连接 24 | connection.connect((err) => { 25 | if (err) { 26 | this.res.json({ code: -1, msg: 'failed'}); 27 | } 28 | }); 29 | 30 | //提交评论 31 | connection.query('INSERT INTO comment SET ?', comment, (err, result) => { 32 | if (err) { 33 | this.res.json({ code: -1, msg: 'failed'}); 34 | return; 35 | } 36 | 37 | this.res.json({ 38 | code: 0, 39 | msg: 'ok' 40 | }); 41 | }); 42 | 43 | //提交完后关闭连接 44 | connection.end(); 45 | } 46 | } 47 | 48 | module.exports = Comment.makeRouteHandler(); -------------------------------------------------------------------------------- /server/routes/video/handlers/commentlist.js: -------------------------------------------------------------------------------- 1 | const RouterBase = require('../../../common/routerbase'); 2 | const config = require('../../../config'); 3 | const mysql = require('mysql'); 4 | 5 | class CommentList extends RouterBase { 6 | handle() { 7 | let vid = this.req.query.vid; 8 | if (!vid) { 9 | this.res.json({ code: -1, msg: 'failed', data: {} }); 10 | return; 11 | } 12 | 13 | //CDB Mysql配置 14 | let connection = mysql.createConnection({ 15 | host: config.host, 16 | password: config.password, 17 | user: config.user, 18 | database: config.database 19 | }); 20 | 21 | //开启数据库连接 22 | connection.connect((err) => { 23 | if (err) { 24 | this.res.json({ code: -1, msg: 'failed', data: {} }); 25 | } 26 | }); 27 | 28 | //查询列表 29 | connection.query('SELECT * from comment where vid = ? order by id desc', [vid], (err, result) => { 30 | if (err) { 31 | this.res.json({ code: -1, msg: 'failed', data: {} }); 32 | return; 33 | } 34 | 35 | this.res.json({ 36 | code: 0, 37 | msg: 'ok', 38 | data: result, 39 | }); 40 | }); 41 | 42 | //查询完后关闭连接 43 | connection.end(); 44 | } 45 | } 46 | 47 | module.exports = CommentList.makeRouteHandler(); -------------------------------------------------------------------------------- /server/routes/video/handlers/list.js: -------------------------------------------------------------------------------- 1 | const RouterBase = require('../../../common/routerbase'); 2 | const config = require('../../../config'); 3 | const mysql = require('mysql'); 4 | 5 | class VideoList extends RouterBase { 6 | handle() { 7 | //CDB Mysql配置 8 | let connection = mysql.createConnection({ 9 | host: config.host, 10 | password: config.password, 11 | user: config.user, 12 | database: config.database 13 | }); 14 | 15 | let pageNo = this.req.query.pageNo; 16 | let pageSize = 6; 17 | let start = (pageNo - 1) * pageSize; 18 | 19 | //开启数据库连接 20 | connection.connect((err) => { 21 | if (err) { 22 | this.res.json({ code: -1, msg: 'failed', data: {} }); 23 | } 24 | }); 25 | 26 | //查询列表 27 | Promise.all([ 28 | this.queryList(start, pageSize, connection), 29 | this.queryTotalPage(pageSize, connection) 30 | ]).then(([list, totalPage]) => { 31 | this.res.json({ 32 | code: 0, 33 | msg: 'ok', 34 | data: {list, totalPage} 35 | }); 36 | }) 37 | 38 | //查询完后关闭连接 39 | connection.end(); 40 | } 41 | 42 | //查询分页列表 43 | queryList(start, pageSize, connection) { 44 | return new Promise((resolve, rejct) => { 45 | connection.query('SELECT * from video limit ?,?', [start, pageSize], (err, result) => { 46 | if (err) { 47 | this.res.json({ code: -1, msg: 'failed', data: {} }); 48 | return; 49 | } 50 | 51 | resolve(result); 52 | }) 53 | }) 54 | } 55 | 56 | //查询总页数 57 | queryTotalPage(pageSize, connection) { 58 | return new Promise((resolve, rejct) => { 59 | connection.query('SELECT count(*) as count from video', (err, result) => { 60 | if (err) { 61 | this.res.json({ code: -1, msg: 'failed', data: {} }); 62 | return; 63 | } 64 | 65 | resolve(Math.ceil(result[0].count / pageSize)); 66 | }) 67 | }) 68 | } 69 | } 70 | 71 | module.exports = VideoList.makeRouteHandler(); -------------------------------------------------------------------------------- /server/routes/video/routehub.js: -------------------------------------------------------------------------------- 1 | module.exports = (router) => { 2 | // 获取视频列表 3 | router.get('/list', require('./handlers/list')); 4 | 5 | // 获取评论列表 6 | router.get('/commentList', require('./handlers/commentlist')); 7 | 8 | // 提交评论 9 | router.post('/comment', require('./handlers/comment')); 10 | }; --------------------------------------------------------------------------------