├── 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 | ![](./pic/code.jpg) 19 | 20 | 21 | ## 界面预览 22 | 23 | ![](./pic/suosui.png) 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 | [![Build Status](https://travis-ci.org/tencentyun/wafer-client-sdk.svg?branch=master)](https://travis-ci.org/tencentyun/wafer-client-sdk) 4 | [![Coverage Status](https://coveralls.io/repos/github/tencentyun/wafer-client-sdk/badge.svg?branch=master)](https://coveralls.io/github/tencentyun/wafer-client-sdk?branch=master) 5 | [![License](https://img.shields.io/github/license/tencentyun/wafer-client-sdk.svg)](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 | 登录成功的回调。 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 | --------------------------------------------------------------------------------