├── admin ├── static │ └── .gitkeep ├── .eslintignore ├── 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 ├── config │ ├── prod.env.js │ ├── test.env.js │ ├── dev.env.js │ └── index.js ├── src │ ├── assets │ │ ├── images │ │ │ ├── l.png │ │ │ ├── m.png │ │ │ ├── about.png │ │ │ ├── group.png │ │ │ ├── bg_top.png │ │ │ └── login_bg.jpg │ │ ├── fonts │ │ │ ├── icomoon.eot │ │ │ ├── icomoon.ttf │ │ │ └── icomoon.woff │ │ └── css │ │ │ ├── 404.css │ │ │ └── admin.css │ ├── App.vue │ ├── api │ │ ├── token.js │ │ ├── image.js │ │ ├── index.js │ │ ├── config.js │ │ ├── user.js │ │ ├── comment.js │ │ ├── content.js │ │ ├── tag.js │ │ └── category.js │ ├── store │ │ ├── index.js │ │ └── modules │ │ │ ├── token.js │ │ │ └── admin.js │ ├── main.js │ ├── view │ │ ├── common │ │ │ ├── 404.vue │ │ │ └── login.vue │ │ ├── user │ │ │ ├── info.vue │ │ │ └── password.vue │ │ ├── category │ │ │ ├── save.vue │ │ │ └── list.vue │ │ ├── tag │ │ │ ├── save.vue │ │ │ └── list.vue │ │ ├── comment │ │ │ └── save.vue │ │ ├── config │ │ │ └── save.vue │ │ ├── page │ │ │ ├── save.vue │ │ │ └── list.vue │ │ └── content │ │ │ └── list.vue │ ├── router │ │ ├── index.js │ │ └── router.js │ └── axios │ │ └── index.js ├── .editorconfig ├── .gitignore ├── .postcssrc.js ├── index.html ├── .babelrc ├── README.md ├── .eslintrc.js └── package.json ├── view ├── bottom.html ├── page.html ├── header.html ├── navbar.html ├── archives.html ├── footer.html ├── comment.html ├── muster.html ├── list.html ├── error_404.html ├── error_500.html └── content.html ├── .eslintrc ├── src ├── config │ ├── config.js │ ├── extend.js │ ├── router.js │ ├── middleware.js │ └── adapter.js ├── model │ ├── comment.js │ ├── user.js │ ├── config.js │ └── content.js ├── logic │ ├── api │ │ ├── category.js │ │ ├── token.js │ │ └── content.js │ └── content.js ├── controller │ ├── index.js │ ├── api │ │ ├── config.js │ │ ├── token.js │ │ ├── meta.js │ │ ├── image.js │ │ ├── comment.js │ │ ├── user.js │ │ └── content.js │ ├── base.js │ └── rest.js ├── bootstrap │ ├── master.js │ └── worker.js ├── service │ ├── cache.js │ ├── upload.js │ └── email.js └── extend │ └── controller.js ├── .gitignore ├── www ├── static │ └── admin │ │ ├── img │ │ └── login_bg.ea367c0.jpg │ │ ├── fonts │ │ ├── fontello.068ca2b.ttf │ │ ├── fontello.e73a064.eot │ │ ├── ionicons.24712f6.ttf │ │ ├── ionicons.2c2ae06.eot │ │ └── ionicons.05acfdb.woff │ │ └── js │ │ ├── 7.5dcfd6857f9b48a8fa35.js │ │ ├── 8.b2ec828d8bcd3da221b5.js │ │ ├── manifest.8f6c27c64bbd02ea355f.js │ │ ├── 9.d601dbc3c4827fedc5aa.js │ │ ├── 16.6e0fbb5e769b225e9842.js │ │ ├── 10.6374464114e769765d2f.js │ │ ├── 11.161957e3e3c44fab68db.js │ │ ├── 17.ede603309e9a3946c3db.js │ │ ├── 14.6c2efb950e8d26ba043e.js │ │ ├── 12.5c3f7e3441ea03f55ae9.js │ │ ├── 13.509c60b02980c8a0329c.js │ │ ├── 5.abfb113cf452a5c6cdb0.js │ │ ├── 15.f0a63d8001465aac2051.js │ │ ├── 0.d67519eeb53a312e070e.js │ │ ├── 6.565567e57f49237492d6.js │ │ └── 2.e046b457f58bda54b4a1.js └── admin.html ├── production.js ├── pm2.json ├── development.js ├── Dockerfile ├── nginx.conf ├── README.md └── package.json /admin/static/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /view/bottom.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "think" 3 | } -------------------------------------------------------------------------------- /src/config/config.js: -------------------------------------------------------------------------------- 1 | // default config 2 | module.exports = { 3 | 4 | }; 5 | -------------------------------------------------------------------------------- /admin/.eslintignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | /config/ 3 | /dist/ 4 | /*.js 5 | /test/unit/coverage/ 6 | -------------------------------------------------------------------------------- /admin/build/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lscho/ThinkJS-Vue.js-blog/HEAD/admin/build/logo.png -------------------------------------------------------------------------------- /admin/config/prod.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = { 3 | NODE_ENV: '"production"' 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.js.map 3 | node_modules/ 4 | runtime/ 5 | logs/ 6 | www/uploads 7 | src/config/*.production.js -------------------------------------------------------------------------------- /admin/src/assets/images/l.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lscho/ThinkJS-Vue.js-blog/HEAD/admin/src/assets/images/l.png -------------------------------------------------------------------------------- /admin/src/assets/images/m.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lscho/ThinkJS-Vue.js-blog/HEAD/admin/src/assets/images/m.png -------------------------------------------------------------------------------- /admin/src/assets/images/about.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lscho/ThinkJS-Vue.js-blog/HEAD/admin/src/assets/images/about.png -------------------------------------------------------------------------------- /admin/src/assets/images/group.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lscho/ThinkJS-Vue.js-blog/HEAD/admin/src/assets/images/group.png -------------------------------------------------------------------------------- /admin/src/assets/fonts/icomoon.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lscho/ThinkJS-Vue.js-blog/HEAD/admin/src/assets/fonts/icomoon.eot -------------------------------------------------------------------------------- /admin/src/assets/fonts/icomoon.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lscho/ThinkJS-Vue.js-blog/HEAD/admin/src/assets/fonts/icomoon.ttf -------------------------------------------------------------------------------- /admin/src/assets/fonts/icomoon.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lscho/ThinkJS-Vue.js-blog/HEAD/admin/src/assets/fonts/icomoon.woff -------------------------------------------------------------------------------- /admin/src/assets/images/bg_top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lscho/ThinkJS-Vue.js-blog/HEAD/admin/src/assets/images/bg_top.png -------------------------------------------------------------------------------- /admin/src/assets/images/login_bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lscho/ThinkJS-Vue.js-blog/HEAD/admin/src/assets/images/login_bg.jpg -------------------------------------------------------------------------------- /www/static/admin/img/login_bg.ea367c0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lscho/ThinkJS-Vue.js-blog/HEAD/www/static/admin/img/login_bg.ea367c0.jpg -------------------------------------------------------------------------------- /www/static/admin/fonts/fontello.068ca2b.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lscho/ThinkJS-Vue.js-blog/HEAD/www/static/admin/fonts/fontello.068ca2b.ttf -------------------------------------------------------------------------------- /www/static/admin/fonts/fontello.e73a064.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lscho/ThinkJS-Vue.js-blog/HEAD/www/static/admin/fonts/fontello.e73a064.eot -------------------------------------------------------------------------------- /www/static/admin/fonts/ionicons.24712f6.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lscho/ThinkJS-Vue.js-blog/HEAD/www/static/admin/fonts/ionicons.24712f6.ttf -------------------------------------------------------------------------------- /www/static/admin/fonts/ionicons.2c2ae06.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lscho/ThinkJS-Vue.js-blog/HEAD/www/static/admin/fonts/ionicons.2c2ae06.eot -------------------------------------------------------------------------------- /www/static/admin/fonts/ionicons.05acfdb.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lscho/ThinkJS-Vue.js-blog/HEAD/www/static/admin/fonts/ionicons.05acfdb.woff -------------------------------------------------------------------------------- /admin/src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /production.js: -------------------------------------------------------------------------------- 1 | const Application = require('thinkjs'); 2 | 3 | const instance = new Application({ 4 | ROOT_PATH: __dirname, 5 | proxy: true, // use proxy 6 | env: 'production' 7 | }); 8 | 9 | instance.run(); 10 | -------------------------------------------------------------------------------- /pm2.json: -------------------------------------------------------------------------------- 1 | { 2 | "apps": [{ 3 | "name": "blog", 4 | "script": "production.js", 5 | "cwd": "/www/blog", 6 | "max_memory_restart": "1G", 7 | "autorestart": true, 8 | "node_args": [], 9 | "args": [], 10 | "env": {} 11 | }] 12 | } -------------------------------------------------------------------------------- /development.js: -------------------------------------------------------------------------------- 1 | const Application = require('thinkjs'); 2 | const watcher = require('think-watcher'); 3 | 4 | const instance = new Application({ 5 | ROOT_PATH: __dirname, 6 | watcher: watcher, 7 | env: 'development' 8 | }); 9 | 10 | instance.run(); 11 | -------------------------------------------------------------------------------- /admin/src/api/token.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | export default { 4 | 5 | create (userInfo) { 6 | return new Promise((resolve, reject) => { 7 | Vue.axios.post('/token', userInfo).then(response => { 8 | resolve(response.data) 9 | }) 10 | }) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /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 | /test/e2e/reports/ 9 | selenium-debug.log 10 | 11 | # Editor directories and files 12 | .idea 13 | .vscode 14 | *.suo 15 | *.ntvs* 16 | *.njsproj 17 | *.sln 18 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/model/comment.js: -------------------------------------------------------------------------------- 1 | module.exports = class extends think.Model { 2 | // 模型关联 3 | get relation() { 4 | return { 5 | parent: { 6 | type: think.Model.BELONG_TO, 7 | model: 'comment', 8 | key: 'parent_id', 9 | fKey: 'id' 10 | } 11 | }; 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /admin/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 后台管理 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/config/extend.js: -------------------------------------------------------------------------------- 1 | const view = require('think-view'); 2 | const model = require('think-model'); 3 | const cache = require('think-cache'); 4 | const session = require('think-session'); 5 | const email = require('think-email'); 6 | 7 | module.exports = [ 8 | view, // make application support view 9 | model(think.app), 10 | cache, 11 | session, 12 | email 13 | ]; 14 | -------------------------------------------------------------------------------- /admin/src/api/image.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | export default { 3 | 4 | upload (data) { 5 | return new Promise((resolve, reject) => { 6 | Vue.axios.post('/image', data, { 7 | headers: { 'Content-Type': 'multipart/form-data' } 8 | }).then(response => { 9 | resolve(response.data) 10 | }).catch(error => { 11 | reject(error) 12 | }) 13 | }) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /admin/src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | 4 | import token from './modules/token' 5 | import admin from './modules/admin' 6 | 7 | Vue.use(Vuex) 8 | 9 | const debug = process.env.NODE_ENV !== 'production' 10 | const actions = {} 11 | const getters = {} 12 | export default new Vuex.Store({ 13 | actions, 14 | getters, 15 | modules: { 16 | token, 17 | admin 18 | }, 19 | strict: debug 20 | }) 21 | -------------------------------------------------------------------------------- /src/logic/api/category.js: -------------------------------------------------------------------------------- 1 | module.exports = class extends think.Logic { 2 | postAction() { 3 | const rules = { 4 | name: { 5 | string: true, 6 | required: true 7 | } 8 | }; 9 | const msgs = { 10 | name: '分类名称不能为空' 11 | }; 12 | const flag = this.validate(rules, msgs); 13 | if (!flag) { 14 | return this.fail(1001, 'validate error', this.validateErrors); 15 | } 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /admin/src/api/index.js: -------------------------------------------------------------------------------- 1 | import category from '@/api/category' 2 | import content from '@/api/content' 3 | import comment from '@/api/comment' 4 | import image from '@/api/image' 5 | import tag from '@/api/tag' 6 | import token from '@/api/token' 7 | import user from '@/api/user' 8 | import config from '@/api/config' 9 | 10 | export { 11 | category, 12 | content, 13 | comment, 14 | image, 15 | tag, 16 | token, 17 | user, 18 | config 19 | } 20 | -------------------------------------------------------------------------------- /src/controller/index.js: -------------------------------------------------------------------------------- 1 | const Base = require('./base.js'); 2 | 3 | module.exports = class extends Base { 4 | /** 5 | * 首页 6 | * @return {[type]} [description] 7 | */ 8 | async indexAction() { 9 | let searchParam = this.post('s'); 10 | if (searchParam) { 11 | searchParam = encodeURIComponent(searchParam); 12 | return this.redirect('/search/' + searchParam + '/'); 13 | } 14 | return this.action('content', 'list'); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /admin/src/api/config.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | export default { 4 | 5 | getList () { 6 | return new Promise((resolve, reject) => { 7 | Vue.axios.get('/config').then(response => { 8 | resolve(response.data) 9 | }) 10 | }) 11 | }, 12 | 13 | update (type, data) { 14 | return new Promise((resolve, reject) => { 15 | Vue.axios.put('/config/' + type, data).then(response => { 16 | resolve(response.data) 17 | }) 18 | }) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /admin/src/api/user.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | export default { 4 | 5 | getInfo (userName) { 6 | return new Promise((resolve, reject) => { 7 | Vue.axios.get('/user/' + userName).then(response => { 8 | resolve(response.data) 9 | }) 10 | }) 11 | }, 12 | 13 | update (userName, data) { 14 | return new Promise((resolve, reject) => { 15 | Vue.axios.put('/user/' + userName, data).then(response => { 16 | resolve(response.data) 17 | }) 18 | }) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /www/admin.html: -------------------------------------------------------------------------------- 1 | 后台管理
-------------------------------------------------------------------------------- /src/logic/api/token.js: -------------------------------------------------------------------------------- 1 | module.exports = class extends think.Logic { 2 | postAction() { 3 | const rules = { 4 | username: { 5 | string: true, 6 | required: true 7 | }, 8 | password: { 9 | string: true, 10 | required: true 11 | } 12 | }; 13 | const msgs = { 14 | username: '用户名不能为空', 15 | password: '密码不能为空' 16 | }; 17 | const flag = this.validate(rules, msgs); 18 | if (!flag) { 19 | return this.fail(1001, 'validate error', this.validateErrors); 20 | } 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /admin/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import App from './App' 4 | import router from './router' 5 | import store from './store' 6 | import axios from './axios' 7 | import VueAxios from 'vue-axios' 8 | import { sync } from 'vuex-router-sync' 9 | 10 | // 插件 11 | Vue.use(Vuex) 12 | Vue.use(VueAxios, axios) 13 | sync(store, router) 14 | 15 | // 配置 16 | Vue.config.productionTip = false 17 | 18 | /* eslint-disable no-new */ 19 | new Vue({ 20 | el: '#app', 21 | store, 22 | router, 23 | template: '', 24 | components: { App } 25 | }) 26 | -------------------------------------------------------------------------------- /view/page.html: -------------------------------------------------------------------------------- 1 | 2 | <% include navbar.html %> 3 |
4 |
5 |

<%= content.title %>

6 |
7 | <%- content.content %> 8 |
9 |
10 | <% include footer.html %> 11 | 12 | <% include bottom.html %> -------------------------------------------------------------------------------- /src/config/router.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | // RESTFUL 3 | [/\/api\/(\w+)(?:\/(.*))?/, 'api/:1?id=:2', 'rest'], 4 | ['/page/about', 'content/page?slug=about'], 5 | ['/page/links', 'content/page?slug=links'], 6 | ['/page/archives', 'content/archives'], 7 | [/\/page\/(\d+)/, 'content/list?page=:1'], 8 | ['/search/:search', 'content/muster'], 9 | ['/category/:category', 'content/muster'], 10 | ['/category/:category/:page', 'content/muster'], 11 | ['/tag/:tag', 'content/muster'], 12 | ['/tag/:tag/:page', 'content/muster'], 13 | ['/:category/:slug', 'content/detail'], 14 | ['/:category/:slug/comment', 'content/comment'] 15 | ]; 16 | -------------------------------------------------------------------------------- /src/model/user.js: -------------------------------------------------------------------------------- 1 | module.exports = class extends think.Model { 2 | 3 | /** 4 | * 验证密码 5 | * @param {Object} userInfo 用户信息 6 | * @param {String} password 明文密码 7 | * @return {[type]} [description] 8 | */ 9 | verifyPassword(userInfo={}, password="") { 10 | return think.md5(think.md5(password) + userInfo.encrypt) === userInfo.password; 11 | } 12 | 13 | /** 14 | * 生成密码 15 | * @param {Object} userInfo 用户信息 16 | * @param {String} password 明文密码 17 | * @return {string} [description] 18 | */ 19 | sign(userInfo={}, password=""){ 20 | return think.md5(think.md5(password) + userInfo.encrypt); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # https://hub.docker.com/_/node 2 | FROM node:16-slim 3 | 4 | # Create and change to the app directory. 5 | WORKDIR /usr/src/app 6 | 7 | # Copy application dependency manifests to the container image. 8 | # A wildcard is used to ensure both package.json AND package-lock.json are copied. 9 | # Copying this separately prevents re-running npm install on every code change. 10 | COPY package*.json ./ 11 | 12 | # Install production dependencies. 13 | RUN npm install --only=production --registry=https://registry.npmmirror.com 14 | 15 | # Copy local code to the container image. 16 | COPY . ./ 17 | 18 | # Run the web service on container startup. 19 | CMD [ "node", "production.js" ] -------------------------------------------------------------------------------- /admin/README.md: -------------------------------------------------------------------------------- 1 | # home 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 e2e tests 24 | npm run e2e 25 | 26 | # run all tests 27 | npm test 28 | ``` 29 | 30 | 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). 31 | -------------------------------------------------------------------------------- /admin/src/store/modules/token.js: -------------------------------------------------------------------------------- 1 | // initial state 2 | const state = { 3 | value: localStorage.getItem('token') 4 | } 5 | 6 | // getters 7 | const getters = { 8 | 9 | getToken: state => state.value, 10 | 11 | verifyToken: state => { 12 | if (!state.value) { 13 | return false 14 | } 15 | 16 | return true 17 | } 18 | } 19 | 20 | // actions 21 | const actions = { 22 | 23 | } 24 | 25 | // mutations 26 | const mutations = { 27 | 28 | setToken (state, value) { 29 | localStorage.setItem('token', value) 30 | state.value = value 31 | }, 32 | 33 | clearToken (state, value) { 34 | localStorage.removeItem('token') 35 | state.value = '' 36 | } 37 | } 38 | 39 | export default { 40 | state, 41 | getters, 42 | actions, 43 | mutations 44 | } 45 | -------------------------------------------------------------------------------- /src/logic/api/content.js: -------------------------------------------------------------------------------- 1 | module.exports = class extends think.Logic { 2 | postAction() { 3 | const rules = { 4 | title: { 5 | string: true, 6 | required: true 7 | }, 8 | category_id: { 9 | int: true, 10 | required: true 11 | }, 12 | create_time: { 13 | date: true, 14 | required: true 15 | }, 16 | status: { 17 | int: true, 18 | required: true 19 | } 20 | }; 21 | const msgs = { 22 | name: '文章标题不能为空', 23 | category_id: '分类必须选择', 24 | create_time: '发布时间不能为空', 25 | status: '文章状态不能为空' 26 | }; 27 | const flag = this.validate(rules, msgs); 28 | if (!flag) { 29 | return this.fail(1001, 'validate error', this.validateErrors); 30 | } 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /src/logic/content.js: -------------------------------------------------------------------------------- 1 | module.exports = class extends think.Logic { 2 | commentAction() { 3 | const rules = { 4 | slug: { 5 | string: true, 6 | required: true, 7 | method: 'GET' 8 | }, 9 | author: { 10 | string: true, 11 | required: true 12 | }, 13 | email: { 14 | email: true, 15 | required: true 16 | }, 17 | text: { 18 | string: true, 19 | required: true 20 | } 21 | }; 22 | const msgs = { 23 | slug: '文章不存在', 24 | author: '名字不能为空', 25 | email: '邮箱不能为空', 26 | text: '留言内容不能为空' 27 | }; 28 | const flag = this.validate(rules, msgs); 29 | if (!flag) { 30 | return this.fail(1001, 'validate error', this.validateErrors); 31 | } 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name example.com www.example.com; 4 | root /www/blog/www; 5 | set $node_port 8360; 6 | 7 | index index.js index.html index.htm; 8 | 9 | location / { 10 | proxy_http_version 1.1; 11 | proxy_set_header X-Real-IP $remote_addr; 12 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 13 | proxy_set_header Host $http_host; 14 | proxy_set_header X-NginX-Proxy true; 15 | proxy_set_header Upgrade $http_upgrade; 16 | proxy_set_header Connection "upgrade"; 17 | proxy_pass http://127.0.0.1:$node_port$request_uri; 18 | proxy_redirect off; 19 | } 20 | 21 | 22 | location ~ ^/(static|uploads)/ { 23 | etag on; 24 | expires max; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/controller/api/config.js: -------------------------------------------------------------------------------- 1 | const BaseRest = require('../rest.js'); 2 | 3 | module.exports = class extends BaseRest { 4 | /** 5 | * 获取配置 6 | * @return {[type]} [description] 7 | */ 8 | async getAction() { 9 | const type = this.get('type'); 10 | const list = await this.model('config').getList(type); 11 | return this.success(list); 12 | } 13 | 14 | /** 15 | * 更新配置 16 | * @return {[type]} [description] 17 | */ 18 | async putAction() { 19 | const data = this.post(); 20 | const res = await this.model('config').save(this.id, data); 21 | if (res) { 22 | await this.hook('configUpdate', {type: this.id, data: data}); 23 | return this.success({ id: res }, '更新成功'); 24 | } else { 25 | return this.fail(1000, '更新失败'); 26 | } 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /src/bootstrap/master.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | think.beforeStartServer(async() => { 3 | // 压缩模板 4 | if (think.env === 'production') { 5 | if (!fs.existsSync(think.ROOT_PATH + '/runtime/')) { 6 | fs.mkdirSync(think.ROOT_PATH + '/runtime/'); 7 | } 8 | if (!fs.existsSync(think.ROOT_PATH + '/runtime/view/')) { 9 | fs.mkdirSync(think.ROOT_PATH + '/runtime/view/'); 10 | } 11 | var minify = require('html-minifier').minify; 12 | const views = fs.readdirSync(think.ROOT_PATH + '/view'); 13 | views.forEach((val, index) => { 14 | const data = fs.readFileSync(think.ROOT_PATH + '/view/' + val, 'utf8'); 15 | const minifyData = minify(data, {removeComments: true, collapseWhitespace: true, minifyJS: true, minifyCSS: true}); 16 | fs.writeFileSync(think.ROOT_PATH + '/runtime/view/' + val, minifyData); 17 | }); 18 | } 19 | }); -------------------------------------------------------------------------------- /src/controller/api/token.js: -------------------------------------------------------------------------------- 1 | const BaseRest = require('../rest.js'); 2 | 3 | module.exports = class extends BaseRest { 4 | // token 生成 5 | async postAction() { 6 | const username = this.post('username'); 7 | const password = this.post('password'); 8 | const user = this.model('user'); 9 | const userInfo = await user.where({ username: username }).find(); 10 | if (think.isEmpty(userInfo)) { 11 | return this.fail('用户不存在'); 12 | } 13 | const result = user.verifyPassword(userInfo, password); 14 | if (think.isEmpty(result)) { 15 | return this.fail('密码不正确'); 16 | } 17 | delete userInfo.password; 18 | delete userInfo.encrypt; 19 | user.where({ username: username }).update({last_login_time: (new Date()).getTime() / 1000}); 20 | const token = await this.session('userInfo', userInfo); 21 | return this.success({ token: token }); 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /src/model/config.js: -------------------------------------------------------------------------------- 1 | module.exports = class extends think.Model { 2 | /** 3 | * 获取配置列表 4 | * @param {String} type [description] 5 | * @return {[type]} [description] 6 | */ 7 | async getList(type = '') { 8 | const map = {}; 9 | if (type) { 10 | map.type = type; 11 | } 12 | const list = await this.where(map).select(); 13 | const data = {}; 14 | for (const i in list) { 15 | data[list[i].name] = JSON.parse(list[i].value); 16 | } 17 | return data; 18 | } 19 | 20 | /** 21 | * 保存配置 22 | * @param {[type]} name [description] 23 | * @param {[type]} value [description] 24 | * @return {[type]} [description] 25 | */ 26 | async save(name, value) { 27 | const data = { 28 | name: name, 29 | value: JSON.stringify(value) 30 | }; 31 | return this.thenUpdate(data, { name: name }); 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /src/service/cache.js: -------------------------------------------------------------------------------- 1 | module.exports = class extends think.Service { 2 | /** 3 | * 注册HOOK点 4 | * @return {[type]} [description] 5 | */ 6 | static registerHook() { 7 | return { 8 | 'comment': ['commentCreate', 'commentUpdate', 'commentDelete'], 9 | 'content': ['contentCreate', 'contentUpdate', 'contentDelete'], 10 | 'config': ['configUpdate'] 11 | }; 12 | } 13 | 14 | /** 15 | * 更新留言缓存 16 | * @param {[type]} data [description] 17 | * @return {[type]} [description] 18 | */ 19 | comment(data) { 20 | think.cache('recent_comment', null); 21 | } 22 | 23 | /** 24 | * 更新内容缓存 25 | * @param {[type]} data [description] 26 | * @return {[type]} [description] 27 | */ 28 | content(data) { 29 | think.cache('recent_content', null); 30 | } 31 | 32 | config(data) { 33 | think.cache('config', null); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /admin/.eslintrc.js: -------------------------------------------------------------------------------- 1 | // https://eslint.org/docs/user-guide/configuring 2 | 3 | module.exports = { 4 | root: true, 5 | parserOptions: { 6 | parser: 'babel-eslint' 7 | }, 8 | env: { 9 | browser: true, 10 | }, 11 | extends: [ 12 | // https://github.com/vuejs/eslint-plugin-vue#priority-a-essential-error-prevention 13 | // consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules. 14 | 'plugin:vue/essential', 15 | // https://github.com/standard/standard/blob/master/docs/RULES-en.md 16 | 'standard' 17 | ], 18 | // required to lint *.vue files 19 | plugins: [ 20 | 'vue' 21 | ], 22 | // add your custom rules here 23 | rules: { 24 | // allow async-await 25 | 'generator-star-spacing': 'off', 26 | // allow debugger during development 27 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off' 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /admin/src/assets/css/404.css: -------------------------------------------------------------------------------- 1 | /* 404页面样式 */ 2 | 3 | .error404-body-con { 4 | width: 700px; 5 | height: 500px; 6 | position: absolute; 7 | left: 50%; 8 | top: 50%; 9 | transform: translate(-50%, -50%); 10 | } 11 | 12 | .error404-body-con-title { 13 | text-align: center; 14 | font-size: 240px; 15 | font-weight: 700; 16 | color: #2d8cf0; 17 | height: 260px; 18 | line-height: 260px; 19 | margin-top: 40px; 20 | } 21 | 22 | .error404-body-con-title span { 23 | display: inline-block; 24 | color: #19be6b; 25 | font-size: 230px; 26 | animation: error404animation 3s ease 0s infinite alternate; 27 | } 28 | 29 | .error404-body-con-message { 30 | display: block; 31 | text-align: center; 32 | font-size: 30px; 33 | font-weight: 500; 34 | letter-spacing: 12px; 35 | color: #dddde2; 36 | } 37 | 38 | .error404-btn-con { 39 | text-align: center; 40 | padding: 20px 0; 41 | margin-bottom: 40px; 42 | } 43 | -------------------------------------------------------------------------------- /admin/src/api/comment.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | export default { 4 | 5 | getList (map = {}) { 6 | return new Promise((resolve, reject) => { 7 | Vue.axios.get('/comment', { params: map }).then(response => { 8 | resolve(response.data) 9 | }) 10 | }) 11 | }, 12 | 13 | getInfo (slug) { 14 | return new Promise((resolve, reject) => { 15 | Vue.axios.get('/comment/' + slug).then(response => { 16 | resolve(response.data) 17 | }) 18 | }) 19 | }, 20 | 21 | update (id, data) { 22 | return new Promise((resolve, reject) => { 23 | Vue.axios.put('/comment/' + id, data).then(response => { 24 | resolve(response.data) 25 | }) 26 | }) 27 | }, 28 | 29 | delete (id) { 30 | return new Promise((resolve, reject) => { 31 | Vue.axios.delete('/comment/' + id).then(response => { 32 | resolve(response.data) 33 | }) 34 | }) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /view/header.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | <% if (title) { %><%=title %> - <% }%><%=site.title %> 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /admin/src/store/modules/admin.js: -------------------------------------------------------------------------------- 1 | // initial state 2 | const state = { 3 | menu: { 4 | // 是否折叠 5 | fold: localStorage.getItem('menu_fold') === 'true' 6 | }, 7 | user: { 8 | // 用户名 9 | name: localStorage.getItem('user_name'), 10 | // 全部信息 11 | info: {} 12 | } 13 | } 14 | 15 | // getters 16 | const getters = { 17 | 18 | getMenu: state => state.menu, 19 | getUser: state => state.user 20 | 21 | } 22 | 23 | // actions 24 | const actions = { 25 | 26 | } 27 | 28 | // mutations 29 | const mutations = { 30 | 31 | setMenuFlod (state, value) { 32 | localStorage.setItem('menu_fold', value) 33 | state.menu.fold = value ? 1 : 0 34 | }, 35 | 36 | setUserName (state, value) { 37 | localStorage.setItem('user_name', value) 38 | state.user.name = value 39 | }, 40 | 41 | setUserInfo (state, value) { 42 | state.user.info = value 43 | } 44 | } 45 | 46 | export default { 47 | state, 48 | getters, 49 | actions, 50 | mutations 51 | } 52 | -------------------------------------------------------------------------------- /admin/src/view/common/404.vue: -------------------------------------------------------------------------------- 1 | 15 | 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 演示 2 | 3 | ![tUkXHm](https://raw.githubusercontent.com/lscho/oss/master/2020/07/21/tUkXHm.png) 4 | 5 | ![MofIuA](https://raw.githubusercontent.com/lscho/oss/master/2020/07/21/MofIuA.png) 6 | 7 | 8 | ## 安装 9 | 10 | 后台默认账号:admin 默认密码:123456 11 | 12 | #### 开发 13 | 14 | 导入sql,修改[配置](https://github.com/lscho/ThinkJS-Vue.js-blog/blob/master/src/config/adapter.js)中 mysql 部分,启动服务 15 | 16 | ```bash 17 | # 启动服务端 18 | npm start 19 | # 启动后台服务 20 | cd ./admin 21 | npm start 22 | ``` 23 | 24 | #### 部署 25 | 26 | ```bash 27 | # 编译 28 | cd ./admin 29 | npm run build 30 | ``` 31 | 32 | 将 `src/` `view/` `www/` `production.js` `package.json` 上传至服务器 33 | 34 | 执行`npm install` 35 | 36 | 修改[配置](https://github.com/lscho/ThinkJS-Vue.js-blog/blob/master/src/config/adapter.js)中 mysql 部分 37 | 38 | 修改[pm2.json](https://github.com/lscho/ThinkJS-Vue.js-blog/blob/master/pm2.json)中 `cwd` 部分,`pm2 start pm2.json` 启动服务 39 | 40 | 参考[nginx.conf](https://github.com/lscho/ThinkJS-Vue.js-blog/blob/master/nginx.conf)进行配置 41 | 42 | -------------------------------------------------------------------------------- /src/controller/api/meta.js: -------------------------------------------------------------------------------- 1 | const BaseRest = require('../rest.js'); 2 | 3 | module.exports = class extends BaseRest { 4 | // 添加分类 5 | async postAction() { 6 | const userInfo = this.userInfo; 7 | const data = { 8 | user_id: userInfo.id, 9 | name: this.post('name'), 10 | slug: this.post('slug'), 11 | type: this.post('type'), 12 | sort: this.post('sort'), 13 | description: this.post('description') 14 | }; 15 | const id = await this.modelInstance.add(data); 16 | if (id) { 17 | return this.success({ id: id }, '添加成功'); 18 | } else { 19 | return this.fail(1000, '添加失败'); 20 | } 21 | } 22 | 23 | // 查询分类 24 | async getAction() { 25 | let data; 26 | if (this.id) { 27 | data = await this.modelInstance.where({ id: this.id }).find(); 28 | return this.success(data); 29 | } 30 | const type = this.get('type') || 'category'; 31 | data = await this.modelInstance.where({ type: type }).order('sort desc').select(); 32 | return this.success(data); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /www/static/admin/js/7.5dcfd6857f9b48a8fa35.js: -------------------------------------------------------------------------------- 1 | webpackJsonp([7],{"H+0r":function(t,e){},U4kt:function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0});r("+skl"),r("H+0r");var s={render:function(){var t=this,e=t.$createElement,r=t._self._c||e;return r("div",{staticClass:"error404"},[r("div",{staticClass:"error404-body-con"},[r("Card",[r("div",{staticClass:"error404-body-con-title"},[t._v("4"),r("span",[r("Icon",{attrs:{type:"ios-navigate-outline"}})],1),t._v("4")]),t._v(" "),r("p",{staticClass:"error404-body-con-message"},[t._v("YOU  LOOK  LOST")]),t._v(" "),r("div",{staticClass:"error404-btn-con"},[r("Button",{staticStyle:{width:"200px"},attrs:{size:"large",type:"text"},on:{click:t.goHome}},[t._v("返回首页")]),t._v(" "),r("Button",{staticStyle:{width:"200px","margin-left":"40px"},attrs:{size:"large",type:"primary"},on:{click:t.backPage}},[t._v("返回上一页")])],1)])],1)])},staticRenderFns:[]},o=r("VU/8")({name:"Error404",methods:{backPage:function(){this.$router.go(-1)},goHome:function(){this.$router.push("/")}}},s,!1,null,null,null);e.default=o.exports}}); 2 | //# sourceMappingURL=7.5dcfd6857f9b48a8fa35.js.map -------------------------------------------------------------------------------- /src/controller/api/image.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const rename = think.promisify(fs.rename, fs); 4 | 5 | module.exports = class extends think.Controller { 6 | 7 | // 图片上传 8 | async postAction() { 9 | const file = this.file('image'); 10 | if (!file) { 11 | return this.fail(1000, '请上传文件'); 12 | } 13 | // path.extname获取文件后缀名,可做上传控制 14 | const extname = path.extname(file.name); 15 | const filename = path.basename(file.path); 16 | const basename = think.md5(filename) + extname; 17 | const date = new Date(); 18 | const year = date.getFullYear(); 19 | const month = (date.getMonth() + 1) < 10 ? '0' + (date.getMonth() + 1) : (date.getMonth() + 1); 20 | const savepath = '/uploads/' + year + '/' + month + '/' + basename; 21 | const filepath = path.join(think.ROOT_PATH, 'www' + savepath); 22 | think.mkdir(path.dirname(filepath)); 23 | rename(file.path, filepath); 24 | 25 | let data={ url: savepath, basename:basename, filepath:filepath}; 26 | await this.hook('upload',data); 27 | delete data.filepath; 28 | return this.success(data, '上传成功'); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /view/navbar.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/api/content.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | export default { 4 | 5 | create (data) { 6 | return new Promise((resolve, reject) => { 7 | Vue.axios.post('/content', data).then(response => { 8 | resolve(response.data) 9 | }) 10 | }) 11 | }, 12 | 13 | getList (map = {}) { 14 | map.type = map.type || 'default' 15 | return new Promise((resolve, reject) => { 16 | Vue.axios.get('/content', { params: map }).then(response => { 17 | resolve(response.data) 18 | }) 19 | }) 20 | }, 21 | 22 | getInfo (slug) { 23 | return new Promise((resolve, reject) => { 24 | Vue.axios.get('/content/' + slug).then(response => { 25 | resolve(response.data) 26 | }) 27 | }) 28 | }, 29 | 30 | update (id, data) { 31 | return new Promise((resolve, reject) => { 32 | Vue.axios.put('/content/' + id, data).then(response => { 33 | resolve(response.data) 34 | }) 35 | }) 36 | }, 37 | 38 | delete (id) { 39 | return new Promise((resolve, reject) => { 40 | Vue.axios.delete('/content/' + id).then(response => { 41 | resolve(response.data) 42 | }) 43 | }) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/config/middleware.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const isDev = think.env === 'development'; 3 | const isFaas = process.env.isFaas; 4 | module.exports = [ 5 | { 6 | handle: 'meta', 7 | options: { 8 | logRequest: isDev, 9 | sendResponseTime: isDev 10 | } 11 | }, 12 | { 13 | handle: 'resource', 14 | enable: isDev||isFaas, 15 | options: { 16 | root: path.join(think.ROOT_PATH, 'www'), 17 | publicPath: /^\/(static|uploads|favicon\.ico|index\.html|admin\.html)/ 18 | } 19 | }, 20 | { 21 | handle: 'trace', 22 | enable: !think.isCli, 23 | options: { 24 | debug: isDev, 25 | templates: { 26 | 404: path.join(think.ROOT_PATH, isDev ? 'view/error_404.html' : 'runtime/view/error_404.html'), 27 | 500: path.join(think.ROOT_PATH, isDev ? 'view/error_500.html' : 'runtime/view/error_500.html') 28 | } 29 | } 30 | }, 31 | { 32 | handle: 'payload', 33 | options: { 34 | uploadDir: path.join(think.ROOT_PATH, 'runtime/data') 35 | } 36 | }, 37 | { 38 | handle: 'router', 39 | options: { 40 | suffix: ['.html'] 41 | } 42 | }, 43 | 'logic', 44 | 'controller' 45 | ]; 46 | -------------------------------------------------------------------------------- /admin/src/api/tag.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | export default { 4 | create (data) { 5 | data.type = 'tag' 6 | return new Promise((resolve, reject) => { 7 | Vue.axios.post('/meta', data).then(response => { 8 | resolve(response.data) 9 | }) 10 | }) 11 | }, 12 | 13 | getList (data, map = {}) { 14 | map.type = 'tag' 15 | return new Promise((resolve, reject) => { 16 | Vue.axios.get('/meta', { params: map }).then(response => { 17 | resolve(response.data) 18 | }) 19 | }) 20 | }, 21 | 22 | getInfo (id, map = {}) { 23 | map.type = 'tag' 24 | return new Promise((resolve, reject) => { 25 | Vue.axios.get('/meta/' + id, { params: map }).then(response => { 26 | resolve(response.data) 27 | }) 28 | }) 29 | }, 30 | 31 | update (id, data) { 32 | data.type = 'tag' 33 | return new Promise((resolve, reject) => { 34 | Vue.axios.put('/meta/' + id, data).then(response => { 35 | resolve(response.data) 36 | }) 37 | }) 38 | }, 39 | 40 | delete (id) { 41 | return new Promise((resolve, reject) => { 42 | Vue.axios.delete('/meta/' + id).then(response => { 43 | resolve(response.data) 44 | }) 45 | }) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /admin/src/api/category.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | export default { 3 | 4 | create (data) { 5 | data.type = 'category' 6 | return new Promise((resolve, reject) => { 7 | Vue.axios.post('/meta', data).then(response => { 8 | resolve(response.data) 9 | }) 10 | }) 11 | }, 12 | 13 | getList (data, map = {}) { 14 | map.type = 'category' 15 | return new Promise((resolve, reject) => { 16 | Vue.axios.get('/meta', { params: map }).then(response => { 17 | resolve(response.data) 18 | }) 19 | }) 20 | }, 21 | 22 | getInfo (id, map = {}) { 23 | map.type = 'category' 24 | return new Promise((resolve, reject) => { 25 | Vue.axios.get('/meta/' + id, { params: map }).then(response => { 26 | resolve(response.data) 27 | }) 28 | }) 29 | }, 30 | 31 | update (id, data) { 32 | data.type = 'category' 33 | return new Promise((resolve, reject) => { 34 | Vue.axios.put('/meta/' + id, data).then(response => { 35 | resolve(response.data) 36 | }) 37 | }) 38 | }, 39 | 40 | delete (id) { 41 | return new Promise((resolve, reject) => { 42 | Vue.axios.delete('/meta/' + id).then(response => { 43 | resolve(response.data) 44 | }) 45 | }) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/bootstrap/worker.js: -------------------------------------------------------------------------------- 1 | think.beforeStartServer(async() => { 2 | const hooks = []; 3 | 4 | for (const Service of Object.values(think.app.services)) { 5 | const isHookService = think.isFunction(Service.registerHook); 6 | if (!isHookService) { 7 | continue; 8 | } 9 | 10 | const service = new Service(); 11 | const serviceHooks = Service.registerHook(); 12 | for (const hookFuncName in serviceHooks) { 13 | if (!think.isFunction(service[hookFuncName])) { 14 | continue; 15 | } 16 | 17 | let funcForHooks = serviceHooks[hookFuncName]; 18 | if (think.isString(funcForHooks)) { 19 | funcForHooks = [funcForHooks]; 20 | } 21 | 22 | if (!think.isArray(funcForHooks)) { 23 | continue; 24 | } 25 | 26 | for (const hookName of funcForHooks) { 27 | if (!hooks[hookName]) { 28 | hooks[hookName] = []; 29 | } 30 | 31 | hooks[hookName].push({ service, method: hookFuncName }); 32 | } 33 | } 34 | } 35 | think.config('hooks', hooks); 36 | 37 | let postTitle = {}; 38 | (await think.model('content').field(['slug', 'title']).select()).forEach(item => { 39 | postTitle[item.slug] = item.title; 40 | }); 41 | think.config('postTitle', postTitle); 42 | }); -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /admin/src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import store from '@/store' 3 | import VueRouter from 'vue-router' 4 | import { routers } from './router' 5 | import { Base64 } from 'js-base64' 6 | import { Message } from 'iview' 7 | 8 | Vue.use(VueRouter) 9 | 10 | // 路由配置 11 | const RouterConfig = { 12 | routes: routers 13 | } 14 | 15 | const router = new VueRouter(RouterConfig) 16 | 17 | router.beforeEach((to, from, next) => { 18 | // 设置title 19 | window.document.title = to.meta.title 20 | let token = localStorage.getItem('token') 21 | let requiresAuth = to.meta.requiresAuth 22 | if (requiresAuth === true) { 23 | if (!token) { 24 | Message.info('请登录') 25 | next('/login') 26 | return false 27 | } 28 | let tokenArray = token.split('.') 29 | if (tokenArray.length !== 3) { 30 | Message.error('身份验证错误,请重新登录') 31 | next('/login') 32 | return false 33 | } 34 | let payload = JSON.parse(Base64.decode(tokenArray[1])) 35 | if (Date.now() > payload.exp * 1000) { 36 | Message.error('登录已超时,请重新登录') 37 | store.commit('clearToken') 38 | next('/login') 39 | return false 40 | } 41 | } 42 | if (token && to.name === 'login') { 43 | next('/home') 44 | return false 45 | } 46 | // 权限检测 TODO 47 | next() 48 | }) 49 | 50 | router.afterEach((to) => { 51 | // 返回顶部 52 | window.scrollTo(0, 0) 53 | }) 54 | export default router 55 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/controller/api/comment.js: -------------------------------------------------------------------------------- 1 | const BaseRest = require('../rest.js'); 2 | 3 | module.exports = class extends BaseRest { 4 | /** 5 | * 获取数据 6 | * @return {[type]} [description] 7 | */ 8 | async getAction() { 9 | let data; 10 | const map = {}; 11 | // 获取详情 12 | if (this.id) { 13 | map.id = this.id; 14 | if (think.isEmpty(this.userInfo)) { 15 | map.status = 99; 16 | } 17 | data = await this.modelInstance.where(map).find(); 18 | 19 | return this.success(data); 20 | } 21 | 22 | // 关键词 23 | const key = this.get('key'); 24 | if (key) { 25 | map['comment.author|comment.text'] = ['like', '%' + key + '%']; 26 | } 27 | // 是否获取全部 28 | const all = this.get('all'); 29 | if (!all || think.isEmpty(this.userInfo)) { 30 | map['comment.status'] = 99; 31 | } 32 | 33 | // 页码 34 | const page = this.get('page') || 1; 35 | // 每页显示数量 36 | const pageSize = this.get('pageSize') || 5; 37 | data = await this.modelInstance 38 | .alias('comment') 39 | .join({ 40 | table: 'content', 41 | join: 'left', 42 | as: 'content', 43 | on: ['content_id', 'id'] 44 | }) 45 | .join({ 46 | table: 'meta', 47 | join: 'left', 48 | as: 'category', 49 | on: ['content.category_id', 'id'] 50 | }) 51 | .where(map) 52 | .page(page, pageSize) 53 | .field('comment.*,content.slug,content.title,content.category_id,category.slug as category') 54 | .order('comment.id desc') 55 | .countSelect(); 56 | return this.success(data); 57 | } 58 | }; 59 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "blog", 3 | "description": "A Simple Bloging Platform Base On ThinkJS 3.x & Vue.js & ES2015+", 4 | "version": "1.0.0", 5 | "author": "lscho ", 6 | "scripts": { 7 | "dev": "node development.js", 8 | "start": "npm run dev", 9 | "lint": "eslint src/", 10 | "lint-fix": "eslint --fix src/" 11 | }, 12 | "dependencies": { 13 | "html-minifier": "^3.5.15", 14 | "qiniu": "^7.1.3", 15 | "think-cache": "^1.0.0", 16 | "think-cache-file": "^1.0.8", 17 | "think-email": "^1.1.0", 18 | "think-logger3": "^1.0.0", 19 | "think-model": "^1.0.0", 20 | "think-model-mysql": "^1.0.0", 21 | "think-session": "^1.0.0", 22 | "think-session-jwt": "^1.0.8", 23 | "think-view": "^1.0.11", 24 | "think-view-ejs": "^0.0.11", 25 | "thinkjs": "^3.0.0" 26 | }, 27 | "devDependencies": { 28 | "think-watcher": "^3.0.0", 29 | "eslint": "^4.2.0", 30 | "eslint-config-think": "^1.0.0" 31 | }, 32 | "repository": "", 33 | "license": "MIT", 34 | "engines": { 35 | "node": ">=7.6.0" 36 | }, 37 | "readmeFilename": "README.md", 38 | "thinkjs": { 39 | "metadata": { 40 | "name": "blog", 41 | "description": "A Simple Bloging Platform Base On ThinkJS 3.x & Vue.js & ES2015+", 42 | "author": "lscho ", 43 | "babel": false 44 | }, 45 | "projectName": "blog", 46 | "templateName": "/usr/local/lib/node_modules/think-cli/default_template", 47 | "cacheTemplatePath": "/home/lscho/.think-templates/-usr-local-lib-node_modules-think-cli-default_template", 48 | "clone": false, 49 | "isMultiModule": false 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /admin/src/view/user/info.vue: -------------------------------------------------------------------------------- 1 | 26 | 66 | -------------------------------------------------------------------------------- /src/controller/base.js: -------------------------------------------------------------------------------- 1 | module.exports = class extends think.Controller { 2 | async __before() { 3 | // 获取站点配置 4 | const config = await think.model('config').cache('config').getList(); 5 | this.assign('site', config.site); 6 | // 默认title 7 | this.assign('title',""); 8 | // 获取用户资料 9 | const user = await think.model('user').cache('user').find(); 10 | this.assign('user', user); 11 | // 最近文章 12 | const recent = await this.getRecent(); 13 | this.assign('recent', recent); 14 | } 15 | 16 | /** 17 | * 最近文章和回复 18 | * @return {[type]} [description] 19 | */ 20 | async getRecent() { 21 | // 最近五篇文章 22 | const content = await think.model('content') 23 | .where({ status: 99, type: 'post' }) 24 | .cache('recent_content') 25 | .limit(5).order('create_time desc') 26 | .fieldReverse('content,markdown') 27 | .select(); 28 | // 最近五条回复 29 | const comment = await think.model('comment') 30 | .cache('recent_comment') 31 | .alias('comment') 32 | .join({ 33 | table: 'content', 34 | join: 'left', 35 | as: 'content', 36 | on: ['content_id', 'id'] 37 | }) 38 | .join({ 39 | table: 'meta', 40 | join: 'left', 41 | as: 'category', 42 | on: ['content.category_id', 'id'] 43 | }) 44 | .where({ 'comment.status': 99 }) 45 | .field('comment.*,content.slug,content.category_id,category.slug as category') 46 | .limit(5) 47 | .order('comment.create_time desc') 48 | .select(); 49 | return { content: content, comment: comment }; 50 | } 51 | 52 | async __call() { 53 | return this.display('404'); 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /src/controller/api/user.js: -------------------------------------------------------------------------------- 1 | const BaseRest = require('../rest.js'); 2 | 3 | module.exports = class extends BaseRest { 4 | // 查询用户信息 5 | async getAction() { 6 | const userInfo = this.userInfo; 7 | if (think.isEmpty(userInfo)) { 8 | this.ctx.status = 401; 9 | return this.fail(-1, '请登录后操作'); 10 | } 11 | let data; 12 | if (this.id) { 13 | data = await this.modelInstance.where({ username: this.id }).fieldReverse('id,password,encrypt').find(); 14 | data.avator = 'https://secure.gravatar.com/avatar/' + think.md5(data.email); 15 | return this.success(data); 16 | } else { 17 | data = await this.modelInstance.select(); 18 | return this.success(data); 19 | } 20 | } 21 | 22 | /** 23 | * 更新用户信息 24 | * @return {[type]} [description] 25 | */ 26 | async putAction() { 27 | 28 | const userInfo = await this.modelInstance.where({ username: this.id }).find(); 29 | let data = this.post(); 30 | if (think.isEmpty(data)) { 31 | return this.fail(1000, '数据不能为空'); 32 | } 33 | console.log(data) 34 | if(!think.isEmpty(data.password)){ 35 | if(data.newPassword!==data.confirmPassword){ 36 | return this.fail(1000, '两次密码不一致'); 37 | } 38 | 39 | if(!this.modelInstance.verifyPassword(userInfo,data.password)){ 40 | return this.fail(1000, '旧密码不正确'); 41 | } 42 | 43 | data.password=this.modelInstance.sign(userInfo,data.newPassword); 44 | } 45 | const rows = await this.modelInstance.where({ id: userInfo.id }).update(data); 46 | if (rows) { 47 | data.id = userInfo.id; 48 | await this.hook('userUpdate', data); 49 | return this.success({ affectedRows: rows }, '更新成功'); 50 | } else { 51 | return this.fail(1000, '更新失败'); 52 | } 53 | } 54 | }; -------------------------------------------------------------------------------- /src/extend/controller.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | /** 4 | * 执行hook 5 | * @param {[type]} name [description] 6 | * @param {...[type]} args [description] 7 | * @return {[type]} [description] 8 | */ 9 | async hook(name, ...args) { 10 | const { hooks } = think.config(); 11 | const hookFuncs = hooks[name]; 12 | if (!think.isArray(hookFuncs)) { 13 | return; 14 | } 15 | for (const { service, method } of hookFuncs) { 16 | await service[method](...args) 17 | }; 18 | }, 19 | 20 | /** 21 | * 发送响应 22 | * @param {[type]} tpl [description] 23 | * @return {[type]} [description] 24 | */ 25 | async renderAndFlush(tpl) { 26 | const firstChunkMinLength = 2014; 27 | let content = await this.render(tpl); 28 | 29 | //first chunk 30 | if (!this.ctx.headerSent) { 31 | this.ctx.type = 'html'; 32 | this.ctx.flushHeaders(); 33 | 34 | let length = content.length; 35 | 36 | // 第一个 chunk 太小会被浏览器缓存起来达不到效果,参考:https://stackoverflow.com/questions/16909227/using-transfer-encoding-chunked-how-much-data-must-be-sent-before-browsers-s 37 | if (length < firstChunkMinLength) { 38 | content += `${' '.repeat(firstChunkMinLength - length)}`; 39 | } 40 | } 41 | 42 | this.ctx.res.write(content); 43 | }, 44 | 45 | /** 46 | * 跳转页面 47 | * 已经发送了正文,就不能再通过 30X 状态码和 Location 头部来跳转页面,输出一段js来跳转 48 | * @param {[type]} url [description] 49 | * @return {[type]} [description] 50 | */ 51 | redirect(url){ 52 | this.ctx.res.write(``); 53 | return this.ctx.res.end(); 54 | } 55 | 56 | }; -------------------------------------------------------------------------------- /www/static/admin/js/8.b2ec828d8bcd3da221b5.js: -------------------------------------------------------------------------------- 1 | webpackJsonp([8],{taz5:function(r,e,t){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var o=t("gyMJ"),s=t("BTaQ"),n={components:{Button:s.Button,Form:s.Form,FormItem:s.FormItem,Input:s.Input},data:function(){var r=this;return{formItem:{password:"",newPassword:"",confirmPassword:""},ruleInline:{password:[{required:!0,message:"密码不能为空",trigger:"blur"}],newPassword:[{required:!0,message:"新密码不能为空",trigger:"blur"}],confirmPassword:[{required:!0,validator:function(e,t,o){""===t?o(new Error("确认密码不能为空")):t!==r.formItem.newPassword?o(new Error("两次密码不一致")):o()},trigger:"blur"}]}}},methods:{post:function(){var r=this;this.$refs.formItem.validate(function(e){e?o.h.update(r.$store.state.admin.user.name,r.formItem).then(function(r){}):r.$Message.error("请填写必要信息")})}},mounted:function(){this.get()}},m={render:function(){var r=this,e=r.$createElement,t=r._self._c||e;return t("Form",{ref:"formItem",attrs:{model:r.formItem,rules:r.ruleInline,"label-width":80}},[t("FormItem",{attrs:{label:"旧密码",prop:"password"}},[t("Input",{model:{value:r.formItem.password,callback:function(e){r.$set(r.formItem,"password",e)},expression:"formItem.password"}})],1),r._v(" "),t("FormItem",{attrs:{label:"新密码",prop:"newPassword"}},[t("Input",{model:{value:r.formItem.newPassword,callback:function(e){r.$set(r.formItem,"newPassword",e)},expression:"formItem.newPassword"}})],1),r._v(" "),t("FormItem",{attrs:{label:"确认密码",prop:"confirmPassword"}},[t("Input",{model:{value:r.formItem.confirmPassword,callback:function(e){r.$set(r.formItem,"confirmPassword",e)},expression:"formItem.confirmPassword"}})],1),r._v(" "),t("FormItem",[t("Button",{attrs:{type:"primary"},on:{click:r.post}},[r._v("保存")])],1)],1)},staticRenderFns:[]},a=t("VU/8")(n,m,!1,null,null,null);e.default=a.exports}}); 2 | //# sourceMappingURL=8.b2ec828d8bcd3da221b5.js.map -------------------------------------------------------------------------------- /www/static/admin/js/manifest.8f6c27c64bbd02ea355f.js: -------------------------------------------------------------------------------- 1 | !function(e){var n=window.webpackJsonp;window.webpackJsonp=function(r,o,c){for(var f,d,i,u=0,b=[];u 3 | <% include navbar.html %> 4 |
5 |
6 | <% for(let i in list){ %> 7 |
8 | <%= i %> 9 |
10 |
11 |
12 | <% list[i].forEach(function(content){ %> 13 |
14 |
15 |
16 | 21 |
22 |
23 | <%= think.datetime(content.create_time, 'MM DD, YYYY')%> 24 |
25 |
26 |
27 |
28 |
29 | <% }); %> 30 |
31 |
32 | <% } %> 33 |
34 |
35 | <% include footer.html %> 36 | 37 | <% include bottom.html %> -------------------------------------------------------------------------------- /www/static/admin/js/16.6e0fbb5e769b225e9842.js: -------------------------------------------------------------------------------- 1 | webpackJsonp([16],{"1rBo":function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var o=r("gyMJ"),m=r("BTaQ"),n={components:{Form:m.Form,FormItem:m.FormItem,Input:m.Input,InputNumber:m.InputNumber,Button:m.Button},data:function(){return{formItem:{id:"",name:"",slug:"",sort:0,description:""},ruleInline:{name:[{required:!0,message:"分类名称必须填写",trigger:"blur"}]}}},methods:{post:function(){var e=this;this.$refs.formItem.validate(function(t){t?e.formItem.id?o.a.update(e.formItem.id,e.formItem).then(function(t){e.$router.push("/category/list")}):o.a.create(e.formItem).then(function(t){e.$router.push("/category/list")}):e.$Message.error("请填写必要信息")})},get:function(e){var t=this;o.a.getInfo(e).then(function(e){t.formItem=e.data})}},mounted:function(){this.$route.query.id&&this.get(this.$route.query.id)}},a={render:function(){var e=this,t=e.$createElement,r=e._self._c||t;return r("Form",{ref:"formItem",attrs:{model:e.formItem,"label-width":80,rules:e.ruleInline}},[r("FormItem",{attrs:{label:"分类名称",prop:"name"}},[r("Input",{attrs:{placeholder:"分类名称"},model:{value:e.formItem.name,callback:function(t){e.$set(e.formItem,"name",t)},expression:"formItem.name"}})],1),e._v(" "),r("FormItem",{attrs:{label:"缩略名"}},[r("Input",{attrs:{placeholder:"分类缩略名用于创建友好的链接形式, 建议使用字母, 数字, 下划线和横杠"},model:{value:e.formItem.slug,callback:function(t){e.$set(e.formItem,"slug",t)},expression:"formItem.slug"}})],1),e._v(" "),r("FormItem",{attrs:{label:"排序"}},[r("InputNumber",{model:{value:e.formItem.sort,callback:function(t){e.$set(e.formItem,"sort",t)},expression:"formItem.sort"}})],1),e._v(" "),r("FormItem",{attrs:{label:"分类描述"}},[r("Input",{attrs:{type:"textarea",autosize:{minRows:2,maxRows:5},placeholder:"此文字用于描述分类, 预置参数"},model:{value:e.formItem.description,callback:function(t){e.$set(e.formItem,"description",t)},expression:"formItem.description"}})],1),e._v(" "),r("FormItem",[r("Button",{attrs:{type:"primary"},on:{click:e.post}},[e._v("提交")])],1)],1)},staticRenderFns:[]},s=r("VU/8")(n,a,!1,null,null,null);t.default=s.exports}}); 2 | //# sourceMappingURL=16.6e0fbb5e769b225e9842.js.map -------------------------------------------------------------------------------- /www/static/admin/js/10.6374464114e769765d2f.js: -------------------------------------------------------------------------------- 1 | webpackJsonp([10],{PKx2:function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var o=r("gyMJ"),m=r("BTaQ"),n={components:{Form:m.Form,FormItem:m.FormItem,Input:m.Input,InputNumber:m.InputNumber,Button:m.Button},data:function(){return{formItem:{id:"",name:"",slug:"",sort:0,description:""},ruleInline:{name:[{required:!0,message:"标签名称必须填写",trigger:"blur"}],slug:[{required:!0,message:"缩略名必须填写",trigger:"blur"}]}}},methods:{post:function(){var e=this;this.$refs.formItem.validate(function(t){t?e.formItem.id?o.f.update(e.formItem.id,e.formItem).then(function(t){e.$router.push("/tag/list")}):o.f.create(e.formItem).then(function(t){e.$router.push("/tag/list")}):e.$Message.error("请填写必要信息")})},get:function(e){var t=this;o.f.getInfo(e).then(function(e){t.formItem=e.data})}},mounted:function(){this.$route.query.id&&this.get(this.$route.query.id)}},s={render:function(){var e=this,t=e.$createElement,r=e._self._c||t;return r("Form",{ref:"formItem",attrs:{model:e.formItem,"label-width":80,rules:e.ruleInline}},[r("FormItem",{attrs:{label:"标签名称",prop:"name"}},[r("Input",{attrs:{placeholder:"标签名称"},model:{value:e.formItem.name,callback:function(t){e.$set(e.formItem,"name",t)},expression:"formItem.name"}})],1),e._v(" "),r("FormItem",{attrs:{label:"缩略名",prop:"slug"}},[r("Input",{attrs:{placeholder:"标签缩略名用于创建友好的链接形式, 建议使用字母, 数字, 下划线和横杠"},model:{value:e.formItem.slug,callback:function(t){e.$set(e.formItem,"slug",t)},expression:"formItem.slug"}})],1),e._v(" "),r("FormItem",{attrs:{label:"排序"}},[r("InputNumber",{model:{value:e.formItem.sort,callback:function(t){e.$set(e.formItem,"sort",t)},expression:"formItem.sort"}})],1),e._v(" "),r("FormItem",{attrs:{label:"标签描述"}},[r("Input",{attrs:{type:"textarea",autosize:{minRows:2,maxRows:5},placeholder:"此文字用于描述标签"},model:{value:e.formItem.description,callback:function(t){e.$set(e.formItem,"description",t)},expression:"formItem.description"}})],1),e._v(" "),r("FormItem",[r("Button",{attrs:{type:"primary"},on:{click:e.post}},[e._v("提交")])],1)],1)},staticRenderFns:[]},u=r("VU/8")(n,s,!1,null,null,null);t.default=u.exports}}); 2 | //# sourceMappingURL=10.6374464114e769765d2f.js.map -------------------------------------------------------------------------------- /admin/src/view/user/password.vue: -------------------------------------------------------------------------------- 1 | 17 | 75 | -------------------------------------------------------------------------------- /admin/src/axios/index.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { Message, LoadingBar } from 'iview' 3 | import store from '@/store' 4 | import router from '@/router' 5 | 6 | axios.defaults.baseURL = '/api' 7 | axios.defaults.timeout = 5000 8 | 9 | // http请求拦截器 10 | axios.interceptors.request.use(config => { 11 | // 进度条开始 12 | LoadingBar.start() 13 | // token 14 | if (localStorage.getItem('token')) { 15 | config.headers.Authorization = localStorage.getItem('token') 16 | } 17 | // 防止缓存 18 | if (config.method === 'post' && config.headers['Content-Type'] !== 'multipart/form-data') { 19 | config.data = { 20 | ...config.data, 21 | _t: Date.parse(new Date()) / 1000 22 | } 23 | } else if (config.method === 'get') { 24 | config.params = { 25 | _t: Date.parse(new Date()) / 1000, 26 | ...config.params 27 | } 28 | } 29 | return config 30 | }, error => { 31 | LoadingBar.error() 32 | Message.error('请求服务器超时') 33 | return error 34 | }) 35 | 36 | // http响应拦截器 37 | axios.interceptors.response.use(data => { 38 | if (!data || typeof data.data !== 'object') { 39 | console.log(data) 40 | LoadingBar.error() 41 | Message.error('服务器响应格式错误') 42 | } else { 43 | let errmsg = '' 44 | const errno = data.data.errno 45 | switch (errno) { 46 | case 1001: 47 | // 数据检验未通过 48 | for (let i in data.data.data) { 49 | errmsg += data.data.data[i] + ';' 50 | } 51 | break 52 | default: 53 | errmsg = data.data.errmsg 54 | break 55 | } 56 | if (errmsg !== '' && errno !== 0) { 57 | Message.error(errmsg) 58 | } 59 | if (errmsg !== '' && errno === 0) { 60 | Message.success(errmsg) 61 | } 62 | } 63 | LoadingBar.finish() 64 | return data 65 | }, error => { 66 | let errmsg = '服务器响应错误' 67 | if (error.response) { 68 | switch (error.response.status) { 69 | case 401: 70 | errmsg = '请登录后操作' 71 | store.commit('clearToken') 72 | router.replace('/login') 73 | break 74 | } 75 | } 76 | LoadingBar.error() 77 | Message.error(errmsg) 78 | return Promise.reject(error.response.data) 79 | }) 80 | export default axios 81 | -------------------------------------------------------------------------------- /www/static/admin/js/11.161957e3e3c44fab68db.js: -------------------------------------------------------------------------------- 1 | webpackJsonp([11],{Vpq0:function(t,e,o){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var n=o("gyMJ"),i=o("BTaQ"),a={components:{Button:i.Button,Table:i.Table,Form:i.Form,FormItem:i.FormItem,Modal:i.Modal,Icon:i.Icon},data:function(){var t=this;return{loading:!0,modal:!1,modal_loading:!1,modal_temp:{},columns:[{title:"标签名称",key:"name"},{title:"缩略名",key:"slug"},{title:"标签描述",key:"description"},{title:"排序",width:100,align:"center",key:"sort"},{title:"文章数量",key:"count",width:100,align:"center"},{title:"操作",key:"action",width:150,align:"center",render:function(e,o){return e("div",[e("Button",{props:{type:"primary",size:"small",icon:"edit"},style:{marginRight:"5px"},on:{click:function(){t.$router.push({path:"/tag/save",query:{id:o.row.id}})}}}),e("Button",{props:{type:"error",size:"small",icon:"trash-a"},on:{click:function(){t.modal=!0,t.modal_temp={id:o.row.id,index:o.index}}}})])}}],data:[]}},methods:{getList:function(){var t=this;n.f.getList().then(function(e){t.data=e.data,t.loading=!1})},del:function(){var t=this;if(!this.modal_temp.id)return!1;n.f.delete(this.modal_temp.id).then(function(e){t.modal=!1,0==e.errno&&t.getList()})},add:function(){this.$router.push("/tag/save")}},mounted:function(){this.getList()}},l={render:function(){var t=this,e=t.$createElement,o=t._self._c||e;return o("div",[o("Form",{ref:"formInline",attrs:{inline:""}},[o("FormItem",[o("Button",{attrs:{type:"success",icon:"plus"},on:{click:t.add}},[t._v("添加标签")])],1)],1),t._v(" "),o("Table",{attrs:{border:"",loading:t.loading,columns:t.columns,data:t.data}}),t._v(" "),o("Modal",{attrs:{width:"360"},model:{value:t.modal,callback:function(e){t.modal=e},expression:"modal"}},[o("p",{staticStyle:{color:"#f60","text-align":"center"},attrs:{slot:"header"},slot:"header"},[o("Icon",{attrs:{type:"ios-information-circle"}}),t._v(" "),o("span",[t._v("删除确认")])],1),t._v(" "),o("div",{staticStyle:{"text-align":"center"}},[o("p",[t._v("删除后数据将无法找回,是否继续删除?")])]),t._v(" "),o("div",{attrs:{slot:"footer"},slot:"footer"},[o("Button",{attrs:{type:"error",size:"large",long:"",loading:t.modal_loading},on:{click:t.del}},[t._v("确认删除")])],1)])],1)},staticRenderFns:[]},r=o("VU/8")(a,l,!1,null,null,null);e.default=r.exports}}); 2 | //# sourceMappingURL=11.161957e3e3c44fab68db.js.map -------------------------------------------------------------------------------- /www/static/admin/js/17.ede603309e9a3946c3db.js: -------------------------------------------------------------------------------- 1 | webpackJsonp([17],{AGRC:function(t,e,o){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var n=o("gyMJ"),i=o("BTaQ"),a={components:{Button:i.Button,Table:i.Table,Form:i.Form,FormItem:i.FormItem,Modal:i.Modal,Icon:i.Icon},data:function(){var t=this;return{loading:!0,modal:!1,modal_loading:!1,modal_temp:{},columns:[{title:"分类名称",key:"name"},{title:"缩略名",key:"slug"},{title:"分类描述",key:"description"},{title:"排序",width:100,align:"center",key:"sort"},{title:"文章数量",key:"count",width:100,align:"center"},{title:"操作",key:"action",width:150,align:"center",render:function(e,o){return e("div",[e("Button",{props:{type:"primary",size:"small",icon:"edit"},style:{marginRight:"5px"},on:{click:function(){t.$router.push({path:"/category/save",query:{id:o.row.id}})}}}),e("Button",{props:{type:"error",size:"small",icon:"trash-a"},on:{click:function(){t.modal=!0,t.modal_temp={id:o.row.id,index:o.index}}}})])}}],data:[]}},methods:{getList:function(){var t=this;n.a.getList().then(function(e){t.data=e.data,t.loading=!1})},del:function(){var t=this;if(!this.modal_temp.id)return!1;n.a.delete(this.modal_temp.id).then(function(e){t.modal=!1,0==e.errno&&t.getList()})},add:function(){this.$router.push("/category/save")}},mounted:function(){this.getList()}},l={render:function(){var t=this,e=t.$createElement,o=t._self._c||e;return o("div",[o("Form",{ref:"formInline",attrs:{inline:""}},[o("FormItem",[o("Button",{attrs:{type:"success",icon:"plus"},on:{click:t.add}},[t._v("添加分类")])],1)],1),t._v(" "),o("Table",{attrs:{border:"",loading:t.loading,columns:t.columns,data:t.data}}),t._v(" "),o("Modal",{attrs:{width:"360"},model:{value:t.modal,callback:function(e){t.modal=e},expression:"modal"}},[o("p",{staticStyle:{color:"#f60","text-align":"center"},attrs:{slot:"header"},slot:"header"},[o("Icon",{attrs:{type:"ios-information-circle"}}),t._v(" "),o("span",[t._v("删除确认")])],1),t._v(" "),o("div",{staticStyle:{"text-align":"center"}},[o("p",[t._v("删除后数据将无法找回,是否继续删除?")])]),t._v(" "),o("div",{attrs:{slot:"footer"},slot:"footer"},[o("Button",{attrs:{type:"error",size:"large",long:"",loading:t.modal_loading},on:{click:t.del}},[t._v("确认删除")])],1)])],1)},staticRenderFns:[]},r=o("VU/8")(a,l,!1,null,null,null);e.default=r.exports}}); 2 | //# sourceMappingURL=17.ede603309e9a3946c3db.js.map -------------------------------------------------------------------------------- /view/footer.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/view/category/save.vue: -------------------------------------------------------------------------------- 1 | 20 | -------------------------------------------------------------------------------- /admin/src/view/tag/save.vue: -------------------------------------------------------------------------------- 1 | 20 | -------------------------------------------------------------------------------- /www/static/admin/js/14.6c2efb950e8d26ba043e.js: -------------------------------------------------------------------------------- 1 | webpackJsonp([14],{nVCP:function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var o=r("BTaQ"),m=r("gyMJ"),a={components:{Form:o.Form,FormItem:o.FormItem,Button:o.Button,RadioGroup:o.RadioGroup,Radio:o.Radio,Input:o.Input},data:function(){return{formItem:{id:"",text:"",status:99},ruleInline:{author:[{required:!0,message:"作者不能为空"}],email:[{required:!0,message:"邮箱不能为空"},{type:"email",message:"邮箱格式错误"}],url:[{required:!0,message:"地址不能为空"},{type:"url",message:"地址格式错误"}],text:[{required:!0,message:"留言内容不能为空"}]}}},methods:{post:function(){var t=this;this.$refs.formItem.validate(function(e){e?m.b.update(t.formItem.id,t.formItem).then(function(e){0==e.errno&&t.$router.push("/comment/list")}):t.$Message.error("请填写必要信息")})},get:function(t){var e=this;m.b.getInfo(t).then(function(t){e.formItem=t.data})}},mounted:function(){this.$route.query.id&&this.get(this.$route.query.id)}},s={render:function(){var t=this,e=t.$createElement,r=t._self._c||e;return r("Form",{ref:"formItem",attrs:{model:t.formItem,rules:t.ruleInline,"label-width":80}},[r("FormItem",{attrs:{label:"作者",prop:"author"}},[r("Input",{attrs:{type:"text"},model:{value:t.formItem.author,callback:function(e){t.$set(t.formItem,"author",e)},expression:"formItem.author"}})],1),t._v(" "),r("FormItem",{attrs:{label:"邮箱",prop:"email"}},[r("Input",{attrs:{type:"text"},model:{value:t.formItem.email,callback:function(e){t.$set(t.formItem,"email",e)},expression:"formItem.email"}})],1),t._v(" "),r("FormItem",{attrs:{label:"地址",prop:"url"}},[r("Input",{attrs:{type:"text"},model:{value:t.formItem.url,callback:function(e){t.$set(t.formItem,"url",e)},expression:"formItem.url"}})],1),t._v(" "),r("FormItem",{attrs:{label:"状态",prop:"status"}},[r("RadioGroup",{model:{value:t.formItem.status,callback:function(e){t.$set(t.formItem,"status",e)},expression:"formItem.status"}},[r("Radio",{attrs:{label:1}},[t._v("隐藏")]),t._v(" "),r("Radio",{attrs:{label:99}},[t._v("显示")])],1)],1),t._v(" "),r("FormItem",{attrs:{label:"内容",prop:"text"}},[r("Input",{attrs:{type:"textarea",autosize:{minRows:2,maxRows:5},placeholder:"Enter something..."},model:{value:t.formItem.text,callback:function(e){t.$set(t.formItem,"text",e)},expression:"formItem.text"}})],1),t._v(" "),r("FormItem",[r("Button",{attrs:{type:"primary"},on:{click:t.post}},[t._v("提交")])],1)],1)},staticRenderFns:[]},u=r("VU/8")(a,s,!1,null,null,null);e.default=u.exports}}); 2 | //# sourceMappingURL=14.6c2efb950e8d26ba043e.js.map -------------------------------------------------------------------------------- /src/service/upload.js: -------------------------------------------------------------------------------- 1 | module.exports = class extends think.Service { 2 | /** 3 | * 注册HOOK点 4 | * @return {[type]} [description] 5 | */ 6 | static registerHook() { 7 | return { 8 | 'qiniu': ['upload'], 9 | 'cdn': ['upload'] 10 | }; 11 | } 12 | 13 | /** 14 | * cdn 链接处理 15 | * @param {[type]} data [description] 16 | * @return {[type]} [description] 17 | */ 18 | async cdn(data) { 19 | const config = await think.model('config').cache('config').getList(); 20 | if (think.isEmpty(config) || think.isEmpty(config.site) || think.isEmpty(config.site.cdn)) { 21 | return false; 22 | } 23 | data.url = config.site.cdn + data.url; 24 | return true; 25 | } 26 | 27 | /** 28 | * 上传到七牛空间 29 | * @param {[type]} data [description] 30 | * @return {[type]} [description] 31 | */ 32 | async qiniu(data) { 33 | // 暂时不使用 34 | return false; 35 | let QN = require("qiniu"); 36 | const config = await think.model('config').cache('config').getList(); 37 | if (think.isEmpty(config)) { 38 | return false; 39 | } 40 | const qiniu = config.qiniu; 41 | if (think.isEmpty(qiniu)) { 42 | return false; 43 | } 44 | 45 | const bucket = qiniu.bucket; 46 | const accessKey = qiniu.access_key; 47 | const secretKey = qiniu.secret_key; 48 | 49 | const mac = new QN.auth.digest.Mac(accessKey, secretKey); 50 | const putPolicy = new QN.rs.PutPolicy({ scope: bucket }); 51 | const uploadToken = putPolicy.uploadToken(mac); 52 | const qnConfig = new QN.conf.Config(); 53 | const formUploader = new QN.form_up.FormUploader(qnConfig); 54 | const putExtra = new QN.form_up.PutExtra(); 55 | 56 | const localFile = data.filepath; 57 | 58 | return new Promise((resolve, reject) => { 59 | formUploader.putFile(uploadToken, data.basename, localFile, putExtra, (respErr, respBody, respInfo) => { 60 | if (respErr) { 61 | reject(respErr); 62 | } 63 | if (respInfo.statusCode == 200) { 64 | resolve(respBody); 65 | } else { 66 | reject(respBody); 67 | } 68 | }); 69 | }) 70 | 71 | } 72 | 73 | }; -------------------------------------------------------------------------------- /admin/src/view/comment/save.vue: -------------------------------------------------------------------------------- 1 | 26 | 93 | -------------------------------------------------------------------------------- /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 | const createLintingRule = () => ({ 12 | test: /\.(js|vue)$/, 13 | loader: 'eslint-loader', 14 | enforce: 'pre', 15 | include: [resolve('src'), resolve('test')], 16 | options: { 17 | formatter: require('eslint-friendly-formatter'), 18 | emitWarning: !config.dev.showEslintErrorsInOverlay 19 | } 20 | }) 21 | 22 | module.exports = { 23 | context: path.resolve(__dirname, '../'), 24 | entry: { 25 | app: './src/main.js' 26 | }, 27 | output: { 28 | path: config.build.assetsRoot, 29 | filename: '[name].js', 30 | publicPath: process.env.NODE_ENV === 'production' 31 | ? config.build.assetsPublicPath 32 | : config.dev.assetsPublicPath 33 | }, 34 | resolve: { 35 | extensions: ['.js', '.vue', '.json'], 36 | alias: { 37 | 'vue$': 'vue/dist/vue.esm.js', 38 | '@': resolve('src'), 39 | } 40 | }, 41 | module: { 42 | rules: [ 43 | ...(config.dev.useEslint ? [createLintingRule()] : []), 44 | { 45 | test: /\.vue$/, 46 | loader: 'vue-loader', 47 | options: vueLoaderConfig 48 | }, 49 | { 50 | test: /\.js$/, 51 | loader: 'babel-loader', 52 | include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')] 53 | }, 54 | { 55 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 56 | loader: 'url-loader', 57 | options: { 58 | limit: 10000, 59 | name: utils.assetsPath('img/[name].[hash:7].[ext]') 60 | } 61 | }, 62 | { 63 | test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, 64 | loader: 'url-loader', 65 | options: { 66 | limit: 10000, 67 | name: utils.assetsPath('media/[name].[hash:7].[ext]') 68 | } 69 | }, 70 | { 71 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 72 | loader: 'url-loader', 73 | options: { 74 | limit: 10000, 75 | name: utils.assetsPath('fonts/[name].[hash:7].[ext]') 76 | } 77 | } 78 | ] 79 | }, 80 | node: { 81 | // prevent webpack from injecting useless setImmediate polyfill because Vue 82 | // source contains it (although only uses it if it's native). 83 | setImmediate: false, 84 | // prevent webpack from injecting mocks to Node native modules 85 | // that does not make sense for the client 86 | dgram: 'empty', 87 | fs: 'empty', 88 | net: 'empty', 89 | tls: 'empty', 90 | child_process: 'empty' 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /admin/src/view/common/login.vue: -------------------------------------------------------------------------------- 1 | 28 | 96 | -------------------------------------------------------------------------------- /www/static/admin/js/12.5c3f7e3441ea03f55ae9.js: -------------------------------------------------------------------------------- 1 | webpackJsonp([12],{LBHn:function(t,e,a){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var n=a("gyMJ"),o=a("BTaQ"),i={components:{Button:o.Button,Table:o.Table,Page:o.Page,Form:o.Form,FormItem:o.FormItem,Input:o.Input,Modal:o.Modal,Icon:o.Icon},data:function(){var t=this;return{loading:!0,modal:!1,modal_loading:!1,modal_temp:{},columns:[{title:"页面名称",key:"title"},{title:"阅读量",key:"view",width:100,align:"center"},{title:"发布时间",key:"create_time",width:200,align:"center",render:function(t,e){return e.row.create_time?t("span",new Date(1e3*e.row.create_time).toLocaleString()):""}},{title:"操作",key:"action",width:150,align:"center",render:function(e,a){return e("div",[e("Button",{props:{type:"primary",size:"small",icon:"edit"},style:{marginRight:"5px"},on:{click:function(){t.$router.push({path:"/page/save",query:{slug:a.row.slug}})}}}),e("Button",{props:{type:"error",size:"small",icon:"trash-a"},on:{click:function(){t.modal=!0,t.modal_temp={id:a.row.id,index:a.index}}}})])}}],data:{},map:{page:1,key:"",all:1,pageSize:10,contentType:"page"}}},methods:{getList:function(){var t=this;this.loading=!0,n.d.getList(this.map).then(function(e){t.data=e.data,t.loading=!1})},del:function(){var t=this;if(!this.modal_temp.id)return!1;n.d.delete(this.modal_temp.id).then(function(e){t.modal=!1,0==e.errno?(t.getList(),t.$Message.success(e.errmsg)):t.$Message.error(e.errmsg)})},changePage:function(t){this.map.page=t,this.getList()},add:function(){this.$router.push("/page/save")}},mounted:function(){this.getList()}},r={render:function(){var t=this,e=t.$createElement,a=t._self._c||e;return a("div",[a("Form",{ref:"formInline",attrs:{model:t.map,inline:""}},[a("div",{staticClass:"search"},[a("Button",{staticClass:"fl",attrs:{type:"success",icon:"plus"},on:{click:t.add}},[t._v("添加页面")]),t._v(" "),a("FormItem",[a("Input",{attrs:{type:"text",placeholder:"关键字"},model:{value:t.map.key,callback:function(e){t.$set(t.map,"key",e)},expression:"map.key"}})],1),t._v(" "),a("FormItem",[a("Button",{attrs:{type:"primary"},on:{click:t.getList}},[t._v("查询")])],1)],1)]),t._v(" "),a("Table",{attrs:{border:"",loading:t.loading,columns:t.columns,data:t.data.data}}),t._v(" "),a("Page",{staticClass:"page",attrs:{total:t.data.count,"page-size":t.data.pagesize,"show-total":""},on:{"on-change":t.changePage}}),t._v(" "),a("Modal",{attrs:{width:"360"},model:{value:t.modal,callback:function(e){t.modal=e},expression:"modal"}},[a("p",{staticStyle:{color:"#f60","text-align":"center"},attrs:{slot:"header"},slot:"header"},[a("Icon",{attrs:{type:"ios-information-circle"}}),t._v(" "),a("span",[t._v("删除确认")])],1),t._v(" "),a("div",{staticStyle:{"text-align":"center"}},[a("p",[t._v("删除后数据将无法找回,是否继续删除?")])]),t._v(" "),a("div",{attrs:{slot:"footer"},slot:"footer"},[a("Button",{attrs:{type:"error",size:"large",long:"",loading:t.modal_loading},on:{click:t.del}},[t._v("确认删除")])],1)])],1)},staticRenderFns:[]},l=a("VU/8")(i,r,!1,null,null,null);e.default=l.exports}}); 2 | //# sourceMappingURL=12.5c3f7e3441ea03f55ae9.js.map -------------------------------------------------------------------------------- /src/service/email.js: -------------------------------------------------------------------------------- 1 | module.exports = class extends think.Service { 2 | /** 3 | * 注册HOOK点 4 | * @return {[type]} [description] 5 | */ 6 | static registerHook() { 7 | return { 8 | 'comment': ['commentCreate'] 9 | }; 10 | } 11 | 12 | /** 13 | * 评论邮件提醒 14 | * @param {[type]} data [description] 15 | * @return {[type]} [description] 16 | */ 17 | async comment(data) { 18 | const config = await think.model('config').cache('config').getList(); 19 | if (think.isEmpty(config)) { 20 | return false; 21 | } 22 | const email = config.email; 23 | if (think.isEmpty(email)) { 24 | return false; 25 | } 26 | 27 | const transport = { 28 | service: email.host, 29 | port: email.port, 30 | secure: !think.isEmpty(email.secure), 31 | auth: { 32 | user: email.user, 33 | pass: email.pass 34 | } 35 | }; 36 | const date = think.datetime(data.create_time * 1000); 37 | const options = {}; 38 | options.from = email.user; 39 | 40 | // 给自己发送一份 41 | if (data.email !== email.user) { 42 | options.to = email.user; 43 | options.subject = '[' + data.content.title + '] 有新的评论'; 44 | options.html = `
45 |

${config.site.title}:${data.content.title} 有新的评论

46 |

${data.author} 说:${data.text}

47 |

时间:${date}
IP:${data.ip}
邮箱:${data.email} [管理评论]

48 |
`; 49 | think.sendEmail(transport, options); 50 | } 51 | 52 | // 给被回复者发送一份 53 | if (data.parent_id) { 54 | const parent = await think.model('comment').where({ id: data.parent_id }).find(); 55 | if (parent.email !== email.user) { 56 | options.to = parent.email; 57 | options.subject = '您在 [' + data.content.title + '] 的评论有了回复 - ' + config.site.url; 58 | options.html = `
59 |

${data.author}:${data.content.title} 有新的回复

60 |

${data.author} 说:${data.text}

61 |

你的评论:${parent.text}

62 |

时间:${date}

63 |
`; 64 | think.sendEmail(transport, options); 65 | } 66 | } 67 | } 68 | }; 69 | -------------------------------------------------------------------------------- /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 | '/api': { 15 | target: 'http://127.0.0.1:8360', 16 | changeOrigin:true, 17 | pathRewrite:{ 18 | //'^/api':'' 19 | } 20 | }, 21 | '/upload': { 22 | target: 'http://127.0.0.1:8360', 23 | changeOrigin:true, 24 | pathRewrite:{ 25 | //'^/upload':'' 26 | } 27 | } 28 | }, 29 | 30 | // Various Dev Server settings 31 | host: 'localhost', // can be overwritten by process.env.HOST 32 | port: 8080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined 33 | autoOpenBrowser: false, 34 | errorOverlay: true, 35 | notifyOnErrors: true, 36 | poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions- 37 | 38 | // Use Eslint Loader? 39 | // If true, your code will be linted during bundling and 40 | // linting errors and warnings will be shown in the console. 41 | useEslint: false, 42 | // If true, eslint errors and warnings will also be shown in the error overlay 43 | // in the browser. 44 | showEslintErrorsInOverlay: false, 45 | 46 | /** 47 | * Source Maps 48 | */ 49 | 50 | // https://webpack.js.org/configuration/devtool/#development 51 | devtool: 'cheap-module-eval-source-map', 52 | 53 | // If you have problems debugging vue-files in devtools, 54 | // set this to false - it *may* help 55 | // https://vue-loader.vuejs.org/en/options.html#cachebusting 56 | cacheBusting: true, 57 | 58 | cssSourceMap: true 59 | }, 60 | 61 | build: { 62 | // Template for index.html 63 | index: path.resolve(__dirname, '../../www/admin.html'), 64 | 65 | // Paths 66 | assetsRoot: path.resolve(__dirname, '../../www/static/admin'), 67 | assetsSubDirectory: '', 68 | assetsPublicPath: '/static/admin/', 69 | 70 | /** 71 | * Source Maps 72 | */ 73 | 74 | productionSourceMap: true, 75 | // https://webpack.js.org/configuration/devtool/#production 76 | devtool: '#source-map', 77 | 78 | // Gzip off by default as many popular static hosts such as 79 | // Surge or Netlify already gzip all static assets for you. 80 | // Before setting to `true`, make sure to: 81 | // npm install --save-dev compression-webpack-plugin 82 | productionGzip: false, 83 | productionGzipExtensions: ['js', 'css'], 84 | 85 | // Run the build command with an extra argument to 86 | // View the bundle analyzer report after build finishes: 87 | // `npm run build --report` 88 | // Set to `true` or `false` to always turn it on or off 89 | bundleAnalyzerReport: process.env.npm_config_report 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /www/static/admin/js/13.509c60b02980c8a0329c.js: -------------------------------------------------------------------------------- 1 | webpackJsonp([13],{DShp:function(t,e,a){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var n=a("gyMJ"),o=a("BTaQ"),i={components:{Button:o.Button,Table:o.Table,Page:o.Page,Form:o.Form,FormItem:o.FormItem,Input:o.Input,Modal:o.Modal,Icon:o.Icon},data:function(){var t=this;return{loading:!0,modal:!1,modal_loading:!1,modal_temp:{},columns:[{title:"文章名称",key:"title"},{title:"分类",key:"category",render:function(t,e){return t("span",e.row.category.name)}},{title:"阅读量",key:"view",width:100,align:"center"},{title:"发布时间",key:"create_time",width:200,align:"center",render:function(t,e){return e.row.create_time?t("span",new Date(1e3*e.row.create_time).toLocaleString()):""}},{title:"操作",key:"action",width:150,align:"center",render:function(e,a){return e("div",[e("Button",{props:{type:"primary",size:"small",icon:"edit"},style:{marginRight:"5px"},on:{click:function(){t.$router.push({path:"/content/save",query:{slug:a.row.slug}})}}}),e("Button",{props:{type:"error",size:"small",icon:"trash-a"},on:{click:function(){t.modal=!0,t.modal_temp={id:a.row.id,index:a.index}}}})])}}],data:{},map:{page:1,key:"",all:1,pageSize:10,contentType:"post"}}},methods:{getList:function(){var t=this;this.loading=!0,n.d.getList(this.map).then(function(e){t.data=e.data,t.loading=!1})},del:function(){var t=this;if(!this.modal_temp.id)return!1;n.d.delete(this.modal_temp.id).then(function(e){t.modal=!1,0==e.errno?(t.getList(),t.$Message.success(e.errmsg)):t.$Message.error(e.errmsg)})},changePage:function(t){this.map.page=t,this.getList()},add:function(){this.$router.push("/content/save")}},mounted:function(){this.getList()}},r={render:function(){var t=this,e=t.$createElement,a=t._self._c||e;return a("div",[a("Form",{ref:"formInline",attrs:{model:t.map,inline:""}},[a("div",{staticClass:"search"},[a("Button",{staticClass:"fl",attrs:{type:"success",icon:"plus"},on:{click:t.add}},[t._v("发布文章")]),t._v(" "),a("FormItem",[a("Input",{attrs:{type:"text",placeholder:"关键字"},model:{value:t.map.key,callback:function(e){t.$set(t.map,"key",e)},expression:"map.key"}})],1),t._v(" "),a("FormItem",[a("Button",{attrs:{type:"primary"},on:{click:t.getList}},[t._v("查询")])],1)],1)]),t._v(" "),a("Table",{attrs:{border:"",loading:t.loading,columns:t.columns,data:t.data.data}}),t._v(" "),a("Page",{staticClass:"page",attrs:{total:t.data.count,"page-size":t.data.pagesize,"show-total":""},on:{"on-change":t.changePage}}),t._v(" "),a("Modal",{attrs:{width:"360"},model:{value:t.modal,callback:function(e){t.modal=e},expression:"modal"}},[a("p",{staticStyle:{color:"#f60","text-align":"center"},attrs:{slot:"header"},slot:"header"},[a("Icon",{attrs:{type:"ios-information-circle"}}),t._v(" "),a("span",[t._v("删除确认")])],1),t._v(" "),a("div",{staticStyle:{"text-align":"center"}},[a("p",[t._v("删除后数据将无法找回,是否继续删除?")])]),t._v(" "),a("div",{attrs:{slot:"footer"},slot:"footer"},[a("Button",{attrs:{type:"error",size:"large",long:"",loading:t.modal_loading},on:{click:t.del}},[t._v("确认删除")])],1)])],1)},staticRenderFns:[]},l=a("VU/8")(i,r,!1,null,null,null);e.default=l.exports}}); 2 | //# sourceMappingURL=13.509c60b02980c8a0329c.js.map -------------------------------------------------------------------------------- /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/router/router.js: -------------------------------------------------------------------------------- 1 | // 登录 2 | export const loginRouter = { 3 | path: '/login', 4 | name: 'login', 5 | meta: { 6 | title: 'Login - 登录' 7 | }, 8 | component: resolve => { require(['@/view/common/login.vue'], resolve) } 9 | } 10 | 11 | // 404 12 | export const page404 = { 13 | path: '/*', 14 | name: 'error-404', 15 | meta: { 16 | title: '404 - 页面不存在' 17 | }, 18 | component: resolve => { require(['@/view/common/404.vue'], resolve) } 19 | } 20 | 21 | // 后台相关路由 22 | export const adminRouter = [{ 23 | path: '/', 24 | name: 'Admin', 25 | meta: { 26 | title: '后台', 27 | requiresAuth: true 28 | }, 29 | redirect: '/home', 30 | component: resolve => { require(['@/view/Main.vue'], resolve) }, 31 | children: [ 32 | { path: 'home', name: 'Home', meta: { title: '控制台', requiresAuth: true }, component: resolve => { require(['@/view/home.vue'], resolve) } }, 33 | { path: 'content/save', name: 'ContentSave', meta: { title: '内容编辑', requiresAuth: true }, component: resolve => { require(['@/view/content/save.vue'], resolve) } }, 34 | { path: 'content/list', name: 'ContentList', meta: { title: '内容列表', requiresAuth: true }, component: resolve => { require(['@/view/content/list.vue'], resolve) } }, 35 | { path: 'page/save', name: 'ContentSave', meta: { title: '页面编辑', requiresAuth: true }, component: resolve => { require(['@/view/page/save.vue'], resolve) } }, 36 | { path: 'page/list', name: 'ContentList', meta: { title: '页面列表', requiresAuth: true }, component: resolve => { require(['@/view/page/list.vue'], resolve) } }, 37 | { path: 'category/save', name: 'CategorySave', meta: { title: '分类编辑', requiresAuth: true }, component: resolve => { require(['@/view/category/save.vue'], resolve) } }, 38 | { path: 'category/list', name: 'CategoryList', meta: { title: '分类列表', requiresAuth: true }, component: resolve => { require(['@/view/category/list.vue'], resolve) } }, 39 | { path: 'comment/save', name: 'CommentSave', meta: { title: '分类编辑', requiresAuth: true }, component: resolve => { require(['@/view/comment/save.vue'], resolve) } }, 40 | { path: 'comment/list', name: 'CommentList', meta: { title: '分类列表', requiresAuth: true }, component: resolve => { require(['@/view/comment/list.vue'], resolve) } }, 41 | { path: 'tag/save', name: 'TagSave', meta: { title: '标签编辑', requiresAuth: true }, component: resolve => { require(['@/view/tag/save.vue'], resolve) } }, 42 | { path: 'tag/list', name: 'TagList', meta: { title: '标签列表', requiresAuth: true }, component: resolve => { require(['@/view/tag/list.vue'], resolve) } }, 43 | { path: 'user/info', name: 'UserInfo', meta: { title: '个人资料', requiresAuth: true }, component: resolve => { require(['@/view/user/info.vue'], resolve) } }, 44 | { path: 'user/password', name: 'UserPassword', meta: { title: '修改密码', requiresAuth: true }, component: resolve => { require(['@/view/user/password.vue'], resolve) } }, 45 | { path: 'config', name: 'System', meta: { title: '系统设置', requiresAuth: true }, component: resolve => { require(['@/view/config/save.vue'], resolve) } } 46 | ] 47 | }] 48 | 49 | // 所有上面定义的路由全部写在一起 50 | export const routers = [ 51 | loginRouter, 52 | ...adminRouter, 53 | page404 54 | ] 55 | -------------------------------------------------------------------------------- /www/static/admin/js/5.abfb113cf452a5c6cdb0.js: -------------------------------------------------------------------------------- 1 | webpackJsonp([5],{"3Pax":function(e,t){},oEX0:function(e,t,a){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var l=a("gyMJ"),s=a("BTaQ"),i={components:{Form:s.Form,FormItem:s.FormItem,Input:s.Input,Button:s.Button,Tabs:s.Tabs,TabPane:s.TabPane,RadioGroup:s.RadioGroup,Radio:s.Radio},data:function(){return{site:{},email:{secure:1},qiniu:{}}},methods:{get:function(){var e=this;l.c.getList().then(function(t){t.data.site&&(e.site=t.data.site),t.data.email&&(e.email=t.data.email),t.data.qiniu&&(e.qiniu=t.data.qiniu)})},post:function(e,t){console.log(e),l.c.update(e,t).then(function(e){})}},mounted:function(){this.get()}},o={render:function(){var e=this,t=e.$createElement,a=e._self._c||t;return a("Tabs",[a("TabPane",{attrs:{label:"站点设置"}},[a("Form",{attrs:{model:e.site,"label-width":80}},[a("FormItem",{attrs:{label:"站点名称"}},[a("Input",{model:{value:e.site.title,callback:function(t){e.$set(e.site,"title",t)},expression:"site.title"}})],1),e._v(" "),a("FormItem",{attrs:{label:"站点地址"}},[a("Input",{model:{value:e.site.url,callback:function(t){e.$set(e.site,"url",t)},expression:"site.url"}})],1),e._v(" "),a("FormItem",{attrs:{label:"cdn域名"}},[a("Input",{model:{value:e.site.cdn,callback:function(t){e.$set(e.site,"cdn",t)},expression:"site.cdn"}})],1),e._v(" "),a("FormItem",{attrs:{label:"关键词"}},[a("Input",{model:{value:e.site.keywords,callback:function(t){e.$set(e.site,"keywords",t)},expression:"site.keywords"}})],1),e._v(" "),a("FormItem",{attrs:{label:"站点描述"}},[a("Input",{model:{value:e.site.description,callback:function(t){e.$set(e.site,"description",t)},expression:"site.description"}})],1),e._v(" "),a("FormItem",{attrs:{label:"底部说明"}},[a("Input",{attrs:{type:"textarea"},model:{value:e.site.footer,callback:function(t){e.$set(e.site,"footer",t)},expression:"site.footer"}})],1),e._v(" "),a("FormItem",[a("Button",{attrs:{type:"primary"},on:{click:function(t){return e.post("site",e.site)}}},[e._v("保存")])],1)],1)],1),e._v(" "),a("TabPane",{attrs:{label:"邮箱设置"}},[a("Form",{attrs:{model:e.email,"label-width":80}},[a("FormItem",{attrs:{label:"服务器"}},[a("Input",{model:{value:e.email.host,callback:function(t){e.$set(e.email,"host",t)},expression:"email.host"}})],1),e._v(" "),a("FormItem",{attrs:{label:"端口"}},[a("Input",{model:{value:e.email.port,callback:function(t){e.$set(e.email,"port",t)},expression:"email.port"}})],1),e._v(" "),a("FormItem",{attrs:{label:"安全模式"}},[a("RadioGroup",{model:{value:e.email.secure,callback:function(t){e.$set(e.email,"secure",t)},expression:"email.secure"}},[a("Radio",{attrs:{label:0}},[e._v("否")]),e._v(" "),a("Radio",{attrs:{label:1}},[e._v("是")])],1)],1),e._v(" "),a("FormItem",{attrs:{label:"账号"}},[a("Input",{model:{value:e.email.user,callback:function(t){e.$set(e.email,"user",t)},expression:"email.user"}})],1),e._v(" "),a("FormItem",{attrs:{label:"密码"}},[a("Input",{model:{value:e.email.pass,callback:function(t){e.$set(e.email,"pass",t)},expression:"email.pass"}})],1),e._v(" "),a("FormItem",[a("Button",{attrs:{type:"primary"},on:{click:function(t){return e.post("email",e.email)}}},[e._v("保存")])],1)],1)],1)],1)},staticRenderFns:[]};var n=a("VU/8")(i,o,!1,function(e){a("3Pax")},null,null);t.default=n.exports}}); 2 | //# sourceMappingURL=5.abfb113cf452a5c6cdb0.js.map -------------------------------------------------------------------------------- /src/model/content.js: -------------------------------------------------------------------------------- 1 | module.exports = class extends think.Model { 2 | // 模型关联 3 | get relation() { 4 | return { 5 | category: { 6 | type: think.Model.BELONG_TO, 7 | model: 'meta', 8 | key: 'category_id', 9 | fKey: 'id', 10 | field: 'id,name,slug,description,count' 11 | }, 12 | tag: { 13 | type: think.Model.MANY_TO_MANY, 14 | model: 'meta', 15 | rModel: 'relationship', 16 | rfKey: 'meta_id', 17 | key: 'id', 18 | fKey: 'content_id', 19 | field: 'id,name,slug,description,count' 20 | }, 21 | comment: { 22 | type: think.Model.HAS_MANY, 23 | key: 'id', 24 | fKey: 'content_id', 25 | where: 'status=99', 26 | order: 'create_time desc' 27 | }, 28 | user: { 29 | type: think.Model.BELONG_TO, 30 | model: 'user', 31 | key: 'user_id', 32 | fKey: 'id', 33 | field: 'id,username,email,qq,github,weibo,zhihu' 34 | } 35 | }; 36 | } 37 | 38 | // 添加文章 39 | async insert(data) { 40 | const tag = data.tag; 41 | delete data.tag; 42 | data = this.parseContent(data); 43 | const id = await this.add(data); 44 | if (id) { 45 | // 添加标签关系 46 | const tagData = []; 47 | for (var i in tag) { 48 | tagData.push({ 49 | content_id: id, 50 | meta_id: tag[i] 51 | }); 52 | } 53 | think.model('relationship').addMany(tagData); 54 | // 更新文章数量 55 | this.updateCount(data.category_id, tag); 56 | } 57 | return id; 58 | } 59 | 60 | // 更新文章 61 | async save(id, data) { 62 | // 查询修改前数据 63 | const oldData = await this.where({ id: id }).find(); 64 | // 修改分类统计 65 | if (oldData.category_id !== data.category_id) { 66 | think.model('meta').where({ id: oldData.category_id }).decrement('count'); 67 | } 68 | 69 | // 更新数据 70 | data = this.parseContent(data); 71 | data.id = id; 72 | const res = await this.where({ id: data.id }).update(data); 73 | 74 | if (res) { 75 | //更新文章数量 76 | this.updateCount(data.category_id, data.tag); 77 | } 78 | 79 | return res; 80 | } 81 | 82 | // 处理内容,生成文章简介 83 | parseContent(data) { 84 | // 描述处理 85 | if (data.content.indexOf('') > -1) { 86 | data.description = data.content.split('')[0]; // 写文章内容时,description部分是前面的部分,要自己写 87 | } else { 88 | data.description = ''; 89 | } 90 | // 唯一标识处理 91 | if (!data.slug) { 92 | data.slug = think.md5(new Date()); 93 | } 94 | return data; 95 | } 96 | 97 | // 更新文章数量 98 | async updateCount(categoryId, tagData) { 99 | // 更新分类数量 100 | const categoryCount = await this.where({ category_id: categoryId }).count(); 101 | think.model('meta').where({ id: categoryId }).update({ count: categoryCount }); 102 | // 更新标签数量 103 | for (var i in tagData) { 104 | const tagCount = await think.model('relationship').where({ meta_id: tagData[i] }).count(); 105 | think.model('meta').where({ id: tagData[i] }).update({ count: tagCount }); 106 | } 107 | } 108 | }; 109 | -------------------------------------------------------------------------------- /www/static/admin/js/15.f0a63d8001465aac2051.js: -------------------------------------------------------------------------------- 1 | webpackJsonp([15],{XfaP:function(t,e,a){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var n=a("gyMJ"),o=a("BTaQ"),r={components:{Button:o.Button,Table:o.Table,Page:o.Page,Form:o.Form,FormItem:o.FormItem,Input:o.Input,Modal:o.Modal,Icon:o.Icon},data:function(){var t=this;return{loading:!0,modal:!1,modal_loading:!1,modal_temp:{},columns:[{title:"文章名称",key:"title",render:function(t,e){return t("a",{attrs:{href:"/"+e.row.category+"/"+e.row.slug+".html",target:"_blank"}},e.row.title)}},{title:"作者",key:"title",render:function(t,e){return t("a",{attrs:{href:e.row.url,target:"_blank"}},e.row.author)}},{title:"IP",key:"ip",render:function(t,e){return t("span",e.row.ip)}},{title:"邮箱",key:"email",render:function(t,e){return t("span",e.row.email)}},{title:"评论",key:"text"},{title:"显示",key:"view",width:100,align:"center",render:function(t,e){return t("Icon",{props:{type:99==e.row.status?"checkmark":"close"},style:{color:99==e.row.status?"#19be6b":"#ed3f14"}})}},{title:"时间",key:"create_time",width:200,align:"center",render:function(t,e){return e.row.create_time?t("span",new Date(1e3*e.row.create_time).toLocaleString()):""}},{title:"操作",key:"action",width:150,align:"center",render:function(e,a){return e("div",[e("Button",{props:{type:"primary",size:"small",icon:"edit"},style:{marginRight:"5px"},on:{click:function(){t.$router.push({path:"/comment/save",query:{id:a.row.id}})}}}),e("Button",{props:{type:"error",size:"small",icon:"trash-a"},on:{click:function(){t.modal=!0,t.modal_temp={id:a.row.id,index:a.index}}}})])}}],data:{},map:{page:1,key:"",all:1,pageSize:10}}},methods:{getList:function(){var t=this;this.loading=!0,n.b.getList(this.map).then(function(e){t.data=e.data,t.loading=!1})},del:function(){var t=this;if(!this.modal_temp.id)return!1;n.b.delete(this.modal_temp.id).then(function(e){t.modal=!1,0==e.errno&&t.getList()})},changePage:function(t){this.map.page=t,this.getList()}},mounted:function(){this.getList()}},i={render:function(){var t=this,e=t.$createElement,a=t._self._c||e;return a("div",[a("Form",{ref:"formInline",staticClass:"search",attrs:{model:t.map,inline:""}},[a("FormItem",[a("Input",{attrs:{type:"text",placeholder:"关键字"},model:{value:t.map.key,callback:function(e){t.$set(t.map,"key",e)},expression:"map.key"}})],1),t._v(" "),a("FormItem",[a("Button",{attrs:{type:"primary"},on:{click:t.getList}},[t._v("查询")])],1)],1),t._v(" "),a("Table",{attrs:{border:"",loading:t.loading,columns:t.columns,data:t.data.data}}),t._v(" "),a("Page",{staticClass:"page",attrs:{total:t.data.count,"page-size":t.data.pagesize,"show-total":""},on:{"on-change":t.changePage}}),t._v(" "),a("Modal",{attrs:{width:"360"},model:{value:t.modal,callback:function(e){t.modal=e},expression:"modal"}},[a("p",{staticStyle:{color:"#f60","text-align":"center"},attrs:{slot:"header"},slot:"header"},[a("Icon",{attrs:{type:"ios-information-circle"}}),t._v(" "),a("span",[t._v("删除确认")])],1),t._v(" "),a("div",{staticStyle:{"text-align":"center"}},[a("p",[t._v("删除后数据将无法找回,是否继续删除?")])]),t._v(" "),a("div",{attrs:{slot:"footer"},slot:"footer"},[a("Button",{attrs:{type:"error",size:"large",long:"",loading:t.modal_loading},on:{click:t.del}},[t._v("确认删除")])],1)])],1)},staticRenderFns:[]},l=a("VU/8")(r,i,!1,null,null,null);e.default=l.exports}}); 2 | //# sourceMappingURL=15.f0a63d8001465aac2051.js.map -------------------------------------------------------------------------------- /src/config/adapter.js: -------------------------------------------------------------------------------- 1 | const fileCache = require('think-cache-file'); 2 | const JWTSession = require('think-session-jwt'); 3 | const mysql = require('think-model-mysql'); 4 | const { Console, File, DateFile } = require('think-logger3'); 5 | const path = require('path'); 6 | const isDev = think.env === 'development'; 7 | const ejs = require('think-view-ejs'); 8 | 9 | /** 10 | * cache adapter config 11 | * @type {Object} 12 | */ 13 | exports.cache = { 14 | type: 'file', 15 | common: { 16 | timeout: 24 * 60 * 60 * 1000 // millisecond 17 | }, 18 | file: { 19 | handle: fileCache, 20 | cachePath: path.join(think.ROOT_PATH, 'runtime/cache'), // absoulte path is necessarily required 21 | pathDepth: 1, 22 | gcInterval: 24 * 60 * 60 * 1000 // gc interval 23 | } 24 | }; 25 | 26 | /** 27 | * model adapter config 28 | * @type {Object} 29 | */ 30 | exports.model = { 31 | type: 'mysql', 32 | common: { 33 | logConnect: isDev, 34 | logSql: isDev, 35 | logger: msg => think.logger.info(msg) 36 | }, 37 | mysql: { 38 | handle: mysql, 39 | database: process.env.MYSQL_DATABASE || 'blog', 40 | prefix: 'ls_', 41 | encoding: 'utf8', 42 | host: process.env.MYSQL_HOST || '127.0.0.1', 43 | port: '', 44 | user: process.env.MYSQL_USERNAME || 'root', 45 | password: process.env.MYSQL_PASSWORD || '123456', 46 | dateStrings: true 47 | } 48 | }; 49 | 50 | /** 51 | * session adapter config 52 | * @type {Object} 53 | */ 54 | exports.session = { 55 | type: 'jwt', 56 | common: { 57 | cookie: { 58 | name: 'thinkjs' 59 | } 60 | }, 61 | jwt: { 62 | handle: JWTSession, 63 | secret: 'lscho', // secret is reqired 64 | tokenType: 'header', // ['query', 'body', 'header', 'cookie'], 'cookie' is default 65 | tokenName: 'authorization', // if tokenType not 'cookie', this will be token name, 'jwt' is default 66 | sign: { 67 | expiresIn: 60 * 60 * 12 68 | }, 69 | verify: { 70 | // verify options is not required 71 | } 72 | } 73 | }; 74 | 75 | /** 76 | * view adapter config 77 | * @type {Object} 78 | */ 79 | exports.view = { 80 | type: 'ejs', 81 | common: { 82 | viewPath: path.join(think.ROOT_PATH, isDev ? 'view' : 'runtime/view'), 83 | extname: '.html', 84 | sep: '_' // seperator between controller and action 85 | }, 86 | ejs: { 87 | // options 88 | handle: ejs, 89 | beforeRender: (ejs, handleOptions) => { 90 | // do something before render the template. 91 | } 92 | } 93 | }; 94 | 95 | /** 96 | * logger adapter config 97 | * @type {Object} 98 | */ 99 | exports.logger = { 100 | type: isDev ? 'console' : 'dateFile', 101 | console: { 102 | handle: Console 103 | }, 104 | file: { 105 | handle: File, 106 | backups: 10, // max chunk number 107 | absolute: true, 108 | maxLogSize: 50 * 1024, // 50M 109 | filename: path.join(think.ROOT_PATH, 'logs/app.log') 110 | }, 111 | dateFile: { 112 | handle: DateFile, 113 | level: 'ALL', 114 | absolute: true, 115 | pattern: '-yyyy-MM-dd', 116 | alwaysIncludePattern: true, 117 | filename: path.join(think.ROOT_PATH, 'logs/app.log') 118 | } 119 | }; 120 | -------------------------------------------------------------------------------- /src/controller/rest.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | 3 | module.exports = class extends think.Controller { 4 | constructor(ctx) { 5 | super(ctx); 6 | this.resource = this.getResource(); 7 | this.id = this.getId(); 8 | assert(think.isFunction(this.model), 'this.model must be a function'); 9 | this.modelInstance = this.model(this.resource); 10 | } 11 | 12 | async __before(action) { 13 | this.header('Access-Control-Allow-Origin', '*'); 14 | 15 | this.userInfo = await this.session('userInfo').catch(_ => ({})); 16 | 17 | const isAllowedMethod = this.isMethod('GET'); 18 | const isAllowedResource = this.resource === 'token'; 19 | const isLogin = !think.isEmpty(this.userInfo); 20 | 21 | if(!isAllowedMethod && !isAllowedResource && !isLogin) { 22 | return this.ctx.throw(401, '请登录后操作'); 23 | } 24 | } 25 | 26 | getResource() { 27 | return this.ctx.controller.split('/').slice(-1)[0]; 28 | } 29 | 30 | getId() { 31 | const id = this.get('id'); 32 | if (id && (think.isString(id) || think.isNumber(id))) { 33 | return id; 34 | } 35 | const last = this.ctx.path.split('/').slice(-1)[0]; 36 | if (last !== this.resource) { 37 | return last; 38 | } 39 | return ''; 40 | } 41 | 42 | async getAction() { 43 | let data; 44 | const pk = this.modelInstance.pk; 45 | if (this.id) { 46 | data = await this.modelInstance.where({ [pk]: this.id }).find(); 47 | return this.success(data); 48 | } 49 | data = await this.modelInstance.order(pk + ' desc').select(); 50 | return this.success(data); 51 | } 52 | 53 | async postAction() { 54 | const pk = this.modelInstance.pk; 55 | const data = this.post(); 56 | delete data[pk]; 57 | if (think.isEmpty(data)) { 58 | return this.fail('data is empty'); 59 | } 60 | const insertId = await this.modelInstance.add(data); 61 | if (insertId) { 62 | data[pk] = insertId; 63 | await this.hook(this.resource + 'Create', data); 64 | } else { 65 | return this.success({ id: insertId }); 66 | } 67 | } 68 | 69 | async deleteAction() { 70 | if (!this.id) { 71 | return this.fail('params error'); 72 | } 73 | const pk = this.modelInstance.pk; 74 | const rows = await this.modelInstance.where({ [pk]: this.id }).delete(); 75 | if (rows) { 76 | await this.hook(this.resource + 'Delete', {[pk]: this.id}); 77 | return this.success({ affectedRows: rows }, '删除成功'); 78 | } else { 79 | return this.fail(1000, '删除失败'); 80 | } 81 | } 82 | 83 | async putAction() { 84 | if (!this.id) { 85 | return this.fail('params error'); 86 | } 87 | const pk = this.modelInstance.pk; 88 | const data = this.post(); 89 | data[pk] = this.id; 90 | if (think.isEmpty(data)) { 91 | return this.fail('data is empty'); 92 | } 93 | const rows = await this.modelInstance.where({ [pk]: this.id }).update(data); 94 | if (rows) { 95 | await this.hook(this.resource + 'Update', data); 96 | return this.success({ affectedRows: rows }, '更新成功'); 97 | } else { 98 | return this.fail(1000, '更新失败'); 99 | } 100 | } 101 | 102 | __call() { 103 | 104 | } 105 | }; 106 | -------------------------------------------------------------------------------- /admin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "admin", 3 | "version": "1.0.0", 4 | "description": "A Vue.js project", 5 | "author": "lscho ", 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 | "e2e": "node test/e2e/runner.js", 12 | "test": "npm run unit && npm run e2e", 13 | "lint": "eslint --ext .js,.vue src/", 14 | "lint-fix": "eslint --fix .js,.vue src/", 15 | "build": "node build/build.js" 16 | }, 17 | "dependencies": { 18 | "axios": "^0.21.2", 19 | "echarts": "^3.8.5", 20 | "iview": "^2.8.0", 21 | "js-base64": "^2.4.8", 22 | "mavon-editor": "^2.4.13", 23 | "vue": "^2.5.2", 24 | "vue-axios": "^2.0.2", 25 | "vue-router": "^3.0.1", 26 | "vuex": "^3.0.0", 27 | "vuex-router-sync": "^5.0.0" 28 | }, 29 | "devDependencies": { 30 | "autoprefixer": "^7.1.2", 31 | "babel-core": "^6.22.1", 32 | "babel-eslint": "^8.2.1", 33 | "babel-helper-vue-jsx-merge-props": "^2.0.3", 34 | "babel-jest": "^21.0.2", 35 | "babel-loader": "^7.1.1", 36 | "babel-plugin-dynamic-import-node": "^1.2.0", 37 | "babel-plugin-syntax-jsx": "^6.18.0", 38 | "babel-plugin-transform-es2015-modules-commonjs": "^6.26.0", 39 | "babel-plugin-transform-runtime": "^6.22.0", 40 | "babel-plugin-transform-vue-jsx": "^3.5.0", 41 | "babel-preset-env": "^1.3.2", 42 | "babel-preset-stage-2": "^6.22.0", 43 | "babel-register": "^6.22.0", 44 | "chalk": "^2.0.1", 45 | "chromedriver": "^2.27.2", 46 | "copy-webpack-plugin": "^4.0.1", 47 | "cross-spawn": "^5.0.1", 48 | "css-loader": "^0.28.0", 49 | "eslint": "^4.15.0", 50 | "eslint-config-standard": "^10.2.1", 51 | "eslint-friendly-formatter": "^3.0.0", 52 | "eslint-loader": "^1.7.1", 53 | "eslint-plugin-import": "^2.7.0", 54 | "eslint-plugin-node": "^5.2.0", 55 | "eslint-plugin-promise": "^3.4.0", 56 | "eslint-plugin-standard": "^3.0.1", 57 | "eslint-plugin-vue": "^4.0.0", 58 | "extract-text-webpack-plugin": "^3.0.0", 59 | "file-loader": "^1.1.4", 60 | "friendly-errors-webpack-plugin": "^1.6.1", 61 | "html-webpack-plugin": "^5.5.0", 62 | "jest": "^29.3.1", 63 | "jest-serializer-vue": "^0.3.0", 64 | "nightwatch": "^2.3.3", 65 | "node-notifier": "^8.0.1", 66 | "optimize-css-assets-webpack-plugin": "^3.2.0", 67 | "ora": "^1.2.0", 68 | "portfinder": "^1.0.13", 69 | "postcss-import": "^11.0.0", 70 | "postcss-loader": "^2.0.8", 71 | "postcss-url": "^7.2.1", 72 | "rimraf": "^2.6.0", 73 | "selenium-server": "^3.0.1", 74 | "semver": "^5.3.0", 75 | "shelljs": "^0.8.5", 76 | "uglifyjs-webpack-plugin": "^1.1.1", 77 | "url-loader": "^0.5.8", 78 | "vue-jest": "^1.0.2", 79 | "vue-loader": "^13.3.0", 80 | "vue-style-loader": "^3.0.1", 81 | "vue-template-compiler": "^2.5.2", 82 | "webpack": "^3.6.0", 83 | "webpack-bundle-analyzer": "^4.6.1", 84 | "webpack-dev-server": "^2.9.1", 85 | "webpack-merge": "^4.1.0" 86 | }, 87 | "engines": { 88 | "node": ">= 6.0.0", 89 | "npm": ">= 3.0.0" 90 | }, 91 | "browserslist": [ 92 | "> 1%", 93 | "last 2 versions", 94 | "not ie <= 8" 95 | ] 96 | } 97 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /view/comment.html: -------------------------------------------------------------------------------- 1 |
2 |
Responses<%if(replyTo){%>/Cancel Reply<%}%> 3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 |
    13 | <%if(content.comment){%> 14 | <%content.comment.forEach(function(comment){%> 15 |
  1. 16 |
    17 |
    18 |
    19 | <%if(comment.url){%><%=comment.author%> 20 | <%}else{%><%=comment.author%> 21 | <%}%> 22 |
    23 |
    24 | <%if(comment.parent){%> 25 | 26 | @<%=comment.parent.author%> 27 | 28 | <%}%> 29 |

    30 | <%=comment.text%> 31 |

    32 |
    33 |
    34 | Reply
    35 |
    36 |
    37 |
  2. 38 | <%});%> 39 | <%}%> 40 |
41 |
42 |
43 | -------------------------------------------------------------------------------- /admin/src/assets/css/admin.css: -------------------------------------------------------------------------------- 1 | /* 后台台样式 */ 2 | 3 | html, 4 | body { 5 | width: 100%; 6 | height: 100%; 7 | background: #f0f0f0; 8 | overflow: hidden; 9 | } 10 | 11 | #app { 12 | height: 100%; 13 | width: 100%; 14 | } 15 | 16 | .layout { 17 | border: 1px solid #d7dde4; 18 | background: #f5f7f9; 19 | position: relative; 20 | border-radius: 4px; 21 | overflow: hidden; 22 | height: 100%; 23 | width: 100%; 24 | } 25 | 26 | .admin { 27 | height: 100%; 28 | } 29 | 30 | .layout-breadcrumb { 31 | padding: 10px 15px 0; 32 | } 33 | 34 | .layout-content { 35 | min-height: 200px; 36 | margin: 15px; 37 | overflow: hidden; 38 | background: #fff; 39 | border-radius: 4px; 40 | } 41 | 42 | .layout-content-main { 43 | padding: 10px; 44 | } 45 | 46 | .layout-copy { 47 | text-align: center; 48 | padding: 10px 0 20px; 49 | color: #9ea7b4; 50 | } 51 | 52 | .layout-rignt { 53 | overflow-y: auto; 54 | max-height: 100%; 55 | } 56 | 57 | .layout-menu-left { 58 | background: #464c5b; 59 | } 60 | 61 | .layout-menu-left ul { 62 | z-index: 500; 63 | } 64 | 65 | .layout-menu-left i { 66 | width: 14px; 67 | } 68 | 69 | .layout-header { 70 | height: 60px; 71 | background: #fff; 72 | box-shadow: 0 1px 1px rgba(0, 0, 0, .1); 73 | } 74 | 75 | .layout-header-right { 76 | text-align: right!important; 77 | line-height: 60px; 78 | padding-right: 10px; 79 | } 80 | 81 | .layout-header-right .home { 82 | padding-right: 10px; 83 | font-size: 18px; 84 | color: #495060; 85 | } 86 | 87 | .layout-logo-left { 88 | width: 90%; 89 | height: 30px; 90 | background: #5b6270; 91 | border-radius: 3px; 92 | margin: 15px auto; 93 | } 94 | 95 | .layout-ceiling-main a { 96 | color: #9ba7b5; 97 | } 98 | 99 | .layout-hide-text .layout-text, 100 | .layout-hide-text .ivu-menu-submenu-title-icon { 101 | display: none; 102 | } 103 | 104 | .ivu-col { 105 | transition: width .2s ease-in-out; 106 | text-align: left; 107 | } 108 | 109 | #editor .editor { 110 | z-index: 800; 111 | } 112 | 113 | @keyframes error404animation { 114 | 0% { 115 | transform: rotatez(0deg); 116 | } 117 | 20% { 118 | transform: rotatez(-60deg); 119 | } 120 | 40% { 121 | transform: rotatez(-10deg); 122 | } 123 | 60% { 124 | transform: rotatez(50deg); 125 | } 126 | 80% { 127 | transform: rotatez(-20deg); 128 | } 129 | 100% { 130 | transform: rotatez(0deg); 131 | } 132 | } 133 | 134 | .user-infor { 135 | height: 135px; 136 | } 137 | 138 | .avator-img { 139 | display: block; 140 | width: 80%; 141 | max-width: 100px; 142 | height: auto; 143 | } 144 | .avatar{ 145 | display: block; 146 | height: 100px; 147 | width: 100px; 148 | } 149 | 150 | .card-user-infor-name { 151 | font-size: 2em; 152 | color: #2d8cf0; 153 | } 154 | 155 | .card-title { 156 | color: #abafbd; 157 | } 158 | 159 | .made-child-con-middle { 160 | height: 100%; 161 | } 162 | 163 | .to-do-list-con { 164 | height: 145px; 165 | overflow: auto; 166 | } 167 | 168 | .to-do-item { 169 | padding: 2px; 170 | } 171 | 172 | .infor-card-con { 173 | height: 100px; 174 | } 175 | 176 | .infor-card-icon-con { 177 | height: 100%; 178 | color: white; 179 | border-radius: 3px 0 0 3px; 180 | } 181 | 182 | .map-con { 183 | height: 305px; 184 | } 185 | 186 | .map-incon { 187 | height: 100%; 188 | } 189 | 190 | .data-source-row { 191 | height: 200px; 192 | } 193 | 194 | .line-chart-con { 195 | height: 150px; 196 | } 197 | 198 | .page { 199 | float: right; 200 | margin-top: 10px; 201 | } 202 | 203 | .search { 204 | text-align: right; 205 | } 206 | 207 | .fl{ 208 | float: left; 209 | } 210 | -------------------------------------------------------------------------------- /view/muster.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | <%include header.html%> 4 | 5 | 6 | <%include navbar.html%> 7 |
8 |
9 | <%if(meta){%> 10 |
11 | <%=meta.key%>: 12 | <%=meta.value%> 13 |
14 | <%}%> 15 |
16 |
17 | <%if(contents.data.length>0){%> 18 | <%contents.data.forEach(function(content){%> 19 |
20 |
21 |
22 | 27 |
28 |
29 | <%=think.datetime(content.create_time*1000,'MM DD, YYYY')%> 30 |
31 |
32 |
33 |
34 |
35 | <%});%> 36 | <%}else{%> 37 |
没有任何数据
38 | <%}%> 39 |
40 |
41 |
42 |
43 | 57 |
58 |
59 | <%include footer.html%> 60 | 61 | 62 | -------------------------------------------------------------------------------- /www/static/admin/js/0.d67519eeb53a312e070e.js: -------------------------------------------------------------------------------- 1 | webpackJsonp([0],{"+skl":function(t,n){},gyMJ:function(t,n,e){"use strict";var a=e("//Fk"),u=e.n(a),o=e("7+uW"),i={create:function(t){return t.type="category",new u.a(function(n,e){o.default.axios.post("/meta",t).then(function(t){n(t.data)})})},getList:function(t){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return n.type="category",new u.a(function(t,e){o.default.axios.get("/meta",{params:n}).then(function(n){t(n.data)})})},getInfo:function(t){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return n.type="category",new u.a(function(e,a){o.default.axios.get("/meta/"+t,{params:n}).then(function(t){e(t.data)})})},update:function(t,n){return n.type="category",new u.a(function(e,a){o.default.axios.put("/meta/"+t,n).then(function(t){e(t.data)})})},delete:function(t){return new u.a(function(n,e){o.default.axios.delete("/meta/"+t).then(function(t){n(t.data)})})}},f={create:function(t){return new u.a(function(n,e){o.default.axios.post("/content",t).then(function(t){n(t.data)})})},getList:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return t.type=t.type||"default",new u.a(function(n,e){o.default.axios.get("/content",{params:t}).then(function(t){n(t.data)})})},getInfo:function(t){return new u.a(function(n,e){o.default.axios.get("/content/"+t).then(function(t){n(t.data)})})},update:function(t,n){return new u.a(function(e,a){o.default.axios.put("/content/"+t,n).then(function(t){e(t.data)})})},delete:function(t){return new u.a(function(n,e){o.default.axios.delete("/content/"+t).then(function(t){n(t.data)})})}},c={getList:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return new u.a(function(n,e){o.default.axios.get("/comment",{params:t}).then(function(t){n(t.data)})})},getInfo:function(t){return new u.a(function(n,e){o.default.axios.get("/comment/"+t).then(function(t){n(t.data)})})},update:function(t,n){return new u.a(function(e,a){o.default.axios.put("/comment/"+t,n).then(function(t){e(t.data)})})},delete:function(t){return new u.a(function(n,e){o.default.axios.delete("/comment/"+t).then(function(t){n(t.data)})})}},r={upload:function(t){return new u.a(function(n,e){o.default.axios.post("/image",t,{headers:{"Content-Type":"multipart/form-data"}}).then(function(t){n(t.data)}).catch(function(t){e(t)})})}},d={create:function(t){return t.type="tag",new u.a(function(n,e){o.default.axios.post("/meta",t).then(function(t){n(t.data)})})},getList:function(t){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return n.type="tag",new u.a(function(t,e){o.default.axios.get("/meta",{params:n}).then(function(n){t(n.data)})})},getInfo:function(t){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return n.type="tag",new u.a(function(e,a){o.default.axios.get("/meta/"+t,{params:n}).then(function(t){e(t.data)})})},update:function(t,n){return n.type="tag",new u.a(function(e,a){o.default.axios.put("/meta/"+t,n).then(function(t){e(t.data)})})},delete:function(t){return new u.a(function(n,e){o.default.axios.delete("/meta/"+t).then(function(t){n(t.data)})})}},s={create:function(t){return new u.a(function(n,e){o.default.axios.post("/token",t).then(function(t){n(t.data)})})}},l={getInfo:function(t){return new u.a(function(n,e){o.default.axios.get("/user/"+t).then(function(t){n(t.data)})})},update:function(t,n){return new u.a(function(e,a){o.default.axios.put("/user/"+t,n).then(function(t){e(t.data)})})}},g={getList:function(){return new u.a(function(t,n){o.default.axios.get("/config").then(function(n){t(n.data)})})},update:function(t,n){return new u.a(function(e,a){o.default.axios.put("/config/"+t,n).then(function(t){e(t.data)})})}};e.d(n,"a",function(){return i}),e.d(n,"d",function(){return f}),e.d(n,"b",function(){return c}),e.d(n,"e",function(){return r}),e.d(n,"f",function(){return d}),e.d(n,"g",function(){return s}),e.d(n,"h",function(){return l}),e.d(n,"c",function(){return g})}}); 2 | //# sourceMappingURL=0.d67519eeb53a312e070e.js.map -------------------------------------------------------------------------------- /view/list.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | <% include navbar.html %> 4 |
5 |
6 |
7 | <% if (contents.data.length>0) { %> 8 | <% contents.data.forEach(function(content){ %> 9 |
10 |
11 | <% if (content.thumb) { %>
12 | <% } %> 13 |
14 | 19 |
20 | in 23 | 24 | <%= content.category.slug %> 25 | 26 |
27 |
28 |
29 | <%- content.description %> 30 |
31 | 32 |
33 |
34 |
35 | <% }); %> 36 | <% } %> 37 |
38 |
39 |
40 | 54 |
55 |
56 | <% include footer.html %> 57 | 58 | <% include bottom.html %> -------------------------------------------------------------------------------- /admin/src/view/tag/list.vue: -------------------------------------------------------------------------------- 1 | 23 | 147 | -------------------------------------------------------------------------------- /admin/src/view/config/save.vue: -------------------------------------------------------------------------------- 1 | 87 | 129 | 134 | -------------------------------------------------------------------------------- /admin/src/view/category/list.vue: -------------------------------------------------------------------------------- 1 | 23 | 147 | -------------------------------------------------------------------------------- /www/static/admin/js/6.565567e57f49237492d6.js: -------------------------------------------------------------------------------- 1 | webpackJsonp([6],{XBLz:function(t,e){},"sWj/":function(t,e,o){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var a=o("Dd8w"),n=o.n(a),r=(o("+skl"),o("XBLz"),o("BTaQ")),s=o("NYxO"),i=o("gyMJ");o("7+uW").default.component("Button",r.Button);var u={components:{Row:r.Row,Col:r.Col,Menu:r.Menu,DropdownMenu:r.DropdownMenu,DropdownItem:r.DropdownItem,Breadcrumb:r.Breadcrumb,BreadcrumbItem:r.BreadcrumbItem,Card:r.Card,Dropdown:r.Dropdown,Icon:r.Icon,Submenu:r.Submenu,MenuItem:r.MenuItem,Button:r.Button,Avatar:r.Avatar},data:function(){return{userInfo:{},fold:this.$store.getters.getMenu.fold,menu:[{name:"控制台",path:"/home",icon:"home"},{name:"内容管理",path:"/content/list",icon:"document"},{name:"页面管理",path:"/page/list",icon:"ios-paper"},{name:"评论管理",path:"/comment/list",icon:"chatbubbles"},{name:"分类管理",path:"/category/list",icon:"shuffle"},{name:"标签管理",path:"/tag/list",icon:"pricetags"},{name:"系统设置",path:"/config",icon:"ios-gear"}]}},computed:n()({iconSize:function(){return this.fold?24:14},breadcrumb:function(){var t=this.route.path.split("/"),e={admin:"后台",home:"控制台",content:"内容",category:"分类",page:"页面",comment:"评论",tag:"标签",save:"编辑",list:"列表",config:"设置"},o=[];for(var a in t){var n=t[a];n&&o.push(e[n]?e[n]:"")}return o}},Object(s.c)({route:function(t){return t.route}})),mounted:function(){this.getUserInfo()},methods:{getUserInfo:function(){var t=this;i.h.getInfo(this.$store.state.admin.user.name).then(function(e){t.$store.commit("setUserInfo",e.data),t.userInfo=e.data})},toggleClick:function(){this.fold=!this.fold,this.$store.commit("setMenuFlod",this.fold)},select:function(t){this.$router.push(t)},loginOut:function(){this.$store.commit("clearToken"),this.$router.push("/login")},dropdownClick:function(t){"loginOut"===t&&this.loginOut(),"userInfo"===t&&this.$router.push("/user/info"),"changePassword"===t&&this.$router.push("/user/password")}}},c={render:function(){var t=this,e=t.$createElement,o=t._self._c||e;return o("div",{staticClass:"layout",class:{"layout-hide-text":t.fold},attrs:{id:"admin"}},[o("Row",{staticClass:"admin",attrs:{type:"flex"}},[o("Col",{staticClass:"layout-menu-left",attrs:{span:t.fold?1:3}},[o("Menu",{attrs:{"active-name":"1",theme:"dark",width:"auto","active-name":t.route.path},on:{"on-select":t.select}},[o("div",{staticClass:"layout-logo-left"}),t._v(" "),t._l(t.menu,function(e,a){return[e.children?t._e():o("MenuItem",{attrs:{name:e.path}},[o("Icon",{attrs:{type:e.icon,size:t.iconSize}}),t._v(" "),o("span",{staticClass:"layout-text"},[t._v(t._s(e.name))])],1)]})],2)],1),t._v(" "),o("Col",{staticClass:"layout-rignt",attrs:{span:t.fold?23:21}},[o("div",{staticClass:"layout-header"},[o("Row",[o("Col",{staticClass:"layout-header-left",attrs:{span:"12"}},[o("Button",{attrs:{type:"text"},on:{click:t.toggleClick}},[o("Icon",{attrs:{type:"navicon",size:"32"}})],1),t._v(" "),o("Breadcrumb",{staticStyle:{"margin-left":"30px",display:"inline-block"}},[o("BreadcrumbItem",{attrs:{to:"/home"}},[t._v("首页")]),t._v(" "),t.breadcrumb[0]?o("BreadcrumbItem",[t._v(t._s(t.breadcrumb[0]))]):t._e(),t._v(" "),t.breadcrumb[1]?o("BreadcrumbItem",[t._v(t._s(t.breadcrumb[1]))]):t._e(),t._v(" "),t.breadcrumb[2]?o("BreadcrumbItem",[t._v(t._s(t.breadcrumb[2]))]):t._e()],1)],1),t._v(" "),o("Col",{staticClass:"layout-header-right",attrs:{span:"12"}},[o("a",{staticClass:"home",attrs:{target:"_blank",href:"/"}},[o("Icon",{attrs:{type:"home"}})],1),t._v(" "),o("Dropdown",{attrs:{trigger:"click"},on:{"on-click":t.dropdownClick}},[o("a",{attrs:{href:"javascript:void(0)"}},[t._v("\n "+t._s(t.userInfo.username)+"\n "),o("Icon",{attrs:{type:"arrow-down-b"}})],1),t._v(" "),o("DropdownMenu",{attrs:{slot:"list"},slot:"list"},[o("DropdownItem",{attrs:{name:"userInfo"}},[t._v("个人资料")]),t._v(" "),o("DropdownItem",{attrs:{name:"changePassword"}},[t._v("修改密码")]),t._v(" "),o("DropdownItem",{attrs:{name:"loginOut"}},[t._v("退出后台")])],1)],1),t._v(" "),o("Avatar",{attrs:{src:t.userInfo.avator,size:"large"}})],1)],1)],1),t._v(" "),o("div",{staticClass:"layout-content"},[o("div",{staticClass:"layout-content-main"},[o("router-view")],1)])])],1)],1)},staticRenderFns:[]},l=o("VU/8")(u,c,!1,null,null,null);e.default=l.exports}}); 2 | //# sourceMappingURL=6.565567e57f49237492d6.js.map -------------------------------------------------------------------------------- /view/error_404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 404 11 | 122 | 123 | 124 |
125 |
126 |
127 |
404
128 |
129 |
130 |
131 |
Back Home 132 |
133 |
134 |
135 | 136 | -------------------------------------------------------------------------------- /view/error_500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 500 11 | 122 | 123 | 124 |
125 |
126 |
127 |
500
128 |
129 |
130 |
131 |
Back Home 132 |
133 |
134 |
135 | 136 | -------------------------------------------------------------------------------- /src/controller/api/content.js: -------------------------------------------------------------------------------- 1 | const BaseRest = require('../rest.js'); 2 | 3 | module.exports = class extends BaseRest { 4 | /** 5 | * 添加内容 6 | * @return {[type]} [description] 7 | */ 8 | async postAction() { 9 | const userInfo = this.userInfo; 10 | const createTime = this.post('create_time') ? (new Date(this.post('create_time'))).getTime() / 1000 : (new Date()).getTime() / 1000; 11 | const data = { 12 | user_id: userInfo.id, 13 | title: this.post('title'), 14 | category_id: this.post('category_id'), 15 | slug: this.post('slug'), 16 | status: this.post('status'), 17 | markdown: this.post('markdown'), 18 | content: this.post('content'), 19 | tag: this.post('tag'), 20 | type: this.post('type'), 21 | thumb: this.post('thumb'), 22 | view: 0, 23 | create_time: createTime, 24 | modify_time: createTime 25 | }; 26 | const id = this.modelInstance.insert(data); 27 | if (id) { 28 | data.id = id; 29 | await this.hook('contentCreate', data); 30 | return this.success({ id: id }, '添加成功'); 31 | } else { 32 | return this.fail(1000, '添加失败'); 33 | } 34 | } 35 | 36 | /** 37 | * 更新内容 38 | * @return {[type]} [description] 39 | */ 40 | async putAction() { 41 | const id = this.id; 42 | if (!this.id) { 43 | return this.fail(1001, '文章不存在'); 44 | } 45 | const data = { 46 | title: this.post('title'), 47 | category_id: this.post('category_id'), 48 | slug: this.post('slug'), 49 | status: this.post('status'), 50 | markdown: this.post('markdown'), 51 | content: this.post('content'), 52 | tag: this.post('tag'), 53 | type: this.post('type'), 54 | thumb: this.post('thumb'), 55 | create_time: this.post('create_time') ? (new Date(this.post('create_time'))).getTime() / 1000 : (new Date()).getTime() / 1000, 56 | modify_time: (new Date()).getTime() / 1000 57 | }; 58 | const res = this.modelInstance.save(id, data); 59 | if (res) { 60 | data.id = id; 61 | await this.hook('contentUpdate', data); 62 | return this.success({ id: id }, '修改成功'); 63 | } else { 64 | return this.fail(1000, '添加失败'); 65 | } 66 | } 67 | 68 | /** 69 | * 获取内容 70 | * @return {[type]} [description] 71 | */ 72 | async getAction() { 73 | let data; 74 | const map = {}; 75 | // 获取详情 76 | if (this.id) { 77 | map.slug = this.id; 78 | if (think.isEmpty(this.userInfo)) { 79 | map.status = 99; 80 | } 81 | data = await this.modelInstance.where(map).find(); 82 | // 增加阅读量 83 | if (!this.userInfo) { 84 | this.modelInstance.where(map).increment('view'); 85 | } 86 | return this.success(data); 87 | } 88 | // 获取列表 89 | const type = this.get('type') || 'default'; 90 | // 归档 91 | if (type === 'archives') { 92 | data = await this.modelInstance.where({ status: 99 }).order('create_time desc').fieldReverse('content,markdown').select(); 93 | return this.success(data); 94 | } 95 | // 热门文章 96 | if (type === 'hot') { 97 | data = await this.modelInstance.where({ status: 99 }).order('view desc').limit(10).field('title,slug,view').select(); 98 | return this.success(data); 99 | } 100 | // 是否获取全部 101 | const all = this.get('all'); 102 | if (!all || think.isEmpty(this.userInfo)) { 103 | map.status = 99; 104 | } 105 | // 关键词 106 | const key = this.get('key'); 107 | if (key) { 108 | map['title|description'] = ['like', '%' + key + '%']; 109 | } 110 | 111 | // 内容类型 112 | const contentType = this.get('contentType') || 'post'; 113 | map['type'] = contentType; 114 | // 页码 115 | const page = this.get('page') || 1; 116 | // 每页显示数量 117 | const pageSize = this.get('pageSize') || 5; 118 | data = await this.modelInstance.where(map).page(page, pageSize).order('create_time desc').fieldReverse('content,markdown').countSelect(); 119 | return this.success(data); 120 | } 121 | 122 | /** 123 | * 删除内容 124 | * @return {[type]} [description] 125 | */ 126 | async deleteAction() { 127 | if (!this.id) { 128 | return this.fail(1001, '文章不存在'); 129 | } 130 | const rows = await this.modelInstance.where({ id: this.id }).delete(); 131 | if (rows) { 132 | await this.hook('contentDelete', {id: this.id}); 133 | return this.success({ affectedRows: rows }, '删除成功'); 134 | } else { 135 | return this.fail(1000, '删除失败'); 136 | } 137 | } 138 | }; 139 | -------------------------------------------------------------------------------- /admin/src/view/page/save.vue: -------------------------------------------------------------------------------- 1 | 19 | 149 | -------------------------------------------------------------------------------- /admin/src/view/page/list.vue: -------------------------------------------------------------------------------- 1 | 31 | 168 | -------------------------------------------------------------------------------- /admin/src/view/content/list.vue: -------------------------------------------------------------------------------- 1 | 31 | 175 | -------------------------------------------------------------------------------- /www/static/admin/js/2.e046b457f58bda54b4a1.js: -------------------------------------------------------------------------------- 1 | webpackJsonp([2],{EfXr:function(M,e){},ZWsv:function(M,e,t){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=t("Dd8w"),s=t.n(r),b=(t("+skl"),t("EfXr"),t("NYxO")),n=t("BTaQ"),o=t("gyMJ"),A={components:{Form:n.Form,FormItem:n.FormItem,Icon:n.Icon,Input:n.Input,Button:n.Button},data:function(){return{userInfo:{username:"",password:""},ruleInline:{username:[{required:!0,message:"请填写用户名",trigger:"blur"}],password:[{required:!0,message:"请填写密码",trigger:"blur"},{type:"string",min:6,message:"密码长度不能小于6位",trigger:"blur"}]}}},computed:s()({},Object(b.b)(["verifyToken"])),mounted:function(){this.verifyToken&&this.$router.push({name:"AdminIndex"})},methods:{login:function(M){var e=this;this.$refs[M].validate(function(M){M?o.g.create(e.userInfo).then(function(M){0==M.errno&&M.data.token?(e.$store.commit("setUserName",e.userInfo.username),e.$store.commit("setToken",M.data.token),e.$router.push("/home")):e.$Message.error(M.errmsg)}):e.$Message.error("请填写必要信息")})},reset:function(){this.userInfo={user:"",password:""}}}},m={render:function(){var M=this,e=M.$createElement,t=M._self._c||e;return t("div",{staticClass:"login-form"},[M._m(0),M._v(" "),t("h1",[M._v("登录")]),M._v(" "),t("div",{staticClass:"login-top"},[t("Form",{ref:"userInfo",attrs:{model:M.userInfo,rules:M.ruleInline}},[t("FormItem",{attrs:{prop:"username"}},[t("Input",{attrs:{type:"text",placeholder:"Username"},model:{value:M.userInfo.username,callback:function(e){M.$set(M.userInfo,"username",e)},expression:"userInfo.username"}},[t("Icon",{attrs:{slot:"prepend",type:"ios-person-outline"},slot:"prepend"})],1)],1),M._v(" "),t("FormItem",{attrs:{prop:"password"}},[t("Input",{attrs:{type:"password",placeholder:"Password"},model:{value:M.userInfo.password,callback:function(e){M.$set(M.userInfo,"password",e)},expression:"userInfo.password"}},[t("Icon",{attrs:{slot:"prepend",type:"ios-locked-outline"},slot:"prepend"})],1)],1),M._v(" "),t("FormItem",{staticStyle:{"text-align":"center"}},[t("Button",{attrs:{type:"primary"},on:{click:function(e){return M.login("userInfo")}}},[M._v("登录")]),M._v(" "),t("Button",{attrs:{type:"error"},on:{click:M.reset}},[M._v("重置")])],1)],1)],1),M._v(" "),t("p",{staticClass:"copy"},[M._v("@lscho")])])},staticRenderFns:[function(){var M=this.$createElement,e=this._self._c||M;return e("div",{staticClass:"top-login"},[e("span",[e("img",{attrs:{src:t("wiDa"),alt:""}})])])}]},a=t("VU/8")(A,m,!1,null,null,null);e.default=a.exports},wiDa:function(M,e){M.exports="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAACkVBMVEX/////AAD/VVX/NWX/MmX/NGb/M2b/M2b/M2b/NGb/M2b/MWf/JG3/MWf/NGb/M2b/NWX/AID/Mmb/M2b/M2b/M2f/OXH/M2b/M2b/M2b/M2X/M2b/M2f/Mmf/M2b/NGb/M2b/M2b/M2b/M2b/N2T/MmT/M2X/Mmb/M2b/M2b/M2b/M2b/M2f/MWf/K1X/M2X/NGb/M2f/M2f/NWr/NGb/M2X/M2f/M2b/MGf/M2f/NGb/M2b/M2X/QID/Mmb/M2b/M2b/M2b/NGb/Mmf/NGj/NGX/Mmb/M2b/M2b/M2X/NGb/M2X/M2X/MmX/M2X/M2f/M2b/M2b/M2b/MWH/M2X/MGD/M2b/M2b/M2b/NWX/M2b/NGf/MmT/M2b/MWf/M2b/M2b/M2b/M2f/Lmj/M2b/MmP/MmT/M2b/NWX/QGD/M2b/Mmb/NGX/M2f/M2b/NGf/NGb/M2b/Mmb/Mmb/M2X/M2b/M2b/M2b/NGj/M2b/M2X/Mmb/M2X/LWn/Mmb/M2b/M2b/M2b/MGj/NGb/M2b/Ll3/M2f/NGX/M2b/M2b/M2b/M2b/Mmf/M2b/N23/M2X/M2X/NGj/NmX/M2b/M2b/M2b/M2X/M2b/M2b/M2b/M2f/M2f/L2j/N2T/NGb/M2b/M2b/M2f/M2f/Nmv/NGX/M2b/M2b/Mmb/M2b/M2b/NGf/NGb/M2f/Mmb/M2X/MWf/K2r/NWX/NGb/NGX/M2f/M2f/M2b/NGn/M2b/M2b/M2b/NWX/Mmb/O2L/M2b/M2X/MmX/NGb/M2b/M2b/Mmb/M2b/MmT/Mmf/Mmf/NGf/M2X/M2b/M2f/NWn/M2X/MmX/Mmj/M2f/M2b/M2b/M2b/NmT/M2f/NGf/M2f/M2YAAAAZI333AAAA2XRSTlMAAQMwZZnO+dijbjkHKsvePwJ1+P6QCR5ajL/jvVwPxveqeEYcPW+i7dojBWg0Bnlxn8wYssRqXyXHwWSDBKwZ79GFUixThNT8q3t0pky6fIL76hW1EPNp/SvmXi7wPuHI9IsW6CQz+jUI4rZYd/JjrS2dp36Rr7k227CJjRFwVdBQIJ7dC7hdzQq0lmb2DpzJQCbr3N+S1UHus64bF4DsMoaVE07l9Z6g10W3gcXRLwxvj0mawjwnfcPZOsAN5GpRZxRLmPEpV01KoaVyIpdgTNhz6dIhpE+paqYLewAAAAFiS0dE2u4DJoIAAAAJcEhZcwAADdcAAA3XAUIom3gAAAAHdElNRQfgBB4ICSflDWjUAAAEJ0lEQVRYw+1W+1tUVRQ9DNBQoDIFGpaAigmMYQrmIwcV8FEiAUYiiSmaqNVYWglMpRBIaGiK+cQsQbPENK1Io7f2zl72WP9N69zDzJx77wxD9X3+xP4+7uxz1t777Hv23usixJAMyY2RqP/h64iOib3JGXfzLfEJ/8l/2HD4ZUTiv3d33XobgpKUPHIQPqNuTxl9x50pY0ZRT02jW/rYceMzJtw1MZN6VrZ7zKS7cwZ648nO/uPS73GJKfydOqwfys3jatq9fDiHTw/nP2OmlvGs+/iY7Qmi0xk8X2F5c0L6Z8wlNq+gsGj+goXUFgH3m/AHmNji4qwkYplLQgUoYXoPqiNzSstoVr7UhHtYkoeEqEhmiIez7f7LgMpgqZZXAY9YLFYA1bKpVj4KrPJY/RNWAzXacenAGqvNWuAx+buuFii1guuBqg3BZTSw0Zbk48AThvIk4HXZ3yBfW24CnrIFWOBP0lUOPG0BNwOTzfG22AI8AzyrtOeAGAu41XxpdUC9LUBDwGYxsNkC+oDnteUG1vEFa4CNwHKlvWhPcBubT19vBxotJk3AaodSXwJ8dtRZpK1jgeYWs8mOwBmOLKDVmt9U4OVgd7TtZCvuMhm8wk6LD1xx+25rgCIO/x7/yC991aCATRq+1xtIe18HMMV2xbI7kDVBaiPH76f+Gv8a/N3YdoATcFDx2qHDHKc19gCOI/LUzFlbVhmscLSzmc8O37YmcWzJ6zLi8Tdo5Wk8wjktKxQhxFHjDNBB5Ztu0XIiydD7ia3hGG26Fho73SHc3ctOanyC8oIMkVpwKrCufStXWnUrfjzdafMvbFaGZYeP1ypt0du1ekQkTXuH13NGLU71tFiyl7vtE1s7OfBRGYd61gb85h48+25Hv771HHk3Jr9a6id1VjpfYlBdU3DnPeP0ERcS24xlwrrSecbV1MhW9KyU3wvnxYB11FFZNe1ict6X1h98aMqyd6zc3FEhdc9slqI90Q99RKBY+2x0XeJGnJ04685y/7JBSiKehfXWqf2PWa3tGhu5+2i385MQlfKcIFKvprTrU3aGkY5g3GqdZXfRKsUlQspnHPMSNZJ7K4EeqTTS/nPN5guuk0U4mUT0glLJcNUymzTzZOSSns84wgYQB9gR69ULzTTI+Utm8pVmcIVEkBreXzh4Q31KnQ9crRCtbO22IP416/ONGEiieeffGtpuNst34nveigb/wE9yhP9pikk9ShsN/Cj65CMg7qvAxYH9RTZT+MnQSM6x4jJwbZxffv6FxXVECCDrnmeYX5Ms6TWN3K/A6Uj+4jfN4Xdx3RTAa2dzu/yhOVwXvb4rQelhgKaIAQQv6k/l4Os1Iy3skpzIAeKAutDIDHJZZH9Zub9CI+eAtEEE2BO+2f6u6h5EgBX1l84PwmxIhuQGyj+8CXWk3eTs4AAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxNi0wNC0zMFQwODowOTozOSswMjowMKVyITUAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTYtMDQtMzBUMDg6MDk6MzkrMDI6MDDUL5mJAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAABJRU5ErkJggg=="}}); 2 | //# sourceMappingURL=2.e046b457f58bda54b4a1.js.map -------------------------------------------------------------------------------- /view/content.html: -------------------------------------------------------------------------------- 1 | 2 | <%include navbar.html%> 3 |
4 |
5 |

<%=content.title%>

6 |
7 |   in   10 | 11 | <%=content.category.slug%> 12 | with  <%=content.comment.length%>  comment
13 |
14 |
15 | 22 | <%-content.content%> 23 | 31 |
32 |
33 | <%include comment.html%> 34 |
35 |
36 |
37 | <%include footer.html%> 38 | 76 | 77 | <% include bottom.html %> -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------