├── .babelrc
├── .dockerignore
├── .eslintignore
├── .eslintrc.json
├── .gitignore
├── LICENSE
├── README.md
├── client
├── build
│ ├── koa2
│ │ ├── dev.js
│ │ └── hot.js
│ ├── setup-dev-server.js
│ ├── webpack.base.config.js
│ ├── webpack.client.config.js
│ └── webpack.server.config.js
├── src
│ ├── api
│ │ ├── article.js
│ │ ├── login.js
│ │ └── tag.js
│ ├── components
│ │ ├── Loading.vue
│ │ └── Pagination.vue
│ ├── lib
│ │ ├── debounce.js
│ │ ├── marked.js
│ │ └── throttle.js
│ └── modules
│ │ ├── admin
│ │ ├── App.vue
│ │ ├── app.js
│ │ ├── assets
│ │ │ ├── logo.png
│ │ │ └── stylus
│ │ │ │ ├── _settings.styl
│ │ │ │ ├── _syntax.styl
│ │ │ │ ├── main.styl
│ │ │ │ ├── preview.styl
│ │ │ │ └── reset.styl
│ │ ├── components
│ │ │ ├── Admin.vue
│ │ │ ├── Editor.vue
│ │ │ ├── List.vue
│ │ │ └── Login.vue
│ │ ├── index.html
│ │ └── store
│ │ │ ├── index.js
│ │ │ ├── modules
│ │ │ ├── auth.js
│ │ │ └── editor.js
│ │ │ └── mutation-types.js
│ │ └── front
│ │ ├── App.vue
│ │ ├── app.js
│ │ ├── assets
│ │ ├── iconfont
│ │ │ ├── iconfont.css
│ │ │ ├── iconfont.eot
│ │ │ ├── iconfont.svg
│ │ │ ├── iconfont.ttf
│ │ │ └── iconfont.woff
│ │ ├── logo.png
│ │ └── stylus
│ │ │ ├── _settings.styl
│ │ │ ├── _syntax.styl
│ │ │ ├── main.styl
│ │ │ ├── markdown.styl
│ │ │ └── reset.styl
│ │ ├── components
│ │ ├── Article.vue
│ │ ├── List.vue
│ │ └── common
│ │ │ ├── Comment.vue
│ │ │ ├── Side.vue
│ │ │ └── Top.vue
│ │ ├── entry-client.js
│ │ ├── entry-server.js
│ │ ├── index.html
│ │ ├── router
│ │ └── index.js
│ │ └── store
│ │ └── index.js
└── static
│ └── .gitkeep
├── docker-compose.prod.yml
├── docker-compose.yml
├── node.dockerfile
├── nodemon.json
├── package-lock.json
├── package.json
├── pm2.json
├── server
├── api
│ ├── index.js
│ └── routes
│ │ ├── articles.js
│ │ ├── tags.js
│ │ └── token.js
├── configs
│ └── index.js
├── controllers
│ ├── articles_controller.js
│ ├── tags_controller.js
│ └── token_controller.js
├── index.js
├── logs
│ └── .gitkeep
├── middleware
│ ├── historyApiFallback.js
│ ├── index.js
│ └── verify.js
├── models
│ ├── article.js
│ ├── tag.js
│ └── user.js
└── start.js
└── yarn.lock
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015", "stage-2"],
3 | "plugins": ["transform-runtime",
4 | ["component", [
5 | {
6 | "libraryName": "element-ui",
7 | "styleLibraryName": "theme-default"
8 | }
9 | ]]]
10 | }
11 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules/
3 | **/dist/
4 | *.log
5 | *.pid
6 | stats.json
7 | test-code/
8 | server/logs/*.log
9 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | **/node_modules/*
2 | **/bower_components/*
3 | static/*
4 | output/*
5 | test/*
6 | **/dist/*
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules/
3 | **/dist/
4 | *.log
5 | *.pid
6 | stats.json
7 | server/configs/private.js
8 | test-code/
9 | server/logs/*.log
10 |
11 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2017-present, Junming Huang (BUPT-HJM)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # vue-blog
2 |
3 | > A single-user blog built with vue2, koa2 and mongodb which supports Server-Side Rendering
4 |
5 | 一个使用vue2、koa2、mongodb搭建的单用户博客,支持markdown编辑,文章标签分类,发布文章/撤回发布文章,支持服务端渲染(Server-Side Rendering)
6 |
7 |
8 |
9 |
10 |
11 |
12 | 访问链接:https://imhjm.com/
13 |
14 |
15 | ## 整体架构
16 |
17 |
18 | - client端分为`front`和`admin`,`webpack2`打包实现多页配置,开发模式下`hot reload`
19 | - admin端使用vue2、vuex、vue-router
20 | - front端直接使用 ~~vue event bus~~ vuex(考虑到今后博客应用可能变复杂)、vue-router, Fastclick解决移动端300ms延迟问题
21 | - 编辑器使用[simplemde](https://github.com/NextStepWebs/simplemde-markdown-editor)
22 | - markdown解析和高亮使用marked.js和highlight.js
23 | - server
24 | - 使用koa2+koa-router实现RESTful API
25 | - mongoose连接mongodb
26 | - 前后端鉴权使用[jwt](https://github.com/auth0/node-jsonwebtoken)
27 | - 实现Server-Side Rendering服务端渲染
28 |
29 | ## 更多细节
30 | - 博客线上地址:http://imhjm.com/
31 | - [基于vue2、koa2、mongodb的个人博客](http://imhjm.com/article/58f76ed0c9eb43547d08ec6c)
32 | - [Vue2服务端渲染实践以及相关解读](http://imhjm.com/article/590710fbe3176b248999f88c)
33 |
34 | > 访问博客线上地址可以获得最新信息
35 |
36 | ## 快速开始
37 | - 需要Node.js 6+版本
38 | - 需要安装mongodb,并且运行mongodb服务,在`server/configs/index.js`中默认连接`mongodb://localhost:27017/vue-blog`
39 | - 配置`server/configs/index.js`,配置admin用户名、密码等,或者新建`server/configs/private.js`
40 |
41 | > 注:可使用docker快速开始,详见后文
42 |
43 | ``` bash
44 | # install dependencies
45 | # 安装依赖,可以使用yarn/npm
46 | npm install # or yarn install
47 |
48 | # serve in dev mode, with hot reload at localhost:8889
49 | # 开发环境,带有HMR,监听8889端口
50 | npm run dev
51 |
52 | # build for production
53 | # 生产环境打包
54 | npm run build
55 |
56 | # serve in production mode (with building)
57 | # 生产环境服务,不带有打包
58 | npm start
59 |
60 | # serve in production mode (without building)
61 | # 生产环境服务,带有打包
62 | npm run prod
63 |
64 | # pm2
65 | # need `npm install pm2 -g`
66 | npm run pm2
67 | ```
68 |
69 | ## 使用docker快速开始
70 | - 首先,需要访问[docker官网](https://www.docker.com/)根据不同操作系统获取docker
71 | - docker官方文档:https://docs.docker.com/
72 | - mongo dockerhub文档:https://hub.docker.com/_/mongo/ (关于auth/volumes一些问题)
73 |
74 | ``` bash
75 | # development mode(use volumes for test-edit-reload cycle)
76 | # 开发模式(使用挂载同步容器和用户主机上的文件以便于开发)
77 | # Build or rebuild services
78 | docker-compose build
79 | # Create and start containers
80 | docker-compose up
81 |
82 | # production mode
83 | # 生产模式
84 | # Build or rebuild services
85 | docker-compose -f docker-compose.prod.yml build
86 | # Create and start containers
87 | docker-compose -f docker-compose.prod.yml up
88 |
89 | # 进入容器开启交互式终端
90 | # (xxx指代容器名或者容器ID,可由`docker ps`查看)
91 | docker exec -it xxx bash
92 | ```
93 |
94 | > 注:为了防止生产环境数据库被改写,生产模式数据库与开发环境数据库的链接不同,开发环境使用vue-blog,生产环境使用vue-blog-prod,具体可以看docker-compose配置文件
95 |
96 |
97 | ## 自定义配置
98 | server端配置文件位于`server/configs`目录下
99 |
100 | ``` javascript
101 | // 可新建private.js定义自己私有的配置
102 | module.exports = {
103 | mongodbSecret: { // 如果mongodb设置用户名/密码可在此配置
104 | user: '',
105 | pass: ''
106 | },
107 | jwt: { // 配置jwt secret
108 | secret: ''
109 | },
110 | admin: { // 配置用户名/密码
111 | user: '',
112 | pwd: ''
113 | },
114 | disqus: { // disqus评论
115 | url: '',
116 | },
117 | baidu: { // 百度统计
118 | url: '',
119 | },
120 | }
121 | ```
122 |
123 | ## LICENSE
124 | [MIT](https://github.com/BUPT-HJM/vue-blog/blob/master/LICENSE)
125 |
--------------------------------------------------------------------------------
/client/build/koa2/dev.js:
--------------------------------------------------------------------------------
1 | const devMiddleware = require('webpack-dev-middleware');
2 |
3 | module.exports = (compiler, opts) => {
4 | const expressMiddleware = devMiddleware(compiler, opts);
5 | let nextFlag = false;
6 | function nextFn() {
7 | nextFlag = true;
8 | }
9 | function devFn(ctx, next) {
10 | expressMiddleware(ctx.req, {
11 | end: (content) => {
12 | ctx.body = content;
13 | },
14 | setHeader: (name, value) => {
15 | ctx.headers[name] = value;
16 | },
17 | }, nextFn);
18 | if (nextFlag) {
19 | nextFlag = false;
20 | return next();
21 | }
22 | }
23 | devFn.fileSystem = expressMiddleware.fileSystem;
24 | return devFn;
25 | };
26 |
--------------------------------------------------------------------------------
/client/build/koa2/hot.js:
--------------------------------------------------------------------------------
1 | const hotMiddleware = require('webpack-hot-middleware');
2 | const PassThrough = require('stream').PassThrough;
3 |
4 | module.exports = (compiler, opts) => {
5 | const expressMiddleware = hotMiddleware(compiler, opts);
6 | return (ctx, next) => {
7 | let stream = new PassThrough();
8 | ctx.body = stream;
9 | return expressMiddleware(ctx.req, {
10 | write: stream.write.bind(stream),
11 | writeHead: (state, headers) => {
12 | ctx.state = state;
13 | ctx.set(headers);
14 | },
15 | }, next);
16 | };
17 | };
18 |
--------------------------------------------------------------------------------
/client/build/setup-dev-server.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const webpack = require('webpack');
3 | const MFS = require('memory-fs');
4 | const clientConfig = require('./webpack.client.config');
5 | const serverConfig = require('./webpack.server.config');
6 |
7 | module.exports = function setupDevServer (app, cb) {
8 | let bundle;
9 | let template;
10 |
11 | const clientCompiler = webpack(clientConfig);
12 | const devMiddleware = require('./koa2/dev.js')(clientCompiler, {
13 | publicPath: clientConfig.output.publicPath,
14 | stats: {
15 | colors: true,
16 | },
17 | // noInfo: true
18 | });
19 | app.use(devMiddleware);
20 |
21 |
22 | clientCompiler.plugin('done', () => {
23 | const fs = devMiddleware.fileSystem;
24 | const filePath = path.join(clientConfig.output.path, 'front.html');
25 | console.log('clientPath', serverConfig.output.path);
26 | if (fs.existsSync(filePath)) {
27 | template = fs.readFileSync(filePath, 'utf-8');
28 | if (bundle) {
29 | console.log('ok');
30 | cb(bundle, template);
31 | }
32 | }
33 | });
34 |
35 | // hot middleware
36 | app.use(require('./koa2/hot.js')(clientCompiler));
37 |
38 | // console.log(serverConfig)
39 | // watch and update server renderer
40 | const serverCompiler = webpack(serverConfig);
41 | const mfs = new MFS();
42 | serverCompiler.outputFileSystem = mfs;
43 | serverCompiler.watch({}, (err, stats) => {
44 | if (err) {
45 | throw err;
46 | }
47 | stats = stats.toJson();
48 | stats.errors.forEach(err => console.error(err));
49 | stats.warnings.forEach(err => console.warn(err));
50 |
51 | console.log('serverPath', serverConfig.output.path);
52 | // read bundle generated by vue-ssr-webpack-plugin
53 | const bundlePath = path.join(serverConfig.output.path, 'vue-ssr-server-bundle.json');
54 | bundle = JSON.parse(mfs.readFileSync(bundlePath, 'utf-8'));
55 | if (template) {
56 | console.log('ok');
57 | cb(bundle, template);
58 | }
59 | });
60 | };
61 |
--------------------------------------------------------------------------------
/client/build/webpack.base.config.js:
--------------------------------------------------------------------------------
1 | const { resolve, join } = require('path');
2 | const webpack = require('webpack');
3 | const HtmlWebpackPlugin = require('html-webpack-plugin');
4 | const nodeModulesPath = resolve(__dirname, '../../node_modules');
5 | const CLIENT_FOLDER = resolve(__dirname, '../');
6 | const SERVER_FOLDER = resolve(__dirname, '../../server');
7 | const productionEnv = process.env.NODE_ENV === 'production' ? true : false;
8 | const ExtractTextPlugin = require('extract-text-webpack-plugin');
9 | const CopyWebpackPlugin = require('copy-webpack-plugin');
10 |
11 | let config = {
12 | devtool: '#cheap-module-eval-source-map',
13 | entry: {
14 | 'modules/admin': [
15 | 'babel-polyfill',
16 | CLIENT_FOLDER + '/src/modules/admin/app',
17 | ],
18 | 'modules/front': [
19 | 'babel-polyfill',
20 | CLIENT_FOLDER + '/src/modules/front/entry-client',
21 | ],
22 | },
23 | output: {
24 | path: CLIENT_FOLDER + '/dist',
25 | filename: '[name].js',
26 | publicPath: '/',
27 | },
28 | externals: {
29 | simplemde: 'SimpleMDE',
30 | },
31 | plugins: [],
32 | module: {
33 | rules: [{
34 | test: /\.vue$/,
35 | loader: 'vue-loader',
36 | options: {
37 | loaders: {
38 | styl: ['vue-style-loader', 'css-loader?minimize', 'stylus-loader'],
39 | stylus: ['vue-style-loader', 'css-loader?minimize', 'stylus-loader'],
40 | css: ['vue-style-loader', 'css-loader?minimize'],
41 | },
42 | preserveWhitespace: false,
43 | postcss: [require('autoprefixer')({ browsers: ['last 7 versions'] })],
44 | },
45 | // include: [
46 | // resolve(__dirname, '../src/'),
47 | // resolve(__dirname, '../../node_modules/vue-slider-component/'),
48 | // ]
49 | }, {
50 | test: /\.js$/,
51 | loader: 'babel-loader',
52 | exclude: nodeModulesPath,
53 | // include: [
54 | // resolve(__dirname, '../src/'),
55 | // resolve(__dirname, '../../node_modules/vue-slider-component/'),
56 | // ]
57 | }, {
58 | test: /\.styl$/,
59 | use: ['style-loader', 'css-loader?minimize', 'stylus-loader'],
60 | include: CLIENT_FOLDER,
61 | }, {
62 | test: /\.css$/,
63 | use: ['style-loader', 'css-loader?minimize'],
64 | }, {
65 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
66 | loader: 'url-loader',
67 | options: {
68 | limit: 10000,
69 | name: 'img/[name].[hash:7].[ext]',
70 | },
71 | }, {
72 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
73 | loader: 'url-loader',
74 | options: {
75 | limit: 10000,
76 | name: 'fonts/[name].[hash:7].[ext]',
77 | },
78 | }],
79 | },
80 | resolve: {
81 | extensions: ['.js', '.vue', '.json'],
82 | modules: [nodeModulesPath],
83 | alias: {
84 | vue$: 'vue/dist/vue.esm.js',
85 | vuex$: 'vuex/dist/vuex.esm.js',
86 | 'vue-router$': 'vue-router/dist/vue-router.esm.js',
87 | simplemde$: 'simplemde/dist/simplemde.min.js',
88 | 'highlight.js$': 'highlight.js/lib/highlight.js',
89 | fastclick: 'fastclick/lib/fastclick.js',
90 | lib: resolve(__dirname, '../src/lib'),
91 | api: resolve(__dirname, '../src/api'),
92 | publicComponents: resolve(__dirname, '../src/components'),
93 | serverConfig: resolve(__dirname, '../../server/configs/'),
94 | },
95 | },
96 | // cache: true
97 | };
98 |
99 | module.exports = config;
100 |
--------------------------------------------------------------------------------
/client/build/webpack.client.config.js:
--------------------------------------------------------------------------------
1 | const { resolve, join } = require('path');
2 | const webpack = require('webpack');
3 | const HtmlWebpackPlugin = require('html-webpack-plugin');
4 | const nodeModulesPath = resolve(__dirname, '../../node_modules');
5 | const CLIENT_FOLDER = resolve(__dirname, '../');
6 | const SERVER_FOLDER = resolve(__dirname, '../../server');
7 | const productionEnv = process.env.NODE_ENV === 'production' ? true : false;
8 | const ExtractTextPlugin = require('extract-text-webpack-plugin');
9 | const CopyWebpackPlugin = require('copy-webpack-plugin');
10 | const merge = require('webpack-merge');
11 | const base = require('./webpack.base.config');
12 |
13 | let config = merge(base, {
14 | plugins: [
15 | new webpack.HotModuleReplacementPlugin(),
16 | // 开启全局的模块热替换(HMR)
17 |
18 | new webpack.NamedModulesPlugin(),
19 | // 当模块热替换(HMR)时在浏览器控制台输出对用户更友好的模块名字信息,
20 |
21 | new HtmlWebpackPlugin({
22 | filename: 'admin.html',
23 | template: CLIENT_FOLDER + '/src/modules/admin/index.html',
24 | inject: 'body',
25 | chunks: productionEnv ? ['modules/manifest_admin', 'modules/vendor_admin', 'modules/admin'] : ['modules/admin'],
26 | minify: { // 压缩的方式
27 | removeComments: true,
28 | collapseWhitespace: true,
29 | removeAttributeQuotes: true,
30 | },
31 | // chunksSortMode: 'dependency'
32 | }),
33 |
34 | new HtmlWebpackPlugin({
35 | filename: 'front.html',
36 | template: CLIENT_FOLDER + '/src/modules/front/index.html',
37 | // inject: 'body',
38 | // inject: false,
39 | chunks: productionEnv ? ['modules/manifest_front', 'modules/vendor_front', 'modules/front'] : ['modules/front'],
40 | minify: { // 压缩的方式
41 | // removeComments: true,
42 | collapseWhitespace: true,
43 | removeAttributeQuotes: true,
44 | },
45 | // chunksSortMode: 'dependency'
46 | }),
47 | // 配置提取出的样式文件
48 | new ExtractTextPlugin('css/[name].[contenthash].css'),
49 |
50 | new webpack.DefinePlugin({
51 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
52 | 'process.env.VUE_ENV': '"client"',
53 | }),
54 | ],
55 | });
56 | config.entry['modules/admin'].unshift('event-source-polyfill', 'webpack-hot-middleware/client?reload=true');
57 | config.entry['modules/front'].unshift('event-source-polyfill', 'webpack-hot-middleware/client?reload=true');
58 |
59 | if (process.env.NODE_ENV === 'production') {
60 | // 删除devtool
61 | delete config.devtool;
62 | // 删除webpack-hot-middleware
63 | config.entry['modules/admin'].splice(0, 2);
64 | config.entry['modules/front'].splice(0, 2);
65 | config.output.filename = '[name].[chunkhash:8].min.js';
66 | // 提取css
67 | config.module.rules[0].options.loaders = {
68 | styl: ExtractTextPlugin.extract({
69 | use: [{
70 | loader: 'css-loader',
71 | options: {
72 | minimize: true,
73 | sourceMap: true,
74 | },
75 | }, {
76 | loader: 'stylus-loader',
77 | options: {
78 | sourceMap: true,
79 | },
80 | }],
81 | fallback: 'vue-style-loader',
82 | }),
83 | stylus: ExtractTextPlugin.extract({
84 | use: [{
85 | loader: 'css-loader',
86 | options: {
87 | minimize: true,
88 | sourceMap: true,
89 | },
90 | }, {
91 | loader: 'stylus-loader',
92 | options: {
93 | sourceMap: true,
94 | },
95 | }],
96 | fallback: 'vue-style-loader',
97 | }),
98 | css: ExtractTextPlugin.extract({
99 | use: [{
100 | loader: 'css-loader',
101 | options: {
102 | minimize: true,
103 | sourceMap: true,
104 | },
105 | }],
106 | fallback: 'vue-style-loader',
107 | }),
108 | };
109 | // 删除HotModuleReplacementPlugin和NamedModulesPlugin
110 | config.plugins.shift();
111 | config.plugins.shift();
112 | config.plugins = config.plugins.concat([
113 | new webpack.optimize.UglifyJsPlugin({
114 | // 最紧凑的输出
115 | beautify: false,
116 | // 删除所有的注释
117 | comments: false,
118 | compress: {
119 | // 在UglifyJs删除没有用到的代码时不输出警告
120 | warnings: false,
121 | // 删除所有的 `console` 语句
122 | // 还可以兼容ie浏览器
123 | drop_console: true,
124 | // 内嵌定义了但是只用到一次的变量
125 | collapse_vars: true,
126 | // 提取出出现多次但是没有定义成变量去引用的静态值
127 | reduce_vars: true,
128 | },
129 | }),
130 | // 分别提取vendor、manifest
131 | new webpack.optimize.CommonsChunkPlugin({
132 | name: 'modules/vendor_admin',
133 | chunks: ['modules/admin'],
134 | minChunks: function (module, count) {
135 | return (
136 | module.resource
137 | && /\.js$/.test(module.resource)
138 | && module.resource.indexOf(
139 | nodeModulesPath
140 | ) === 0
141 | );
142 | },
143 | }),
144 | new webpack.optimize.CommonsChunkPlugin({
145 | name: 'modules/manifest_admin',
146 | chunks: ['modules/vendor_admin'],
147 | }),
148 | new webpack.optimize.CommonsChunkPlugin({
149 | name: 'modules/vendor_front',
150 | chunks: ['modules/front'],
151 | minChunks: function (module, count) {
152 | return (
153 | module.resource
154 | && /\.js$/.test(module.resource)
155 | && module.resource.indexOf(
156 | nodeModulesPath
157 | ) === 0
158 | );
159 | },
160 | }),
161 | new webpack.optimize.CommonsChunkPlugin({
162 | name: 'modules/manifest_front',
163 | chunks: ['modules/vendor_front'],
164 | }),
165 | // copy static
166 | new CopyWebpackPlugin([{
167 | from: CLIENT_FOLDER + '/static',
168 | to: CLIENT_FOLDER + '/dist/static',
169 | ignore: ['.*'],
170 | }]),
171 | ]);
172 | }
173 | console.log(config);
174 | module.exports = config;
175 |
--------------------------------------------------------------------------------
/client/build/webpack.server.config.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack');
2 | const merge = require('webpack-merge');
3 | const nodeExternals = require('webpack-node-externals');
4 | const base = require('./webpack.base.config');
5 | const VueSSRServerPlugin = require('vue-server-renderer/server-plugin');
6 | const { resolve, join } = require('path');
7 | const CLIENT_FOLDER = resolve(__dirname, '../');
8 | let config = merge(base, {
9 | target: 'node',
10 | devtool: '#source-map',
11 | entry: CLIENT_FOLDER + '/src/modules/front/entry-server.js',
12 | output: {
13 | filename: 'server-bundle.js',
14 | libraryTarget: 'commonjs2',
15 | },
16 | resolve: {
17 | },
18 | externals: nodeExternals({
19 | whitelist: [/\.vue$/, /\.css$/],
20 | }),
21 | plugins: [
22 | new webpack.DefinePlugin({
23 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
24 | 'process.env.VUE_ENV': '"server"',
25 | }),
26 | new VueSSRServerPlugin(),
27 | ],
28 | });
29 | module.exports = config;
30 |
--------------------------------------------------------------------------------
/client/src/api/article.js:
--------------------------------------------------------------------------------
1 | import Axios from 'axios';
2 | // 为了让服务端渲染正确请求数据
3 | if (typeof window === 'undefined') {
4 | Axios.defaults.baseURL = 'http://127.0.0.1:8889';
5 | }
6 | export default {
7 | createArticle(title, content, publish, tags) {
8 | let abstract;
9 | if (content.indexOf('') !== -1) {
10 | abstract = content.split('')[0];
11 | } else {
12 | abstract = '';
13 | }
14 | return Axios.post('/api/articles', { title, content, publish, abstract, tags });
15 | },
16 | getAllArticles(tag = '', page = 1, limit = 0) {
17 | return Axios.get(`/api/articles?tag=${tag}&page=${page}&limit=${limit}`);
18 | },
19 | getAllPublishArticles(tag = '', page = 1, limit = 0) {
20 | return Axios.get(`/api/publishArticles?tag=${tag}&page=${page}&limit=${limit}`);
21 | },
22 | saveArticle(id, article) {
23 | console.log(article);
24 | return Axios.patch('/api/articles/' + id, article);
25 | },
26 | publishArticle(id) {
27 | return Axios.patch('/api/articles/' + id, { publish: true });
28 | },
29 | notPublishArticle(id) {
30 | return Axios.patch('/api/articles/' + id, { publish: false });
31 | },
32 | deleteArticle(id) {
33 | return Axios.delete('/api/articles/' + id);
34 | },
35 | getArticle(id) {
36 | return Axios.get('/api/articles/' + id);
37 | },
38 | };
39 |
--------------------------------------------------------------------------------
/client/src/api/login.js:
--------------------------------------------------------------------------------
1 | import Axios from 'axios';
2 | export default {
3 | createToken(username, password) {
4 | return Axios.post('/api/token', { username, password });
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/client/src/api/tag.js:
--------------------------------------------------------------------------------
1 | import Axios from 'axios';
2 | // 为了让服务端渲染正确请求数据
3 | if (typeof window === 'undefined') {
4 | Axios.defaults.baseURL = 'http://127.0.0.1:8889';
5 | }
6 | export default {
7 | createTag(name) {
8 | return Axios.post('/api/tags', {name: name});
9 | },
10 | getAllTags() {
11 | return Axios.get('/api/tags');
12 | },
13 | modifyTag(id, name) {
14 | return Axios.patch('/api/tags/' + id, name);
15 | },
16 | deleteTag(id) {
17 | return Axios.delete('/api/tags/' + id);
18 | },
19 | };
20 |
--------------------------------------------------------------------------------
/client/src/components/Loading.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
{{ loadingMsg }}
10 |
11 |
12 |
13 |
14 |
31 |
32 |
39 |
40 |
145 |
--------------------------------------------------------------------------------
/client/src/components/Pagination.vue:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
15 |
89 |
90 |
111 |
--------------------------------------------------------------------------------
/client/src/lib/debounce.js:
--------------------------------------------------------------------------------
1 | export default function (fn, time) {
2 | let timer;
3 | return function () {
4 | var args = arguments;
5 | clearTimeout(timer);
6 | timer = setTimeout(() => {
7 | fn.apply(this, args);
8 | }, time || 300);
9 | };
10 | }
11 |
--------------------------------------------------------------------------------
/client/src/lib/marked.js:
--------------------------------------------------------------------------------
1 | import highlight from 'highlight.js';
2 | import marked from 'marked';
3 | const languages = ['cpp', 'xml', 'bash', 'coffeescript', 'css', 'markdown', 'http', 'java', 'javascript', 'json', 'less', 'makefile', 'nginx', 'php', 'python', 'scss', 'sql', 'stylus'];
4 | highlight.registerLanguage('cpp', require('highlight.js/lib/languages/cpp'));
5 | highlight.registerLanguage('xml', require('highlight.js/lib/languages/xml'));
6 | highlight.registerLanguage('bash', require('highlight.js/lib/languages/bash'));
7 | highlight.registerLanguage('coffeescript', require('highlight.js/lib/languages/coffeescript'));
8 | highlight.registerLanguage('css', require('highlight.js/lib/languages/css'));
9 | highlight.registerLanguage('markdown', require('highlight.js/lib/languages/markdown'));
10 | highlight.registerLanguage('http', require('highlight.js/lib/languages/http'));
11 | highlight.registerLanguage('java', require('highlight.js/lib/languages/java'));
12 | highlight.registerLanguage('javascript', require('highlight.js/lib/languages/javascript'));
13 | highlight.registerLanguage('json', require('highlight.js/lib/languages/json'));
14 | highlight.registerLanguage('less', require('highlight.js/lib/languages/less'));
15 | highlight.registerLanguage('makefile', require('highlight.js/lib/languages/makefile'));
16 | highlight.registerLanguage('nginx', require('highlight.js/lib/languages/nginx'));
17 | highlight.registerLanguage('php', require('highlight.js/lib/languages/php'));
18 | highlight.registerLanguage('python', require('highlight.js/lib/languages/python'));
19 | highlight.registerLanguage('scss', require('highlight.js/lib/languages/scss'));
20 | highlight.registerLanguage('sql', require('highlight.js/lib/languages/sql'));
21 | highlight.registerLanguage('stylus', require('highlight.js/lib/languages/stylus'));
22 | highlight.configure({
23 | classPrefix: '', // don't append class prefix
24 | });
25 | // https://github.com/chjj/marked
26 | marked.setOptions({
27 | renderer: new marked.Renderer(),
28 | gfm: true,
29 | pedantic: false,
30 | sanitize: false,
31 | tables: true,
32 | breaks: true,
33 | smartLists: true,
34 | smartypants: true,
35 | highlight: function (code, lang) {
36 | if (!lang) {
37 | return;
38 | }
39 | if (!~languages.indexOf(lang)) {
40 | return highlight.highlightAuto(code).value;
41 | }
42 | return highlight.highlight(lang, code).value;
43 | },
44 | });
45 | export default marked;
46 |
--------------------------------------------------------------------------------
/client/src/lib/throttle.js:
--------------------------------------------------------------------------------
1 | export default function (fn, time) {
2 | var timer;
3 | var firstTime = true;
4 | return function () {
5 | var args = arguments;
6 | if (firstTime) {
7 | fn.apply(this, arguments);
8 | firstTime = false;
9 | return firstTime;
10 | }
11 | if (timer) {
12 | return false;
13 | }
14 | timer = setTimeout(() => {
15 | clearTimeout(timer);
16 | timer = null;
17 | fn.apply(this, args);
18 | }, time || 500);
19 | };
20 | }
--------------------------------------------------------------------------------
/client/src/modules/admin/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
12 |
13 |
26 |
--------------------------------------------------------------------------------
/client/src/modules/admin/app.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import VueRouter from 'vue-router';
3 | import Axios from 'axios';
4 |
5 | import { Message } from 'element-ui';
6 | import { MessageBox } from 'element-ui';
7 |
8 | import './assets/stylus/main.styl';
9 |
10 | import App from './App.vue';
11 | import store from './store';
12 |
13 | // 按需引入element-ui相关弹出
14 | Vue.prototype.$msgbox = MessageBox;
15 | Vue.prototype.$alert = MessageBox.alert;
16 | Vue.prototype.$confirm = MessageBox.confirm;
17 | Vue.prototype.$prompt = MessageBox.prompt;
18 | Vue.prototype.$message = (options) => { // 重新定义默认参数
19 | options = Object.assign(options, { duration: 500 });
20 | return Message(options);
21 | };
22 | Vue.prototype.$message.error = (err) => { // 重新定义默认参数
23 | var options = {
24 | message: err,
25 | duration: 500,
26 | type: 'error',
27 | };
28 | return Message(options);
29 | };
30 |
31 | Vue.use(VueRouter);
32 |
33 | const Login = resolve => require(['./components/Login.vue'], resolve);
34 | const Admin = resolve => require(['./components/Admin.vue'], resolve);
35 | const routes = [
36 | { path: '/admin/login', component: Login, meta: { authPage: true } },
37 | { path: '/admin', component: Admin }, {
38 | path: '*',
39 | redirect: '/admin', // 输入其他不存在的地址自动跳回首页
40 | },
41 | ];
42 |
43 | const router = new VueRouter({
44 | mode: 'history',
45 | routes,
46 | });
47 |
48 | router.beforeEach((to, from, next) => {
49 | // console.log(store.state);
50 | console.log(to);
51 | if (to.meta.authPage) { // login
52 | console.log('login');
53 | if (store.state.auth.token) {
54 | next('/admin');
55 | }
56 | next();
57 | } else {
58 | console.log('admin');
59 | if (store.state.auth.token) {
60 | Axios.defaults.headers.common['Authorization'] = 'Bearer ' + store.state.auth.token; // 全局设定header的token验证,注意Bearer后有个空格
61 | next();
62 | } else {
63 | console.log('没有token');
64 | next('/admin/login');
65 | }
66 | }
67 | });
68 |
69 | // axios拦截返回,拦截token过期
70 | Axios.interceptors.response.use(function (response) {
71 | return response;
72 | }, function (error) {
73 | if (error.response.data.error.indexOf('token') !== -1) {
74 | store.commit('DELETE_TOKEN');
75 | }
76 | return Promise.reject(error);
77 | });
78 |
79 | new Vue({
80 | el: '#app',
81 | render: h => h(App),
82 | router,
83 | store,
84 | });
85 |
--------------------------------------------------------------------------------
/client/src/modules/admin/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BUPT-HJM/vue-blog/747306b702fe84d215478403d61f49332e15f829/client/src/modules/admin/assets/logo.png
--------------------------------------------------------------------------------
/client/src/modules/admin/assets/stylus/_settings.styl:
--------------------------------------------------------------------------------
1 | // font faces
2 | $body-font = "Helvetica Neue",Helvetica,"PingFang SC","Hiragino Sans GB","Microsoft YaHei","微软雅黑",Arial,sans-serif;
3 |
4 |
5 | // font sizes
6 | $body-font-size = 15px
7 |
8 | // colors
9 | $grey-bg = #efefef
10 | $blue = #0288D1
11 | $orange = #FF8400
12 | $grey-publish = #aab2b3
13 | $login-text = #757575
14 |
15 | // border
16 | $border-radius = 3px
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/client/src/modules/admin/assets/stylus/_syntax.styl:
--------------------------------------------------------------------------------
1 | .gutter pre
2 | color #999
3 | pre
4 | color: #525252
5 | .function .keyword,
6 | .constant
7 | color: #0092db
8 | .keyword,
9 | .attribute
10 | color: #e96900
11 | .number,
12 | .literal
13 | color: #AE81FF
14 | .tag,
15 | .tag .title,
16 | .change,
17 | .winutils,
18 | .flow,
19 | .lisp .title,
20 | .clojure .built_in,
21 | .nginx .title,
22 | .tex .special
23 | color: #2973b7
24 | .class .title
25 | color: #42b983
26 | .symbol,
27 | .symbol .string,
28 | .value,
29 | .regexp
30 | color: $blue
31 | .title
32 | color: #A6E22E
33 | .tag .value,
34 | .string,
35 | .subst,
36 | .haskell .type,
37 | .preprocessor,
38 | .ruby .class .parent,
39 | .built_in,
40 | .sql .aggregate,
41 | .django .template_tag,
42 | .django .variable,
43 | .smalltalk .class,
44 | .javadoc,
45 | .django .filter .argument,
46 | .smalltalk .localvars,
47 | .smalltalk .array,
48 | .attr_selector,
49 | .pseudo,
50 | .addition,
51 | .stream,
52 | .envvar,
53 | .apache .tag,
54 | .apache .cbracket,
55 | .tex .command,
56 | .prompt
57 | color: $blue
58 | .comment,
59 | .java .annotation,
60 | .python .decorator,
61 | .template_comment,
62 | .pi,
63 | .doctype,
64 | .deletion,
65 | .shebang,
66 | .apache .sqbracket,
67 | .tex .formula
68 | color: #b3b3b3
69 | .coffeescript .javascript,
70 | .javascript .xml,
71 | .tex .formula,
72 | .xml .javascript,
73 | .xml .vbscript,
74 | .xml .css,
75 | .xml .cdata
76 | opacity: 0.5
--------------------------------------------------------------------------------
/client/src/modules/admin/assets/stylus/main.styl:
--------------------------------------------------------------------------------
1 | @import "reset"
2 | @import "_settings"
3 | @import "_syntax"
4 |
5 |
6 |
7 | [v-cloak]
8 | display none
9 |
10 | html, body
11 | font-size $body-font-size
12 | input, button
13 | border-radius $border-radius
14 |
--------------------------------------------------------------------------------
/client/src/modules/admin/assets/stylus/preview.styl:
--------------------------------------------------------------------------------
1 | @import './_settings.styl'
2 | // modify github markdown css
3 | // @font-face {
4 | // font-family: octicons-link;
5 | // src: url(data:font/woff;charset=utf-8;base64,d09GRgABAAAAAAZwABAAAAAACFQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABEU0lHAAAGaAAAAAgAAAAIAAAAAUdTVUIAAAZcAAAACgAAAAoAAQAAT1MvMgAAAyQAAABJAAAAYFYEU3RjbWFwAAADcAAAAEUAAACAAJThvmN2dCAAAATkAAAABAAAAAQAAAAAZnBnbQAAA7gAAACyAAABCUM+8IhnYXNwAAAGTAAAABAAAAAQABoAI2dseWYAAAFsAAABPAAAAZwcEq9taGVhZAAAAsgAAAA0AAAANgh4a91oaGVhAAADCAAAABoAAAAkCA8DRGhtdHgAAAL8AAAADAAAAAwGAACfbG9jYQAAAsAAAAAIAAAACABiATBtYXhwAAACqAAAABgAAAAgAA8ASm5hbWUAAAToAAABQgAAAlXu73sOcG9zdAAABiwAAAAeAAAAME3QpOBwcmVwAAAEbAAAAHYAAAB/aFGpk3jaTY6xa8JAGMW/O62BDi0tJLYQincXEypYIiGJjSgHniQ6umTsUEyLm5BV6NDBP8Tpts6F0v+k/0an2i+itHDw3v2+9+DBKTzsJNnWJNTgHEy4BgG3EMI9DCEDOGEXzDADU5hBKMIgNPZqoD3SilVaXZCER3/I7AtxEJLtzzuZfI+VVkprxTlXShWKb3TBecG11rwoNlmmn1P2WYcJczl32etSpKnziC7lQyWe1smVPy/Lt7Kc+0vWY/gAgIIEqAN9we0pwKXreiMasxvabDQMM4riO+qxM2ogwDGOZTXxwxDiycQIcoYFBLj5K3EIaSctAq2kTYiw+ymhce7vwM9jSqO8JyVd5RH9gyTt2+J/yUmYlIR0s04n6+7Vm1ozezUeLEaUjhaDSuXHwVRgvLJn1tQ7xiuVv/ocTRF42mNgZGBgYGbwZOBiAAFGJBIMAAizAFoAAABiAGIAznjaY2BkYGAA4in8zwXi+W2+MjCzMIDApSwvXzC97Z4Ig8N/BxYGZgcgl52BCSQKAA3jCV8CAABfAAAAAAQAAEB42mNgZGBg4f3vACQZQABIMjKgAmYAKEgBXgAAeNpjYGY6wTiBgZWBg2kmUxoDA4MPhGZMYzBi1AHygVLYQUCaawqDA4PChxhmh/8ODDEsvAwHgMKMIDnGL0x7gJQCAwMAJd4MFwAAAHjaY2BgYGaA4DAGRgYQkAHyGMF8NgYrIM3JIAGVYYDT+AEjAwuDFpBmA9KMDEwMCh9i/v8H8sH0/4dQc1iAmAkALaUKLgAAAHjaTY9LDsIgEIbtgqHUPpDi3gPoBVyRTmTddOmqTXThEXqrob2gQ1FjwpDvfwCBdmdXC5AVKFu3e5MfNFJ29KTQT48Ob9/lqYwOGZxeUelN2U2R6+cArgtCJpauW7UQBqnFkUsjAY/kOU1cP+DAgvxwn1chZDwUbd6CFimGXwzwF6tPbFIcjEl+vvmM/byA48e6tWrKArm4ZJlCbdsrxksL1AwWn/yBSJKpYbq8AXaaTb8AAHja28jAwOC00ZrBeQNDQOWO//sdBBgYGRiYWYAEELEwMTE4uzo5Zzo5b2BxdnFOcALxNjA6b2ByTswC8jYwg0VlNuoCTWAMqNzMzsoK1rEhNqByEyerg5PMJlYuVueETKcd/89uBpnpvIEVomeHLoMsAAe1Id4AAAAAAAB42oWQT07CQBTGv0JBhagk7HQzKxca2sJCE1hDt4QF+9JOS0nbaaYDCQfwCJ7Au3AHj+LO13FMmm6cl7785vven0kBjHCBhfpYuNa5Ph1c0e2Xu3jEvWG7UdPDLZ4N92nOm+EBXuAbHmIMSRMs+4aUEd4Nd3CHD8NdvOLTsA2GL8M9PODbcL+hD7C1xoaHeLJSEao0FEW14ckxC+TU8TxvsY6X0eLPmRhry2WVioLpkrbp84LLQPGI7c6sOiUzpWIWS5GzlSgUzzLBSikOPFTOXqly7rqx0Z1Q5BAIoZBSFihQYQOOBEdkCOgXTOHA07HAGjGWiIjaPZNW13/+lm6S9FT7rLHFJ6fQbkATOG1j2OFMucKJJsxIVfQORl+9Jyda6Sl1dUYhSCm1dyClfoeDve4qMYdLEbfqHf3O/AdDumsjAAB42mNgYoAAZQYjBmyAGYQZmdhL8zLdDEydARfoAqIAAAABAAMABwAKABMAB///AA8AAQAAAAAAAAAAAAAAAAABAAAAAA==) format('woff');
6 | // }
7 |
8 | .editor-box
9 | .CodeMirror
10 | height: 400px
11 | .editor-preview,
12 | .editor-preview-side
13 | -ms-text-size-adjust: 100%;
14 | -webkit-text-size-adjust: 100%;
15 | line-height: 1.5;
16 | color: #24292e;
17 | font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
18 | font-size: 16px;
19 | line-height: 1.5;
20 | word-wrap: break-word;
21 | background: white;
22 | .pl-c {
23 | color: #969896;
24 | }
25 |
26 | .pl-c1,
27 | .pl-s .pl-v {
28 | color: #0086b3;
29 | }
30 |
31 | .pl-e,
32 | .pl-en {
33 | color: #795da3;
34 | }
35 |
36 | .pl-smi,
37 | .pl-s .pl-s1 {
38 | color: #333;
39 | }
40 |
41 | .pl-ent {
42 | color: #63a35c;
43 | }
44 |
45 | .pl-k {
46 | color: #a71d5d;
47 | }
48 |
49 | .pl-s,
50 | .pl-pds,
51 | .pl-s .pl-pse .pl-s1,
52 | .pl-sr,
53 | .pl-sr .pl-cce,
54 | .pl-sr .pl-sre,
55 | .pl-sr .pl-sra {
56 | color: #183691;
57 | }
58 |
59 | .pl-v,
60 | .pl-smw {
61 | color: #ed6a43;
62 | }
63 |
64 | .pl-bu {
65 | color: #b52a1d;
66 | }
67 |
68 | .pl-ii {
69 | color: #f8f8f8;
70 | background-color: #b52a1d;
71 | }
72 |
73 | .pl-c2 {
74 | color: #f8f8f8;
75 | background-color: #b52a1d;
76 | }
77 |
78 | .pl-c2::before {
79 | content: "^M";
80 | }
81 |
82 | .pl-sr .pl-cce {
83 | font-weight: bold;
84 | color: #63a35c;
85 | }
86 |
87 | .pl-ml {
88 | color: #693a17;
89 | }
90 |
91 | .pl-mh,
92 | .pl-mh .pl-en,
93 | .pl-ms {
94 | font-weight: bold;
95 | color: #1d3e81;
96 | }
97 |
98 | .pl-mq {
99 | color: #008080;
100 | }
101 |
102 | .pl-mi {
103 | font-style: italic;
104 | color: #333;
105 | }
106 |
107 | .pl-mb {
108 | font-weight: bold;
109 | color: #333;
110 | }
111 |
112 | .pl-md {
113 | color: #bd2c00;
114 | background-color: #ffecec;
115 | }
116 |
117 | .pl-mi1 {
118 | color: #55a532;
119 | background-color: #eaffea;
120 | }
121 |
122 | .pl-mc {
123 | color: #ef9700;
124 | background-color: #ffe3b4;
125 | }
126 |
127 | .pl-mi2 {
128 | color: #d8d8d8;
129 | background-color: #808080;
130 | }
131 |
132 | .pl-mdr {
133 | font-weight: bold;
134 | color: #795da3;
135 | }
136 |
137 | .pl-mo {
138 | color: #1d3e81;
139 | }
140 |
141 | .pl-ba {
142 | color: #595e62;
143 | }
144 |
145 | .pl-sg {
146 | color: #c0c0c0;
147 | }
148 |
149 | .pl-corl {
150 | text-decoration: underline;
151 | color: #183691;
152 | }
153 |
154 | .octicon {
155 | display: inline-block;
156 | vertical-align: text-top;
157 | fill: currentColor;
158 | }
159 |
160 | a {
161 | background-color: transparent;
162 | -webkit-text-decoration-skip: objects;
163 | }
164 |
165 | a:active,
166 | a:hover {
167 | outline-width: 0;
168 | }
169 |
170 | strong {
171 | font-weight: inherit;
172 | }
173 |
174 | strong {
175 | font-weight: bolder;
176 | }
177 |
178 | h1 {
179 | font-size: 2em;
180 | margin: 0.67em 0;
181 | }
182 |
183 | img {
184 | border-style: none;
185 | }
186 |
187 | svg:not(:root) {
188 | overflow: hidden;
189 | }
190 |
191 | code,
192 | kbd,
193 | pre {
194 | font-family: monospace, monospace;
195 | font-size: 1em;
196 | }
197 |
198 | hr {
199 | box-sizing: content-box;
200 | height: 0;
201 | overflow: visible;
202 | }
203 |
204 | input {
205 | font: inherit;
206 | margin: 0;
207 | }
208 |
209 | input {
210 | overflow: visible;
211 | }
212 |
213 | [type="checkbox"] {
214 | box-sizing: border-box;
215 | padding: 0;
216 | }
217 |
218 | * {
219 | box-sizing: border-box;
220 | }
221 |
222 | input {
223 | font-family: inherit;
224 | font-size: inherit;
225 | line-height: inherit;
226 | }
227 |
228 | a {
229 | color: #0366d6;
230 | text-decoration: none;
231 | }
232 |
233 | a:hover {
234 | text-decoration: underline;
235 | }
236 |
237 | strong {
238 | font-weight: 600;
239 | }
240 |
241 | hr {
242 | height: 0;
243 | margin: 15px 0;
244 | overflow: hidden;
245 | background: transparent;
246 | border: 0;
247 | border-bottom: 1px solid #dfe2e5;
248 | }
249 |
250 | hr::before {
251 | display: table;
252 | content: "";
253 | }
254 |
255 | hr::after {
256 | display: table;
257 | clear: both;
258 | content: "";
259 | }
260 |
261 | table {
262 | border-spacing: 0;
263 | border-collapse: collapse;
264 | }
265 |
266 | td,
267 | th {
268 | padding: 0;
269 | }
270 |
271 | h1,
272 | h2,
273 | h3,
274 | h4,
275 | h5,
276 | h6 {
277 | margin-top: 0;
278 | margin-bottom: 0;
279 | }
280 |
281 | h1 {
282 | font-size: 32px;
283 | font-weight: 600;
284 | }
285 |
286 | h2 {
287 | font-size: 24px;
288 | font-weight: 600;
289 | }
290 |
291 | h3 {
292 | font-size: 20px;
293 | font-weight: 600;
294 | }
295 |
296 | h4 {
297 | font-size: 16px;
298 | font-weight: 600;
299 | }
300 |
301 | h5 {
302 | font-size: 14px;
303 | font-weight: 600;
304 | }
305 |
306 | h6 {
307 | font-size: 12px;
308 | font-weight: 600;
309 | }
310 |
311 | p {
312 | margin-top: 0;
313 | margin-bottom: 10px;
314 | }
315 |
316 | blockquote {
317 | margin: 0;
318 | }
319 |
320 | ul,
321 | ol {
322 | padding-left: 0;
323 | margin-top: 0;
324 | margin-bottom: 0;
325 | }
326 |
327 | ol ol,
328 | ul ol {
329 | list-style-type: lower-roman;
330 | }
331 |
332 | ul ul ol,
333 | ul ol ol,
334 | ol ul ol,
335 | ol ol ol {
336 | list-style-type: lower-alpha;
337 | }
338 |
339 | dd {
340 | margin-left: 0;
341 | }
342 |
343 | code {
344 | font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;
345 | font-size: 12px;
346 | }
347 |
348 | pre {
349 | margin-top: 0;
350 | margin-bottom: 0;
351 | font: 12px "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;
352 | }
353 |
354 | .octicon {
355 | vertical-align: text-bottom;
356 | }
357 |
358 | .pl-0 {
359 | padding-left: 0 !important;
360 | }
361 |
362 | .pl-1 {
363 | padding-left: 4px !important;
364 | }
365 |
366 | .pl-2 {
367 | padding-left: 8px !important;
368 | }
369 |
370 | .pl-3 {
371 | padding-left: 16px !important;
372 | }
373 |
374 | .pl-4 {
375 | padding-left: 24px !important;
376 | }
377 |
378 | .pl-5 {
379 | padding-left: 32px !important;
380 | }
381 |
382 | .pl-6 {
383 | padding-left: 40px !important;
384 | }
385 |
386 | .markdown-body::before {
387 | display: table;
388 | content: "";
389 | }
390 |
391 | .markdown-body::after {
392 | display: table;
393 | clear: both;
394 | content: "";
395 | }
396 |
397 | .markdown-body>*:first-child {
398 | margin-top: 0 !important;
399 | }
400 |
401 | .markdown-body>*:last-child {
402 | margin-bottom: 0 !important;
403 | }
404 |
405 | a:not([href]) {
406 | color: inherit;
407 | text-decoration: none;
408 | }
409 |
410 | .anchor {
411 | float: left;
412 | padding-right: 4px;
413 | margin-left: -20px;
414 | line-height: 1;
415 | }
416 |
417 | .anchor:focus {
418 | outline: none;
419 | }
420 |
421 | p,
422 | blockquote,
423 | ul,
424 | ol,
425 | dl,
426 | table,
427 | pre {
428 | margin-top: 0;
429 | margin-bottom: 16px;
430 | }
431 |
432 | hr {
433 | height: 0.25em;
434 | padding: 0;
435 | margin: 24px 0;
436 | background-color: #e1e4e8;
437 | border: 0;
438 | }
439 |
440 | blockquote {
441 | padding: 0 1em;
442 | color: #6a737d;
443 | border-left: 0.25em solid #dfe2e5;
444 | }
445 |
446 | blockquote>:first-child {
447 | margin-top: 0;
448 | }
449 |
450 | blockquote>:last-child {
451 | margin-bottom: 0;
452 | }
453 |
454 | kbd {
455 | display: inline-block;
456 | padding: 3px 5px;
457 | font-size: 11px;
458 | line-height: 10px;
459 | color: #444d56;
460 | vertical-align: middle;
461 | background-color: #fafbfc;
462 | border: solid 1px #c6cbd1;
463 | border-bottom-color: #959da5;
464 | border-radius: 3px;
465 | box-shadow: inset 0 -1px 0 #959da5;
466 | }
467 |
468 | h1,
469 | h2,
470 | h3,
471 | h4,
472 | h5,
473 | h6 {
474 | margin-top: 24px;
475 | margin-bottom: 16px;
476 | font-weight: 600;
477 | line-height: 1.25;
478 | }
479 |
480 | h1 .octicon-link,
481 | h2 .octicon-link,
482 | h3 .octicon-link,
483 | h4 .octicon-link,
484 | h5 .octicon-link,
485 | h6 .octicon-link {
486 | color: #1b1f23;
487 | vertical-align: middle;
488 | visibility: hidden;
489 | }
490 |
491 | h1:hover .anchor,
492 | h2:hover .anchor,
493 | h3:hover .anchor,
494 | h4:hover .anchor,
495 | h5:hover .anchor,
496 | h6:hover .anchor {
497 | text-decoration: none;
498 | }
499 |
500 | h1:hover .anchor .octicon-link,
501 | h2:hover .anchor .octicon-link,
502 | h3:hover .anchor .octicon-link,
503 | h4:hover .anchor .octicon-link,
504 | h5:hover .anchor .octicon-link,
505 | h6:hover .anchor .octicon-link {
506 | visibility: visible;
507 | }
508 |
509 | h1 {
510 | padding-bottom: 0.3em;
511 | font-size: 2em;
512 | border-bottom: 1px solid #eaecef;
513 | }
514 |
515 | h2 {
516 | padding-bottom: 0.3em;
517 | font-size: 1.5em;
518 | border-bottom: 1px solid #eaecef;
519 | }
520 |
521 | h3 {
522 | font-size: 1.25em;
523 | }
524 |
525 | h4 {
526 | font-size: 1em;
527 | }
528 |
529 | h5 {
530 | font-size: 0.875em;
531 | }
532 |
533 | h6 {
534 | font-size: 0.85em;
535 | color: #6a737d;
536 | }
537 |
538 | ul,
539 | ol {
540 | padding-left: 2em;
541 | }
542 |
543 | ul ul,
544 | ul ol,
545 | ol ol,
546 | ol ul {
547 | margin-top: 0;
548 | margin-bottom: 0;
549 | }
550 |
551 | li>p {
552 | margin-top: 16px;
553 | }
554 |
555 | li+li {
556 | margin-top: 0.25em;
557 | }
558 |
559 | dl {
560 | padding: 0;
561 | }
562 |
563 | dl dt {
564 | padding: 0;
565 | margin-top: 16px;
566 | font-size: 1em;
567 | font-style: italic;
568 | font-weight: 600;
569 | }
570 |
571 | dl dd {
572 | padding: 0 16px;
573 | margin-bottom: 16px;
574 | }
575 |
576 | table {
577 | display: block;
578 | width: 100%;
579 | overflow: auto;
580 | }
581 |
582 | table th {
583 | font-weight: 600;
584 | }
585 |
586 | table th,
587 | table td {
588 | padding: 6px 13px;
589 | border: 1px solid #dfe2e5;
590 | }
591 |
592 | table tr {
593 | background-color: #fff;
594 | border-top: 1px solid #c6cbd1;
595 | }
596 |
597 | table tr:nth-child(2n) {
598 | background-color: #f6f8fa;
599 | }
600 |
601 | img {
602 | max-width: 100%;
603 | box-sizing: content-box;
604 | background-color: #fff;
605 | }
606 |
607 | code {
608 | padding: 0;
609 | padding-top: 0.2em;
610 | padding-bottom: 0.2em;
611 | margin: 0;
612 | font-size: 85%;
613 | background-color: rgba(27,31,35,0.05);
614 | border-radius: 3px;
615 | }
616 |
617 | code::before,
618 | code::after {
619 | letter-spacing: -0.2em;
620 | content: "\00a0";
621 | }
622 |
623 | pre {
624 | word-wrap: normal;
625 | }
626 |
627 | pre>code {
628 | padding: 0;
629 | margin: 0;
630 | font-size: 100%;
631 | word-break: normal;
632 | white-space: pre;
633 | background: transparent;
634 | border: 0;
635 | }
636 |
637 | .highlight {
638 | margin-bottom: 16px;
639 | }
640 |
641 | .highlight pre {
642 | margin-bottom: 0;
643 | word-break: normal;
644 | }
645 |
646 | .highlight pre,
647 | pre {
648 | padding: 16px;
649 | overflow: auto;
650 | font-size: 85%;
651 | line-height: 1.45;
652 | background-color: #f6f8fa;
653 | border-radius: 3px;
654 | }
655 |
656 | pre code {
657 | display: inline;
658 | max-width: auto;
659 | padding: 0;
660 | margin: 0;
661 | overflow: visible;
662 | line-height: inherit;
663 | word-wrap: normal;
664 | background-color: transparent;
665 | border: 0;
666 | }
667 |
668 | pre code::before,
669 | pre code::after {
670 | content: normal;
671 | }
672 |
673 | .full-commit .btn-outline:not(:disabled):hover {
674 | color: #005cc5;
675 | border-color: #005cc5;
676 | }
677 |
678 | kbd {
679 | display: inline-block;
680 | padding: 3px 5px;
681 | font: 11px "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;
682 | line-height: 10px;
683 | color: #444d56;
684 | vertical-align: middle;
685 | background-color: #fcfcfc;
686 | border: solid 1px #c6cbd1;
687 | border-bottom-color: #959da5;
688 | border-radius: 3px;
689 | box-shadow: inset 0 -1px 0 #959da5;
690 | }
691 |
692 | :checked+.radio-label {
693 | position: relative;
694 | z-index: 1;
695 | border-color: #0366d6;
696 | }
697 |
698 | .task-list-item {
699 | list-style-type: none;
700 | }
701 |
702 | .task-list-item+.task-list-item {
703 | margin-top: 3px;
704 | }
705 |
706 | .task-list-item input {
707 | margin: 0 0.2em 0.25em -1.6em;
708 | vertical-align: middle;
709 | }
710 |
711 | hr {
712 | border-bottom-color: #eee;
713 | }
--------------------------------------------------------------------------------
/client/src/modules/admin/assets/stylus/reset.styl:
--------------------------------------------------------------------------------
1 | /*!
2 | * ress.css • v1.1.2
3 | * MIT License
4 | * github.com/filipelinhares/ress
5 | */
6 |
7 | /* # =================================================================
8 | # Global selectors
9 | # ================================================================= */
10 |
11 | html {
12 | box-sizing: border-box;
13 | //overflow-y: scroll; /* All browsers without overlaying scrollbars */
14 | -webkit-text-size-adjust: 100%; /* iOS 8+ */
15 | }
16 |
17 | *,
18 | ::before,
19 | ::after {
20 | box-sizing: inherit;
21 | }
22 |
23 | ::before,
24 | ::after {
25 | text-decoration: inherit; /* Inherit text-decoration and vertical align to ::before and ::after pseudo elements */
26 | vertical-align: inherit;
27 | }
28 |
29 | /* Remove margin, padding of all elements and set background-no-repeat as default */
30 | * {
31 | background-repeat: no-repeat; /* Set `background-repeat: no-repeat` to all elements */
32 | padding: 0; /* Reset `padding` and `margin` of all elements */
33 | margin: 0;
34 | }
35 |
36 | /* # =================================================================
37 | # General elements
38 | # ================================================================= */
39 |
40 | /* Add the correct display in iOS 4-7.*/
41 | audio:not([controls]) {
42 | display: none;
43 | height: 0;
44 | }
45 |
46 | hr {
47 | overflow: visible; /* Show the overflow in Edge and IE */
48 | }
49 |
50 | /*
51 | * Correct `block` display not defined for any HTML5 element in IE 8/9
52 | * Correct `block` display not defined for `details` or `summary` in IE 10/11
53 | * and Firefox
54 | * Correct `block` display not defined for `main` in IE 11
55 | */
56 | article,
57 | aside,
58 | details,
59 | figcaption,
60 | figure,
61 | footer,
62 | header,
63 | main,
64 | menu,
65 | nav,
66 | section,
67 | summary {
68 | display: block;
69 | }
70 |
71 | summary {
72 | display: list-item; /* Add the correct display in all browsers */
73 | }
74 |
75 | small {
76 | font-size: 80%; /* Set font-size to 80% in `small` elements */
77 | }
78 |
79 | [hidden],
80 | template {
81 | display: none; /* Add the correct display in IE */
82 | }
83 |
84 | abbr[title] {
85 | border-bottom: 1px dotted; /* Add a bordered underline effect in all browsers */
86 | text-decoration: none; /* Remove text decoration in Firefox 40+ */
87 | }
88 |
89 | a {
90 | background-color: transparent; /* Remove the gray background on active links in IE 10 */
91 | -webkit-text-decoration-skip: objects; /* Remove gaps in links underline in iOS 8+ and Safari 8+ */
92 | }
93 |
94 | a:active,
95 | a:hover {
96 | outline-width: 0; /* Remove the outline when hovering in all browsers */
97 | }
98 |
99 | code,
100 | kbd,
101 | pre,
102 | samp {
103 | font-family: monospace, monospace; /* Specify the font family of code elements */
104 | }
105 |
106 | b,
107 | strong {
108 | font-weight: bolder; /* Correct style set to `bold` in Edge 12+, Safari 6.2+, and Chrome 18+ */
109 | }
110 |
111 | dfn {
112 | font-style: italic; /* Address styling not present in Safari and Chrome */
113 | }
114 |
115 | /* Address styling not present in IE 8/9 */
116 | mark {
117 | background-color: #ff0;
118 | color: #000;
119 | }
120 |
121 | /* https://gist.github.com/unruthless/413930 */
122 | sub,
123 | sup {
124 | font-size: 75%;
125 | line-height: 0;
126 | position: relative;
127 | vertical-align: baseline;
128 | }
129 |
130 | sub {
131 | bottom: -0.25em;
132 | }
133 |
134 | sup {
135 | top: -0.5em;
136 | }
137 |
138 | /* # =================================================================
139 | # Forms
140 | # ================================================================= */
141 |
142 | input {
143 | border-radius: 0;
144 | }
145 |
146 | /* Apply cursor pointer to button elements */
147 | button,
148 | [type="button"],
149 | [type="reset"],
150 | [type="submit"],
151 | [role="button"] {
152 | cursor: pointer;
153 | }
154 |
155 | /* Replace pointer cursor in disabled elements */
156 | [disabled] {
157 | cursor: default;
158 | }
159 |
160 | [type="number"] {
161 | width: auto; /* Firefox 36+ */
162 | }
163 |
164 | [type="search"] {
165 | -webkit-appearance: textfield; /* Safari 8+ */
166 | }
167 |
168 | [type="search"]::-webkit-search-cancel-button,
169 | [type="search"]::-webkit-search-decoration {
170 | -webkit-appearance: none; /* Safari 8 */
171 | }
172 |
173 | textarea {
174 | overflow: auto; /* Internet Explorer 11+ */
175 | resize: vertical; /* Specify textarea resizability */
176 | }
177 |
178 | button,
179 | input,
180 | optgroup,
181 | select,
182 | textarea {
183 | font: inherit; /* Specify font inheritance of form elements */
184 | }
185 |
186 | optgroup {
187 | font-weight: bold; /* Restore the font weight unset by the previous rule. */
188 | }
189 |
190 | button {
191 | overflow: visible; /* Address `overflow` set to `hidden` in IE 8/9/10/11 */
192 | }
193 |
194 | /* Remove inner padding and border in Firefox 4+ */
195 | button::-moz-focus-inner,
196 | [type="button"]::-moz-focus-inner,
197 | [type="reset"]::-moz-focus-inner,
198 | [type="submit"]::-moz-focus-inner {
199 | border-style: 0;
200 | padding: 0;
201 | }
202 |
203 | /* Replace focus style removed in the border reset above */
204 | button:-moz-focusring,
205 | [type="button"]::-moz-focus-inner,
206 | [type="reset"]::-moz-focus-inner,
207 | [type="submit"]::-moz-focus-inner {
208 | outline: 1px dotted ButtonText;
209 | }
210 |
211 | button,
212 | html [type="button"], /* Prevent a WebKit bug where (2) destroys native `audio` and `video`controls in Android 4 */
213 | [type="reset"],
214 | [type="submit"] {
215 | -webkit-appearance: button; /* Correct the inability to style clickable types in iOS */
216 | }
217 |
218 | button,
219 | select {
220 | text-transform: none; /* Firefox 40+, Internet Explorer 11- */
221 | }
222 |
223 | /* Remove the default button styling in all browsers */
224 | button,
225 | input,
226 | select,
227 | textarea {
228 | background-color: transparent;
229 | border-style: none;
230 | color: inherit;
231 | }
232 |
233 | /* Style select like a standard input */
234 | select {
235 | -moz-appearance: none; /* Firefox 36+ */
236 | -webkit-appearance: none; /* Chrome 41+ */
237 | }
238 |
239 | select::-ms-expand {
240 | display: none; /* Internet Explorer 11+ */
241 | }
242 |
243 | select::-ms-value {
244 | color: currentColor; /* Internet Explorer 11+ */
245 | }
246 |
247 | legend {
248 | border: 0; /* Correct `color` not being inherited in IE 8/9/10/11 */
249 | color: inherit; /* Correct the color inheritance from `fieldset` elements in IE */
250 | display: table; /* Correct the text wrapping in Edge and IE */
251 | max-width: 100%; /* Correct the text wrapping in Edge and IE */
252 | white-space: normal; /* Correct the text wrapping in Edge and IE */
253 | }
254 |
255 | ::-webkit-file-upload-button {
256 | -webkit-appearance: button; /* Correct the inability to style clickable types in iOS and Safari */
257 | font: inherit; /* Change font properties to `inherit` in Chrome and Safari */
258 | }
259 |
260 | [type="search"] {
261 | -webkit-appearance: textfield; /* Correct the odd appearance in Chrome and Safari */
262 | outline-offset: -2px; /* Correct the outline style in Safari */
263 | }
264 |
265 | /* # =================================================================
266 | # Specify media element style
267 | # ================================================================= */
268 |
269 | img {
270 | border-style: none; /* Remove border when inside `a` element in IE 8/9/10 */
271 | }
272 |
273 | /* Add the correct vertical alignment in Chrome, Firefox, and Opera */
274 | progress {
275 | vertical-align: baseline;
276 | }
277 |
278 | svg:not(:root) {
279 | overflow: hidden; /* Internet Explorer 11- */
280 | }
281 |
282 | audio,
283 | canvas,
284 | progress,
285 | video {
286 | display: inline-block; /* Internet Explorer 11+, Windows Phone 8.1+ */
287 | }
288 |
289 | /* # =================================================================
290 | # Accessibility
291 | # ================================================================= */
292 |
293 | /* Hide content from screens but not screenreaders */
294 | @media screen {
295 | [hidden~="screen"] {
296 | display: inherit;
297 | }
298 | [hidden~="screen"]:not(:active):not(:focus):not(:target) {
299 | position: absolute !important;
300 | clip: rect(0 0 0 0) !important;
301 | }
302 | }
303 |
304 | /* Specify the progress cursor of updating elements */
305 | [aria-busy="true"] {
306 | cursor: progress;
307 | }
308 |
309 | /* Specify the pointer cursor of trigger elements */
310 | [aria-controls] {
311 | cursor: pointer;
312 | }
313 |
314 | /* Specify the unstyled cursor of disabled, not-editable, or otherwise inoperable elements */
315 | [aria-disabled] {
316 | cursor: default;
317 | }
318 |
319 | /* # =================================================================
320 | # Selection
321 | # ================================================================= */
322 |
323 | /* Specify text selection background color and omit drop shadow */
324 |
325 | ::-moz-selection {
326 | background-color: #b3d4fc; /* Required when declaring ::selection */
327 | color: #000;
328 | text-shadow: none;
329 | }
330 |
331 | ::selection {
332 | background-color: #b3d4fc; /* Required when declaring ::selection */
333 | color: #000;
334 | text-shadow: none;
335 | }
336 |
--------------------------------------------------------------------------------
/client/src/modules/admin/components/Admin.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
54 |
55 |
76 |
--------------------------------------------------------------------------------
/client/src/modules/admin/components/Editor.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
请开始你的表演
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | -
14 | {{tag.name}}
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
298 |
299 |
302 |
344 |
--------------------------------------------------------------------------------
/client/src/modules/admin/components/List.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 标签
5 |
6 |
7 | -
8 |
9 | {{tag.name}}
10 |
11 |
12 |
13 |
14 | - 新建文章
15 | -
16 |
{{ article.title | cutTitle}}
17 |
18 |
19 |
{{tag.name}}
20 |
{{article.createTime}}
21 |
22 | 已发布
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
177 |
178 |
179 |
249 |
--------------------------------------------------------------------------------
/client/src/modules/admin/components/Login.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
51 |
52 |
53 |
79 |
--------------------------------------------------------------------------------
/client/src/modules/admin/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | vue-blog-admin
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/client/src/modules/admin/store/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import Vuex from 'vuex';
3 | import auth from './modules/auth';
4 | import editor from './modules/editor';
5 |
6 | Vue.use(Vuex);
7 |
8 | const debug = process.env.NODE_ENV !== 'production';
9 |
10 | export default new Vuex.Store({
11 | modules: {
12 | auth,
13 | editor,
14 | },
15 | strict: debug,
16 | });
17 |
--------------------------------------------------------------------------------
/client/src/modules/admin/store/modules/auth.js:
--------------------------------------------------------------------------------
1 | import * as types from '../mutation-types';
2 | import api from '../../../../api/login.js';
3 |
4 | const state = {
5 | token: sessionStorage.getItem('vue-blog-token'),
6 | };
7 | // actions
8 | const actions = {
9 | createToken({ commit, state }, { username, password }) {
10 | return api.createToken(username, password).then(res => {
11 | if (res.data.success) {
12 | console.log(res.data.token);
13 | commit(types.CREATE_TOKEN, res.data.token);
14 | } else {
15 | commit(types.DELETE_TOKEN);
16 | }
17 | return new Promise((resolve, reject) => {
18 | resolve(res);
19 | });
20 | });
21 | },
22 | };
23 |
24 | const mutations = {
25 | [types.CREATE_TOKEN](state, token) {
26 | state.token = token;
27 | sessionStorage.setItem('vue-blog-token', token);
28 | },
29 | [types.DELETE_TOKEN](state) {
30 | state.token = null;
31 | sessionStorage.setItem('vue-blog-token', '');
32 | },
33 | };
34 | export default {
35 | state,
36 | actions,
37 | mutations,
38 | };
39 |
--------------------------------------------------------------------------------
/client/src/modules/admin/store/modules/editor.js:
--------------------------------------------------------------------------------
1 | import * as types from '../mutation-types';
2 | import api from '../../../../api/article.js';
3 | import tagApi from '../../../../api/tag.js';
4 |
5 | const state = {
6 | articleList: [],
7 | tagList: [],
8 | currentArticle: {
9 | id: -1,
10 | index: -1,
11 | content: '',
12 | title: '',
13 | tags: [],
14 | save: true,
15 | publish: false,
16 | },
17 | allPage: 1,
18 | curPage: 1,
19 | selectTagArr: [],
20 | };
21 | // getters
22 | const getters = {
23 | articleList: state => state.articleList,
24 | tagList: state => state.tagList,
25 | currentArticle: state => state.currentArticle,
26 | allPage: state => state.allPage,
27 | curPage: state => state.curPage,
28 | selectTagArr: state => state.selectTagArr,
29 | };
30 | // actions
31 | const actions = {
32 | createArticle({ commit, state }, { title, content, publish, tags }) {
33 | return api.createArticle(title, content, publish, tags).then(res => {
34 | console.log(res.data);
35 | if (res.data.success) {
36 | const article = {
37 | save: true,
38 | };
39 | }
40 | return new Promise((resolve, reject) => {
41 | resolve(res);
42 | });
43 | }).catch(err => {
44 | console.log(err);
45 | return new Promise((resolve, reject) => {
46 | reject(err);
47 | });
48 | });
49 | },
50 | getAllArticles({ commit, state, dispatch }, { tag = '', page = 1, limit = 0 } = {}) {
51 | return api.getAllArticles(tag, page, limit).then(res => {
52 | if (res.data.success) {
53 | commit(types.GET_ALL_ARTICLES, { articleList: res.data.articleArr, allPage: res.data.allPage, curPage: page });
54 | dispatch('getCurrentArticle', 0);
55 | }
56 | return new Promise((resolve, reject) => {
57 | resolve(res);
58 | });
59 | }).catch(err => {
60 | console.log(err);
61 | return new Promise((resolve, reject) => {
62 | reject(err);
63 | });
64 | });
65 | },
66 | getCurrentArticle({ commit, state }, index) {
67 | let article;
68 | console.log('currentIndex:', index);
69 | if (state.articleList.length === 0 || index === -1) {
70 | article = {
71 | id: -1,
72 | index: -1,
73 | title: '',
74 | content: '',
75 | save: true,
76 | publish: false,
77 | tags: [],
78 | };
79 | } else {
80 | article = {
81 | id: state.articleList[index].id,
82 | index: index,
83 | title: state.articleList[index].title,
84 | content: state.articleList[index].content,
85 | save: true,
86 | publish: state.articleList[index].publish,
87 | tags: state.articleList[index].tags,
88 | };
89 | }
90 | commit(types.GET_CURRENT_ARTICLE, article);
91 | },
92 | changeArticle({ commit, state }) {
93 | commit(types.CHANGE_ARTICLE);
94 | },
95 | saveArticle({ commit, state }, { id, article }) {
96 | return api.saveArticle(id, article).then(res => {
97 | if (res.data.success) {
98 | commit(types.SAVE_ARTICLE, { id, article });
99 | }
100 | return new Promise((resolve, reject) => {
101 | resolve(res);
102 | });
103 | });
104 | },
105 | publishArticle({ commit, state }, { id }) {
106 | return api.publishArticle(id).then(res => {
107 | if (res.data.success) {
108 | commit(types.PUBLISH_ARTICLE, id);
109 | }
110 | return new Promise((resolve, reject) => {
111 | resolve(res);
112 | });
113 | });
114 | },
115 | notPublishArticle({ commit, state }, { id }) {
116 | return api.notPublishArticle(id).then(res => {
117 | if (res.data.success) {
118 | commit(types.NOT_PUBLISH_ARTICLE, id);
119 | }
120 | return new Promise((resolve, reject) => {
121 | resolve(res);
122 | });
123 | });
124 | },
125 | deleteArticle({ commit, state }, { id, index }) {
126 | return api.deleteArticle(id).then(res => {
127 | if (res.data.success) {
128 | if (state.articleList.length <= 1) {
129 | let article = {
130 | id: -1,
131 | index: 0,
132 | title: '',
133 | content: '',
134 | save: false,
135 | publish: false,
136 | };
137 | commit(types.GET_CURRENT_ARTICLE, article);
138 | }
139 | }
140 | return new Promise((resolve, reject) => {
141 | resolve(res);
142 | });
143 | });
144 | },
145 | createTag({ commit, state }, { name }) {
146 | return tagApi.createTag(name).then(res => {
147 | if (res.data.success) {
148 | commit(types.CREATE_TAG, res.data.tag);
149 | }
150 | return new Promise((resolve, reject) => {
151 | resolve(res);
152 | });
153 | });
154 | },
155 | getAllTags({ commit, state }) {
156 | return tagApi.getAllTags().then(res => {
157 | if (res.data.success) {
158 | commit(types.GET_ALL_TAGS, res.data.tagArr);
159 | }
160 | return new Promise((resolve, reject) => {
161 | resolve(res);
162 | });
163 | });
164 | },
165 | modifyTag({ commit, state }, { id, name }) {
166 | return tagApi.modifyTag(id, name).then(res => {
167 | if (res.data.success) {
168 | commit(types.MODIFY_TAG, { id, name });
169 | }
170 | return new Promise((resolve, reject) => {
171 | resolve(res);
172 | });
173 | });
174 | },
175 | deleteTag({ commit, state }, { id }) {
176 | return tagApi.deleteTag(id).then(res => {
177 | if (res.data.success) {
178 | commit(types.DELETE_TAG, id);
179 | }
180 | return new Promise((resolve, reject) => {
181 | resolve(res);
182 | });
183 | });
184 | },
185 | deleteCurrentTag({ commit, state }, { index }) {
186 | commit(types.DELETE_CURRENT_TAG, index);
187 | return new Promise((resolve, reject) => {
188 | resolve();
189 | });
190 | },
191 | };
192 |
193 | const mutations = {
194 | [types.CREATE_ARTICLE](state, article) {
195 | state.articleList.unshift(article);
196 | state.currentArticle = article;
197 | },
198 | [types.SAVE_ARTICLE](state, { id, article }) {
199 | state.currentArticle.save = true;
200 | let now = state.articleList.find(p => p.id === id);
201 | if (now) {
202 | now.title = article.title;
203 | now.content = article.content;
204 | now.abstract = article.abstract;
205 | now.tags = article.tags;
206 | now.lastEditTime = article.lastEditTime;
207 | }
208 | },
209 | [types.PUBLISH_ARTICLE](state) {
210 | state.currentArticle.publish = true;
211 | },
212 | [types.GET_ALL_ARTICLES](state, { articleList, allPage, curPage }) {
213 | state.articleList = articleList;
214 | state.allPage = allPage;
215 | state.curPage = curPage;
216 | },
217 | [types.GET_CURRENT_ARTICLE](state, article) {
218 | state.currentArticle = article;
219 | },
220 | [types.CHANGE_ARTICLE](state) {
221 | state.currentArticle.save = false;
222 | },
223 | [types.PUBLISH_ARTICLE](state, id) {
224 | state.currentArticle.publish = true;
225 | state.articleList.find(p => p.id === id).publish = true;
226 | },
227 | [types.NOT_PUBLISH_ARTICLE](state, id) {
228 | state.currentArticle.publish = false;
229 | state.articleList.find(p => p.id === id).publish = false;
230 | },
231 | [types.DELETE_ARTICLE](state, index) {
232 | state.articleList.splice(index, 1);
233 | if (state.articleList.length === 0) {
234 | return;
235 | }
236 | if (index > state.articleList.length - 1) {
237 | index = state.articleList.length - 1;
238 | }
239 | state.currentArticle = state.articleList[index];
240 | state.currentArticle.index = index;
241 | state.currentArticle.save = true;
242 | },
243 | [types.CREATE_TAG](state, tag) {
244 | state.currentArticle.tags.push(tag);
245 | },
246 | [types.MODIFY_TAG](state, name) {
247 | state.currentArticle.tags.push(name);
248 |
249 | },
250 | [types.DELETE_TAG](state, id) {
251 | state.tagList = state.tagList.filter((e) => {
252 | return e.id !== id;
253 | });
254 | state.currentArticle.tags = state.currentArticle.tags.filter((e) => {
255 | return e.id !== id;
256 | });
257 | state.selectTagArr = state.selectTagArr.filter((e) => {
258 | return e !== id;
259 | });
260 |
261 | },
262 | [types.DELETE_CURRENT_TAG](state, index) {
263 | state.currentArticle.tags.splice(index, 1);
264 | },
265 | [types.GET_ALL_TAGS](state, tagList) {
266 | state.tagList = tagList;
267 | },
268 | [types.SET_ALL_PAGE](state, allPage) {
269 | state.allPage = allPage;
270 | },
271 | [types.SET_CUR_PAGE](state, curPage) {
272 | state.curPage = curPage;
273 | },
274 | [types.TOGGLE_SELECT_TAG](state, id) {
275 | if (!state.selectTagArr.includes(id)) {
276 | state.selectTagArr.push(id);
277 | } else {
278 | state.selectTagArr = state.selectTagArr.filter((e) => {
279 | return e !== id;
280 | });
281 | }
282 | },
283 | [types.CLEAR_SELECT_TAG](state) {
284 | state.selectTagArr = [];
285 | },
286 | };
287 | export default {
288 | state,
289 | getters,
290 | actions,
291 | mutations,
292 | };
293 |
--------------------------------------------------------------------------------
/client/src/modules/admin/store/mutation-types.js:
--------------------------------------------------------------------------------
1 | // auth
2 | export const CREATE_TOKEN = 'CREATE_TOKEN';
3 | export const DELETE_TOKEN = 'DELETE_TOKEN';
4 | // edit
5 | export const CREATE_ARTICLE = 'CREATE_ARITCLE';
6 | export const SAVE_ARTICLE = 'SAVE_ARITCLE';
7 | export const CHANGE_ARTICLE = 'CHANGE_ARITCLE';
8 | export const PUBLISH_ARTICLE = 'PUBLISH_ARTICLE';
9 | export const NOT_PUBLISH_ARTICLE = 'NOT_PUBLISH_ARTICLE';
10 | export const GET_ALL_ARTICLES = 'GET_ALL_ARTICLES';
11 | export const GET_CURRENT_ARTICLE = 'GET_CURRENT_ARTICLE';
12 | export const DELETE_ARTICLE = 'DELETE_ARTICLE';
13 | // tag
14 | export const CREATE_TAG = 'CREATE_TAG';
15 | export const MODIFY_TAG = 'MODIFY_TAG';
16 | export const DELETE_TAG = 'DELETE_TAG';
17 | export const DELETE_CURRENT_TAG = 'DELETE_CURRENT_TAG';
18 | export const GET_ALL_TAGS = 'GET_ALL_TAGS';
19 | export const TOGGLE_SELECT_TAG = 'TOGGLE_SELECT_TAG';
20 | export const CLEAR_SELECT_TAG = 'CLEAR_SELECT_TAG';
21 | // page
22 | export const GET_ALL_PAGE = 'GET_ALL_PAGE';
23 | export const GET_CUR_PAGE = 'GET_CUR_PAGE';
24 |
--------------------------------------------------------------------------------
/client/src/modules/front/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
17 |
18 |
19 |
20 |
21 |
22 |
32 |
33 |
72 |
77 |
--------------------------------------------------------------------------------
/client/src/modules/front/app.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import Axios from 'axios';
3 | import { createStore } from './store';
4 | import { createRouter } from './router';
5 | import { sync } from 'vuex-router-sync';
6 |
7 | import App from './App.vue';
8 |
9 | // 解决移动端300ms延迟问题
10 | if (typeof window !== 'undefined') {
11 | const Fastclick = require('fastclick');
12 | Fastclick.attach(document.body);
13 | }
14 |
15 | // 每次服务端请求渲染时会重新createApp,初始化这些store、router
16 | // 不然会出现数据还是原来的数据没有变化的问题
17 | export function createApp(ssrContext) {
18 | const store = createStore();
19 | const router = createRouter();
20 |
21 | sync(store, router);
22 |
23 | if (typeof window !== 'undefined') {
24 | router.beforeEach((to, from, next) => {
25 | if (to.path === '/' && store.state.sideBoxOpen) {
26 | store.commit('CLOSE_SIDEBOX');
27 | setTimeout(function () {
28 | next();
29 | }, 100);
30 | } else {
31 | next();
32 | }
33 | });
34 | }
35 |
36 | const app = new Vue({
37 | router,
38 | store,
39 | ssrContext,
40 | render: h => h(App),
41 | });
42 |
43 | return { app, router, store };
44 | }
45 |
--------------------------------------------------------------------------------
/client/src/modules/front/assets/iconfont/iconfont.css:
--------------------------------------------------------------------------------
1 |
2 | @font-face {font-family: "iconfont";
3 | src: url('iconfont.eot?t=1492408788915'); /* IE9*/
4 | src: url('iconfont.eot?t=1492408788915#iefix') format('embedded-opentype'), /* IE6-IE8 */
5 | url('iconfont.woff?t=1492408788915') format('woff'), /* chrome, firefox */
6 | url('iconfont.ttf?t=1492408788915') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/
7 | url('iconfont.svg?t=1492408788915#iconfont') format('svg'); /* iOS 4.1- */
8 | }
9 |
10 | .iconfont {
11 | font-family:"iconfont" !important;
12 | font-size:16px;
13 | font-style:normal;
14 | -webkit-font-smoothing: antialiased;
15 | -moz-osx-font-smoothing: grayscale;
16 | }
17 |
18 | .icon-github:before { content: "\e6ed"; }
19 |
20 |
--------------------------------------------------------------------------------
/client/src/modules/front/assets/iconfont/iconfont.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BUPT-HJM/vue-blog/747306b702fe84d215478403d61f49332e15f829/client/src/modules/front/assets/iconfont/iconfont.eot
--------------------------------------------------------------------------------
/client/src/modules/front/assets/iconfont/iconfont.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
44 |
--------------------------------------------------------------------------------
/client/src/modules/front/assets/iconfont/iconfont.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BUPT-HJM/vue-blog/747306b702fe84d215478403d61f49332e15f829/client/src/modules/front/assets/iconfont/iconfont.ttf
--------------------------------------------------------------------------------
/client/src/modules/front/assets/iconfont/iconfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BUPT-HJM/vue-blog/747306b702fe84d215478403d61f49332e15f829/client/src/modules/front/assets/iconfont/iconfont.woff
--------------------------------------------------------------------------------
/client/src/modules/front/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BUPT-HJM/vue-blog/747306b702fe84d215478403d61f49332e15f829/client/src/modules/front/assets/logo.png
--------------------------------------------------------------------------------
/client/src/modules/front/assets/stylus/_settings.styl:
--------------------------------------------------------------------------------
1 | // font faces
2 | $body-font = "Helvetica Neue",Helvetica,"PingFang SC","Hiragino Sans GB","Microsoft YaHei","微软雅黑",Arial,sans-serif;
3 | //$body-font = Source Sans Pro,Helvetica Neue,Arial,sans-serif;
4 |
5 | // font sizes
6 | $body-font-size = 15px
7 |
8 | // colors
9 | $grey-bg = #efefef
10 | $blue = #0288D1
11 | $blue-link = #868686
12 | $orange = #FF8400
13 | $grey-publish = #aab2b3
14 | $login-text = #757575
15 | $grey = #bfbfbf
16 | $grey-dark = #808080
17 |
18 | // border
19 | $border-radius = 3px
20 |
21 |
--------------------------------------------------------------------------------
/client/src/modules/front/assets/stylus/_syntax.styl:
--------------------------------------------------------------------------------
1 | .gutter pre
2 | color #999
3 |
4 | pre
5 | color: #525252
6 | .function .keyword,
7 | .constant
8 | color: #0092db
9 | .keyword,
10 | .attribute
11 | color: #e96900
12 | .number,
13 | .literal
14 | color: #AE81FF
15 | .tag,
16 | .tag .title,
17 | .change,
18 | .winutils,
19 | .flow,
20 | .lisp .title,
21 | .clojure .built_in,
22 | .nginx .title,
23 | .tex .special
24 | color: #2973b7
25 | .class .title
26 | color: #42b983
27 | .symbol,
28 | .symbol .string,
29 | .value,
30 | .regexp
31 | color: $dark-blue
32 | .title
33 | color: #A6E22E
34 | .tag .value,
35 | .string,
36 | .subst,
37 | .haskell .type,
38 | .preprocessor,
39 | .ruby .class .parent,
40 | .built_in,
41 | .sql .aggregate,
42 | .django .template_tag,
43 | .django .variable,
44 | .smalltalk .class,
45 | .javadoc,
46 | .django .filter .argument,
47 | .smalltalk .localvars,
48 | .smalltalk .array,
49 | .attr_selector,
50 | .pseudo,
51 | .addition,
52 | .stream,
53 | .envvar,
54 | .apache .tag,
55 | .apache .cbracket,
56 | .tex .command,
57 | .prompt
58 | color: $dark-blue
59 | .comment,
60 | .java .annotation,
61 | .python .decorator,
62 | .template_comment,
63 | .pi,
64 | .doctype,
65 | .deletion,
66 | .shebang,
67 | .apache .sqbracket,
68 | .tex .formula
69 | color: #b3b3b3
70 | .coffeescript .javascript,
71 | .javascript .xml,
72 | .tex .formula,
73 | .xml .javascript,
74 | .xml .vbscript,
75 | .xml .css,
76 | .xml .cdata
77 | opacity: 0.5
--------------------------------------------------------------------------------
/client/src/modules/front/assets/stylus/main.styl:
--------------------------------------------------------------------------------
1 | @import "reset"
2 | @import "_settings"
3 | @import "_syntax"
4 |
5 |
6 | [v-cloak]
7 | display none
8 |
9 | html, body
10 | height 100%
11 | font-size $body-font-size
12 | font-family $body-font
13 | -webkit-font-smoothing antialiased
14 | -moz-osx-font-smoothing grayscale
15 | background-color white
16 | input, button
17 | border-radius $border-radius
18 |
19 |
--------------------------------------------------------------------------------
/client/src/modules/front/assets/stylus/markdown.styl:
--------------------------------------------------------------------------------
1 | .markdown-body {
2 | -ms-text-size-adjust: 100%;
3 | -webkit-text-size-adjust: 100%;
4 | line-height: 1.5;
5 | color: #24292e;
6 | font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
7 | font-size: 16px;
8 | line-height: 1.5;
9 | word-wrap: break-word;
10 | }
11 |
12 | .markdown-body .pl-c {
13 | color: #969896;
14 | }
15 |
16 | .markdown-body .pl-c1,
17 | .markdown-body .pl-s .pl-v {
18 | color: #0086b3;
19 | }
20 |
21 | .markdown-body .pl-e,
22 | .markdown-body .pl-en {
23 | color: #795da3;
24 | }
25 |
26 | .markdown-body .pl-smi,
27 | .markdown-body .pl-s .pl-s1 {
28 | color: #333;
29 | }
30 |
31 | .markdown-body .pl-ent {
32 | color: #63a35c;
33 | }
34 |
35 | .markdown-body .pl-k {
36 | color: #a71d5d;
37 | }
38 |
39 | .markdown-body .pl-s,
40 | .markdown-body .pl-pds,
41 | .markdown-body .pl-s .pl-pse .pl-s1,
42 | .markdown-body .pl-sr,
43 | .markdown-body .pl-sr .pl-cce,
44 | .markdown-body .pl-sr .pl-sre,
45 | .markdown-body .pl-sr .pl-sra {
46 | color: #183691;
47 | }
48 |
49 | .markdown-body .pl-v,
50 | .markdown-body .pl-smw {
51 | color: #ed6a43;
52 | }
53 |
54 | .markdown-body .pl-bu {
55 | color: #b52a1d;
56 | }
57 |
58 | .markdown-body .pl-ii {
59 | color: #f8f8f8;
60 | background-color: #b52a1d;
61 | }
62 |
63 | .markdown-body .pl-c2 {
64 | color: #f8f8f8;
65 | background-color: #b52a1d;
66 | }
67 |
68 | .markdown-body .pl-c2::before {
69 | content: "^M";
70 | }
71 |
72 | .markdown-body .pl-sr .pl-cce {
73 | font-weight: bold;
74 | color: #63a35c;
75 | }
76 |
77 | .markdown-body .pl-ml {
78 | color: #693a17;
79 | }
80 |
81 | .markdown-body .pl-mh,
82 | .markdown-body .pl-mh .pl-en,
83 | .markdown-body .pl-ms {
84 | font-weight: bold;
85 | color: #1d3e81;
86 | }
87 |
88 | .markdown-body .pl-mq {
89 | color: #008080;
90 | }
91 |
92 | .markdown-body .pl-mi {
93 | font-style: italic;
94 | color: #333;
95 | }
96 |
97 | .markdown-body .pl-mb {
98 | font-weight: bold;
99 | color: #333;
100 | }
101 |
102 | .markdown-body .pl-md {
103 | color: #bd2c00;
104 | background-color: #ffecec;
105 | }
106 |
107 | .markdown-body .pl-mi1 {
108 | color: #55a532;
109 | background-color: #eaffea;
110 | }
111 |
112 | .markdown-body .pl-mc {
113 | color: #ef9700;
114 | background-color: #ffe3b4;
115 | }
116 |
117 | .markdown-body .pl-mi2 {
118 | color: #d8d8d8;
119 | background-color: #808080;
120 | }
121 |
122 | .markdown-body .pl-mdr {
123 | font-weight: bold;
124 | color: #795da3;
125 | }
126 |
127 | .markdown-body .pl-mo {
128 | color: #1d3e81;
129 | }
130 |
131 | .markdown-body .pl-ba {
132 | color: #595e62;
133 | }
134 |
135 | .markdown-body .pl-sg {
136 | color: #c0c0c0;
137 | }
138 |
139 | .markdown-body .pl-corl {
140 | text-decoration: underline;
141 | color: #183691;
142 | }
143 |
144 | .markdown-body .octicon {
145 | display: inline-block;
146 | vertical-align: text-top;
147 | fill: currentColor;
148 | }
149 |
150 | .markdown-body a {
151 | background-color: transparent;
152 | -webkit-text-decoration-skip: objects;
153 | }
154 |
155 | .markdown-body a:active,
156 | .markdown-body a:hover {
157 | outline-width: 0;
158 | }
159 |
160 | .markdown-body strong {
161 | font-weight: inherit;
162 | }
163 |
164 | .markdown-body strong {
165 | font-weight: bolder;
166 | }
167 |
168 | .markdown-body h1 {
169 | font-size: 2em;
170 | margin: 0.67em 0;
171 | }
172 |
173 | .markdown-body img {
174 | border-style: none;
175 | }
176 |
177 | .markdown-body svg:not(:root) {
178 | overflow: hidden;
179 | }
180 |
181 | .markdown-body code,
182 | .markdown-body kbd,
183 | .markdown-body pre {
184 | font-family: monospace, monospace;
185 | font-size: 1em;
186 | }
187 |
188 | .markdown-body hr {
189 | box-sizing: content-box;
190 | height: 0;
191 | overflow: visible;
192 | }
193 |
194 | .markdown-body input {
195 | font: inherit;
196 | margin: 0;
197 | }
198 |
199 | .markdown-body input {
200 | overflow: visible;
201 | }
202 |
203 | .markdown-body [type="checkbox"] {
204 | box-sizing: border-box;
205 | padding: 0;
206 | }
207 |
208 | .markdown-body * {
209 | box-sizing: border-box;
210 | }
211 |
212 | .markdown-body input {
213 | font-family: inherit;
214 | font-size: inherit;
215 | line-height: inherit;
216 | }
217 |
218 | .markdown-body a {
219 | color: #0366d6;
220 | text-decoration: none;
221 | }
222 |
223 | .markdown-body a:hover {
224 | text-decoration: underline;
225 | }
226 |
227 | .markdown-body strong {
228 | font-weight: 600;
229 | }
230 |
231 | .markdown-body hr {
232 | height: 0;
233 | margin: 15px 0;
234 | overflow: hidden;
235 | background: transparent;
236 | border: 0;
237 | border-bottom: 1px solid #dfe2e5;
238 | }
239 |
240 | .markdown-body hr::before {
241 | display: table;
242 | content: "";
243 | }
244 |
245 | .markdown-body hr::after {
246 | display: table;
247 | clear: both;
248 | content: "";
249 | }
250 |
251 | .markdown-body table {
252 | border-spacing: 0;
253 | border-collapse: collapse;
254 | }
255 |
256 | .markdown-body td,
257 | .markdown-body th {
258 | padding: 0;
259 | }
260 |
261 | .markdown-body h1,
262 | .markdown-body h2,
263 | .markdown-body h3,
264 | .markdown-body h4,
265 | .markdown-body h5,
266 | .markdown-body h6 {
267 | margin-top: 0;
268 | margin-bottom: 0;
269 | }
270 |
271 | .markdown-body h1 {
272 | font-size: 32px;
273 | font-weight: 600;
274 | }
275 |
276 | .markdown-body h2 {
277 | font-size: 24px;
278 | font-weight: 600;
279 | }
280 |
281 | .markdown-body h3 {
282 | font-size: 20px;
283 | font-weight: 600;
284 | }
285 |
286 | .markdown-body h4 {
287 | font-size: 16px;
288 | font-weight: 600;
289 | }
290 |
291 | .markdown-body h5 {
292 | font-size: 14px;
293 | font-weight: 600;
294 | }
295 |
296 | .markdown-body h6 {
297 | font-size: 12px;
298 | font-weight: 600;
299 | }
300 |
301 | .markdown-body p {
302 | margin-top: 0;
303 | margin-bottom: 10px;
304 | }
305 |
306 | .markdown-body blockquote {
307 | margin: 0;
308 | }
309 |
310 | .markdown-body ul,
311 | .markdown-body ol {
312 | padding-left: 0;
313 | margin-top: 0;
314 | margin-bottom: 0;
315 | }
316 |
317 | .markdown-body ol ol,
318 | .markdown-body ul ol {
319 | list-style-type: lower-roman;
320 | }
321 |
322 | .markdown-body ul ul ol,
323 | .markdown-body ul ol ol,
324 | .markdown-body ol ul ol,
325 | .markdown-body ol ol ol {
326 | list-style-type: lower-alpha;
327 | }
328 |
329 | .markdown-body dd {
330 | margin-left: 0;
331 | }
332 |
333 | .markdown-body code {
334 | font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;
335 | font-size: 12px;
336 | }
337 |
338 | .markdown-body pre {
339 | margin-top: 0;
340 | margin-bottom: 0;
341 | font: 12px "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;
342 | }
343 |
344 | .markdown-body .octicon {
345 | vertical-align: text-bottom;
346 | }
347 |
348 | .markdown-body .pl-0 {
349 | padding-left: 0 !important;
350 | }
351 |
352 | .markdown-body .pl-1 {
353 | padding-left: 4px !important;
354 | }
355 |
356 | .markdown-body .pl-2 {
357 | padding-left: 8px !important;
358 | }
359 |
360 | .markdown-body .pl-3 {
361 | padding-left: 16px !important;
362 | }
363 |
364 | .markdown-body .pl-4 {
365 | padding-left: 24px !important;
366 | }
367 |
368 | .markdown-body .pl-5 {
369 | padding-left: 32px !important;
370 | }
371 |
372 | .markdown-body .pl-6 {
373 | padding-left: 40px !important;
374 | }
375 |
376 | .markdown-body::before {
377 | display: table;
378 | content: "";
379 | }
380 |
381 | .markdown-body::after {
382 | display: table;
383 | //clear: both;
384 | content: "";
385 | }
386 |
387 | .markdown-body>*:first-child {
388 | margin-top: 0 !important;
389 | }
390 |
391 | .markdown-body>*:last-child {
392 | margin-bottom: 0 !important;
393 | }
394 |
395 | .markdown-body a:not([href]) {
396 | color: inherit;
397 | text-decoration: none;
398 | }
399 |
400 | .markdown-body .anchor {
401 | float: left;
402 | padding-right: 4px;
403 | margin-left: -20px;
404 | line-height: 1;
405 | }
406 |
407 | .markdown-body .anchor:focus {
408 | outline: none;
409 | }
410 |
411 | .markdown-body p,
412 | .markdown-body blockquote,
413 | .markdown-body ul,
414 | .markdown-body ol,
415 | .markdown-body dl,
416 | .markdown-body table,
417 | .markdown-body pre {
418 | margin-top: 0;
419 | margin-bottom: 16px;
420 | }
421 |
422 | .markdown-body hr {
423 | height: 0.25em;
424 | padding: 0;
425 | margin: 24px 0;
426 | background-color: #e1e4e8;
427 | border: 0;
428 | }
429 |
430 | .markdown-body blockquote {
431 | padding: 0 1em;
432 | color: #6a737d;
433 | border-left: 0.25em solid #dfe2e5;
434 | }
435 |
436 | .markdown-body blockquote>:first-child {
437 | margin-top: 0;
438 | }
439 |
440 | .markdown-body blockquote>:last-child {
441 | margin-bottom: 0;
442 | }
443 |
444 | .markdown-body kbd {
445 | display: inline-block;
446 | padding: 3px 5px;
447 | font-size: 11px;
448 | line-height: 10px;
449 | color: #444d56;
450 | vertical-align: middle;
451 | background-color: #fafbfc;
452 | border: solid 1px #c6cbd1;
453 | border-bottom-color: #959da5;
454 | border-radius: 3px;
455 | box-shadow: inset 0 -1px 0 #959da5;
456 | }
457 |
458 | .markdown-body h1,
459 | .markdown-body h2,
460 | .markdown-body h3,
461 | .markdown-body h4,
462 | .markdown-body h5,
463 | .markdown-body h6 {
464 | margin-top: 24px;
465 | margin-bottom: 16px;
466 | font-weight: 600;
467 | line-height: 1.25;
468 | }
469 |
470 | .markdown-body h1 .octicon-link,
471 | .markdown-body h2 .octicon-link,
472 | .markdown-body h3 .octicon-link,
473 | .markdown-body h4 .octicon-link,
474 | .markdown-body h5 .octicon-link,
475 | .markdown-body h6 .octicon-link {
476 | color: #1b1f23;
477 | vertical-align: middle;
478 | visibility: hidden;
479 | }
480 |
481 | .markdown-body h1:hover .anchor,
482 | .markdown-body h2:hover .anchor,
483 | .markdown-body h3:hover .anchor,
484 | .markdown-body h4:hover .anchor,
485 | .markdown-body h5:hover .anchor,
486 | .markdown-body h6:hover .anchor {
487 | text-decoration: none;
488 | }
489 |
490 | .markdown-body h1:hover .anchor .octicon-link,
491 | .markdown-body h2:hover .anchor .octicon-link,
492 | .markdown-body h3:hover .anchor .octicon-link,
493 | .markdown-body h4:hover .anchor .octicon-link,
494 | .markdown-body h5:hover .anchor .octicon-link,
495 | .markdown-body h6:hover .anchor .octicon-link {
496 | visibility: visible;
497 | }
498 |
499 | .markdown-body h1 {
500 | padding-bottom: 0.3em;
501 | font-size: 2em;
502 | border-bottom: 1px solid #eaecef;
503 | }
504 |
505 | .markdown-body h2 {
506 | padding-bottom: 0.3em;
507 | font-size: 1.5em;
508 | border-bottom: 1px solid #eaecef;
509 | }
510 |
511 | .markdown-body h3 {
512 | font-size: 1.25em;
513 | }
514 |
515 | .markdown-body h4 {
516 | font-size: 1em;
517 | }
518 |
519 | .markdown-body h5 {
520 | font-size: 0.875em;
521 | }
522 |
523 | .markdown-body h6 {
524 | font-size: 0.85em;
525 | color: #6a737d;
526 | }
527 |
528 | .markdown-body ul,
529 | .markdown-body ol {
530 | padding-left: 2em;
531 | }
532 |
533 | .markdown-body ul ul,
534 | .markdown-body ul ol,
535 | .markdown-body ol ol,
536 | .markdown-body ol ul {
537 | margin-top: 0;
538 | margin-bottom: 0;
539 | }
540 |
541 | .markdown-body li>p {
542 | margin-top: 16px;
543 | }
544 |
545 | .markdown-body li+li {
546 | margin-top: 0.25em;
547 | }
548 |
549 | .markdown-body dl {
550 | padding: 0;
551 | }
552 |
553 | .markdown-body dl dt {
554 | padding: 0;
555 | margin-top: 16px;
556 | font-size: 1em;
557 | font-style: italic;
558 | font-weight: 600;
559 | }
560 |
561 | .markdown-body dl dd {
562 | padding: 0 16px;
563 | margin-bottom: 16px;
564 | }
565 |
566 | .markdown-body table {
567 | display: block;
568 | width: 100%;
569 | overflow: auto;
570 | }
571 |
572 | .markdown-body table th {
573 | font-weight: 600;
574 | }
575 |
576 | .markdown-body table th,
577 | .markdown-body table td {
578 | padding: 6px 13px;
579 | border: 1px solid #dfe2e5;
580 | }
581 |
582 | .markdown-body table tr {
583 | background-color: #fff;
584 | border-top: 1px solid #c6cbd1;
585 | }
586 |
587 | .markdown-body table tr:nth-child(2n) {
588 | background-color: #f6f8fa;
589 | }
590 |
591 | .markdown-body img {
592 | max-width: 100%;
593 | box-sizing: content-box;
594 | background-color: #fff;
595 | }
596 |
597 | .markdown-body code {
598 | padding: 0;
599 | padding-top: 0.2em;
600 | padding-bottom: 0.2em;
601 | margin: 0;
602 | font-size: 85%;
603 | background-color: rgba(27,31,35,0.05);
604 | border-radius: 3px;
605 | }
606 |
607 | .markdown-body code::before,
608 | .markdown-body code::after {
609 | letter-spacing: -0.2em;
610 | content: "\00a0";
611 | }
612 |
613 | .markdown-body pre {
614 | word-wrap: normal;
615 | }
616 |
617 | .markdown-body pre>code {
618 | padding: 0;
619 | margin: 0;
620 | font-size: 100%;
621 | word-break: normal;
622 | white-space: pre;
623 | background: transparent;
624 | border: 0;
625 | }
626 |
627 | .markdown-body .highlight {
628 | margin-bottom: 16px;
629 | }
630 |
631 | .markdown-body .highlight pre {
632 | margin-bottom: 0;
633 | word-break: normal;
634 | }
635 |
636 | .markdown-body .highlight pre,
637 | .markdown-body pre {
638 | padding: 16px;
639 | overflow: auto;
640 | font-size: 85%;
641 | line-height: 1.45;
642 | background-color: #f6f8fa;
643 | border-radius: 3px;
644 | }
645 |
646 | .markdown-body pre code {
647 | display: inline;
648 | max-width: auto;
649 | padding: 0;
650 | margin: 0;
651 | overflow: visible;
652 | line-height: inherit;
653 | word-wrap: normal;
654 | background-color: transparent;
655 | border: 0;
656 | }
657 |
658 | .markdown-body pre code::before,
659 | .markdown-body pre code::after {
660 | content: normal;
661 | }
662 |
663 | .markdown-body .full-commit .btn-outline:not(:disabled):hover {
664 | color: #005cc5;
665 | border-color: #005cc5;
666 | }
667 |
668 | .markdown-body kbd {
669 | display: inline-block;
670 | padding: 3px 5px;
671 | font: 11px "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;
672 | line-height: 10px;
673 | color: #444d56;
674 | vertical-align: middle;
675 | background-color: #fcfcfc;
676 | border: solid 1px #c6cbd1;
677 | border-bottom-color: #959da5;
678 | border-radius: 3px;
679 | box-shadow: inset 0 -1px 0 #959da5;
680 | }
681 |
682 | .markdown-body :checked+.radio-label {
683 | position: relative;
684 | z-index: 1;
685 | border-color: #0366d6;
686 | }
687 |
688 | .markdown-body .task-list-item {
689 | list-style-type: none;
690 | }
691 |
692 | .markdown-body .task-list-item+.task-list-item {
693 | margin-top: 3px;
694 | }
695 |
696 | .markdown-body .task-list-item input {
697 | margin: 0 0.2em 0.25em -1.6em;
698 | vertical-align: middle;
699 | }
700 |
701 | .markdown-body hr {
702 | border-bottom-color: #eee;
703 | }
--------------------------------------------------------------------------------
/client/src/modules/front/assets/stylus/reset.styl:
--------------------------------------------------------------------------------
1 | /*!
2 | * ress.css • v1.1.2
3 | * MIT License
4 | * github.com/filipelinhares/ress
5 | */
6 |
7 | /* # =================================================================
8 | # Global selectors
9 | # ================================================================= */
10 |
11 | html {
12 | box-sizing: border-box;
13 | //overflow-y: scroll; /* All browsers without overlaying scrollbars */
14 | -webkit-text-size-adjust: 100%; /* iOS 8+ */
15 | }
16 |
17 | *,
18 | ::before,
19 | ::after {
20 | box-sizing: inherit;
21 | }
22 |
23 | ::before,
24 | ::after {
25 | text-decoration: inherit; /* Inherit text-decoration and vertical align to ::before and ::after pseudo elements */
26 | vertical-align: inherit;
27 | }
28 |
29 | /* Remove margin, padding of all elements and set background-no-repeat as default */
30 | * {
31 | background-repeat: no-repeat; /* Set `background-repeat: no-repeat` to all elements */
32 | padding: 0; /* Reset `padding` and `margin` of all elements */
33 | margin: 0;
34 | }
35 |
36 | /* # =================================================================
37 | # General elements
38 | # ================================================================= */
39 |
40 | /* Add the correct display in iOS 4-7.*/
41 | audio:not([controls]) {
42 | display: none;
43 | height: 0;
44 | }
45 |
46 | hr {
47 | overflow: visible; /* Show the overflow in Edge and IE */
48 | }
49 |
50 | /*
51 | * Correct `block` display not defined for any HTML5 element in IE 8/9
52 | * Correct `block` display not defined for `details` or `summary` in IE 10/11
53 | * and Firefox
54 | * Correct `block` display not defined for `main` in IE 11
55 | */
56 | article,
57 | aside,
58 | details,
59 | figcaption,
60 | figure,
61 | footer,
62 | header,
63 | main,
64 | menu,
65 | nav,
66 | section,
67 | summary {
68 | display: block;
69 | }
70 |
71 | summary {
72 | display: list-item; /* Add the correct display in all browsers */
73 | }
74 |
75 | small {
76 | font-size: 80%; /* Set font-size to 80% in `small` elements */
77 | }
78 |
79 | [hidden],
80 | template {
81 | display: none; /* Add the correct display in IE */
82 | }
83 |
84 | abbr[title] {
85 | border-bottom: 1px dotted; /* Add a bordered underline effect in all browsers */
86 | text-decoration: none; /* Remove text decoration in Firefox 40+ */
87 | }
88 |
89 | a {
90 | background-color: transparent; /* Remove the gray background on active links in IE 10 */
91 | -webkit-text-decoration-skip: objects; /* Remove gaps in links underline in iOS 8+ and Safari 8+ */
92 | }
93 |
94 | a:active,
95 | a:hover {
96 | outline-width: 0; /* Remove the outline when hovering in all browsers */
97 | }
98 |
99 | code,
100 | kbd,
101 | pre,
102 | samp {
103 | font-family: monospace, monospace; /* Specify the font family of code elements */
104 | }
105 |
106 | b,
107 | strong {
108 | font-weight: bolder; /* Correct style set to `bold` in Edge 12+, Safari 6.2+, and Chrome 18+ */
109 | }
110 |
111 | dfn {
112 | font-style: italic; /* Address styling not present in Safari and Chrome */
113 | }
114 |
115 | /* Address styling not present in IE 8/9 */
116 | mark {
117 | background-color: #ff0;
118 | color: #000;
119 | }
120 |
121 | /* https://gist.github.com/unruthless/413930 */
122 | sub,
123 | sup {
124 | font-size: 75%;
125 | line-height: 0;
126 | position: relative;
127 | vertical-align: baseline;
128 | }
129 |
130 | sub {
131 | bottom: -0.25em;
132 | }
133 |
134 | sup {
135 | top: -0.5em;
136 | }
137 |
138 | /* # =================================================================
139 | # Forms
140 | # ================================================================= */
141 |
142 | input {
143 | border-radius: 0;
144 | }
145 |
146 | /* Apply cursor pointer to button elements */
147 | button,
148 | [type="button"],
149 | [type="reset"],
150 | [type="submit"],
151 | [role="button"] {
152 | cursor: pointer;
153 | }
154 |
155 | /* Replace pointer cursor in disabled elements */
156 | [disabled] {
157 | cursor: default;
158 | }
159 |
160 | [type="number"] {
161 | width: auto; /* Firefox 36+ */
162 | }
163 |
164 | [type="search"] {
165 | -webkit-appearance: textfield; /* Safari 8+ */
166 | }
167 |
168 | [type="search"]::-webkit-search-cancel-button,
169 | [type="search"]::-webkit-search-decoration {
170 | -webkit-appearance: none; /* Safari 8 */
171 | }
172 |
173 | textarea {
174 | overflow: auto; /* Internet Explorer 11+ */
175 | resize: vertical; /* Specify textarea resizability */
176 | }
177 |
178 | button,
179 | input,
180 | optgroup,
181 | select,
182 | textarea {
183 | font: inherit; /* Specify font inheritance of form elements */
184 | }
185 |
186 | optgroup {
187 | font-weight: bold; /* Restore the font weight unset by the previous rule. */
188 | }
189 |
190 | button {
191 | overflow: visible; /* Address `overflow` set to `hidden` in IE 8/9/10/11 */
192 | }
193 |
194 | /* Remove inner padding and border in Firefox 4+ */
195 | button::-moz-focus-inner,
196 | [type="button"]::-moz-focus-inner,
197 | [type="reset"]::-moz-focus-inner,
198 | [type="submit"]::-moz-focus-inner {
199 | border-style: 0;
200 | padding: 0;
201 | }
202 |
203 | /* Replace focus style removed in the border reset above */
204 | button:-moz-focusring,
205 | [type="button"]::-moz-focus-inner,
206 | [type="reset"]::-moz-focus-inner,
207 | [type="submit"]::-moz-focus-inner {
208 | outline: 1px dotted ButtonText;
209 | }
210 |
211 | button,
212 | html [type="button"], /* Prevent a WebKit bug where (2) destroys native `audio` and `video`controls in Android 4 */
213 | [type="reset"],
214 | [type="submit"] {
215 | -webkit-appearance: button; /* Correct the inability to style clickable types in iOS */
216 | }
217 |
218 | button,
219 | select {
220 | text-transform: none; /* Firefox 40+, Internet Explorer 11- */
221 | }
222 |
223 | /* Remove the default button styling in all browsers */
224 | button,
225 | input,
226 | select,
227 | textarea {
228 | background-color: transparent;
229 | border-style: none;
230 | color: inherit;
231 | }
232 |
233 | /* Style select like a standard input */
234 | select {
235 | -moz-appearance: none; /* Firefox 36+ */
236 | -webkit-appearance: none; /* Chrome 41+ */
237 | }
238 |
239 | select::-ms-expand {
240 | display: none; /* Internet Explorer 11+ */
241 | }
242 |
243 | select::-ms-value {
244 | color: currentColor; /* Internet Explorer 11+ */
245 | }
246 |
247 | legend {
248 | border: 0; /* Correct `color` not being inherited in IE 8/9/10/11 */
249 | color: inherit; /* Correct the color inheritance from `fieldset` elements in IE */
250 | display: table; /* Correct the text wrapping in Edge and IE */
251 | max-width: 100%; /* Correct the text wrapping in Edge and IE */
252 | white-space: normal; /* Correct the text wrapping in Edge and IE */
253 | }
254 |
255 | ::-webkit-file-upload-button {
256 | -webkit-appearance: button; /* Correct the inability to style clickable types in iOS and Safari */
257 | font: inherit; /* Change font properties to `inherit` in Chrome and Safari */
258 | }
259 |
260 | [type="search"] {
261 | -webkit-appearance: textfield; /* Correct the odd appearance in Chrome and Safari */
262 | outline-offset: -2px; /* Correct the outline style in Safari */
263 | }
264 |
265 | /* # =================================================================
266 | # Specify media element style
267 | # ================================================================= */
268 |
269 | img {
270 | border-style: none; /* Remove border when inside `a` element in IE 8/9/10 */
271 | }
272 |
273 | /* Add the correct vertical alignment in Chrome, Firefox, and Opera */
274 | progress {
275 | vertical-align: baseline;
276 | }
277 |
278 | svg:not(:root) {
279 | overflow: hidden; /* Internet Explorer 11- */
280 | }
281 |
282 | audio,
283 | canvas,
284 | progress,
285 | video {
286 | display: inline-block; /* Internet Explorer 11+, Windows Phone 8.1+ */
287 | }
288 |
289 | /* # =================================================================
290 | # Accessibility
291 | # ================================================================= */
292 |
293 | /* Hide content from screens but not screenreaders */
294 | @media screen {
295 | [hidden~="screen"] {
296 | display: inherit;
297 | }
298 | [hidden~="screen"]:not(:active):not(:focus):not(:target) {
299 | position: absolute !important;
300 | clip: rect(0 0 0 0) !important;
301 | }
302 | }
303 |
304 | /* Specify the progress cursor of updating elements */
305 | [aria-busy="true"] {
306 | cursor: progress;
307 | }
308 |
309 | /* Specify the pointer cursor of trigger elements */
310 | [aria-controls] {
311 | cursor: pointer;
312 | }
313 |
314 | /* Specify the unstyled cursor of disabled, not-editable, or otherwise inoperable elements */
315 | [aria-disabled] {
316 | cursor: default;
317 | }
318 |
319 | /* # =================================================================
320 | # Selection
321 | # ================================================================= */
322 |
323 | /* Specify text selection background color and omit drop shadow */
324 |
325 | ::-moz-selection {
326 | background-color: #b3d4fc; /* Required when declaring ::selection */
327 | color: #000;
328 | text-shadow: none;
329 | }
330 |
331 | ::selection {
332 | background-color: #b3d4fc; /* Required when declaring ::selection */
333 | color: #000;
334 | text-shadow: none;
335 | }
336 |
--------------------------------------------------------------------------------
/client/src/modules/front/components/Article.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
{{currentPost.title}}
9 |
{{currentPost.createTime}}
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
102 |
103 |
106 |
143 |
--------------------------------------------------------------------------------
/client/src/modules/front/components/List.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | -
9 | 筛选
10 | {{filterMsg}}
11 | 分类
12 |
13 |
14 | -
15 |
{{ article.title }}
16 |
17 |
{{article.createTime}}
18 |
19 |
20 |
21 | 继续阅读...
22 |
23 |
24 |
25 |
26 |
27 |
30 |
31 |
32 |
33 |
34 |
124 |
125 |
205 |
--------------------------------------------------------------------------------
/client/src/modules/front/components/common/Comment.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
33 |
34 |
39 |
--------------------------------------------------------------------------------
/client/src/modules/front/components/common/Side.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |

