├── .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 | 
15 |
16 |
17 | ## 部署和运行
18 |
19 | 拿到了本小程序源码的朋友可以尝试自己运行起来。
20 |
21 | ### 整体架构
22 |
23 | 
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 | 
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 | 
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 | 
76 |
77 | 解析生效后,我们在浏览器使用域名就可以进行 HTTPS 访问。
78 |
79 | 
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 | 
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 | 
137 |
138 | 然后点击调试即可打开新片预告Demo开始体验。
139 |
140 | 
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 |
23 |
24 |
25 |
26 | 发表中...
27 |
28 |
29 |
30 | 发表成功
31 |
--------------------------------------------------------------------------------
/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 |
20 | 加载中...
21 |
--------------------------------------------------------------------------------
/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 | };
--------------------------------------------------------------------------------