├── README.md ├── admin ├── static │ └── .gitkeep ├── src │ ├── store │ │ ├── tag │ │ │ ├── getters.js │ │ │ ├── state.js │ │ │ ├── mutation-types.js │ │ │ ├── index.js │ │ │ ├── actions.js │ │ │ └── mutations.js │ │ ├── user │ │ │ ├── getters.js │ │ │ ├── mutation-types.js │ │ │ ├── state.js │ │ │ ├── mutations.js │ │ │ ├── index.js │ │ │ └── actions.js │ │ └── index.js │ ├── pages │ │ ├── wxk │ │ │ └── index.vue │ │ ├── manage │ │ │ ├── index.vue │ │ │ ├── navBar.vue │ │ │ └── tabsView.vue │ │ ├── usermanage │ │ │ ├── userinfo │ │ │ │ ├── index.js │ │ │ │ └── index.vue │ │ │ └── enchashment │ │ │ │ ├── index.js │ │ │ │ └── index.vue │ │ ├── qualitymanage │ │ │ └── review │ │ │ │ ├── index.js │ │ │ │ └── index.vue │ │ └── login │ │ │ └── index.vue │ ├── assets │ │ └── logo.png │ ├── style │ │ ├── mixin.scss │ │ └── common.scss │ ├── App.vue │ ├── utils │ │ ├── user.js │ │ └── utils.js │ ├── router │ │ ├── permission.js │ │ └── index.js │ ├── service │ │ ├── env.js │ │ ├── api.js │ │ └── fetch.js │ ├── main.js │ └── components │ │ └── HelloWorld.vue ├── build │ ├── logo.png │ ├── vue-loader.conf.js │ ├── build.js │ ├── check-versions.js │ ├── webpack.base.conf.js │ ├── utils.js │ ├── webpack.dev.conf.js │ └── webpack.prod.conf.js ├── test │ └── unit │ │ ├── setup.js │ │ ├── .eslintrc │ │ ├── specs │ │ └── HelloWorld.spec.js │ │ └── jest.conf.js ├── config │ ├── prod.env.js │ ├── test.env.js │ ├── dev.env.js │ └── index.js ├── .editorconfig ├── .gitignore ├── .postcssrc.js ├── index.html ├── .babelrc ├── README.md └── package.json ├── client ├── README.md ├── components │ ├── README.md │ ├── lock │ │ ├── index.json │ │ ├── index.wxml │ │ ├── index.wxss │ │ └── index.js │ ├── nav │ │ ├── index.json │ │ ├── index.wxml │ │ ├── index.js │ │ └── index.wxss │ ├── tip │ │ ├── index.json │ │ ├── index.wxml │ │ ├── index.js │ │ └── index.wxss │ ├── booklist │ │ ├── index.json │ │ ├── index.wxml │ │ ├── index.wxss │ │ └── index.js │ ├── contact │ │ ├── index.json │ │ ├── index.wxml │ │ ├── index.js │ │ └── index.wxss │ ├── wallet │ │ ├── index.json │ │ ├── index.wxml │ │ ├── index.js │ │ └── index.wxss │ └── booklistnav │ │ ├── index.json │ │ ├── index.wxml │ │ ├── index.js │ │ └── index.wxss ├── pages │ ├── logs │ │ ├── logs.json │ │ ├── logs.wxss │ │ ├── logs.wxml │ │ └── logs.js │ ├── login │ │ ├── index.json │ │ ├── index.wxml │ │ ├── index.js │ │ └── index.wxss │ ├── editcourse │ │ ├── index.json │ │ ├── index.wxml │ │ ├── index.wxss │ │ └── index.js │ ├── enchashment │ │ ├── index.json │ │ ├── index.wxss │ │ ├── index.wxml │ │ └── index.js │ ├── setting │ │ ├── index.json │ │ ├── index.wxss │ │ ├── index.wxml │ │ └── index.js │ ├── search │ │ ├── index.json │ │ ├── index.wxml │ │ ├── index.wxss │ │ └── index.js │ ├── wallet │ │ ├── index.json │ │ ├── index.wxml │ │ ├── index.wxss │ │ └── index.js │ ├── collection │ │ ├── index.json │ │ ├── index.wxss │ │ ├── index.wxml │ │ └── index.js │ ├── myblist │ │ ├── index.json │ │ ├── index.wxml │ │ ├── index.wxss │ │ └── index.js │ ├── purchase │ │ ├── index.json │ │ ├── index.wxml │ │ ├── index.wxss │ │ └── index.js │ ├── index │ │ ├── index.json │ │ ├── index.wxss │ │ ├── index.wxml │ │ └── index.js │ └── details │ │ ├── index.json │ │ ├── index.wxml │ │ ├── index.wxss │ │ └── index.js ├── images │ ├── 1.png │ ├── 2.png │ ├── 3.png │ ├── 4.png │ ├── 5.png │ ├── Back.png │ ├── head.png │ ├── lock.png │ ├── logo.png │ ├── pen.png │ ├── pkg.png │ ├── Shape.png │ ├── github.png │ ├── weibo.png │ ├── weixin.png │ ├── collect1.png │ ├── collect2.png │ └── blank-page.png ├── service │ ├── api.js │ └── user.js ├── app.wxss ├── app.json ├── project.config.json ├── app.js └── utils │ └── util.js └── server ├── .eslintignore ├── .eslintrc ├── app.js ├── utils ├── util.js └── token.js ├── .gitignore ├── app ├── router.js ├── controller │ ├── home.js │ ├── user.js │ ├── base.js │ ├── admin │ │ ├── base.js │ │ ├── user.js │ │ ├── review.js │ │ ├── admin.js │ │ ├── enchashment.js │ │ └── auth.js │ ├── enchashment.js │ ├── collect.js │ ├── purchase.js │ ├── course.js │ └── auth.js ├── middleware │ ├── interceptor.js │ └── admin_interceptor.js ├── service │ ├── enchashment.js │ ├── admin │ │ ├── admin.js │ │ ├── review.js │ │ └── enchashment.js │ ├── collect.js │ ├── user.js │ ├── purchase.js │ └── course.js ├── router │ ├── admin.js │ └── client.js └── extend │ └── helper.js ├── .travis.yml ├── config ├── plugin.js └── config.default.js ├── appveyor.yml ├── .autod.conf.js ├── test └── app │ └── controller │ └── home.test.js ├── README.md ├── README.zh-CN.md └── package.json /README.md: -------------------------------------------------------------------------------- 1 | # booklist -------------------------------------------------------------------------------- /admin/static/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | # booklist -------------------------------------------------------------------------------- /server/.eslintignore: -------------------------------------------------------------------------------- 1 | coverage 2 | -------------------------------------------------------------------------------- /client/components/README.md: -------------------------------------------------------------------------------- 1 | # booklist -------------------------------------------------------------------------------- /admin/src/store/tag/getters.js: -------------------------------------------------------------------------------- 1 | export default{ 2 | 3 | } -------------------------------------------------------------------------------- /admin/src/store/user/getters.js: -------------------------------------------------------------------------------- 1 | export default{ 2 | 3 | } -------------------------------------------------------------------------------- /server/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-egg" 3 | } 4 | -------------------------------------------------------------------------------- /client/pages/logs/logs.json: -------------------------------------------------------------------------------- 1 | { 2 | "navigationBarTitleText": "查看启动日志" 3 | } -------------------------------------------------------------------------------- /client/pages/login/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "navigationBarTitleText": "用户信息授权" 3 | } -------------------------------------------------------------------------------- /admin/src/pages/wxk/index.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/build/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ftd-2018/booklist/HEAD/admin/build/logo.png -------------------------------------------------------------------------------- /admin/test/unit/setup.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | Vue.config.productionTip = false 4 | -------------------------------------------------------------------------------- /client/components/lock/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "component": true, 3 | "usingComponents": {} 4 | } -------------------------------------------------------------------------------- /client/components/nav/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "component": true, 3 | "usingComponents": {} 4 | } -------------------------------------------------------------------------------- /client/components/tip/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "component": true, 3 | "usingComponents": {} 4 | } -------------------------------------------------------------------------------- /client/images/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ftd-2018/booklist/HEAD/client/images/1.png -------------------------------------------------------------------------------- /client/images/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ftd-2018/booklist/HEAD/client/images/2.png -------------------------------------------------------------------------------- /client/images/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ftd-2018/booklist/HEAD/client/images/3.png -------------------------------------------------------------------------------- /client/images/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ftd-2018/booklist/HEAD/client/images/4.png -------------------------------------------------------------------------------- /client/images/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ftd-2018/booklist/HEAD/client/images/5.png -------------------------------------------------------------------------------- /client/service/api.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | baseURL: "https://www.duolaimier.cn/" 3 | } -------------------------------------------------------------------------------- /admin/src/store/tag/state.js: -------------------------------------------------------------------------------- 1 | export default { 2 | num: 0, 3 | visitedViews: [] 4 | }; -------------------------------------------------------------------------------- /client/components/booklist/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "component": true, 3 | "usingComponents": {} 4 | } -------------------------------------------------------------------------------- /client/components/contact/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "component": true, 3 | "usingComponents": {} 4 | } -------------------------------------------------------------------------------- /client/components/wallet/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "component": true, 3 | "usingComponents": {} 4 | } -------------------------------------------------------------------------------- /client/images/Back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ftd-2018/booklist/HEAD/client/images/Back.png -------------------------------------------------------------------------------- /client/images/head.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ftd-2018/booklist/HEAD/client/images/head.png -------------------------------------------------------------------------------- /client/images/lock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ftd-2018/booklist/HEAD/client/images/lock.png -------------------------------------------------------------------------------- /client/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ftd-2018/booklist/HEAD/client/images/logo.png -------------------------------------------------------------------------------- /client/images/pen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ftd-2018/booklist/HEAD/client/images/pen.png -------------------------------------------------------------------------------- /client/images/pkg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ftd-2018/booklist/HEAD/client/images/pkg.png -------------------------------------------------------------------------------- /admin/config/prod.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = { 3 | NODE_ENV: '"production"' 4 | } 5 | -------------------------------------------------------------------------------- /admin/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ftd-2018/booklist/HEAD/admin/src/assets/logo.png -------------------------------------------------------------------------------- /client/components/booklistnav/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "component": true, 3 | "usingComponents": {} 4 | } -------------------------------------------------------------------------------- /client/images/Shape.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ftd-2018/booklist/HEAD/client/images/Shape.png -------------------------------------------------------------------------------- /client/images/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ftd-2018/booklist/HEAD/client/images/github.png -------------------------------------------------------------------------------- /client/images/weibo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ftd-2018/booklist/HEAD/client/images/weibo.png -------------------------------------------------------------------------------- /client/images/weixin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ftd-2018/booklist/HEAD/client/images/weixin.png -------------------------------------------------------------------------------- /client/images/collect1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ftd-2018/booklist/HEAD/client/images/collect1.png -------------------------------------------------------------------------------- /client/images/collect2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ftd-2018/booklist/HEAD/client/images/collect2.png -------------------------------------------------------------------------------- /server/app.js: -------------------------------------------------------------------------------- 1 | // app.js 2 | module.exports = app => { 3 | // 使用 app 对象‘ 4 | app.token = ""; 5 | }; 6 | -------------------------------------------------------------------------------- /admin/test/unit/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jest": true 4 | }, 5 | "globals": { 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /client/images/blank-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ftd-2018/booklist/HEAD/client/images/blank-page.png -------------------------------------------------------------------------------- /admin/src/store/user/mutation-types.js: -------------------------------------------------------------------------------- 1 | export const LOGOUT = 'LOGOUT'; 2 | export const GET_USERINFO = 'GET_USERINFO'; -------------------------------------------------------------------------------- /admin/src/style/mixin.scss: -------------------------------------------------------------------------------- 1 | @mixin relative { 2 | position: relative; 3 | width: 100%; 4 | height: 100%; 5 | } -------------------------------------------------------------------------------- /server/utils/util.js: -------------------------------------------------------------------------------- 1 | class Util { 2 | isEmpty(obj){ 3 | if(obj) return false; 4 | return true; 5 | } 6 | } 7 | 8 | module.exports = Util; -------------------------------------------------------------------------------- /client/components/contact/index.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 客服微信:fcsboy 4 | 5 | -------------------------------------------------------------------------------- /client/pages/logs/logs.wxss: -------------------------------------------------------------------------------- 1 | .log-list { 2 | display: flex; 3 | flex-direction: column; 4 | padding: 40rpx; 5 | } 6 | .log-item { 7 | margin: 10rpx; 8 | } 9 | -------------------------------------------------------------------------------- /admin/src/store/user/state.js: -------------------------------------------------------------------------------- 1 | export default { 2 | token: localStorage.getItem("token"), 3 | username: localStorage.getItem("userInfo") // 用户名 4 | }; 5 | 6 | -------------------------------------------------------------------------------- /admin/src/store/tag/mutation-types.js: -------------------------------------------------------------------------------- 1 | export const GET_NUMBER = 'GET_NUMBER'; 2 | export const ADD_VISITED_VIEWS = 'ADD_VISITED_VIEWS'; 3 | export const DEL_VISITED_VIEWS = 'DEL_VISITED_VIEWS'; -------------------------------------------------------------------------------- /admin/config/test.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const merge = require('webpack-merge') 3 | const devEnv = require('./dev.env') 4 | 5 | module.exports = merge(devEnv, { 6 | NODE_ENV: '"testing"' 7 | }) 8 | -------------------------------------------------------------------------------- /client/components/tip/index.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 客服微信「 fcsboy 」 4 | 5 | -------------------------------------------------------------------------------- /client/pages/editcourse/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "navigationBarTitleText": "写书单", 3 | "usingComponents": { 4 | "ftdNav": "/components/nav/index", 5 | "ftdTip": "/components/tip/index" 6 | } 7 | } -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | logs/ 2 | npm-debug.log 3 | yarn-error.log 4 | node_modules/ 5 | package-lock.json 6 | yarn.lock 7 | coverage/ 8 | .idea/ 9 | run/ 10 | .DS_Store 11 | *.sw* 12 | *.un~ 13 | -------------------------------------------------------------------------------- /admin/config/dev.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const merge = require('webpack-merge') 3 | const prodEnv = require('./prod.env') 4 | 5 | module.exports = merge(prodEnv, { 6 | NODE_ENV: '"development"' 7 | }) 8 | -------------------------------------------------------------------------------- /admin/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /client/pages/enchashment/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "usingComponents": { 3 | "ftdNav": "/components/nav/index", 4 | "ftdTip": "/components/tip/index" 5 | }, 6 | "navigationBarTitleText": "提现" 7 | } -------------------------------------------------------------------------------- /client/pages/setting/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "navigationBarTitleText": "我的设置", 3 | "usingComponents": { 4 | "ftdNav": "/components/nav/index", 5 | "ftdTip": "/components/tip/index" 6 | } 7 | } -------------------------------------------------------------------------------- /server/app/router.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @param {Egg.Application} app - egg application 5 | */ 6 | module.exports = app => { 7 | require('./router/client')(app); 8 | require('./router/admin')(app); 9 | }; -------------------------------------------------------------------------------- /client/pages/logs/logs.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{index + 1}}. {{log}} 5 | 6 | 7 | -------------------------------------------------------------------------------- /server/.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - '8' 5 | install: 6 | - npm i npminstall && npminstall 7 | script: 8 | - npm run ci 9 | after_script: 10 | - npminstall codecov && codecov 11 | -------------------------------------------------------------------------------- /client/pages/search/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "navigationBarTitleText": "搜索", 3 | "usingComponents": { 4 | "ftdNav": "/components/nav/index", 5 | "ftdBL": "/components/booklist/index", 6 | "ftdTip": "/components/tip/index" 7 | } 8 | } -------------------------------------------------------------------------------- /client/pages/wallet/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "usingComponents": { 3 | "ftdNav": "/components/nav/index", 4 | "ftdWallet": "/components/wallet/index", 5 | "ftdTip": "/components/tip/index" 6 | }, 7 | "navigationBarTitleText": "我的钱包" 8 | } -------------------------------------------------------------------------------- /client/pages/collection/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "navigationBarTitleText": "我的收藏", 3 | "usingComponents": { 4 | "ftdBLN": "/components/booklistnav/index", 5 | "ftdNav": "/components/nav/index", 6 | "ftdTip": "/components/tip/index" 7 | } 8 | } -------------------------------------------------------------------------------- /client/pages/myblist/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "usingComponents": { 3 | "ftdBLN": "/components/booklistnav/index", 4 | "ftdNav": "/components/nav/index", 5 | "ftdTip": "/components/tip/index" 6 | }, 7 | "navigationBarTitleText": "我的书单" 8 | } -------------------------------------------------------------------------------- /client/pages/purchase/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "usingComponents": { 3 | "ftdBLN": "/components/booklistnav/index", 4 | "ftdNav": "/components/nav/index", 5 | "ftdTip": "/components/tip/index" 6 | }, 7 | "navigationBarTitleText": "我的订阅" 8 | } -------------------------------------------------------------------------------- /admin/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | /dist/ 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | /test/unit/coverage/ 8 | 9 | # Editor directories and files 10 | .idea 11 | .vscode 12 | *.suo 13 | *.ntvs* 14 | *.njsproj 15 | *.sln 16 | -------------------------------------------------------------------------------- /server/config/plugin.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // had enabled by egg 4 | // exports.static = true; 5 | exports.mysql = { 6 | enable: true, 7 | package: 'egg-mysql', 8 | }; 9 | 10 | 11 | exports.cors = { 12 | enable: true, 13 | package: 'egg-cors', 14 | }; -------------------------------------------------------------------------------- /client/pages/index/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "navigationBarTitleText": "书单分享", 3 | "usingComponents": { 4 | "ftdNav": "/components/nav/index", 5 | "ftdBL": "/components/booklist/index", 6 | "ftdTip": "/components/tip/index" 7 | }, 8 | "enablePullDownRefresh":true 9 | } -------------------------------------------------------------------------------- /admin/src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 16 | -------------------------------------------------------------------------------- /server/app/controller/home.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Controller = require('egg').Controller; 4 | 5 | class HomeController extends Controller { 6 | async index() { 7 | this.ctx.body = "{message:'请求成功', data:1}"; 8 | } 9 | } 10 | 11 | module.exports = HomeController; 12 | -------------------------------------------------------------------------------- /admin/src/store/user/mutations.js: -------------------------------------------------------------------------------- 1 | import * as types from './mutation-types' 2 | 3 | export default{ 4 | [types.LOGOUT]: (state, token) => { 5 | state.token = token 6 | }, 7 | [types.GET_USERINFO]: (state, getuserinfo) =>{ 8 | state.username = getuserinfo.username; 9 | } 10 | } -------------------------------------------------------------------------------- /client/pages/details/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "usingComponents": { 3 | "ftdBLN": "/components/booklistnav/index", 4 | "ftdNav": "/components/nav/index", 5 | "ftdLock": "/components/lock/index", 6 | "ftdContact": "/components/contact/index", 7 | "ftdTip": "/components/tip/index" 8 | } 9 | } -------------------------------------------------------------------------------- /admin/.postcssrc.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | "plugins": { 5 | "postcss-import": {}, 6 | "postcss-url": {}, 7 | // to edit target browsers: use "browserslist" field in package.json 8 | "autoprefixer": {} 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /admin/src/store/tag/index.js: -------------------------------------------------------------------------------- 1 | import state from './state'; 2 | import mutations from './mutations'; 3 | import getters from './getters'; 4 | import actions from './actions'; 5 | 6 | export default { 7 | namespaced: true, 8 | state, 9 | mutations, 10 | getters, 11 | actions 12 | }; -------------------------------------------------------------------------------- /admin/src/store/user/index.js: -------------------------------------------------------------------------------- 1 | import state from './state'; 2 | import mutations from './mutations'; 3 | import getters from './getters'; 4 | import actions from './actions'; 5 | 6 | export default { 7 | namespaced: true, 8 | state, 9 | mutations, 10 | getters, 11 | actions 12 | }; -------------------------------------------------------------------------------- /client/components/contact/index.js: -------------------------------------------------------------------------------- 1 | // components/contact/index.js 2 | Component({ 3 | /** 4 | * 组件的属性列表 5 | */ 6 | properties: { 7 | 8 | }, 9 | 10 | /** 11 | * 组件的初始数据 12 | */ 13 | data: { 14 | 15 | }, 16 | 17 | /** 18 | * 组件的方法列表 19 | */ 20 | methods: { 21 | 22 | } 23 | }) 24 | -------------------------------------------------------------------------------- /server/appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | matrix: 3 | - nodejs_version: '8' 4 | 5 | install: 6 | - ps: Install-Product node $env:nodejs_version 7 | - npm i npminstall && node_modules\.bin\npminstall 8 | 9 | test_script: 10 | - node --version 11 | - npm --version 12 | - npm run test 13 | 14 | build: off 15 | -------------------------------------------------------------------------------- /admin/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | admin 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /client/components/contact/index.wxss: -------------------------------------------------------------------------------- 1 | /* components/contact/index.wxss */ 2 | .ftd-contact{ 3 | position: fixed; 4 | font-size: 12px; 5 | color: #fff; 6 | width: 750rpx; 7 | bottom: 0; 8 | height: 80rpx; 9 | line-height: 80rpx; 10 | left: 0; 11 | text-align: center; 12 | background: rgba(0, 0, 0, .3); 13 | } -------------------------------------------------------------------------------- /client/pages/logs/logs.js: -------------------------------------------------------------------------------- 1 | //logs.js 2 | const util = require('../../utils/util.js') 3 | 4 | Page({ 5 | data: { 6 | logs: [] 7 | }, 8 | onLoad: function () { 9 | this.setData({ 10 | logs: (wx.getStorageSync('logs') || []).map(log => { 11 | return util.formatTime(new Date(log)) 12 | }) 13 | }) 14 | } 15 | }) 16 | -------------------------------------------------------------------------------- /client/app.wxss: -------------------------------------------------------------------------------- 1 | /**app.wxss**/ 2 | view,image,text,navigator{ 3 | list-style: none; 4 | padding:0; 5 | margin:0; 6 | 7 | } 8 | .container { 9 | font-family: PingFang-SC-Heavy; 10 | height: 100%; 11 | display: flex; 12 | flex-direction: column; 13 | align-items: center; 14 | justify-content: space-between; 15 | padding: 118rpx 0 0 0; 16 | box-sizing: border-box; 17 | } 18 | -------------------------------------------------------------------------------- /admin/src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Vuex from 'vuex'; 3 | import createPersistedState from "vuex-persistedstate"; 4 | Vue.use(Vuex); 5 | 6 | import user from './user/'; 7 | import tag from './tag/'; 8 | 9 | export default new Vuex.Store({ 10 | modules: { 11 | user, 12 | tag 13 | }, 14 | plugins: [createPersistedState({ storage: window.sessionStorage })] 15 | }); 16 | -------------------------------------------------------------------------------- /admin/src/utils/user.js: -------------------------------------------------------------------------------- 1 | const userInfo = 'userInfo' 2 | 3 | function getUserInfo(){ 4 | // return localStorage.getItem(JSON.parse(userInfo)); 5 | } 6 | 7 | function setUserInfo(token){ 8 | return localStorage.setItem(userInfo, JSON.stringify(token)); 9 | } 10 | 11 | function removeUserInfo(){ 12 | return localStorage.removeItem(userInfo); 13 | } 14 | 15 | export { 16 | getUserInfo, 17 | setUserInfo, 18 | removeUserInfo 19 | } -------------------------------------------------------------------------------- /admin/test/unit/specs/HelloWorld.spec.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import HelloWorld from '@/components/HelloWorld' 3 | 4 | describe('HelloWorld.vue', () => { 5 | it('should render correct contents', () => { 6 | const Constructor = Vue.extend(HelloWorld) 7 | const vm = new Constructor().$mount() 8 | expect(vm.$el.querySelector('.hello h1').textContent) 9 | .toEqual('Welcome to Your Vue.js App') 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /client/pages/index/index.wxss: -------------------------------------------------------------------------------- 1 | /**index.wxss**/ 2 | .serach{ 3 | width: 640rpx; 4 | height: 92rpx; 5 | margin: 0 auto; 6 | background: #F5F5F5; 7 | border-radius: 16rpx; 8 | font-size: 30rpx; 9 | color: rgba(36, 37, 61, .2); 10 | line-height: 92rpx; 11 | padding-left: 30rpx; 12 | margin-bottom: 36rpx; 13 | } 14 | .bottomTips{ 15 | font-size: 28rpx; 16 | text-align: center; 17 | padding: 50rpx; 18 | color: #ccc; 19 | } -------------------------------------------------------------------------------- /admin/src/store/tag/actions.js: -------------------------------------------------------------------------------- 1 | import { 2 | ADD_VISITED_VIEWS, 3 | DEL_VISITED_VIEWS 4 | } from './mutation-types'; 5 | 6 | export default { 7 | addVisitedViews({ commit }, view){ 8 | commit(ADD_VISITED_VIEWS, view) 9 | }, 10 | delVisitedViews({ commit, state }, view){ 11 | return new Promise((resolve) => { 12 | commit(DEL_VISITED_VIEWS, view); 13 | resolve([...state.visitedViews]) 14 | }); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /client/components/lock/index.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 剩余50%的内容订阅后可查看 9 | 直接购买阅读全文 10 | {{price}}积分 11 | -------------------------------------------------------------------------------- /server/app/controller/user.js: -------------------------------------------------------------------------------- 1 | const Base = require('./base'); 2 | 3 | class UserController extends Base { 4 | async setInfo(){ 5 | const {ctx, app} = this; 6 | const param = ctx.request.body; 7 | param.id = app.userId; 8 | let result = await ctx.service.user.update(param); 9 | if(result.affectedRows === 1){ 10 | return this.success("更新成功"); 11 | }else{ 12 | return this.fail("更新失败"); 13 | } 14 | } 15 | } 16 | 17 | module.exports = UserController; -------------------------------------------------------------------------------- /server/app/middleware/interceptor.js: -------------------------------------------------------------------------------- 1 | module.exports = options => { 2 | return async function interceptor(ctx, next) { 3 | if(ctx.path == '/' || ctx.path == '/auth/loginByWeixinAction'){ 4 | 5 | await next(); 6 | return; 7 | } 8 | // 根据token值获取用户id 9 | ctx.app.token = ctx.header['x-booklist-token'] || ''; 10 | let token =new ctx.helper.Token(ctx); 11 | ctx.app.userId = await token.getUserId(); 12 | await next(); 13 | }; 14 | }; 15 | -------------------------------------------------------------------------------- /admin/src/store/user/actions.js: -------------------------------------------------------------------------------- 1 | import * as types from './mutation-types'; 2 | 3 | export default { 4 | logOut({ commit }) { 5 | return new Promise(async (resolve, reject) => { 6 | // let result = await logout(); 7 | commit('LOGOUT', '') 8 | localStorage.removeItem("userInfo") 9 | resolve() 10 | }).catch(error => { 11 | reject(error) 12 | }) 13 | }, 14 | getUserInfo({commit}, data) { 15 | commit('GET_USERINFO', data); 16 | } 17 | }; -------------------------------------------------------------------------------- /client/components/wallet/index.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ type == 1? '被订阅' : '订阅' }} 5 | 已支付 6 | 7 | 8 | {{type == 1? '+' : '-'}}{{price}} 9 | {{time}} 10 | 11 | 12 | -------------------------------------------------------------------------------- /admin/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "modules": false, 5 | "targets": { 6 | "browsers": ["> 1%", "last 2 versions", "not ie <= 8"] 7 | } 8 | }], 9 | "stage-2" 10 | ], 11 | "plugins": ["transform-vue-jsx", "transform-runtime"], 12 | "env": { 13 | "test": { 14 | "presets": ["env", "stage-2"], 15 | "plugins": ["transform-vue-jsx", "transform-es2015-modules-commonjs", "dynamic-import-node"] 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /server/app/middleware/admin_interceptor.js: -------------------------------------------------------------------------------- 1 | module.exports = options => { 2 | return async function adminInterceptor(ctx, next) { 3 | // if(ctx.path == '/' || ctx.path == '/auth/loginByWeixinAction'){ 4 | // await next(); 5 | // return; 6 | // } 7 | // // 根据token值获取用户id 8 | // ctx.app.token = ctx.header['x-bladmin-token'] || ''; 9 | // let token =new ctx.helper.Token(ctx); 10 | // ctx.app.userId = await token.getUserId(); 11 | await next(); 12 | }; 13 | }; -------------------------------------------------------------------------------- /server/app/service/enchashment.js: -------------------------------------------------------------------------------- 1 | const Service = require('egg').Service; 2 | 3 | class EnchashmentService extends Service { 4 | async newAndSave(credit, wechat){ 5 | const {app, ctx} = this; 6 | const result = await app.mysql.insert("enchashment", { 7 | user_id: app.userId, 8 | enchashment_credit: credit, 9 | verify: 0, 10 | wechat: wechat, 11 | verify_time: ctx.helper.timest() 12 | }); 13 | return result; 14 | } 15 | } 16 | 17 | module.exports = EnchashmentService; -------------------------------------------------------------------------------- /client/pages/login/index.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 书单分享 7 | 大学专业课书单是一个专门提供各个大学学生的专业课书单的社区,在这里,您可以看到各个学校的各个专业的最全面的专业课书单。 8 | 9 | 10 | 请升级微信版本 11 | 12 | 13 | -------------------------------------------------------------------------------- /server/.autod.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | write: true, 5 | prefix: '^', 6 | plugin: 'autod-egg', 7 | test: [ 8 | 'test', 9 | 'benchmark', 10 | ], 11 | dep: [ 12 | 'egg', 13 | 'egg-scripts', 14 | ], 15 | devdep: [ 16 | 'egg-ci', 17 | 'egg-bin', 18 | 'egg-mock', 19 | 'autod', 20 | 'autod-egg', 21 | 'eslint', 22 | 'eslint-config-egg', 23 | 'webstorm-disable-index', 24 | ], 25 | exclude: [ 26 | './test/fixtures', 27 | './dist', 28 | ], 29 | }; 30 | 31 | -------------------------------------------------------------------------------- /server/app/service/admin/admin.js: -------------------------------------------------------------------------------- 1 | const Service = require('egg').Service; 2 | 3 | class AdminService extends Service{ 4 | async insert(data){ 5 | const result = await this.app.mysql.insert("admin", data); 6 | return result; 7 | } 8 | 9 | async getByUName(username){ 10 | const result = await this.app.mysql.get("admin", { 11 | username: username 12 | }); 13 | 14 | return result; 15 | } 16 | 17 | async update(data){ 18 | await this.app.mysql.update("admin", data); 19 | } 20 | } 21 | 22 | module.exports = AdminService; 23 | 24 | -------------------------------------------------------------------------------- /admin/src/router/permission.js: -------------------------------------------------------------------------------- 1 | import router from './index.js' 2 | import store from '../store' 3 | 4 | const whiteList = ['/login']// 不重定向白名单 5 | 6 | router.beforeEach((to, from, next) => { 7 | if(localStorage.getItem('token')){ // 判断是否有token 8 | if(to.path === '/login'){ 9 | next({ 10 | path: '/' 11 | }) 12 | }else{ 13 | next(); 14 | } 15 | }else{ 16 | if (whiteList.indexOf(to.path) !== -1) { // 在免登录白名单,直接进入 17 | next(); 18 | }else{ 19 | next({ 20 | path: '/login' 21 | }) 22 | } 23 | 24 | } 25 | }); 26 | 27 | -------------------------------------------------------------------------------- /client/components/booklist/index.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{title}} 5 | 6 | 7 | {{master}} 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /client/components/booklistnav/index.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{item.title}} 5 | 审核中 6 | 7 | 8 | 9 | 加载中... 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /admin/src/service/env.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 配置编译环境和线上环境之间的切换 3 | * 4 | * baseUrl: 域名地址 5 | * routerMode: 路由模式 6 | * imgBaseUrl: 图片所在域名地址 7 | * 8 | */ 9 | 10 | let baseUrl = ''; 11 | let routerMode = 'history'; 12 | let imgBaseUrl = ''; 13 | 14 | 15 | baseUrl = 'https://www.duolaimier.cn/'; 16 | 17 | // baseUrl = 'http://192.168.5.105:8088/manage'; 18 | // if (process.env.NODE_ENV == 'development') { 19 | 20 | // }else if(process.env.NODE_ENV == 'production'){ 21 | // // baseUrl = 'http://cangdu.org:8001'; 22 | // } 23 | 24 | export { 25 | baseUrl, 26 | routerMode, 27 | imgBaseUrl, 28 | } -------------------------------------------------------------------------------- /server/test/app/controller/home.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { app, assert } = require('egg-mock/bootstrap'); 4 | 5 | describe('test/app/controller/home.test.js', () => { 6 | 7 | it('should assert', function* () { 8 | const pkg = require('../../../package.json'); 9 | assert(app.config.keys.startsWith(pkg.name)); 10 | 11 | // const ctx = app.mockContext({}); 12 | // yield ctx.service.xx(); 13 | }); 14 | 15 | it('should GET /', () => { 16 | return app.httpRequest() 17 | .get('/') 18 | .expect('hi, egg') 19 | .expect(200); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /client/pages/collection/index.wxss: -------------------------------------------------------------------------------- 1 | /**index.wxss**/ 2 | .course-blank,.course-data { 3 | display: flex; 4 | flex-direction: column; 5 | align-items: center; 6 | } 7 | 8 | .course-blank .blank-img{ 9 | width:290rpx; 10 | height:240rpx; 11 | margin:240rpx auto 0; 12 | } 13 | 14 | .course__title{ 15 | font-size: 36rpx; 16 | color: #24253D; 17 | margin-bottom: 30rpx; 18 | width: 670rpx; 19 | } 20 | 21 | .course-blank .blank-text{ 22 | font-size:28rpx; 23 | line-height: 40rpx; 24 | margin-top:20rpx; 25 | } 26 | 27 | .blank-text .write-course{ 28 | color:orange; 29 | } 30 | 31 | -------------------------------------------------------------------------------- /server/app/controller/base.js: -------------------------------------------------------------------------------- 1 | const { Controller } = require('egg'); 2 | 3 | class BaseController extends Controller{ 4 | constructor(props) { 5 | super(props); 6 | } 7 | success(data, status) { 8 | let result = { 9 | error: "操作成功", 10 | msg: "操作成功", 11 | status: status || 0, 12 | result: data 13 | }; 14 | this.ctx.body = result; 15 | } 16 | fail(data, status) { 17 | let result = { 18 | error: "操作失败", 19 | msg: "操作失败", 20 | status: status || -1, 21 | result: data 22 | }; 23 | this.ctx.body = result; 24 | } 25 | } 26 | 27 | 28 | module.exports = BaseController; -------------------------------------------------------------------------------- /server/app/controller/admin/base.js: -------------------------------------------------------------------------------- 1 | const { Controller } = require('egg'); 2 | 3 | class BaseController extends Controller{ 4 | constructor(props) { 5 | super(props); 6 | } 7 | success(data, status) { 8 | let result = { 9 | error: "操作成功", 10 | msg: "操作成功", 11 | status: status || 0, 12 | result: data 13 | }; 14 | this.ctx.body = result; 15 | } 16 | fail(data, status) { 17 | let result = { 18 | error: "操作失败", 19 | msg: "操作失败", 20 | status: status || -1, 21 | result: data 22 | }; 23 | this.ctx.body = result; 24 | } 25 | } 26 | 27 | 28 | module.exports = BaseController; -------------------------------------------------------------------------------- /client/components/booklistnav/index.js: -------------------------------------------------------------------------------- 1 | // components/booklistnav/index.js 2 | Component({ 3 | /** 4 | * 组件的属性列表 5 | */ 6 | properties: { 7 | collections:{ 8 | type:Array, 9 | value:[] 10 | }, 11 | hasMore:{ 12 | type:Boolean, 13 | value: false 14 | }, 15 | loading:{ 16 | type: Boolean, 17 | value: false 18 | } 19 | }, 20 | ready(){ 21 | console.log(this.properties.collections); 22 | }, 23 | /** 24 | * 组件的初始数据 25 | */ 26 | data: { 27 | 28 | }, 29 | 30 | /** 31 | * 组件的方法列表 32 | */ 33 | methods: { 34 | 35 | } 36 | }) 37 | -------------------------------------------------------------------------------- /admin/src/main.js: -------------------------------------------------------------------------------- 1 | // The Vue build version to load with the `import` command 2 | // (runtime-only or standalone) has been set in webpack.base.conf with an alias. 3 | import Vue from 'vue' 4 | import App from './App' 5 | import router from './router' 6 | import './router/permission.js' 7 | import ElementUI from 'element-ui'; 8 | import 'element-ui/lib/theme-chalk/index.css'; 9 | import store from './store/' 10 | 11 | Vue.use(ElementUI); 12 | Vue.config.productionTip = false 13 | 14 | /* eslint-disable no-new */ 15 | new Vue({ 16 | el: '#app', 17 | router, 18 | store, 19 | components: { App }, 20 | template: '' 21 | }) 22 | -------------------------------------------------------------------------------- /server/utils/token.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken'); 2 | const secret = 'FTD#@HBJRR@@gf'; 3 | class Token { 4 | async create(userInfo){ 5 | const token = jwt.sign(userInfo, secret); 6 | return token; 7 | } 8 | 9 | async parse() { 10 | if (app.token) { 11 | try { 12 | return jwt.verify(app.token, secret); 13 | } catch (err) { 14 | return null; 15 | } 16 | } 17 | return null; 18 | } 19 | 20 | async verify() { 21 | const result = await this.parse(); 22 | if (think.isEmpty(result)) { 23 | return false; 24 | } 25 | 26 | return true; 27 | } 28 | } 29 | module.export = Token; -------------------------------------------------------------------------------- /server/README.md: -------------------------------------------------------------------------------- 1 | # server 2 | 3 | booklist server 4 | 5 | ## QuickStart 6 | 7 | 8 | 9 | see [egg docs][egg] for more detail. 10 | 11 | ### Development 12 | 13 | ```bash 14 | $ npm i 15 | $ npm run dev 16 | $ open http://localhost:7001/ 17 | ``` 18 | 19 | ### Deploy 20 | 21 | ```bash 22 | $ npm start 23 | $ npm stop 24 | ``` 25 | 26 | ### npm scripts 27 | 28 | - Use `npm run lint` to check code style. 29 | - Use `npm test` to run unit test. 30 | - Use `npm run autod` to auto detect dependencies upgrade, see [autod](https://www.npmjs.com/package/autod) for more detail. 31 | 32 | 33 | [egg]: https://eggjs.org -------------------------------------------------------------------------------- /admin/README.md: -------------------------------------------------------------------------------- 1 | # admin 2 | 3 | > A Vue.js project 4 | 5 | ## Build Setup 6 | 7 | ``` bash 8 | # install dependencies 9 | npm install 10 | 11 | # serve with hot reload at localhost:8080 12 | npm run dev 13 | 14 | # build for production with minification 15 | npm run build 16 | 17 | # build for production and view the bundle analyzer report 18 | npm run build --report 19 | 20 | # run unit tests 21 | npm run unit 22 | 23 | # run all tests 24 | npm test 25 | ``` 26 | 27 | For a detailed explanation on how things work, check out the [guide](http://vuejs-templates.github.io/webpack/) and [docs for vue-loader](http://vuejs.github.io/vue-loader). 28 | -------------------------------------------------------------------------------- /client/pages/login/index.js: -------------------------------------------------------------------------------- 1 | const user = require('../../service/user.js'); 2 | const app = getApp(); 3 | // pages/login/index.js 4 | Page({ 5 | 6 | /** 7 | * 页面的初始数据 8 | */ 9 | data: { 10 | canIUse: wx.canIUse('button.open-type.getUserInfo') 11 | }, 12 | bindGetUserInfo: function (e) { 13 | user.loginByWeixin().then(res => { 14 | app.globalData.userInfo = res.result.userInfo; 15 | app.globalData.token = res.result.token; 16 | wx.redirectTo({ 17 | url: '/pages/index/index' 18 | }) 19 | }).catch((err) => { 20 | console.log(err) 21 | }); 22 | }, 23 | 24 | }) -------------------------------------------------------------------------------- /admin/build/vue-loader.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const utils = require('./utils') 3 | const config = require('../config') 4 | const isProduction = process.env.NODE_ENV === 'production' 5 | const sourceMapEnabled = isProduction 6 | ? config.build.productionSourceMap 7 | : config.dev.cssSourceMap 8 | 9 | module.exports = { 10 | loaders: utils.cssLoaders({ 11 | sourceMap: sourceMapEnabled, 12 | extract: isProduction 13 | }), 14 | cssSourceMap: sourceMapEnabled, 15 | cacheBusting: config.dev.cacheBusting, 16 | transformToRequire: { 17 | video: ['src', 'poster'], 18 | source: 'src', 19 | img: 'src', 20 | image: 'xlink:href' 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /client/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "pages": [ 3 | "pages/index/index", 4 | "pages/details/index", 5 | "pages/myblist/index", 6 | "pages/collection/index", 7 | "pages/setting/index", 8 | "pages/editcourse/index", 9 | "pages/logs/logs", 10 | "pages/search/index", 11 | "pages/login/index", 12 | "pages/purchase/index", 13 | "pages/enchashment/index", 14 | "pages/wallet/index" 15 | ], 16 | "window": { 17 | "backgroundTextStyle": "light", 18 | "navigationBarBackgroundColor": "#fff", 19 | "navigationBarTitleText": "书单分享", 20 | "navigationBarTextStyle": "black" 21 | } 22 | } -------------------------------------------------------------------------------- /client/pages/collection/index.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 我的收藏 6 | 7 | 8 | 9 | 还没有收藏课程哦~ 10 | 11 | 12 | 13 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /admin/src/store/tag/mutations.js: -------------------------------------------------------------------------------- 1 | import { 2 | GET_NUMBER, 3 | ADD_VISITED_VIEWS, 4 | DEL_VISITED_VIEWS 5 | } from './mutation-types.js' 6 | 7 | export default{ 8 | [GET_NUMBER](state, num){ 9 | state.num = num; 10 | }, 11 | [ADD_VISITED_VIEWS](state, view){ 12 | if (state.visitedViews.some(v => v.path === view.path)) return; 13 | state.visitedViews.push({ name: view.name, path: view.path }); 14 | }, 15 | [DEL_VISITED_VIEWS](state, view){ 16 | let index; 17 | for (const [i, v] of state.visitedViews.entries()) { 18 | if (v.path === view.path) { 19 | index = i 20 | break; 21 | } 22 | } 23 | state.visitedViews.splice(index, 1); 24 | } 25 | } -------------------------------------------------------------------------------- /server/app/controller/admin/user.js: -------------------------------------------------------------------------------- 1 | const Base = require('./base'); 2 | 3 | class UserController extends Base { 4 | async getUserList(){ 5 | const {ctx} = this; 6 | const param = ctx.request.body; 7 | const result = await ctx.service.user.searchByTelWX(param); 8 | return this.success(result); 9 | } 10 | 11 | async updateCredit(){ 12 | const {ctx} = this; 13 | const credit = ctx.request.body.credit; 14 | const id = ctx.request.body.id; 15 | const result = await ctx.service.user.updateCredit(id, credit); 16 | if(result.affectedRows === 1){ 17 | return this.success("更新成功"); 18 | }else{ 19 | return this.fail("更新失败"); 20 | } 21 | } 22 | } 23 | 24 | module.exports = UserController; -------------------------------------------------------------------------------- /client/pages/login/index.wxss: -------------------------------------------------------------------------------- 1 | /* pages/login/index.wxss */ 2 | .logo { 3 | padding-top: 25rpx; 4 | padding-bottom: 16rpx; 5 | } 6 | .logo image{ 7 | width: 102rpx; 8 | height: 153rpx; 9 | } 10 | .title{ 11 | color: #333; 12 | font-size: 36rpx; 13 | font-weight: bold; 14 | margin-bottom: 32rpx; 15 | } 16 | .intr{ 17 | width: 670rpx; 18 | color: #333; 19 | font-size: 24rpx; 20 | letter-spacing: 1.61px; 21 | margin-bottom: 100rpx; 22 | } 23 | .btn{ 24 | width: 686rpx; 25 | height: 92rpx; 26 | } 27 | .btn button{ 28 | background-color: #00B92E; 29 | color: #FFF; 30 | } 31 | .btn .update{ 32 | text-align: center; 33 | font-size: 30rpx; 34 | } -------------------------------------------------------------------------------- /client/pages/myblist/index.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 我的书单 6 | 7 | 8 | 还没有添加书单哦,现在去写书单 9 | 10 | 11 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /client/project.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "项目配置文件。", 3 | "packOptions": { 4 | "ignore": [] 5 | }, 6 | "setting": { 7 | "urlCheck": false, 8 | "es6": true, 9 | "postcss": true, 10 | "minified": true, 11 | "newFeature": true 12 | }, 13 | "compileType": "miniprogram", 14 | "libVersion": "2.0.9", 15 | "appid": "", 16 | "projectname": "booklist", 17 | "isGameTourist": false, 18 | "condition": { 19 | "search": { 20 | "current": -1, 21 | "list": [] 22 | }, 23 | "conversation": { 24 | "current": -1, 25 | "list": [] 26 | }, 27 | "game": { 28 | "currentL": -1, 29 | "list": [] 30 | }, 31 | "miniprogram": { 32 | "current": -1, 33 | "list": [] 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /client/pages/purchase/index.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 我的订阅 6 | 7 | 8 | 还没有订阅专业书单哦,现在去订阅 9 | 10 | 11 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /client/pages/index/index.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 请输入专业名称 6 | 7 | 8 | 17 | 18 | 19 | 20 | 加载中... 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /server/README.zh-CN.md: -------------------------------------------------------------------------------- 1 | # server 2 | 3 | booklist server 4 | 5 | ## 快速入门 6 | 7 | 8 | 9 | 如需进一步了解,参见 [egg 文档][egg]。 10 | 11 | ### 本地开发 12 | 13 | ```bash 14 | $ npm i 15 | $ npm run dev 16 | $ open http://localhost:7001/ 17 | ``` 18 | 19 | ### 部署 20 | 21 | ```bash 22 | $ npm start 23 | $ npm stop 24 | ``` 25 | 26 | ### 单元测试 27 | 28 | - [egg-bin] 内置了 [mocha], [thunk-mocha], [power-assert], [istanbul] 等框架,让你可以专注于写单元测试,无需理会配套工具。 29 | - 断言库非常推荐使用 [power-assert]。 30 | - 具体参见 [egg 文档 - 单元测试](https://eggjs.org/zh-cn/core/unittest)。 31 | 32 | ### 内置指令 33 | 34 | - 使用 `npm run lint` 来做代码风格检查。 35 | - 使用 `npm test` 来执行单元测试。 36 | - 使用 `npm run autod` 来自动检测依赖更新,详细参见 [autod](https://www.npmjs.com/package/autod) 。 37 | 38 | 39 | [egg]: https://eggjs.org 40 | -------------------------------------------------------------------------------- /client/components/wallet/index.js: -------------------------------------------------------------------------------- 1 | const util = require('../../utils/util.js'); 2 | // components/purchase/index.js 3 | Component({ 4 | /** 5 | * 组件的属性列表 6 | */ 7 | properties: { 8 | type: { 9 | type: Number, 10 | value: 1 11 | }, 12 | price:{ 13 | type: Number, 14 | value: 0 15 | }, 16 | addTime:{ 17 | type:Number, 18 | value: 0 19 | } 20 | }, 21 | 22 | /** 23 | * 组件的初始数据 24 | */ 25 | data: { 26 | time: '' 27 | }, 28 | ready:function(){ 29 | console.log(123,this.properties.addTime); 30 | this.setData({ 31 | time: util.formatTime(new Date(this.properties.addTime*1000)) 32 | }); 33 | }, 34 | /** 35 | * 组件的方法列表 36 | */ 37 | methods: { 38 | 39 | } 40 | }) 41 | -------------------------------------------------------------------------------- /admin/test/unit/jest.conf.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | rootDir: path.resolve(__dirname, '../../'), 5 | moduleFileExtensions: [ 6 | 'js', 7 | 'json', 8 | 'vue' 9 | ], 10 | moduleNameMapper: { 11 | '^@/(.*)$': '/src/$1' 12 | }, 13 | transform: { 14 | '^.+\\.js$': '/node_modules/babel-jest', 15 | '.*\\.(vue)$': '/node_modules/vue-jest' 16 | }, 17 | snapshotSerializers: ['/node_modules/jest-serializer-vue'], 18 | setupFiles: ['/test/unit/setup'], 19 | mapCoverage: true, 20 | coverageDirectory: '/test/unit/coverage', 21 | collectCoverageFrom: [ 22 | 'src/**/*.{js,vue}', 23 | '!src/main.js', 24 | '!src/router/index.js', 25 | '!**/node_modules/**' 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /client/components/nav/index.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 写书单 6 | 7 | 8 | 9 | 10 | 13 | 14 | {{item.text}} 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /client/components/booklistnav/index.wxss: -------------------------------------------------------------------------------- 1 | /* components/booklistnav/index.wxss */ 2 | .ftd-booklistnav_all{ 3 | position: relative; 4 | width: 630rpx; 5 | padding: 17rpx 22rpx 17rpx 18rpx; 6 | font-size: 34rpx; 7 | color: #24253D; 8 | background: #F5F5F5; 9 | border-radius: 16rpx; 10 | margin-bottom: 20rpx; 11 | } 12 | .ftd-booklistnav_all navigator{ 13 | width: 500rpx; 14 | overflow: hidden; 15 | text-overflow: ellipsis; 16 | white-space: nowrap; 17 | } 18 | .ftd-booklistnav_verify{ 19 | position: absolute; 20 | right: 25rpx; 21 | top: 20rpx; 22 | font-size: 20rpx; 23 | border-radius: 2px; 24 | border: 1px solid #D73316; 25 | padding: 5rpx 10rpx; 26 | color: #D73316; 27 | } 28 | 29 | .bottomTips{ 30 | font-size: 28rpx; 31 | text-align: center; 32 | padding: 50rpx; 33 | color: #ccc; 34 | } -------------------------------------------------------------------------------- /client/pages/myblist/index.wxss: -------------------------------------------------------------------------------- 1 | /**index.wxss**/ 2 | .course-blank,.course-data { 3 | display: flex; 4 | flex-direction: column; 5 | align-items: center; 6 | } 7 | .course__title{ 8 | font-size: 36rpx; 9 | color: #24253D; 10 | margin-bottom: 30rpx; 11 | width: 670rpx; 12 | } 13 | .course-blank .blank-img{ 14 | width:290rpx; 15 | height:240rpx; 16 | margin:240rpx auto 0; 17 | } 18 | 19 | .course-blank .blank-text{ 20 | font-size:28rpx; 21 | line-height: 40rpx; 22 | margin-top:20rpx; 23 | } 24 | 25 | .blank-text .write-course{ 26 | color:orange; 27 | } 28 | 29 | .course-data .course-list{ 30 | width:652rpx; 31 | height: 82rpx; 32 | line-height: 82rpx; 33 | font-size: 34rpx; 34 | background-color:#F5F5F5; 35 | margin:30rpx auto 0; 36 | padding-left: 18rpx; 37 | border-radius: 16rpx; 38 | color: #24253D; 39 | } -------------------------------------------------------------------------------- /client/pages/purchase/index.wxss: -------------------------------------------------------------------------------- 1 | /* pages/purchased/index.wxss */ 2 | .course-blank,.course-data { 3 | display: flex; 4 | flex-direction: column; 5 | align-items: center; 6 | } 7 | .course__title{ 8 | font-size: 36rpx; 9 | color: #24253D; 10 | margin-bottom: 30rpx; 11 | width: 670rpx; 12 | } 13 | .course-blank .blank-img{ 14 | width:290rpx; 15 | height:240rpx; 16 | margin:240rpx auto 0; 17 | } 18 | 19 | .course-blank .blank-text{ 20 | font-size:28rpx; 21 | line-height: 40rpx; 22 | margin-top:20rpx; 23 | } 24 | 25 | .blank-text .write-course{ 26 | color:orange; 27 | } 28 | 29 | .course-data .course-list{ 30 | width:652rpx; 31 | height: 82rpx; 32 | line-height: 82rpx; 33 | font-size: 34rpx; 34 | background-color:#F5F5F5; 35 | margin:30rpx auto 0; 36 | padding-left: 18rpx; 37 | border-radius: 16rpx; 38 | color: #24253D; 39 | } -------------------------------------------------------------------------------- /client/pages/details/index.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{userInfo.username}} 8 | {{userInfo.undergraduate}} 9 | 10 | 11 | 编辑专业 12 | 13 | 14 | 15 | {{item.text}} 16 | 17 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /client/components/tip/index.js: -------------------------------------------------------------------------------- 1 | // components/tip/index.js 2 | Component({ 3 | /** 4 | * 组件的属性列表 5 | */ 6 | properties: { 7 | 8 | }, 9 | 10 | /** 11 | * 组件的初始数据 12 | */ 13 | data: { 14 | isShow: '', 15 | isGo: false, 16 | y:300+"px" 17 | }, 18 | 19 | /** 20 | * 组件的方法列表 21 | */ 22 | methods: { 23 | showTip(){ 24 | const that = this; 25 | if(this.data.isGo == false){ 26 | this.setData({ 27 | isGo: true, 28 | isShow: 'animated bounceInRight' 29 | }); 30 | 31 | setTimeout(function(){ 32 | that.setData({ 33 | isGo: false, 34 | isShow: 'animated fadeInLeft' 35 | }); 36 | },4000); 37 | }else{ 38 | 39 | } 40 | }, 41 | dragTip(e){ 42 | this.setData({ 43 | y: e.touches[0].clientY+"px" 44 | }); 45 | } 46 | } 47 | }) 48 | -------------------------------------------------------------------------------- /client/pages/setting/index.wxss: -------------------------------------------------------------------------------- 1 | .my-setting{ 2 | display: flex; 3 | flex-direction: column; 4 | padding: 118rpx 0 0 0; 5 | } 6 | .setting-inp{ 7 | width:684rpx; 8 | height: 100rpx; 9 | padding:0 32rpx; 10 | font-size:28rpx; 11 | line-height: 100rpx; 12 | color:#333333; 13 | background-color:#FFFFFF; 14 | } 15 | .setting-inp .label{ 16 | float:left; 17 | /* width:490rpx; */ 18 | } 19 | .setting-inp .inp-text{ 20 | float:right; 21 | /* width:190rpx; */ 22 | height: 100rpx; 23 | text-align: right; 24 | } 25 | .inp-text input{ 26 | height: 100rpx; 27 | } 28 | 29 | .setting-login .login-btn{ 30 | width:686rpx; 31 | height:93rpx; 32 | margin:80rpx auto 0; 33 | font-size:28rpx; 34 | line-height:93rpx; 35 | color:#FFFFFF; 36 | background-color:#D73316; 37 | border-radius: 8rpx; 38 | } 39 | 40 | .search-placeholder{ 41 | font-size: 30rpx; 42 | color: rgba(36, 37, 61, .3); 43 | } -------------------------------------------------------------------------------- /server/app/controller/enchashment.js: -------------------------------------------------------------------------------- 1 | const Base = require('./base'); 2 | 3 | class EnchashmentController extends Base { 4 | async add(){ 5 | const {ctx, app} = this; 6 | const credit = ctx.request.body.credit; 7 | const wechat = ctx.request.body.wechat; 8 | 9 | // 保证 credit 为数字 10 | if(!/^[0-9]*$/.test(credit)) 11 | return this.fail("credit类型错误"); 12 | 13 | const uInfo = await ctx.service.user.find({ 14 | id: app.userId 15 | }); 16 | const result = await ctx.service.user.update({ 17 | id: app.userId, 18 | credit: uInfo.credit - credit 19 | }); 20 | 21 | if(result.affectedRows === 1){ 22 | const resultSave = await ctx.service.enchashment.newAndSave(credit, wechat); 23 | if(resultSave.affectedRows === 1) 24 | return this.success("提审成功"); 25 | else 26 | return this.fail("已扣除积分,但提审失败"); 27 | }else{ 28 | return this.fail("提审失败") 29 | } 30 | } 31 | } 32 | 33 | module.exports = EnchashmentController; -------------------------------------------------------------------------------- /server/app/controller/admin/review.js: -------------------------------------------------------------------------------- 1 | const Base = require('./base'); 2 | 3 | class ReviewController extends Base { 4 | async getList(){ 5 | const {ctx} = this; 6 | const result = await ctx.service.admin.review.search(ctx.request.body); 7 | return this.success(result); 8 | } 9 | 10 | async publish(){ 11 | const {ctx} = this; 12 | const courseID = ctx.request.body.courseID; 13 | const result = await ctx.service.admin.review.isPublish(courseID, 1); 14 | if(result.affectedRows === 1){ 15 | return this.success("发布成功"); 16 | }else{ 17 | return this.fail("发布失败"); 18 | } 19 | } 20 | 21 | async revoke(){ 22 | const {ctx} = this; 23 | const courseID = ctx.request.body.courseID; 24 | const result = await ctx.service.admin.review.isPublish(courseID, 0); 25 | if(result.affectedRows === 1){ 26 | return this.success("撤销成功"); 27 | }else{ 28 | return this.fail("撤销失败"); 29 | } 30 | } 31 | } 32 | 33 | module.exports = ReviewController; -------------------------------------------------------------------------------- /server/app/controller/admin/admin.js: -------------------------------------------------------------------------------- 1 | const Base = require('./base'); 2 | const bcrypt = require('bcrypt'); 3 | 4 | class AdminController extends Base { 5 | async add(){ 6 | const {ctx} = this; 7 | const username = "admin"; 8 | const password = "888888"; 9 | const addTime = ctx.helper.timest(); 10 | 11 | // 密码加盐操作 12 | const saltRounds = 10; 13 | const salt = bcrypt.genSaltSync(saltRounds); 14 | const hash = bcrypt.hashSync(password, salt); 15 | 16 | if(!username || !password || !salt || !hash){ 17 | return this.fail("参数无效"); 18 | } 19 | 20 | // 把 salt 和 hash 存入数据库 21 | const result = await ctx.service.admin.admin.insert({ 22 | username: username, 23 | password_hash: hash, 24 | password_salt: salt, 25 | add_time: addTime 26 | }); 27 | 28 | if(result.affectedRows === 1){ 29 | return this.success("插入成功"); 30 | }else{ 31 | return this.fail("插入失败"); 32 | } 33 | } 34 | } 35 | 36 | module.exports = AdminController; -------------------------------------------------------------------------------- /client/app.js: -------------------------------------------------------------------------------- 1 | var user = require('./service/user.js'); 2 | //app.js 3 | App({ 4 | onLaunch: function () { 5 | const that = this; 6 | // 查看是否授权 7 | user.checkLogin().then(res => { 8 | that.globalData.userInfo = wx.getStorageSync('userInfo'); 9 | that.globalData.token = wx.getStorageSync('token'); 10 | // if (that.employIdCallback) { 11 | // that.employIdCallback(wx.getStorageSync('userInfo')); 12 | // } 13 | }).catch(() => { 14 | user.loginByWeixin().then(res => { 15 | that.globalData.userInfo = res.result.userInfo; 16 | that.globalData.token = res.result.token; 17 | }).catch((err) => { 18 | console.log(err); 19 | }); 20 | }); 21 | }, 22 | globalData: { 23 | userInfo: { 24 | username: '', 25 | avatar: '', 26 | undergraduate: '', 27 | master_school: '', 28 | wechat_id: '', 29 | tel_id:'' 30 | }, 31 | token: '', 32 | } 33 | }) -------------------------------------------------------------------------------- /client/pages/search/index.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 搜索 7 | 8 | 9 | 18 | 19 | 哎呀,结果溜走了,换个词试试 20 | 21 | 22 | 加载中... 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /client/pages/search/index.wxss: -------------------------------------------------------------------------------- 1 | /* pages/search/index.wxss */ 2 | .search{ 3 | display: flex; 4 | align-items: center; 5 | margin-bottom: 36rpx; 6 | } 7 | .search__input{ 8 | height: 92rpx; 9 | width: 500rpx; 10 | background: #F5F5F5; 11 | border-radius: 16rpx; 12 | padding:0 30rpx; 13 | margin-right: 30rpx; 14 | } 15 | .search__btn{ 16 | width: 100rpx; 17 | height: 72rpx; 18 | background: #D73316; 19 | color: #fff; 20 | font-size: 12px; 21 | line-height: 72rpx; 22 | text-align: center; 23 | border-radius: 16rpx; 24 | } 25 | .booklist{ 26 | /* display: flex; 27 | justify-content: space-between; 28 | width: 670rpx; 29 | flex-wrap: wrap; */ 30 | } 31 | .search__placeholder{ 32 | font-size: 30rpx; 33 | color: rgba(36, 37, 61, .2); 34 | } 35 | .toptip{ 36 | font-size: 28rpx; 37 | color: #ccc; 38 | padding-top: 100rpx; 39 | } 40 | 41 | .bottomTips{ 42 | font-size: 28rpx; 43 | text-align: center; 44 | padding: 50rpx; 45 | color: #ccc; 46 | } -------------------------------------------------------------------------------- /server/app/service/admin/review.js: -------------------------------------------------------------------------------- 1 | const Service = require('egg').Service; 2 | 3 | class ReviewService extends Service{ 4 | async search(param){ 5 | const {app} = this; 6 | const wechat_id = param.wechat_id || ''; 7 | const tel_id = param.tel_id || ''; 8 | const title = param.title || ''; 9 | const publish = param.publish || 0; 10 | let str = `select u.username,u.wechat_id,u.tel_id,u.credit,c.title,c.my_course,c.publish,c.id from user as u left join course as c on u.id=c.user_id where u.wechat_id LIKE '%${wechat_id}%' AND u.tel_id LIKE '%${tel_id}%' AND c.title LIKE '%${title}%' AND c.publish LIKE '%${publish}%'`; 11 | const result = await app.mysql.query(str); 12 | return result; 13 | } 14 | 15 | async isPublish(courseID, type){ 16 | const {app} = this; 17 | console.log(courseID, type); 18 | const result = await app.mysql.update('course', { 19 | id: courseID, 20 | publish: type 21 | }); 22 | return result; 23 | } 24 | } 25 | 26 | module.exports = ReviewService; -------------------------------------------------------------------------------- /server/app/service/admin/enchashment.js: -------------------------------------------------------------------------------- 1 | const Service = require('egg').Service; 2 | 3 | class EnchashmentService extends Service{ 4 | async search(param){ 5 | const {app} = this; 6 | const wechat_id = param.wechat_id || ''; 7 | const tel_id = param.tel_id || ''; 8 | const username = param.username || ''; 9 | const verify = param.verify || 0; 10 | let str = `select u.username,u.wechat_id,u.tel_id,u.credit,e.enchashment_credit,e.verify,e.id from user as u left join enchashment as e on u.id=e.user_id where u.wechat_id LIKE '%${wechat_id}%' AND u.tel_id LIKE '%${tel_id}%' AND u.username LIKE '%${username}%' AND e.verify LIKE '%${verify}%'`; 11 | const result = await app.mysql.query(str); 12 | return result; 13 | } 14 | 15 | async isEnchashment(enchasmentID, type){ 16 | const {app} = this; 17 | const result = await app.mysql.update('enchashment', { 18 | id: enchasmentID, 19 | verify: type 20 | }); 21 | return result; 22 | } 23 | } 24 | 25 | module.exports = EnchashmentService; -------------------------------------------------------------------------------- /client/pages/enchashment/index.wxss: -------------------------------------------------------------------------------- 1 | /* pages/enchashment/index.wxss */ 2 | .form-box{ 3 | width:100%; 4 | background-color: #fff; 5 | margin-top: 20rpx; 6 | } 7 | .form_row{ 8 | width: 720rpx; 9 | height: 88rpx; 10 | line-height: 88rpx; 11 | margin-left: 30rpx; 12 | border-bottom: 1rpx solid #eee; 13 | display: flex; 14 | font-size: 28rpx; 15 | /*justify-content: space-between;*/ 16 | } 17 | .form_label{ 18 | width: 160rpx; 19 | color: #000 20 | } 21 | .form_right{ 22 | flex: 1; 23 | height: 88rpx; 24 | line-height: 88rpx; 25 | } 26 | .form_input{ 27 | height: 100%; 28 | font-size: 28rpx; 29 | padding-right: 30rpx; 30 | } 31 | 32 | .form_money{ 33 | width: 720rpx; 34 | font-size: 14px; 35 | color: #b2b2b2; 36 | margin-top: 30rpx; 37 | margin-left: 30rpx; 38 | } 39 | 40 | .form-btn{ 41 | width: 690rpx; 42 | height: 80rpx; 43 | margin-top:30rpx; 44 | font-size: 30rpx; 45 | border-radius: 100rpx; 46 | } -------------------------------------------------------------------------------- /client/components/wallet/index.wxss: -------------------------------------------------------------------------------- 1 | /* components/purchase/index.wxss */ 2 | .ftd-wallet{ 3 | display: flex; 4 | height: 132rpx; 5 | margin-left: 30rpx; 6 | margin-right: 30rpx; 7 | -webkit-box-align: center; 8 | -moz-box-align: center; 9 | -ms-flex-align: center; 10 | align-items: center; 11 | border-bottom: 1px solid #e5e5e5; 12 | } 13 | 14 | .ftd-walletall_left{ 15 | display: flex; 16 | flex-direction: column; 17 | } 18 | 19 | .ftd-walletall_title{ 20 | font-size: 32rpx; 21 | color: #333333; 22 | } 23 | 24 | .ftd-wallet_status{ 25 | font-size: 24rpx; 26 | color: #999999; 27 | } 28 | 29 | .ftd-wallet_right{ 30 | margin-left: auto; 31 | display: flex; 32 | flex-direction: column; 33 | } 34 | 35 | .ftd-wallet_amount{ 36 | color: #D73316; 37 | font-size: 32rpx; 38 | text-align: right; 39 | } 40 | 41 | .ftd-wallet_time{ 42 | font-size: 24rpx; 43 | color: #999999; 44 | } 45 | 46 | .bottomTips{ 47 | font-size: 28rpx; 48 | text-align: center; 49 | padding: 50rpx; 50 | color: #ccc; 51 | } -------------------------------------------------------------------------------- /server/app/controller/admin/enchashment.js: -------------------------------------------------------------------------------- 1 | const Base = require('./base'); 2 | 3 | class EnchashmentController extends Base { 4 | async getList(){ 5 | const {ctx} = this; 6 | const result = await ctx.service.admin.enchashment.search(ctx.request.body); 7 | return this.success(result); 8 | } 9 | 10 | async enchashment(){ 11 | const {ctx} = this; 12 | const enchashmentID = ctx.request.body.enchashmentID; 13 | const result = await ctx.service.admin.enchashment.isEnchashment(enchashmentID, 1); 14 | if(result.affectedRows === 1){ 15 | return this.success("提现成功"); 16 | }else{ 17 | return this.fail("提现失败"); 18 | } 19 | } 20 | 21 | async revoke(){ 22 | const {ctx} = this; 23 | const enchashmentID = ctx.request.body.enchashmentID; 24 | const result = await ctx.service.admin.enchashment.isEnchashment(enchashmentID, 0); 25 | if(result.affectedRows === 1){ 26 | return this.success("撤销成功"); 27 | }else{ 28 | return this.fail("撤销失败"); 29 | } 30 | } 31 | } 32 | 33 | module.exports = EnchashmentController; -------------------------------------------------------------------------------- /client/pages/details/index.wxss: -------------------------------------------------------------------------------- 1 | /**index.wxss**/ 2 | .userinfo { 3 | display: flex; 4 | width: 670rpx; 5 | margin-bottom: 30rpx; 6 | } 7 | 8 | .userinfo__avatar { 9 | width: 100rpx; 10 | height: 100rpx; 11 | border-radius: 50%; 12 | margin-right: 26rpx; 13 | } 14 | 15 | .userinfo__nickname { 16 | color: #24253D ; 17 | font-size: 34rpx; 18 | margin-bottom: 10rpx; 19 | padding-top: 10rpx; 20 | } 21 | 22 | .userinfo__des{ 23 | font-size: 24rpx; 24 | color: rgba(36,37,61,0.50); 25 | } 26 | 27 | .userinfo__edit{ 28 | flex: 1; 29 | } 30 | 31 | .userinfo__btn{ 32 | float: right; 33 | padding: 8rpx 15rpx; 34 | border: 1px solid #C7C7C7; 35 | border-radius: 50rpx; 36 | font-size: 26rpx; 37 | color: #C7C7C7; 38 | margin-top: 15rpx; 39 | } 40 | 41 | .usermotto { 42 | margin-top: 200px; 43 | } 44 | 45 | .booklist{ 46 | width: 630rpx; 47 | padding: 17rpx 22rpx 17rpx 18rpx; 48 | font-size: 34rpx; 49 | color: #24253D; 50 | background: #F5F5F5; 51 | border-radius: 16rpx; 52 | margin-bottom: 20rpx; 53 | } -------------------------------------------------------------------------------- /admin/src/service/api.js: -------------------------------------------------------------------------------- 1 | import fetch from './fetch' 2 | 3 | /** 4 | * 登录 5 | */ 6 | export const login = data => fetch('auth/login', data); 7 | 8 | /** 9 | * 获取文章审查机制列表 10 | */ 11 | export const getReviewList = data => fetch('admin/review/getList', data); 12 | 13 | /** 14 | * 发布 15 | */ 16 | export const publish = data => fetch('admin/review/publish', data); 17 | 18 | /** 19 | * 撤销 20 | */ 21 | export const revoke = data => fetch('admin/review/revoke', data); 22 | 23 | /** 24 | * 获取用户列表 25 | */ 26 | export const getUserList = data => fetch('admin/user/getUserList', data); 27 | 28 | /** 29 | * 修改用户积分 30 | */ 31 | export const updateCredit = data => fetch('admin/user/updateCredit', data); 32 | 33 | /** 34 | * 获取提现列表 35 | */ 36 | export const getEnchaList = data => fetch('admin/enchashment/getList', data); 37 | 38 | /** 39 | * 提现 40 | */ 41 | export const enchashment = data => fetch('admin/enchashment/enchashment', data); 42 | 43 | /** 44 | * 提现撤销 45 | */ 46 | export const enchaRevoke = data => fetch('admin/enchashment/revoke', data); 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /server/app/router/admin.js: -------------------------------------------------------------------------------- 1 | module.exports = app => { 2 | const { router, controller } = app; 3 | const adminInterceptor = app.middleware.adminInterceptor(); 4 | router.post('/admin/addAdmin', adminInterceptor, controller.admin.admin.add); 5 | router.post('/auth/login', adminInterceptor, controller.admin.auth.login); 6 | router.post('/admin/review/getList', adminInterceptor, controller.admin.review.getList); 7 | router.post('/admin/review/publish', adminInterceptor, controller.admin.review.publish); 8 | router.post('/admin/review/revoke', adminInterceptor, controller.admin.review.revoke); 9 | router.post('/admin/user/getUserList', adminInterceptor, controller.admin.user.getUserList); 10 | router.post('/admin/user/updateCredit', adminInterceptor, controller.admin.user.updateCredit); 11 | router.post('/admin/enchashment/getList', adminInterceptor, controller.admin.enchashment.getList); 12 | router.post('/admin/enchashment/enchashment', adminInterceptor, controller.admin.enchashment.enchashment); 13 | router.post('/admin/enchashment/revoke', adminInterceptor, controller.admin.enchashment.revoke); 14 | } -------------------------------------------------------------------------------- /server/config/config.default.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = appInfo => { 4 | const config = exports = {}; 5 | 6 | // use for cookie sign key, should change to your own and keep security 7 | config.keys = appInfo.name + '_1528019285960_9766'; 8 | 9 | // add your config here 10 | config.middleware = []; 11 | 12 | // 小程序只能存storage,关闭csrf 13 | config.security = { 14 | csrf: { 15 | enable: false 16 | }, 17 | }; 18 | 19 | config.wx = { 20 | secret: '', 21 | appid: '' 22 | } 23 | 24 | config.mysql = { 25 | client: { 26 | host: 'cd-cdb-3c9c8yyp.sql.tencentcdb.com', 27 | // 端口号 28 | port: '63885', 29 | // 用户名 30 | user: 'root', 31 | // 密码 32 | password: '', 33 | // 数据库名 34 | database: '', 35 | }, 36 | // 所有数据库配置的默认值 37 | default:{}, 38 | 39 | // 是否加载到 app 上,默认开启 40 | app: true, 41 | // 是否加载到 agent 上,默认关闭 42 | agent: false 43 | } 44 | 45 | config.cors = { 46 | origin: '*', 47 | allowMethods: 'GET,HEAD,PUT,POST,DELETE,PATCH' 48 | }; 49 | 50 | return config; 51 | }; 52 | -------------------------------------------------------------------------------- /client/pages/wallet/index.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 账号剩余(积分) 9 | {{credit}} 10 | 11 | 充值 12 | 提现到微信 13 | 14 | 15 | 16 | 交易记录 17 | 18 | 24 | 25 | 26 | 27 | 加载中... 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /client/pages/collection/index.js: -------------------------------------------------------------------------------- 1 | const util = require('../../utils/util.js'); 2 | const app=getApp() 3 | Page({ 4 | data: { 5 | myCollection: [], 6 | page: 1, 7 | size: 14, 8 | hasMore: false, 9 | loading: false 10 | }, 11 | loadMore:function(){ 12 | const that = this; 13 | if (!this.data.hasMore) return; 14 | util.request('collect/listMyCollectRange', { page: this.data.page++, size: this.data.size}).then(res => { 15 | if (res.status === 0) { 16 | if (res.result.length) { 17 | that.setData({ 18 | myCollection: that.data.myCollection.concat(res.result), 19 | loading: false 20 | }); 21 | }else{ 22 | that.setData({ hasMore: false }) 23 | } 24 | } 25 | }); 26 | }, 27 | onLoad: function(){ 28 | this.setData({ hasMore: true, loading: true }) 29 | this.loadMore(); 30 | }, 31 | onReachBottom: function () { 32 | this.setData({ 33 | loading: true 34 | }) 35 | this.loadMore(); 36 | }, 37 | goToWrite: function () { 38 | wx.navigateTo({ 39 | url: '../editcourse/index' 40 | }); 41 | } 42 | }) -------------------------------------------------------------------------------- /client/components/lock/index.wxss: -------------------------------------------------------------------------------- 1 | /* components/lock/index.wxss */ 2 | .ftd-lock{ 3 | width: 750rpx; 4 | display: flex; 5 | flex-direction: column; 6 | align-items: center; 7 | box-shadow: 0 -70rpx 20px #fff 8 | } 9 | .content{ 10 | width: 100%; 11 | height: 30rpx; 12 | } 13 | .ftd-lock .lock{ 14 | position: relative; 15 | width: 670rpx; 16 | box-sizing: border-box; 17 | } 18 | .lock .lock_img{ 19 | width: 48rpx; 20 | height: 48rpx; 21 | padding: 10rpx; 22 | background: #fff; 23 | position: absolute; 24 | left: 50%; 25 | top: -32rpx; 26 | margin-left: -32rpx; 27 | } 28 | .lock .lock_line{ 29 | border-bottom: 1px solid #EDEDED; 30 | } 31 | 32 | .ftd-lock .intr{ 33 | margin-top: 30rpx; 34 | font-size: 14px; 35 | color: #D73316; 36 | } 37 | 38 | .ftd-lock .tip{ 39 | margin-top: 30rpx; 40 | font-size: 14px; 41 | color: #818181; 42 | } 43 | 44 | .ftd-lock .payBtn{ 45 | margin-top: 25rpx; 46 | padding: 10rpx 48rpx; 47 | background: #FFFAFA; 48 | border: 1px solid #FF7055; 49 | border-radius: 100rpx; 50 | color: #D73316; 51 | font-size: 14px; 52 | } -------------------------------------------------------------------------------- /client/components/booklist/index.wxss: -------------------------------------------------------------------------------- 1 | /* components/booklist/index.wxss */ 2 | .ftd-booklist{ 3 | position: relative; 4 | background: #F5F5F5; 5 | border-radius: 16rpx; 6 | width: 690rpx; 7 | height: 144rpx; 8 | font-size: 28rpx; 9 | color: #24253D; 10 | margin:0 auto; 11 | margin-bottom: 20rpx; 12 | } 13 | .ftd-booklist navigator{ 14 | overflow: hidden; 15 | text-overflow: ellipsis; 16 | white-space: nowrap; 17 | width: 614rpx; 18 | height: 120rpx; 19 | padding-left: 30rpx; 20 | padding-top: 20rpx; 21 | font-size: 28rpx; 22 | color: #24253D; 23 | } 24 | .ftd-booklist__collectBackImage{ 25 | position: absolute; 26 | width: 37rpx; 27 | height: 34rpx; 28 | top: 18rpx; 29 | right: 30rpx; 30 | } 31 | .ftd-booklist .ftd-booklist__head{ 32 | position: absolute; 33 | width: 50rpx; 34 | height: 50rpx; 35 | border-radius: 25rpx; 36 | top: 74rpx; 37 | left: 30rpx; 38 | } 39 | .ftd-booklist__master{ 40 | position: absolute; 41 | left: 98rpx; 42 | top: 83rpx; 43 | font-size: 24rpx; 44 | color: rgba(36,37,61,0.50); 45 | } 46 | .payCount{ 47 | position: absolute; 48 | color: #949494; 49 | font-size: 22rpx; 50 | right: 30rpx; 51 | bottom: 28rpx; 52 | } -------------------------------------------------------------------------------- /client/pages/purchase/index.js: -------------------------------------------------------------------------------- 1 | const app = getApp() 2 | const util = require('../../utils/util.js'); 3 | 4 | Page({ 5 | data: { 6 | myPurchase: [], 7 | page: 1, 8 | size: 30, 9 | hasMore: false, 10 | loading: false 11 | }, 12 | loadMore:function(){ 13 | const that = this; 14 | if (!this.data.hasMore) return; 15 | util.request('purchase/listMyPurchase', { page: this.data.page++, size: this.data.size }).then(res => { 16 | if (res.status === 0) { 17 | let result = res.result; 18 | if (result.length){ 19 | result = util.changeArr(result, 'id', 'course_id'); 20 | that.setData({ 21 | myPurchase: that.data.myPurchase.concat(result), 22 | loading: false 23 | }); 24 | }else{ 25 | that.setData({ hasMore: false }) 26 | } 27 | } 28 | }); 29 | }, 30 | onLoad: function () { 31 | this.setData({ hasMore: true, loading: true }) 32 | this.loadMore(); 33 | }, 34 | onReachBottom: function () { 35 | this.setData({ 36 | loading: true 37 | }) 38 | this.loadMore(); 39 | }, 40 | goToWrite: function () { 41 | wx.reLaunch({ 42 | url: '../index/index' 43 | }); 44 | } 45 | }) -------------------------------------------------------------------------------- /client/components/lock/index.js: -------------------------------------------------------------------------------- 1 | const util = require('../../utils/util.js'); 2 | // components/lock/index.js 3 | Component({ 4 | /** 5 | * 组件的属性列表 6 | */ 7 | properties: { 8 | price: { 9 | type: String, 10 | value: "" 11 | }, 12 | courseID:{ 13 | type: String, 14 | value: "" 15 | } 16 | }, 17 | 18 | /** 19 | * 组件的初始数据 20 | */ 21 | data: { 22 | 23 | }, 24 | 25 | /** 26 | * 组件的方法列表 27 | */ 28 | methods: { 29 | pay:function(){ 30 | const that = this; 31 | util.request('purchase/addPurchase', { courseID: this.properties.courseID }).then(res => { 32 | if(res.status == 0){ 33 | wx.showToast({ 34 | title: res.result, 35 | icon: 'success', 36 | duration: 2000, 37 | complete: function () { 38 | that.triggerEvent("evenPay"); 39 | } 40 | }) 41 | }else{ 42 | wx.showModal({ 43 | title: "订阅失败", 44 | content: res.result, 45 | showCancel: false 46 | }) 47 | } 48 | }); 49 | } 50 | } 51 | }) 52 | -------------------------------------------------------------------------------- /client/pages/enchashment/index.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 | 提现微信 8 | 9 | 10 | 11 | 12 | 13 | 提现积分 14 | 15 | 16 | 17 | 18 | 19 | 钱包总积分 {{credit}} 20 | 21 | 22 | 1、积分兑换现金的规则为 1 积分 = 0.5 元; 2、每次提现最低为 100 积分; 3、每次提现收取最终现金的 30% 作为手续费,提现申请后 1-3 天将会到账。 23 | 24 | 25 | 26 |
27 | 28 | 29 |
30 | -------------------------------------------------------------------------------- /client/pages/myblist/index.js: -------------------------------------------------------------------------------- 1 | const app=getApp() 2 | const util = require('../../utils/util.js'); 3 | 4 | Page({ 5 | data:{ 6 | booklist:[], 7 | page: 1, 8 | size: 30, 9 | hasMore: false, 10 | loading: false 11 | }, 12 | loadMore:function(){ 13 | const that = this; 14 | if (!this.data.hasMore) return; 15 | util.request('course/listMyCourse',{ page: this.data.page++, size: this.data.size }).then(res => { 16 | if (res.status === 0) { 17 | let result = res.result; 18 | if (result.length){ 19 | result = util.changeArr(result, 'id', 'course_id'); 20 | that.setData({ 21 | booklist: that.data.booklist.concat(result), 22 | loading: false 23 | }); 24 | }else{ 25 | that.setData({ hasMore: false }) 26 | } 27 | } 28 | }); 29 | }, 30 | onLoad: function(){ 31 | this.setData({ hasMore: true, loading: true }) 32 | this.loadMore(); 33 | }, 34 | onReachBottom: function () { 35 | this.setData({ 36 | loading: true 37 | }) 38 | this.loadMore(); 39 | }, 40 | goToWrite: function(){ 41 | wx.navigateTo({ 42 | url: '../setting/index', 43 | url: '../editcourse/index' 44 | }); 45 | } 46 | }) -------------------------------------------------------------------------------- /server/app/controller/collect.js: -------------------------------------------------------------------------------- 1 | const Base = require('./base'); 2 | 3 | class CollectController extends Base { 4 | async listMyCollect(){ 5 | const {ctx, app} = this; 6 | const result = await ctx.service.collect.listMyCollect(); 7 | return this.success(result); 8 | } 9 | 10 | async listMyCollectRange(){ 11 | const {ctx} = this; 12 | const page = ctx.request.body.page; 13 | const size = ctx.request.body.size; 14 | const result = await ctx.service.collect.listMyCollectRange(page, size); 15 | return this.success(result); 16 | } 17 | 18 | async addOrDeleteCollect(){ 19 | const {ctx, app} = this; 20 | const courseID = ctx.request.body.courseID; 21 | const collect = await ctx.service.collect.getByUID(courseID); 22 | let collectRes = null; 23 | let handleType = 1; 24 | if(ctx.helper.isEmpty(collect)){ 25 | // add 26 | collectRes = await ctx.service.collect.insert({ 27 | user_id: app.userId, 28 | course_id: courseID 29 | }); 30 | }else{ 31 | // delete 32 | collectRes = await ctx.service.collect.delete(collect.id); 33 | handleType = 0; 34 | } 35 | 36 | if(collectRes){ 37 | return this.success(handleType); 38 | } 39 | 40 | return this.fail("操作失败"); 41 | } 42 | } 43 | 44 | module.exports = CollectController; -------------------------------------------------------------------------------- /admin/src/utils/utils.js: -------------------------------------------------------------------------------- 1 | export function typeOf(obj) { 2 | const toString = Object.prototype.toString; 3 | const map = { 4 | '[object Boolean]': 'boolean', 5 | '[object Number]': 'number', 6 | '[object String]': 'string', 7 | '[object Function]': 'function', 8 | '[object Array]': 'array', 9 | '[object Date]': 'date', 10 | '[object RegExp]': 'regExp', 11 | '[object Undefined]': 'undefined', 12 | '[object Null]': 'null', 13 | '[object Object]': 'object' 14 | }; 15 | return map[toString.call(obj)]; 16 | } 17 | 18 | /** 19 | * 深拷贝 20 | */ 21 | export const deepCopy = (obj) => { 22 | const t = typeOf(obj); 23 | let o; 24 | 25 | if (t === 'array') { 26 | o = []; 27 | } else if (t === 'object') { 28 | o = {}; 29 | } else { 30 | return obj; 31 | } 32 | 33 | if (t === 'array') { 34 | for (let i = 0; i < obj.length; i++) { 35 | o.push(deepCopy(obj[i])); 36 | } 37 | } else if (t === 'object') { 38 | for (let i in obj) { 39 | o[i] = deepCopy(obj[i]); 40 | } 41 | } 42 | return o; 43 | } 44 | 45 | export const removeNull = function(obj){ 46 | let o = deepCopy(obj); 47 | let param = {}; 48 | for(let k in o){ 49 | if(o[k] != null && o[k] != undefined && o[k] != '' ){ 50 | param[k] = o[k]; 51 | } 52 | } 53 | return param; 54 | } -------------------------------------------------------------------------------- /admin/src/pages/manage/index.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 39 | -------------------------------------------------------------------------------- /server/app/controller/admin/auth.js: -------------------------------------------------------------------------------- 1 | const Base = require('./base'); 2 | const bcrypt = require('bcrypt'); 3 | 4 | class AuthController extends Base { 5 | constructor(props) { 6 | super(props); 7 | this.token =new this.ctx.helper.Token(this.ctx); 8 | } 9 | async login(){ 10 | const {ctx} = this; 11 | const username = ctx.request.body.userName; 12 | const password = ctx.request.body.password; 13 | const admin = await ctx.service.admin.admin.getByUName(username); 14 | if (ctx.helper.isEmpty(admin)) { 15 | return this.fail('不存在该用户'); 16 | } 17 | // 解盐 18 | if (!bcrypt.compareSync(password, admin.password_hash)) { 19 | return this.fail('密码不正确'); 20 | } 21 | 22 | // 更新登录信息 23 | await ctx.service.admin.admin.update({ 24 | id: admin.id, 25 | last_login_time: ctx.helper.timest(), 26 | last_login_ip: ctx.ip 27 | }); 28 | 29 | const sessionKey = await this.token.create({ 30 | user_id: admin.id 31 | }); 32 | 33 | if (ctx.helper.isEmpty(sessionKey)) { 34 | return this.fail('登录失败'); 35 | } 36 | 37 | const userInfo = { 38 | id: admin.id, 39 | username: admin.username 40 | }; 41 | 42 | return this.success({ token: sessionKey, userInfo: userInfo }); 43 | } 44 | } 45 | 46 | module.exports = AuthController; -------------------------------------------------------------------------------- /server/app/controller/purchase.js: -------------------------------------------------------------------------------- 1 | const Base = require('./base'); 2 | 3 | class PurchaseController extends Base { 4 | async addPurchase(){ 5 | const {ctx, app} = this; 6 | const courseID = ctx.request.body.courseID; 7 | const status = await ctx.service.purchase.addPurchase(courseID); 8 | if(status == 1){ 9 | this.success("订阅成功"); 10 | }else{ 11 | this.fail("账户积分不足,请加客服微信fcsboy,进行充值"); 12 | } 13 | } 14 | 15 | async listMyPurchase(){ 16 | const {ctx, app} = this; 17 | const page = ctx.request.body.page; 18 | const size = ctx.request.body.size; 19 | const result = await ctx.service.purchase.selectMyPurchase(page, size); 20 | return this.success(result); 21 | } 22 | 23 | async listMyPurchaseAboutMe(){ 24 | const {ctx, app} = this; 25 | const page = ctx.request.body.page; 26 | const size = ctx.request.body.size; 27 | const result = await ctx.service.purchase.selectPurchaseAboutMe(page, size); 28 | const userInfo = await ctx.service.user.find({ 29 | id: app.userId 30 | }); 31 | let param = { 32 | list: result, 33 | credit: userInfo.credit 34 | } 35 | 36 | return this.success(param); 37 | } 38 | 39 | async getPayCount(){ 40 | const {ctx, app} = this; 41 | const result = await ctx.service.purchase.getPayCount(); 42 | return result; 43 | } 44 | } 45 | 46 | module.exports = PurchaseController; -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "1.0.0", 4 | "description": "booklist server", 5 | "private": true, 6 | "dependencies": { 7 | "bcrypt": "^2.0.1", 8 | "egg": "^2.2.1", 9 | "egg-cors": "^2.0.0", 10 | "egg-mysql": "^3.0.0", 11 | "egg-scripts": "^2.5.0", 12 | "jsonwebtoken": "^8.3.0", 13 | "log4js": "^3.0.5" 14 | }, 15 | "devDependencies": { 16 | "autod": "^3.0.1", 17 | "autod-egg": "^1.0.0", 18 | "egg-bin": "^4.3.5", 19 | "egg-ci": "^1.8.0", 20 | "egg-mock": "^3.14.0", 21 | "eslint": "^4.11.0", 22 | "eslint-config-egg": "^6.0.0", 23 | "webstorm-disable-index": "^1.2.0" 24 | }, 25 | "engines": { 26 | "node": ">=8.9.0" 27 | }, 28 | "scripts": { 29 | "start": "egg-scripts start --sticky", 30 | "stop": "egg-scripts stop --title=egg-server-server", 31 | "dev": "egg-bin dev --sticky", 32 | "debug": "egg-bin debug", 33 | "test": "npm run lint -- --fix && npm run test-local", 34 | "test-local": "egg-bin test", 35 | "cov": "egg-bin cov", 36 | "lint": "eslint .", 37 | "ci": "npm run lint && npm run cov", 38 | "autod": "autod" 39 | }, 40 | "ci": { 41 | "version": "8" 42 | }, 43 | "repository": { 44 | "type": "git", 45 | "url": "" 46 | }, 47 | "author": "xiaoxiaoxiao", 48 | "license": "MIT" 49 | } 50 | -------------------------------------------------------------------------------- /server/app/service/collect.js: -------------------------------------------------------------------------------- 1 | const Service = require('egg').Service; 2 | 3 | class CollectService extends Service{ 4 | async getByUID(courseID){ 5 | const {app} = this; 6 | const result = await app.mysql.get('collect', {user_id: app.userId, course_id: courseID}) 7 | return result; 8 | } 9 | 10 | async selectByUID(){ 11 | const result = await this.app.mysql.select('collect', { 12 | where: {user_id: this.app.userId} 13 | }); 14 | return result; 15 | } 16 | 17 | async insert(data){ 18 | const result = await this.app.mysql.insert('collect', data); 19 | return result; 20 | } 21 | 22 | async delete(id){ 23 | const result = await this.app.mysql.delete('collect', { 24 | id: id, 25 | }); 26 | return result; 27 | } 28 | 29 | async listMyCollect(){ 30 | const {app} = this; 31 | const result = await app.mysql.query('select * from course left join collect on course.id=collect.course_id where collect.user_id='+app.userId+';'); 32 | return result; 33 | } 34 | 35 | async listMyCollectRange(page, size){ 36 | const {app} = this; 37 | const result = await app.mysql.query('select collect.course_id,course.title,course.publish from course left join collect on course.id=collect.course_id where collect.user_id='+app.userId+' LIMIT '+(page-1)*size+','+size); 38 | return result; 39 | } 40 | } 41 | 42 | module.exports = CollectService; -------------------------------------------------------------------------------- /admin/build/build.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | require('./check-versions')() 3 | 4 | process.env.NODE_ENV = 'production' 5 | 6 | const ora = require('ora') 7 | const rm = require('rimraf') 8 | const path = require('path') 9 | const chalk = require('chalk') 10 | const webpack = require('webpack') 11 | const config = require('../config') 12 | const webpackConfig = require('./webpack.prod.conf') 13 | 14 | const spinner = ora('building for production...') 15 | spinner.start() 16 | 17 | rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => { 18 | if (err) throw err 19 | webpack(webpackConfig, (err, stats) => { 20 | spinner.stop() 21 | if (err) throw err 22 | process.stdout.write(stats.toString({ 23 | colors: true, 24 | modules: false, 25 | children: false, // If you are using ts-loader, setting this to true will make TypeScript errors show up during build. 26 | chunks: false, 27 | chunkModules: false 28 | }) + '\n\n') 29 | 30 | if (stats.hasErrors()) { 31 | console.log(chalk.red(' Build failed with errors.\n')) 32 | process.exit(1) 33 | } 34 | 35 | console.log(chalk.cyan(' Build complete.\n')) 36 | console.log(chalk.yellow( 37 | ' Tip: built files are meant to be served over an HTTP server.\n' + 38 | ' Opening index.html over file:// won\'t work.\n' 39 | )) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /client/pages/wallet/index.wxss: -------------------------------------------------------------------------------- 1 | /* pages/wallet/index.wxss */ 2 | .container{ 3 | background: #f7f7f7; 4 | } 5 | .main{ 6 | position: relative; 7 | background-color: white; 8 | width: 750rpx; 9 | height: 455rpx; 10 | text-align: center; 11 | } 12 | .main_img{ 13 | margin-top: 50rpx; 14 | font-size:0; 15 | } 16 | .main_img image{ 17 | width: 82rpx; 18 | height: 82rpx; 19 | } 20 | .main_intr{ 21 | font-size: 24rpx; 22 | color: #999999; 23 | margin-top: 30rpx; 24 | } 25 | .main_credit{ 26 | font-size: 44rpx; 27 | color: #24253D; 28 | } 29 | .main_btn{ 30 | display: flex; 31 | padding: 50rpx 68rpx 0; 32 | justify-content: space-between; 33 | } 34 | .main_recharge{ 35 | width: 288rpx; 36 | height: 88rpx; 37 | line-height: 88rpx; 38 | background: #D73316; 39 | border-radius: 8rpx; 40 | font-size: 28rpx; 41 | color: #FFFFFF; 42 | } 43 | 44 | .main_gowx{ 45 | font-size: 28rpx; 46 | width: 288rpx; 47 | height: 88rpx; 48 | line-height: 88rpx; 49 | border: 1px solid #D73316; 50 | border-radius: 8rpx; 51 | color: #D73316; 52 | } 53 | .list{ 54 | width: 750rpx; 55 | } 56 | .list_title{ 57 | font-size: 24rpx; 58 | color: #999999; 59 | padding: 35rpx 0 35rpx 35rpx; 60 | } 61 | 62 | .list_content{ 63 | background-color: white; 64 | } 65 | 66 | .bottomTips{ 67 | font-size: 28rpx; 68 | text-align: center; 69 | padding: 50rpx; 70 | color: #ccc; 71 | } -------------------------------------------------------------------------------- /client/service/user.js: -------------------------------------------------------------------------------- 1 | const api = require('./api.js'); 2 | const util = require('../utils/util.js'); 3 | 4 | /** 5 | * 调用微信登录 6 | */ 7 | const loginByWeixin = ()=> { 8 | let code = null; 9 | return new Promise(function (resolve, reject) { 10 | return util.login().then((res) => { 11 | code = res.code; 12 | return util.getUserInfo(); 13 | }).then((userInfo) => { 14 | //登录远程服务器 15 | util.request("auth/loginByWeixinAction", { code: code, userInfo: userInfo }, 'POST').then(res => { 16 | if (res.status === 0) { 17 | //存储用户信息 18 | wx.setStorageSync('userInfo', res.result.userInfo); 19 | wx.setStorageSync('token', res.result.token); 20 | resolve(res); 21 | } else { 22 | reject(res); 23 | } 24 | }).catch((err) => { 25 | reject(err); 26 | }); 27 | }).catch((err) => { 28 | reject(err); 29 | }) 30 | }); 31 | } 32 | 33 | 34 | /** 35 | * 判断用户是否登录 36 | */ 37 | const checkLogin = ()=> { 38 | return new Promise(function (resolve, reject) { 39 | if (wx.getStorageSync('userInfo') && wx.getStorageSync('token')) { 40 | util.checkSession().then(() => { 41 | resolve(true); 42 | }).catch(() => { 43 | reject(false); 44 | }); 45 | 46 | } else { 47 | reject(false); 48 | } 49 | }); 50 | } 51 | 52 | module.exports = { 53 | loginByWeixin, 54 | checkLogin, 55 | }; -------------------------------------------------------------------------------- /client/pages/editcourse/index.wxml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 专业 7 | 9 | 10 | 11 | 12 | 13 | x 14 | {{item.name}} 15 | 16 | 17 | 18 | 书名 19 | 21 | 22 | 23 | 作者 24 | 26 | 27 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /admin/build/check-versions.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const chalk = require('chalk') 3 | const semver = require('semver') 4 | const packageConfig = require('../package.json') 5 | const shell = require('shelljs') 6 | 7 | function exec (cmd) { 8 | return require('child_process').execSync(cmd).toString().trim() 9 | } 10 | 11 | const versionRequirements = [ 12 | { 13 | name: 'node', 14 | currentVersion: semver.clean(process.version), 15 | versionRequirement: packageConfig.engines.node 16 | } 17 | ] 18 | 19 | if (shell.which('npm')) { 20 | versionRequirements.push({ 21 | name: 'npm', 22 | currentVersion: exec('npm --version'), 23 | versionRequirement: packageConfig.engines.npm 24 | }) 25 | } 26 | 27 | module.exports = function () { 28 | const warnings = [] 29 | 30 | for (let i = 0; i < versionRequirements.length; i++) { 31 | const mod = versionRequirements[i] 32 | 33 | if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) { 34 | warnings.push(mod.name + ': ' + 35 | chalk.red(mod.currentVersion) + ' should be ' + 36 | chalk.green(mod.versionRequirement) 37 | ) 38 | } 39 | } 40 | 41 | if (warnings.length) { 42 | console.log('') 43 | console.log(chalk.yellow('To use this template, you must update following to modules:')) 44 | console.log() 45 | 46 | for (let i = 0; i < warnings.length; i++) { 47 | const warning = warnings[i] 48 | console.log(' ' + warning) 49 | } 50 | 51 | console.log() 52 | process.exit(1) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /admin/src/pages/usermanage/userinfo/index.js: -------------------------------------------------------------------------------- 1 | import {getUserList, updateCredit} from "@/service/api"; 2 | import {removeNull} from "@/utils/utils" 3 | export default { 4 | data(){ 5 | return{ 6 | listQuery:{ 7 | wechat_id: undefined, 8 | tel_id: undefined, 9 | username: undefined 10 | }, 11 | list:[] 12 | } 13 | }, 14 | mounted(){ 15 | this.handleCtr(); 16 | }, 17 | methods:{ 18 | async handleCtr(){ 19 | const result = await this.getListMode(); 20 | if(result.status == 0) 21 | this.list = result.result; 22 | 23 | }, 24 | async getListMode(){ 25 | const param = removeNull(this.listQuery); 26 | let result = await getUserList(param); 27 | if(result.status != 0){} 28 | result.result = result.result.map(v => { 29 | this.$set(v, 'edit', false) // https://vuejs.org/v2/guide/reactivity.html 30 | v.originalTitle = v.credit // will be used when user click the cancel botton 31 | return v 32 | }) 33 | return result; 34 | }, 35 | cancelEdit(row) { 36 | row.credit = row.originalTitle 37 | row.edit = false 38 | this.$message({ 39 | message: '取消修改', 40 | type: 'warning' 41 | }) 42 | }, 43 | async confirmEdit(row) { 44 | console.log(row.id, row.credit); 45 | const result = await updateCredit({ 46 | id:row.id, 47 | credit:row.credit 48 | }); 49 | if(result.status == 0){ 50 | row.edit = false 51 | row.originalTitle = row.credit 52 | this.$message({ 53 | message: result.result, 54 | type: 'success' 55 | }) 56 | } 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /client/pages/wallet/index.js: -------------------------------------------------------------------------------- 1 | const util = require('../../utils/util.js'); 2 | // pages/wallet/index.js 3 | Page({ 4 | 5 | /** 6 | * 页面的初始数据 7 | */ 8 | data: { 9 | listPurchase: [], 10 | credit: '', 11 | page: 1, 12 | size: 30, 13 | hasMore: false, 14 | loading: false 15 | }, 16 | loadMore:function(){ 17 | const that = this; 18 | if (!this.data.hasMore) return; 19 | util.request('purchase/listMyPurchaseAboutMe', { page: this.data.page++, size: this.data.size }).then(res => { 20 | if (res.status === 0) { 21 | let result = res.result; 22 | if (result.list.length) { 23 | that.setData({ 24 | listPurchase: that.data.listPurchase.concat(result.list), 25 | loading: false 26 | }); 27 | } else { 28 | that.setData({ hasMore: false }) 29 | } 30 | that.setData({ 31 | credit: result.credit 32 | }); 33 | } 34 | }); 35 | }, 36 | /** 37 | * 生命周期函数--监听页面加载 38 | */ 39 | onLoad: function (options) { 40 | this.setData({ hasMore: true, loading: true }) 41 | this.loadMore(); 42 | }, 43 | onReachBottom: function () { 44 | this.setData({ 45 | loading: true 46 | }) 47 | this.loadMore(); 48 | }, 49 | showCue:function(){ 50 | wx.showModal({ 51 | content: "加客服微信「 fcsboy 」,再进行充值", 52 | confirmText: "确定", 53 | showCancel: false 54 | }); 55 | } 56 | }) -------------------------------------------------------------------------------- /server/app/router/client.js: -------------------------------------------------------------------------------- 1 | module.exports = app => { 2 | const { router, controller } = app; 3 | const interceptor = app.middleware.interceptor(); 4 | router.post('/auth/loginByWeixinAction', interceptor, controller.auth.loginByWeixinAction); 5 | router.post('/course/addCourse', interceptor, controller.course.addCourse); 6 | router.post('/course/listMyCourse', interceptor, controller.course.listMyCourse); 7 | router.post('/course/listCourseDetail', interceptor, controller.course.listCourseDetail); 8 | router.post('/course/listAllCourse', interceptor, controller.course.listAllCourse); 9 | router.post('/collect/addOrDeleteCollect', interceptor, controller.collect.addOrDeleteCollect); 10 | router.post('/collect/listMyCollect', interceptor, controller.collect.listMyCollect); 11 | router.post('/collect/listMyCollectRange', interceptor, controller.collect.listMyCollectRange); 12 | router.post('/course/listSearchCourse', interceptor, controller.course.listSearchCourse); 13 | router.post('/user/setInfo', interceptor, controller.user.setInfo); 14 | router.post('/purchase/addPurchase', interceptor, controller.purchase.addPurchase); 15 | router.post('/purchase/listMyPurchase', interceptor, controller.purchase.listMyPurchase); 16 | router.post('/purchase/listMyPurchaseAboutMe', interceptor, controller.purchase.listMyPurchaseAboutMe); 17 | router.post('/enchashment/add', interceptor, controller.enchashment.add); 18 | router.post('/course/update', interceptor, controller.course.update); 19 | router.post('/purchase/getPayCount', interceptor, controller.purchase.getPayCount); 20 | 21 | } -------------------------------------------------------------------------------- /admin/src/pages/qualitymanage/review/index.js: -------------------------------------------------------------------------------- 1 | import {getReviewList, publish, revoke} from "@/service/api" 2 | import {removeNull} from "@/utils/utils" 3 | export default { 4 | data(){ 5 | return{ 6 | listQuery:{ 7 | tel_id: null, 8 | publish: '0', 9 | wechat_id: null, 10 | title: null, 11 | }, 12 | publishOptions:[{ 13 | key: '0', 14 | label: '未审核' 15 | },{ 16 | key: '1', 17 | label: '已发布' 18 | }], 19 | list: [] 20 | } 21 | }, 22 | mounted(){ 23 | this.handleCtr(); 24 | }, 25 | methods:{ 26 | async handleCtr(){ 27 | const result = await this.getListMode(); 28 | if(result.status == 0) 29 | this.list = result.result; 30 | }, 31 | async getListMode(){ 32 | const param = removeNull(this.listQuery); 33 | let result = await getReviewList(param); 34 | return result; 35 | }, 36 | async publish(index, row){ 37 | const result = await publish({ 38 | courseID: row.id 39 | }); 40 | if(result.status == 0){ 41 | this.$message({ 42 | message: result.result, 43 | type: 'success' 44 | }); 45 | this.list.splice(index, 1); 46 | } 47 | }, 48 | async revoke(index, row){ 49 | const result = await revoke({ 50 | courseID: row.id 51 | }); 52 | if(result.status == 0){ 53 | this.$message({ 54 | message: result.result, 55 | type: 'success' 56 | }); 57 | this.list.splice(index, 1); 58 | } 59 | } 60 | }, 61 | filters:{ 62 | statusFilter(status){ 63 | let str = ""; 64 | if(status == 0){ 65 | str = "审核中" 66 | }else{ 67 | str = "已发布"; 68 | } 69 | return str; 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /admin/src/pages/manage/navBar.vue: -------------------------------------------------------------------------------- 1 | 20 | 43 | 65 | -------------------------------------------------------------------------------- /client/pages/search/index.js: -------------------------------------------------------------------------------- 1 | const util = require('../../utils/util.js'); 2 | // pages/search/index.js 3 | Page({ 4 | 5 | /** 6 | * 页面的初始数据 7 | */ 8 | data: { 9 | inputValue: '', 10 | courseList: [], 11 | showTip: false, 12 | page: 1, 13 | size: 30, 14 | hasMore: false, 15 | loading:false 16 | }, 17 | 18 | loadMore:function(){ 19 | const that = this; 20 | if (!this.data.hasMore) return; 21 | util.request("course/listSearchCourse", { title: that.data.inputValue, page: this.data.page++, size: this.data.size}).then(res => { 22 | if (res.status === 0) { 23 | if (res.result.length){ 24 | that.setData({ 25 | courseList: that.data.courseList.concat(res.result), 26 | loading:false 27 | }); 28 | }else{ 29 | that.setData({ hasMore: false }) 30 | } 31 | if (that.data.courseList.length == 0){ 32 | that.setData({ 33 | showTip: true 34 | }); 35 | } 36 | } 37 | }); 38 | }, 39 | /** 40 | * 页面上拉触底事件的处理函数 41 | */ 42 | onReachBottom: function () { 43 | this.setData({ 44 | loading: true 45 | }) 46 | this.loadMore(); 47 | }, 48 | /** 49 | * 用户点击右上角分享 50 | */ 51 | onShareAppMessage: function () { 52 | 53 | }, 54 | bindKeyInput: function(e){ 55 | this.setData({ 56 | inputValue: e.detail.value 57 | }); 58 | }, 59 | search: function(){ 60 | this.setData({ 61 | courseList: [], 62 | page: 1 63 | }); 64 | this.setData({ 65 | hasMore: true, 66 | loading:true, 67 | showTip: false 68 | }); 69 | this.loadMore(); 70 | } 71 | }) -------------------------------------------------------------------------------- /client/pages/index/index.js: -------------------------------------------------------------------------------- 1 | const util = require('../../utils/util.js'); 2 | //获取应用实例 3 | const app = getApp() 4 | 5 | Page({ 6 | data: { 7 | page: 1, 8 | size: 30, 9 | courseList:[], 10 | hasMore: false, 11 | loading: false 12 | }, 13 | //事件处理函数 14 | bindViewTap: function() { 15 | wx.navigateTo({ 16 | url: '../myblist/index' 17 | }) 18 | }, 19 | loadMore: function () { 20 | const that = this; 21 | if (!this.data.hasMore) return; 22 | util.request('course/listAllCourse', { page:this.data.page++, size: this.data.size}).then(res => { 23 | if (res.status === 0) { 24 | wx.stopPullDownRefresh(); 25 | if (res.result.length) { 26 | that.setData({ 27 | courseList: that.data.courseList.concat(res.result), 28 | loading: false 29 | }); 30 | }else{ 31 | that.setData({ hasMore: false }) 32 | } 33 | 34 | } 35 | }); 36 | }, 37 | onLoad: function () { 38 | this.setData({ hasMore: true, loading:true}) 39 | this.loadMore(); 40 | }, 41 | getUserInfo: function(e) { 42 | app.globalData.userInfo = e.detail.userInfo 43 | this.setData({ 44 | userInfo: e.detail.userInfo, 45 | hasUserInfo: true 46 | }); 47 | }, 48 | onReachBottom() { 49 | this.setData({ 50 | loading: true 51 | }) 52 | this.loadMore() 53 | }, 54 | /** 55 | * 页面相关事件处理函数--监听用户下拉动作 56 | */ 57 | onPullDownRefresh() { 58 | this.setData({ 59 | courseList:[], 60 | page: 1 61 | }); 62 | this.setData({ 63 | hasMore:true, 64 | loading: true 65 | }); 66 | this.loadMore(); 67 | } 68 | }) 69 | -------------------------------------------------------------------------------- /admin/src/pages/usermanage/enchashment/index.js: -------------------------------------------------------------------------------- 1 | import {getEnchaList, enchashment, enchaRevoke} from "@/service/api" 2 | import {removeNull} from "@/utils/utils" 3 | export default { 4 | data(){ 5 | return{ 6 | listQuery:{ 7 | tel_id: null, 8 | verify: '0', 9 | wechat_id: null, 10 | username: null, 11 | }, 12 | enchashmentOptions:[{ 13 | key: '0', 14 | label: '未提现' 15 | },{ 16 | key: '1', 17 | label: '已提现' 18 | }], 19 | list: [] 20 | } 21 | }, 22 | mounted(){ 23 | this.handleCtr(); 24 | }, 25 | methods:{ 26 | async handleCtr(){ 27 | const result = await this.getListMode(); 28 | if(result.status == 0) 29 | this.list = result.result; 30 | }, 31 | async getListMode(){ 32 | const param = removeNull(this.listQuery); 33 | let result = await getEnchaList(param); 34 | return result; 35 | }, 36 | async enchashment(index, row){ 37 | const result = await enchashment({ 38 | enchashmentID: row.id 39 | }); 40 | if(result.status == 0){ 41 | this.$message({ 42 | message: result.result, 43 | type: 'success' 44 | }); 45 | this.list.splice(index, 1); 46 | } 47 | }, 48 | async revoke(index, row){ 49 | const result = await enchaRevoke({ 50 | enchashmentID: row.id 51 | }); 52 | if(result.status == 0){ 53 | this.$message({ 54 | message: result.result, 55 | type: 'success' 56 | }); 57 | this.list.splice(index, 1); 58 | } 59 | } 60 | }, 61 | filters:{ 62 | statusFilter(status){ 63 | let str = ""; 64 | if(status == 0){ 65 | str = "审核中" 66 | }else{ 67 | str = "已提现"; 68 | } 69 | return str; 70 | }, 71 | calculatCash(val){ 72 | return (val/2*0.7).toFixed(2); 73 | } 74 | } 75 | } -------------------------------------------------------------------------------- /admin/src/pages/manage/tabsView.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 59 | 60 | -------------------------------------------------------------------------------- /admin/src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | Vue.use(Router) 4 | 5 | // v1.0.0 6 | const manage = r => require.ensure([], () => r(require('@/pages/manage')), 'manage'); 7 | const wxk = r => require.ensure([], () => r(require('@/pages/wxk/')), 'wxk'); 8 | const login = r => require.ensure([], () => r(require('@/pages/login/')), 'login'); //登录 9 | const review = r => require.ensure([], () => r(require('@/pages/qualitymanage/review/index.vue')), 'review'); // 文章审核机制 10 | const enchashment = r => require.ensure([], () => r(require('@/pages/usermanage/enchashment/index.vue')), 'enchashment'); // 提现 11 | const userInfo = r => require.ensure([], () => r(require('@/pages/usermanage/userinfo/index.vue')), 'userinfo'); // 提现 12 | 13 | export default new Router({ 14 | mode: 'history', 15 | routes: [ 16 | { 17 | path: '/', 18 | redirect: '/manage' 19 | }, 20 | { 21 | path: '*', 22 | redirect: '/' 23 | }, 24 | { 25 | path: '/login', 26 | name: '登录', 27 | component: login 28 | }, 29 | { 30 | path: '/manage', 31 | component: manage, 32 | children: [{ 33 | path: '/review', 34 | component: review, 35 | name: '文章审核机制', 36 | meta: {nav: ['内容管理', '文章审核机制']} 37 | },{ 38 | path: '/enchashment', 39 | component: enchashment, 40 | name: '用户提现记录', 41 | meta: {nav: ['用户管理', '用户提现记录']} 42 | },{ 43 | path: '/userinfo', 44 | component: userInfo, 45 | name: '用户信息', 46 | meta: {nav: ['用户管理', '用户信息']} 47 | }] 48 | } 49 | ] 50 | }) 51 | -------------------------------------------------------------------------------- /server/app/service/user.js: -------------------------------------------------------------------------------- 1 | const Service = require('egg').Service; 2 | class UserService extends Service { 3 | async find(data){ 4 | const result = await this.app.mysql.get('user', data); 5 | let obj; 6 | if(result){ 7 | obj = { 8 | id: result.id, 9 | username: result.username, 10 | avatar: result.avatar, 11 | undergraduate: result.undergraduate, 12 | master_school: result.master_school, 13 | wechat_id: result.wechat_id, 14 | tel_id: result.tel_id, 15 | credit: result.credit, 16 | introduce: result.introduce 17 | }; 18 | }else{ 19 | obj = null; 20 | } 21 | return obj ; 22 | } 23 | 24 | async insert(data) { 25 | const result = await this.app.mysql.insert('user', data); 26 | return result; 27 | } 28 | 29 | async update(data){ 30 | const result = await this.app.mysql.update('user', data); 31 | return result; 32 | } 33 | 34 | async select(data){ 35 | const result = await this.app.mysql.select('user', data); 36 | return result; 37 | } 38 | 39 | async searchByTelWX(param){ 40 | const {app} = this; 41 | const wechat_id = param.wechat_id || ''; 42 | const tel_id = param.tel_id || ''; 43 | const username = param.username || ''; 44 | let str = `select id,wechat_id,username,tel_id,credit from user where wechat_id LIKE '%${wechat_id}%' AND tel_id LIKE '%${tel_id}%' AND username LIKE '%${username}%'`; 45 | const result = await app.mysql.query(str); 46 | return result; 47 | } 48 | 49 | async updateCredit(id, credit){ 50 | const {app} = this; 51 | const result = await app.mysql.update('user',{ 52 | id: id, 53 | credit : credit 54 | }); 55 | return result; 56 | } 57 | 58 | } 59 | module.exports = UserService; 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /client/components/booklist/index.js: -------------------------------------------------------------------------------- 1 | const util = require('../../utils/util.js'); 2 | // components/booklist/index.js 3 | Component({ 4 | /** 5 | * 组件的属性列表 6 | */ 7 | properties: { 8 | 9 | title: { 10 | type: String, 11 | value: '', 12 | }, 13 | isCollect: { 14 | type: String, 15 | value: '0' 16 | }, 17 | courseID:{ 18 | type: String, 19 | value: '' 20 | }, 21 | payCount:{ 22 | type: String, 23 | value: "0" 24 | }, 25 | headImg:{ 26 | type: String, 27 | value: "/images/head.png" 28 | }, 29 | master:{ 30 | type: String, 31 | value: "123" 32 | } 33 | }, 34 | /** 35 | * 组件的初始数据 36 | */ 37 | data: { 38 | noCollectImage: "/images/collect2.png", 39 | hasCollectImage: "/images/collect1.png", 40 | collectBackImage: "/images/collect2.png", 41 | userHasCollect: 1 42 | }, 43 | ready(){ 44 | if(this.properties.isCollect == 1){ 45 | this.setData({ 46 | "collectBackImage": this.data.hasCollectImage, 47 | "userHasCollect": 0 48 | }); 49 | }else{ 50 | this.setData({ 51 | "collectBackImage": this.data.noCollectImage, 52 | "userHasCollect": 1 53 | }); 54 | } 55 | }, 56 | /** 57 | * 组件的方法列表 58 | */ 59 | methods: { 60 | collectBookList(){ 61 | util.request('collect/addOrDeleteCollect', { courseID: this.properties.courseID }).then(res => { 62 | if (res.status === 0) { 63 | if(res.result == 0){ 64 | // 取消 65 | this.setData({ 66 | 'collectBackImage': this.data.noCollectImage 67 | }); 68 | }else{ 69 | // 收藏 70 | this.setData({ 71 | 'collectBackImage': this.data.hasCollectImage 72 | }); 73 | } 74 | } 75 | }); 76 | } 77 | } 78 | }) 79 | -------------------------------------------------------------------------------- /client/pages/editcourse/index.wxss: -------------------------------------------------------------------------------- 1 | .edit-course{ 2 | display: flex; 3 | flex-direction: column; 4 | padding: 118rpx 0 0 0; 5 | } 6 | .edit-course .course-inp{ 7 | width:750rpx; 8 | height: 100rpx; 9 | padding:0 32rpx; 10 | font-size:28rpx; 11 | line-height: 100rpx; 12 | color:#333333; 13 | background-color:#FFFFFF; 14 | } 15 | .search-placeholder{ 16 | color: rgba(36, 37, 61, .3); 17 | } 18 | .course-inp .label{ 19 | float:left; 20 | width:390rpx; 21 | } 22 | .course-inp .inp-text{ 23 | float:left; 24 | width:290rpx; 25 | height: 100rpx; 26 | } 27 | .inp-text input{ 28 | height: 100rpx; 29 | } 30 | 31 | .course-inp-two{ 32 | width:750rpx; 33 | height: 20rpx; 34 | background-color:#F5F5F5; 35 | } 36 | .course-add{ 37 | flex-direction: column; 38 | } 39 | .add-text{ 40 | position: relative; 41 | width:574rpx; 42 | padding: 17rpx 0; 43 | font-size:34rpx; 44 | background-color:#F5F5F5; 45 | margin:30rpx auto 0; 46 | padding-left: 36rpx; 47 | padding-right: 60rpx; 48 | border-radius: 16rpx; 49 | color: #24253D; 50 | } 51 | 52 | .add-text .text{ 53 | word-break: break-all; 54 | } 55 | 56 | .course-login .login-btn{ 57 | width:686rpx; 58 | height:93rpx; 59 | margin:80rpx auto 0; 60 | font-size:28rpx; 61 | line-height:93rpx; 62 | color:#FFFFFF; 63 | background-color:#D73316; 64 | border-radius: 8rpx; 65 | } 66 | 67 | .login-submit{ 68 | width:686rpx; 69 | height:93rpx; 70 | line-height:93rpx; 71 | background: #fff; 72 | border: 1px solid #D73316; 73 | margin-top: 24rpx; 74 | font-size: 28rpx; 75 | color: #D73316; 76 | } 77 | .login-submit.noMessage{ 78 | border: 1px solid rgba(36, 37, 61, .2); 79 | color: rgba(36, 37, 61, .2); 80 | } 81 | 82 | .close{ 83 | position: absolute; 84 | width: 40rpx; 85 | height: 40rpx; 86 | border-radius: 20rpx; 87 | background: rgba(0, 0, 0, .6); 88 | right: 10rpx; 89 | top: 10rpx; 90 | color: #fff; 91 | line-height: 36rpx; 92 | text-align: center; 93 | } -------------------------------------------------------------------------------- /client/pages/enchashment/index.js: -------------------------------------------------------------------------------- 1 | const util = require('../../utils/util.js'); 2 | // pages/enchashment/index.js 3 | Page({ 4 | 5 | /** 6 | * 页面的初始数据 7 | */ 8 | data: { 9 | credit: 0 10 | }, 11 | 12 | /** 13 | * 生命周期函数--监听页面加载 14 | */ 15 | onLoad: function (options) { 16 | this.setData({ 17 | credit: options.credit 18 | }); 19 | }, 20 | bindSave:function(e){ 21 | const wechat = e.detail.value.wechat || ""; 22 | const credit = e.detail.value.credit || ""; 23 | if(wechat == ""){ 24 | wx.showToast({ 25 | title: '微信号不能为空', 26 | icon: 'none', 27 | duration: 2000 28 | }); 29 | return; 30 | } 31 | 32 | if(credit == ""){ 33 | wx.showToast({ 34 | title: '积分不能为空', 35 | icon: 'none', 36 | duration: 2000 37 | }); 38 | return; 39 | } 40 | 41 | if (!/^[0-9]*$/.test(credit)) { 42 | wx.showToast({ 43 | title: '积分为数字类型', 44 | icon: 'none', 45 | duration: 2000 46 | }); 47 | return; 48 | } 49 | 50 | if(credit < 100){ 51 | wx.showToast({ 52 | title: '积分不能少于100', 53 | icon: 'none', 54 | duration: 2000 55 | }); 56 | return; 57 | } 58 | 59 | if (credit > parseInt(this.data.credit)){ 60 | wx.showToast({ 61 | title: '超过了可提现积分', 62 | icon: 'none', 63 | duration: 2000 64 | }); 65 | return; 66 | } 67 | 68 | const that = this; 69 | const param = { 70 | credit: credit, 71 | wechat: wechat 72 | } 73 | util.request('enchashment/add', param).then(res => { 74 | if (res.status === 0) { 75 | wx.showToast({ 76 | title: res.result, 77 | icon: 'success', 78 | duration: 5000, 79 | complete:function(){ 80 | wx.navigateTo({ 81 | url: '../wallet/index', 82 | }) 83 | } 84 | }); 85 | } 86 | }); 87 | } 88 | }) -------------------------------------------------------------------------------- /client/components/nav/index.js: -------------------------------------------------------------------------------- 1 | var appInstance = getApp(); 2 | var util = require("../../utils/util.js"); 3 | var api = require("../../service/api.js"); 4 | // components/nav/index.js 5 | Component({ 6 | /** 7 | * 组件的属性列表 8 | */ 9 | properties: { 10 | type:{ 11 | type:Boolean, 12 | value: true 13 | } 14 | }, 15 | 16 | /** 17 | * 组件的初始数据 18 | */ 19 | data: { 20 | title: "写书单", 21 | src : "/images/head.png", 22 | penimg: "/images/pen.png", 23 | logo: "/images/logo.png", 24 | navItems:[{ 25 | url: "/pages/collection/index", 26 | text: "我的收藏", 27 | imgUrl: "/images/1.png" 28 | },{ 29 | url: "/pages/myblist/index", 30 | text: "我的书单", 31 | imgUrl: "/images/2.png" 32 | }, 33 | // { 34 | // url: "/pages/purchase/index", 35 | // text: "我的订阅", 36 | // imgUrl: "/images/3.png" 37 | // }, 38 | // { 39 | // url: "/pages/wallet/index", 40 | // text: "我的钱包", 41 | // imgUrl: "/images/4.png" 42 | // }, 43 | { 44 | url: "/pages/setting/index", 45 | text: "我的设置", 46 | imgUrl: "/images/5.png" 47 | } 48 | ], 49 | isShowNav: false 50 | }, 51 | ready:function(){ 52 | let userInfo = wx.getStorageSync('userInfo'); 53 | let token = wx.getStorageSync('token'); 54 | 55 | // 页面显示 56 | if (userInfo && token) { 57 | appInstance.globalData.userInfo = userInfo; 58 | appInstance.globalData.token = token; 59 | } 60 | 61 | if (appInstance.globalData.userInfo.avatar){ 62 | this.setData({ 63 | src: appInstance.globalData.userInfo.avatar, 64 | }); 65 | } 66 | }, 67 | moved:function(){ 68 | console.log(123123); 69 | }, 70 | /** 71 | * 组件的方法列表 72 | */ 73 | methods: { 74 | showNav:function(){ 75 | if(this.data.isShowNav){ 76 | this.setData({ 77 | isShowNav: false 78 | }) 79 | }else{ 80 | this.setData({ 81 | isShowNav: true 82 | }) 83 | } 84 | } 85 | } 86 | }) 87 | -------------------------------------------------------------------------------- /server/app/controller/course.js: -------------------------------------------------------------------------------- 1 | const Base = require('./base'); 2 | 3 | class CourseController extends Base { 4 | async addCourse(){ 5 | const ctx = this.ctx; 6 | const title = ctx.request.body.title; 7 | const myCourse = ctx.request.body.myCourse; 8 | let addResult = await ctx.service.course.insert({ 9 | title: title, 10 | my_course: myCourse, 11 | user_id: ctx.app.userId 12 | }); 13 | 14 | if(addResult.affectedRows === 1){ 15 | return this.success("插入成功"); 16 | }else{ 17 | return this.fail("插入失败"); 18 | } 19 | } 20 | 21 | async update(){ 22 | const {ctx} = this; 23 | const title = ctx.request.body.title; 24 | const myCourse = ctx.request.body.myCourse; 25 | const courseID = ctx.request.body.courseID; 26 | const result = await ctx.service.course.update(courseID, myCourse, title); 27 | if(result.affectedRows === 1){ 28 | return this.success("更新成功"); 29 | }else{ 30 | return this.fail("更新失败"); 31 | } 32 | } 33 | 34 | async listMyCourse(){ 35 | const {ctx} = this; 36 | const page = ctx.request.body.page; 37 | const size = ctx.request.body.size; 38 | let result = await ctx.service.course.selectByUID(page, size); 39 | return this.success(result); 40 | } 41 | 42 | async listCourseDetail(){ 43 | const ctx = this.ctx; 44 | const id = ctx.request.body.id; 45 | let result = await ctx.service.course.getByID(id); 46 | return this.success(result); 47 | } 48 | 49 | async listAllCourse(){ 50 | const {ctx} = this; 51 | const page = ctx.request.body.page; 52 | const size = ctx.request.body.size; 53 | console.log(123123); 54 | const getCollectCourse = await ctx.service.course.selectCourseWithCollect(page, size); 55 | return this.success(getCollectCourse); 56 | } 57 | 58 | async listSearchCourse(){ 59 | const {ctx} = this; 60 | const title = ctx.request.body.title; 61 | const page = ctx.request.body.page; 62 | const size = ctx.request.body.size; 63 | const getSearchCourse = await ctx.service.course.selectCourseWithTitle(title, page, size); 64 | return this.success(getSearchCourse); 65 | } 66 | } 67 | 68 | module.exports = CourseController; -------------------------------------------------------------------------------- /client/pages/setting/index.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 用户名 5 | 7 | 8 | 13 | 14 | 本科院校及专业 15 | 17 | 18 | 19 | 研究生院校及专业(选填) 20 | 22 | 23 | 24 | 职业经历 25 | 27 | 28 | 29 | 微信号 30 | 32 | 33 | 34 | 手机号 35 | 37 | 38 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /client/pages/details/index.js: -------------------------------------------------------------------------------- 1 | var api = require('../../service/api.js'); 2 | const util = require('../../utils/util.js'); 3 | //index.js 4 | //获取应用实例 5 | const app = getApp() 6 | 7 | Page({ 8 | data: { 9 | motto: 'Hello World', 10 | userInfo: {}, 11 | hasUserInfo: false, 12 | canIUse: wx.canIUse('button.open-type.getUserInfo'), 13 | booklist:[], 14 | isPay: true, 15 | price: '', 16 | courseID: '', 17 | showEdit: false, 18 | myCourse: '', 19 | title:'' 20 | }, 21 | //事件处理函数 22 | bindViewTap: function() { 23 | }, 24 | onLoad: function (options) { 25 | this.setData({ 26 | courseID: options.courseID, 27 | title: options.title 28 | }); 29 | wx.setNavigationBarTitle({ 30 | title: options.title, 31 | }); 32 | this.getMode(); 33 | }, 34 | getMode:function(){ 35 | const that = this; 36 | util.request('course/listCourseDetail', { id: this.data.courseID }).then(res => { 37 | if (res.status === 0) { 38 | let arr = []; 39 | let result = res.result; 40 | let resultList = util.splitStr(result.my_course); 41 | for (let index in resultList) { 42 | arr.push({ 43 | text: resultList[index] 44 | }) 45 | } 46 | that.setData({ 47 | booklist: arr, 48 | userInfo: { 49 | username: result.username, 50 | undergraduate: result.undergraduate, 51 | avatar: result.avatar 52 | }, 53 | isPay: result.isPay, 54 | price: result.price, 55 | showEdit: result.showEdit, 56 | myCourse: result.my_course 57 | }); 58 | } 59 | }); 60 | }, 61 | getUserInfo: function(e) { 62 | app.globalData.userInfo = e.detail.userInfo 63 | this.setData({ 64 | userInfo: e.detail.userInfo, 65 | hasUserInfo: true 66 | }) 67 | }, 68 | hasPay:function(){ 69 | this.getMode(); 70 | }, 71 | editCourse:function(){ 72 | wx.redirectTo({ 73 | url: '../editcourse/index?myCourse=' + this.data.myCourse + '&title='+this.data.title + '&courseID='+this.data.courseID 74 | }) 75 | } 76 | }) 77 | -------------------------------------------------------------------------------- /server/app/service/purchase.js: -------------------------------------------------------------------------------- 1 | const Service = require('egg').Service; 2 | 3 | class PurchaseService extends Service { 4 | async addPurchase(courseID){ 5 | const {app,ctx} = this; 6 | const uInfo = await ctx.service.user.find({ 7 | id: app.userId 8 | }); 9 | const courseInfo = await ctx.service.course.selectByID(courseID); 10 | let status = 0; 11 | if(uInfo.credit >= courseInfo.price){ 12 | 13 | const result = await app.mysql.update("user", { 14 | id: app.userId, 15 | credit: uInfo.credit - courseInfo.price 16 | }); 17 | if(result.affectedRows === 1){ 18 | const result = await app.mysql.insert("purchase",{ 19 | course_id: courseID, 20 | user_id: app.userId, 21 | add_time: ctx.helper.timest() 22 | }); 23 | if(result.affectedRows === 1){ 24 | status = 1; 25 | } 26 | } 27 | } 28 | return status; 29 | } 30 | 31 | async selectMyPurchase(page, size){ 32 | const {app, ctx} = this; 33 | const result = await app.mysql.query('select c.id,c.title,c.publish from course as c left join purchase as p on c.id=p.course_id where p.user_id='+app.userId+' LIMIT '+(page-1)*size+','+size); 34 | return result; 35 | } 36 | 37 | async selectPurchaseAboutMe(page, size){ 38 | const {app, ctx} = this; 39 | const result = await app.mysql.query('select c.price,p.user_id,p.add_time from purchase as p left join course as c on c.id=p.course_id where p.course_id IN (select course_id from purchase where course_id IN (select id from course where user_id='+app.userId+')) OR p.user_id='+app.userId+' ORDER BY p.add_time LIMIT '+(page-1)*size+','+size); 40 | 41 | // 修改result字段,user_id 属性去除,user_id 为自身则是购买类型 0,不是自身则被订购类型 1 42 | if(result.length > 0){ 43 | for(let i in result){ 44 | if(result[i].user_id == app.userId) 45 | result[i]['type'] = 0; 46 | else 47 | result[i]['type'] = 1; 48 | delete result[i]['user_id']; 49 | } 50 | } 51 | return result; 52 | } 53 | 54 | async getPayCount(courseID){ 55 | const {app, ctx} = this; 56 | const result = await app.mysql.select('purchase', { 57 | where: { 58 | course_id: courseID 59 | } 60 | }); 61 | return result.length; 62 | } 63 | } 64 | 65 | module.exports = PurchaseService; -------------------------------------------------------------------------------- /server/app/extend/helper.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken'); 2 | const secret = 'FTD#@HBJRR@@gf'; 3 | 4 | 5 | // exports.token = { 6 | // async create(userInfo){ 7 | // const token = jwt.sign(userInfo, secret); 8 | // return token; 9 | // }, 10 | 11 | // async parse(ctx) { 12 | // if (ctx.app.token) { 13 | // try { 14 | // return jwt.verify(ctx.app.token, secret); 15 | // } catch (err) { 16 | // return null; 17 | // } 18 | // } 19 | // return null; 20 | // }, 21 | 22 | // async getUserId(ctx){ 23 | // const token = ctx.app.token; 24 | // console.log(123123, app); 25 | // if (!token) { 26 | // return 0; 27 | // } 28 | 29 | // // const result = await this.parse(ctx); 30 | // // console.log('result', result); 31 | // // if (isEmpty(result) || result.user_id <= 0) { 32 | // // return 0; 33 | // // } 34 | 35 | // // return result.user_id; 36 | // } 37 | // } 38 | 39 | const isEmpty = function(obj){ 40 | if(obj) return false; 41 | return true; 42 | } 43 | 44 | exports.isEmpty = isEmpty; 45 | 46 | class Token{ 47 | constructor(ctx){ 48 | this.ctx = ctx; 49 | } 50 | async create(userInfo){ 51 | const token = jwt.sign(userInfo, secret); 52 | return token; 53 | } 54 | 55 | async parse() { 56 | if (this.ctx.app.token) { 57 | try { 58 | return jwt.verify(this.ctx.app.token, secret); 59 | } catch (err) { 60 | return null; 61 | } 62 | } 63 | return null; 64 | } 65 | 66 | async getUserId(){ 67 | const token = this.ctx.app.token; 68 | if (!token) { 69 | return 0; 70 | } 71 | const result = await this.parse(); 72 | if (isEmpty(result) || result.data.user_id <= 0) { 73 | return 0; 74 | } 75 | return result.data.user_id; 76 | } 77 | } 78 | 79 | exports.Token = Token; 80 | 81 | //从1970年开始的毫秒数然后截取10位变成 从1970年开始的秒数 82 | const timest = function() { 83 | var tmp = Date.parse( new Date() ).toString(); 84 | tmp = tmp.substr(0,10); 85 | return tmp; 86 | } 87 | 88 | exports.timest = timest; 89 | 90 | const halfArr = function(str){ 91 | let arr = str.split(","); 92 | arr = arr.slice(Math.ceil(arr.length/2)).join(','); 93 | return arr; 94 | } 95 | 96 | exports.halfArr = halfArr; -------------------------------------------------------------------------------- /admin/config/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | // Template version: 1.3.1 3 | // see http://vuejs-templates.github.io/webpack for documentation. 4 | 5 | const path = require('path') 6 | 7 | module.exports = { 8 | dev: { 9 | 10 | // Paths 11 | assetsSubDirectory: 'static', 12 | assetsPublicPath: '/', 13 | proxyTable: {}, 14 | 15 | // Various Dev Server settings 16 | host: 'localhost', // can be overwritten by process.env.HOST 17 | port: 8080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined 18 | autoOpenBrowser: false, 19 | errorOverlay: true, 20 | notifyOnErrors: true, 21 | poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions- 22 | 23 | 24 | /** 25 | * Source Maps 26 | */ 27 | 28 | // https://webpack.js.org/configuration/devtool/#development 29 | devtool: 'cheap-module-eval-source-map', 30 | 31 | // If you have problems debugging vue-files in devtools, 32 | // set this to false - it *may* help 33 | // https://vue-loader.vuejs.org/en/options.html#cachebusting 34 | cacheBusting: true, 35 | 36 | cssSourceMap: true 37 | }, 38 | 39 | build: { 40 | // Template for index.html 41 | index: path.resolve(__dirname, '../dist/index.html'), 42 | 43 | // Paths 44 | assetsRoot: path.resolve(__dirname, '../dist'), 45 | assetsSubDirectory: 'static', 46 | assetsPublicPath: '/', 47 | 48 | /** 49 | * Source Maps 50 | */ 51 | 52 | productionSourceMap: true, 53 | // https://webpack.js.org/configuration/devtool/#production 54 | devtool: '#source-map', 55 | 56 | // Gzip off by default as many popular static hosts such as 57 | // Surge or Netlify already gzip all static assets for you. 58 | // Before setting to `true`, make sure to: 59 | // npm install --save-dev compression-webpack-plugin 60 | productionGzip: false, 61 | productionGzipExtensions: ['js', 'css'], 62 | 63 | // Run the build command with an extra argument to 64 | // View the bundle analyzer report after build finishes: 65 | // `npm run build --report` 66 | // Set to `true` or `false` to always turn it on or off 67 | bundleAnalyzerReport: process.env.npm_config_report 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /admin/src/style/common.scss: -------------------------------------------------------------------------------- 1 | body, div, span, header, footer, nav, section, aside, article, ul, dl, dt, dd, li, a, p, h1, h2, h3, h4,h5, h6, i, b, textarea, button, input, select, figure, figcaption { 2 | padding: 0; 3 | margin: 0; 4 | list-style: none; 5 | font-style: normal; 6 | text-decoration: none; 7 | border: none; 8 | font-family: "Microsoft Yahei",sans-serif; 9 | -webkit-tap-highlight-color:transparent; 10 | -webkit-font-smoothing: antialiased; 11 | &:focus { 12 | outline: none; 13 | } 14 | } 15 | 16 | /*定义滚动条高宽及背景 高宽分别对应横竖滚动条的尺寸*/ 17 | ::-webkit-scrollbar 18 | { 19 | width: 0px; 20 | height: 0px; 21 | background-color: #F5F5F5; 22 | } 23 | 24 | /*定义滚动条轨道 内阴影+圆角*/ 25 | ::-webkit-scrollbar-track 26 | { 27 | -webkit-box-shadow: inset 0 0 1px rgba(0,0,0,0); 28 | border-radius: 10px; 29 | background-color: #F5F5F5; 30 | } 31 | 32 | /*定义滑块 内阴影+圆角*/ 33 | ::-webkit-scrollbar-thumb 34 | { 35 | border-radius: 10px; 36 | -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3); 37 | background-color: #555; 38 | } 39 | 40 | input[type="button"], input[type="submit"], input[type="search"], input[type="reset"] { 41 | -webkit-appearance: none; 42 | } 43 | 44 | textarea { -webkit-appearance: none;} 45 | 46 | html,body{ 47 | height: 100%; 48 | width: 100%; 49 | // background-color: #F5F5F5; 50 | } 51 | 52 | .fillcontain{ 53 | height: 100%; 54 | width: 100%; 55 | } 56 | .clear:after{ 57 | content: ''; 58 | display: block; 59 | clear: both; 60 | } 61 | 62 | .clear{ 63 | zoom:1; 64 | } 65 | 66 | .back_img{ 67 | background-repeat: no-repeat; 68 | background-size: 100% 100%; 69 | } 70 | 71 | .margin{ 72 | margin: 0 auto; 73 | } 74 | 75 | .left{ 76 | float: left; 77 | } 78 | 79 | .right{ 80 | float: right; 81 | } 82 | 83 | .hide{ 84 | display: none; 85 | } 86 | 87 | .show{ 88 | display: block; 89 | } 90 | 91 | .ellipsis{ 92 | overflow: hidden; 93 | text-overflow: ellipsis; 94 | white-space: nowrap; 95 | } 96 | 97 | .width1200{ 98 | width: 1200px; 99 | } 100 | 101 | .app-container { 102 | padding: 20px; 103 | } -------------------------------------------------------------------------------- /client/components/nav/index.wxss: -------------------------------------------------------------------------------- 1 | /* components/nav/index.wxss */ 2 | .ftd-nav{ 3 | width: 750rpx; 4 | height: 90rpx; 5 | background: #FFFFFF; 6 | box-shadow: 0 4px 4px 0 rgba(227,227,227,0.50); 7 | position: absolute; 8 | top: 0; 9 | left: 0; 10 | } 11 | .ftd-nav__logo{ 12 | position: absolute; 13 | top: 12rpx; 14 | left: 45rpx; 15 | width: 47rpx; 16 | height: 58rpx; 17 | } 18 | .ftd-nav__img{ 19 | width: 32rpx; 20 | height: 32rpx; 21 | margin-right: 12rpx; 22 | } 23 | .ftd-nav__write{ 24 | position: absolute; 25 | width: 136rpx; 26 | padding-right: 24rpx; 27 | height: 50rpx; 28 | top: 20rpx; 29 | right: 114rpx; 30 | font-size: 28rpx; 31 | text-align: right; 32 | color:#FFFFFF; 33 | line-height: 50rpx; 34 | background: #D73316; 35 | background-size: 23rpx 22rpx; 36 | border-radius: 70rpx; 37 | } 38 | .ftd-nav__sub{ 39 | position: absolute; 40 | width: 105rpx; 41 | height: 40rpx; 42 | top: 24rpx; 43 | right: 110rpx; 44 | font-size: 10px; 45 | text-align: center; 46 | color:#FFFFFF; 47 | line-height: 40rpx; 48 | padding-right: 6rpx; 49 | background: #D73316; 50 | background-size: 23rpx 22rpx; 51 | } 52 | .ftd-nav__write image{ 53 | position: absolute; 54 | left: 18rpx; 55 | top: 12rpx; 56 | width: 28rpx; 57 | height: 26rpx; 58 | } 59 | .ftd-nav__icon{ 60 | position: absolute; 61 | top: 15rpx; 62 | right: 30rpx; 63 | width: 60rpx; 64 | height: 60rpx; 65 | border-radius: 30rpx; 66 | } 67 | .ftd-nav__panel{ 68 | position: absolute; 69 | top: 82rpx; 70 | right: 13rpx; 71 | padding: 15rpx 42rpx; 72 | background: #FFFFFF; 73 | border: 1px solid #F0F0F0; 74 | box-shadow: 0 4px 8px 0 rgba(221,221,221,0.50); 75 | z-index: 99999; 76 | } 77 | /* .ftd-nav__panel::after{ 78 | content: ''; 79 | position: absolute; 80 | transform: rotate(45deg) translate(6px, 6px); 81 | display: block; 82 | width: 14rpx; 83 | height: 14rpx; 84 | background: #fff; 85 | top: -23rpx; 86 | right: 40rpx; 87 | border-top: 1px solid #F0F0F0; 88 | border-left: 1px solid #F0F0F0; 89 | z-index: -1; 90 | } */ 91 | .ftd-nav__content{ 92 | display: flex; 93 | align-items:center; 94 | padding:15rpx 0; 95 | } 96 | .ftd-nav__navigator{ 97 | font-size: 28rpx; 98 | color: #24253D; 99 | text-align: center; 100 | } -------------------------------------------------------------------------------- /admin/build/webpack.base.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const utils = require('./utils') 4 | const config = require('../config') 5 | const vueLoaderConfig = require('./vue-loader.conf') 6 | 7 | function resolve (dir) { 8 | return path.join(__dirname, '..', dir) 9 | } 10 | 11 | 12 | 13 | module.exports = { 14 | context: path.resolve(__dirname, '../'), 15 | entry: { 16 | app: './src/main.js' 17 | }, 18 | output: { 19 | path: config.build.assetsRoot, 20 | filename: '[name].js', 21 | publicPath: process.env.NODE_ENV === 'production' 22 | ? config.build.assetsPublicPath 23 | : config.dev.assetsPublicPath 24 | }, 25 | resolve: { 26 | extensions: ['.js', '.vue', '.json'], 27 | alias: { 28 | 'vue$': 'vue/dist/vue.esm.js', 29 | '@': resolve('src'), 30 | } 31 | }, 32 | module: { 33 | rules: [ 34 | { 35 | test: /\.vue$/, 36 | loader: 'vue-loader', 37 | options: vueLoaderConfig 38 | }, 39 | { 40 | test: /\.js$/, 41 | loader: 'babel-loader', 42 | include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')] 43 | }, 44 | { 45 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 46 | loader: 'url-loader', 47 | options: { 48 | limit: 10000, 49 | name: utils.assetsPath('img/[name].[hash:7].[ext]') 50 | } 51 | }, 52 | { 53 | test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, 54 | loader: 'url-loader', 55 | options: { 56 | limit: 10000, 57 | name: utils.assetsPath('media/[name].[hash:7].[ext]') 58 | } 59 | }, 60 | { 61 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 62 | loader: 'url-loader', 63 | options: { 64 | limit: 10000, 65 | name: utils.assetsPath('fonts/[name].[hash:7].[ext]') 66 | } 67 | } 68 | ] 69 | }, 70 | node: { 71 | // prevent webpack from injecting useless setImmediate polyfill because Vue 72 | // source contains it (although only uses it if it's native). 73 | setImmediate: false, 74 | // prevent webpack from injecting mocks to Node native modules 75 | // that does not make sense for the client 76 | dgram: 'empty', 77 | fs: 'empty', 78 | net: 'empty', 79 | tls: 'empty', 80 | child_process: 'empty' 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /admin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "admin", 3 | "version": "1.0.0", 4 | "description": "A Vue.js project", 5 | "author": "hbj <301085344@qq.com>", 6 | "private": true, 7 | "scripts": { 8 | "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js", 9 | "start": "npm run dev", 10 | "unit": "jest --config test/unit/jest.conf.js --coverage", 11 | "test": "npm run unit", 12 | "build": "node build/build.js" 13 | }, 14 | "dependencies": { 15 | "element-ui": "^2.4.1", 16 | "vue": "^2.5.2", 17 | "vue-router": "^3.0.1", 18 | "vuex": "^3.0.1", 19 | "vuex-persistedstate": "^2.5.4" 20 | }, 21 | "devDependencies": { 22 | "autoprefixer": "^7.1.2", 23 | "babel-core": "^6.22.1", 24 | "babel-helper-vue-jsx-merge-props": "^2.0.3", 25 | "babel-jest": "^21.0.2", 26 | "babel-loader": "^7.1.1", 27 | "babel-plugin-dynamic-import-node": "^1.2.0", 28 | "babel-plugin-syntax-jsx": "^6.18.0", 29 | "babel-plugin-transform-es2015-modules-commonjs": "^6.26.0", 30 | "babel-plugin-transform-runtime": "^6.22.0", 31 | "babel-plugin-transform-vue-jsx": "^3.5.0", 32 | "babel-preset-env": "^1.3.2", 33 | "babel-preset-stage-2": "^6.22.0", 34 | "chalk": "^2.0.1", 35 | "copy-webpack-plugin": "^4.0.1", 36 | "css-loader": "^0.28.0", 37 | "extract-text-webpack-plugin": "^3.0.0", 38 | "file-loader": "^1.1.4", 39 | "friendly-errors-webpack-plugin": "^1.6.1", 40 | "html-webpack-plugin": "^2.30.1", 41 | "jest": "^22.0.4", 42 | "jest-serializer-vue": "^0.3.0", 43 | "node-notifier": "^5.1.2", 44 | "node-sass": "^4.9.0", 45 | "optimize-css-assets-webpack-plugin": "^3.2.0", 46 | "ora": "^1.2.0", 47 | "portfinder": "^1.0.13", 48 | "postcss-import": "^11.0.0", 49 | "postcss-loader": "^2.0.8", 50 | "postcss-url": "^7.2.1", 51 | "rimraf": "^2.6.0", 52 | "sass-loader": "^7.0.3", 53 | "semver": "^5.3.0", 54 | "shelljs": "^0.7.6", 55 | "uglifyjs-webpack-plugin": "^1.1.1", 56 | "url-loader": "^0.5.8", 57 | "vue-jest": "^1.0.2", 58 | "vue-loader": "^13.3.0", 59 | "vue-style-loader": "^3.0.1", 60 | "vue-template-compiler": "^2.5.2", 61 | "webpack": "^3.6.0", 62 | "webpack-bundle-analyzer": "^2.9.0", 63 | "webpack-dev-server": "^2.9.1", 64 | "webpack-merge": "^4.1.0" 65 | }, 66 | "engines": { 67 | "node": ">= 6.0.0", 68 | "npm": ">= 3.0.0" 69 | }, 70 | "browserslist": [ 71 | "> 1%", 72 | "last 2 versions", 73 | "not ie <= 8" 74 | ] 75 | } 76 | -------------------------------------------------------------------------------- /admin/src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 85 | 86 | 96 | 97 | 98 | 114 | -------------------------------------------------------------------------------- /admin/src/service/fetch.js: -------------------------------------------------------------------------------- 1 | import { 2 | baseUrl 3 | } from './env' 4 | import Vue from 'vue' 5 | import router from '@/router/index'; 6 | 7 | export default async(url = '', data = {}, type = 'POST', token = 'token', method = 'promise') => { 8 | type = type.toUpperCase(); 9 | url = baseUrl + url; 10 | 11 | 12 | let dataStr = ''; //数据拼接字符串,参数要: a=1&b=2。不能{a:1,b:2}的方式,所以要对data做处理 13 | Object.keys(data).forEach(key => { 14 | dataStr += key + '=' + data[key] + '&'; 15 | }) 16 | dataStr = dataStr.substr(0, dataStr.lastIndexOf('&')); 17 | 18 | if (type == 'GET') { 19 | if (dataStr !== '') { 20 | url = url + '?' + dataStr; 21 | } 22 | } 23 | 24 | if (window.fetch && method == 'fetch') { 25 | 26 | 27 | let requestConfig = { 28 | credentials: true, 29 | method: type, 30 | headers: { 31 | 'Accept': 'application/json', 32 | 'Content-type': 'application/x-www-form-urlencoded' 33 | }, 34 | mode: "cors", 35 | cache: "force-cache" 36 | } 37 | 38 | if (type == 'POST') { 39 | Object.defineProperty(requestConfig, 'body', { 40 | value: dataStr 41 | }) 42 | } 43 | 44 | try { 45 | const response = await fetch(url, requestConfig); 46 | const responseJson = await response.json(); 47 | return responseJson 48 | } catch (error) { 49 | throw new Error(error) 50 | } 51 | } else { 52 | return new Promise((resolve, reject) => { 53 | let requestObj; 54 | if (window.XMLHttpRequest) { 55 | requestObj = new XMLHttpRequest(); 56 | } else { 57 | requestObj = new ActiveXObject; 58 | } 59 | 60 | let sendData = ''; 61 | if (type == 'POST') { 62 | sendData = dataStr; 63 | } 64 | 65 | requestObj.open(type, url, true); 66 | requestObj.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); 67 | if(token == 'token'){ 68 | requestObj.setRequestHeader("token", localStorage.getItem('token') || ''); 69 | } 70 | 71 | requestObj.send(sendData); 72 | 73 | requestObj.onreadystatechange = () => { 74 | if (requestObj.readyState == 4) { 75 | if (requestObj.status == 200) { 76 | let obj = requestObj.response 77 | if(obj.status == -3){ 78 | router.push({path: "/login"}); 79 | return; 80 | } 81 | if (typeof obj !== 'object') { 82 | try{ 83 | obj = JSON.parse(obj); 84 | }catch(e){ 85 | console.log(e); 86 | } 87 | 88 | } 89 | resolve(obj) 90 | } else { 91 | reject(requestObj) 92 | } 93 | } 94 | } 95 | }) 96 | } 97 | } -------------------------------------------------------------------------------- /server/app/controller/auth.js: -------------------------------------------------------------------------------- 1 | const Base = require('./base'); 2 | const Util = require('../../utils/util'); 3 | const util = new Util(); 4 | 5 | class AuthController extends Base { 6 | constructor(props) { 7 | super(props); 8 | this.token =new this.ctx.helper.Token(this.ctx); 9 | } 10 | async loginByWeixinAction() { 11 | let token = new this.ctx.helper.Token(this.ctx); 12 | const ctx = this.ctx; 13 | const code = ctx.request.body.code; 14 | const fullUserInfo = ctx.request.body.userInfo; 15 | const userInfo = fullUserInfo.userInfo; 16 | 17 | // 获取openid 18 | const options = { 19 | method: 'GET', 20 | contentType: 'json', 21 | data: { 22 | grant_type: 'authorization_code', 23 | js_code: code, 24 | secret: this.config.wx.secret, 25 | appid: this.config.wx.appid 26 | }, 27 | dataType: 'json' 28 | }; 29 | 30 | const sessionData = await ctx.curl('https://api.weixin.qq.com/sns/jscode2session',options); 31 | if (!sessionData.data.openid) { 32 | return this.fail('登录失败1'); 33 | } 34 | // 验证用户信息完整性(数字签名验证) 35 | const crypto = require('crypto'); 36 | const sha1 = crypto.createHash('sha1').update(fullUserInfo.rawData + sessionData.data.session_key).digest('hex'); 37 | if (fullUserInfo.signature !== sha1) { 38 | return this.fail('登录失败2'); 39 | } 40 | //根据openid查找用户是否已经注册 41 | let userMsg = await ctx.service.user.find({openid: sessionData.data.openid}); 42 | if(ctx.helper.isEmpty(userMsg)){ 43 | let result = await ctx.service.user.insert({ 44 | username: userInfo.nickName, 45 | register_time: parseInt(new Date().getTime() / 1000), 46 | last_login_time: parseInt(new Date().getTime() / 1000), 47 | openid: sessionData.data.openid, 48 | avatar: userInfo.avatarUrl || '', 49 | gender: userInfo.gender || 1, // 性别 0:未知、1:男、2:女 50 | }); 51 | 52 | if(result.affectedRows === 1){ 53 | console.log("插入成功"); 54 | } 55 | userMsg = {}; 56 | userMsg.id = result.insertId; 57 | } 58 | 59 | sessionData.data.user_id = userMsg.id; 60 | // 查询用户信息 61 | const resultNewUser = await ctx.service.user.find({ 62 | id: userMsg.id 63 | }); 64 | 65 | const newUserInfo = { 66 | avatar: resultNewUser.avatar, 67 | username: resultNewUser.username, 68 | undergraduate: resultNewUser.undergraduate, 69 | master_school: resultNewUser.master_school, 70 | wechat_id: resultNewUser.wechat_id, 71 | tel_id: resultNewUser.tel_id, 72 | introduce: resultNewUser.introduce 73 | } 74 | 75 | // 更新登录信息 76 | const resultUpdata = await ctx.service.user.update({ 77 | id: userMsg.id, 78 | last_login_time: parseInt(new Date().getTime() / 1000) 79 | }); 80 | 81 | if(resultUpdata.affectedRows === 1){ 82 | console.log("更新成功"); 83 | } 84 | 85 | // 生成 Token 返回 客户端 86 | const sessionKey = await this.token.create(sessionData); 87 | 88 | return this.success({ token:sessionKey, userInfo: newUserInfo }); 89 | 90 | } 91 | } 92 | 93 | module.exports = AuthController; -------------------------------------------------------------------------------- /admin/src/pages/usermanage/userinfo/index.vue: -------------------------------------------------------------------------------- 1 | 59 | 63 | -------------------------------------------------------------------------------- /admin/build/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const config = require('../config') 4 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 5 | const packageConfig = require('../package.json') 6 | 7 | exports.assetsPath = function (_path) { 8 | const assetsSubDirectory = process.env.NODE_ENV === 'production' 9 | ? config.build.assetsSubDirectory 10 | : config.dev.assetsSubDirectory 11 | 12 | return path.posix.join(assetsSubDirectory, _path) 13 | } 14 | 15 | exports.cssLoaders = function (options) { 16 | options = options || {} 17 | 18 | const cssLoader = { 19 | loader: 'css-loader', 20 | options: { 21 | sourceMap: options.sourceMap 22 | } 23 | } 24 | 25 | const postcssLoader = { 26 | loader: 'postcss-loader', 27 | options: { 28 | sourceMap: options.sourceMap 29 | } 30 | } 31 | 32 | // generate loader string to be used with extract text plugin 33 | function generateLoaders (loader, loaderOptions) { 34 | const loaders = options.usePostCSS ? [cssLoader, postcssLoader] : [cssLoader] 35 | 36 | if (loader) { 37 | loaders.push({ 38 | loader: loader + '-loader', 39 | options: Object.assign({}, loaderOptions, { 40 | sourceMap: options.sourceMap 41 | }) 42 | }) 43 | } 44 | 45 | // Extract CSS when that option is specified 46 | // (which is the case during production build) 47 | if (options.extract) { 48 | return ExtractTextPlugin.extract({ 49 | use: loaders, 50 | fallback: 'vue-style-loader' 51 | }) 52 | } else { 53 | return ['vue-style-loader'].concat(loaders) 54 | } 55 | } 56 | 57 | // https://vue-loader.vuejs.org/en/configurations/extract-css.html 58 | return { 59 | css: generateLoaders(), 60 | postcss: generateLoaders(), 61 | less: generateLoaders('less'), 62 | sass: generateLoaders('sass', { indentedSyntax: true }), 63 | scss: generateLoaders('sass'), 64 | stylus: generateLoaders('stylus'), 65 | styl: generateLoaders('stylus') 66 | } 67 | } 68 | 69 | // Generate loaders for standalone style files (outside of .vue) 70 | exports.styleLoaders = function (options) { 71 | const output = [] 72 | const loaders = exports.cssLoaders(options) 73 | 74 | for (const extension in loaders) { 75 | const loader = loaders[extension] 76 | output.push({ 77 | test: new RegExp('\\.' + extension + '$'), 78 | use: loader 79 | }) 80 | } 81 | 82 | return output 83 | } 84 | 85 | exports.createNotifierCallback = () => { 86 | const notifier = require('node-notifier') 87 | 88 | return (severity, errors) => { 89 | if (severity !== 'error') return 90 | 91 | const error = errors[0] 92 | const filename = error.file && error.file.split('!').pop() 93 | 94 | notifier.notify({ 95 | title: packageConfig.name, 96 | message: severity + ': ' + error.name, 97 | subtitle: filename || '', 98 | icon: path.join(__dirname, 'logo.png') 99 | }) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /admin/src/pages/usermanage/enchashment/index.vue: -------------------------------------------------------------------------------- 1 | 71 | 75 | -------------------------------------------------------------------------------- /admin/src/pages/qualitymanage/review/index.vue: -------------------------------------------------------------------------------- 1 | 71 | 75 | -------------------------------------------------------------------------------- /admin/build/webpack.dev.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const utils = require('./utils') 3 | const webpack = require('webpack') 4 | const config = require('../config') 5 | const merge = require('webpack-merge') 6 | const path = require('path') 7 | const baseWebpackConfig = require('./webpack.base.conf') 8 | const CopyWebpackPlugin = require('copy-webpack-plugin') 9 | const HtmlWebpackPlugin = require('html-webpack-plugin') 10 | const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') 11 | const portfinder = require('portfinder') 12 | 13 | const HOST = process.env.HOST 14 | const PORT = process.env.PORT && Number(process.env.PORT) 15 | 16 | const devWebpackConfig = merge(baseWebpackConfig, { 17 | module: { 18 | rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true }) 19 | }, 20 | // cheap-module-eval-source-map is faster for development 21 | devtool: config.dev.devtool, 22 | 23 | // these devServer options should be customized in /config/index.js 24 | devServer: { 25 | clientLogLevel: 'warning', 26 | historyApiFallback: { 27 | rewrites: [ 28 | { from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') }, 29 | ], 30 | }, 31 | hot: true, 32 | contentBase: false, // since we use CopyWebpackPlugin. 33 | compress: true, 34 | host: HOST || config.dev.host, 35 | port: PORT || config.dev.port, 36 | open: config.dev.autoOpenBrowser, 37 | overlay: config.dev.errorOverlay 38 | ? { warnings: false, errors: true } 39 | : false, 40 | publicPath: config.dev.assetsPublicPath, 41 | proxy: config.dev.proxyTable, 42 | quiet: true, // necessary for FriendlyErrorsPlugin 43 | watchOptions: { 44 | poll: config.dev.poll, 45 | } 46 | }, 47 | plugins: [ 48 | new webpack.DefinePlugin({ 49 | 'process.env': require('../config/dev.env') 50 | }), 51 | new webpack.HotModuleReplacementPlugin(), 52 | new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update. 53 | new webpack.NoEmitOnErrorsPlugin(), 54 | // https://github.com/ampedandwired/html-webpack-plugin 55 | new HtmlWebpackPlugin({ 56 | filename: 'index.html', 57 | template: 'index.html', 58 | inject: true 59 | }), 60 | // copy custom static assets 61 | new CopyWebpackPlugin([ 62 | { 63 | from: path.resolve(__dirname, '../static'), 64 | to: config.dev.assetsSubDirectory, 65 | ignore: ['.*'] 66 | } 67 | ]) 68 | ] 69 | }) 70 | 71 | module.exports = new Promise((resolve, reject) => { 72 | portfinder.basePort = process.env.PORT || config.dev.port 73 | portfinder.getPort((err, port) => { 74 | if (err) { 75 | reject(err) 76 | } else { 77 | // publish the new Port, necessary for e2e tests 78 | process.env.PORT = port 79 | // add port to devServer config 80 | devWebpackConfig.devServer.port = port 81 | 82 | // Add FriendlyErrorsPlugin 83 | devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({ 84 | compilationSuccessInfo: { 85 | messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`], 86 | }, 87 | onErrors: config.dev.notifyOnErrors 88 | ? utils.createNotifierCallback() 89 | : undefined 90 | })) 91 | 92 | resolve(devWebpackConfig) 93 | } 94 | }) 95 | }) 96 | -------------------------------------------------------------------------------- /client/pages/setting/index.js: -------------------------------------------------------------------------------- 1 | const util = require('../../utils/util.js'); 2 | const app=getApp() 3 | 4 | Page({ 5 | data:{ 6 | userInp:'', 7 | collegesInp:'', 8 | graduateInp:'', 9 | weiChatInp:'', 10 | mobileInp:'', 11 | introInp:'' 12 | }, 13 | onLoad:function(){ 14 | this.setData({ 15 | userInp: app.globalData.userInfo.username, 16 | collegesInp: app.globalData.userInfo.undergraduate, 17 | graduateInp: app.globalData.userInfo.master_school, 18 | introInp: app.globalData.userInfo.introduce, 19 | weiChatInp: app.globalData.userInfo.wechat_id, 20 | mobileInp: app.globalData.userInfo.tel_id 21 | }); 22 | }, 23 | userInp(e){ 24 | this.setData({ 25 | userInp:e.detail.value 26 | }) 27 | }, 28 | myAvatar(e){ 29 | this.setData({ 30 | myAvatar:e.detail.value 31 | }) 32 | }, 33 | collegesInp(e){ 34 | this.setData({ 35 | collegesInp:e.detail.value 36 | }) 37 | }, 38 | graduateInp(e){ 39 | this.setData({ 40 | graduateInp:e.detail.value 41 | }) 42 | }, 43 | introInp(e) { 44 | this.setData({ 45 | introInp: e.detail.value 46 | }) 47 | }, 48 | weiChatInp(e){ 49 | this.setData({ 50 | weiChatInp:e.detail.value 51 | }) 52 | }, 53 | mobileInp(e){ 54 | this.setData({ 55 | mobileInp:e.detail.value 56 | }) 57 | }, 58 | loginBtnClick(){ 59 | const that = this; 60 | let param = {}; 61 | if (this.data.userInp == ''){ 62 | wx.showToast({ 63 | title: '用户名不能为空', 64 | icon: 'none' 65 | }); 66 | return; 67 | } 68 | if (this.data.collegesInp == ''){ 69 | wx.showToast({ 70 | title: '本科院校不能为空', 71 | icon: 'none' 72 | }); 73 | return; 74 | } 75 | if (this.data.introInp == '') { 76 | wx.showToast({ 77 | title: '个人简介不能为空', 78 | icon: 'none' 79 | }); 80 | return; 81 | } 82 | if (this.data.weiChatInp == ''){ 83 | wx.showToast({ 84 | title: '微信号不能为空', 85 | icon: 'none' 86 | }); 87 | return; 88 | } 89 | 90 | if (this.data.mobileInp == ''){ 91 | wx.showToast({ 92 | title: '手机号不能为空', 93 | icon: 'none' 94 | }); 95 | return; 96 | } 97 | param.username = this.data.userInp; 98 | param.undergraduate = this.data.collegesInp; 99 | param.master_school = this.data.graduateInp; 100 | param.introduce = this.data.introInp; 101 | param.wechat_id = this.data.weiChatInp; 102 | param.tel_id = this.data.mobileInp; 103 | util.request('user/setInfo', param).then(res => { 104 | if (res.status === 0) { 105 | wx.showToast({ 106 | title: res.result, 107 | icon: 'none', 108 | duration: 2000, 109 | complete: function(){ 110 | setTimeout(function(){ 111 | wx.navigateBack(); 112 | },2000); 113 | } 114 | }); 115 | Object.assign(app.globalData.userInfo, param); 116 | wx.setStorageSync("userInfo", app.globalData.userInfo); 117 | } 118 | }); 119 | }, 120 | }) -------------------------------------------------------------------------------- /client/components/tip/index.wxss: -------------------------------------------------------------------------------- 1 | /* components/tip/index.wxss */ 2 | .ftd-tip{ 3 | position: fixed; 4 | right: 20rpx; 5 | transform: translateX(100%); 6 | background: #D73316; 7 | opacity: .3; 8 | border-bottom-left-radius: 8rpx; 9 | border-top-left-radius: 8rpx; 10 | padding: 15rpx 20rpx 15rpx 20rpx; 11 | color: #fff; 12 | font-size: 24rpx; 13 | z-index: 9999; 14 | } 15 | 16 | .animated { 17 | -webkit-animation-duration: 1s; 18 | animation-duration: 1s; 19 | -webkit-animation-fill-mode: both; 20 | animation-fill-mode: both; 21 | } 22 | 23 | .animated.infinite { 24 | -webkit-animation-iteration-count: infinite; 25 | animation-iteration-count: infinite; 26 | } 27 | 28 | .animated.flip { 29 | -webkit-backface-visibility: visible; 30 | backface-visibility: visible; 31 | -webkit-animation-name: flip; 32 | animation-name: flip; 33 | } 34 | 35 | @-webkit-keyframes bounceInRight { 36 | from, 37 | 60%, 38 | 75%, 39 | 90%, 40 | to { 41 | -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); 42 | animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); 43 | } 44 | 45 | from { 46 | opacity: 0; 47 | -webkit-transform: translate3d(3000px, 0, 0); 48 | transform: translate3d(3000px, 0, 0); 49 | } 50 | 51 | 60% { 52 | opacity: .8; 53 | -webkit-transform: translate3d(-25px, 0, 0); 54 | transform: translate3d(-25px, 0, 0); 55 | } 56 | 57 | 75% { 58 | -webkit-transform: translate3d(10px, 0, 0); 59 | transform: translate3d(10px, 0, 0); 60 | } 61 | 62 | 90% { 63 | -webkit-transform: translate3d(-5px, 0, 0); 64 | transform: translate3d(-5px, 0, 0); 65 | } 66 | 67 | to { 68 | opacity: 1; 69 | right: -5rpx; 70 | -webkit-transform: translate3d(0, 0, 0); 71 | transform: translate3d(0, 0, 0); 72 | } 73 | } 74 | 75 | @keyframes bounceInRight { 76 | from, 77 | 60%, 78 | 75%, 79 | 90%, 80 | to { 81 | -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); 82 | animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); 83 | } 84 | 85 | from { 86 | opacity: 0; 87 | -webkit-transform: translate3d(3000px, 0, 0); 88 | transform: translate3d(3000px, 0, 0); 89 | } 90 | 91 | 60% { 92 | opacity: .8; 93 | -webkit-transform: translate3d(-25px, 0, 0); 94 | transform: translate3d(-25px, 0, 0); 95 | } 96 | 97 | 75% { 98 | -webkit-transform: translate3d(10px, 0, 0); 99 | transform: translate3d(10px, 0, 0); 100 | } 101 | 102 | 90% { 103 | -webkit-transform: translate3d(-5px, 0, 0); 104 | transform: translate3d(-5px, 0, 0); 105 | } 106 | 107 | to { 108 | opacity: 1; 109 | right: -5rpx; 110 | -webkit-transform: translate3d(0, 0, 0); 111 | transform: translate3d(0, 0, 0); 112 | } 113 | } 114 | 115 | .bounceInRight { 116 | -webkit-animation-name: bounceInRight; 117 | animation-name: bounceInRight; 118 | } 119 | 120 | @-webkit-keyframes fadeInLeft { 121 | from { 122 | opacity: 1; 123 | -webkit-transform: translate3d(0, 0, 0); 124 | transform: translate3d(0, 0, 0); 125 | } 126 | 127 | to { 128 | opacity: .3; 129 | -webkit-transform: translate3d(100%, 0, 0); 130 | transform: translate3d(100%, 0, 0); 131 | } 132 | } 133 | 134 | @keyframes fadeInLeft { 135 | from { 136 | opacity: 1; 137 | right: -10rpx; 138 | -webkit-transform: translate3d(0, 0, 0); 139 | transform: translate3d(0, 0, 0); 140 | } 141 | 142 | to { 143 | opacity: .3; 144 | -webkit-transform: translate3d(100%, 0, 0); 145 | transform: translate3d(100%, 0, 0); 146 | } 147 | } 148 | 149 | .fadeInLeft { 150 | -webkit-animation-name: fadeInLeft; 151 | animation-name: fadeInLeft; 152 | } -------------------------------------------------------------------------------- /server/app/service/course.js: -------------------------------------------------------------------------------- 1 | const Service = require('egg').Service; 2 | 3 | class CourseService extends Service { 4 | async insert(data){ 5 | const result = await this.app.mysql.insert('course', data); 6 | return result; 7 | } 8 | 9 | async update(courseID, myCourse, title){ 10 | const {ctx, app} = this; 11 | const result = await app.mysql.update('course', { 12 | id: courseID, 13 | my_course: myCourse, 14 | title: title 15 | }); 16 | return result; 17 | } 18 | 19 | async selectByUID(page, size){ 20 | const result = await this.app.mysql.select('course', { 21 | where:{user_id: this.app.userId}, 22 | columns: ['id', 'title', 'publish'], 23 | limit: size, 24 | offset: (page-1)*size 25 | }); 26 | return result; 27 | } 28 | 29 | async selectByID(id){ 30 | const {ctx,app} = this; 31 | const result = await app.mysql.get('course', { 32 | id: id 33 | }); 34 | return result; 35 | } 36 | 37 | async getByID(id){ 38 | const {ctx,app} = this; 39 | const result = await app.mysql.query('select c.user_id,c.title,c.my_course,c.price,u.username,u.avatar,u.undergraduate,u.master_school from course c left join user u on c.user_id=u.id where c.id='+ id); 40 | let tmp = {}; 41 | if(result.length >= 1){ 42 | tmp = result[0]; 43 | } 44 | 45 | // 自己写的课程和价格为0的课程直接公开 46 | if(tmp && tmp.user_id && tmp.user_id == app.userId || tmp.price == 0){ 47 | tmp.isPay = true; 48 | // 自己写的文章展示编辑 49 | if(tmp.user_id == app.userId){ 50 | tmp.showEdit = true; 51 | }else{ 52 | tmp.showEdit = false; 53 | } 54 | return tmp; 55 | } 56 | const resultPurchase = await app.mysql.get('purchase',{ 57 | user_id: app.userId, 58 | course_id: id 59 | }); 60 | tmp.showEdit = false; 61 | if(resultPurchase){ 62 | tmp.isPay = true; 63 | }else{ 64 | // 未购买课程只公开一半 65 | tmp.my_course = ctx.helper.halfArr(tmp.my_course); 66 | tmp.isPay = false; 67 | } 68 | return tmp; 69 | } 70 | 71 | async selectCourseWithCollect(page, size){ 72 | const {ctx, app} = this; 73 | const collect = await ctx.service.collect.listMyCollect(); 74 | let allCourse = await app.mysql.select('course',{ 75 | where:{publish: 1}, 76 | columns: ['id', 'title', 'user_id'], 77 | limit: size, 78 | offset: (page - 1)*size 79 | }); 80 | for(let i = 0; i < allCourse.length; i++){ 81 | allCourse[i].isCollect = 0; // 未收藏 82 | let length = await ctx.service.purchase.getPayCount(allCourse[i].id); 83 | allCourse[i].payCount = length; 84 | for(let j = 0; j < collect.length; j++){ 85 | if(collect[j].course_id == allCourse[i].id){ 86 | allCourse[i].isCollect = 1; // 已收藏 87 | } 88 | } 89 | let result = await ctx.service.user.find({id:allCourse[i].user_id}); 90 | allCourse[i].headImg = result.avatar; 91 | allCourse[i].undergraduate = result.username+"/"+result.undergraduate; 92 | } 93 | return allCourse; 94 | } 95 | 96 | async selectCourseWithTitle(title, page, size){ 97 | const {ctx, app} = this; 98 | const collect = await ctx.service.collect.listMyCollect(); 99 | let allCourse = await app.mysql.query('select id,title,user_id from course where title like "%'+title+'%" AND publish=1 LIMIT '+(page-1)*size+','+size); 100 | for(let i = 0; i < allCourse.length; i++){ 101 | allCourse[i].isCollect = 0; // 未收藏 102 | let length = await ctx.service.purchase.getPayCount(allCourse[i].id); 103 | allCourse[i].payCount = length; 104 | for(let j = 0; j < collect.length; j++){ 105 | if(collect[j].course_id == allCourse[i].id){ 106 | allCourse[i].isCollect = 1; // 已收藏 107 | } 108 | } 109 | let result = await ctx.service.user.find({id:allCourse[i].user_id}); 110 | allCourse[i].headImg = result.avatar; 111 | allCourse[i].undergraduate = result.username+"/"+result.undergraduate; 112 | } 113 | return allCourse; 114 | } 115 | } 116 | 117 | module.exports = CourseService; -------------------------------------------------------------------------------- /client/utils/util.js: -------------------------------------------------------------------------------- 1 | var api = require('../service/api.js'); 2 | 3 | const formatTime = date => { 4 | const year = date.getFullYear() 5 | const month = date.getMonth() + 1 6 | const day = date.getDate() 7 | const hour = date.getHours() 8 | const minute = date.getMinutes() 9 | const second = date.getSeconds() 10 | 11 | return [year, month, day].map(formatNumber).join('-') + ' ' + [hour, minute, second].map(formatNumber).join(':') 12 | } 13 | 14 | const formatNumber = n => { 15 | n = n.toString() 16 | return n[1] ? n : '0' + n 17 | } 18 | 19 | /** 20 | * 调用微信登录 21 | */ 22 | const login = ()=> { 23 | return new Promise(function (resolve, reject) { 24 | wx.login({ 25 | success: function (res) { 26 | if (res.code) { 27 | //登录远程服务器 28 | console.log(res) 29 | resolve(res); 30 | } else { 31 | reject(res); 32 | } 33 | }, 34 | fail: function (err) { 35 | reject(err); 36 | } 37 | }); 38 | }); 39 | } 40 | 41 | const getUserInfo = ()=>{ 42 | return new Promise(function (resolve, reject) { 43 | wx.getSetting({ 44 | success: function (res) { 45 | if (res.authSetting['scope.userInfo']) { 46 | // 已经授权,可以直接调用 getUserInfo 获取头像昵称 47 | wx.getUserInfo({ 48 | withCredentials: true, 49 | success: function (res) { 50 | console.log(res) 51 | resolve(res); 52 | }, 53 | fail: function (err) { 54 | reject(err); 55 | } 56 | }) 57 | }else{ 58 | wx.redirectTo({ 59 | url: '/pages/login/index', 60 | }) 61 | } 62 | } 63 | }); 64 | 65 | }); 66 | } 67 | 68 | /** 69 | * 检查微信会话是否过期 70 | */ 71 | const checkSession = ()=> { 72 | return new Promise(function (resolve, reject) { 73 | wx.checkSession({ 74 | success: function () { 75 | resolve(true); 76 | }, 77 | fail: function () { 78 | reject(false); 79 | } 80 | }) 81 | }); 82 | } 83 | 84 | 85 | /** 86 | * 封封微信的的request 87 | */ 88 | const request = (url, data = {}, method = "POST")=> { 89 | return new Promise(function (resolve, reject) { 90 | wx.request({ 91 | url: api.baseURL+url, 92 | data: data, 93 | method: method, 94 | header: { 95 | 'Content-Type': 'application/json', 96 | 'X-Booklist-Token': wx.getStorageSync('token') 97 | }, 98 | success: function (res) { 99 | if (res.statusCode == 200) { 100 | if (res.data.status == -3) { 101 | //需要登录后才可以操作 102 | let code = null; 103 | return login().then((res) => { 104 | code = res.code; 105 | return getUserInfo(); 106 | }).then((userInfo) => { 107 | //登录远程服务器 108 | request(api.AuthLoginByWeixin, { code: code, userInfo: userInfo }, 'POST').then(res => { 109 | if (res.errno === 0) { 110 | //存储用户信息 111 | wx.setStorageSync('userInfo', res.data.userInfo); 112 | wx.setStorageSync('token', res.data.token); 113 | 114 | resolve(res); 115 | } else { 116 | reject(res); 117 | } 118 | }).catch((err) => { 119 | reject(err); 120 | }); 121 | }).catch((err) => { 122 | reject(err); 123 | }) 124 | } else { 125 | resolve(res.data); 126 | } 127 | } else { 128 | reject(res.errMsg); 129 | } 130 | }, 131 | fail: function (err) { 132 | reject(err) 133 | console.log("failed") 134 | } 135 | }) 136 | }); 137 | } 138 | 139 | const leaveLastStr = (str) => { 140 | return str.slice(0, str.length - 1); 141 | } 142 | 143 | const splitStr = (str) =>{ 144 | return str.split(","); 145 | } 146 | 147 | const changeArr = (arr, source, target) => { 148 | const reg = new RegExp(source, "g"); 149 | let n = JSON.parse(JSON.stringify(arr).replace(reg, target)); 150 | return n; 151 | } 152 | 153 | module.exports = { 154 | login, 155 | getUserInfo, 156 | checkSession, 157 | request, 158 | leaveLastStr, 159 | splitStr, 160 | formatTime, 161 | changeArr 162 | } 163 | -------------------------------------------------------------------------------- /client/pages/editcourse/index.js: -------------------------------------------------------------------------------- 1 | const util = require("../../utils/util.js"); 2 | const app=getApp() 3 | 4 | Page({ 5 | data:{ 6 | titleInp:'', 7 | nameInp:'', 8 | authorInp:'', 9 | courseList:[], 10 | author:'', 11 | allCourse: '', 12 | isAdd: true, 13 | courseID: '', 14 | type: false 15 | }, 16 | onLoad(options){ 17 | if(options.myCourse && options.title){ 18 | let resultList = util.splitStr(options.myCourse); 19 | let arr = []; 20 | for (let index in resultList) { 21 | arr.push({ 22 | name: resultList[index] 23 | }) 24 | } 25 | this.setData({ 26 | courseList: arr, 27 | titleInp: options.title, 28 | isAdd: false, 29 | courseID: options.courseID 30 | }); 31 | 32 | this.setData({ 33 | allCourse: this.regroup() 34 | }); 35 | } 36 | 37 | this.goSetting(); 38 | }, 39 | goSetting: function () { 40 | if (!app.globalData.userInfo.undergraduate || !app.globalData.userInfo.username || !app.globalData.userInfo.wechat_id || !app.globalData.userInfo.tel_id) { 41 | wx.showToast({ 42 | title: '先完善个人信息', 43 | icon: 'none', 44 | duration: 2000, 45 | complete:function(){ 46 | setTimeout(function(){ 47 | wx.redirectTo({ 48 | url: '/pages/setting/index', 49 | }); 50 | },2000); 51 | } 52 | }) 53 | 54 | } 55 | }, 56 | titleInp(e){ 57 | this.setData({ 58 | titleInp:e.detail.value 59 | }) 60 | }, 61 | nameInp(e){ 62 | this.setData({ 63 | nameInp:e.detail.value 64 | }) 65 | }, 66 | authorInp(e){ 67 | this.setData({ 68 | authorInp:e.detail.value 69 | }) 70 | }, 71 | addBtnClick(){ 72 | var list=this.data.courseList; 73 | if (this.data.nameInp == '' || this.data.authorInp == ''){ 74 | wx.showToast({ 75 | title: '书单信息不能为空', 76 | icon: 'none' 77 | }); 78 | return; 79 | } 80 | list.push({name: "《"+this.data.nameInp+"》"+' '+this.data.authorInp+"著"}); 81 | this.setData({ 82 | courseList:list 83 | }); 84 | this.setData({ 85 | allCourse: this.regroup(), 86 | nameInp: '', 87 | authorInp:'' 88 | }) 89 | }, 90 | regroup(){ 91 | var list = this.data.courseList; 92 | var concatStr = ''; 93 | for (var i = 0; i < list.length; i++) { 94 | concatStr += list[i].name + ','; 95 | } 96 | return util.leaveLastStr(concatStr); 97 | }, 98 | deleteCourse(e){ 99 | this.data.courseList.splice(e.target.dataset.itemIndex, 1); 100 | this.setData({ 101 | courseList: this.data.courseList, 102 | allCourse: this.regroup() 103 | }); 104 | }, 105 | submitHandle(){ 106 | const that = this; 107 | if (this.data.courseList.length <= 0) 108 | return; 109 | if (this.data.titleInp == "") { 110 | wx.showToast({ 111 | title: '专业名不能为空', 112 | icon: 'none' 113 | }) 114 | return; 115 | } 116 | if (this.data.allCourse == "") { 117 | wx.showToast({ 118 | title: '书单信息不能为空', 119 | icon: 'none' 120 | }) 121 | return; 122 | } 123 | 124 | if (this.data.isAdd) { 125 | // 添加 126 | util.request("course/addCourse", 127 | { 128 | title: this.data.titleInp, 129 | myCourse: this.data.allCourse 130 | }, 'POST').then(function (res) { 131 | if (res.status === 0) { 132 | wx.showToast({ 133 | title: '添加成功', 134 | icon: 'none', 135 | duration: 2000, 136 | complete: function () { 137 | setTimeout(function(){ 138 | wx.redirectTo({ 139 | url: '../../pages/myblist/index', 140 | }); 141 | },2000); 142 | } 143 | }) 144 | } 145 | }); 146 | } else { 147 | // 更新 148 | util.request("course/update", { 149 | courseID: this.data.courseID, 150 | title: this.data.titleInp, 151 | myCourse: this.data.allCourse 152 | }).then(res => { 153 | wx.showToast({ 154 | title: res.result, 155 | icon: 'none', 156 | duration: 2000, 157 | complete: function () { 158 | setTimeout(function () { 159 | wx.redirectTo({ 160 | url: '../../pages/details/index?courseID=' + that.data.courseID + '&title=' + that.data.titleInp, 161 | }); 162 | }, 2000); 163 | 164 | } 165 | }); 166 | }); 167 | } 168 | } 169 | }) -------------------------------------------------------------------------------- /admin/build/webpack.prod.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const utils = require('./utils') 4 | const webpack = require('webpack') 5 | const config = require('../config') 6 | const merge = require('webpack-merge') 7 | const baseWebpackConfig = require('./webpack.base.conf') 8 | const CopyWebpackPlugin = require('copy-webpack-plugin') 9 | const HtmlWebpackPlugin = require('html-webpack-plugin') 10 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 11 | const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin') 12 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin') 13 | 14 | const env = process.env.NODE_ENV === 'testing' 15 | ? require('../config/test.env') 16 | : require('../config/prod.env') 17 | 18 | const webpackConfig = merge(baseWebpackConfig, { 19 | module: { 20 | rules: utils.styleLoaders({ 21 | sourceMap: config.build.productionSourceMap, 22 | extract: true, 23 | usePostCSS: true 24 | }) 25 | }, 26 | devtool: config.build.productionSourceMap ? config.build.devtool : false, 27 | output: { 28 | path: config.build.assetsRoot, 29 | filename: utils.assetsPath('js/[name].[chunkhash].js'), 30 | chunkFilename: utils.assetsPath('js/[id].[chunkhash].js') 31 | }, 32 | plugins: [ 33 | // http://vuejs.github.io/vue-loader/en/workflow/production.html 34 | new webpack.DefinePlugin({ 35 | 'process.env': env 36 | }), 37 | new UglifyJsPlugin({ 38 | uglifyOptions: { 39 | compress: { 40 | warnings: false 41 | } 42 | }, 43 | sourceMap: config.build.productionSourceMap, 44 | parallel: true 45 | }), 46 | // extract css into its own file 47 | new ExtractTextPlugin({ 48 | filename: utils.assetsPath('css/[name].[contenthash].css'), 49 | // Setting the following option to `false` will not extract CSS from codesplit chunks. 50 | // Their CSS will instead be inserted dynamically with style-loader when the codesplit chunk has been loaded by webpack. 51 | // It's currently set to `true` because we are seeing that sourcemaps are included in the codesplit bundle as well when it's `false`, 52 | // increasing file size: https://github.com/vuejs-templates/webpack/issues/1110 53 | allChunks: true, 54 | }), 55 | // Compress extracted CSS. We are using this plugin so that possible 56 | // duplicated CSS from different components can be deduped. 57 | new OptimizeCSSPlugin({ 58 | cssProcessorOptions: config.build.productionSourceMap 59 | ? { safe: true, map: { inline: false } } 60 | : { safe: true } 61 | }), 62 | // generate dist index.html with correct asset hash for caching. 63 | // you can customize output by editing /index.html 64 | // see https://github.com/ampedandwired/html-webpack-plugin 65 | new HtmlWebpackPlugin({ 66 | filename: process.env.NODE_ENV === 'testing' 67 | ? 'index.html' 68 | : config.build.index, 69 | template: 'index.html', 70 | inject: true, 71 | minify: { 72 | removeComments: true, 73 | collapseWhitespace: true, 74 | removeAttributeQuotes: true 75 | // more options: 76 | // https://github.com/kangax/html-minifier#options-quick-reference 77 | }, 78 | // necessary to consistently work with multiple chunks via CommonsChunkPlugin 79 | chunksSortMode: 'dependency' 80 | }), 81 | // keep module.id stable when vendor modules does not change 82 | new webpack.HashedModuleIdsPlugin(), 83 | // enable scope hoisting 84 | new webpack.optimize.ModuleConcatenationPlugin(), 85 | // split vendor js into its own file 86 | new webpack.optimize.CommonsChunkPlugin({ 87 | name: 'vendor', 88 | minChunks (module) { 89 | // any required modules inside node_modules are extracted to vendor 90 | return ( 91 | module.resource && 92 | /\.js$/.test(module.resource) && 93 | module.resource.indexOf( 94 | path.join(__dirname, '../node_modules') 95 | ) === 0 96 | ) 97 | } 98 | }), 99 | // extract webpack runtime and module manifest to its own file in order to 100 | // prevent vendor hash from being updated whenever app bundle is updated 101 | new webpack.optimize.CommonsChunkPlugin({ 102 | name: 'manifest', 103 | minChunks: Infinity 104 | }), 105 | // This instance extracts shared chunks from code splitted chunks and bundles them 106 | // in a separate chunk, similar to the vendor chunk 107 | // see: https://webpack.js.org/plugins/commons-chunk-plugin/#extra-async-commons-chunk 108 | new webpack.optimize.CommonsChunkPlugin({ 109 | name: 'app', 110 | async: 'vendor-async', 111 | children: true, 112 | minChunks: 3 113 | }), 114 | 115 | // copy custom static assets 116 | new CopyWebpackPlugin([ 117 | { 118 | from: path.resolve(__dirname, '../static'), 119 | to: config.build.assetsSubDirectory, 120 | ignore: ['.*'] 121 | } 122 | ]) 123 | ] 124 | }) 125 | 126 | if (config.build.productionGzip) { 127 | const CompressionWebpackPlugin = require('compression-webpack-plugin') 128 | 129 | webpackConfig.plugins.push( 130 | new CompressionWebpackPlugin({ 131 | asset: '[path].gz[query]', 132 | algorithm: 'gzip', 133 | test: new RegExp( 134 | '\\.(' + 135 | config.build.productionGzipExtensions.join('|') + 136 | ')$' 137 | ), 138 | threshold: 10240, 139 | minRatio: 0.8 140 | }) 141 | ) 142 | } 143 | 144 | if (config.build.bundleAnalyzerReport) { 145 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin 146 | webpackConfig.plugins.push(new BundleAnalyzerPlugin()) 147 | } 148 | 149 | module.exports = webpackConfig 150 | -------------------------------------------------------------------------------- /admin/src/pages/login/index.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 117 | 118 | --------------------------------------------------------------------------------