├── client
├── pages
│ ├── detail
│ │ ├── detail.json
│ │ ├── detail.js
│ │ ├── detail.wxml
│ │ └── detail.wxss
│ ├── cast
│ │ ├── cast.json
│ │ ├── cast.wxss
│ │ ├── cast.wxml
│ │ └── cast.js
│ ├── douban
│ │ ├── store.js
│ │ ├── config.js
│ │ ├── douban.json
│ │ ├── douban.js
│ │ ├── functions.js
│ │ ├── douban.wxss
│ │ └── douban.wxml
│ ├── me
│ │ ├── user-unlogin.png
│ │ ├── me.json
│ │ ├── me.wxml
│ │ ├── me.wxss
│ │ └── me.js
│ └── mock
│ │ └── film.js
├── imgs
│ ├── me.png
│ ├── me-d.png
│ ├── douban.png
│ ├── loading.gif
│ └── douban-d.png
├── font-awesome-4.7.0
│ ├── fonts
│ │ ├── FontAwesome.otf
│ │ ├── fontawesome-webfont.eot
│ │ ├── fontawesome-webfont.ttf
│ │ ├── fontawesome-webfont.woff
│ │ └── fontawesome-webfont.woff2
│ └── css
│ │ └── font-awesome.css
├── app.js
├── vendor
│ └── wafer2-client-sdk
│ │ ├── lib
│ │ ├── utils.js
│ │ ├── session.js
│ │ ├── constants.js
│ │ ├── wxTunnel.js
│ │ ├── request.js
│ │ ├── login.js
│ │ └── tunnel.js
│ │ ├── index.js
│ │ ├── LICENSE
│ │ ├── package.json
│ │ └── README.md
├── config.js
├── app.json
└── utils
│ └── util.js
├── pic
├── code.jpg
├── suosui.png
├── suosui01.png
├── suosui02.png
└── suosui03.png
├── server
├── controllers
│ ├── upload.js
│ ├── login.js
│ ├── user.js
│ ├── index.js
│ ├── message.js
│ └── tunnel.js
├── process.prod.json
├── .eslintrc.json
├── nodemon.json
├── app.js
├── tools.md
├── middlewares
│ └── response.js
├── qcloud.js
├── config.js
├── tools
│ ├── initdb.js
│ └── cAuth.sql
├── routes
│ └── index.js
├── package.json
└── README.md
├── project.config.json
├── README.md
└── .gitignore
/client/pages/detail/detail.json:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/client/pages/cast/cast.json:
--------------------------------------------------------------------------------
1 | { }
2 |
--------------------------------------------------------------------------------
/pic/code.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maqingbo/suosui/HEAD/pic/code.jpg
--------------------------------------------------------------------------------
/client/pages/douban/store.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | location: null
3 | }
4 |
--------------------------------------------------------------------------------
/pic/suosui.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maqingbo/suosui/HEAD/pic/suosui.png
--------------------------------------------------------------------------------
/pic/suosui01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maqingbo/suosui/HEAD/pic/suosui01.png
--------------------------------------------------------------------------------
/pic/suosui02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maqingbo/suosui/HEAD/pic/suosui02.png
--------------------------------------------------------------------------------
/pic/suosui03.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maqingbo/suosui/HEAD/pic/suosui03.png
--------------------------------------------------------------------------------
/client/imgs/me.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maqingbo/suosui/HEAD/client/imgs/me.png
--------------------------------------------------------------------------------
/client/imgs/me-d.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maqingbo/suosui/HEAD/client/imgs/me-d.png
--------------------------------------------------------------------------------
/client/imgs/douban.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maqingbo/suosui/HEAD/client/imgs/douban.png
--------------------------------------------------------------------------------
/client/imgs/loading.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maqingbo/suosui/HEAD/client/imgs/loading.gif
--------------------------------------------------------------------------------
/client/imgs/douban-d.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maqingbo/suosui/HEAD/client/imgs/douban-d.png
--------------------------------------------------------------------------------
/client/pages/me/user-unlogin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maqingbo/suosui/HEAD/client/pages/me/user-unlogin.png
--------------------------------------------------------------------------------
/client/font-awesome-4.7.0/fonts/FontAwesome.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maqingbo/suosui/HEAD/client/font-awesome-4.7.0/fonts/FontAwesome.otf
--------------------------------------------------------------------------------
/client/font-awesome-4.7.0/fonts/fontawesome-webfont.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maqingbo/suosui/HEAD/client/font-awesome-4.7.0/fonts/fontawesome-webfont.eot
--------------------------------------------------------------------------------
/client/font-awesome-4.7.0/fonts/fontawesome-webfont.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maqingbo/suosui/HEAD/client/font-awesome-4.7.0/fonts/fontawesome-webfont.ttf
--------------------------------------------------------------------------------
/client/font-awesome-4.7.0/fonts/fontawesome-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maqingbo/suosui/HEAD/client/font-awesome-4.7.0/fonts/fontawesome-webfont.woff
--------------------------------------------------------------------------------
/client/font-awesome-4.7.0/fonts/fontawesome-webfont.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maqingbo/suosui/HEAD/client/font-awesome-4.7.0/fonts/fontawesome-webfont.woff2
--------------------------------------------------------------------------------
/client/pages/douban/config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @Date: 2017-12-26 14:19
3 | * @Email: wmaqingbo@163.com
4 | * @Last modified time: 2017-12-26 14:32
5 | */
6 | module.exports = {
7 | baiduAK: '6PdgVKXkGX8g4uTFGe18Q9yoTDutWaxw'
8 | }
9 |
--------------------------------------------------------------------------------
/client/pages/me/me.json:
--------------------------------------------------------------------------------
1 | {
2 | "navigationBarBackgroundColor": "#f6f6f6",
3 | "navigationBarTextStyle": "black",
4 | "navigationBarTitleText": "小马的日常琐碎",
5 | "backgroundColor": "#f6f6f6",
6 | "backgroundTextStyle": "light"
7 | }
8 |
--------------------------------------------------------------------------------
/server/controllers/upload.js:
--------------------------------------------------------------------------------
1 | const { uploader } = require('../qcloud')
2 |
3 | module.exports = async ctx => {
4 | // 获取上传之后的结果
5 | // 具体可以查看:
6 | const data = await uploader(ctx.req)
7 |
8 | ctx.state.data = data
9 | }
10 |
--------------------------------------------------------------------------------
/client/pages/douban/douban.json:
--------------------------------------------------------------------------------
1 | {
2 | "navigationBarBackgroundColor": "#f0f0f0",
3 | "navigationBarTextStyle": "black",
4 | "navigationBarTitleText": "影院热映影片",
5 | "backgroundColor": "#f0f0f0",
6 | "backgroundTextStyle": "light"
7 | }
8 |
--------------------------------------------------------------------------------
/project.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "client": "./client",
3 | "svr": "./server",
4 | "miniprogramRoot": "./client",
5 | "qcloudRoot": "./server",
6 | "setting": {
7 | "newFeature": true
8 | },
9 | "appid": "wx661e7cf91c7a55d6",
10 | "projectname": "suosui",
11 | "condition": {}
12 | }
--------------------------------------------------------------------------------
/server/process.prod.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "session",
3 | "script": "app.js",
4 | "cwd": "./",
5 | "exec_mode": "fork",
6 | "watch": true,
7 | "ignore_watch": ["tmp"],
8 | "env": {
9 | "NODE_ENV": "production"
10 | },
11 | "engines": {
12 | "node": ">=7.6"
13 | }
14 | }
--------------------------------------------------------------------------------
/server/controllers/login.js:
--------------------------------------------------------------------------------
1 | // 登录授权接口
2 | module.exports = async (ctx, next) => {
3 | // 通过 Koa 中间件进行登录之后
4 | // 登录信息会被存储到 ctx.state.$wxInfo
5 | // 具体查看:
6 | if (ctx.state.$wxInfo.loginState) {
7 | ctx.state.data = ctx.state.$wxInfo.userinfo
8 | ctx.state.data['time'] = Math.floor(Date.now() / 1000)
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/client/app.js:
--------------------------------------------------------------------------------
1 | //app.js
2 | var qcloud = require('./vendor/wafer2-client-sdk/index')
3 | var config = require('./config')
4 |
5 | App({
6 | onLaunch: function () {
7 | console.log('App Launch')
8 | },
9 | onShow: function () {
10 | console.log('App Show')
11 | },
12 | onHide: function () {
13 | console.log('App Hide')
14 | }
15 | })
--------------------------------------------------------------------------------
/server/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "parser": "babel-eslint",
4 | "parserOptions": {
5 | "sourceType": "module"
6 | },
7 | "extends": "standard",
8 | "rules": {
9 | "indent": [2, 4, { "SwitchCase": 1 }],
10 | "arrow-parens": 0,
11 | "generator-star-spacing": 0
12 | },
13 | "env": {
14 | "mocha": true
15 | }
16 | }
--------------------------------------------------------------------------------
/server/controllers/user.js:
--------------------------------------------------------------------------------
1 | module.exports = async (ctx, next) => {
2 | // 通过 Koa 中间件进行登录态校验之后
3 | // 登录信息会被存储到 ctx.state.$wxInfo
4 | // 具体查看:
5 | if (ctx.state.$wxInfo.loginState === 1) {
6 | // loginState 为 1,登录态校验成功
7 | ctx.state.data = ctx.state.$wxInfo.userinfo
8 | } else {
9 | ctx.state.code = -1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/server/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "restartable": "rs",
3 | "ignore": [
4 | ".git",
5 | "node_modules/**/node_modules"
6 | ],
7 | "verbose": true,
8 | "execMap": {
9 | "js": "node --harmony"
10 | },
11 | "env": {
12 | "NODE_ENV": "development",
13 | "DEBUG": "*,-nodemon:*,-nodemon,-knex:pool"
14 | },
15 | "ext": "js json"
16 | }
17 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | > 2019-1-12 更新
2 | >
3 | > API没问题了,但是个人小程序不允许涉及文娱资讯,审核未通过`(ノ-_-)ノ~┻━┻`
4 |
5 | > 2018-1-26 更新
6 | >
7 | > 豆瓣把API禁掉了,本小程序已废`(ノ-_-)ノ~┻━┻`
8 |
9 | ## suosui
10 |
11 | 微信小程序 - 小马的日常琐碎
12 |
13 | - 一个简单的微信小程序,可以查看自己所在城市的影院热门电影,包括豆瓣评分和剧情简介等。
14 | - 使用的豆瓣电影 API,有限制,所以打开不是很快
15 | - 纯属练手,仅供娱乐
16 | - 已发布体验版,微信搜索“小马的日常琐碎”或扫描下面二维码打开小程序
17 |
18 | 
19 |
20 |
21 | ## 界面预览
22 |
23 | 
24 |
--------------------------------------------------------------------------------
/client/pages/cast/cast.wxss:
--------------------------------------------------------------------------------
1 | /**
2 | * @Date: 2018-01-01 11:44
3 | * @Email: wmaqingbo@163.com
4 | * @Last modified time: 2018-01-01 20:05
5 | */
6 |
7 | /* loading 界面 */
8 | .page-loading {
9 | width: 100%;
10 | height: 100%;
11 | /* background-color: pink; */
12 | display: flex;
13 | justify-content: center;
14 | margin-top: 400rpx;
15 | }
16 | .loadinggif {
17 | width: 400rpx;
18 | height: 300rpx;
19 | }
20 |
21 | /* 加载完成 */
22 |
--------------------------------------------------------------------------------
/client/vendor/wafer2-client-sdk/lib/utils.js:
--------------------------------------------------------------------------------
1 |
2 | /**
3 | * 拓展对象
4 | */
5 | exports.extend = function extend(target) {
6 | var sources = Array.prototype.slice.call(arguments, 1);
7 |
8 | for (var i = 0; i < sources.length; i += 1) {
9 | var source = sources[i];
10 | for (var key in source) {
11 | if (source.hasOwnProperty(key)) {
12 | target[key] = source[key];
13 | }
14 | }
15 | }
16 |
17 | return target;
18 | };
--------------------------------------------------------------------------------
/client/vendor/wafer2-client-sdk/lib/session.js:
--------------------------------------------------------------------------------
1 | var constants = require('./constants');
2 | var SESSION_KEY = 'weapp_session_' + constants.WX_SESSION_MAGIC_ID;
3 |
4 | var Session = {
5 | get: function () {
6 | return wx.getStorageSync(SESSION_KEY) || null;
7 | },
8 |
9 | set: function (session) {
10 | wx.setStorageSync(SESSION_KEY, session);
11 | },
12 |
13 | clear: function () {
14 | wx.removeStorageSync(SESSION_KEY);
15 | },
16 | };
17 |
18 | module.exports = Session;
--------------------------------------------------------------------------------
/server/app.js:
--------------------------------------------------------------------------------
1 | const Koa = require('koa')
2 | const app = new Koa()
3 | const debug = require('debug')('koa-weapp-demo')
4 | const response = require('./middlewares/response')
5 | const bodyParser = require('koa-bodyparser')
6 | const config = require('./config')
7 |
8 | // 使用响应处理中间件
9 | app.use(response)
10 |
11 | // 解析请求体
12 | app.use(bodyParser())
13 |
14 | // 引入路由分发
15 | const router = require('./routes')
16 | app.use(router.routes())
17 |
18 | // 启动程序,监听端口
19 | app.listen(config.port, () => debug(`listening on port ${config.port}`))
20 |
--------------------------------------------------------------------------------
/client/pages/cast/cast.wxml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | {{cast.name}}/{{cast.gender}}
19 |
20 |
21 |
--------------------------------------------------------------------------------
/server/tools.md:
--------------------------------------------------------------------------------
1 | # 腾讯云小程序解决方案 Demo 工具使用文档
2 |
3 | 本文件夹下的脚本为腾讯云小程序解决方案 Demo 配套的工具,旨在让用户方便快捷的使用并创建小程序的开发环境。
4 |
5 | 工具包括:
6 |
7 | - [数据库初始化工具](#数据库初始化工具)
8 |
9 | ## 数据库初始化工具
10 |
11 | 本工具是为了让用户快速的按照腾讯云制定的数据库 schema 创建符合 SDK 标准的数据库结构。
12 |
13 | _**注意**:本工具支持的 MySQL 版本为 **5.7**,并且需提前在数据库中创建名为 `cAuth` 的数据库。`charset` 设置为 `utf8mb4`。_
14 |
15 | 快速使用:
16 |
17 | ```bash
18 | npm run initdb
19 | ```
20 |
21 | 或直接执行 `tools` 目录下的 `initdb.js` 文件:
22 |
23 | ```bash
24 | # 请保证已经执行了 npm install 安装了所需要的依赖
25 | node tools/initdb.js
26 | ```
27 |
28 | 我们提供了初始化的 SQL 文件,你也可以用其他数据库工具(如 Navicat)直接导入 SQL 文件。
29 |
--------------------------------------------------------------------------------
/client/config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 小程序配置文件
3 | */
4 |
5 | // 此处主机域名修改成腾讯云解决方案分配的域名
6 | // var host = 'https://123456.qcloud.la';
7 | var host = 'https://zvuiobhu.qcloud.la';
8 |
9 | var config = {
10 |
11 | // 下面的地址配合云端 Demo 工作
12 | service: {
13 | host,
14 |
15 | // 登录地址,用于建立会话
16 | loginUrl: `${host}/weapp/login`,
17 |
18 | // 测试的请求地址,用于测试会话
19 | requestUrl: `${host}/weapp/user`,
20 |
21 | // 测试的信道服务地址
22 | tunnelUrl: `${host}/weapp/tunnel`,
23 |
24 | // 上传图片接口
25 | uploadUrl: `${host}/weapp/upload`
26 | }
27 | };
28 |
29 | module.exports = config;
30 |
--------------------------------------------------------------------------------
/client/vendor/wafer2-client-sdk/index.js:
--------------------------------------------------------------------------------
1 | var constants = require('./lib/constants');
2 | var login = require('./lib/login');
3 | var Session = require('./lib/session');
4 | var request = require('./lib/request');
5 | var Tunnel = require('./lib/tunnel');
6 |
7 | var exports = module.exports = {
8 | login: login.login,
9 | setLoginUrl: login.setLoginUrl,
10 | LoginError: login.LoginError,
11 |
12 | clearSession: Session.clear,
13 |
14 | request: request.request,
15 | RequestError: request.RequestError,
16 |
17 | Tunnel: Tunnel,
18 | };
19 |
20 | // 导出错误类型码
21 | Object.keys(constants).forEach(function (key) {
22 | if (key.indexOf('ERR_') === 0) {
23 | exports[key] = constants[key];
24 | }
25 | });
--------------------------------------------------------------------------------
/client/vendor/wafer2-client-sdk/lib/constants.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | WX_HEADER_CODE: 'X-WX-Code',
3 | WX_HEADER_ENCRYPTED_DATA: 'X-WX-Encrypted-Data',
4 | WX_HEADER_IV: 'X-WX-IV',
5 | WX_HEADER_ID: 'X-WX-Id',
6 | WX_HEADER_SKEY: 'X-WX-Skey',
7 |
8 | WX_SESSION_MAGIC_ID: 'F2C224D4-2BCE-4C64-AF9F-A6D872000D1A',
9 |
10 | ERR_INVALID_PARAMS: 'ERR_INVALID_PARAMS',
11 |
12 | ERR_WX_LOGIN_FAILED: 'ERR_WX_LOGIN_FAILED',
13 | ERR_WX_GET_USER_INFO: 'ERR_WX_GET_USER_INFO',
14 | ERR_LOGIN_TIMEOUT: 'ERR_LOGIN_TIMEOUT',
15 | ERR_LOGIN_FAILED: 'ERR_LOGIN_FAILED',
16 | ERR_LOGIN_SESSION_NOT_RECEIVED: 'ERR_LOGIN_MISSING_SESSION',
17 |
18 | ERR_SESSION_INVALID: 'ERR_SESSION_INVALID',
19 | ERR_CHECK_LOGIN_FAILED: 'ERR_CHECK_LOGIN_FAILED',
20 | };
--------------------------------------------------------------------------------
/server/controllers/index.js:
--------------------------------------------------------------------------------
1 | const _ = require('lodash')
2 | const fs = require('fs')
3 | const path = require('path')
4 |
5 | /**
6 | * 映射 d 文件夹下的文件为模块
7 | */
8 | const mapDir = d => {
9 | const tree = {}
10 |
11 | // 获得当前文件夹下的所有的文件夹和文件
12 | const [dirs, files] = _(fs.readdirSync(d)).partition(p => fs.statSync(path.join(d, p)).isDirectory())
13 |
14 | // 映射文件夹
15 | dirs.forEach(dir => {
16 | tree[dir] = mapDir(path.join(d, dir))
17 | })
18 |
19 | // 映射文件
20 | files.forEach(file => {
21 | if (path.extname(file) === '.js') {
22 | tree[path.basename(file, '.js')] = require(path.join(d, file))
23 | }
24 | })
25 |
26 | return tree
27 | }
28 |
29 | // 默认导出当前文件夹下的映射
30 | module.exports = mapDir(path.join(__dirname))
31 |
--------------------------------------------------------------------------------
/client/pages/cast/cast.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @Date: 2017-12-26 17:24
3 | * @Email: wmaqingbo@163.com
4 | * @Last modified time: 2018-01-01 12:24
5 | */
6 |
7 | Page({
8 | data: {
9 | cast: {},
10 | showLoading: true,
11 | options: null
12 | },
13 | onLoad: function (options) {
14 | var that = this
15 | wx.setNavigationBarTitle({
16 | title: options.name
17 | })
18 | wx.request({
19 | url: 'https://api.douban.com/v2/movie/celebrity/' + options.id,
20 | header: {
21 | 'content-type': 'json'
22 | },
23 | success: function (res) {
24 | var data = res.data
25 | console.log(data);
26 | that.setData({
27 | cast: data,
28 | showLoading: false
29 | })
30 | }
31 | })
32 | }
33 | })
34 |
--------------------------------------------------------------------------------
/client/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "pages":[
3 | "pages/douban/douban",
4 | "pages/me/me",
5 | "pages/detail/detail",
6 | "pages/cast/cast"
7 | ],
8 | "window":{
9 | "backgroundColor":"#F6F6F6",
10 | "backgroundTextStyle":"light",
11 | "navigationBarBackgroundColor": "#F6F6F6",
12 | "navigationBarTitleText": "小马的日常琐碎",
13 | "navigationBarTextStyle":"black"
14 | },
15 | "tabBar": {
16 | "color": "#7A7E83",
17 | "selectedColor": "#37bf4c",
18 | "backgroundColor": "#ffffff",
19 | "list": [{
20 | "pagePath": "pages/douban/douban",
21 | "text": "热映中",
22 | "iconPath": "imgs/douban.png",
23 | "selectedIconPath": "imgs/douban-d.png"
24 | }, {
25 | "pagePath": "pages/me/me",
26 | "text": "关于我",
27 | "iconPath": "imgs/me.png",
28 | "selectedIconPath": "imgs/me-d.png"
29 | }]
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/server/controllers/message.js:
--------------------------------------------------------------------------------
1 | const { message: { checkSignature } } = require('../qcloud')
2 |
3 | /**
4 | * 响应 GET 请求(响应微信配置时的签名检查请求)
5 | */
6 | async function get (ctx, next) {
7 | const { signature, timestamp, nonce, echostr } = ctx.query
8 | if (checkSignature(signature, timestamp, nonce)) ctx.body = echostr
9 | else ctx.body = 'ERR_WHEN_CHECK_SIGNATURE'
10 | }
11 |
12 | async function post (ctx, next) {
13 | // 检查签名,确认是微信发出的请求
14 | const { signature, timestamp, nonce } = ctx.query
15 | if (!checkSignature(signature, timestamp, nonce)) ctx.body = 'ERR_WHEN_CHECK_SIGNATURE'
16 |
17 | /**
18 | * 解析微信发送过来的请求体
19 | * 可查看微信文档:https://mp.weixin.qq.com/debug/wxadoc/dev/api/custommsg/receive.html#接收消息和事件
20 | */
21 | const body = ctx.request.body
22 |
23 | ctx.body = 'success'
24 | }
25 |
26 | module.exports = {
27 | post,
28 | get
29 | }
30 |
--------------------------------------------------------------------------------
/server/middlewares/response.js:
--------------------------------------------------------------------------------
1 | const debug = require('debug')('koa-weapp-demo')
2 |
3 | /**
4 | * 响应处理模块
5 | */
6 | module.exports = async function (ctx, next) {
7 | try {
8 | // 调用下一个 middleware
9 | await next()
10 |
11 | // 处理响应结果
12 | // 如果直接写入在 body 中,则不作处理
13 | // 如果写在 ctx.body 为空,则使用 state 作为响应
14 | ctx.body = ctx.body ? ctx.body : {
15 | code: ctx.state.code !== undefined ? ctx.state.code : 0,
16 | data: ctx.state.data !== undefined ? ctx.state.data : {}
17 | }
18 | } catch (e) {
19 | // catch 住全局的错误信息
20 | debug('Catch Error: %o', e)
21 |
22 | // 设置状态码为 200 - 服务端错误
23 | ctx.status = 200
24 |
25 | // 输出详细的错误信息
26 | ctx.body = {
27 | code: -1,
28 | error: e && e.message ? e.message : e.toString()
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/client/vendor/wafer2-client-sdk/lib/wxTunnel.js:
--------------------------------------------------------------------------------
1 | /* istanbul ignore next */
2 | const noop = () => void(0);
3 |
4 | let onOpen, onClose, onMessage, onError;
5 |
6 | /* istanbul ignore next */
7 | function listen(listener) {
8 | if (listener) {
9 | onOpen = listener.onOpen;
10 | onClose = listener.onClose;
11 | onMessage = listener.onMessage;
12 | onError = listener.onError;
13 | } else {
14 | onOpen = noop;
15 | onClose = noop;
16 | onMessage = noop;
17 | onError = noop;
18 | }
19 | }
20 |
21 | /* istanbul ignore next */
22 | function bind() {
23 | wx.onSocketOpen(result => onOpen(result));
24 | wx.onSocketClose(result => onClose(result));
25 | wx.onSocketMessage(result => onMessage(result));
26 | wx.onSocketError(error => onError(error));
27 | }
28 |
29 | listen(null);
30 | bind();
31 |
32 | module.exports = { listen };
--------------------------------------------------------------------------------
/server/qcloud.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs')
2 | const qcloud = require('wafer-node-sdk')
3 |
4 | // 获取基础配置
5 | const configs = require('./config')
6 |
7 | // 获取 sdk.config
8 | const sdkConfig = (() => {
9 | const sdkConfigPath = '/data/release/sdk.config.json'
10 |
11 | // 检查文件是否存在
12 | try {
13 | const stats = fs.statSync(sdkConfigPath)
14 |
15 | if (!stats.isFile()) {
16 | console.log('sdk.config.json 不存在,将使用 config.js 中的配置')
17 | return {}
18 | }
19 | } catch (e) {
20 | return {}
21 | }
22 |
23 | // 返回配置信息
24 | try {
25 | const content = fs.readFileSync(sdkConfigPath, 'utf8')
26 | return JSON.parse(content)
27 | } catch (e) {
28 | // 如果配置读取错误或者 JSON 解析错误,则输出空配置项
29 | console.log('sdk.config.json 解析错误,不是 JSON 字符串')
30 | return {}
31 | }
32 | })()
33 |
34 | // 初始化 SDK
35 | // 将基础配置和 sdk.config 合并传入 SDK 并导出初始化完成的 SDK
36 | module.exports = qcloud(Object.assign({}, sdkConfig, configs))
37 |
--------------------------------------------------------------------------------
/server/config.js:
--------------------------------------------------------------------------------
1 | const CONF = {
2 | port: '5757',
3 | rootPathname: '',
4 |
5 | // 微信小程序 App ID
6 | appId: '',
7 |
8 | // 微信小程序 App Secret
9 | appSecret: '',
10 |
11 | // 是否使用腾讯云代理登录小程序
12 | useQcloudLogin: true,
13 |
14 | /**
15 | * MySQL 配置,用来存储 session 和用户信息
16 | * 若使用了腾讯云微信小程序解决方案
17 | * 开发环境下,MySQL 的初始密码为您的微信小程序 appid
18 | */
19 | mysql: {
20 | host: 'localhost',
21 | port: 3306,
22 | user: 'root',
23 | db: 'cAuth',
24 | pass: 'wxe26d80e0d67c40c2',
25 | char: 'utf8mb4'
26 | },
27 |
28 | cos: {
29 | /**
30 | * 地区简称
31 | * @查看 https://cloud.tencent.com/document/product/436/6224
32 | */
33 | region: 'ap-guangzhou',
34 | // Bucket 名称
35 | fileBucket: 'qcloudtest',
36 | // 文件夹
37 | uploadFolder: ''
38 | },
39 |
40 | // 微信登录态有效期
41 | wxLoginExpires: 7200,
42 | wxMessageToken: 'abcdefgh'
43 | }
44 |
45 | module.exports = CONF
46 |
--------------------------------------------------------------------------------
/client/utils/util.js:
--------------------------------------------------------------------------------
1 | const formatTime = date => {
2 | const year = date.getFullYear()
3 | const month = date.getMonth() + 1
4 | const day = date.getDate()
5 | const hour = date.getHours()
6 | const minute = date.getMinutes()
7 | const second = date.getSeconds()
8 |
9 | return [year, month, day].map(formatNumber).join('/') + ' ' + [hour, minute, second].map(formatNumber).join(':')
10 | }
11 |
12 | const formatNumber = n => {
13 | n = n.toString()
14 | return n[1] ? n : '0' + n
15 | }
16 |
17 |
18 | // 显示繁忙提示
19 | var showBusy = text => wx.showToast({
20 | title: text,
21 | icon: 'loading',
22 | duration: 10000
23 | })
24 |
25 | // 显示成功提示
26 | var showSuccess = text => wx.showToast({
27 | title: text,
28 | icon: 'success'
29 | })
30 |
31 | // 显示失败提示
32 | var showModel = (title, content) => {
33 | wx.hideToast();
34 |
35 | wx.showModal({
36 | title,
37 | content: JSON.stringify(content),
38 | showCancel: false
39 | })
40 | }
41 |
42 | module.exports = { formatTime, showBusy, showSuccess, showModel }
43 |
--------------------------------------------------------------------------------
/server/tools/initdb.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 腾讯云微信小程序解决方案
3 | * Demo 数据库初始化脚本
4 | * @author Jason
5 | */
6 | const fs = require('fs')
7 | const path = require('path')
8 | const { mysql: config } = require('../config')
9 |
10 | console.log('\n======================================')
11 | console.log('开始初始化数据库...')
12 |
13 | // 初始化 SQL 文件路径
14 | const INIT_DB_FILE = path.join(__dirname, './cAuth.sql')
15 |
16 | const DB = require('knex')({
17 | client: 'mysql',
18 | connection: {
19 | host: config.host,
20 | port: config.port,
21 | user: config.user,
22 | password: config.pass,
23 | database: config.db,
24 | charset: config.char,
25 | multipleStatements: true
26 | }
27 | })
28 |
29 | console.log(`准备读取 SQL 文件:${INIT_DB_FILE}`)
30 |
31 | // 读取 .sql 文件内容
32 | const content = fs.readFileSync(INIT_DB_FILE, 'utf8')
33 |
34 | console.log('开始执行 SQL 文件...')
35 |
36 | // 执行 .sql 文件内容
37 | DB.raw(content).then(res => {
38 | console.log('数据库初始化成功!')
39 | process.exit(0)
40 | }, err => {
41 | throw new Error(err)
42 | })
43 |
--------------------------------------------------------------------------------
/server/routes/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * ajax 服务路由集合
3 | */
4 | const router = require('koa-router')({
5 | prefix: '/weapp'
6 | })
7 | const controllers = require('../controllers')
8 |
9 | // 从 sdk 中取出中间件
10 | // 这里展示如何使用 Koa 中间件完成登录态的颁发与验证
11 | const { auth: { authorizationMiddleware, validationMiddleware } } = require('../qcloud')
12 |
13 | // --- 登录与授权 Demo --- //
14 | // 登录接口
15 | router.get('/login', authorizationMiddleware, controllers.login)
16 | // 用户信息接口(可以用来验证登录态)
17 | router.get('/user', validationMiddleware, controllers.user)
18 |
19 | // --- 图片上传 Demo --- //
20 | // 图片上传接口,小程序端可以直接将 url 填入 wx.uploadFile 中
21 | router.post('/upload', controllers.upload)
22 |
23 | // --- 信道服务接口 Demo --- //
24 | // GET 用来响应请求信道地址的
25 | router.get('/tunnel', controllers.tunnel.get)
26 | // POST 用来处理信道传递过来的消息
27 | router.post('/tunnel', controllers.tunnel.post)
28 |
29 | // --- 客服消息接口 Demo --- //
30 | // GET 用来响应小程序后台配置时发送的验证请求
31 | router.get('/message', controllers.message.get)
32 | // POST 用来处理微信转发过来的客服消息
33 | router.post('/message', controllers.message.post)
34 |
35 | module.exports = router
36 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | # Runtime data
6 | pids
7 | *.pid
8 | *.seed
9 | *.pid.lock
10 | # Directory for instrumented libs generated by jscoverage/JSCover
11 | lib-cov
12 | # Coverage directory used by tools like istanbul
13 | coverage
14 | # nyc test coverage
15 | .nyc_output
16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
17 | .grunt
18 | # Bower dependency directory (https://bower.io/)
19 | bower_components
20 | # node-waf configuration
21 | .lock-wscript
22 | # Compiled binary addons (http://nodejs.org/api/addons.html)
23 | build/Release
24 | # Dependency directories
25 | node_modules/
26 | jspm_packages/
27 | # Typescript v1 declaration files
28 | typings/
29 | # Optional npm cache directory
30 | .npm
31 | # Optional eslint cache
32 | .eslintcache
33 | # Optional REPL history
34 | .node_repl_history
35 | # Output of 'npm pack'
36 | *.tgz
37 | # Yarn Integrity file
38 | .yarn-integrity
39 | # dotenv environment variables file
40 | .env
41 | .vscode
42 | # ignore sh
43 | sh/
44 | # ignore test sdk.config.json
45 | sdk.config.json
--------------------------------------------------------------------------------
/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "koa-weapp-demo",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "app.js",
6 | "scripts": {
7 | "start": "pm2 start process.prod.json --no-daemon",
8 | "dev": "nodemon --config nodemon.json app.js",
9 | "initdb": "npm install && node tools/initdb.js"
10 | },
11 | "author": "Jason",
12 | "license": "MIT",
13 | "dependencies": {
14 | "axios": "^0.15.3",
15 | "knex": "^0.13.0",
16 | "koa": "^2.0.0",
17 | "koa-bodyparser": "^3.2.0",
18 | "koa-log4": "^2.1.0",
19 | "koa-router": "^7.0.1",
20 | "lodash": "^4.17.4",
21 | "mkdir-p": "0.0.7",
22 | "mysql": "^2.14.1",
23 | "pify": "^2.3.0",
24 | "wafer-node-sdk": "^1.1.1"
25 | },
26 | "devDependencies": {
27 | "babel-eslint": "^7.1.0",
28 | "debug": "^2.6.8",
29 | "eslint": "^3.9.1",
30 | "eslint-config-standard": "^6.2.1",
31 | "eslint-plugin-promise": "^3.3.1",
32 | "eslint-plugin-standard": "^2.0.1"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/client/vendor/wafer2-client-sdk/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.
--------------------------------------------------------------------------------
/client/pages/me/me.wxml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
11 | 关于我
12 | Github 求 star ヾ(≧O≦)〃嗷~
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | Github:wmaqingbo
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | Blog:小马的日常琐碎
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | 个人网站:maqingbo.top
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | 豆瓣:wmaqingbo
48 |
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/server/tools/cAuth.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Navicat Premium Data Transfer
3 |
4 | Source Server : Localhost
5 | Source Server Type : MySQL
6 | Source Server Version : 50717
7 | Source Host : localhost
8 | Source Database : cAuth
9 |
10 | Target Server Type : MySQL
11 | Target Server Version : 50717
12 | File Encoding : utf-8
13 |
14 | Date: 08/10/2017 22:22:52 PM
15 | */
16 |
17 | SET NAMES utf8;
18 | SET FOREIGN_KEY_CHECKS = 0;
19 |
20 | -- ----------------------------
21 | -- Table structure for `cSessionInfo`
22 | -- ----------------------------
23 | DROP TABLE IF EXISTS `cSessionInfo`;
24 | CREATE TABLE `cSessionInfo` (
25 | `open_id` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
26 | `uuid` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
27 | `skey` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
28 | `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
29 | `last_visit_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
30 | `session_key` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
31 | `user_info` varchar(2048) COLLATE utf8mb4_unicode_ci NOT NULL,
32 | PRIMARY KEY (`open_id`),
33 | KEY `openid` (`open_id`) USING BTREE,
34 | KEY `skey` (`skey`) USING BTREE
35 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='会话管理用户信息';
36 |
37 | SET FOREIGN_KEY_CHECKS = 1;
38 |
--------------------------------------------------------------------------------
/client/vendor/wafer2-client-sdk/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "_from": "wafer2-client-sdk",
3 | "_id": "wafer2-client-sdk@1.0.0",
4 | "_inBundle": false,
5 | "_integrity": "sha1-4hExQwJ+2YIN3LOn0EtbBd8uTYg=",
6 | "_location": "/wafer2-client-sdk",
7 | "_phantomChildren": {},
8 | "_requested": {
9 | "type": "tag",
10 | "registry": true,
11 | "raw": "wafer2-client-sdk",
12 | "name": "wafer2-client-sdk",
13 | "escapedName": "wafer2-client-sdk",
14 | "rawSpec": "",
15 | "saveSpec": null,
16 | "fetchSpec": "latest"
17 | },
18 | "_requiredBy": [
19 | "#USER",
20 | "/"
21 | ],
22 | "_resolved": "http://r.tnpm.oa.com/wafer2-client-sdk/download/wafer2-client-sdk-1.0.0.tgz",
23 | "_shasum": "e2113143027ed9820ddcb3a7d04b5b05df2e4d88",
24 | "_spec": "wafer2-client-sdk",
25 | "_where": "/Users/Jason/Tencent/ide-test/wafer-client-demo",
26 | "author": {
27 | "name": "CFETeam"
28 | },
29 | "bugs": {
30 | "url": "https://github.com/tencentyun/wafer2-client-sdk/issues"
31 | },
32 | "bundleDependencies": false,
33 | "deprecated": false,
34 | "description": "Wafer client SDK",
35 | "directories": {
36 | "lib": "lib"
37 | },
38 | "homepage": "https://github.com/tencentyun/wafer2-client-sdk#readme",
39 | "license": "MIT",
40 | "main": "index.js",
41 | "name": "wafer2-client-sdk",
42 | "repository": {
43 | "type": "git",
44 | "url": "git+https://github.com/tencentyun/wafer2-client-sdk.git"
45 | },
46 | "version": "1.0.0"
47 | }
48 |
--------------------------------------------------------------------------------
/client/pages/me/me.wxss:
--------------------------------------------------------------------------------
1 | /**
2 | * @Date: 2017-12-25 17:14
3 | * @Email: wmaqingbo@163.com
4 | * @Last modified time: 2017-12-26 15:00
5 | */
6 |
7 | page {
8 | background: #F6F6F6;
9 | display: flex;
10 | flex-direction: column;
11 | justify-content: flex-start;
12 | }
13 |
14 | .container {
15 | padding: 0 40rpx;
16 | }
17 |
18 | .title {
19 | width: 100%;
20 | height: 160rpx;
21 | align-items: flex-start;
22 | text-align: left;
23 | }
24 |
25 | .title-main {
26 | width: 100%;
27 | height: 60rpx;
28 | line-height: 60rpx;
29 | font-size: 50rpx;
30 | color: #888;
31 | margin-top: 30rpx;
32 | }
33 |
34 | .title-minor {
35 | width: 100%;
36 | height: 40rpx;
37 | line-height: 40rpx;
38 | font-size: 30rpx;
39 | color: #aaa;
40 | margin-top: 10rpx;
41 | }
42 |
43 | .github, .blog, .site, .douban {
44 | margin-top: 30rpx;
45 | height: 140rpx;
46 | width: 100%;
47 | background: #FFF;
48 | border-left: none;
49 | border-right: none;
50 | display: flex;
51 | flex-direction: row;
52 | align-items: center;
53 | transition: all 300ms ease;
54 | }
55 |
56 | .fa {
57 | display: flex;
58 | justify-content: center;
59 | align-items: center;
60 | width: 140rpx;
61 | height: 140rpx;
62 | font-size: 40rpx;
63 | }
64 |
65 | .fa-flash {
66 | color: #ff9933;
67 | }
68 |
69 | .fa-glass {
70 | color: #ef4d4d;
71 | }
72 | .fa-link {
73 | color: #37bf4c;
74 | }
75 |
76 | .nickname {
77 | font-size: 32rpx;
78 | color: #007AFF;
79 | }
80 |
--------------------------------------------------------------------------------
/client/pages/douban/douban.js:
--------------------------------------------------------------------------------
1 | var functions = require('./functions.js')
2 | var store = require('./store.js')
3 | // 本地测试地址
4 | // var film_list = require('../mock/filmlist.js')
5 | var url = 'https://api.douban.com/v2/movie/in_theaters'
6 | var count = 20
7 | Page({
8 | data: {
9 | city: "",
10 | films: [],
11 | // 本地测试数据
12 | // films: film_list.film_list.subjects,
13 | hasMore: false,
14 | showLoading: true,
15 | start: 0
16 | },
17 | // 下拉刷新,失效
18 | onPullDownRefresh: function() {
19 | console.log('onPullDownRefresh', new Date())
20 | },
21 | scroll: function(e) {
22 | //console.log(e)
23 | },
24 | // 生命周期钩子
25 | onLoad: function() {
26 | var that = this
27 | functions.getCity(function(city) {
28 | // console.log(city)
29 | that.setData({city: city})
30 | functions.fetchFilms.call(that, url, city, 0, count, function(data) {
31 | // console.log(data)
32 | that.setData({start: data.count+1})
33 | that.setData({showLoading: false})
34 | })
35 | })
36 | },
37 | // 下拉刷新?
38 | scrolltolower: function() {
39 | var that = this
40 | functions.getCity(function(city) {
41 | functions.fetchFilms.call(that, url, city, that.data.start, count, function(data) {})
42 | })
43 | },
44 | // 点击查看详情
45 | viewDetail: function(e) {
46 | var ds = e.currentTarget.dataset;
47 | // console.log(ds);
48 | wx.navigateTo({
49 | url: '../detail/detail?id=' + ds.id + '&title=' + ds.title + '&type=ing'
50 | })
51 | }
52 | })
53 |
--------------------------------------------------------------------------------
/client/pages/detail/detail.js:
--------------------------------------------------------------------------------
1 | // 本地测试地址
2 | var film = require('../mock/film.js')
3 | Page({
4 | data: {
5 | film: {},
6 | // film: film.film,
7 | showLoading: true,
8 | options: null
9 | },
10 | onLoad: function(options) {
11 | // console.log(this.data.film)
12 | var that = this
13 | wx.setNavigationBarTitle({title: options.title})
14 | wx.request({
15 | url: 'https://api.douban.com/v2/movie/subject/' + options.id + '?apikey=0b2bdeda43b5688921839c8ecb20399b',
16 | header: {
17 | 'content-type': 'json'
18 | },
19 | success: function(res) {
20 | var data = res.data
21 | console.log(data);
22 | that.setData({film: data, showLoading: false})
23 | }
24 | })
25 | },
26 | // 点击查看演员页面
27 | viewCast: function(e) {
28 | var ds = e.currentTarget.dataset;
29 | wx.navigateTo({
30 | url: '../cast/cast?id=' + ds.id + '&name=' + ds.name + '&type=ing'
31 | })
32 | },
33 | // 分享此页面
34 | onShareAppMessage: function(res) {
35 | var that = this
36 | if (res.from === 'button') {
37 | // 来自页面内转发按钮
38 | console.log(res.target)
39 | }
40 | return {
41 | title: that.data.film.title,
42 | desc: '自定义分享描述',
43 | path: '/pages/detail/detail?id=' + that.data.film.id + '&title=' + that.data.film.title + '&type=ing',
44 | success: function(res) {
45 | // 转发成功
46 | // console.log('转发成功');
47 | }, fail: function(res) {
48 | // 转发失败
49 | // console.log('转发失败');
50 | }}
51 | }
52 | })
53 |
--------------------------------------------------------------------------------
/client/pages/me/me.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @Date: 2017-12-25 17:14
3 | * @Email: wmaqingbo@163.com
4 | * @Last modified time: 2017-12-25 19:59
5 | */
6 |
7 | //index.js
8 | var qcloud = require('../../vendor/wafer2-client-sdk/index')
9 | var config = require('../../config')
10 | var util = require('../../utils/util.js')
11 |
12 | Page({
13 | data: {
14 | userInfo: {},
15 | logged: false,
16 | takeSession: false,
17 | requestResult: ''
18 | },
19 |
20 | // 用户登录示例
21 | login: function() {
22 | if (this.data.logged) return
23 |
24 | util.showBusy('正在登录')
25 | var that = this
26 |
27 | // 调用登录接口
28 | qcloud.login({
29 | success(result) {
30 | if (result) {
31 | util.showSuccess('登录成功')
32 | that.setData({
33 | userInfo: result,
34 | logged: true
35 | })
36 | } else {
37 | // 如果不是首次登录,不会返回用户信息,请求用户信息接口获取
38 | qcloud.request({
39 | url: config.service.requestUrl,
40 | login: true,
41 | success(result) {
42 | util.showSuccess('登录成功')
43 | that.setData({
44 | userInfo: result.data.data,
45 | logged: true
46 | })
47 | },
48 |
49 | fail(error) {
50 | util.showModel('请求失败', error)
51 | console.log('request fail', error)
52 | }
53 | })
54 | }
55 | },
56 |
57 | fail(error) {
58 | util.showModel('登录失败', error)
59 | console.log('登录失败', error)
60 | }
61 | })
62 | },
63 |
64 | // 切换是否带有登录态
65 | switchRequestMode: function (e) {
66 | this.setData({
67 | takeSession: e.detail.value
68 | })
69 | this.doRequest()
70 | },
71 |
72 | logTitle() {
73 | console.log(event);
74 | }
75 | })
76 |
--------------------------------------------------------------------------------
/client/pages/douban/functions.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @Date: 2017-12-26 12:30
3 | * @Email: wmaqingbo@163.com
4 | * @Last modified time: 2018-01-08 22:35
5 | */
6 |
7 | var config = require('./config.js')
8 | var store = require('./store.js')
9 | module.exports = {
10 | // 获取位置坐标
11 | // 使用百度API逆向解析
12 | // 将百度地图返回的位置对象,以参数的形式传入 cb 函数
13 | getLocation: function (cb) {
14 | var location = store.location
15 |
16 | if (location) {
17 | cb(location)
18 | return;
19 | }
20 |
21 | wx.getLocation({
22 | success: function (res) {
23 | // 获取经纬度
24 | var locationParam = res.latitude + ',' + res.longitude
25 | // 使用百度地图逆向解析出地址信息
26 | wx.request({
27 | url: 'https://api.map.baidu.com/geocoder/v2/?ak=' + config.baiduAK + '&location=' + locationParam + '1&output=json&pois=1',
28 | header: {
29 | "Content-Type": "json",
30 | },
31 | success: function (res) {
32 | var data = res.data
33 | // console.log(data);
34 | store.location = data.result
35 | console.log(data.result);
36 | // data.result 为百度地图返回的位置信息,是一个对象
37 | // 将百度地图返回的位置对象,以参数的形式传入 cb 函数(跨域)
38 | cb(data.result)
39 | }
40 | })
41 | }
42 | })
43 | },
44 | // 获取所在城市
45 | getCity: function (cb) {
46 | this.getLocation(function (location) {
47 | cb(location.addressComponent.city.replace('市', ''))
48 | })
49 | },
50 | // 从豆瓣获取用户所在城市的热映电影
51 | fetchFilms: function (url, city, start, count, cb) {
52 | var that = this
53 | // apikey为固定值
54 | wx.request({
55 | url: url + '?apikey=0b2bdeda43b5688921839c8ecb20399b' + '&city=' + city + '&start=' + start + '&count=' + count,
56 | header: {
57 | "Content-Type": "json",
58 | },
59 | success: function (res) {
60 | var data = res.data
61 | if (data.subjects.length === 0) {
62 | that.setData({
63 | hasMore: false,
64 | })
65 | } else {
66 | that.setData({
67 | films: that.data.films.concat(data.subjects),
68 | start: that.data.start + data.subjects.length
69 | })
70 | }
71 | cb(data)
72 | }
73 | })
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/client/pages/douban/douban.wxss:
--------------------------------------------------------------------------------
1 | /**
2 | * @Date: 2017-12-26 10:30
3 | * @Email: wmaqingbo@163.com
4 | * @Last modified time: 2018-01-08 22:28
5 | */
6 |
7 | /* loading 界面 */
8 | .page-loading {
9 | width: 100%;
10 | height: 100%;
11 | /* background-color: pink; */
12 | display: flex;
13 | justify-content: center;
14 | margin-top: 400rpx;
15 | }
16 | .loadinggif {
17 | width: 400rpx;
18 | height: 300rpx;
19 | }
20 |
21 | /* 列表 */
22 | .film-list {
23 | background-color: #fafafa;
24 | }
25 |
26 | .city {
27 | margin-left: 25rpx;
28 | }
29 |
30 | .page-title {
31 | font-size: 30rpx;
32 | color: #333;
33 | font-weight: bold;
34 | padding-left: 5rpx;
35 | }
36 |
37 | .film-item {
38 | width: 93%;
39 | min-height: 200rpx;
40 | background-color: #fff;
41 | box-sizing: border-box;
42 | padding: 25rpx;
43 | padding-left: 250rpx;
44 | padding-right: 25rpx;
45 | margin: 130rpx auto 0 auto;
46 | border-radius: 4px;
47 | box-shadow: 0 0 8px rgba(0,0,0,.2);
48 |
49 | display: flex;
50 | justify-content: flex-start;
51 |
52 | position: relative;
53 | }
54 | /* 电影-图片 */
55 | .film-image {
56 | position: absolute;
57 | left: 25rpx;
58 | bottom: 25rpx;
59 | background-color: #f0f0f0;
60 | display: inline-block;
61 | width: 200rpx;
62 | height: 280rpx;
63 | border-radius: 8px;
64 | overflow: hidden;
65 | }
66 |
67 | .film-image image {
68 | width: 200rpx;
69 | height: 280rpx;
70 | }
71 |
72 | /* 电影-介绍 */
73 | .film-info {
74 | display: inline-block;
75 | position: relative;
76 | }
77 |
78 | /* 电影名字 */
79 |
80 | .film-title-wrap {
81 | margin-bottom: 30rpx;
82 | }
83 |
84 | .film-title {
85 | font-size: 36rpx;
86 | color: #333;
87 | font-weight: bold;
88 | }
89 |
90 | .label {
91 | color: #666;
92 | }
93 |
94 | /* 评分 */
95 | .film-rating {
96 | width: auto;
97 | height: auto;
98 | display: flex;
99 | justify-content: center;
100 | align-items: center;
101 | position: absolute;
102 | right: -80rpx;
103 | top: 0;
104 | }
105 |
106 | .rating {
107 | font-size: 40rpx;
108 | color: #ffac2d;
109 | font-weight: 800;
110 | /* font-style: italic; */
111 | }
112 |
113 | .film-title-wrap, .genres, .directors, .casts, .pubdate {
114 | width: 340rpx;
115 | overflow: hidden;
116 | text-overflow: ellipsis;
117 | white-space: nowrap;
118 | }
119 |
120 | .genres, .directors, .casts, .pubdate {
121 | font-size: 22rpx;
122 | color: #666;
123 | height: 32rpx;
124 | line-height: 32rpx;
125 | }
126 | .casts {
127 | max-height: 64rpx;
128 | line-height: 32rpx;
129 | }
130 | .person::after {
131 | content: "/";
132 | }
133 | .person:last-child::after {
134 | content: "";
135 | }
136 |
137 | /* 上拉加载更多 */
138 | .load-more-wrap {
139 | width: 100%;
140 | height: 100rpx;
141 | font-size: 24rpx;
142 | color: #ccc;
143 | line-height: 100rpx;
144 | text-align: center;
145 | }
146 |
--------------------------------------------------------------------------------
/client/pages/douban/douban.wxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | {{film.title}}
29 |
30 |
31 |
32 |
33 | {{film.rating.average}}
34 |
35 |
36 | 0
37 |
38 |
39 |
40 |
41 |
42 | {{genre}}
43 |
44 |
45 |
46 |
52 |
53 |
54 |
55 | {{cast.name}}
56 |
57 |
58 |
59 |
60 | {{film.mainland_pubdate}} 上映
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 | 玩命加载中
72 |
73 |
74 |
75 |
76 |
77 | 没有更多内容了
78 |
79 |
80 |
81 |
82 |
83 |
--------------------------------------------------------------------------------
/client/pages/detail/detail.wxml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | {{film.title}}
33 |
34 |
35 |
36 |
37 | {{film.rating.average}}
38 |
39 |
40 | 暂无评分
41 |
42 | ({{film.ratings_count}}人评分)
43 |
44 |
45 |
46 |
47 | {{genre}}
48 |
49 |
50 |
51 |
52 | {{film.pubdate}} 上映
53 |
54 |
55 |
56 | 看过 : {{film.collect_count}}
57 | 想看 : {{film.wish_count}}
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 | {{film.summary}}
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 | {{tag}}
76 |
77 |
78 |
79 |
80 |
81 | 演职员表
82 |
83 |
84 |
85 |
86 |
87 | {{cast.name}}
88 |
89 |
90 |
91 |
92 |
--------------------------------------------------------------------------------
/server/README.md:
--------------------------------------------------------------------------------
1 | # 腾讯云小程序解决方案 Demo - Node.js
2 |
3 | Node.js 版本 Wafer SDK 的服务端 Demo
4 |
5 | ## 下载源码
6 |
7 | 你可以直接通过 git 将代码 clone 到本地,也可以点击[这里](https://github.com/tencentyun/wafer-node-server-demo/releases)下载。
8 |
9 | ```bash
10 | git clone https://github.com/tencentyun/wafer-node-server-demo.git
11 | ```
12 |
13 | ## 开始使用
14 |
15 | #### 安装依赖
16 |
17 | ```bash
18 | # 安装全局依赖
19 | npm i pm2 nodemon -g
20 |
21 | # 安装项目依赖
22 | npm i
23 | ```
24 |
25 | #### 启动项目
26 |
27 | ```bash
28 | # 开发环境,监听文件变化自动重启,并会输出 debug 信息
29 | tnpm run dev
30 |
31 | # 线上部署环境
32 | tnpm start
33 | ```
34 |
35 | 按照[小程序创建资源配置指引](https://github.com/tencentyun/weapp-doc)进行操作,可以得到运行本示例所需的资源和服务,其中包括已部署好的示例代码及自动下发的 SDK 配置文件 `/etc/qcloud/sdk.config`。
36 |
37 | - 示例代码部署目录:`/data/release/node-weapp-demo`
38 | - 运行示例的 Node 版本:`v8.1.0`
39 | - Node 进程管理工具:`pm2`
40 |
41 | ## 项目结构
42 |
43 | ```
44 | koa-weapp-demo
45 | ├── README.md
46 | ├── app.js
47 | ├── controllers
48 | │ ├── index.js
49 | │ ├── login.js
50 | │ ├── message.js
51 | │ ├── tunnel.js
52 | │ ├── upload.js
53 | │ └── user.js
54 | ├── middlewares
55 | │ └── response.js
56 | ├── config.js
57 | ├── package.json
58 | ├── process.json
59 | ├── nodemon.json
60 | ├── qcloud.js
61 | └── routes
62 | └── index.js
63 | ```
64 | `app.js` 是 Demo 的主入口文件,Demo 使用 Koa 框架,在 `app.js` 创建一个 Koa 实例并响应请求。
65 |
66 | `routes/index.js` 是 Demo 的路由定义文件
67 |
68 | `controllers` 存放 Demo 所有业务逻辑的目录,`index.js` 不需要修改,他会动态的将 `controllers` 文件夹下的目录结构映射成 modules 的 Object,例如 Demo 中的目录将会被映射成如下的结构:
69 |
70 | ```javascript
71 | // index.js 输出
72 | {
73 | login: require('login'),
74 | message: require('message'),
75 | tunnel: require('tunnel'),
76 | upload: require('upload'),
77 | user: require('user')
78 | }
79 | ```
80 |
81 | `qcloud.js` 导出了一个 SDK 的单例,包含了所有的 SDK 接口,之后使用的时候只需要 `require` 这个文件就行,无需重复初始化 SDK。
82 |
83 | `config.js` 主要的配置如下:
84 |
85 | ```javascript
86 | {
87 | port: '5757', // 项目启动的端口
88 |
89 | appId: 'wx00dd00dd00dd00dd', // 微信小程序 App ID
90 | appSecret: 'abcdefg', // 微信小程序 App Secret
91 | wxLoginExpires: 7200, // 微信登录态有效期
92 | useQcloudLogin: false, // 是否使用腾讯云代理登录
93 |
94 | /**
95 | * MySQL 配置,用来存储用户登录态和用户信息
96 | * 如果不提供 MySQL 配置,模式会使用自动配置好的本地镜像中的 MySQL 储存信息
97 | * 具体查看文档-登录态储存和校验
98 | **/
99 | mysql: {
100 | host: 'localhost',
101 | port: 3306,
102 | user: 'root',
103 | db: 'cAuth',
104 | pass: '',
105 | char: 'utf8'
106 | },
107 |
108 | // COS 配置,用于上传模块使用
109 | cos: {
110 | /**
111 | * 区域
112 | * 华北:cn-north
113 | * 华东:cn-east
114 | * 华南:cn-south
115 | * 西南:cn-southwest
116 | */
117 | region: 'cn-south',
118 | fileBucket: 'test', // Bucket 名称
119 | uploadFolder: '' // 文件夹
120 | }
121 | }
122 | ```
123 |
124 | 除了 `config.js` ,腾讯云还会在你初始化小程序解决方案的时候,向你的机器下发 `sdk.config`,里面包含了你的腾讯云 AppId、SecretId、SecretKey 和服务器等信息,无需修改,`qcloud.js` 会自动引入。如果你想要在自己的机器上部署 SDK 的 Demo,请查看[自行部署 Demo 说明]()。
125 |
126 | 除此以外,关于 SDK 的详细配置信息,还可以查看 [SDK 的 API 文档]()。
--------------------------------------------------------------------------------
/client/vendor/wafer2-client-sdk/lib/request.js:
--------------------------------------------------------------------------------
1 | var constants = require('./constants');
2 | var utils = require('./utils');
3 | var Session = require('./session');
4 | var loginLib = require('./login');
5 |
6 | var noop = function noop() {};
7 |
8 | var buildAuthHeader = function buildAuthHeader(session) {
9 | var header = {};
10 |
11 | if (session) {
12 | header[constants.WX_HEADER_SKEY] = session;
13 | }
14 |
15 | return header;
16 | };
17 |
18 | /***
19 | * @class
20 | * 表示请求过程中发生的异常
21 | */
22 | var RequestError = (function () {
23 | function RequestError(type, message) {
24 | Error.call(this, message);
25 | this.type = type;
26 | this.message = message;
27 | }
28 |
29 | RequestError.prototype = new Error();
30 | RequestError.prototype.constructor = RequestError;
31 |
32 | return RequestError;
33 | })();
34 |
35 | function request(options) {
36 | if (typeof options !== 'object') {
37 | var message = '请求传参应为 object 类型,但实际传了 ' + (typeof options) + ' 类型';
38 | throw new RequestError(constants.ERR_INVALID_PARAMS, message);
39 | }
40 |
41 | var requireLogin = options.login;
42 | var success = options.success || noop;
43 | var fail = options.fail || noop;
44 | var complete = options.complete || noop;
45 | var originHeader = options.header || {};
46 |
47 | // 成功回调
48 | var callSuccess = function () {
49 | success.apply(null, arguments);
50 | complete.apply(null, arguments);
51 | };
52 |
53 | // 失败回调
54 | var callFail = function (error) {
55 | fail.call(null, error);
56 | complete.call(null, error);
57 | };
58 |
59 | // 是否已经进行过重试
60 | var hasRetried = false;
61 |
62 | if (requireLogin) {
63 | doRequestWithLogin();
64 | } else {
65 | doRequest();
66 | }
67 |
68 | // 登录后再请求
69 | function doRequestWithLogin() {
70 | loginLib.login({ success: doRequest, fail: callFail });
71 | }
72 |
73 | // 实际进行请求的方法
74 | function doRequest() {
75 | var authHeader = buildAuthHeader(Session.get());
76 |
77 | wx.request(utils.extend({}, options, {
78 | header: utils.extend({}, originHeader, authHeader),
79 |
80 | success: function (response) {
81 | var data = response.data;
82 |
83 | var error, message;
84 | if (data && data.code === -1) {
85 | Session.clear();
86 | // 如果是登录态无效,并且还没重试过,会尝试登录后刷新凭据重新请求
87 | if (!hasRetried) {
88 | hasRetried = true;
89 | doRequestWithLogin();
90 | return;
91 | }
92 |
93 | message = '登录态已过期';
94 | error = new RequestError(data.error, message);
95 |
96 | callFail(error);
97 | return;
98 | } else {
99 | callSuccess.apply(null, arguments);
100 | }
101 | },
102 |
103 | fail: callFail,
104 | complete: noop,
105 | }));
106 | };
107 |
108 | };
109 |
110 | module.exports = {
111 | RequestError: RequestError,
112 | request: request,
113 | };
--------------------------------------------------------------------------------
/client/pages/detail/detail.wxss:
--------------------------------------------------------------------------------
1 | /**
2 | * @Date: 2017-12-26 17:24
3 | * @Email: wmaqingbo@163.com
4 | * @Last modified time: 2018-01-01 11:58
5 | */
6 |
7 | /* loading 界面 */
8 | .page-loading {
9 | width: 100%;
10 | height: 100%;
11 | /* background-color: pink; */
12 | display: flex;
13 | justify-content: center;
14 | margin-top: 400rpx;
15 | }
16 | .loadinggif {
17 | width: 400rpx;
18 | height: 300rpx;
19 | }
20 |
21 | /* 加载完成 */
22 | .film-detail {
23 | width: 100%;
24 | min-height: 100%;
25 | background-color: #fff;
26 | overflow: hidden;
27 | padding-bottom: 30rpx;
28 | }
29 |
30 | .film-item {
31 | width: 100%;
32 | height: auto;
33 | position: relative;
34 | overflow: hidden;
35 | }
36 |
37 | /* 背景层 */
38 | .mask {
39 | width: 100%;
40 | height: 100%;
41 | position: absolute;
42 | z-index: 1;
43 | filter: blur(6px);
44 | transform: scale(1.1);
45 | }
46 |
47 | .mask > image {
48 | width: 100%;
49 | height: 100%;
50 | }
51 |
52 | /* 信息 =========================================================================*/
53 | .film-info {
54 | position: relative;
55 | top: 0;
56 | left: 0;
57 | z-index: 10;
58 |
59 | box-sizing: border-box;
60 | padding: 80rpx 30rpx;
61 | overflow: hidden;
62 | }
63 |
64 | /* 图片 ======================================================================== */
65 | .film-image {
66 | float: left;
67 | width: 200rpx;
68 | height: 280rpx;
69 | border-radius: 8px;
70 | overflow: hidden;
71 | box-shadow: 0 0 8px rgba(255,255,255,.4);
72 | }
73 |
74 | .film-image > image {
75 | width: 200rpx;
76 | height: 280rpx;
77 | }
78 |
79 | /* 其他介绍 ================================*/
80 |
81 | .film-info .other_info {
82 | float: left;
83 | display: flex;
84 | flex-direction: column;
85 | justify-content: center;
86 | margin-left: 50rpx;
87 | color: #fff;
88 | }
89 |
90 | /* 名字 */
91 |
92 | .film-title {
93 | width: 100%;
94 | height: 40rpx;
95 | line-height: 40rpx;
96 | font-size: 34rpx;
97 | font-weight: bold;
98 | }
99 |
100 | /* 分数 */
101 | .film-rating {
102 | margin-top: 15rpx;
103 | }
104 | .rating {
105 | font-size: 40rpx;
106 | color: #ffac2d;
107 | font-weight: 800;
108 | }
109 | .ratings_count {
110 | font-size: 24rpx;
111 | }
112 |
113 | .genres {
114 | font-size: 24rpx;
115 | height: 40rpx;
116 | line-height: 40rpx;
117 | overflow: hidden;
118 | margin-top: 15rpx;
119 | }
120 | .collect-wish {
121 | margin-top: 15rpx;
122 | }
123 | .collect_tag, .collect_tag2 {
124 | font-size: 24rpx;
125 | padding: 7rpx 20rpx;
126 | border: 1px solid #ffac2d;
127 | border-radius: 10rpx;
128 | }
129 | .collect_tag2 {
130 | margin-left: 15rpx;
131 | }
132 | .person::after {
133 | content: "/";
134 | color: #fff;
135 | }
136 | .person:last-child::after {
137 | content: "";
138 | }
139 |
140 | /* 剧情简介 ============================================= */
141 | .summary {
142 | padding: 25rpx;
143 | }
144 | .summary_content {
145 | font-size: 28rpx;
146 | color: #333;
147 | line-height: 1.5;
148 | /* text-indent: 2em; */
149 | }
150 |
151 | /* 间隔 */
152 | .gap {
153 | width: 100%;
154 | height: 30rpx;
155 | background-color: #f6f6f6;
156 | }
157 |
158 | /* 标签 ==================================================*/
159 | .tags {
160 | width: 100%;
161 | height: auto;
162 | background-color: #ffffff;
163 | padding: 25rpx;
164 | box-sizing: border-box;
165 | }
166 |
167 | .tags_tag {
168 | display: inline-block;
169 | font-size: 24rpx;
170 | color: #999;
171 | padding: 5rpx 15rpx;
172 | /* border: 1px solid #eee; */
173 | border: 1px solid #ffac2d;
174 | border-radius: 24rpx;
175 | margin: 10rpx;
176 | }
177 |
178 | /* 演职员表 ======================================*/
179 | .casts {
180 | height: 300rpx;
181 | background-color: #ffffff;
182 | padding-left: 25rpx;
183 | }
184 |
185 | .casts_cast {
186 | float: left;
187 | margin-right: 15rpx;
188 | }
189 |
190 | .cast_title {
191 | width: 100%;
192 | padding: 10rpx 25rpx;
193 | font-size: 26rpx;
194 | color: #333;
195 | }
196 |
197 | .cast_pic {
198 | width: 160rpx;
199 | height: 220rpx;
200 | border-radius: 10px;
201 | overflow: hidden;
202 | }
203 |
204 | .cast_pic image {
205 | width: 100%;
206 | height: 100%;
207 | border-radius: 10px;
208 | }
209 |
210 | .cast_name {
211 | width: 160rpx;
212 | height: auto;
213 | box-sizing: border-box;
214 | text-align: center;
215 | padding: 10rpx 0;
216 | font-size: 24rpx;
217 | color: #555;
218 | }
219 |
--------------------------------------------------------------------------------
/server/controllers/tunnel.js:
--------------------------------------------------------------------------------
1 | const { tunnel } = require('../qcloud')
2 | const debug = require('debug')('koa-weapp-demo')
3 |
4 | /**
5 | * 这里实现一个简单的聊天室
6 | * userMap 为 tunnelId 和 用户信息的映射
7 | * 实际使用请使用数据库存储
8 | */
9 | const userMap = {}
10 |
11 | // 保存 当前已连接的 WebSocket 信道ID列表
12 | const connectedTunnelIds = []
13 |
14 | /**
15 | * 调用 tunnel.broadcast() 进行广播
16 | * @param {String} type 消息类型
17 | * @param {String} content 消息内容
18 | */
19 | const $broadcast = (type, content) => {
20 | tunnel.broadcast(connectedTunnelIds, type, content)
21 | .then(result => {
22 | const invalidTunnelIds = result.data && result.data.invalidTunnelIds || []
23 |
24 | if (invalidTunnelIds.length) {
25 | console.log('检测到无效的信道 IDs =>', invalidTunnelIds)
26 |
27 | // 从 userMap 和 connectedTunnelIds 中将无效的信道记录移除
28 | invalidTunnelIds.forEach(tunnelId => {
29 | delete userMap[tunnelId]
30 |
31 | const index = connectedTunnelIds.indexOf(tunnelId)
32 | if (~index) {
33 | connectedTunnelIds.splice(index, 1)
34 | }
35 | })
36 | }
37 | })
38 | }
39 |
40 | /**
41 | * 调用 TunnelService.closeTunnel() 关闭信道
42 | * @param {String} tunnelId 信道ID
43 | */
44 | const $close = (tunnelId) => {
45 | tunnel.closeTunnel(tunnelId)
46 | }
47 |
48 | /**
49 | * 实现 onConnect 方法
50 | * 在客户端成功连接 WebSocket 信道服务之后会调用该方法,
51 | * 此时通知所有其它在线的用户当前总人数以及刚加入的用户是谁
52 | */
53 | function onConnect (tunnelId) {
54 | console.log(`[onConnect] =>`, { tunnelId })
55 |
56 | if (tunnelId in userMap) {
57 | connectedTunnelIds.push(tunnelId)
58 |
59 | $broadcast('people', {
60 | 'total': connectedTunnelIds.length,
61 | 'enter': userMap[tunnelId]
62 | })
63 | } else {
64 | console.log(`Unknown tunnelId(${tunnelId}) was connectd, close it`)
65 | $close(tunnelId)
66 | }
67 | }
68 |
69 | /**
70 | * 实现 onMessage 方法
71 | * 客户端推送消息到 WebSocket 信道服务器上后,会调用该方法,此时可以处理信道的消息。
72 | * 在本示例,我们处理 `speak` 类型的消息,该消息表示有用户发言。
73 | * 我们把这个发言的信息广播到所有在线的 WebSocket 信道上
74 | */
75 | function onMessage (tunnelId, type, content) {
76 | console.log(`[onMessage] =>`, { tunnelId, type, content })
77 |
78 | switch (type) {
79 | case 'speak':
80 | if (tunnelId in userMap) {
81 | $broadcast('speak', {
82 | 'who': userMap[tunnelId],
83 | 'word': content.word
84 | })
85 | } else {
86 | $close(tunnelId)
87 | }
88 | break
89 |
90 | default:
91 | break
92 | }
93 | }
94 |
95 | /**
96 | * 实现 onClose 方法
97 | * 客户端关闭 WebSocket 信道或者被信道服务器判断为已断开后,
98 | * 会调用该方法,此时可以进行清理及通知操作
99 | */
100 | function onClose (tunnelId) {
101 | console.log(`[onClose] =>`, { tunnelId })
102 |
103 | if (!(tunnelId in userMap)) {
104 | console.log(`[onClose][Invalid TunnelId]=>`, tunnelId)
105 | $close(tunnelId)
106 | return
107 | }
108 |
109 | const leaveUser = userMap[tunnelId]
110 | delete userMap[tunnelId]
111 |
112 | const index = connectedTunnelIds.indexOf(tunnelId)
113 | if (~index) {
114 | connectedTunnelIds.splice(index, 1)
115 | }
116 |
117 | // 聊天室没有人了(即无信道ID)不再需要广播消息
118 | if (connectedTunnelIds.length > 0) {
119 | $broadcast('people', {
120 | 'total': connectedTunnelIds.length,
121 | 'leave': leaveUser
122 | })
123 | }
124 | }
125 |
126 | module.exports = {
127 | // 小程序请求 websocket 地址
128 | get: async ctx => {
129 | const data = await tunnel.getTunnelUrl(ctx.req)
130 | const tunnelInfo = data.tunnel
131 |
132 | userMap[tunnelInfo.tunnelId] = data.userinfo
133 |
134 | ctx.state.data = tunnelInfo
135 | },
136 |
137 | // 信道将信息传输过来的时候
138 | post: async ctx => {
139 | const packet = await tunnel.onTunnelMessage(ctx.request.body)
140 |
141 | debug('Tunnel recive a package: %o', packet)
142 |
143 | switch (packet.type) {
144 | case 'connect':
145 | onConnect(packet.tunnelId)
146 | break
147 | case 'message':
148 | onMessage(packet.tunnelId, packet.content.messageType, packet.content.messageContent)
149 | break
150 | case 'close':
151 | onClose(packet.tunnelId)
152 | break
153 | }
154 | }
155 |
156 | }
157 |
--------------------------------------------------------------------------------
/client/vendor/wafer2-client-sdk/lib/login.js:
--------------------------------------------------------------------------------
1 | var utils = require('./utils');
2 | var constants = require('./constants');
3 | var Session = require('./session');
4 |
5 | /***
6 | * @class
7 | * 表示登录过程中发生的异常
8 | */
9 | var LoginError = (function () {
10 | function LoginError(type, message) {
11 | Error.call(this, message);
12 | this.type = type;
13 | this.message = message;
14 | }
15 |
16 | LoginError.prototype = new Error();
17 | LoginError.prototype.constructor = LoginError;
18 |
19 | return LoginError;
20 | })();
21 |
22 | /**
23 | * 微信登录,获取 code 和 encryptData
24 | */
25 | var getWxLoginResult = function getLoginCode(callback) {
26 | wx.login({
27 | success: function (loginResult) {
28 | wx.getUserInfo({
29 | success: function (userResult) {
30 | callback(null, {
31 | code: loginResult.code,
32 | encryptedData: userResult.encryptedData,
33 | iv: userResult.iv,
34 | userInfo: userResult.userInfo,
35 | });
36 | },
37 |
38 | fail: function (userError) {
39 | var error = new LoginError(constants.ERR_WX_GET_USER_INFO, '获取微信用户信息失败,请检查网络状态');
40 | error.detail = userError;
41 | callback(error, null);
42 | },
43 | });
44 | },
45 |
46 | fail: function (loginError) {
47 | var error = new LoginError(constants.ERR_WX_LOGIN_FAILED, '微信登录失败,请检查网络状态');
48 | error.detail = loginError;
49 | callback(error, null);
50 | },
51 | });
52 | };
53 |
54 | var noop = function noop() {};
55 | var defaultOptions = {
56 | method: 'GET',
57 | success: noop,
58 | fail: noop,
59 | loginUrl: null,
60 | };
61 |
62 | /**
63 | * @method
64 | * 进行服务器登录,以获得登录会话
65 | *
66 | * @param {Object} options 登录配置
67 | * @param {string} options.loginUrl 登录使用的 URL,服务器应该在这个 URL 上处理登录请求
68 | * @param {string} [options.method] 请求使用的 HTTP 方法,默认为 "GET"
69 | * @param {Function} options.success(userInfo) 登录成功后的回调函数,参数 userInfo 微信用户信息
70 | * @param {Function} options.fail(error) 登录失败后的回调函数,参数 error 错误信息
71 | */
72 | var login = function login(options) {
73 | options = utils.extend({}, defaultOptions, options);
74 |
75 | if (!defaultOptions.loginUrl) {
76 | options.fail(new LoginError(constants.ERR_INVALID_PARAMS, '登录错误:缺少登录地址,请通过 setLoginUrl() 方法设置登录地址'));
77 | return;
78 | }
79 |
80 | var doLogin = () => getWxLoginResult(function (wxLoginError, wxLoginResult) {
81 | if (wxLoginError) {
82 | options.fail(wxLoginError);
83 | return;
84 | }
85 |
86 | var userInfo = wxLoginResult.userInfo;
87 |
88 | // 构造请求头,包含 code、encryptedData 和 iv
89 | var code = wxLoginResult.code;
90 | var encryptedData = wxLoginResult.encryptedData;
91 | var iv = wxLoginResult.iv;
92 | var header = {};
93 |
94 | header[constants.WX_HEADER_CODE] = code;
95 | header[constants.WX_HEADER_ENCRYPTED_DATA] = encryptedData;
96 | header[constants.WX_HEADER_IV] = iv;
97 |
98 | // 请求服务器登录地址,获得会话信息
99 | wx.request({
100 | url: options.loginUrl,
101 | header: header,
102 | method: options.method,
103 | data: options.data,
104 | success: function (result) {
105 | var data = result.data;
106 |
107 | // 成功地响应会话信息
108 | if (data && data.code === 0 && data.data.skey) {
109 | var res = data.data
110 | if (res.userinfo) {
111 | Session.set(res.skey);
112 | options.success(userInfo);
113 | } else {
114 | var errorMessage = '登录失败(' + data.error + '):' + (data.message || '未知错误');
115 | var noSessionError = new LoginError(constants.ERR_LOGIN_SESSION_NOT_RECEIVED, errorMessage);
116 | options.fail(noSessionError);
117 | }
118 |
119 | // 没有正确响应会话信息
120 | } else {
121 | var noSessionError = new LoginError(constants.ERR_LOGIN_SESSION_NOT_RECEIVED, JSON.stringify(data));
122 | options.fail(noSessionError);
123 | }
124 | },
125 |
126 | // 响应错误
127 | fail: function (loginResponseError) {
128 | var error = new LoginError(constants.ERR_LOGIN_FAILED, '登录失败,可能是网络错误或者服务器发生异常');
129 | options.fail(error);
130 | },
131 | });
132 | });
133 |
134 | var session = Session.get();
135 | if (session) {
136 | wx.checkSession({
137 | success: function () {
138 | options.success(session.userInfo);
139 | },
140 |
141 | fail: function () {
142 | Session.clear();
143 | doLogin();
144 | },
145 | });
146 | } else {
147 | doLogin();
148 | }
149 | };
150 |
151 | var setLoginUrl = function (loginUrl) {
152 | defaultOptions.loginUrl = loginUrl;
153 | };
154 |
155 | module.exports = {
156 | LoginError: LoginError,
157 | login: login,
158 | setLoginUrl: setLoginUrl,
159 | };
--------------------------------------------------------------------------------
/client/vendor/wafer2-client-sdk/README.md:
--------------------------------------------------------------------------------
1 | # 微信小程序客户端腾讯云增强 SDK
2 |
3 | [](https://travis-ci.org/tencentyun/wafer-client-sdk)
4 | [](https://coveralls.io/github/tencentyun/wafer-client-sdk?branch=master)
5 | [](LICENSE)
6 |
7 | 本 项目是 [Wafer](https://github.com/tencentyun/wafer-solution) 的组成部分,为小程序客户端开发提供 SDK 支持会话服务和信道服务。
8 |
9 | ## SDK 获取与安装
10 |
11 | 解决方案[客户端 Demo](https://github.com/tencentyun/wafer-client-demo) 已经集成并使用最新版的 SDK,需要快速了解的可以从 Demo 开始。
12 |
13 | 如果需要单独开始,本 SDK 已经发布为 bower 模块,可以直接安装到小程序目录中。
14 |
15 | ```sh
16 | npm install -g bower
17 | bower install qcloud-weapp-client-sdk
18 | ```
19 |
20 | 安装之后,就可以使用 `require` 引用 SDK 模块:
21 |
22 | ```js
23 | var qcloud = require('./bower_components/qcloud-weapp-client-sdk/index.js');
24 | ```
25 |
26 | ## 会话服务
27 |
28 | [会话服务](https://github.com/tencentyun/wafer-solution/wiki/%E4%BC%9A%E8%AF%9D%E6%9C%8D%E5%8A%A1)让小程序拥有会话管理能力。
29 |
30 | ### 登录
31 |
32 | 登录可以在小程序和服务器之间建立会话,服务器由此可以获取到用户的标识和信息。
33 |
34 | ```js
35 | var qcloud = require('./bower_components/qcloud-weapp-client-sdk/index.js');
36 |
37 | // 设置登录地址
38 | qcloud.setLoginUrl('https://199447.qcloud.la/login');
39 | qcloud.login({
40 | success: function (userInfo) {
41 | console.log('登录成功', userInfo);
42 | },
43 | fail: function (err) {
44 | console.log('登录失败', err);
45 | }
46 | });
47 | ```
48 | 本 SDK 需要配合云端 SDK 才能提供完整会话服务。通过 [setLoginUrl](#setLoginUrl) 设置登录地址,云服务器在该地址上使用云端 SDK 处理登录请求。
49 |
50 | > `setLoginUrl` 方法设置登录地址之后会一直有效,因此你可以在微信小程序启动时设置。
51 |
52 | 登录成功后,可以获取到当前微信用户的基本信息。
53 |
54 | ### 请求
55 |
56 | 如果希望小程序的网络请求包含会话,登录之后使用 [request](#request) 方法进行网络请求即可。
57 |
58 | ```js
59 | qcloud.request({
60 | url: 'http://199447.qcloud.la/user',
61 | success: function (response) {
62 | console.log(response);
63 | },
64 | fail: function (err) {
65 | console.log(err);
66 | }
67 | });
68 | ```
69 |
70 | 如果调用 `request` 之前还没有登录,则请求不会带有会话。`request` 方法也支持 `login` 参数支持在请求之前自动登录。
71 |
72 | ```js
73 | // 使用 login 参数之前,需要设置登录地址
74 | qcloud.setLoginUrl('https://199447.qcloud.la/login');
75 | qcloud.request({
76 | login: true,
77 | url: 'http://199447.qcloud.la/user',
78 | success: function (response) {
79 | console.log(response);
80 | },
81 | fail: function (err) {
82 | console.log(err);
83 | }
84 | });
85 | ```
86 |
87 | 关于会话服务详细技术说明,请参考 [Wiki](https://github.com/tencentyun/wafer-solution/wiki/%E4%BC%9A%E8%AF%9D%E6%9C%8D%E5%8A%A1)。
88 |
89 | ## 信道服务
90 |
91 | [信道服务](https://github.com/tencentyun/wafer-solution/wiki/%E4%BF%A1%E9%81%93%E6%9C%8D%E5%8A%A1)小程序支持利用腾讯云的信道资源使用 WebSocket 服务。
92 |
93 | ```js
94 | // 创建信道,需要给定后台服务地址
95 | var tunnel = this.tunnel = new qcloud.Tunnel('https://199447.qcloud.la/tunnel');
96 |
97 | // 监听信道内置消息,包括 connect/close/reconnecting/reconnect/error
98 | tunnel.on('connect', () => console.log('WebSocket 信道已连接'));
99 | tunnel.on('close', () => console.log('WebSocket 信道已断开'));
100 | tunnel.on('reconnecting', () => console.log('WebSocket 信道正在重连...'));
101 | tunnel.on('reconnect', () => console.log('WebSocket 信道重连成功'));
102 | tunnel.on('error', error => console.error('信道发生错误:', error));
103 |
104 | // 监听自定义消息(服务器进行推送)
105 | tunnel.on('speak', speak => console.log('收到 speak 消息:', speak));
106 |
107 | // 打开信道
108 | tunnel.open();
109 | // 发送消息
110 | tunnel.emit('speak', { word: "hello", who: { nickName: "techird" }});
111 | // 关闭信道
112 | tunnel.close();
113 | ```
114 |
115 | 信道服务同样需要业务服务器配合云端 SDK 支持,构造信道实例的时候需要提供业务服务器提供的信道服务地址。通过监听信道消息以及自定义消息来通过信道实现业务。
116 |
117 | 关于信道使用的更完整实例,建议参考客户端 Demo 中的[三木聊天室应用源码](https://github.com/tencentyun/wafer-client-demo/blob/master/pages/chat/chat.js)。
118 |
119 | 关于信道服务详细技术说明,请参考 [Wiki](https://github.com/tencentyun/wafer-solution/wiki/%E4%BF%A1%E9%81%93%E6%9C%8D%E5%8A%A1)。
120 |
121 | ## API
122 |
123 |
124 | ### setLoginUrl
125 | 设置会话服务登录地址。
126 |
127 | #### 语法
128 | ```js
129 | qcloud.setLoginUrl(loginUrl);
130 | ```
131 |
132 | #### 参数
133 | |参数 |类型 |说明
134 | |-------------|---------------|--------------
135 | |loginUrl |string |会话服务登录地址
136 |
137 | ### login
138 | 登录,建立微信小程序会话。
139 |
140 | #### 语法
141 | ```js
142 | qcloud.login(options);
143 | ```
144 |
145 | #### 参数
146 | |参数 |类型 |说明
147 | |-------------|---------------|--------------
148 | |options |PlainObject |会话服务登录地址
149 | |options.success | () => void | 登录成功的回调
150 | |options.error | (error) => void | 登录失败的回调
151 |
152 |
153 | ### request
154 | 进行带会话的请求。
155 |
156 | #### 语法
157 | ```js
158 | qcloud.request(options);
159 | ```
160 |
161 | #### 参数
162 | |参数 |类型 |说明
163 | |-------------|---------------|--------------
164 | |options |PlainObject | 会话服务登录地址
165 | |options.login | bool | 是否自动登录以获取会话,默认为 false
166 | |options.url | string | 必填,要请求的地址
167 | |options.header | PlainObject | 请求头设置,不允许设置 Referer
168 | |options.method | string | 请求的方法,默认为 GET
169 | |options.success | (response) => void | 登录成功的回调。
- `response.statusCode`:请求返回的状态码
- `response.data`:请求返回的数据
170 | |options.error | (error) => void | 登录失败的回调
171 | |options.complete | () => void | 登录完成后回调,无论成功还是失败
172 |
173 | ### Tunnel
174 |
175 | 表示一个信道。由于小程序的限制,同一时间只能有一个打开的信道。
176 |
177 | #### constructor
178 |
179 | ##### 语法
180 | ```js
181 | var tunnel = new Tunnel(tunnelUrl);
182 | ```
183 |
184 | #### 参数
185 | |参数 |类型 |说明
186 | |-------------|---------------|--------------
187 | |tunnelUrl |String | 会话服务登录地址
188 |
189 |
190 | #### on
191 | 监听信道上的事件。信道上事件包括系统事件和服务器推送消息。
192 |
193 | ##### 语法
194 | ```js
195 | tunnel.on(type, listener);
196 | ```
197 |
198 | ##### 参数
199 | |参数 |类型 |说明
200 | |-------------|---------------|--------------
201 | |type |string | 监听的事件类型
202 | |listener |(message?: any) => void | 监听器,具体类型的事件发生时调用监听器。如果是消息,则会有消息内容。
203 |
204 | ##### 事件
205 | |事件 |说明
206 | |-------------|-------------------------------
207 | |connect |信道连接成功后回调
208 | |close |信道关闭后回调
209 | |reconnecting |信道发生重连时回调
210 | |reconnected |信道重连成功后回调
211 | |error |信道发生错误后回调
212 | |[message] |信道服务器推送过来的消息类型,如果消息类型和上面内置的时间类型冲突,需要在监听的时候在消息类型前加 `@`
213 | |\* |监听所有事件和消息,监听器第一个参数接收到时间或消息类型
214 |
215 | #### open
216 | 打开信道,建立连接。由于小程序的限制,同一时间只能有一个打开的信道。
217 |
218 | ##### 语法
219 | ```js
220 | tunnel.open();
221 | ```
222 |
223 | #### emit
224 | 向信道推送消息。
225 |
226 | ##### 语法
227 | ```js
228 | tunnel.emit(type, content);
229 | ```
230 |
231 | ##### 参数
232 | |参数 |类型 |说明
233 | |-------------|---------------|--------------
234 | |type |string | 要推送的消息的类型
235 | |content |any | 要推送的消息的内容
236 |
237 | #### close
238 | 关闭信道
239 |
240 | ##### 语法
241 | ```js
242 | tunnel.close();
243 | ```
244 |
245 | ## LICENSE
246 |
247 | [MIT](LICENSE)
248 |
--------------------------------------------------------------------------------
/client/vendor/wafer2-client-sdk/lib/tunnel.js:
--------------------------------------------------------------------------------
1 | var requestLib = require('./request');
2 | var wxTunnel = require('./wxTunnel');
3 |
4 | /**
5 | * 当前打开的信道,同一时间只能有一个信道打开
6 | */
7 | var currentTunnel = null;
8 |
9 | // 信道状态枚举
10 | var STATUS_CLOSED = Tunnel.STATUS_CLOSED = 'CLOSED';
11 | var STATUS_CONNECTING = Tunnel.STATUS_CONNECTING = 'CONNECTING';
12 | var STATUS_ACTIVE = Tunnel.STATUS_ACTIVE = 'ACTIVE';
13 | var STATUS_RECONNECTING = Tunnel.STATUS_RECONNECTING = 'RECONNECTING';
14 |
15 | // 错误类型枚举
16 | var ERR_CONNECT_SERVICE = Tunnel.ERR_CONNECT_SERVICE = 1001;
17 | var ERR_CONNECT_SOCKET = Tunnel.ERR_CONNECT_SOCKET = 1002;
18 | var ERR_RECONNECT = Tunnel.ERR_RECONNECT = 2001;
19 | var ERR_SOCKET_ERROR = Tunnel.ERR_SOCKET_ERROR = 3001;
20 |
21 | // 包类型枚举
22 | var PACKET_TYPE_MESSAGE = 'message';
23 | var PACKET_TYPE_PING = 'ping';
24 | var PACKET_TYPE_PONG = 'pong';
25 | var PACKET_TYPE_TIMEOUT = 'timeout';
26 | var PACKET_TYPE_CLOSE = 'close';
27 |
28 | // 断线重连最多尝试 5 次
29 | var DEFAULT_MAX_RECONNECT_TRY_TIMES = 5;
30 |
31 | // 每次重连前,等待时间的增量值
32 | var DEFAULT_RECONNECT_TIME_INCREASE = 1000;
33 |
34 | function Tunnel(serviceUrl) {
35 | if (currentTunnel && currentTunnel.status !== STATUS_CLOSED) {
36 | throw new Error('当前有未关闭的信道,请先关闭之前的信道,再打开新信道');
37 | }
38 |
39 | currentTunnel = this;
40 |
41 | // 等确认微信小程序全面支持 ES6 就不用那么麻烦了
42 | var me = this;
43 |
44 | //=========================================================================
45 | // 暴露实例状态以及方法
46 | //=========================================================================
47 | this.serviceUrl = serviceUrl;
48 | this.socketUrl = null;
49 | this.status = null;
50 |
51 | this.open = openConnect;
52 | this.on = registerEventHandler;
53 | this.emit = emitMessagePacket;
54 | this.close = close;
55 |
56 | this.isClosed = isClosed;
57 | this.isConnecting = isConnecting;
58 | this.isActive = isActive;
59 | this.isReconnecting = isReconnecting;
60 |
61 |
62 | //=========================================================================
63 | // 信道状态处理,状态说明:
64 | // closed - 已关闭
65 | // connecting - 首次连接
66 | // active - 当前信道已经在工作
67 | // reconnecting - 断线重连中
68 | //=========================================================================
69 | function isClosed() { return me.status === STATUS_CLOSED; }
70 | function isConnecting() { return me.status === STATUS_CONNECTING; }
71 | function isActive() { return me.status === STATUS_ACTIVE; }
72 | function isReconnecting() { return me.status === STATUS_RECONNECTING; }
73 |
74 | function setStatus(status) {
75 | var lastStatus = me.status;
76 | if (lastStatus !== status) {
77 | me.status = status;
78 | }
79 | }
80 |
81 | // 初始为关闭状态
82 | setStatus(STATUS_CLOSED);
83 |
84 |
85 | //=========================================================================
86 | // 信道事件处理机制
87 | // 信道事件包括:
88 | // connect - 连接已建立
89 | // close - 连接被关闭(包括主动关闭和被动关闭)
90 | // reconnecting - 开始重连
91 | // reconnect - 重连成功
92 | // error - 发生错误,其中包括连接失败、重连失败、解包失败等等
93 | // [message] - 信道服务器发送过来的其它事件类型,如果事件类型和上面内置的事件类型冲突,将在事件类型前面添加前缀 `@`
94 | //=========================================================================
95 | var preservedEventTypes = 'connect,close,reconnecting,reconnect,error'.split(',');
96 | var eventHandlers = [];
97 |
98 | /**
99 | * 注册消息处理函数
100 | * @param {string} messageType 支持内置消息类型("connect"|"close"|"reconnecting"|"reconnect"|"error")以及业务消息类型
101 | */
102 | function registerEventHandler(eventType, eventHandler) {
103 | if (typeof eventHandler === 'function') {
104 | eventHandlers.push([eventType, eventHandler]);
105 | }
106 | }
107 |
108 | /**
109 | * 派发事件,通知所有处理函数进行处理
110 | */
111 | function dispatchEvent(eventType, eventPayload) {
112 | eventHandlers.forEach(function (handler) {
113 | var handleType = handler[0];
114 | var handleFn = handler[1];
115 |
116 | if (handleType === '*') {
117 | handleFn(eventType, eventPayload);
118 | } else if (handleType === eventType) {
119 | handleFn(eventPayload);
120 | }
121 | });
122 | }
123 |
124 | /**
125 | * 派发事件,事件类型和系统保留冲突的,事件名会自动加上 '@' 前缀
126 | */
127 | function dispatchEscapedEvent(eventType, eventPayload) {
128 | if (preservedEventTypes.indexOf(eventType) > -1) {
129 | eventType = '@' + eventType;
130 | }
131 |
132 | dispatchEvent(eventType, eventPayload);
133 | }
134 |
135 |
136 | //=========================================================================
137 | // 信道连接控制
138 | //=========================================================================
139 | var isFirstConnection = true;
140 | var isOpening = false;
141 |
142 | /**
143 | * 连接信道服务器,获取 WebSocket 连接地址,获取地址成功后,开始进行 WebSocket 连接
144 | */
145 | function openConnect() {
146 | if (isOpening) return;
147 | isOpening = true;
148 |
149 | // 只有关闭状态才会重新进入准备中
150 | setStatus(isFirstConnection ? STATUS_CONNECTING : STATUS_RECONNECTING);
151 |
152 | requestLib.request({
153 | url: serviceUrl,
154 | method: 'GET',
155 | success: function (response) {
156 | if (+response.statusCode === 200 && response.data && response.data.data.connectUrl) {
157 | openSocket(me.socketUrl = response.data.data.connectUrl);
158 | } else {
159 | dispatchConnectServiceError(response);
160 | }
161 | },
162 | fail: dispatchConnectServiceError,
163 | complete: () => isOpening = false,
164 | });
165 |
166 | function dispatchConnectServiceError(detail) {
167 | if (isFirstConnection) {
168 | setStatus(STATUS_CLOSED);
169 |
170 | dispatchEvent('error', {
171 | code: ERR_CONNECT_SERVICE,
172 | message: '连接信道服务失败,网络错误或者信道服务没有正确响应',
173 | detail: detail || null,
174 | });
175 |
176 | } else {
177 | startReconnect(detail);
178 | }
179 | }
180 | }
181 |
182 | /**
183 | * 打开 WebSocket 连接,打开后,注册微信的 Socket 处理方法
184 | */
185 | function openSocket(url) {
186 | wxTunnel.listen({
187 | onOpen: handleSocketOpen,
188 | onMessage: handleSocketMessage,
189 | onClose: handleSocketClose,
190 | onError: handleSocketError,
191 | });
192 |
193 | wx.connectSocket({ url: url });
194 | isFirstConnection = false;
195 | }
196 |
197 |
198 | //=========================================================================
199 | // 处理消息通讯
200 | //
201 | // packet - 数据包,序列化形式为 `${type}` 或者 `${type}:${content}`
202 | // packet.type - 包类型,包括 message, ping, pong, close
203 | // packet.content? - 当包类型为 message 的时候,会附带 message 数据
204 | //
205 | // message - 消息体,会使用 JSON 序列化后作为 packet.content
206 | // message.type - 消息类型,表示业务消息类型
207 | // message.content? - 消息实体,可以为任意类型,表示消息的附带数据,也可以为空
208 | //
209 | // 数据包示例:
210 | // - 'ping' 表示 Ping 数据包
211 | // - 'message:{"type":"speak","content":"hello"}' 表示一个打招呼的数据包
212 | //=========================================================================
213 |
214 | // 连接还没成功建立的时候,需要发送的包会先存放到队列里
215 | var queuedPackets = [];
216 |
217 | /**
218 | * WebSocket 打开之后,更新状态,同时发送所有遗留的数据包
219 | */
220 | function handleSocketOpen() {
221 | /* istanbul ignore else */
222 | if (isConnecting()) {
223 | dispatchEvent('connect');
224 |
225 | }
226 | else if (isReconnecting()) {
227 | dispatchEvent('reconnect');
228 | resetReconnectionContext();
229 | }
230 |
231 | setStatus(STATUS_ACTIVE);
232 | emitQueuedPackets();
233 | nextPing();
234 | }
235 |
236 | /**
237 | * 收到 WebSocket 数据包,交给处理函数
238 | */
239 | function handleSocketMessage(message) {
240 | resolvePacket(message.data);
241 | }
242 |
243 | /**
244 | * 发送数据包,如果信道没有激活,将先存放队列
245 | */
246 | function emitPacket(packet) {
247 | if (isActive()) {
248 | sendPacket(packet);
249 | } else {
250 | queuedPackets.push(packet);
251 | }
252 | }
253 |
254 | /**
255 | * 数据包推送到信道
256 | */
257 | function sendPacket(packet) {
258 | var encodedPacket = [packet.type];
259 |
260 | if (packet.content) {
261 | encodedPacket.push(JSON.stringify(packet.content));
262 | }
263 |
264 | wx.sendSocketMessage({
265 | data: encodedPacket.join(':'),
266 | fail: handleSocketError,
267 | });
268 | }
269 |
270 | function emitQueuedPackets() {
271 | queuedPackets.forEach(emitPacket);
272 |
273 | // empty queued packets
274 | queuedPackets.length = 0;
275 | }
276 |
277 | /**
278 | * 发送消息包
279 | */
280 | function emitMessagePacket(messageType, messageContent) {
281 | var packet = {
282 | type: PACKET_TYPE_MESSAGE,
283 | content: {
284 | type: messageType,
285 | content: messageContent,
286 | },
287 | };
288 |
289 | emitPacket(packet);
290 | }
291 |
292 | /**
293 | * 发送 Ping 包
294 | */
295 | function emitPingPacket() {
296 | emitPacket({ type: PACKET_TYPE_PING });
297 | }
298 |
299 | /**
300 | * 发送关闭包
301 | */
302 | function emitClosePacket() {
303 | emitPacket({ type: PACKET_TYPE_CLOSE });
304 | }
305 |
306 | /**
307 | * 解析并处理从信道接收到的包
308 | */
309 | function resolvePacket(raw) {
310 | var packetParts = raw.split(':');
311 | var packetType = packetParts.shift();
312 | var packetContent = packetParts.join(':') || null;
313 | var packet = { type: packetType };
314 |
315 | if (packetContent) {
316 | try {
317 | packet.content = JSON.parse(packetContent);
318 | } catch (e) {}
319 | }
320 |
321 | switch (packet.type) {
322 | case PACKET_TYPE_MESSAGE:
323 | handleMessagePacket(packet);
324 | break;
325 | case PACKET_TYPE_PONG:
326 | handlePongPacket(packet);
327 | break;
328 | case PACKET_TYPE_TIMEOUT:
329 | handleTimeoutPacket(packet);
330 | break;
331 | case PACKET_TYPE_CLOSE:
332 | handleClosePacket(packet);
333 | break;
334 | default:
335 | handleUnknownPacket(packet);
336 | break;
337 | }
338 | }
339 |
340 | /**
341 | * 收到消息包,直接 dispatch 给处理函数
342 | */
343 | function handleMessagePacket(packet) {
344 | var message = packet.content;
345 | dispatchEscapedEvent(message.type, message.content);
346 | }
347 |
348 |
349 | //=========================================================================
350 | // 心跳、断开与重连处理
351 | //=========================================================================
352 |
353 | /**
354 | * Ping-Pong 心跳检测超时控制,这个值有两个作用:
355 | * 1. 表示收到服务器的 Pong 相应之后,过多久再发下一次 Ping
356 | * 2. 如果 Ping 发送之后,超过这个时间还没收到 Pong,断开与服务器的连接
357 | * 该值将在与信道服务器建立连接后被更新
358 | */
359 | let pingPongTimeout = 15000;
360 | let pingTimer = 0;
361 | let pongTimer = 0;
362 |
363 | /**
364 | * 信道服务器返回 Ping-Pong 控制超时时间
365 | */
366 | function handleTimeoutPacket(packet) {
367 | var timeout = packet.content * 1000;
368 | /* istanbul ignore else */
369 | if (!isNaN(timeout)) {
370 | pingPongTimeout = timeout;
371 | ping();
372 | }
373 | }
374 |
375 | /**
376 | * 收到服务器 Pong 响应,定时发送下一个 Ping
377 | */
378 | function handlePongPacket(packet) {
379 | nextPing();
380 | }
381 |
382 | /**
383 | * 发送下一个 Ping 包
384 | */
385 | function nextPing() {
386 | clearTimeout(pingTimer);
387 | clearTimeout(pongTimer);
388 | pingTimer = setTimeout(ping, pingPongTimeout);
389 | }
390 |
391 | /**
392 | * 发送 Ping,等待 Pong
393 | */
394 | function ping() {
395 | /* istanbul ignore else */
396 | if (isActive()) {
397 | emitPingPacket();
398 |
399 | // 超时没有响应,关闭信道
400 | pongTimer = setTimeout(handlePongTimeout, pingPongTimeout);
401 | }
402 | }
403 |
404 | /**
405 | * Pong 超时没有响应,信道可能已经不可用,需要断开重连
406 | */
407 | function handlePongTimeout() {
408 | startReconnect('服务器已失去响应');
409 | }
410 |
411 | // 已经重连失败的次数
412 | var reconnectTryTimes = 0;
413 |
414 | // 最多允许失败次数
415 | var maxReconnectTryTimes = Tunnel.MAX_RECONNECT_TRY_TIMES || DEFAULT_MAX_RECONNECT_TRY_TIMES;
416 |
417 | // 重连前等待的时间
418 | var waitBeforeReconnect = 0;
419 |
420 | // 重连前等待时间增量
421 | var reconnectTimeIncrease = Tunnel.RECONNECT_TIME_INCREASE || DEFAULT_RECONNECT_TIME_INCREASE;
422 |
423 | var reconnectTimer = 0;
424 |
425 | function startReconnect(lastError) {
426 | if (reconnectTryTimes >= maxReconnectTryTimes) {
427 | close();
428 |
429 | dispatchEvent('error', {
430 | code: ERR_RECONNECT,
431 | message: '重连失败',
432 | detail: lastError,
433 | });
434 | }
435 | else {
436 | wx.closeSocket();
437 | waitBeforeReconnect += reconnectTimeIncrease;
438 | setStatus(STATUS_RECONNECTING);
439 | reconnectTimer = setTimeout(doReconnect, waitBeforeReconnect);
440 | }
441 |
442 | if (reconnectTryTimes === 0) {
443 | dispatchEvent('reconnecting');
444 | }
445 |
446 | reconnectTryTimes += 1;
447 | }
448 |
449 | function doReconnect() {
450 | openConnect();
451 | }
452 |
453 | function resetReconnectionContext() {
454 | reconnectTryTimes = 0;
455 | waitBeforeReconnect = 0;
456 | }
457 |
458 | /**
459 | * 收到服务器的关闭请求
460 | */
461 | function handleClosePacket(packet) {
462 | close();
463 | }
464 |
465 | function handleUnknownPacket(packet) {
466 | // throw away
467 | }
468 |
469 | var isClosing = false;
470 |
471 | /**
472 | * 收到 WebSocket 断开的消息,处理断开逻辑
473 | */
474 | function handleSocketClose() {
475 | /* istanbul ignore if */
476 | if (isClosing) return;
477 |
478 | /* istanbul ignore else */
479 | if (isActive()) {
480 | // 意外断开的情况,进行重连
481 | startReconnect('链接已断开');
482 | }
483 | }
484 |
485 | function close() {
486 | isClosing = true;
487 | closeSocket();
488 | setStatus(STATUS_CLOSED);
489 | resetReconnectionContext();
490 | isFirstConnection = false;
491 | clearTimeout(pingTimer);
492 | clearTimeout(pongTimer);
493 | clearTimeout(reconnectTimer);
494 | dispatchEvent('close');
495 | isClosing = false;
496 | }
497 |
498 | function closeSocket(emitClose) {
499 | if (isActive() && emitClose !== false) {
500 | emitClosePacket();
501 | }
502 |
503 | wx.closeSocket();
504 | }
505 |
506 |
507 | //=========================================================================
508 | // 错误处理
509 | //=========================================================================
510 |
511 | /**
512 | * 错误处理
513 | */
514 | function handleSocketError(detail) {
515 | switch (me.status) {
516 | case Tunnel.STATUS_CONNECTING:
517 | dispatchEvent('error', {
518 | code: ERR_SOCKET_ERROR,
519 | message: '连接信道失败,网络错误或者信道服务不可用',
520 | detail: detail,
521 | });
522 | break;
523 | }
524 | }
525 |
526 | }
527 |
528 | module.exports = Tunnel;
--------------------------------------------------------------------------------
/client/pages/mock/film.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | film: {
3 | "rating": {
4 | "max": 10,
5 | "average": 2.4,
6 | "details": {
7 | "1": 76,
8 | "2": 5,
9 | "3": 3,
10 | "4": 0,
11 | "5": 1
12 | },
13 | "stars": "15",
14 | "min": 0
15 | },
16 | "reviews_count": 13,
17 | "videos": [
18 | {
19 | "source": {
20 | "literal": "qq",
21 | "pic": "https://img3.doubanio.com/f/movie/0a74f4379607fa731489d7f34daa545df9481fa0/pics/movie/video-qq.png",
22 | "name": "腾讯视频"
23 | },
24 | "sample_link": "http://v.qq.com/x/cover/xzvr5axh7r6u524.html?ptag=douban.movie",
25 | "video_id": "xzvr5axh7r6u524",
26 | "need_pay": true
27 | }, {
28 | "source": {
29 | "literal": "cctv6",
30 | "pic": "https://img3.doubanio.com/f/movie/8476600ca686384b5f314dca063ffb33f993f579/pics/movie/video-cctv6.png",
31 | "name": "1905电影网"
32 | },
33 | "sample_link": "http://www.1905.com/vod/play/1202714.shtml?__hz=6e0721b2c6977135",
34 | "video_id": "1202714",
35 | "need_pay": false
36 | }
37 | ],
38 | "wish_count": 247,
39 | "original_title": "恐怖理发店",
40 | "blooper_urls": [],
41 | "collect_count": 730,
42 | "images": {
43 | "small": "http://img1.doubanio.com/view/photo/s_ratio_poster/public/p2406903891.webp",
44 | "large": "http://img1.doubanio.com/view/photo/s_ratio_poster/public/p2406903891.webp",
45 | "medium": "http://img1.doubanio.com/view/photo/s_ratio_poster/public/p2406903891.webp"
46 | },
47 | "douban_site": "",
48 | "year": "2017",
49 | "popular_comments": [
50 | {
51 | "rating": {
52 | "max": 5,
53 | "value": 0,
54 | "min": 0
55 | },
56 | "useful_count": 30,
57 | "author": {
58 | "uid": "113544445",
59 | "avatar": "http://img1.doubanio.com/icon/u113544445-1.jpg",
60 | "signature": "",
61 | "alt": "http://www.douban.com/people/113544445/",
62 | "id": "113544445",
63 | "name": "坦克手马洋洋"
64 | },
65 | "subject_id": "26865690",
66 | "content": "妈的,中国没有一个地方不可怕",
67 | "created_at": "2016-12-31 17:28:27",
68 | "id": "1129589264"
69 | }, {
70 | "rating": {
71 | "max": 5,
72 | "value": 1,
73 | "min": 0
74 | },
75 | "useful_count": 0,
76 | "author": {
77 | "uid": "56906475",
78 | "avatar": "http://img1.doubanio.com/icon/u56906475-11.jpg",
79 | "signature": "成为更好的人",
80 | "alt": "http://www.douban.com/people/56906475/",
81 | "id": "56906475",
82 | "name": "格林童话"
83 | },
84 | "subject_id": "26865690",
85 | "content": "真棒",
86 | "created_at": "2018-05-21 14:46:54",
87 | "id": "1377114344"
88 | }, {
89 | "rating": {
90 | "max": 5,
91 | "value": 1,
92 | "min": 0
93 | },
94 | "useful_count": 3,
95 | "author": {
96 | "uid": "1091411",
97 | "avatar": "http://img1.doubanio.com/icon/u1091411-10.jpg",
98 | "signature": "Hello WANKER",
99 | "alt": "http://www.douban.com/people/1091411/",
100 | "id": "1091411",
101 | "name": "李圣经"
102 | },
103 | "subject_id": "26865690",
104 | "content": "89分钟……毫无内涵",
105 | "created_at": "2017-01-23 15:33:10",
106 | "id": "1140974290"
107 | }, {
108 | "rating": {
109 | "max": 5,
110 | "value": 0,
111 | "min": 0
112 | },
113 | "useful_count": 0,
114 | "author": {
115 | "uid": "154292831",
116 | "avatar": "http://img1.doubanio.com/icon/u154292831-1.jpg",
117 | "signature": "",
118 | "alt": "http://www.douban.com/people/154292831/",
119 | "id": "154292831",
120 | "name": "开心快乐每一天"
121 | },
122 | "subject_id": "26865690",
123 | "content": "这装神弄鬼的干嘛呢?",
124 | "created_at": "2018-08-09 18:36:08",
125 | "id": "1431018348"
126 | }
127 | ],
128 | "alt": "https://movie.douban.com/subject/26865690/",
129 | "id": "26865690",
130 | "mobile_url": "https://movie.douban.com/subject/26865690/mobile",
131 | "photos_count": 27,
132 | "pubdate": "2017-01-06",
133 | "title": "恐怖理发店",
134 | "do_count": null,
135 | "has_video": true,
136 | "share_url": "http://m.douban.com/movie/subject/26865690",
137 | "seasons_count": null,
138 | "languages": ["汉语普通话"],
139 | "schedule_url": "",
140 | "writers": [
141 | {
142 | "avatars": {
143 | "small": "http://img3.doubanio.com/f/movie/ca527386eb8c4e325611e22dfcb04cc116d6b423/pics/movie/celebrity-default-small.png",
144 | "large": "http://img1.doubanio.com/f/movie/63acc16ca6309ef191f0378faf793d1096a3e606/pics/movie/celebrity-default-large.png",
145 | "medium": "http://img3.doubanio.com/f/movie/8dd0c794499fe925ae2ae89ee30cd225750457b4/pics/movie/celebrity-default-medium.png"
146 | },
147 | "name_en": "Ran Ji",
148 | "name": "纪然",
149 | "alt": "https://movie.douban.com/celebrity/1366595/",
150 | "id": "1366595"
151 | }, {
152 | "avatars": {
153 | "small": "http://img3.doubanio.com/view/celebrity/s_ratio_celebrity/public/p1490348628.29.webp",
154 | "large": "http://img3.doubanio.com/view/celebrity/s_ratio_celebrity/public/p1490348628.29.webp",
155 | "medium": "http://img3.doubanio.com/view/celebrity/s_ratio_celebrity/public/p1490348628.29.webp"
156 | },
157 | "name_en": "Shilei Lu",
158 | "name": "陆诗雷",
159 | "alt": "https://movie.douban.com/celebrity/1360707/",
160 | "id": "1360707"
161 | }
162 | ],
163 | "pubdates": ["2017-01-06(中国大陆)"],
164 | "website": "",
165 | "tags": [
166 | "惊悚",
167 | "烂片",
168 | "一个星都不想给!",
169 | "烂片之中的烂片啊~",
170 | "垃圾",
171 | "中国",
172 | "狗屎",
173 | "呵呵",
174 | "烂透了",
175 | "真的好恐怖啊!"
176 | ],
177 | "has_schedule": false,
178 | "durations": ["89分钟"],
179 | "genres": [
180 | "爱情", "悬疑", "惊悚"
181 | ],
182 | "collection": null,
183 | "trailers": [
184 | {
185 | "medium": "http://img3.doubanio.com/img/trailer/medium/2395934439.jpg?",
186 | "title": "预告片:正式版 (中文字幕)",
187 | "subject_id": "26865690",
188 | "alt": "https://movie.douban.com/trailer/206905/",
189 | "small": "http://img3.doubanio.com/img/trailer/small/2395934439.jpg?",
190 | "resource_url": "http://vt1.doubanio.com/201901121414/2f395e99942567f5805014d5393ea378/view/movie/M/302060905.mp4",
191 | "id": "206905"
192 | }, {
193 | "medium": "http://img3.doubanio.com/img/trailer/medium/2408079427.jpg?",
194 | "title": "预告片:终极版 (中文字幕)",
195 | "subject_id": "26865690",
196 | "alt": "https://movie.douban.com/trailer/209536/",
197 | "small": "http://img3.doubanio.com/img/trailer/small/2408079427.jpg?",
198 | "resource_url": "http://vt1.doubanio.com/201901121414/1b5381bb315dc9ecc54b579ef42f4640/view/movie/M/302090536.mp4",
199 | "id": "209536"
200 | }, {
201 | "medium": "http://img1.doubanio.com/img/trailer/medium/2406384532.jpg?",
202 | "title": "预告片:激情版 (中文字幕)",
203 | "subject_id": "26865690",
204 | "alt": "https://movie.douban.com/trailer/209076/",
205 | "small": "http://img1.doubanio.com/img/trailer/small/2406384532.jpg?",
206 | "resource_url": "http://vt1.doubanio.com/201901121414/43bf6e6df23db89db28ab43ac2a3b479/view/movie/M/302090076.mp4",
207 | "id": "209076"
208 | }
209 | ],
210 | "episodes_count": null,
211 | "trailer_urls": [
212 | "http://vt1.doubanio.com/201901121414/2f395e99942567f5805014d5393ea378/view/movie/M/302060905.mp4", "http://vt1.doubanio.com/201901121414/1b5381bb315dc9ecc54b579ef42f4640/view/movie/M/302090536.mp4", "http://vt1.doubanio.com/201901121414/43bf6e6df23db89db28ab43ac2a3b479/view/movie/M/302090076.mp4"
213 | ],
214 | "has_ticket": false,
215 | "bloopers": [],
216 | "clip_urls": [],
217 | "current_season": null,
218 | "casts": [
219 | {
220 | "avatars": {
221 | "small": "http://img3.doubanio.com/view/celebrity/s_ratio_celebrity/public/p1403756298.69.webp",
222 | "large": "http://img3.doubanio.com/view/celebrity/s_ratio_celebrity/public/p1403756298.69.webp",
223 | "medium": "http://img3.doubanio.com/view/celebrity/s_ratio_celebrity/public/p1403756298.69.webp"
224 | },
225 | "name_en": "Guoer Yin",
226 | "name": "殷果儿",
227 | "alt": "https://movie.douban.com/celebrity/1340984/",
228 | "id": "1340984"
229 | }, {
230 | "avatars": {
231 | "small": "http://img3.doubanio.com/view/celebrity/s_ratio_celebrity/public/p1539679193.26.webp",
232 | "large": "http://img3.doubanio.com/view/celebrity/s_ratio_celebrity/public/p1539679193.26.webp",
233 | "medium": "http://img3.doubanio.com/view/celebrity/s_ratio_celebrity/public/p1539679193.26.webp"
234 | },
235 | "name_en": "Qing'an Ren",
236 | "name": "任青安",
237 | "alt": "https://movie.douban.com/celebrity/1359164/",
238 | "id": "1359164"
239 | }, {
240 | "avatars": {
241 | "small": "http://img1.doubanio.com/view/celebrity/s_ratio_celebrity/public/p1451209491.55.webp",
242 | "large": "http://img1.doubanio.com/view/celebrity/s_ratio_celebrity/public/p1451209491.55.webp",
243 | "medium": "http://img1.doubanio.com/view/celebrity/s_ratio_celebrity/public/p1451209491.55.webp"
244 | },
245 | "name_en": "Sung-goo Kang",
246 | "name": "姜星丘",
247 | "alt": "https://movie.douban.com/celebrity/1353667/",
248 | "id": "1353667"
249 | }, {
250 | "avatars": {
251 | "small": "http://img3.doubanio.com/view/celebrity/s_ratio_celebrity/public/p1478601324.49.webp",
252 | "large": "http://img3.doubanio.com/view/celebrity/s_ratio_celebrity/public/p1478601324.49.webp",
253 | "medium": "http://img3.doubanio.com/view/celebrity/s_ratio_celebrity/public/p1478601324.49.webp"
254 | },
255 | "name_en": "Jiamin Chen",
256 | "name": "陈嘉敏",
257 | "alt": "https://movie.douban.com/celebrity/1340988/",
258 | "id": "1340988"
259 | }
260 | ],
261 | "countries": ["中国大陆"],
262 | "mainland_pubdate": "2017-01-06",
263 | "photos": [
264 | {
265 | "thumb": "https://img3.doubanio.com/view/photo/m/public/p2411789693.webp",
266 | "image": "https://img3.doubanio.com/view/photo/l/public/p2411789693.webp",
267 | "cover": "https://img3.doubanio.com/view/photo/sqs/public/p2411789693.webp",
268 | "alt": "https://movie.douban.com/photos/photo/2411789693/",
269 | "id": "2411789693",
270 | "icon": "https://img3.doubanio.com/view/photo/s/public/p2411789693.webp"
271 | }, {
272 | "thumb": "https://img3.doubanio.com/view/photo/m/public/p2406383762.webp",
273 | "image": "https://img3.doubanio.com/view/photo/l/public/p2406383762.webp",
274 | "cover": "https://img3.doubanio.com/view/photo/sqs/public/p2406383762.webp",
275 | "alt": "https://movie.douban.com/photos/photo/2406383762/",
276 | "id": "2406383762",
277 | "icon": "https://img3.doubanio.com/view/photo/s/public/p2406383762.webp"
278 | }, {
279 | "thumb": "https://img1.doubanio.com/view/photo/m/public/p2411789707.webp",
280 | "image": "https://img1.doubanio.com/view/photo/l/public/p2411789707.webp",
281 | "cover": "https://img1.doubanio.com/view/photo/sqs/public/p2411789707.webp",
282 | "alt": "https://movie.douban.com/photos/photo/2411789707/",
283 | "id": "2411789707",
284 | "icon": "https://img1.doubanio.com/view/photo/s/public/p2411789707.webp"
285 | }, {
286 | "thumb": "https://img3.doubanio.com/view/photo/m/public/p2411789702.webp",
287 | "image": "https://img3.doubanio.com/view/photo/l/public/p2411789702.webp",
288 | "cover": "https://img3.doubanio.com/view/photo/sqs/public/p2411789702.webp",
289 | "alt": "https://movie.douban.com/photos/photo/2411789702/",
290 | "id": "2411789702",
291 | "icon": "https://img3.doubanio.com/view/photo/s/public/p2411789702.webp"
292 | }, {
293 | "thumb": "https://img3.doubanio.com/view/photo/m/public/p2408074732.webp",
294 | "image": "https://img3.doubanio.com/view/photo/l/public/p2408074732.webp",
295 | "cover": "https://img3.doubanio.com/view/photo/sqs/public/p2408074732.webp",
296 | "alt": "https://movie.douban.com/photos/photo/2408074732/",
297 | "id": "2408074732",
298 | "icon": "https://img3.doubanio.com/view/photo/s/public/p2408074732.webp"
299 | }, {
300 | "thumb": "https://img3.doubanio.com/view/photo/m/public/p2408074723.webp",
301 | "image": "https://img3.doubanio.com/view/photo/l/public/p2408074723.webp",
302 | "cover": "https://img3.doubanio.com/view/photo/sqs/public/p2408074723.webp",
303 | "alt": "https://movie.douban.com/photos/photo/2408074723/",
304 | "id": "2408074723",
305 | "icon": "https://img3.doubanio.com/view/photo/s/public/p2408074723.webp"
306 | }, {
307 | "thumb": "https://img3.doubanio.com/view/photo/m/public/p2408074715.webp",
308 | "image": "https://img3.doubanio.com/view/photo/l/public/p2408074715.webp",
309 | "cover": "https://img3.doubanio.com/view/photo/sqs/public/p2408074715.webp",
310 | "alt": "https://movie.douban.com/photos/photo/2408074715/",
311 | "id": "2408074715",
312 | "icon": "https://img3.doubanio.com/view/photo/s/public/p2408074715.webp"
313 | }, {
314 | "thumb": "https://img3.doubanio.com/view/photo/m/public/p2406383761.webp",
315 | "image": "https://img3.doubanio.com/view/photo/l/public/p2406383761.webp",
316 | "cover": "https://img3.doubanio.com/view/photo/sqs/public/p2406383761.webp",
317 | "alt": "https://movie.douban.com/photos/photo/2406383761/",
318 | "id": "2406383761",
319 | "icon": "https://img3.doubanio.com/view/photo/s/public/p2406383761.webp"
320 | }, {
321 | "thumb": "https://img1.doubanio.com/view/photo/m/public/p2406383759.webp",
322 | "image": "https://img1.doubanio.com/view/photo/l/public/p2406383759.webp",
323 | "cover": "https://img1.doubanio.com/view/photo/sqs/public/p2406383759.webp",
324 | "alt": "https://movie.douban.com/photos/photo/2406383759/",
325 | "id": "2406383759",
326 | "icon": "https://img1.doubanio.com/view/photo/s/public/p2406383759.webp"
327 | }, {
328 | "thumb": "https://img3.doubanio.com/view/photo/m/public/p2395927790.webp",
329 | "image": "https://img3.doubanio.com/view/photo/l/public/p2395927790.webp",
330 | "cover": "https://img3.doubanio.com/view/photo/sqs/public/p2395927790.webp",
331 | "alt": "https://movie.douban.com/photos/photo/2395927790/",
332 | "id": "2395927790",
333 | "icon": "https://img3.doubanio.com/view/photo/s/public/p2395927790.webp"
334 | }
335 | ],
336 | "summary": "位于深山小镇的理发店发生的一系列灵异奇闻,殷果儿、任青安、姜星丘等人陷入危难绝境中无法脱身,和理发店有关联的人物接连被惨绝杀害,血腥残暴引来人心惶惶,而抽丝剥茧之后的真相更加令人心惊胆战。",
337 | "clips": [],
338 | "subtype": "movie",
339 | "directors": [
340 | {
341 | "avatars": {
342 | "small": "http://img3.doubanio.com/view/celebrity/s_ratio_celebrity/public/p1490348628.29.webp",
343 | "large": "http://img3.doubanio.com/view/celebrity/s_ratio_celebrity/public/p1490348628.29.webp",
344 | "medium": "http://img3.doubanio.com/view/celebrity/s_ratio_celebrity/public/p1490348628.29.webp"
345 | },
346 | "name_en": "Shilei Lu",
347 | "name": "陆诗雷",
348 | "alt": "https://movie.douban.com/celebrity/1360707/",
349 | "id": "1360707"
350 | }
351 | ],
352 | "comments_count": 248,
353 | "popular_reviews": [
354 | {
355 | "rating": {
356 | "max": 5,
357 | "value": 1,
358 | "min": 0
359 | },
360 | "title": "国产恐怖片,注定成烂片?",
361 | "subject_id": "26865690",
362 | "author": {
363 | "uid": "123404248",
364 | "avatar": "http://img1.doubanio.com/icon/u123404248-5.jpg",
365 | "signature": "",
366 | "alt": "http://www.douban.com/people/123404248/",
367 | "id": "123404248",
368 | "name": "世界奇妙物语"
369 | },
370 | "summary": "这一系列国产恐怖片太多,现在总结下国产电影拍摄门槛为什么这么低…… 1.找个导演,内地导演优先考虑(省钱)。 2.去网上热搜榜(也是经纪公司)上挑几个网红明星(省钱)。网红明星就像木偶一样被装扮上了。 3.去...",
371 | "alt": "https://movie.douban.com/review/8301338/",
372 | "id": "8301338"
373 | }, {
374 | "rating": {
375 | "max": 5,
376 | "value": 1,
377 | "min": 0
378 | },
379 | "title": "导演别拍电影了,快回家陪你父母,不然小心他们扮鬼吓你!",
380 | "subject_id": "26865690",
381 | "author": {
382 | "uid": "BIANJU20170418",
383 | "avatar": "http://img1.doubanio.com/icon/u82851721-3.jpg",
384 | "signature": "",
385 | "alt": "http://www.douban.com/people/BIANJU20170418/",
386 | "id": "82851721",
387 | "name": "游侠一笑"
388 | },
389 | "summary": "《恐怖游泳馆》、《恐怖电影院》,恐怖厕所、恐怖你妈隔壁,继“诡”、“惊魂”、“灵”、“怨”后,国产可怕片的片名誓要在“恐怖”路上走到底。 一连看了三部菲尔幕出品的国产恐怖片,这也够恐怖的,还是那句...",
390 | "alt": "https://movie.douban.com/review/8578229/",
391 | "id": "8578229"
392 | }, {
393 | "rating": {
394 | "max": 5,
395 | "value": 1,
396 | "min": 0
397 | },
398 | "title": "这lj电影我还是去电影院看的 看了20分钟我就出来了什么j8玩意",
399 | "subject_id": "26865690",
400 | "author": {
401 | "uid": "u43434343",
402 | "avatar": "http://img1.doubanio.com/icon/u85207511-1.jpg",
403 | "signature": "失之东隅,收之桑榆",
404 | "alt": "http://www.douban.com/people/u43434343/",
405 | "id": "85207511",
406 | "name": "我是传奇"
407 | },
408 | "summary": "这lj电影我还是去电影院看的 看了20分钟我就出来了 什么j8玩意 这lj电影我还是去电影院看的 看了20分钟我就出来了 什么j8玩意 这lj电影我还是去电影院看的 看了20分钟我就出来了 什么j8玩意 这lj电影我...",
409 | "alt": "https://movie.douban.com/review/9605462/",
410 | "id": "9605462"
411 | }, {
412 | "rating": {
413 | "max": 5,
414 | "value": 5,
415 | "min": 0
416 | },
417 | "title": "我看到了国产恐怖片的新希望!",
418 | "subject_id": "26865690",
419 | "author": {
420 | "uid": "177624181",
421 | "avatar": "http://img1.doubanio.com/icon/u177624181-3.jpg",
422 | "signature": "",
423 | "alt": "http://www.douban.com/people/177624181/",
424 | "id": "177624181",
425 | "name": "Shamless"
426 | },
427 | "summary": "说句良心话,恐怖片我看了也不下一万部了,古今中外无所不览。没办法,人闲是非多,就找点恐怖片打发时间。可是豚鼠系列,大头怪婴,下水道杀手,笔仙贞子碟仙,聊斋吸血鬼荒村,总之各种各样的吧,我是没有见过...",
428 | "alt": "https://movie.douban.com/review/9315542/",
429 | "id": "9315542"
430 | }, {
431 | "rating": {
432 | "max": 5,
433 | "value": 1,
434 | "min": 0
435 | },
436 | "title": "差到不行",
437 | "subject_id": "26865690",
438 | "author": {
439 | "uid": "158559795",
440 | "avatar": "http://img3.doubanio.com/icon/user_normal.jpg",
441 | "signature": "",
442 | "alt": "http://www.douban.com/people/158559795/",
443 | "id": "158559795",
444 | "name": "依旧箜絔"
445 | },
446 | "summary": "真的很烂 很烂 成了喜剧 如果评论涉及电影和小说的结局和关键情节,请勾选「有关键情节透露」,豆瓣将显示提示,以免没有看过的人扫兴。 为了尊重创作者的劳动,请不要转载他人文章或提供下载信息。豆瓣鼓励有益...",
447 | "alt": "https://movie.douban.com/review/8394178/",
448 | "id": "8394178"
449 | }, {
450 | "rating": {
451 | "max": 5,
452 | "value": 1,
453 | "min": 0
454 | },
455 | "title": "2017年1月14日",
456 | "subject_id": "26865690",
457 | "author": {
458 | "uid": "106658069",
459 | "avatar": "http://img1.doubanio.com/icon/u106658069-4.jpg",
460 | "signature": "",
461 | "alt": "http://www.douban.com/people/106658069/",
462 | "id": "106658069",
463 | "name": "两两"
464 | },
465 | "summary": "小萌说要去看,从头到尾全是槽点,这剧本无论怎么拍都不会好了…怪不得邓sir对我写的鬼故事如此有信心,因为大家都是这水平吗… 不过老实说,这个编剧犯的错误我也犯过:故事和线索不集中。写《杀人犯》的时候,...",
466 | "alt": "https://movie.douban.com/review/8823383/",
467 | "id": "8823383"
468 | }, {
469 | "rating": {
470 | "max": 5,
471 | "value": 1,
472 | "min": 0
473 | },
474 | "title": "老套路没创意",
475 | "subject_id": "26865690",
476 | "author": {
477 | "uid": "149343489",
478 | "avatar": "http://img1.doubanio.com/icon/u149343489-1.jpg",
479 | "signature": "",
480 | "alt": "http://www.douban.com/people/149343489/",
481 | "id": "149343489",
482 | "name": "🗿"
483 | },
484 | "summary": "烂片 嘈点太多了好吗 前面刚开始有鬼出现 后面大部分都是情感戏 最后结果又是人为扮鬼 很多现象也是无法解释的 电为什么说停就停 为什么里面的人可以轻松找到模特厘米的代号? 每个人那么容易认出自己的手掌印 ?...",
485 | "alt": "https://movie.douban.com/review/8278482/",
486 | "id": "8278482"
487 | }, {
488 | "rating": {
489 | "max": 5,
490 | "value": 1,
491 | "min": 0
492 | },
493 | "title": "?",
494 | "subject_id": "26865690",
495 | "author": {
496 | "uid": "154276285",
497 | "avatar": "http://img1.doubanio.com/icon/u154276285-1.jpg",
498 | "signature": "",
499 | "alt": "http://www.douban.com/people/154276285/",
500 | "id": "154276285",
501 | "name": "👧"
502 | },
503 | "summary": "超级烂片,让她爹玩一宿,预告片剪辑不错,此片看完预告片即可,看了多余,漏洞百出,穿帮镜头无数,无厘头到了极致。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。...",
504 | "alt": "https://movie.douban.com/review/8278145/",
505 | "id": "8278145"
506 | }, {
507 | "rating": {
508 | "max": 5,
509 | "value": 4,
510 | "min": 0
511 | },
512 | "title": "《恐怖理发店》:青丝犹在,魂魄已飞",
513 | "subject_id": "26865690",
514 | "author": {
515 | "uid": "41576647",
516 | "avatar": "http://img1.doubanio.com/icon/u41576647-3.jpg",
517 | "signature": "",
518 | "alt": "http://www.douban.com/people/41576647/",
519 | "id": "41576647",
520 | "name": "丑鱼尼莫"
521 | },
522 | "summary": "《恐怖理发店》讲述的是一个发生在理发店的灵异事件,而灵异的背后,总有一些说不清道不明的真相在作祟。但是,当真相一点点水落石出的时候,又总会叫人心悸、惊厥,毛骨悚然,不寒而栗的感觉也悄上心头。 荒山...",
523 | "alt": "https://movie.douban.com/review/8239886/",
524 | "id": "8239886"
525 | }, {
526 | "rating": {
527 | "max": 5,
528 | "value": 4,
529 | "min": 0
530 | },
531 | "title": "Word天呀!以后再也不敢去理发店了",
532 | "subject_id": "26865690",
533 | "author": {
534 | "uid": "70359207",
535 | "avatar": "http://img3.doubanio.com/icon/u70359207-8.jpg",
536 | "signature": "百度百家、今日头条作家、影评人",
537 | "alt": "http://www.douban.com/people/70359207/",
538 | "id": "70359207",
539 | "name": "大侃"
540 | },
541 | "summary": " 惊悚、恐怖类的影片,每周都在影院里现身,不但有固定的消费群体和受众,还时不时灵光一闪在票房上创出佳绩,《恐怖游泳馆》、《床下有人》、《枕边有张脸》等都是其中的代表。当下,观众的欣赏口味不断提升,...",
542 | "alt": "https://movie.douban.com/review/8239440/",
543 | "id": "8239440"
544 | }
545 | ],
546 | "ratings_count": 677,
547 | "aka": ["Ghost in Barber's"]
548 | }
549 | }
550 |
--------------------------------------------------------------------------------
/client/font-awesome-4.7.0/css/font-awesome.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome
3 | * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License)
4 | */
5 | /* FONT PATH
6 | * -------------------------- */
7 | @font-face {
8 | font-family: 'FontAwesome';
9 | src: url('../fonts/fontawesome-webfont.eot?v=4.7.0');
10 | src: url('../fonts/fontawesome-webfont.eot?#iefix&v=4.7.0') format('embedded-opentype'), url('../fonts/fontawesome-webfont.woff2?v=4.7.0') format('woff2'), url('../fonts/fontawesome-webfont.woff?v=4.7.0') format('woff'), url('../fonts/fontawesome-webfont.ttf?v=4.7.0') format('truetype'), url('../fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular') format('svg');
11 | font-weight: normal;
12 | font-style: normal;
13 | }
14 | .fa {
15 | display: inline-block;
16 | font: normal normal normal 14px/1 FontAwesome;
17 | font-size: inherit;
18 | text-rendering: auto;
19 | -webkit-font-smoothing: antialiased;
20 | -moz-osx-font-smoothing: grayscale;
21 | }
22 | /* makes the font 33% larger relative to the icon container */
23 | .fa-lg {
24 | font-size: 1.33333333em;
25 | line-height: 0.75em;
26 | vertical-align: -15%;
27 | }
28 | .fa-2x {
29 | font-size: 2em;
30 | }
31 | .fa-3x {
32 | font-size: 3em;
33 | }
34 | .fa-4x {
35 | font-size: 4em;
36 | }
37 | .fa-5x {
38 | font-size: 5em;
39 | }
40 | .fa-fw {
41 | width: 1.28571429em;
42 | text-align: center;
43 | }
44 | .fa-ul {
45 | padding-left: 0;
46 | margin-left: 2.14285714em;
47 | list-style-type: none;
48 | }
49 | .fa-ul > li {
50 | position: relative;
51 | }
52 | .fa-li {
53 | position: absolute;
54 | left: -2.14285714em;
55 | width: 2.14285714em;
56 | top: 0.14285714em;
57 | text-align: center;
58 | }
59 | .fa-li.fa-lg {
60 | left: -1.85714286em;
61 | }
62 | .fa-border {
63 | padding: .2em .25em .15em;
64 | border: solid 0.08em #eeeeee;
65 | border-radius: .1em;
66 | }
67 | .fa-pull-left {
68 | float: left;
69 | }
70 | .fa-pull-right {
71 | float: right;
72 | }
73 | .fa.fa-pull-left {
74 | margin-right: .3em;
75 | }
76 | .fa.fa-pull-right {
77 | margin-left: .3em;
78 | }
79 | /* Deprecated as of 4.4.0 */
80 | .pull-right {
81 | float: right;
82 | }
83 | .pull-left {
84 | float: left;
85 | }
86 | .fa.pull-left {
87 | margin-right: .3em;
88 | }
89 | .fa.pull-right {
90 | margin-left: .3em;
91 | }
92 | .fa-spin {
93 | -webkit-animation: fa-spin 2s infinite linear;
94 | animation: fa-spin 2s infinite linear;
95 | }
96 | .fa-pulse {
97 | -webkit-animation: fa-spin 1s infinite steps(8);
98 | animation: fa-spin 1s infinite steps(8);
99 | }
100 | @-webkit-keyframes fa-spin {
101 | 0% {
102 | -webkit-transform: rotate(0deg);
103 | transform: rotate(0deg);
104 | }
105 | 100% {
106 | -webkit-transform: rotate(359deg);
107 | transform: rotate(359deg);
108 | }
109 | }
110 | @keyframes fa-spin {
111 | 0% {
112 | -webkit-transform: rotate(0deg);
113 | transform: rotate(0deg);
114 | }
115 | 100% {
116 | -webkit-transform: rotate(359deg);
117 | transform: rotate(359deg);
118 | }
119 | }
120 | .fa-rotate-90 {
121 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";
122 | -webkit-transform: rotate(90deg);
123 | -ms-transform: rotate(90deg);
124 | transform: rotate(90deg);
125 | }
126 | .fa-rotate-180 {
127 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";
128 | -webkit-transform: rotate(180deg);
129 | -ms-transform: rotate(180deg);
130 | transform: rotate(180deg);
131 | }
132 | .fa-rotate-270 {
133 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";
134 | -webkit-transform: rotate(270deg);
135 | -ms-transform: rotate(270deg);
136 | transform: rotate(270deg);
137 | }
138 | .fa-flip-horizontal {
139 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";
140 | -webkit-transform: scale(-1, 1);
141 | -ms-transform: scale(-1, 1);
142 | transform: scale(-1, 1);
143 | }
144 | .fa-flip-vertical {
145 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";
146 | -webkit-transform: scale(1, -1);
147 | -ms-transform: scale(1, -1);
148 | transform: scale(1, -1);
149 | }
150 | :root .fa-rotate-90,
151 | :root .fa-rotate-180,
152 | :root .fa-rotate-270,
153 | :root .fa-flip-horizontal,
154 | :root .fa-flip-vertical {
155 | filter: none;
156 | }
157 | .fa-stack {
158 | position: relative;
159 | display: inline-block;
160 | width: 2em;
161 | height: 2em;
162 | line-height: 2em;
163 | vertical-align: middle;
164 | }
165 | .fa-stack-1x,
166 | .fa-stack-2x {
167 | position: absolute;
168 | left: 0;
169 | width: 100%;
170 | text-align: center;
171 | }
172 | .fa-stack-1x {
173 | line-height: inherit;
174 | }
175 | .fa-stack-2x {
176 | font-size: 2em;
177 | }
178 | .fa-inverse {
179 | color: #ffffff;
180 | }
181 | /* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen
182 | readers do not read off random characters that represent icons */
183 | .fa-glass:before {
184 | content: "\f000";
185 | }
186 | .fa-music:before {
187 | content: "\f001";
188 | }
189 | .fa-search:before {
190 | content: "\f002";
191 | }
192 | .fa-envelope-o:before {
193 | content: "\f003";
194 | }
195 | .fa-heart:before {
196 | content: "\f004";
197 | }
198 | .fa-star:before {
199 | content: "\f005";
200 | }
201 | .fa-star-o:before {
202 | content: "\f006";
203 | }
204 | .fa-user:before {
205 | content: "\f007";
206 | }
207 | .fa-film:before {
208 | content: "\f008";
209 | }
210 | .fa-th-large:before {
211 | content: "\f009";
212 | }
213 | .fa-th:before {
214 | content: "\f00a";
215 | }
216 | .fa-th-list:before {
217 | content: "\f00b";
218 | }
219 | .fa-check:before {
220 | content: "\f00c";
221 | }
222 | .fa-remove:before,
223 | .fa-close:before,
224 | .fa-times:before {
225 | content: "\f00d";
226 | }
227 | .fa-search-plus:before {
228 | content: "\f00e";
229 | }
230 | .fa-search-minus:before {
231 | content: "\f010";
232 | }
233 | .fa-power-off:before {
234 | content: "\f011";
235 | }
236 | .fa-signal:before {
237 | content: "\f012";
238 | }
239 | .fa-gear:before,
240 | .fa-cog:before {
241 | content: "\f013";
242 | }
243 | .fa-trash-o:before {
244 | content: "\f014";
245 | }
246 | .fa-home:before {
247 | content: "\f015";
248 | }
249 | .fa-file-o:before {
250 | content: "\f016";
251 | }
252 | .fa-clock-o:before {
253 | content: "\f017";
254 | }
255 | .fa-road:before {
256 | content: "\f018";
257 | }
258 | .fa-download:before {
259 | content: "\f019";
260 | }
261 | .fa-arrow-circle-o-down:before {
262 | content: "\f01a";
263 | }
264 | .fa-arrow-circle-o-up:before {
265 | content: "\f01b";
266 | }
267 | .fa-inbox:before {
268 | content: "\f01c";
269 | }
270 | .fa-play-circle-o:before {
271 | content: "\f01d";
272 | }
273 | .fa-rotate-right:before,
274 | .fa-repeat:before {
275 | content: "\f01e";
276 | }
277 | .fa-refresh:before {
278 | content: "\f021";
279 | }
280 | .fa-list-alt:before {
281 | content: "\f022";
282 | }
283 | .fa-lock:before {
284 | content: "\f023";
285 | }
286 | .fa-flag:before {
287 | content: "\f024";
288 | }
289 | .fa-headphones:before {
290 | content: "\f025";
291 | }
292 | .fa-volume-off:before {
293 | content: "\f026";
294 | }
295 | .fa-volume-down:before {
296 | content: "\f027";
297 | }
298 | .fa-volume-up:before {
299 | content: "\f028";
300 | }
301 | .fa-qrcode:before {
302 | content: "\f029";
303 | }
304 | .fa-barcode:before {
305 | content: "\f02a";
306 | }
307 | .fa-tag:before {
308 | content: "\f02b";
309 | }
310 | .fa-tags:before {
311 | content: "\f02c";
312 | }
313 | .fa-book:before {
314 | content: "\f02d";
315 | }
316 | .fa-bookmark:before {
317 | content: "\f02e";
318 | }
319 | .fa-print:before {
320 | content: "\f02f";
321 | }
322 | .fa-camera:before {
323 | content: "\f030";
324 | }
325 | .fa-font:before {
326 | content: "\f031";
327 | }
328 | .fa-bold:before {
329 | content: "\f032";
330 | }
331 | .fa-italic:before {
332 | content: "\f033";
333 | }
334 | .fa-text-height:before {
335 | content: "\f034";
336 | }
337 | .fa-text-width:before {
338 | content: "\f035";
339 | }
340 | .fa-align-left:before {
341 | content: "\f036";
342 | }
343 | .fa-align-center:before {
344 | content: "\f037";
345 | }
346 | .fa-align-right:before {
347 | content: "\f038";
348 | }
349 | .fa-align-justify:before {
350 | content: "\f039";
351 | }
352 | .fa-list:before {
353 | content: "\f03a";
354 | }
355 | .fa-dedent:before,
356 | .fa-outdent:before {
357 | content: "\f03b";
358 | }
359 | .fa-indent:before {
360 | content: "\f03c";
361 | }
362 | .fa-video-camera:before {
363 | content: "\f03d";
364 | }
365 | .fa-photo:before,
366 | .fa-image:before,
367 | .fa-picture-o:before {
368 | content: "\f03e";
369 | }
370 | .fa-pencil:before {
371 | content: "\f040";
372 | }
373 | .fa-map-marker:before {
374 | content: "\f041";
375 | }
376 | .fa-adjust:before {
377 | content: "\f042";
378 | }
379 | .fa-tint:before {
380 | content: "\f043";
381 | }
382 | .fa-edit:before,
383 | .fa-pencil-square-o:before {
384 | content: "\f044";
385 | }
386 | .fa-share-square-o:before {
387 | content: "\f045";
388 | }
389 | .fa-check-square-o:before {
390 | content: "\f046";
391 | }
392 | .fa-arrows:before {
393 | content: "\f047";
394 | }
395 | .fa-step-backward:before {
396 | content: "\f048";
397 | }
398 | .fa-fast-backward:before {
399 | content: "\f049";
400 | }
401 | .fa-backward:before {
402 | content: "\f04a";
403 | }
404 | .fa-play:before {
405 | content: "\f04b";
406 | }
407 | .fa-pause:before {
408 | content: "\f04c";
409 | }
410 | .fa-stop:before {
411 | content: "\f04d";
412 | }
413 | .fa-forward:before {
414 | content: "\f04e";
415 | }
416 | .fa-fast-forward:before {
417 | content: "\f050";
418 | }
419 | .fa-step-forward:before {
420 | content: "\f051";
421 | }
422 | .fa-eject:before {
423 | content: "\f052";
424 | }
425 | .fa-chevron-left:before {
426 | content: "\f053";
427 | }
428 | .fa-chevron-right:before {
429 | content: "\f054";
430 | }
431 | .fa-plus-circle:before {
432 | content: "\f055";
433 | }
434 | .fa-minus-circle:before {
435 | content: "\f056";
436 | }
437 | .fa-times-circle:before {
438 | content: "\f057";
439 | }
440 | .fa-check-circle:before {
441 | content: "\f058";
442 | }
443 | .fa-question-circle:before {
444 | content: "\f059";
445 | }
446 | .fa-info-circle:before {
447 | content: "\f05a";
448 | }
449 | .fa-crosshairs:before {
450 | content: "\f05b";
451 | }
452 | .fa-times-circle-o:before {
453 | content: "\f05c";
454 | }
455 | .fa-check-circle-o:before {
456 | content: "\f05d";
457 | }
458 | .fa-ban:before {
459 | content: "\f05e";
460 | }
461 | .fa-arrow-left:before {
462 | content: "\f060";
463 | }
464 | .fa-arrow-right:before {
465 | content: "\f061";
466 | }
467 | .fa-arrow-up:before {
468 | content: "\f062";
469 | }
470 | .fa-arrow-down:before {
471 | content: "\f063";
472 | }
473 | .fa-mail-forward:before,
474 | .fa-share:before {
475 | content: "\f064";
476 | }
477 | .fa-expand:before {
478 | content: "\f065";
479 | }
480 | .fa-compress:before {
481 | content: "\f066";
482 | }
483 | .fa-plus:before {
484 | content: "\f067";
485 | }
486 | .fa-minus:before {
487 | content: "\f068";
488 | }
489 | .fa-asterisk:before {
490 | content: "\f069";
491 | }
492 | .fa-exclamation-circle:before {
493 | content: "\f06a";
494 | }
495 | .fa-gift:before {
496 | content: "\f06b";
497 | }
498 | .fa-leaf:before {
499 | content: "\f06c";
500 | }
501 | .fa-fire:before {
502 | content: "\f06d";
503 | }
504 | .fa-eye:before {
505 | content: "\f06e";
506 | }
507 | .fa-eye-slash:before {
508 | content: "\f070";
509 | }
510 | .fa-warning:before,
511 | .fa-exclamation-triangle:before {
512 | content: "\f071";
513 | }
514 | .fa-plane:before {
515 | content: "\f072";
516 | }
517 | .fa-calendar:before {
518 | content: "\f073";
519 | }
520 | .fa-random:before {
521 | content: "\f074";
522 | }
523 | .fa-comment:before {
524 | content: "\f075";
525 | }
526 | .fa-magnet:before {
527 | content: "\f076";
528 | }
529 | .fa-chevron-up:before {
530 | content: "\f077";
531 | }
532 | .fa-chevron-down:before {
533 | content: "\f078";
534 | }
535 | .fa-retweet:before {
536 | content: "\f079";
537 | }
538 | .fa-shopping-cart:before {
539 | content: "\f07a";
540 | }
541 | .fa-folder:before {
542 | content: "\f07b";
543 | }
544 | .fa-folder-open:before {
545 | content: "\f07c";
546 | }
547 | .fa-arrows-v:before {
548 | content: "\f07d";
549 | }
550 | .fa-arrows-h:before {
551 | content: "\f07e";
552 | }
553 | .fa-bar-chart-o:before,
554 | .fa-bar-chart:before {
555 | content: "\f080";
556 | }
557 | .fa-twitter-square:before {
558 | content: "\f081";
559 | }
560 | .fa-facebook-square:before {
561 | content: "\f082";
562 | }
563 | .fa-camera-retro:before {
564 | content: "\f083";
565 | }
566 | .fa-key:before {
567 | content: "\f084";
568 | }
569 | .fa-gears:before,
570 | .fa-cogs:before {
571 | content: "\f085";
572 | }
573 | .fa-comments:before {
574 | content: "\f086";
575 | }
576 | .fa-thumbs-o-up:before {
577 | content: "\f087";
578 | }
579 | .fa-thumbs-o-down:before {
580 | content: "\f088";
581 | }
582 | .fa-star-half:before {
583 | content: "\f089";
584 | }
585 | .fa-heart-o:before {
586 | content: "\f08a";
587 | }
588 | .fa-sign-out:before {
589 | content: "\f08b";
590 | }
591 | .fa-linkedin-square:before {
592 | content: "\f08c";
593 | }
594 | .fa-thumb-tack:before {
595 | content: "\f08d";
596 | }
597 | .fa-external-link:before {
598 | content: "\f08e";
599 | }
600 | .fa-sign-in:before {
601 | content: "\f090";
602 | }
603 | .fa-trophy:before {
604 | content: "\f091";
605 | }
606 | .fa-github-square:before {
607 | content: "\f092";
608 | }
609 | .fa-upload:before {
610 | content: "\f093";
611 | }
612 | .fa-lemon-o:before {
613 | content: "\f094";
614 | }
615 | .fa-phone:before {
616 | content: "\f095";
617 | }
618 | .fa-square-o:before {
619 | content: "\f096";
620 | }
621 | .fa-bookmark-o:before {
622 | content: "\f097";
623 | }
624 | .fa-phone-square:before {
625 | content: "\f098";
626 | }
627 | .fa-twitter:before {
628 | content: "\f099";
629 | }
630 | .fa-facebook-f:before,
631 | .fa-facebook:before {
632 | content: "\f09a";
633 | }
634 | .fa-github:before {
635 | content: "\f09b";
636 | }
637 | .fa-unlock:before {
638 | content: "\f09c";
639 | }
640 | .fa-credit-card:before {
641 | content: "\f09d";
642 | }
643 | .fa-feed:before,
644 | .fa-rss:before {
645 | content: "\f09e";
646 | }
647 | .fa-hdd-o:before {
648 | content: "\f0a0";
649 | }
650 | .fa-bullhorn:before {
651 | content: "\f0a1";
652 | }
653 | .fa-bell:before {
654 | content: "\f0f3";
655 | }
656 | .fa-certificate:before {
657 | content: "\f0a3";
658 | }
659 | .fa-hand-o-right:before {
660 | content: "\f0a4";
661 | }
662 | .fa-hand-o-left:before {
663 | content: "\f0a5";
664 | }
665 | .fa-hand-o-up:before {
666 | content: "\f0a6";
667 | }
668 | .fa-hand-o-down:before {
669 | content: "\f0a7";
670 | }
671 | .fa-arrow-circle-left:before {
672 | content: "\f0a8";
673 | }
674 | .fa-arrow-circle-right:before {
675 | content: "\f0a9";
676 | }
677 | .fa-arrow-circle-up:before {
678 | content: "\f0aa";
679 | }
680 | .fa-arrow-circle-down:before {
681 | content: "\f0ab";
682 | }
683 | .fa-globe:before {
684 | content: "\f0ac";
685 | }
686 | .fa-wrench:before {
687 | content: "\f0ad";
688 | }
689 | .fa-tasks:before {
690 | content: "\f0ae";
691 | }
692 | .fa-filter:before {
693 | content: "\f0b0";
694 | }
695 | .fa-briefcase:before {
696 | content: "\f0b1";
697 | }
698 | .fa-arrows-alt:before {
699 | content: "\f0b2";
700 | }
701 | .fa-group:before,
702 | .fa-users:before {
703 | content: "\f0c0";
704 | }
705 | .fa-chain:before,
706 | .fa-link:before {
707 | content: "\f0c1";
708 | }
709 | .fa-cloud:before {
710 | content: "\f0c2";
711 | }
712 | .fa-flask:before {
713 | content: "\f0c3";
714 | }
715 | .fa-cut:before,
716 | .fa-scissors:before {
717 | content: "\f0c4";
718 | }
719 | .fa-copy:before,
720 | .fa-files-o:before {
721 | content: "\f0c5";
722 | }
723 | .fa-paperclip:before {
724 | content: "\f0c6";
725 | }
726 | .fa-save:before,
727 | .fa-floppy-o:before {
728 | content: "\f0c7";
729 | }
730 | .fa-square:before {
731 | content: "\f0c8";
732 | }
733 | .fa-navicon:before,
734 | .fa-reorder:before,
735 | .fa-bars:before {
736 | content: "\f0c9";
737 | }
738 | .fa-list-ul:before {
739 | content: "\f0ca";
740 | }
741 | .fa-list-ol:before {
742 | content: "\f0cb";
743 | }
744 | .fa-strikethrough:before {
745 | content: "\f0cc";
746 | }
747 | .fa-underline:before {
748 | content: "\f0cd";
749 | }
750 | .fa-table:before {
751 | content: "\f0ce";
752 | }
753 | .fa-magic:before {
754 | content: "\f0d0";
755 | }
756 | .fa-truck:before {
757 | content: "\f0d1";
758 | }
759 | .fa-pinterest:before {
760 | content: "\f0d2";
761 | }
762 | .fa-pinterest-square:before {
763 | content: "\f0d3";
764 | }
765 | .fa-google-plus-square:before {
766 | content: "\f0d4";
767 | }
768 | .fa-google-plus:before {
769 | content: "\f0d5";
770 | }
771 | .fa-money:before {
772 | content: "\f0d6";
773 | }
774 | .fa-caret-down:before {
775 | content: "\f0d7";
776 | }
777 | .fa-caret-up:before {
778 | content: "\f0d8";
779 | }
780 | .fa-caret-left:before {
781 | content: "\f0d9";
782 | }
783 | .fa-caret-right:before {
784 | content: "\f0da";
785 | }
786 | .fa-columns:before {
787 | content: "\f0db";
788 | }
789 | .fa-unsorted:before,
790 | .fa-sort:before {
791 | content: "\f0dc";
792 | }
793 | .fa-sort-down:before,
794 | .fa-sort-desc:before {
795 | content: "\f0dd";
796 | }
797 | .fa-sort-up:before,
798 | .fa-sort-asc:before {
799 | content: "\f0de";
800 | }
801 | .fa-envelope:before {
802 | content: "\f0e0";
803 | }
804 | .fa-linkedin:before {
805 | content: "\f0e1";
806 | }
807 | .fa-rotate-left:before,
808 | .fa-undo:before {
809 | content: "\f0e2";
810 | }
811 | .fa-legal:before,
812 | .fa-gavel:before {
813 | content: "\f0e3";
814 | }
815 | .fa-dashboard:before,
816 | .fa-tachometer:before {
817 | content: "\f0e4";
818 | }
819 | .fa-comment-o:before {
820 | content: "\f0e5";
821 | }
822 | .fa-comments-o:before {
823 | content: "\f0e6";
824 | }
825 | .fa-flash:before,
826 | .fa-bolt:before {
827 | content: "\f0e7";
828 | }
829 | .fa-sitemap:before {
830 | content: "\f0e8";
831 | }
832 | .fa-umbrella:before {
833 | content: "\f0e9";
834 | }
835 | .fa-paste:before,
836 | .fa-clipboard:before {
837 | content: "\f0ea";
838 | }
839 | .fa-lightbulb-o:before {
840 | content: "\f0eb";
841 | }
842 | .fa-exchange:before {
843 | content: "\f0ec";
844 | }
845 | .fa-cloud-download:before {
846 | content: "\f0ed";
847 | }
848 | .fa-cloud-upload:before {
849 | content: "\f0ee";
850 | }
851 | .fa-user-md:before {
852 | content: "\f0f0";
853 | }
854 | .fa-stethoscope:before {
855 | content: "\f0f1";
856 | }
857 | .fa-suitcase:before {
858 | content: "\f0f2";
859 | }
860 | .fa-bell-o:before {
861 | content: "\f0a2";
862 | }
863 | .fa-coffee:before {
864 | content: "\f0f4";
865 | }
866 | .fa-cutlery:before {
867 | content: "\f0f5";
868 | }
869 | .fa-file-text-o:before {
870 | content: "\f0f6";
871 | }
872 | .fa-building-o:before {
873 | content: "\f0f7";
874 | }
875 | .fa-hospital-o:before {
876 | content: "\f0f8";
877 | }
878 | .fa-ambulance:before {
879 | content: "\f0f9";
880 | }
881 | .fa-medkit:before {
882 | content: "\f0fa";
883 | }
884 | .fa-fighter-jet:before {
885 | content: "\f0fb";
886 | }
887 | .fa-beer:before {
888 | content: "\f0fc";
889 | }
890 | .fa-h-square:before {
891 | content: "\f0fd";
892 | }
893 | .fa-plus-square:before {
894 | content: "\f0fe";
895 | }
896 | .fa-angle-double-left:before {
897 | content: "\f100";
898 | }
899 | .fa-angle-double-right:before {
900 | content: "\f101";
901 | }
902 | .fa-angle-double-up:before {
903 | content: "\f102";
904 | }
905 | .fa-angle-double-down:before {
906 | content: "\f103";
907 | }
908 | .fa-angle-left:before {
909 | content: "\f104";
910 | }
911 | .fa-angle-right:before {
912 | content: "\f105";
913 | }
914 | .fa-angle-up:before {
915 | content: "\f106";
916 | }
917 | .fa-angle-down:before {
918 | content: "\f107";
919 | }
920 | .fa-desktop:before {
921 | content: "\f108";
922 | }
923 | .fa-laptop:before {
924 | content: "\f109";
925 | }
926 | .fa-tablet:before {
927 | content: "\f10a";
928 | }
929 | .fa-mobile-phone:before,
930 | .fa-mobile:before {
931 | content: "\f10b";
932 | }
933 | .fa-circle-o:before {
934 | content: "\f10c";
935 | }
936 | .fa-quote-left:before {
937 | content: "\f10d";
938 | }
939 | .fa-quote-right:before {
940 | content: "\f10e";
941 | }
942 | .fa-spinner:before {
943 | content: "\f110";
944 | }
945 | .fa-circle:before {
946 | content: "\f111";
947 | }
948 | .fa-mail-reply:before,
949 | .fa-reply:before {
950 | content: "\f112";
951 | }
952 | .fa-github-alt:before {
953 | content: "\f113";
954 | }
955 | .fa-folder-o:before {
956 | content: "\f114";
957 | }
958 | .fa-folder-open-o:before {
959 | content: "\f115";
960 | }
961 | .fa-smile-o:before {
962 | content: "\f118";
963 | }
964 | .fa-frown-o:before {
965 | content: "\f119";
966 | }
967 | .fa-meh-o:before {
968 | content: "\f11a";
969 | }
970 | .fa-gamepad:before {
971 | content: "\f11b";
972 | }
973 | .fa-keyboard-o:before {
974 | content: "\f11c";
975 | }
976 | .fa-flag-o:before {
977 | content: "\f11d";
978 | }
979 | .fa-flag-checkered:before {
980 | content: "\f11e";
981 | }
982 | .fa-terminal:before {
983 | content: "\f120";
984 | }
985 | .fa-code:before {
986 | content: "\f121";
987 | }
988 | .fa-mail-reply-all:before,
989 | .fa-reply-all:before {
990 | content: "\f122";
991 | }
992 | .fa-star-half-empty:before,
993 | .fa-star-half-full:before,
994 | .fa-star-half-o:before {
995 | content: "\f123";
996 | }
997 | .fa-location-arrow:before {
998 | content: "\f124";
999 | }
1000 | .fa-crop:before {
1001 | content: "\f125";
1002 | }
1003 | .fa-code-fork:before {
1004 | content: "\f126";
1005 | }
1006 | .fa-unlink:before,
1007 | .fa-chain-broken:before {
1008 | content: "\f127";
1009 | }
1010 | .fa-question:before {
1011 | content: "\f128";
1012 | }
1013 | .fa-info:before {
1014 | content: "\f129";
1015 | }
1016 | .fa-exclamation:before {
1017 | content: "\f12a";
1018 | }
1019 | .fa-superscript:before {
1020 | content: "\f12b";
1021 | }
1022 | .fa-subscript:before {
1023 | content: "\f12c";
1024 | }
1025 | .fa-eraser:before {
1026 | content: "\f12d";
1027 | }
1028 | .fa-puzzle-piece:before {
1029 | content: "\f12e";
1030 | }
1031 | .fa-microphone:before {
1032 | content: "\f130";
1033 | }
1034 | .fa-microphone-slash:before {
1035 | content: "\f131";
1036 | }
1037 | .fa-shield:before {
1038 | content: "\f132";
1039 | }
1040 | .fa-calendar-o:before {
1041 | content: "\f133";
1042 | }
1043 | .fa-fire-extinguisher:before {
1044 | content: "\f134";
1045 | }
1046 | .fa-rocket:before {
1047 | content: "\f135";
1048 | }
1049 | .fa-maxcdn:before {
1050 | content: "\f136";
1051 | }
1052 | .fa-chevron-circle-left:before {
1053 | content: "\f137";
1054 | }
1055 | .fa-chevron-circle-right:before {
1056 | content: "\f138";
1057 | }
1058 | .fa-chevron-circle-up:before {
1059 | content: "\f139";
1060 | }
1061 | .fa-chevron-circle-down:before {
1062 | content: "\f13a";
1063 | }
1064 | .fa-html5:before {
1065 | content: "\f13b";
1066 | }
1067 | .fa-css3:before {
1068 | content: "\f13c";
1069 | }
1070 | .fa-anchor:before {
1071 | content: "\f13d";
1072 | }
1073 | .fa-unlock-alt:before {
1074 | content: "\f13e";
1075 | }
1076 | .fa-bullseye:before {
1077 | content: "\f140";
1078 | }
1079 | .fa-ellipsis-h:before {
1080 | content: "\f141";
1081 | }
1082 | .fa-ellipsis-v:before {
1083 | content: "\f142";
1084 | }
1085 | .fa-rss-square:before {
1086 | content: "\f143";
1087 | }
1088 | .fa-play-circle:before {
1089 | content: "\f144";
1090 | }
1091 | .fa-ticket:before {
1092 | content: "\f145";
1093 | }
1094 | .fa-minus-square:before {
1095 | content: "\f146";
1096 | }
1097 | .fa-minus-square-o:before {
1098 | content: "\f147";
1099 | }
1100 | .fa-level-up:before {
1101 | content: "\f148";
1102 | }
1103 | .fa-level-down:before {
1104 | content: "\f149";
1105 | }
1106 | .fa-check-square:before {
1107 | content: "\f14a";
1108 | }
1109 | .fa-pencil-square:before {
1110 | content: "\f14b";
1111 | }
1112 | .fa-external-link-square:before {
1113 | content: "\f14c";
1114 | }
1115 | .fa-share-square:before {
1116 | content: "\f14d";
1117 | }
1118 | .fa-compass:before {
1119 | content: "\f14e";
1120 | }
1121 | .fa-toggle-down:before,
1122 | .fa-caret-square-o-down:before {
1123 | content: "\f150";
1124 | }
1125 | .fa-toggle-up:before,
1126 | .fa-caret-square-o-up:before {
1127 | content: "\f151";
1128 | }
1129 | .fa-toggle-right:before,
1130 | .fa-caret-square-o-right:before {
1131 | content: "\f152";
1132 | }
1133 | .fa-euro:before,
1134 | .fa-eur:before {
1135 | content: "\f153";
1136 | }
1137 | .fa-gbp:before {
1138 | content: "\f154";
1139 | }
1140 | .fa-dollar:before,
1141 | .fa-usd:before {
1142 | content: "\f155";
1143 | }
1144 | .fa-rupee:before,
1145 | .fa-inr:before {
1146 | content: "\f156";
1147 | }
1148 | .fa-cny:before,
1149 | .fa-rmb:before,
1150 | .fa-yen:before,
1151 | .fa-jpy:before {
1152 | content: "\f157";
1153 | }
1154 | .fa-ruble:before,
1155 | .fa-rouble:before,
1156 | .fa-rub:before {
1157 | content: "\f158";
1158 | }
1159 | .fa-won:before,
1160 | .fa-krw:before {
1161 | content: "\f159";
1162 | }
1163 | .fa-bitcoin:before,
1164 | .fa-btc:before {
1165 | content: "\f15a";
1166 | }
1167 | .fa-file:before {
1168 | content: "\f15b";
1169 | }
1170 | .fa-file-text:before {
1171 | content: "\f15c";
1172 | }
1173 | .fa-sort-alpha-asc:before {
1174 | content: "\f15d";
1175 | }
1176 | .fa-sort-alpha-desc:before {
1177 | content: "\f15e";
1178 | }
1179 | .fa-sort-amount-asc:before {
1180 | content: "\f160";
1181 | }
1182 | .fa-sort-amount-desc:before {
1183 | content: "\f161";
1184 | }
1185 | .fa-sort-numeric-asc:before {
1186 | content: "\f162";
1187 | }
1188 | .fa-sort-numeric-desc:before {
1189 | content: "\f163";
1190 | }
1191 | .fa-thumbs-up:before {
1192 | content: "\f164";
1193 | }
1194 | .fa-thumbs-down:before {
1195 | content: "\f165";
1196 | }
1197 | .fa-youtube-square:before {
1198 | content: "\f166";
1199 | }
1200 | .fa-youtube:before {
1201 | content: "\f167";
1202 | }
1203 | .fa-xing:before {
1204 | content: "\f168";
1205 | }
1206 | .fa-xing-square:before {
1207 | content: "\f169";
1208 | }
1209 | .fa-youtube-play:before {
1210 | content: "\f16a";
1211 | }
1212 | .fa-dropbox:before {
1213 | content: "\f16b";
1214 | }
1215 | .fa-stack-overflow:before {
1216 | content: "\f16c";
1217 | }
1218 | .fa-instagram:before {
1219 | content: "\f16d";
1220 | }
1221 | .fa-flickr:before {
1222 | content: "\f16e";
1223 | }
1224 | .fa-adn:before {
1225 | content: "\f170";
1226 | }
1227 | .fa-bitbucket:before {
1228 | content: "\f171";
1229 | }
1230 | .fa-bitbucket-square:before {
1231 | content: "\f172";
1232 | }
1233 | .fa-tumblr:before {
1234 | content: "\f173";
1235 | }
1236 | .fa-tumblr-square:before {
1237 | content: "\f174";
1238 | }
1239 | .fa-long-arrow-down:before {
1240 | content: "\f175";
1241 | }
1242 | .fa-long-arrow-up:before {
1243 | content: "\f176";
1244 | }
1245 | .fa-long-arrow-left:before {
1246 | content: "\f177";
1247 | }
1248 | .fa-long-arrow-right:before {
1249 | content: "\f178";
1250 | }
1251 | .fa-apple:before {
1252 | content: "\f179";
1253 | }
1254 | .fa-windows:before {
1255 | content: "\f17a";
1256 | }
1257 | .fa-android:before {
1258 | content: "\f17b";
1259 | }
1260 | .fa-linux:before {
1261 | content: "\f17c";
1262 | }
1263 | .fa-dribbble:before {
1264 | content: "\f17d";
1265 | }
1266 | .fa-skype:before {
1267 | content: "\f17e";
1268 | }
1269 | .fa-foursquare:before {
1270 | content: "\f180";
1271 | }
1272 | .fa-trello:before {
1273 | content: "\f181";
1274 | }
1275 | .fa-female:before {
1276 | content: "\f182";
1277 | }
1278 | .fa-male:before {
1279 | content: "\f183";
1280 | }
1281 | .fa-gittip:before,
1282 | .fa-gratipay:before {
1283 | content: "\f184";
1284 | }
1285 | .fa-sun-o:before {
1286 | content: "\f185";
1287 | }
1288 | .fa-moon-o:before {
1289 | content: "\f186";
1290 | }
1291 | .fa-archive:before {
1292 | content: "\f187";
1293 | }
1294 | .fa-bug:before {
1295 | content: "\f188";
1296 | }
1297 | .fa-vk:before {
1298 | content: "\f189";
1299 | }
1300 | .fa-weibo:before {
1301 | content: "\f18a";
1302 | }
1303 | .fa-renren:before {
1304 | content: "\f18b";
1305 | }
1306 | .fa-pagelines:before {
1307 | content: "\f18c";
1308 | }
1309 | .fa-stack-exchange:before {
1310 | content: "\f18d";
1311 | }
1312 | .fa-arrow-circle-o-right:before {
1313 | content: "\f18e";
1314 | }
1315 | .fa-arrow-circle-o-left:before {
1316 | content: "\f190";
1317 | }
1318 | .fa-toggle-left:before,
1319 | .fa-caret-square-o-left:before {
1320 | content: "\f191";
1321 | }
1322 | .fa-dot-circle-o:before {
1323 | content: "\f192";
1324 | }
1325 | .fa-wheelchair:before {
1326 | content: "\f193";
1327 | }
1328 | .fa-vimeo-square:before {
1329 | content: "\f194";
1330 | }
1331 | .fa-turkish-lira:before,
1332 | .fa-try:before {
1333 | content: "\f195";
1334 | }
1335 | .fa-plus-square-o:before {
1336 | content: "\f196";
1337 | }
1338 | .fa-space-shuttle:before {
1339 | content: "\f197";
1340 | }
1341 | .fa-slack:before {
1342 | content: "\f198";
1343 | }
1344 | .fa-envelope-square:before {
1345 | content: "\f199";
1346 | }
1347 | .fa-wordpress:before {
1348 | content: "\f19a";
1349 | }
1350 | .fa-openid:before {
1351 | content: "\f19b";
1352 | }
1353 | .fa-institution:before,
1354 | .fa-bank:before,
1355 | .fa-university:before {
1356 | content: "\f19c";
1357 | }
1358 | .fa-mortar-board:before,
1359 | .fa-graduation-cap:before {
1360 | content: "\f19d";
1361 | }
1362 | .fa-yahoo:before {
1363 | content: "\f19e";
1364 | }
1365 | .fa-google:before {
1366 | content: "\f1a0";
1367 | }
1368 | .fa-reddit:before {
1369 | content: "\f1a1";
1370 | }
1371 | .fa-reddit-square:before {
1372 | content: "\f1a2";
1373 | }
1374 | .fa-stumbleupon-circle:before {
1375 | content: "\f1a3";
1376 | }
1377 | .fa-stumbleupon:before {
1378 | content: "\f1a4";
1379 | }
1380 | .fa-delicious:before {
1381 | content: "\f1a5";
1382 | }
1383 | .fa-digg:before {
1384 | content: "\f1a6";
1385 | }
1386 | .fa-pied-piper-pp:before {
1387 | content: "\f1a7";
1388 | }
1389 | .fa-pied-piper-alt:before {
1390 | content: "\f1a8";
1391 | }
1392 | .fa-drupal:before {
1393 | content: "\f1a9";
1394 | }
1395 | .fa-joomla:before {
1396 | content: "\f1aa";
1397 | }
1398 | .fa-language:before {
1399 | content: "\f1ab";
1400 | }
1401 | .fa-fax:before {
1402 | content: "\f1ac";
1403 | }
1404 | .fa-building:before {
1405 | content: "\f1ad";
1406 | }
1407 | .fa-child:before {
1408 | content: "\f1ae";
1409 | }
1410 | .fa-paw:before {
1411 | content: "\f1b0";
1412 | }
1413 | .fa-spoon:before {
1414 | content: "\f1b1";
1415 | }
1416 | .fa-cube:before {
1417 | content: "\f1b2";
1418 | }
1419 | .fa-cubes:before {
1420 | content: "\f1b3";
1421 | }
1422 | .fa-behance:before {
1423 | content: "\f1b4";
1424 | }
1425 | .fa-behance-square:before {
1426 | content: "\f1b5";
1427 | }
1428 | .fa-steam:before {
1429 | content: "\f1b6";
1430 | }
1431 | .fa-steam-square:before {
1432 | content: "\f1b7";
1433 | }
1434 | .fa-recycle:before {
1435 | content: "\f1b8";
1436 | }
1437 | .fa-automobile:before,
1438 | .fa-car:before {
1439 | content: "\f1b9";
1440 | }
1441 | .fa-cab:before,
1442 | .fa-taxi:before {
1443 | content: "\f1ba";
1444 | }
1445 | .fa-tree:before {
1446 | content: "\f1bb";
1447 | }
1448 | .fa-spotify:before {
1449 | content: "\f1bc";
1450 | }
1451 | .fa-deviantart:before {
1452 | content: "\f1bd";
1453 | }
1454 | .fa-soundcloud:before {
1455 | content: "\f1be";
1456 | }
1457 | .fa-database:before {
1458 | content: "\f1c0";
1459 | }
1460 | .fa-file-pdf-o:before {
1461 | content: "\f1c1";
1462 | }
1463 | .fa-file-word-o:before {
1464 | content: "\f1c2";
1465 | }
1466 | .fa-file-excel-o:before {
1467 | content: "\f1c3";
1468 | }
1469 | .fa-file-powerpoint-o:before {
1470 | content: "\f1c4";
1471 | }
1472 | .fa-file-photo-o:before,
1473 | .fa-file-picture-o:before,
1474 | .fa-file-image-o:before {
1475 | content: "\f1c5";
1476 | }
1477 | .fa-file-zip-o:before,
1478 | .fa-file-archive-o:before {
1479 | content: "\f1c6";
1480 | }
1481 | .fa-file-sound-o:before,
1482 | .fa-file-audio-o:before {
1483 | content: "\f1c7";
1484 | }
1485 | .fa-file-movie-o:before,
1486 | .fa-file-video-o:before {
1487 | content: "\f1c8";
1488 | }
1489 | .fa-file-code-o:before {
1490 | content: "\f1c9";
1491 | }
1492 | .fa-vine:before {
1493 | content: "\f1ca";
1494 | }
1495 | .fa-codepen:before {
1496 | content: "\f1cb";
1497 | }
1498 | .fa-jsfiddle:before {
1499 | content: "\f1cc";
1500 | }
1501 | .fa-life-bouy:before,
1502 | .fa-life-buoy:before,
1503 | .fa-life-saver:before,
1504 | .fa-support:before,
1505 | .fa-life-ring:before {
1506 | content: "\f1cd";
1507 | }
1508 | .fa-circle-o-notch:before {
1509 | content: "\f1ce";
1510 | }
1511 | .fa-ra:before,
1512 | .fa-resistance:before,
1513 | .fa-rebel:before {
1514 | content: "\f1d0";
1515 | }
1516 | .fa-ge:before,
1517 | .fa-empire:before {
1518 | content: "\f1d1";
1519 | }
1520 | .fa-git-square:before {
1521 | content: "\f1d2";
1522 | }
1523 | .fa-git:before {
1524 | content: "\f1d3";
1525 | }
1526 | .fa-y-combinator-square:before,
1527 | .fa-yc-square:before,
1528 | .fa-hacker-news:before {
1529 | content: "\f1d4";
1530 | }
1531 | .fa-tencent-weibo:before {
1532 | content: "\f1d5";
1533 | }
1534 | .fa-qq:before {
1535 | content: "\f1d6";
1536 | }
1537 | .fa-wechat:before,
1538 | .fa-weixin:before {
1539 | content: "\f1d7";
1540 | }
1541 | .fa-send:before,
1542 | .fa-paper-plane:before {
1543 | content: "\f1d8";
1544 | }
1545 | .fa-send-o:before,
1546 | .fa-paper-plane-o:before {
1547 | content: "\f1d9";
1548 | }
1549 | .fa-history:before {
1550 | content: "\f1da";
1551 | }
1552 | .fa-circle-thin:before {
1553 | content: "\f1db";
1554 | }
1555 | .fa-header:before {
1556 | content: "\f1dc";
1557 | }
1558 | .fa-paragraph:before {
1559 | content: "\f1dd";
1560 | }
1561 | .fa-sliders:before {
1562 | content: "\f1de";
1563 | }
1564 | .fa-share-alt:before {
1565 | content: "\f1e0";
1566 | }
1567 | .fa-share-alt-square:before {
1568 | content: "\f1e1";
1569 | }
1570 | .fa-bomb:before {
1571 | content: "\f1e2";
1572 | }
1573 | .fa-soccer-ball-o:before,
1574 | .fa-futbol-o:before {
1575 | content: "\f1e3";
1576 | }
1577 | .fa-tty:before {
1578 | content: "\f1e4";
1579 | }
1580 | .fa-binoculars:before {
1581 | content: "\f1e5";
1582 | }
1583 | .fa-plug:before {
1584 | content: "\f1e6";
1585 | }
1586 | .fa-slideshare:before {
1587 | content: "\f1e7";
1588 | }
1589 | .fa-twitch:before {
1590 | content: "\f1e8";
1591 | }
1592 | .fa-yelp:before {
1593 | content: "\f1e9";
1594 | }
1595 | .fa-newspaper-o:before {
1596 | content: "\f1ea";
1597 | }
1598 | .fa-wifi:before {
1599 | content: "\f1eb";
1600 | }
1601 | .fa-calculator:before {
1602 | content: "\f1ec";
1603 | }
1604 | .fa-paypal:before {
1605 | content: "\f1ed";
1606 | }
1607 | .fa-google-wallet:before {
1608 | content: "\f1ee";
1609 | }
1610 | .fa-cc-visa:before {
1611 | content: "\f1f0";
1612 | }
1613 | .fa-cc-mastercard:before {
1614 | content: "\f1f1";
1615 | }
1616 | .fa-cc-discover:before {
1617 | content: "\f1f2";
1618 | }
1619 | .fa-cc-amex:before {
1620 | content: "\f1f3";
1621 | }
1622 | .fa-cc-paypal:before {
1623 | content: "\f1f4";
1624 | }
1625 | .fa-cc-stripe:before {
1626 | content: "\f1f5";
1627 | }
1628 | .fa-bell-slash:before {
1629 | content: "\f1f6";
1630 | }
1631 | .fa-bell-slash-o:before {
1632 | content: "\f1f7";
1633 | }
1634 | .fa-trash:before {
1635 | content: "\f1f8";
1636 | }
1637 | .fa-copyright:before {
1638 | content: "\f1f9";
1639 | }
1640 | .fa-at:before {
1641 | content: "\f1fa";
1642 | }
1643 | .fa-eyedropper:before {
1644 | content: "\f1fb";
1645 | }
1646 | .fa-paint-brush:before {
1647 | content: "\f1fc";
1648 | }
1649 | .fa-birthday-cake:before {
1650 | content: "\f1fd";
1651 | }
1652 | .fa-area-chart:before {
1653 | content: "\f1fe";
1654 | }
1655 | .fa-pie-chart:before {
1656 | content: "\f200";
1657 | }
1658 | .fa-line-chart:before {
1659 | content: "\f201";
1660 | }
1661 | .fa-lastfm:before {
1662 | content: "\f202";
1663 | }
1664 | .fa-lastfm-square:before {
1665 | content: "\f203";
1666 | }
1667 | .fa-toggle-off:before {
1668 | content: "\f204";
1669 | }
1670 | .fa-toggle-on:before {
1671 | content: "\f205";
1672 | }
1673 | .fa-bicycle:before {
1674 | content: "\f206";
1675 | }
1676 | .fa-bus:before {
1677 | content: "\f207";
1678 | }
1679 | .fa-ioxhost:before {
1680 | content: "\f208";
1681 | }
1682 | .fa-angellist:before {
1683 | content: "\f209";
1684 | }
1685 | .fa-cc:before {
1686 | content: "\f20a";
1687 | }
1688 | .fa-shekel:before,
1689 | .fa-sheqel:before,
1690 | .fa-ils:before {
1691 | content: "\f20b";
1692 | }
1693 | .fa-meanpath:before {
1694 | content: "\f20c";
1695 | }
1696 | .fa-buysellads:before {
1697 | content: "\f20d";
1698 | }
1699 | .fa-connectdevelop:before {
1700 | content: "\f20e";
1701 | }
1702 | .fa-dashcube:before {
1703 | content: "\f210";
1704 | }
1705 | .fa-forumbee:before {
1706 | content: "\f211";
1707 | }
1708 | .fa-leanpub:before {
1709 | content: "\f212";
1710 | }
1711 | .fa-sellsy:before {
1712 | content: "\f213";
1713 | }
1714 | .fa-shirtsinbulk:before {
1715 | content: "\f214";
1716 | }
1717 | .fa-simplybuilt:before {
1718 | content: "\f215";
1719 | }
1720 | .fa-skyatlas:before {
1721 | content: "\f216";
1722 | }
1723 | .fa-cart-plus:before {
1724 | content: "\f217";
1725 | }
1726 | .fa-cart-arrow-down:before {
1727 | content: "\f218";
1728 | }
1729 | .fa-diamond:before {
1730 | content: "\f219";
1731 | }
1732 | .fa-ship:before {
1733 | content: "\f21a";
1734 | }
1735 | .fa-user-secret:before {
1736 | content: "\f21b";
1737 | }
1738 | .fa-motorcycle:before {
1739 | content: "\f21c";
1740 | }
1741 | .fa-street-view:before {
1742 | content: "\f21d";
1743 | }
1744 | .fa-heartbeat:before {
1745 | content: "\f21e";
1746 | }
1747 | .fa-venus:before {
1748 | content: "\f221";
1749 | }
1750 | .fa-mars:before {
1751 | content: "\f222";
1752 | }
1753 | .fa-mercury:before {
1754 | content: "\f223";
1755 | }
1756 | .fa-intersex:before,
1757 | .fa-transgender:before {
1758 | content: "\f224";
1759 | }
1760 | .fa-transgender-alt:before {
1761 | content: "\f225";
1762 | }
1763 | .fa-venus-double:before {
1764 | content: "\f226";
1765 | }
1766 | .fa-mars-double:before {
1767 | content: "\f227";
1768 | }
1769 | .fa-venus-mars:before {
1770 | content: "\f228";
1771 | }
1772 | .fa-mars-stroke:before {
1773 | content: "\f229";
1774 | }
1775 | .fa-mars-stroke-v:before {
1776 | content: "\f22a";
1777 | }
1778 | .fa-mars-stroke-h:before {
1779 | content: "\f22b";
1780 | }
1781 | .fa-neuter:before {
1782 | content: "\f22c";
1783 | }
1784 | .fa-genderless:before {
1785 | content: "\f22d";
1786 | }
1787 | .fa-facebook-official:before {
1788 | content: "\f230";
1789 | }
1790 | .fa-pinterest-p:before {
1791 | content: "\f231";
1792 | }
1793 | .fa-whatsapp:before {
1794 | content: "\f232";
1795 | }
1796 | .fa-server:before {
1797 | content: "\f233";
1798 | }
1799 | .fa-user-plus:before {
1800 | content: "\f234";
1801 | }
1802 | .fa-user-times:before {
1803 | content: "\f235";
1804 | }
1805 | .fa-hotel:before,
1806 | .fa-bed:before {
1807 | content: "\f236";
1808 | }
1809 | .fa-viacoin:before {
1810 | content: "\f237";
1811 | }
1812 | .fa-train:before {
1813 | content: "\f238";
1814 | }
1815 | .fa-subway:before {
1816 | content: "\f239";
1817 | }
1818 | .fa-medium:before {
1819 | content: "\f23a";
1820 | }
1821 | .fa-yc:before,
1822 | .fa-y-combinator:before {
1823 | content: "\f23b";
1824 | }
1825 | .fa-optin-monster:before {
1826 | content: "\f23c";
1827 | }
1828 | .fa-opencart:before {
1829 | content: "\f23d";
1830 | }
1831 | .fa-expeditedssl:before {
1832 | content: "\f23e";
1833 | }
1834 | .fa-battery-4:before,
1835 | .fa-battery:before,
1836 | .fa-battery-full:before {
1837 | content: "\f240";
1838 | }
1839 | .fa-battery-3:before,
1840 | .fa-battery-three-quarters:before {
1841 | content: "\f241";
1842 | }
1843 | .fa-battery-2:before,
1844 | .fa-battery-half:before {
1845 | content: "\f242";
1846 | }
1847 | .fa-battery-1:before,
1848 | .fa-battery-quarter:before {
1849 | content: "\f243";
1850 | }
1851 | .fa-battery-0:before,
1852 | .fa-battery-empty:before {
1853 | content: "\f244";
1854 | }
1855 | .fa-mouse-pointer:before {
1856 | content: "\f245";
1857 | }
1858 | .fa-i-cursor:before {
1859 | content: "\f246";
1860 | }
1861 | .fa-object-group:before {
1862 | content: "\f247";
1863 | }
1864 | .fa-object-ungroup:before {
1865 | content: "\f248";
1866 | }
1867 | .fa-sticky-note:before {
1868 | content: "\f249";
1869 | }
1870 | .fa-sticky-note-o:before {
1871 | content: "\f24a";
1872 | }
1873 | .fa-cc-jcb:before {
1874 | content: "\f24b";
1875 | }
1876 | .fa-cc-diners-club:before {
1877 | content: "\f24c";
1878 | }
1879 | .fa-clone:before {
1880 | content: "\f24d";
1881 | }
1882 | .fa-balance-scale:before {
1883 | content: "\f24e";
1884 | }
1885 | .fa-hourglass-o:before {
1886 | content: "\f250";
1887 | }
1888 | .fa-hourglass-1:before,
1889 | .fa-hourglass-start:before {
1890 | content: "\f251";
1891 | }
1892 | .fa-hourglass-2:before,
1893 | .fa-hourglass-half:before {
1894 | content: "\f252";
1895 | }
1896 | .fa-hourglass-3:before,
1897 | .fa-hourglass-end:before {
1898 | content: "\f253";
1899 | }
1900 | .fa-hourglass:before {
1901 | content: "\f254";
1902 | }
1903 | .fa-hand-grab-o:before,
1904 | .fa-hand-rock-o:before {
1905 | content: "\f255";
1906 | }
1907 | .fa-hand-stop-o:before,
1908 | .fa-hand-paper-o:before {
1909 | content: "\f256";
1910 | }
1911 | .fa-hand-scissors-o:before {
1912 | content: "\f257";
1913 | }
1914 | .fa-hand-lizard-o:before {
1915 | content: "\f258";
1916 | }
1917 | .fa-hand-spock-o:before {
1918 | content: "\f259";
1919 | }
1920 | .fa-hand-pointer-o:before {
1921 | content: "\f25a";
1922 | }
1923 | .fa-hand-peace-o:before {
1924 | content: "\f25b";
1925 | }
1926 | .fa-trademark:before {
1927 | content: "\f25c";
1928 | }
1929 | .fa-registered:before {
1930 | content: "\f25d";
1931 | }
1932 | .fa-creative-commons:before {
1933 | content: "\f25e";
1934 | }
1935 | .fa-gg:before {
1936 | content: "\f260";
1937 | }
1938 | .fa-gg-circle:before {
1939 | content: "\f261";
1940 | }
1941 | .fa-tripadvisor:before {
1942 | content: "\f262";
1943 | }
1944 | .fa-odnoklassniki:before {
1945 | content: "\f263";
1946 | }
1947 | .fa-odnoklassniki-square:before {
1948 | content: "\f264";
1949 | }
1950 | .fa-get-pocket:before {
1951 | content: "\f265";
1952 | }
1953 | .fa-wikipedia-w:before {
1954 | content: "\f266";
1955 | }
1956 | .fa-safari:before {
1957 | content: "\f267";
1958 | }
1959 | .fa-chrome:before {
1960 | content: "\f268";
1961 | }
1962 | .fa-firefox:before {
1963 | content: "\f269";
1964 | }
1965 | .fa-opera:before {
1966 | content: "\f26a";
1967 | }
1968 | .fa-internet-explorer:before {
1969 | content: "\f26b";
1970 | }
1971 | .fa-tv:before,
1972 | .fa-television:before {
1973 | content: "\f26c";
1974 | }
1975 | .fa-contao:before {
1976 | content: "\f26d";
1977 | }
1978 | .fa-500px:before {
1979 | content: "\f26e";
1980 | }
1981 | .fa-amazon:before {
1982 | content: "\f270";
1983 | }
1984 | .fa-calendar-plus-o:before {
1985 | content: "\f271";
1986 | }
1987 | .fa-calendar-minus-o:before {
1988 | content: "\f272";
1989 | }
1990 | .fa-calendar-times-o:before {
1991 | content: "\f273";
1992 | }
1993 | .fa-calendar-check-o:before {
1994 | content: "\f274";
1995 | }
1996 | .fa-industry:before {
1997 | content: "\f275";
1998 | }
1999 | .fa-map-pin:before {
2000 | content: "\f276";
2001 | }
2002 | .fa-map-signs:before {
2003 | content: "\f277";
2004 | }
2005 | .fa-map-o:before {
2006 | content: "\f278";
2007 | }
2008 | .fa-map:before {
2009 | content: "\f279";
2010 | }
2011 | .fa-commenting:before {
2012 | content: "\f27a";
2013 | }
2014 | .fa-commenting-o:before {
2015 | content: "\f27b";
2016 | }
2017 | .fa-houzz:before {
2018 | content: "\f27c";
2019 | }
2020 | .fa-vimeo:before {
2021 | content: "\f27d";
2022 | }
2023 | .fa-black-tie:before {
2024 | content: "\f27e";
2025 | }
2026 | .fa-fonticons:before {
2027 | content: "\f280";
2028 | }
2029 | .fa-reddit-alien:before {
2030 | content: "\f281";
2031 | }
2032 | .fa-edge:before {
2033 | content: "\f282";
2034 | }
2035 | .fa-credit-card-alt:before {
2036 | content: "\f283";
2037 | }
2038 | .fa-codiepie:before {
2039 | content: "\f284";
2040 | }
2041 | .fa-modx:before {
2042 | content: "\f285";
2043 | }
2044 | .fa-fort-awesome:before {
2045 | content: "\f286";
2046 | }
2047 | .fa-usb:before {
2048 | content: "\f287";
2049 | }
2050 | .fa-product-hunt:before {
2051 | content: "\f288";
2052 | }
2053 | .fa-mixcloud:before {
2054 | content: "\f289";
2055 | }
2056 | .fa-scribd:before {
2057 | content: "\f28a";
2058 | }
2059 | .fa-pause-circle:before {
2060 | content: "\f28b";
2061 | }
2062 | .fa-pause-circle-o:before {
2063 | content: "\f28c";
2064 | }
2065 | .fa-stop-circle:before {
2066 | content: "\f28d";
2067 | }
2068 | .fa-stop-circle-o:before {
2069 | content: "\f28e";
2070 | }
2071 | .fa-shopping-bag:before {
2072 | content: "\f290";
2073 | }
2074 | .fa-shopping-basket:before {
2075 | content: "\f291";
2076 | }
2077 | .fa-hashtag:before {
2078 | content: "\f292";
2079 | }
2080 | .fa-bluetooth:before {
2081 | content: "\f293";
2082 | }
2083 | .fa-bluetooth-b:before {
2084 | content: "\f294";
2085 | }
2086 | .fa-percent:before {
2087 | content: "\f295";
2088 | }
2089 | .fa-gitlab:before {
2090 | content: "\f296";
2091 | }
2092 | .fa-wpbeginner:before {
2093 | content: "\f297";
2094 | }
2095 | .fa-wpforms:before {
2096 | content: "\f298";
2097 | }
2098 | .fa-envira:before {
2099 | content: "\f299";
2100 | }
2101 | .fa-universal-access:before {
2102 | content: "\f29a";
2103 | }
2104 | .fa-wheelchair-alt:before {
2105 | content: "\f29b";
2106 | }
2107 | .fa-question-circle-o:before {
2108 | content: "\f29c";
2109 | }
2110 | .fa-blind:before {
2111 | content: "\f29d";
2112 | }
2113 | .fa-audio-description:before {
2114 | content: "\f29e";
2115 | }
2116 | .fa-volume-control-phone:before {
2117 | content: "\f2a0";
2118 | }
2119 | .fa-braille:before {
2120 | content: "\f2a1";
2121 | }
2122 | .fa-assistive-listening-systems:before {
2123 | content: "\f2a2";
2124 | }
2125 | .fa-asl-interpreting:before,
2126 | .fa-american-sign-language-interpreting:before {
2127 | content: "\f2a3";
2128 | }
2129 | .fa-deafness:before,
2130 | .fa-hard-of-hearing:before,
2131 | .fa-deaf:before {
2132 | content: "\f2a4";
2133 | }
2134 | .fa-glide:before {
2135 | content: "\f2a5";
2136 | }
2137 | .fa-glide-g:before {
2138 | content: "\f2a6";
2139 | }
2140 | .fa-signing:before,
2141 | .fa-sign-language:before {
2142 | content: "\f2a7";
2143 | }
2144 | .fa-low-vision:before {
2145 | content: "\f2a8";
2146 | }
2147 | .fa-viadeo:before {
2148 | content: "\f2a9";
2149 | }
2150 | .fa-viadeo-square:before {
2151 | content: "\f2aa";
2152 | }
2153 | .fa-snapchat:before {
2154 | content: "\f2ab";
2155 | }
2156 | .fa-snapchat-ghost:before {
2157 | content: "\f2ac";
2158 | }
2159 | .fa-snapchat-square:before {
2160 | content: "\f2ad";
2161 | }
2162 | .fa-pied-piper:before {
2163 | content: "\f2ae";
2164 | }
2165 | .fa-first-order:before {
2166 | content: "\f2b0";
2167 | }
2168 | .fa-yoast:before {
2169 | content: "\f2b1";
2170 | }
2171 | .fa-themeisle:before {
2172 | content: "\f2b2";
2173 | }
2174 | .fa-google-plus-circle:before,
2175 | .fa-google-plus-official:before {
2176 | content: "\f2b3";
2177 | }
2178 | .fa-fa:before,
2179 | .fa-font-awesome:before {
2180 | content: "\f2b4";
2181 | }
2182 | .fa-handshake-o:before {
2183 | content: "\f2b5";
2184 | }
2185 | .fa-envelope-open:before {
2186 | content: "\f2b6";
2187 | }
2188 | .fa-envelope-open-o:before {
2189 | content: "\f2b7";
2190 | }
2191 | .fa-linode:before {
2192 | content: "\f2b8";
2193 | }
2194 | .fa-address-book:before {
2195 | content: "\f2b9";
2196 | }
2197 | .fa-address-book-o:before {
2198 | content: "\f2ba";
2199 | }
2200 | .fa-vcard:before,
2201 | .fa-address-card:before {
2202 | content: "\f2bb";
2203 | }
2204 | .fa-vcard-o:before,
2205 | .fa-address-card-o:before {
2206 | content: "\f2bc";
2207 | }
2208 | .fa-user-circle:before {
2209 | content: "\f2bd";
2210 | }
2211 | .fa-user-circle-o:before {
2212 | content: "\f2be";
2213 | }
2214 | .fa-user-o:before {
2215 | content: "\f2c0";
2216 | }
2217 | .fa-id-badge:before {
2218 | content: "\f2c1";
2219 | }
2220 | .fa-drivers-license:before,
2221 | .fa-id-card:before {
2222 | content: "\f2c2";
2223 | }
2224 | .fa-drivers-license-o:before,
2225 | .fa-id-card-o:before {
2226 | content: "\f2c3";
2227 | }
2228 | .fa-quora:before {
2229 | content: "\f2c4";
2230 | }
2231 | .fa-free-code-camp:before {
2232 | content: "\f2c5";
2233 | }
2234 | .fa-telegram:before {
2235 | content: "\f2c6";
2236 | }
2237 | .fa-thermometer-4:before,
2238 | .fa-thermometer:before,
2239 | .fa-thermometer-full:before {
2240 | content: "\f2c7";
2241 | }
2242 | .fa-thermometer-3:before,
2243 | .fa-thermometer-three-quarters:before {
2244 | content: "\f2c8";
2245 | }
2246 | .fa-thermometer-2:before,
2247 | .fa-thermometer-half:before {
2248 | content: "\f2c9";
2249 | }
2250 | .fa-thermometer-1:before,
2251 | .fa-thermometer-quarter:before {
2252 | content: "\f2ca";
2253 | }
2254 | .fa-thermometer-0:before,
2255 | .fa-thermometer-empty:before {
2256 | content: "\f2cb";
2257 | }
2258 | .fa-shower:before {
2259 | content: "\f2cc";
2260 | }
2261 | .fa-bathtub:before,
2262 | .fa-s15:before,
2263 | .fa-bath:before {
2264 | content: "\f2cd";
2265 | }
2266 | .fa-podcast:before {
2267 | content: "\f2ce";
2268 | }
2269 | .fa-window-maximize:before {
2270 | content: "\f2d0";
2271 | }
2272 | .fa-window-minimize:before {
2273 | content: "\f2d1";
2274 | }
2275 | .fa-window-restore:before {
2276 | content: "\f2d2";
2277 | }
2278 | .fa-times-rectangle:before,
2279 | .fa-window-close:before {
2280 | content: "\f2d3";
2281 | }
2282 | .fa-times-rectangle-o:before,
2283 | .fa-window-close-o:before {
2284 | content: "\f2d4";
2285 | }
2286 | .fa-bandcamp:before {
2287 | content: "\f2d5";
2288 | }
2289 | .fa-grav:before {
2290 | content: "\f2d6";
2291 | }
2292 | .fa-etsy:before {
2293 | content: "\f2d7";
2294 | }
2295 | .fa-imdb:before {
2296 | content: "\f2d8";
2297 | }
2298 | .fa-ravelry:before {
2299 | content: "\f2d9";
2300 | }
2301 | .fa-eercast:before {
2302 | content: "\f2da";
2303 | }
2304 | .fa-microchip:before {
2305 | content: "\f2db";
2306 | }
2307 | .fa-snowflake-o:before {
2308 | content: "\f2dc";
2309 | }
2310 | .fa-superpowers:before {
2311 | content: "\f2dd";
2312 | }
2313 | .fa-wpexplorer:before {
2314 | content: "\f2de";
2315 | }
2316 | .fa-meetup:before {
2317 | content: "\f2e0";
2318 | }
2319 | .sr-only {
2320 | position: absolute;
2321 | width: 1px;
2322 | height: 1px;
2323 | padding: 0;
2324 | margin: -1px;
2325 | overflow: hidden;
2326 | clip: rect(0, 0, 0, 0);
2327 | border: 0;
2328 | }
2329 | .sr-only-focusable:active,
2330 | .sr-only-focusable:focus {
2331 | position: static;
2332 | width: auto;
2333 | height: auto;
2334 | margin: 0;
2335 | overflow: visible;
2336 | clip: auto;
2337 | }
2338 |
--------------------------------------------------------------------------------