6 |
小深刻的秋鼠
7 |
Love Life, Love sharing
8 |
13 |
14 | -
15 | {{tag.name}}
16 |
17 |
18 |
26 |
27 |
28 |
29 |
30 |
125 |
126 |
127 |
270 |
273 |
274 |
--------------------------------------------------------------------------------
/client/src/modules/front/components/common/Top.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
36 |
37 |
69 |
--------------------------------------------------------------------------------
/client/src/modules/front/entry-client.js:
--------------------------------------------------------------------------------
1 | import { createApp } from './app';
2 | const { app, router, store } = createApp();
3 |
4 | // store替换使client rendering和server rendering匹配
5 | if (window.__INITIAL_STATE__) {
6 | store.replaceState(window.__INITIAL_STATE__);
7 | }
8 |
9 | // 挂载#app
10 | router.onReady(() => {
11 | app.$mount('#app');
12 | });
13 |
14 | // service worker还没启用
15 | // // service worker
16 | // if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
17 | // navigator.serviceWorker.register('/service-worker.js')
18 | // }
19 |
--------------------------------------------------------------------------------
/client/src/modules/front/entry-server.js:
--------------------------------------------------------------------------------
1 | import { createApp } from './app';
2 |
3 | const isDev = process.env.NODE_ENV !== 'production';
4 |
5 | export default context => {
6 | const s = isDev && Date.now();
7 | // 注意下面这句话要写在export函数里供服务端渲染调用,重新初始化那store、router
8 | const { app, router, store } = createApp();
9 | return new Promise((resolve, reject) => {
10 | router.push(context.url);
11 | router.onReady(() => {
12 | const matchedComponents = router.getMatchedComponents();
13 | if (!matchedComponents.length) {
14 | reject({ code: 404 });
15 | }
16 | Promise.all(matchedComponents.map(component => {
17 | if (component.preFetch) {
18 | // 调用组件上的preFetch(这部分只能拿到router第一级别组件,子组件的preFetch拿不到)
19 | return component.preFetch(store);
20 | }
21 | })).then(() => {
22 | isDev && console.log(`data pre-fetch: ${Date.now() - s}ms`);
23 | // 暴露数据到HTMl,使得客户端渲染拿到数据,跟服务端渲染匹配
24 | context.state = store.state;
25 | context.state.posts.forEach((element, index) => {
26 | context.state.posts[index].content = '';
27 | });
28 | if (/\/article\//g.test(context.url)) {
29 | context.title = context.state.currentPost.title;
30 | }
31 | resolve(app);
32 | }).catch(reject);
33 | });
34 | });
35 | };
36 |
--------------------------------------------------------------------------------
/client/src/modules/front/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {{ title }}
7 | <% for (var chunk of webpack.chunks) {
8 | for (var file of chunk.files) {
9 | if (file.match(/\.(js|css)$/)) {
10 | if (file.indexOf('admin') == -1) {%>
11 |
14 | <% }}}} %>
15 | {{{ renderURLScript('disqus') }}}
16 | {{{ renderURLScript('baidu') }}}
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/client/src/modules/front/router/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import VueRouter from 'vue-router';
3 | // import List from '../components/List.vue'
4 | // import Article from '../components/Article.vue'
5 |
6 | Vue.use(VueRouter);
7 |
8 | const List = resolve => require(['../components/List.vue'], resolve);
9 | const Article = resolve => require(['../components/Article.vue'], resolve);
10 |
11 | export function createRouter() {
12 | const router = new VueRouter({
13 | mode: 'history',
14 | scrollBehavior: (to, from, savedPosition) => {
15 | if (savedPosition) {
16 | return savedPosition;
17 | } else {
18 | return { x: 0, y: 0 };
19 | }
20 | },
21 | routes: [
22 | { path: '/', component: List },
23 | { path: '/article/:id', component: Article, meta: { scrollToTop: true } },
24 | { path: '/page/:page', component: List },
25 | { path: '*', redirect: '/' },
26 | ],
27 | });
28 | if (typeof window !== 'undefined') {
29 | router.afterEach((to, from) => {
30 | if (document && !(/\/article\//g.test(to.path))) {
31 | document.querySelector('title').innerText = 'HJM\'s Blog';
32 | }
33 | });
34 | }
35 | return router;
36 | }
37 |
--------------------------------------------------------------------------------
/client/src/modules/front/store/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import Vuex from 'vuex';
3 | import articleApi from 'api/article.js';
4 | import tagApi from 'api/tag.js';
5 | import marked from 'lib/marked.js';
6 |
7 | Vue.use(Vuex);
8 |
9 | export function createStore() {
10 | return new Vuex.Store({
11 | state: {
12 | currentPost: {
13 | content: '',
14 | id: '',
15 | },
16 | currentPostCompile: '',
17 | posts: [],
18 | allPage: 0,
19 | curPage: 0,
20 | tags: [],
21 | selectTags: [],
22 | sideBoxOpen: false,
23 | },
24 |
25 | actions: {
26 | getAllPosts({ commit, state }, { tag = '', page = 1, limit = 5 } = {}) {
27 |
28 | return articleApi.getAllPublishArticles(tag, page, limit).then(res => {
29 | commit('GET_ALL_POSTS', { posts: res.data.articleArr, allPage: res.data.allPage, curPage: page });
30 | return new Promise((resolve, reject) => {
31 | resolve(res);
32 | });
33 | });
34 | },
35 | getAllTags({ commit, state }) {
36 | return tagApi.getAllTags().then(res => {
37 | commit('GET_ALL_TAGS', res.data.tagArr);
38 | return new Promise((resolve, reject) => {
39 | resolve(res);
40 | });
41 | });
42 | },
43 | getPost({ commit, state }, id) {
44 | let article = state.posts.find((post) => post.id === id);
45 | if (!article && state.currentPost.id === id) {
46 | article = state.currentPost;
47 | }
48 | if (article && article.content) {
49 | commit('GET_POST', article);
50 | return new Promise((resolve, reject) => {
51 | resolve(article);
52 | });
53 | } else {
54 | return articleApi.getArticle(id).then(res => {
55 | commit('GET_POST', res.data.article);
56 | return new Promise((resolve, reject) => {
57 | resolve(res);
58 | });
59 | }).catch((err) => {
60 | // console.log(err)
61 | });
62 | }
63 | },
64 | },
65 |
66 | mutations: {
67 | GET_ALL_POSTS: (state, { posts, allPage, curPage }) => {
68 | if (isNaN(+allPage)) {
69 | allPage = 0;
70 | }
71 | if (isNaN(+curPage)) {
72 | curPage = 0;
73 | }
74 | state.posts = posts;
75 | state.allPage = +allPage;
76 | state.curPage = +curPage;
77 | },
78 | GET_ALL_TAGS: (state, tags) => {
79 | state.tags = tags;
80 | },
81 | SET_SELECT_TAGS: (state, tags) => {
82 | state.selectTags = tags;
83 | },
84 | TOGGLE_SELECT_TAGS: (state, { id, name }) => {
85 | if (typeof state.selectTags.find(function (e) {
86 | return e.id === id;
87 | }) === 'undefined') {
88 | state.selectTags.push({
89 | id,
90 | name,
91 | });
92 | } else {
93 | state.selectTags = state.selectTags.filter((e) => {
94 | return e.id !== id;
95 | });
96 | }
97 | },
98 | TOGGLE_SIDEBOX: (state) => {
99 | state.sideBoxOpen = !state.sideBoxOpen;
100 | },
101 | CLOSE_SIDEBOX: (state) => {
102 | state.sideBoxOpen = false;
103 | },
104 | GET_POST: (state, article) => {
105 | state.currentPost = article;
106 | state.currentPostCompile = marked(state.currentPost.content);
107 | },
108 | },
109 | getters: {
110 | posts: state => state.posts,
111 | tags: state => state.tags,
112 | curPage: state => state.curPage,
113 | allPage: state => state.allPage,
114 | selectTags: state => state.selectTags,
115 | searchTags: state => {
116 | return state.selectTags.map((item) => item.id);
117 | },
118 | sideBoxOpen: state => state.sideBoxOpen,
119 | currentPost: state => state.currentPost,
120 | currentPostCompile: state => state.currentPostCompile,
121 | },
122 | });
123 | }
124 |
--------------------------------------------------------------------------------
/client/static/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BUPT-HJM/vue-blog/747306b702fe84d215478403d61f49332e15f829/client/static/.gitkeep
--------------------------------------------------------------------------------
/docker-compose.prod.yml:
--------------------------------------------------------------------------------
1 | version: "3"
2 | services:
3 | node:
4 | build:
5 | context: .
6 | dockerfile: node.dockerfile
7 | ports:
8 | - "8889:8889"
9 | environment:
10 | NODE_ENV: production
11 | MONGO_URL: mongodb://mongodb/vue-blog-prod
12 | command: ["npm", "run", "prod"]
13 | links:
14 | - mongodb
15 | depends_on:
16 | - mongodb
17 | restart: on-failure
18 | mongodb:
19 | image: mongo
20 | ports:
21 | - "27017:27017"
22 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3"
2 | services:
3 | node:
4 | build:
5 | context: .
6 | dockerfile: node.dockerfile
7 | ports:
8 | - "8889:8889"
9 | environment:
10 | NODE_ENV: development
11 | MONGO_URL: mongodb://mongodb/vue-blog
12 | volumes:
13 | - .:/home/app/vue-blog
14 | - /home/app/vue-blog/node_modules
15 | command: ["npm", "run", "dev"]
16 | links:
17 | - mongodb
18 | depends_on:
19 | - mongodb
20 | mongodb:
21 | image: mongo
22 | ports:
23 | - "27017:27017"
24 |
--------------------------------------------------------------------------------
/node.dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:8.9.2
2 |
3 | MAINTAINER https://github.com/BUPT-HJM
4 |
5 | ENV HOME=/home/app
6 |
7 | COPY package.json package-lock.json $HOME/vue-blog/
8 |
9 | WORKDIR $HOME/vue-blog
10 | RUN npm install --registry=https://registry.npm.taobao.org
11 |
12 | COPY . $HOME/vue-blog
13 |
--------------------------------------------------------------------------------
/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "restartable": "rs",
3 | "watch": [
4 | "server",
5 | "webpack.config.js"
6 | ],
7 | "verbose": true,
8 | "execMap": {
9 | "js": "node --harmony"
10 | },
11 | "env": {
12 | "NODE_ENV": "development"
13 | },
14 | "ext": "js json css html"
15 | }
16 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vue-blog",
3 | "version": "1.0.0",
4 | "license": "MIT",
5 | "description": "a blog platform built with vue2, koa2 and mongo",
6 | "scripts": {
7 | "start": "cross-env NODE_ENV=production node server/start.js",
8 | "dev": "nodemon server/start.js",
9 | "prod": "npm run build && npm start",
10 | "build": "npm run build:client && npm run build:server",
11 | "build:client": "rimraf ./client/dist && cross-env NODE_ENV=production webpack --config ./client/build/webpack.client.config.js --colors --profile --display-modules --progress",
12 | "build:server": "cross-env NODE_ENV=production webpack --config ./client/build/webpack.server.config.js --colors --profile --display-modules --progress",
13 | "analyze": "rimraf ./client/dist && cross-env NODE_ENV=production webpack --config ./client/build/webpack.client.config.js --json > stats.json && webpack-bundle-analyzer stats.json -p 1111",
14 | "pm2": "npm run build && pm2 start pm2.json --env production"
15 | },
16 | "dependencies": {
17 | "axios": "^0.16.0",
18 | "element-ui": "^1.2.7",
19 | "fastclick": "^1.0.6",
20 | "font-awesome": "^4.7.0",
21 | "highlight.js": "^9.10.0",
22 | "koa": "^2.2.0",
23 | "koa-bodyparser": "1",
24 | "koa-compress": "^2.0.0",
25 | "koa-connect-history-api-fallback": "^0.3.1",
26 | "koa-convert": "^1.2.0",
27 | "koa-logger": "^2.0.1",
28 | "koa-onerror": "^3.1.0",
29 | "koa-router": "^7.1.1",
30 | "koa-static": "^3.0.0",
31 | "lru-cache": "^4.0.2",
32 | "marked": "^0.3.6",
33 | "md5": "^2.2.1",
34 | "moment": "^2.18.1",
35 | "mongoose": "^4.9.3",
36 | "require-dir": "^0.3.2",
37 | "simplemde": "^1.11.2",
38 | "ssri": "^5.2.2",
39 | "strip-ansi": "3.0.1",
40 | "vue": "^2.3.0",
41 | "vue-router": "^2.5.2",
42 | "vue-server-renderer": "^2.3.0",
43 | "vuex": "^2.3.1",
44 | "vuex-router-sync": "^4.1.2"
45 | },
46 | "devDependencies": {
47 | "autoprefixer": "^6.7.7",
48 | "babel-core": "^6.0.0",
49 | "babel-eslint": "^8.2.3",
50 | "babel-loader": "^6.4.1",
51 | "babel-plugin-component": "^0.9.1",
52 | "babel-plugin-transform-runtime": "^6.23.0",
53 | "babel-polyfill": "^6.26.0",
54 | "babel-preset-es2015": "^6.24.0",
55 | "babel-preset-stage-0": "^6.22.0",
56 | "connect-history-api-fallback": "^1.3.0",
57 | "copy-webpack-plugin": "^4.0.1",
58 | "cross-env": "^4.0.0",
59 | "css-loader": "^0.28.0",
60 | "cz-conventional-changelog": "^2.1.0",
61 | "eslint": "^4.19.1",
62 | "eslint-plugin-vue": "^4.5.0",
63 | "event-source-polyfill": "^0.0.12",
64 | "extract-text-webpack-plugin": "^2.1.0",
65 | "file-loader": "*",
66 | "html-webpack-plugin": "^2.28.0",
67 | "jsonwebtoken": "^7.3.0",
68 | "nodemon": "^1.11.0",
69 | "postcss-loader": "^1.3.3",
70 | "rimraf": "^2.6.1",
71 | "style-loader": "^0.16.1",
72 | "stylus": "0.52.4",
73 | "stylus-loader": "^3.0.1",
74 | "url-loader": "^0.5.8",
75 | "vue-loader": "^12.0.2",
76 | "vue-ssr-webpack-plugin": "^3.0.0",
77 | "vue-template-compiler": "^2.3.0",
78 | "webpack": "^2.3.2",
79 | "webpack-bundle-analyzer": "^2.4.0",
80 | "webpack-dev-middleware": "^1.10.1",
81 | "webpack-hot-middleware": "^2.18.0",
82 | "webpack-merge": "^4.1.0",
83 | "webpack-node-externals": "^1.5.4"
84 | },
85 | "config": {
86 | "commitizen": {
87 | "path": "./node_modules/cz-conventional-changelog"
88 | }
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/pm2.json:
--------------------------------------------------------------------------------
1 | [{
2 | "name": "vue-blog",
3 | "script": "server/start.js",
4 | "error_file": "server/logs/app-err.log",
5 | "out_file": "server/logs/app-out.log",
6 | "env_dev": {
7 | "NODE_ENV": "development"
8 | },
9 | "env_production": {
10 | "NODE_ENV": "production"
11 | }
12 | }]
13 | // pm2 install pm2-logrotate
14 | // pm2 set pm2-logrotate:retain 100
15 | // pm2 set pm2-logrotate:size 1M
16 |
--------------------------------------------------------------------------------
/server/api/index.js:
--------------------------------------------------------------------------------
1 | import compose from 'koa-compose';
2 | import Router from 'koa-router';
3 | import convert from 'koa-convert';
4 | import config from '../configs';
5 | import requireDir from 'require-dir';
6 | const routes = requireDir('./routes');
7 |
8 | export default function api() {
9 | const router = new Router({
10 | prefix: config.app.baseApi,
11 | });
12 | Object.keys(routes).forEach(name => {
13 | return routes[name]['default'](router);
14 | });
15 | return convert.compose([
16 | router.routes(),
17 | router.allowedMethods(),
18 | ]);
19 | }
20 |
--------------------------------------------------------------------------------
/server/api/routes/articles.js:
--------------------------------------------------------------------------------
1 | import * as $ from '../../controllers/articles_controller.js';
2 | import verify from '../../middleware/verify.js';
3 |
4 |
5 | export default async(router) => {
6 | router.get('/articles', verify, $.getAllArticles)
7 | .post('/articles', verify, $.createArticle)
8 | .patch('/articles/:id', verify, $.modifyArticle)
9 | .get('/articles/:id', $.getArticle)
10 | .delete('/articles/:id', verify, $.deleteArticle)
11 | .get('/publishArticles', $.getAllPublishArticles);
12 | };
13 |
--------------------------------------------------------------------------------
/server/api/routes/tags.js:
--------------------------------------------------------------------------------
1 | import * as $ from '../../controllers/tags_controller.js';
2 | import verify from '../../middleware/verify.js';
3 |
4 | export default async(router) => {
5 | router.post('/tags', verify, $.createTag)
6 | .get('/tags', $.getAllTags)
7 | .patch('/tags/:id', verify, $.modifyTag)
8 | .delete('/tags/:id', verify, $.deleteTag);
9 | };
10 |
--------------------------------------------------------------------------------
/server/api/routes/token.js:
--------------------------------------------------------------------------------
1 | import * as $ from '../../controllers/token_controller.js';
2 |
3 | export default async(router) => {
4 | $.initUser();
5 | router.post('/token', $.login);
6 | };
7 |
--------------------------------------------------------------------------------
/server/configs/index.js:
--------------------------------------------------------------------------------
1 | import fs from 'fs';
2 | let config = {
3 | app: {
4 | port: process.env.PORT || 8889,
5 | baseApi: '/api',
6 | },
7 | mongodb: {
8 | url: process.env.MONGO_URL || 'mongodb://localhost:27017/vue-blog',
9 | },
10 | jwt: {
11 | secret: 'me', // 默认
12 | },
13 | mongodbSecret: { // mongodb用户和密码
14 | user: '',
15 | pass: '',
16 | },
17 | admin: { // 后台初始化的用户名密码
18 | user: 'admin',
19 | pwd: 'password',
20 | },
21 | disqus: { // disqus
22 | url: '',
23 | },
24 | baidu: { // 百度统计
25 | url: '',
26 | },
27 | };
28 |
29 | // 可在private.js定义自己私有的配置
30 | // module.exports = {
31 | // mongodbSecret: {
32 | // user: '',
33 | // pass: ''
34 | // },
35 | // jwt: {
36 | // secret: 'xxx'
37 | // },
38 | // admin: {
39 | // user: '',
40 | // pwd: ''
41 | // }
42 | // }
43 | if (fs.existsSync(__dirname + '/private.js')) {
44 | config = Object.assign(config, require('./private.js'));
45 | }
46 | console.log(config);
47 | export default config;
48 |
--------------------------------------------------------------------------------
/server/controllers/articles_controller.js:
--------------------------------------------------------------------------------
1 | import Article from '../models/article.js';
2 | import md5 from 'md5';
3 | import jwt from 'jsonwebtoken';
4 | import config from '../configs/';
5 |
6 | export async function createArticle(ctx) {
7 | const title = ctx.request.body.title;
8 | const content = ctx.request.body.content;
9 | const abstract = ctx.request.body.abstract;
10 | const publish = ctx.request.body.publish;
11 | const tags = ctx.request.body.tags;
12 | const createTime = new Date();
13 | const lastEditTime = new Date();
14 | if (title === '') {
15 | ctx.throw(400, '标题不能为空');
16 | }
17 | if (content === '') {
18 | ctx.throw(400, '文章内容不能为空');
19 | }
20 | if (abstract === '') {
21 | ctx.throw(400, '摘要不能为空');
22 | }
23 | const article = new Article({
24 | title,
25 | content,
26 | abstract,
27 | publish,
28 | tags,
29 | createTime,
30 | lastEditTime,
31 | });
32 | let createResult = await article.save().catch(err => {
33 | ctx.throw(500, '服务器内部错误');
34 | });
35 | await Article.populate(createResult, { path: 'tags' }, function (err, result) {
36 | createResult = result;
37 | // console.log(result)
38 |
39 | });
40 | console.log('文章创建成功');
41 | ctx.body = {
42 | success: true,
43 | article: createResult,
44 | };
45 |
46 | }
47 |
48 | export async function getAllArticles(ctx) {
49 | const tag = ctx.query.tag;
50 | const page = +ctx.query.page;
51 | const limit = +ctx.query.limit || 4;
52 | let skip = 0;
53 | let articleArr;
54 | let allPage;
55 | let allNum;
56 |
57 | if (page !== 0) {
58 | skip = limit * (page - 1);
59 | }
60 |
61 | if (tag === '') {
62 | articleArr = await Article.find()
63 | .populate('tags')
64 | .sort({ createTime: -1 })
65 | .limit(limit)
66 | .skip(skip).catch(err => {
67 | ctx.throw(500, '服务器内部错误');
68 | });
69 | allNum = await Article.count().catch(err => {
70 | this.throw(500, '服务器内部错误');
71 | });
72 | } else {
73 | let tagArr = tag.split(',');
74 | // console.log(tagArr)
75 | articleArr = await Article.find({
76 | tags: { $in: tagArr },
77 | })
78 | .populate('tags')
79 | .sort({ createTime: -1 })
80 | .limit(limit)
81 | .skip(skip).catch(err => {
82 | ctx.throw(500, '服务器内部错误');
83 | });
84 | allNum = await Article.find({
85 | tags: { $in: tagArr },
86 | }).count().catch(err => {
87 | ctx.throw(500, '服务器内部错误');
88 | });
89 | }
90 | allPage = Math.ceil(allNum / limit);
91 | ctx.body = {
92 | success: true,
93 | articleArr,
94 | allPage: allPage,
95 | };
96 | }
97 |
98 | export async function getAllPublishArticles(ctx) {
99 | const tag = ctx.query.tag;
100 | const page = +ctx.query.page;
101 | const limit = +ctx.query.limit || 4;
102 | let skip = 0;
103 | let articleArr;
104 | let allPage;
105 | let allNum;
106 |
107 | if (page !== 0) {
108 | skip = limit * (page - 1);
109 | }
110 |
111 | if (tag === '') {
112 | articleArr = await Article.find({
113 | publish: true,
114 | })
115 | .populate('tags')
116 | .sort({ createTime: -1 })
117 | .limit(limit)
118 | .skip(skip).catch(err => {
119 | ctx.throw(500, '服务器内部错误');
120 | });
121 | allNum = await Article.find({
122 | publish: true,
123 | }).count().catch(err => {
124 | this.throw(500, '服务器内部错误');
125 | });
126 | } else {
127 | let tagArr = tag.split(',');
128 | // console.log(tagArr)
129 | articleArr = await Article.find({
130 | tags: { $in: tagArr },
131 | publish: true,
132 | })
133 | .populate('tags')
134 | .sort({ createTime: -1 })
135 | .limit(limit)
136 | .skip(skip).catch(err => {
137 | ctx.throw(500, '服务器内部错误');
138 | });
139 | allNum = await Article.find({
140 | tags: { $in: tagArr },
141 | }).count().catch(err => {
142 | ctx.throw(500, '服务器内部错误');
143 | });
144 | }
145 |
146 | allPage = Math.ceil(allNum / limit);
147 |
148 |
149 | ctx.body = {
150 | success: true,
151 | articleArr,
152 | allPage: allPage,
153 | };
154 | }
155 |
156 |
157 | export async function modifyArticle(ctx) {
158 | // console.log(ctx.request.body)
159 | const id = ctx.params.id;
160 | const title = ctx.request.body.title;
161 | const content = ctx.request.body.content;
162 | const abstract = ctx.request.body.abstract;
163 | const tags = ctx.request.body.tags;
164 | if (title === '') {
165 | ctx.throw(400, '标题不能为空');
166 | }
167 | if (content === '') {
168 | ctx.throw(400, '文章内容不能为空');
169 | }
170 | if (abstract === '') {
171 | ctx.throw(400, '摘要不能为空');
172 | }
173 |
174 | /* if (tags.length === 0) {
175 | ctx.throw(400, '标签不能为空')
176 | } */
177 | const article = await Article.findByIdAndUpdate(id, { $set: ctx.request.body }).catch(err => {
178 | if (err.name === 'CastError') {
179 | ctx.throw(400, 'id不存在');
180 | } else {
181 | ctx.throw(500, '服务器内部错误');
182 | }
183 | });
184 | ctx.body = {
185 | success: true,
186 | };
187 | }
188 |
189 | export async function getArticle(ctx) {
190 | const id = ctx.params.id;
191 | if (id === '') {
192 | ctx.throw(400, 'id不能为空');
193 | }
194 |
195 | /* if (tags.length === 0) {
196 | ctx.throw(400, '标签不能为空')
197 | } */
198 | const article = await Article.findById(id).catch(err => {
199 | if (err.name === 'CastError') {
200 | ctx.throw(400, 'id不存在');
201 | } else {
202 | ctx.throw(500, '服务器内部错误');
203 | }
204 | });
205 | ctx.body = {
206 | success: true,
207 | article: article,
208 | };
209 | }
210 |
211 | export async function deleteArticle(ctx) {
212 | const id = ctx.params.id;
213 | const article = await Article.findByIdAndRemove(id).catch(err => {
214 | if (err.name === 'CastError') {
215 | this.throw(400, 'id不存在');
216 | } else {
217 | this.throw(500, '服务器内部错误');
218 | }
219 | });
220 | ctx.body = {
221 | success: true,
222 | };
223 | }
224 |
225 | export async function publishArticle(ctx) {
226 | const id = ctx.params.id;
227 | const article = await Article.findByIdAndUpdate(id, { $set: { publish: true } }).catch(err => {
228 | if (err.name === 'CastError') {
229 | this.throw(400, 'id不存在');
230 | } else {
231 | this.throw(500, '服务器内部错误');
232 | }
233 | });
234 | ctx.body = {
235 | success: true,
236 | };
237 | }
238 |
239 | export async function notPublishArticle(ctx) {
240 | const id = ctx.params.id;
241 | const article = await Article.findByIdAndUpdate(id, { $set: { publish: false } }).catch(err => {
242 | if (err.name === 'CastError') {
243 | this.throw(400, 'id不存在');
244 | } else {
245 | this.throw(500, '服务器内部错误');
246 | }
247 | });
248 | ctx.body = {
249 | success: true,
250 | };
251 | }
252 |
--------------------------------------------------------------------------------
/server/controllers/tags_controller.js:
--------------------------------------------------------------------------------
1 | import Tag from '../models/tag.js';
2 | import Article from '../models/article';
3 |
4 | export async function createTag(ctx) {
5 | const tagName = ctx.request.body.name;
6 | if (tagName === '') {
7 | ctx.throw(400, '标签名不能为空');
8 | }
9 | const tag = await Tag.findOne({ name: tagName }).catch(err => {
10 | ctx.throw(500, '服务器错误');
11 | });
12 | console.log(tag);
13 | if (tag !== null) {
14 | ctx.body = {
15 | success: true,
16 | tag: tag,
17 | };
18 | return;
19 | }
20 | const newTag = new Tag({
21 | name: tagName,
22 | });
23 | const result = await newTag.save().catch(err => {
24 | ctx.throw(500, '服务器错误');
25 | });
26 | ctx.body = {
27 | success: true,
28 | tag: result,
29 | };
30 | }
31 |
32 | export async function getAllTags(ctx) {
33 | const tagArr = await Tag.find().catch(err => {
34 | ctx.throw(500, '服务器内部错误');
35 | });
36 | ctx.body = {
37 | success: true,
38 | tagArr,
39 | };
40 | }
41 |
42 | export async function modifyTag(ctx) {
43 | const id = ctx.params.id;
44 | const name = ctx.request.body.name;
45 | if (name === '') {
46 | ctx.throw(400, '标签名不能为空');
47 | }
48 | const tag = await Article.findByIdAndUpdate(id, { $set: { name: name } }).catch(err => {
49 | if (err.name === 'CastError') {
50 | ctx.throw(400, 'id不存在');
51 | } else {
52 | ctx.throw(500, '服务器内部错误');
53 | }
54 | });
55 | ctx.body = {
56 | success: true,
57 | };
58 | }
59 |
60 | export async function deleteTag(ctx) {
61 | const id = ctx.params.id;
62 | const tag = await Tag.findByIdAndRemove(id).catch(err => {
63 | if (err.name === 'CastError') {
64 | ctx.throw(400, 'id不存在');
65 | } else {
66 | ctx.throw(500, '服务器内部错误');
67 | }
68 | });
69 | ctx.body = {
70 | success: true,
71 | };
72 | }
73 |
--------------------------------------------------------------------------------
/server/controllers/token_controller.js:
--------------------------------------------------------------------------------
1 | import User from '../models/user.js';
2 | import md5 from 'md5';
3 | import jwt from 'jsonwebtoken';
4 | import config from '../configs/';
5 |
6 | export async function initUser() {
7 | let user = await User.find().exec().catch(err => {
8 | console.log(err);
9 | });
10 | if (user.length === 0) {
11 | // 目前还没做修改密码的功能,因为是单用户系统觉得需求不大
12 | // 如果想更换用户名/密码,先将数据库原有user删除(drop)
13 | // 配置中加入用户名密码,重启服务即可
14 | user = new User({
15 | name: 'hjm',
16 | username: config.admin.user,
17 | password: md5(config.admin.pwd).toUpperCase(),
18 | avatar: '',
19 | createTime: new Date(),
20 | });
21 | await user.save().catch(err => {
22 | console.log(err);
23 | });
24 | }
25 | }
26 |
27 | export async function login(ctx) {
28 | const username = ctx.request.body.username;
29 | const password = ctx.request.body.password;
30 | let user = await User.findOne({
31 | username,
32 | }).exec();
33 | if (user !== null) {
34 | if (user.password === password) {
35 | const token = jwt.sign({
36 | uid: user._id,
37 | name: user.name,
38 | exp: Math.floor(Date.now() / 1000) + 24 * 60 * 60, // 1 hours
39 | }, config.jwt.secret);
40 | ctx.body = {
41 | success: true,
42 | uid: user._id,
43 | name: user.name,
44 | token: token,
45 | };
46 | } else {
47 | ctx.throw(401, '密码错误');
48 | }
49 | } else {
50 | ctx.throw(401, '用户名错误');
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/server/index.js:
--------------------------------------------------------------------------------
1 | import koa from 'koa';
2 | import convert from 'koa-convert';
3 | import onerror from 'koa-onerror';
4 | import serve from 'koa-static';
5 | import mongoose from 'mongoose';
6 | import historyApiFallback from './middleware/historyApiFallback';
7 | import config from './configs';
8 | import middleware from './middleware';
9 | import api from './api';
10 | import path from 'path';
11 | import fs from 'fs';
12 | import { createBundleRenderer } from 'vue-server-renderer';
13 | const resolve = file => path.resolve(__dirname, file);
14 |
15 | mongoose.Promise = Promise;
16 | // connect mongodb
17 | mongoose.connect(config.mongodb.url, config.mongodbSecret);
18 | mongoose.connection.on('error', console.error);
19 |
20 | const isProd = process.env.NODE_ENV === 'production';
21 | const router = require('koa-router')();
22 | const routerInfo = require('koa-router')();
23 |
24 | const app = new koa();
25 |
26 | // middleware
27 | app.use(middleware());
28 | onerror(app);
29 |
30 | // api/router
31 | app.use(api());
32 |
33 | app.use(serve('./client/static'));
34 |
35 | // 创建渲染器,开启组件缓存
36 | let renderer;
37 |
38 | function createRenderer(bundle, template) {
39 | return createBundleRenderer(bundle, {
40 | template,
41 | cache: require('lru-cache')({
42 | max: 1000,
43 | maxAge: 1000 * 60 * 15,
44 | }),
45 | runInNewContext: false,
46 | });
47 | }
48 |
49 | // 提示webpack还在工作
50 | routerInfo.get('*', async(ctx, next) => {
51 | if (!renderer) {
52 | ctx.body = 'waiting for compilation... refresh in a moment.';
53 | return ctx.body;
54 | }
55 | return next();
56 | });
57 |
58 | app.use(routerInfo.routes());
59 |
60 | // 对路由admin直接走historyApiFallback,而不是用服务端渲染
61 | app.use(convert(historyApiFallback({
62 | verbose: true,
63 | index: '/admin.html',
64 | rewrites: [
65 | { from: /^\/admin$/, to: '/admin.html' },
66 | { from: /^\/admin\/login/, to: '/admin.html' },
67 | ],
68 | path: /^\/admin/,
69 | })));
70 |
71 | if (isProd) {
72 | // 生产环境下直接读取构造渲染器
73 | const bundle = require('../client/dist/vue-ssr-server-bundle.json');
74 | const template = fs.readFileSync(resolve('../client/dist/front.html'), 'utf-8');
75 | renderer = createRenderer(bundle, template);
76 | app.use(serve('./client/dist'));
77 | } else {
78 | // 开发环境下使用hot/dev middleware拿到bundle与template
79 | require('../client/build/setup-dev-server')(app, (bundle, template) => {
80 | renderer = createRenderer(bundle, template);
81 | });
82 | }
83 |
84 | // 流式渲染
85 | router.get('*', async(ctx, next) => {
86 | let req = ctx.req;
87 | // 由于koa内有处理type,此处需要额外修改content-type
88 | ctx.type = 'html';
89 | const s = Date.now();
90 | let context = {
91 | title: 'HJM\'s blog',
92 | url: req.url,
93 | renderURLScript: (type) => {
94 | if (config[type].url !== '') {
95 | return ``;
96 | }
97 | return '';
98 | },
99 | };
100 | // let r = renderer.renderToStream(context)
101 | // .on('data', chunk => {
102 | // console.log(chunk)
103 | // console.log("__________________")
104 | // })
105 | // .on('end', () => console.log(`whole request: ${Date.now() - s}ms`))
106 | // ctx.body = r
107 | function renderToStringPromise() {
108 | return new Promise((resolve, reject) => {
109 | renderer.renderToString(context, (err, html) => {
110 | if (err) {
111 | console.log(err);
112 | }
113 | if (!isProd) {
114 | console.log(`whole request: ${Date.now() - s}ms`);
115 | }
116 | resolve(html);
117 | });
118 | });
119 | }
120 | ctx.body = await renderToStringPromise();
121 | });
122 |
123 | app
124 | .use(router.routes())
125 | .use(router.allowedMethods());
126 |
127 | // create server
128 | app.listen(config.app.port, () => {
129 | console.log('The server is running at http://localhost:' + config.app.port);
130 | });
131 |
132 | export default app;
133 |
--------------------------------------------------------------------------------
/server/logs/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BUPT-HJM/vue-blog/747306b702fe84d215478403d61f49332e15f829/server/logs/.gitkeep
--------------------------------------------------------------------------------
/server/middleware/historyApiFallback.js:
--------------------------------------------------------------------------------
1 | function historyApiFallback (options) {
2 | const expressMiddleware = require('connect-history-api-fallback')(options);
3 | const url = require('url');
4 | return (ctx, next) => {
5 | let parseUrl = url.parse(ctx.req.url);
6 | // 添加path match,让不匹配的路由可以直接穿过中间件
7 | if (!parseUrl.pathname.match(options.path)) {
8 | return next();
9 | }
10 | // 修改content-type
11 | ctx.type = 'html';
12 | return expressMiddleware(ctx.req, ctx.res, next);
13 | };
14 | }
15 |
16 | module.exports = historyApiFallback;
17 |
--------------------------------------------------------------------------------
/server/middleware/index.js:
--------------------------------------------------------------------------------
1 | import logger from 'koa-logger';
2 | import bodyParser from 'koa-bodyparser';
3 | import convert from 'koa-convert';
4 | import onerror from 'koa-onerror';
5 | import compress from 'koa-compress';
6 |
7 | export default function middleware() {
8 | return convert.compose(
9 | logger(),
10 | bodyParser(),
11 | compress({
12 | filter: function (content_type) {
13 | if (/event-stream/i.test(content_type)) {
14 | // 为了让hot reload生效,不对__webpack_hmr压缩
15 | return false;
16 | } else {
17 | return true;
18 | }
19 | },
20 | })
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/server/middleware/verify.js:
--------------------------------------------------------------------------------
1 | import jwt from 'jsonwebtoken';
2 | import config from '../configs/';
3 | export default async(ctx, next) => {
4 | // console.log(ctx.get('Authorization'));
5 | const authorization = ctx.get('Authorization');
6 | if (authorization === '') {
7 | ctx.throw(401, 'no token detected in http header \'Authorization\'');
8 | }
9 | const token = authorization.split(' ')[1];
10 | let tokenContent;
11 | try {
12 | tokenContent = await jwt.verify(token, config.jwt.secret);
13 | } catch (err) {
14 | if ('TokenExpiredError' === err.name) {
15 | ctx.throw(401, 'token expired,请及时本地保存数据!');
16 | }
17 | ctx.throw(401, 'invalid token');
18 | }
19 | console.log('鉴权成功');
20 | await next();
21 | };
22 |
--------------------------------------------------------------------------------
/server/models/article.js:
--------------------------------------------------------------------------------
1 | import mongoose from 'mongoose';
2 | import moment from 'moment';
3 | moment.locale('zh-cn');
4 | const Schema = mongoose.Schema;
5 | const articleSchema = new Schema({
6 | title: String,
7 | content: String,
8 | abstract: String,
9 | tags: [{
10 | type: Schema.Types.ObjectId,
11 | ref: 'tag',
12 | }],
13 | publish: {
14 | type: Boolean,
15 | default: false,
16 | },
17 | createTime: {
18 | type: Date,
19 | },
20 | lastEditTime: {
21 | type: Date,
22 | default: Date.now,
23 | },
24 | }, { versionKey: false });
25 | articleSchema.set('toJSON', { getters: true, virtuals: true });
26 | articleSchema.set('toObject', { getters: true, virtuals: true });
27 | articleSchema.path('createTime').get(function (v) {
28 | return moment(v).format('lll');
29 | });
30 | articleSchema.path('lastEditTime').get(function (v) {
31 | return moment(v).format('lll');
32 | });
33 | module.exports = mongoose.model('article', articleSchema);
34 |
--------------------------------------------------------------------------------
/server/models/tag.js:
--------------------------------------------------------------------------------
1 | import mongoose from 'mongoose';
2 | const Schema = mongoose.Schema;
3 | const tagSchema = new Schema({
4 | name: {
5 | type: String,
6 | default: '',
7 | },
8 | }, { versionKey: false });
9 | tagSchema.set('toJSON', { getters: true, virtuals: true });
10 | tagSchema.set('toObject', { getters: true, virtuals: true }); // 普通+虚拟
11 |
12 | module.exports = mongoose.model('tag', tagSchema);
13 |
--------------------------------------------------------------------------------
/server/models/user.js:
--------------------------------------------------------------------------------
1 | import mongoose from 'mongoose';
2 | const Schema = mongoose.Schema;
3 | const userSchema = new Schema({
4 | name: String,
5 | username: String,
6 | password: String,
7 | avatar: String,
8 | createTime: String,
9 | }, { versionKey: false });
10 | module.exports = mongoose.model('user', userSchema);
11 |
--------------------------------------------------------------------------------
/server/start.js:
--------------------------------------------------------------------------------
1 | require('babel-core/register')({
2 | presets: ['es2015', 'stage-0'],
3 | });
4 | require('babel-polyfill');
5 |
6 | module.exports = require('./index.js');
7 |
8 |
--------------------------------------------------------------------------------