├── .babelrc
├── .editorconfig
├── .eslintignore
├── .gitignore
├── .postcssrc.js
├── README.md
├── admin
├── build
│ ├── build.js
│ ├── check-versions.js
│ ├── dev-client.js
│ ├── dev-server.js
│ ├── utils.js
│ ├── vue-loader.conf.js
│ ├── webpack.base.conf.js
│ ├── webpack.dev.conf.js
│ └── webpack.prod.conf.js
├── config
│ ├── dev.env.js
│ ├── index.js
│ └── prod.env.js
├── index.html
└── src
│ ├── App.vue
│ ├── assets
│ ├── font
│ │ ├── ODelI1aHBYDBqgeIAH2zlIa1YDtoarzwSXxTHggEXMw.woff2
│ │ ├── ODelI1aHBYDBqgeIAH2zlJbPFduIYtoLzwST68uhz_Y.woff2
│ │ ├── kaishu.ttf
│ │ ├── mAcLJWdPWDNiDJwJvcWKc3YhjbSpvc47ee6xR_80Hnw.woff2
│ │ ├── toadOcfmlt9b38dHJxOBGKyGJhAh-RE0BxGcd_izyev3rGVtsTkPsbDajuO5ueQw.woff2
│ │ └── toadOcfmlt9b38dHJxOBGMzFoXZ-Kj537nB_-9jJhlA.woff2
│ ├── img
│ │ ├── icon
│ │ │ ├── demo.css
│ │ │ ├── demo_fontclass.html
│ │ │ ├── demo_symbol.html
│ │ │ ├── demo_unicode.html
│ │ │ ├── iconfont.css
│ │ │ ├── iconfont.eot
│ │ │ ├── iconfont.js
│ │ │ ├── iconfont.svg
│ │ │ ├── iconfont.ttf
│ │ │ └── iconfont.woff
│ │ ├── logo.png
│ │ ├── star-half.png
│ │ ├── star-off.png
│ │ ├── star-on.png
│ │ └── subhead.png
│ └── style
│ │ ├── _font.scss
│ │ ├── _variable.scss
│ │ └── index.scss
│ ├── components
│ ├── About.vue
│ ├── List.vue
│ ├── Login.vue
│ ├── ReadingList.vue
│ ├── Tag.vue
│ └── common
│ │ ├── ArticleList.vue
│ │ ├── Editor.vue
│ │ ├── HeaderNav.vue
│ │ ├── SideNav.vue
│ │ └── Star.vue
│ ├── main.js
│ ├── router
│ └── index.js
│ └── store
│ ├── actions.js
│ ├── getters.js
│ ├── index.js
│ ├── mutations.js
│ └── state.js
├── client
├── build
│ ├── build.js
│ ├── check-versions.js
│ ├── dev-client.js
│ ├── dev-server.js
│ ├── utils.js
│ ├── vue-loader.conf.js
│ ├── webpack.base.conf.js
│ ├── webpack.dev.conf.js
│ └── webpack.prod.conf.js
├── config
│ ├── dev.env.js
│ ├── index.js
│ ├── prod.env.js
│ └── test.env.js
├── index.html
└── src
│ ├── App.vue
│ ├── assets
│ ├── font
│ │ ├── ODelI1aHBYDBqgeIAH2zlIa1YDtoarzwSXxTHggEXMw.woff2
│ │ ├── ODelI1aHBYDBqgeIAH2zlJbPFduIYtoLzwST68uhz_Y.woff2
│ │ ├── mAcLJWdPWDNiDJwJvcWKc3YhjbSpvc47ee6xR_80Hnw.woff2
│ │ ├── toadOcfmlt9b38dHJxOBGKyGJhAh-RE0BxGcd_izyev3rGVtsTkPsbDajuO5ueQw.woff2
│ │ └── toadOcfmlt9b38dHJxOBGMzFoXZ-Kj537nB_-9jJhlA.woff2
│ ├── img
│ │ ├── logo.png
│ │ ├── star-half.png
│ │ ├── star-off.png
│ │ └── star-on.png
│ └── style
│ │ ├── _font.scss
│ │ ├── _highlight.scss
│ │ ├── _variable.scss
│ │ └── index.scss
│ ├── components
│ ├── About.vue
│ ├── Archive.vue
│ ├── Article.vue
│ ├── ArticleList.vue
│ ├── ReadingList.vue
│ ├── Tag.vue
│ └── common
│ │ ├── HeaderNav.vue
│ │ └── Star.vue
│ ├── main.js
│ ├── router
│ └── index.js
│ └── utils
│ └── parseMarkdown.js
├── favicon.ico
├── package.json
├── server
├── .babelrc
├── config
│ ├── index.js
│ └── pm2_config.json
├── controllers
│ ├── articles.js
│ ├── books.js
│ ├── briefs.js
│ ├── introductions.js
│ ├── tags.js
│ └── tokens.js
├── index.js
├── middlewares
│ ├── check.js
│ └── verify.js
├── models
│ ├── articles.js
│ ├── books.js
│ ├── briefs.js
│ ├── introductions.js
│ ├── tags.js
│ └── users.js
├── routes
│ ├── articles.js
│ ├── books.js
│ ├── briefs.js
│ ├── index.js
│ ├── introductions.js
│ ├── tags.js
│ └── tokens.js
├── sql
│ └── ashen_db.sql
└── utils
│ ├── escape.js
│ ├── query.js
│ └── routesLoader.js
└── static
└── .gitkeep
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | ["env", {
4 | "modules": false,
5 | "targets": {
6 | "browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
7 | }
8 | }],
9 | "stage-2"
10 | ],
11 | "plugins": ["transform-runtime"]
12 | }
13 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | indent_style = space
6 | indent_size = 4
7 | end_of_line = lf
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | client/build/*.js
2 | client/config/*.js
3 | admin/build/*.js
4 | admin/config/*.js
5 | admin/src/assets/*
6 | admin/src/main.js
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules/
3 | dist/
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Editor directories and files
9 | .idea
10 | .vscode
11 | *.suo
12 | *.ntvs*
13 | *.njsproj
14 | *.sln
15 |
16 | package-lock.json*
17 |
18 | client/dist/
19 | admin/dist/
20 |
21 | client/.eslintrc.js
22 | admin/.eslintrc.js
23 | server/.eslintrc.js
24 |
--------------------------------------------------------------------------------
/.postcssrc.js:
--------------------------------------------------------------------------------
1 | // https://github.com/michael-ciniawsky/postcss-load-config
2 |
3 | module.exports = {
4 | "plugins": {
5 | // to edit target browsers: use "browserslist" field in package.json
6 | "autoprefixer": {}
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Ashen Blog
2 |
3 | > v1.1
4 |
5 | > Ashen Blog Management System, developed with Koa2 and Vue2, easily build your own blog.
6 |
7 | > Ashen Blog 管理系统,采用 Koa2 和 Vue2 完成开发,帮助你轻松的搭建自己的博客。
8 |
9 | ### 介绍
10 |
11 | Ashen Blog系统遵循ES6+的代码标准,前端采用了Vue 2.x作为开发框架,后端采用了Koa 2.x作为RESTful API 服务器开发框架,是一款的前后端分离并利用axios进行数据通信的单页面应用。
12 |
13 | Client端展示博客,目前有:文章列表、文章详情、日期归档、标签归档、阅读列表和个人介绍。
14 |
15 | Admin端管理博客,目前支持:Markdown编写博客、快捷按键及Tool bars、自动保存博客、批量标签管理、阅读列表管理、撰写个人介绍。
16 |
17 | Server端作为RESTful API服务器,负责与Client/Admin端进行数据通信。
18 |
19 | 数据持久化方面使用Mysql作为数据库。
20 |
21 | demo地址:
22 |
23 | [无火的余灰](http://58.87.77.212)
24 |
25 | 
26 | 客户端界面
27 |
28 | 
29 | 管理端界面
30 |
31 | ### 快速使用
32 |
33 | 下载好项目以后,首先安装依赖:
34 |
35 | ```bash
36 | npm install
37 | ```
38 |
39 | 需要修改config文件:
40 |
41 | ```bash
42 | # 修改baseUrl为你的服务器地址
43 | vim admin/src/main.js
44 |
45 | # 修改baseUrl为你的服务器地址
46 | vim client/src/main.js
47 |
48 | # 修改数据库配置db为你的数据库配置
49 | vim server/config/index.js
50 | ```
51 |
52 | ### Client 端
53 |
54 | 使用命令:
55 |
56 | ```bash
57 | # 以开发模式运行Client 端
58 | npm run dev-client
59 |
60 | # 打包Client端
61 | npm run build-client
62 | ```
63 |
64 | Client端展示博客,目前有:文章列表、文章详情、日期归档、标签归档、阅读列表和个人介绍。
65 |
66 | 使用marked实现Markdown解析。
67 |
68 | 使用highlight.js实现代码高亮。
69 |
70 | 使用moment对显示日期进行格式化。
71 |
72 | ### Admin 端
73 |
74 | 使用命令:
75 |
76 | ```bash
77 | # 以开发模式运行Admin 端
78 | npm run dev-admin
79 |
80 | # 打包Admin端
81 | npm run build-admin
82 | ```
83 |
84 | Admin端管理博客,目前支持:Markdown编写博客、快捷按键及Tool bars、自动保存博客、批量标签管理、阅读列表管理、撰写个人介绍。
85 |
86 | `初始账号:admin`
87 |
88 | `初始密码:1qaz@wsx`
89 |
90 | 使用Simplemde实现Markdown编写,支持快捷键和自动保存,具体快捷键请查看相关文档:[simple-markdown-editor](https://github.com/sparksuite/simplemde-markdown-editor)
91 |
92 | 利用函数去抖及axios实现文章的自动保存。
93 |
94 | ### Server 端
95 |
96 | 使用命令:
97 |
98 | ```bash
99 | # 以开发模式运行Server 端
100 | npm run dev-server
101 |
102 | # 部署服务(请先全局安装pm2)
103 | npm start
104 | ```
105 |
106 | Server端作为RESTful API服务器,负责与Client/Admin端进行数据通信。
107 |
108 | 利用JWT实现鉴权系统。
109 |
110 | 利用Koa2及一些中间件和工具函数实现REST。
111 |
112 | ### Contribute
113 |
114 | 欢迎提交issue。
115 |
116 | 欢迎提交pr,请fork dev分支,并在其上编写代码。
117 |
118 | 非常感谢!
119 |
120 | ### 致谢
121 |
122 | [Chuck Liu的Kov-Blog](https://github.com/Ma63d/kov-blog)
123 |
124 | ### License
125 |
126 | [MIT](https://opensource.org/licenses/MIT)
127 |
--------------------------------------------------------------------------------
/admin/build/build.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | require('./check-versions')()
3 |
4 | process.env.NODE_ENV = 'production'
5 |
6 | const ora = require('ora')
7 | const rm = require('rimraf')
8 | const path = require('path')
9 | const chalk = require('chalk')
10 | const webpack = require('webpack')
11 | const config = require('../config/index')
12 | const webpackConfig = require('./webpack.prod.conf')
13 |
14 | const spinner = ora('building for production...')
15 | spinner.start()
16 |
17 | rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
18 | if (err) throw err
19 | webpack(webpackConfig, function (err, stats) {
20 | spinner.stop()
21 | if (err) throw err
22 | process.stdout.write(stats.toString({
23 | colors: true,
24 | modules: false,
25 | children: false,
26 | chunks: false,
27 | chunkModules: false
28 | }) + '\n\n')
29 |
30 | if (stats.hasErrors()) {
31 | console.log(chalk.red(' Build failed with errors.\n'))
32 | process.exit(1)
33 | }
34 |
35 | console.log(chalk.cyan(' Build complete.\n'))
36 | console.log(chalk.yellow(
37 | ' Tip: built files are meant to be served over an HTTP server.\n' +
38 | ' Opening index.html over file:// won\'t work.\n'
39 | ))
40 | })
41 | })
42 |
--------------------------------------------------------------------------------
/admin/build/check-versions.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const chalk = require('chalk')
3 | const semver = require('semver')
4 | const packageConfig = require('../../package.json')
5 | const shell = require('shelljs')
6 | function exec (cmd) {
7 | return require('child_process').execSync(cmd).toString().trim()
8 | }
9 |
10 | const versionRequirements = [
11 | {
12 | name: 'node',
13 | currentVersion: semver.clean(process.version),
14 | versionRequirement: packageConfig.engines.node
15 | }
16 | ]
17 |
18 | if (shell.which('npm')) {
19 | versionRequirements.push({
20 | name: 'npm',
21 | currentVersion: exec('npm --version'),
22 | versionRequirement: packageConfig.engines.npm
23 | })
24 | }
25 |
26 | module.exports = function () {
27 | const warnings = []
28 | for (let i = 0; i < versionRequirements.length; i++) {
29 | const mod = versionRequirements[i]
30 | if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
31 | warnings.push(mod.name + ': ' +
32 | chalk.red(mod.currentVersion) + ' should be ' +
33 | chalk.green(mod.versionRequirement)
34 | )
35 | }
36 | }
37 |
38 | if (warnings.length) {
39 | console.log('')
40 | console.log(chalk.yellow('To use this template, you must update following to modules:'))
41 | console.log()
42 | for (let i = 0; i < warnings.length; i++) {
43 | const warning = warnings[i]
44 | console.log(' ' + warning)
45 | }
46 | console.log()
47 | process.exit(1)
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/admin/build/dev-client.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | 'use strict'
3 | require('eventsource-polyfill')
4 | const hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true')
5 |
6 | hotClient.subscribe(function (event) {
7 | if (event.action === 'reload') {
8 | window.location.reload()
9 | }
10 | })
11 |
--------------------------------------------------------------------------------
/admin/build/dev-server.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | require('./check-versions')()
3 |
4 | const config = require('../config/index')
5 | if (!process.env.NODE_ENV) {
6 | process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV)
7 | }
8 |
9 | const opn = require('opn')
10 | const path = require('path')
11 | const express = require('express')
12 | const webpack = require('webpack')
13 | const proxyMiddleware = require('http-proxy-middleware')
14 | const webpackConfig = require('./webpack.dev.conf')
15 |
16 | // default port where dev server listens for incoming traffic
17 | const port = process.env.PORT || config.dev.port
18 | // automatically open browser, if not set will be false
19 | const autoOpenBrowser = !!config.dev.autoOpenBrowser
20 | // Define HTTP proxies to your custom API backend
21 | // https://github.com/chimurai/http-proxy-middleware
22 | const proxyTable = config.dev.proxyTable
23 |
24 | const app = express()
25 | const compiler = webpack(webpackConfig)
26 |
27 | const devMiddleware = require('webpack-dev-middleware')(compiler, {
28 | publicPath: webpackConfig.output.publicPath,
29 | quiet: true
30 | })
31 |
32 | const hotMiddleware = require('webpack-hot-middleware')(compiler, {
33 | log: false,
34 | heartbeat: 2000
35 | })
36 | // force page reload when html-webpack-plugin template changes
37 | // currently disabled until this is resolved:
38 | // https://github.com/jantimon/html-webpack-plugin/issues/680
39 | // compiler.plugin('compilation', function (compilation) {
40 | // compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) {
41 | // hotMiddleware.publish({ action: 'reload' })
42 | // cb()
43 | // })
44 | // })
45 |
46 | // enable hot-reload and state-preserving
47 | // compilation error display
48 | app.use(hotMiddleware)
49 |
50 | // proxy api requests
51 | Object.keys(proxyTable).forEach(function (context) {
52 | const options = proxyTable[context]
53 | if (typeof options === 'string') {
54 | options = { target: options }
55 | }
56 | app.use(proxyMiddleware(options.filter || context, options))
57 | })
58 |
59 | // handle fallback for HTML5 history API
60 | app.use(require('connect-history-api-fallback')())
61 |
62 | // serve webpack bundle output
63 | app.use(devMiddleware)
64 |
65 | // serve pure static assets
66 | const staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory)
67 | app.use(staticPath, express.static('./static'))
68 |
69 | const uri = 'http://localhost:' + port
70 |
71 | var _resolve
72 | var _reject
73 | var readyPromise = new Promise((resolve, reject) => {
74 | _resolve = resolve
75 | _reject = reject
76 | })
77 |
78 | var server
79 | var portfinder = require('portfinder')
80 | portfinder.basePort = port
81 |
82 | console.log('> Starting dev server...')
83 | devMiddleware.waitUntilValid(() => {
84 | portfinder.getPort((err, port) => {
85 | if (err) {
86 | _reject(err)
87 | }
88 | process.env.PORT = port
89 | var uri = 'http://localhost:' + port
90 | console.log('> Listening at ' + uri + '\n')
91 | // when env is testing, don't need open it
92 | if (autoOpenBrowser && process.env.NODE_ENV !== 'testing') {
93 | opn(uri)
94 | }
95 | server = app.listen(port)
96 | _resolve()
97 | })
98 | })
99 |
100 | module.exports = {
101 | ready: readyPromise,
102 | close: () => {
103 | server.close()
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/admin/build/utils.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const path = require('path')
3 | const config = require('../config/index')
4 | const ExtractTextPlugin = require('extract-text-webpack-plugin')
5 |
6 | exports.assetsPath = function (_path) {
7 | const assetsSubDirectory = process.env.NODE_ENV === 'production'
8 | ? config.build.assetsSubDirectory
9 | : config.dev.assetsSubDirectory
10 | return path.posix.join(assetsSubDirectory, _path)
11 | }
12 |
13 | exports.cssLoaders = function (options) {
14 | options = options || {}
15 |
16 | const cssLoader = {
17 | loader: 'css-loader',
18 | options: {
19 | minimize: process.env.NODE_ENV === 'production',
20 | sourceMap: options.sourceMap
21 | }
22 | }
23 |
24 | // generate loader string to be used with extract text plugin
25 | function generateLoaders (loader, loaderOptions) {
26 | const loaders = [cssLoader]
27 | if (loader) {
28 | loaders.push({
29 | loader: loader + '-loader',
30 | options: Object.assign({}, loaderOptions, {
31 | sourceMap: options.sourceMap
32 | })
33 | })
34 | }
35 |
36 | // Extract CSS when that option is specified
37 | // (which is the case during production build)
38 | if (options.extract) {
39 | return ExtractTextPlugin.extract({
40 | use: loaders,
41 | fallback: 'vue-style-loader'
42 | })
43 | } else {
44 | return ['vue-style-loader'].concat(loaders)
45 | }
46 | }
47 |
48 | // https://vue-loader.vuejs.org/en/configurations/extract-css.html
49 | return {
50 | css: generateLoaders(),
51 | postcss: generateLoaders(),
52 | less: generateLoaders('less'),
53 | sass: generateLoaders('sass', { indentedSyntax: true }),
54 | scss: generateLoaders('sass').concat({
55 | loader: 'sass-resources-loader',
56 | options: {
57 | resources: path.resolve(__dirname, '../src/assets/style/_variable.scss')
58 | }
59 | }),
60 | stylus: generateLoaders('stylus'),
61 | styl: generateLoaders('stylus')
62 | }
63 | }
64 |
65 | // Generate loaders for standalone style files (outside of .vue)
66 | exports.styleLoaders = function (options) {
67 | const output = []
68 | const loaders = exports.cssLoaders(options)
69 | for (const extension in loaders) {
70 | const loader = loaders[extension]
71 | output.push({
72 | test: new RegExp('\\.' + extension + '$'),
73 | use: loader
74 | })
75 | }
76 | return output
77 | }
78 |
--------------------------------------------------------------------------------
/admin/build/vue-loader.conf.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const utils = require('./utils')
3 | const config = require('../config/index')
4 | const isProduction = process.env.NODE_ENV === 'production'
5 |
6 | module.exports = {
7 | loaders: utils.cssLoaders({
8 | sourceMap: isProduction
9 | ? config.build.productionSourceMap
10 | : config.dev.cssSourceMap,
11 | extract: isProduction
12 | }),
13 | transformToRequire: {
14 | video: 'src',
15 | source: 'src',
16 | img: 'src',
17 | image: 'xlink:href'
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/admin/build/webpack.base.conf.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const path = require('path')
3 | const utils = require('./utils')
4 | const config = require('../config/index')
5 | const vueLoaderConfig = require('./vue-loader.conf')
6 |
7 | function resolve(dir) {
8 | return path.join(__dirname, '..', dir)
9 | }
10 |
11 | module.exports = {
12 | entry: {
13 | app: './admin/src/main.js'
14 | },
15 | output: {
16 | path: config.build.assetsRoot,
17 | filename: '[name].js',
18 | publicPath: process.env.NODE_ENV === 'production'
19 | ? config.build.assetsPublicPath
20 | : config.dev.assetsPublicPath
21 | },
22 | resolve: {
23 | extensions: ['.js', '.vue', '.json'],
24 | alias: {
25 | 'vue$': 'vue/dist/vue.esm.js',
26 | '@': resolve('src')
27 | }
28 | },
29 | module: {
30 | rules: [
31 | // {
32 | // test: /\.(js|vue)$/,
33 | // loader: 'eslint-loader',
34 | // enforce: 'pre',
35 | // include: [resolve('src'), resolve('test')],
36 | // options: {
37 | // formatter: require('eslint-friendly-formatter')
38 | // }
39 | // },
40 | {
41 | test: /\.vue$/,
42 | loader: 'vue-loader',
43 | options: vueLoaderConfig
44 | },
45 | {
46 | test: /\.js$/,
47 | loader: 'babel-loader',
48 | include: [resolve('src'), resolve('test')]
49 | },
50 | {
51 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
52 | loader: 'url-loader',
53 | options: {
54 | limit: 10000,
55 | name: utils.assetsPath('img/[name].[hash:7].[ext]')
56 | }
57 | },
58 | {
59 | test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
60 | loader: 'url-loader',
61 | options: {
62 | limit: 10000,
63 | name: utils.assetsPath('media/[name].[hash:7].[ext]')
64 | }
65 | },
66 | {
67 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
68 | loader: 'url-loader',
69 | options: {
70 | limit: 100000,
71 | name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
72 | }
73 | }
74 | ]
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/admin/build/webpack.dev.conf.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const utils = require('./utils')
3 | const webpack = require('webpack')
4 | const config = require('../config/index')
5 | const merge = require('webpack-merge')
6 | const baseWebpackConfig = require('./webpack.base.conf')
7 | const HtmlWebpackPlugin = require('html-webpack-plugin')
8 | const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
9 |
10 | // add hot-reload related code to entry chunks
11 | Object.keys(baseWebpackConfig.entry).forEach(function (name) {
12 | baseWebpackConfig.entry[name] = ['./admin/build/dev-client'].concat(baseWebpackConfig.entry[name])
13 | })
14 |
15 | module.exports = merge(baseWebpackConfig, {
16 | module: {
17 | rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap })
18 | },
19 | // cheap-module-eval-source-map is faster for development
20 | devtool: '#cheap-module-eval-source-map',
21 | plugins: [
22 | new webpack.DefinePlugin({
23 | 'process.env': config.dev.env
24 | }),
25 | // https://github.com/glenjamin/webpack-hot-middleware#installation--usage
26 | new webpack.HotModuleReplacementPlugin(),
27 | new webpack.NoEmitOnErrorsPlugin(),
28 | // https://github.com/ampedandwired/html-webpack-plugin
29 | new HtmlWebpackPlugin({
30 | filename: 'index.html',
31 | template: './admin/index.html',
32 | favicon: './favicon.ico',
33 | inject: true
34 | }),
35 | new FriendlyErrorsPlugin(),
36 | new webpack.ProvidePlugin({
37 | axios: 'axios'
38 | })
39 | ]
40 | })
41 |
--------------------------------------------------------------------------------
/admin/build/webpack.prod.conf.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const path = require('path')
3 | const utils = require('./utils')
4 | const webpack = require('webpack')
5 | const config = require('../config/index')
6 | const merge = require('webpack-merge')
7 | const baseWebpackConfig = require('./webpack.base.conf')
8 | const CopyWebpackPlugin = require('copy-webpack-plugin')
9 | const HtmlWebpackPlugin = require('html-webpack-plugin')
10 | const ExtractTextPlugin = require('extract-text-webpack-plugin')
11 | const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
12 |
13 | const env = config.build.env
14 |
15 | const webpackConfig = merge(baseWebpackConfig, {
16 | module: {
17 | rules: utils.styleLoaders({
18 | sourceMap: config.build.productionSourceMap,
19 | extract: true
20 | })
21 | },
22 | devtool: config.build.productionSourceMap ? '#source-map' : false,
23 | output: {
24 | path: config.build.assetsRoot,
25 | filename: utils.assetsPath('js/[name].[chunkhash].js'),
26 | chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
27 | },
28 | plugins: [
29 | new webpack.ContextReplacementPlugin(/moment[\\\/]locale$/, /^\.\/(zh-cn|en-gb)$/),
30 | new webpack.ProvidePlugin({
31 | axios: 'axios'
32 | }),
33 | // http://vuejs.github.io/vue-loader/en/workflow/production.html
34 | new webpack.DefinePlugin({
35 | 'process.env': env
36 | }),
37 | // UglifyJs do not support ES6+, you can also use babel-minify for better treeshaking: https://github.com/babel/minify
38 | new webpack.optimize.UglifyJsPlugin({
39 | compress: {
40 | warnings: false
41 | },
42 | sourceMap: true
43 | }),
44 | // extract css into its own file
45 | new ExtractTextPlugin({
46 | filename: utils.assetsPath('css/[name].[contenthash].css')
47 | }),
48 | // Compress extracted CSS. We are using this plugin so that possible
49 | // duplicated CSS from different components can be deduped.
50 | new OptimizeCSSPlugin({
51 | cssProcessorOptions: {
52 | safe: true
53 | }
54 | }),
55 | // generate dist index.html with correct asset hash for caching.
56 | // you can customize output by editing /index.html
57 | // see https://github.com/ampedandwired/html-webpack-plugin
58 | new HtmlWebpackPlugin({
59 | filename: config.build.index,
60 | favicon: './favicon.ico',
61 | template: './admin/index.html',
62 | inject: true,
63 | minify: {
64 | removeComments: true,
65 | collapseWhitespace: true,
66 | removeAttributeQuotes: true
67 | // more options:
68 | // https://github.com/kangax/html-minifier#options-quick-reference
69 | },
70 | // necessary to consistently work with multiple chunks via CommonsChunkPlugin
71 | chunksSortMode: 'dependency'
72 | }),
73 | // keep module.id stable when vender modules does not change
74 | new webpack.HashedModuleIdsPlugin(),
75 | // split vendor js into its own file
76 | new webpack.optimize.CommonsChunkPlugin({
77 | name: 'vendor',
78 | minChunks: function (module) {
79 | // any required modules inside node_modules are extracted to vendor
80 | return (
81 | module.resource &&
82 | /\.js$/.test(module.resource) &&
83 | module.resource.indexOf(
84 | path.join(__dirname, '../../node_modules')
85 | ) === 0
86 | )
87 | }
88 | }),
89 | // extract webpack runtime and module manifest to its own file in order to
90 | // prevent vendor hash from being updated whenever app bundle is updated
91 | new webpack.optimize.CommonsChunkPlugin({
92 | name: 'manifest',
93 | chunks: ['vendor']
94 | }),
95 | // copy custom static assets
96 | new CopyWebpackPlugin([
97 | {
98 | from: path.resolve(__dirname, '../../static'),
99 | to: config.build.assetsSubDirectory,
100 | ignore: ['.*']
101 | }
102 | ])
103 | ]
104 | })
105 |
106 | if (config.build.productionGzip) {
107 | const CompressionWebpackPlugin = require('compression-webpack-plugin')
108 |
109 | webpackConfig.plugins.push(
110 | new CompressionWebpackPlugin({
111 | asset: '[path].gz[query]',
112 | algorithm: 'gzip',
113 | test: new RegExp(
114 | '\\.(' +
115 | config.build.productionGzipExtensions.join('|') +
116 | ')$'
117 | ),
118 | threshold: 10240,
119 | minRatio: 0.8
120 | })
121 | )
122 | }
123 |
124 | if (config.build.bundleAnalyzerReport) {
125 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
126 | webpackConfig.plugins.push(new BundleAnalyzerPlugin())
127 | }
128 |
129 | module.exports = webpackConfig
130 |
--------------------------------------------------------------------------------
/admin/config/dev.env.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const merge = require('webpack-merge')
3 | const prodEnv = require('./prod.env')
4 |
5 | module.exports = merge(prodEnv, {
6 | NODE_ENV: '"development"'
7 | })
8 |
--------------------------------------------------------------------------------
/admin/config/index.js:
--------------------------------------------------------------------------------
1 |
2 | 'use strict'
3 | // Template version: 1.1.1
4 | // see http://vuejs-templates.github.io/webpack for documentation.
5 |
6 | const path = require('path')
7 |
8 | module.exports = {
9 | build: {
10 | env: require('./prod.env'),
11 | index: path.resolve(__dirname, '../dist/index.html'),
12 | assetsRoot: path.resolve(__dirname, '../dist'),
13 | assetsSubDirectory: 'static',
14 | assetsPublicPath: './',
15 | productionSourceMap: true,
16 | // Gzip off by default as many popular static hosts such as
17 | // Surge or Netlify already gzip all static assets for you.
18 | // Before setting to `true`, make sure to:
19 | // npm install --save-dev compression-webpack-plugin
20 | productionGzip: false,
21 | productionGzipExtensions: ['js', 'css'],
22 | // Run the build command with an extra argument to
23 | // View the bundle analyzer report after build finishes:
24 | // `npm run build --report`
25 | // Set to `true` or `false` to always turn it on or off
26 | bundleAnalyzerReport: process.env.npm_config_report
27 | },
28 | dev: {
29 | env: require('./dev.env'),
30 | port: process.env.PORT || 8080,
31 | autoOpenBrowser: true,
32 | assetsSubDirectory: 'static',
33 | assetsPublicPath: '/',
34 | proxyTable: {},
35 | // CSS Sourcemaps off by default because relative paths are "buggy"
36 | // with this option, according to the CSS-Loader README
37 | // (https://github.com/webpack/css-loader#sourcemaps)
38 | // In our experience, they generally work as expected,
39 | // just be aware of this issue when enabling this option.
40 | cssSourceMap: false
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/admin/config/prod.env.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | module.exports = {
3 | NODE_ENV: '"production"'
4 | }
5 |
--------------------------------------------------------------------------------
/admin/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 无火的余灰-后台管理
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/admin/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
39 |
40 |
43 |
--------------------------------------------------------------------------------
/admin/src/assets/font/ODelI1aHBYDBqgeIAH2zlIa1YDtoarzwSXxTHggEXMw.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StudentWan/ashen-blog/ba4d3e3a5b47c5ab671fe98fe733eadada45d258/admin/src/assets/font/ODelI1aHBYDBqgeIAH2zlIa1YDtoarzwSXxTHggEXMw.woff2
--------------------------------------------------------------------------------
/admin/src/assets/font/ODelI1aHBYDBqgeIAH2zlJbPFduIYtoLzwST68uhz_Y.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StudentWan/ashen-blog/ba4d3e3a5b47c5ab671fe98fe733eadada45d258/admin/src/assets/font/ODelI1aHBYDBqgeIAH2zlJbPFduIYtoLzwST68uhz_Y.woff2
--------------------------------------------------------------------------------
/admin/src/assets/font/kaishu.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StudentWan/ashen-blog/ba4d3e3a5b47c5ab671fe98fe733eadada45d258/admin/src/assets/font/kaishu.ttf
--------------------------------------------------------------------------------
/admin/src/assets/font/mAcLJWdPWDNiDJwJvcWKc3YhjbSpvc47ee6xR_80Hnw.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StudentWan/ashen-blog/ba4d3e3a5b47c5ab671fe98fe733eadada45d258/admin/src/assets/font/mAcLJWdPWDNiDJwJvcWKc3YhjbSpvc47ee6xR_80Hnw.woff2
--------------------------------------------------------------------------------
/admin/src/assets/font/toadOcfmlt9b38dHJxOBGKyGJhAh-RE0BxGcd_izyev3rGVtsTkPsbDajuO5ueQw.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StudentWan/ashen-blog/ba4d3e3a5b47c5ab671fe98fe733eadada45d258/admin/src/assets/font/toadOcfmlt9b38dHJxOBGKyGJhAh-RE0BxGcd_izyev3rGVtsTkPsbDajuO5ueQw.woff2
--------------------------------------------------------------------------------
/admin/src/assets/font/toadOcfmlt9b38dHJxOBGMzFoXZ-Kj537nB_-9jJhlA.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StudentWan/ashen-blog/ba4d3e3a5b47c5ab671fe98fe733eadada45d258/admin/src/assets/font/toadOcfmlt9b38dHJxOBGMzFoXZ-Kj537nB_-9jJhlA.woff2
--------------------------------------------------------------------------------
/admin/src/assets/img/icon/demo.css:
--------------------------------------------------------------------------------
1 | *{margin: 0;padding: 0;list-style: none;}
2 | /*
3 | KISSY CSS Reset
4 | 理念:1. reset 的目的不是清除浏览器的默认样式,这仅是部分工作。清除和重置是紧密不可分的。
5 | 2. reset 的目的不是让默认样式在所有浏览器下一致,而是减少默认样式有可能带来的问题。
6 | 3. reset 期望提供一套普适通用的基础样式。但没有银弹,推荐根据具体需求,裁剪和修改后再使用。
7 | 特色:1. 适应中文;2. 基于最新主流浏览器。
8 | 维护:玉伯, 正淳
9 | */
10 |
11 | /** 清除内外边距 **/
12 | body, h1, h2, h3, h4, h5, h6, hr, p, blockquote, /* structural elements 结构元素 */
13 | dl, dt, dd, ul, ol, li, /* list elements 列表元素 */
14 | pre, /* text formatting elements 文本格式元素 */
15 | form, fieldset, legend, button, input, textarea, /* form elements 表单元素 */
16 | th, td /* table elements 表格元素 */ {
17 | margin: 0;
18 | padding: 0;
19 | }
20 |
21 | /** 设置默认字体 **/
22 | body,
23 | button, input, select, textarea /* for ie */ {
24 | font: 12px/1.5 tahoma, arial, \5b8b\4f53, sans-serif;
25 | }
26 | h1, h2, h3, h4, h5, h6 { font-size: 100%; }
27 | address, cite, dfn, em, var { font-style: normal; } /* 将斜体扶正 */
28 | code, kbd, pre, samp { font-family: courier new, courier, monospace; } /* 统一等宽字体 */
29 | small { font-size: 12px; } /* 小于 12px 的中文很难阅读,让 small 正常化 */
30 |
31 | /** 重置列表元素 **/
32 | ul, ol { list-style: none; }
33 |
34 | /** 重置文本格式元素 **/
35 | a { text-decoration: none; }
36 | a:hover { text-decoration: underline; }
37 |
38 |
39 | /** 重置表单元素 **/
40 | legend { color: #000; } /* for ie6 */
41 | fieldset, img { border: 0; } /* img 搭车:让链接里的 img 无边框 */
42 | button, input, select, textarea { font-size: 100%; } /* 使得表单元素在 ie 下能继承字体大小 */
43 | /* 注:optgroup 无法扶正 */
44 |
45 | /** 重置表格元素 **/
46 | table { border-collapse: collapse; border-spacing: 0; }
47 |
48 | /* 清除浮动 */
49 | .ks-clear:after, .clear:after {
50 | content: '\20';
51 | display: block;
52 | height: 0;
53 | clear: both;
54 | }
55 | .ks-clear, .clear {
56 | *zoom: 1;
57 | }
58 |
59 | .main {
60 | padding: 30px 100px;
61 | width: 960px;
62 | margin: 0 auto;
63 | }
64 | .main h1{font-size:36px; color:#333; text-align:left;margin-bottom:30px; border-bottom: 1px solid #eee;}
65 |
66 | .helps{margin-top:40px;}
67 | .helps pre{
68 | padding:20px;
69 | margin:10px 0;
70 | border:solid 1px #e7e1cd;
71 | background-color: #fffdef;
72 | overflow: auto;
73 | }
74 |
75 | .icon_lists{
76 | width: 100% !important;
77 |
78 | }
79 |
80 | .icon_lists li{
81 | float:left;
82 | width: 100px;
83 | height:180px;
84 | text-align: center;
85 | list-style: none !important;
86 | }
87 | .icon_lists .icon{
88 | font-size: 42px;
89 | line-height: 100px;
90 | margin: 10px 0;
91 | color:#333;
92 | -webkit-transition: font-size 0.25s ease-out 0s;
93 | -moz-transition: font-size 0.25s ease-out 0s;
94 | transition: font-size 0.25s ease-out 0s;
95 |
96 | }
97 | .icon_lists .icon:hover{
98 | font-size: 100px;
99 | }
100 |
101 |
102 |
103 | .markdown {
104 | color: #666;
105 | font-size: 14px;
106 | line-height: 1.8;
107 | }
108 |
109 | .highlight {
110 | line-height: 1.5;
111 | }
112 |
113 | .markdown img {
114 | vertical-align: middle;
115 | max-width: 100%;
116 | }
117 |
118 | .markdown h1 {
119 | color: #404040;
120 | font-weight: 500;
121 | line-height: 40px;
122 | margin-bottom: 24px;
123 | }
124 |
125 | .markdown h2,
126 | .markdown h3,
127 | .markdown h4,
128 | .markdown h5,
129 | .markdown h6 {
130 | color: #404040;
131 | margin: 1.6em 0 0.6em 0;
132 | font-weight: 500;
133 | clear: both;
134 | }
135 |
136 | .markdown h1 {
137 | font-size: 28px;
138 | }
139 |
140 | .markdown h2 {
141 | font-size: 22px;
142 | }
143 |
144 | .markdown h3 {
145 | font-size: 16px;
146 | }
147 |
148 | .markdown h4 {
149 | font-size: 14px;
150 | }
151 |
152 | .markdown h5 {
153 | font-size: 12px;
154 | }
155 |
156 | .markdown h6 {
157 | font-size: 12px;
158 | }
159 |
160 | .markdown hr {
161 | height: 1px;
162 | border: 0;
163 | background: #e9e9e9;
164 | margin: 16px 0;
165 | clear: both;
166 | }
167 |
168 | .markdown p,
169 | .markdown pre {
170 | margin: 1em 0;
171 | }
172 |
173 | .markdown > p,
174 | .markdown > blockquote,
175 | .markdown > .highlight,
176 | .markdown > ol,
177 | .markdown > ul {
178 | width: 80%;
179 | }
180 |
181 | .markdown ul > li {
182 | list-style: circle;
183 | }
184 |
185 | .markdown > ul li,
186 | .markdown blockquote ul > li {
187 | margin-left: 20px;
188 | padding-left: 4px;
189 | }
190 |
191 | .markdown > ul li p,
192 | .markdown > ol li p {
193 | margin: 0.6em 0;
194 | }
195 |
196 | .markdown ol > li {
197 | list-style: decimal;
198 | }
199 |
200 | .markdown > ol li,
201 | .markdown blockquote ol > li {
202 | margin-left: 20px;
203 | padding-left: 4px;
204 | }
205 |
206 | .markdown code {
207 | margin: 0 3px;
208 | padding: 0 5px;
209 | background: #eee;
210 | border-radius: 3px;
211 | }
212 |
213 | .markdown pre {
214 | border-radius: 6px;
215 | background: #f7f7f7;
216 | padding: 20px;
217 | }
218 |
219 | .markdown pre code {
220 | border: none;
221 | background: #f7f7f7;
222 | margin: 0;
223 | }
224 |
225 | .markdown strong,
226 | .markdown b {
227 | font-weight: 600;
228 | }
229 |
230 | .markdown > table {
231 | border-collapse: collapse;
232 | border-spacing: 0px;
233 | empty-cells: show;
234 | border: 1px solid #e9e9e9;
235 | width: 95%;
236 | margin-bottom: 24px;
237 | }
238 |
239 | .markdown > table th {
240 | white-space: nowrap;
241 | color: #333;
242 | font-weight: 600;
243 |
244 | }
245 |
246 | .markdown > table th,
247 | .markdown > table td {
248 | border: 1px solid #e9e9e9;
249 | padding: 8px 16px;
250 | text-align: left;
251 | }
252 |
253 | .markdown > table th {
254 | background: #F7F7F7;
255 | }
256 |
257 | .markdown blockquote {
258 | font-size: 90%;
259 | color: #999;
260 | border-left: 4px solid #e9e9e9;
261 | padding-left: 0.8em;
262 | margin: 1em 0;
263 | font-style: italic;
264 | }
265 |
266 | .markdown blockquote p {
267 | margin: 0;
268 | }
269 |
270 | .markdown .anchor {
271 | opacity: 0;
272 | transition: opacity 0.3s ease;
273 | margin-left: 8px;
274 | }
275 |
276 | .markdown .waiting {
277 | color: #ccc;
278 | }
279 |
280 | .markdown h1:hover .anchor,
281 | .markdown h2:hover .anchor,
282 | .markdown h3:hover .anchor,
283 | .markdown h4:hover .anchor,
284 | .markdown h5:hover .anchor,
285 | .markdown h6:hover .anchor {
286 | opacity: 1;
287 | display: inline-block;
288 | }
289 |
290 | .markdown > br,
291 | .markdown > p > br {
292 | clear: both;
293 | }
294 |
295 |
296 | .hljs {
297 | display: block;
298 | background: white;
299 | padding: 0.5em;
300 | color: #333333;
301 | overflow-x: auto;
302 | }
303 |
304 | .hljs-comment,
305 | .hljs-meta {
306 | color: #969896;
307 | }
308 |
309 | .hljs-string,
310 | .hljs-variable,
311 | .hljs-template-variable,
312 | .hljs-strong,
313 | .hljs-emphasis,
314 | .hljs-quote {
315 | color: #df5000;
316 | }
317 |
318 | .hljs-keyword,
319 | .hljs-selector-tag,
320 | .hljs-type {
321 | color: #a71d5d;
322 | }
323 |
324 | .hljs-literal,
325 | .hljs-symbol,
326 | .hljs-bullet,
327 | .hljs-attribute {
328 | color: #0086b3;
329 | }
330 |
331 | .hljs-section,
332 | .hljs-name {
333 | color: #63a35c;
334 | }
335 |
336 | .hljs-tag {
337 | color: #333333;
338 | }
339 |
340 | .hljs-title,
341 | .hljs-attr,
342 | .hljs-selector-id,
343 | .hljs-selector-class,
344 | .hljs-selector-attr,
345 | .hljs-selector-pseudo {
346 | color: #795da3;
347 | }
348 |
349 | .hljs-addition {
350 | color: #55a532;
351 | background-color: #eaffea;
352 | }
353 |
354 | .hljs-deletion {
355 | color: #bd2c00;
356 | background-color: #ffecec;
357 | }
358 |
359 | .hljs-link {
360 | text-decoration: underline;
361 | }
362 |
363 | pre{
364 | background: #fff;
365 | }
366 |
367 |
368 |
369 |
370 |
371 |
--------------------------------------------------------------------------------
/admin/src/assets/img/icon/demo_fontclass.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | IconFont
7 |
8 |
9 |
10 |
11 |
12 |
IconFont 图标
13 |
14 |
15 | -
16 |
17 |
会员标签
18 | .icon-huiyuanbiaoqian
19 |
20 |
21 | -
22 |
23 |
会员 男
24 | .icon-huiyuannan
25 |
26 |
27 | -
28 |
29 |
全部
30 | .icon-quanbu
31 |
32 |
33 | -
34 |
35 |
在线指导
36 | .icon-zaixianzhidao
37 |
38 |
39 | -
40 |
41 |
退出
42 | .icon-tuichu
43 |
44 |
45 |
46 |
47 |
font-class引用
48 |
49 |
50 |
font-class是unicode使用方式的一种变种,主要是解决unicode书写不直观,语意不明确的问题。
51 |
与unicode使用方式相比,具有如下特点:
52 |
53 | - 兼容性良好,支持ie8+,及所有现代浏览器。
54 | - 相比于unicode语意明确,书写更直观。可以很容易分辨这个icon是什么。
55 | - 因为使用class来定义图标,所以当要替换图标时,只需要修改class里面的unicode引用。
56 | - 不过因为本质上还是使用的字体,所以多色图标还是不支持的。
57 |
58 |
使用步骤如下:
59 |
第一步:引入项目下面生成的fontclass代码:
60 |
61 |
62 |
63 |
第二步:挑选相应图标并获取类名,应用于页面:
64 |
<i class="iconfont icon-xxx"></i>
65 |
66 | "iconfont"是你项目下的font-family。可以通过编辑项目查看,默认是"iconfont"。
67 |
68 |
69 |
70 |
71 |
--------------------------------------------------------------------------------
/admin/src/assets/img/icon/demo_symbol.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | IconFont
7 |
8 |
9 |
10 |
24 |
25 |
26 |
27 |
IconFont 图标
28 |
29 |
30 | -
31 |
34 |
会员标签
35 | #icon-huiyuanbiaoqian
36 |
37 |
38 | -
39 |
42 |
会员 男
43 | #icon-huiyuannan
44 |
45 |
46 | -
47 |
50 |
全部
51 | #icon-quanbu
52 |
53 |
54 | -
55 |
58 |
在线指导
59 | #icon-zaixianzhidao
60 |
61 |
62 | -
63 |
66 |
退出
67 | #icon-tuichu
68 |
69 |
70 |
71 |
72 |
73 |
symbol引用
74 |
75 |
76 |
这是一种全新的使用方式,应该说这才是未来的主流,也是平台目前推荐的用法。相关介绍可以参考这篇文章
77 | 这种用法其实是做了一个svg的集合,与另外两种相比具有如下特点:
78 |
79 | - 支持多色图标了,不再受单色限制。
80 | - 通过一些技巧,支持像字体那样,通过
font-size
,color
来调整样式。
81 | - 兼容性较差,支持 ie9+,及现代浏览器。
82 | - 浏览器渲染svg的性能一般,还不如png。
83 |
84 |
使用步骤如下:
85 |
第一步:引入项目下面生成的symbol代码:
86 |
87 |
第二步:加入通用css代码(引入一次就行):
88 |
<style type="text/css">
89 | .icon {
90 | width: 1em; height: 1em;
91 | vertical-align: -0.15em;
92 | fill: currentColor;
93 | overflow: hidden;
94 | }
95 | </style>
96 |
第三步:挑选相应图标并获取类名,应用于页面:
97 |
<svg class="icon" aria-hidden="true">
98 | <use xlink:href="#icon-xxx"></use>
99 | </svg>
100 |
101 |
102 |
103 |
104 |
--------------------------------------------------------------------------------
/admin/src/assets/img/icon/demo_unicode.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | IconFont
7 |
8 |
9 |
29 |
30 |
31 |
32 |
IconFont 图标
33 |
34 |
35 | -
36 |
37 |
会员标签
38 | 
39 |
40 |
41 | -
42 |
43 |
会员 男
44 | 
45 |
46 |
47 | -
48 |
49 |
全部
50 | 
51 |
52 |
53 | -
54 |
55 |
在线指导
56 | 
57 |
58 |
59 | -
60 |
61 |
退出
62 | 
63 |
64 |
65 |
66 |
unicode引用
67 |
68 |
69 |
unicode是字体在网页端最原始的应用方式,特点是:
70 |
71 | - 兼容性最好,支持ie6+,及所有现代浏览器。
72 | - 支持按字体的方式去动态调整图标大小,颜色等等。
73 | - 但是因为是字体,所以不支持多色。只能使用平台里单色的图标,就算项目里有多色图标也会自动去色。
74 |
75 |
76 | 注意:新版iconfont支持多色图标,这些多色图标在unicode模式下将不能使用,如果有需求建议使用symbol的引用方式
77 |
78 |
unicode使用步骤如下:
79 |
第一步:拷贝项目下面生成的font-face
80 |
@font-face {
81 | font-family: 'iconfont';
82 | src: url('iconfont.eot');
83 | src: url('iconfont.eot?#iefix') format('embedded-opentype'),
84 | url('iconfont.woff') format('woff'),
85 | url('iconfont.ttf') format('truetype'),
86 | url('iconfont.svg#iconfont') format('svg');
87 | }
88 |
89 |
第二步:定义使用iconfont的样式
90 |
.iconfont{
91 | font-family:"iconfont" !important;
92 | font-size:16px;font-style:normal;
93 | -webkit-font-smoothing: antialiased;
94 | -webkit-text-stroke-width: 0.2px;
95 | -moz-osx-font-smoothing: grayscale;
96 | }
97 |
98 |
第三步:挑选相应图标并获取字体编码,应用于页面
99 |
<i class="iconfont">3</i>
100 |
101 |
102 | "iconfont"是你项目下的font-family。可以通过编辑项目查看,默认是"iconfont"。
103 |
104 |
105 |
106 |
107 |
108 |
109 |
--------------------------------------------------------------------------------
/admin/src/assets/img/icon/iconfont.css:
--------------------------------------------------------------------------------
1 |
2 | @font-face {font-family: "iconfont";
3 | src: url('iconfont.eot?t=1510823770568'); /* IE9*/
4 | src: url('iconfont.eot?t=1510823770568#iefix') format('embedded-opentype'), /* IE6-IE8 */
5 | url('data:application/x-font-woff;charset=utf-8;base64,d09GRgABAAAAAAfEAAsAAAAAC3QAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABHU1VCAAABCAAAADMAAABCsP6z7U9TLzIAAAE8AAAARAAAAFZW7kmBY21hcAAAAYAAAACDAAAB3nnTpZdnbHlmAAACBAAAA5UAAAS4rS7i0WhlYWQAAAWcAAAALwAAADYPhjSxaGhlYQAABcwAAAAcAAAAJAfeA4hobXR4AAAF6AAAABMAAAAcG+kAAGxvY2EAAAX8AAAAEAAAABAD1gTobWF4cAAABgwAAAAfAAAAIAEWAF1uYW1lAAAGLAAAAUUAAAJtPlT+fXBvc3QAAAd0AAAATwAAAGz2r6LHeJxjYGRgYOBikGPQYWB0cfMJYeBgYGGAAJAMY05meiJQDMoDyrGAaQ4gZoOIAgCKIwNPAHicY2Bk/ss4gYGVgYOpk+kMAwNDP4RmfM1gxMjBwMDEwMrMgBUEpLmmMDgwVDyfxNzwv4EhhrmBoQEozAiSAwAv5Q0EeJzFkVEKhDAMRCfaLYuIJ/E4+7Hn8EvwRxB6AC85x9BJI8juBZzyCjPQpCQAXgBaMYoEWIHBtSq1mrfoap7wkR/wRoOMmRMXbizcj0P5v79lenMf941qJnXOMhmPyZ5r/au+3t/L+UTmC32RU6CpgUvgG+MW+NZYAt8m9wD5BBlkJXQAeJxlU02IHFUQfvV6Xv/tTPe+/pnu+f/pnWndmFFnZ3rA7M72wRVmXTS4EJggMjl4MKDXHJSwF0EkghgIHoeNRMw1kINklyYiuXjIKZCAsqJHhdw89dPqmV1BHJqurnpf1dRXXz3CCPn7V+mB5BObvEBeJa+Ti4SAfA7aBq1BKxz06DlwW8z1HEMKg7ClBO2etAVeW3aK/WjQ9WRFNsGAOmy0+lHYoyEMB2N6AfrFGkCpUt63OlVL+hJ0P6x/KnbpIbiNoGqOz4vJS9tOv2mr1/KWVbKsG6rMmEppzjTgQ6+oMU2XxTfMLLsPGi/SBuRLYXlvWmhWrCufDT6qdTwN4OAA7ErT+Hablzk+n5SLtlVSVguqXy4Eaw5c+33Ft/O17m8Efznk+of0s+QQndRJn1xAptEghG5bgQbYUHQUJBJ0ehBK3WgEUd/rDvC1OJDb3cEog7MFbaRP/9Q08Vwv6eJenF6F1WpV3GFcFyfaCt2lORWm1b2vNbukw2qtJm7pvqPDB7qeXtRLjlOy4Qs9O8Ia99OY3hPP63tYIMcmUl6FQLEUuFSbZEnLk1uLSlcx5y/8cpzKkk8iJVJMTNIgm+RN5NMyQHHr4LXGMBr2wHPkoNXubgG6LQyjTB5+9qMtGHQDBIRBltCWa+AUR3iygQAXQ9kMmmmyM6UwXKfJ+hDotMLNNDZ5FogxkM7p9dnsOjU5N2ESJdEE8PNtWKsklTWE72TJBG0GxqR4Z/pLhj11IcFcrIABkzcxGWs0F84c07HKHPMxe8nzJ+kh8pSIRVrkFeRZgPPAUBcmhx0UphP1gXWiOsgKQ/2QifdflxJxBy7DZXEf5pyLmWGKGecwZ6vifbgJNwu+pubFvtjPa0z19f+HaHp09256srK28pixx2hOqHjv0aOcWvLZkyeyV8hrP/7rMr+QV0/1mWHfdbJOXiNjQjqZHIth420pZuLgoPmpONwJOaqCAJdHI/tMigAhQyTkbgylS6qRJia3DBpno0yT5SwpQRtzUxCTG9Y8WQ7wzHyu41gNi9f5mZZo08To0eaiyDMGkCAwXuLRbMcx3he66P977L9NXibEjrBjxQB3uUPY1XLQ6C4XZgydftHlDl6VIR9EP+Ce66PN3NPbh89yEG9qoOD27uwCE0fHIsfegI/x7+uc06Nst6Mbb91+mkP0O19Fuq/roMTfXTlOGUuP3z18iNwOFouGb2ztH3BN4uwAAAB4nGNgZGBgAGLVlw5n4vltvjJwszCAwDXjiigE/f8ACwOzA5DLwcAEEgUAGwkJkQB4nGNgZGBgbvjfwBDDwgACQJKRARWwAwBHDQJweJxjYWBgYH7JwMDCgIkBFrMBBQAAAAAAAHYA3gFSAa4CDgJceJxjYGRgYGBnCGRgZQABJiDmAkIGhv9gPgMAEWMBdAB4nGWPTU7DMBCFX/oHpBKqqGCH5AViASj9EatuWFRq911036ZOmyqJI8et1ANwHo7ACTgC3IA78EgnmzaWx9+8eWNPANzgBx6O3y33kT1cMjtyDRe4F65TfxBukF+Em2jjVbhF/U3YxzOmwm10YXmD17hi9oR3YQ8dfAjXcI1P4Tr1L+EG+Vu4iTv8CrfQ8erCPuZeV7iNRy/2x1YvnF6p5UHFockikzm/gple75KFrdLqnGtbxCZTg6BfSVOdaVvdU+zXQ+ciFVmTqgmrOkmMyq3Z6tAFG+fyUa8XiR6EJuVYY/62xgKOcQWFJQ6MMUIYZIjK6Og7VWb0r7FDwl57Vj3N53RbFNT/c4UBAvTPXFO6stJ5Ok+BPV8bUnV0K27LnpQ0kV7NSRKyQl7WtlRC6gE2ZVeOEXpc0Yk/KGdI/wAJWm7IAAAAeJxticEKgCAQBfdZaQjRN251cC8rQgvm11fU0bnNDDn6iNQnwGHAiAkeATOhrsnkMtZNOBdhjb8rqy9vt6Wx1Oe0JAdnf5rsyYhuqRsWAQA=') format('woff'),
6 | url('iconfont.ttf?t=1510823770568') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/
7 | url('iconfont.svg?t=1510823770568#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-huiyuanbiaoqian:before { content: "\e772"; }
19 |
20 | .icon-huiyuannan:before { content: "\e77b"; }
21 |
22 | .icon-quanbu:before { content: "\e783"; }
23 |
24 | .icon-zaixianzhidao:before { content: "\e786"; }
25 |
26 | .icon-tuichu:before { content: "\e792"; }
27 |
28 |
--------------------------------------------------------------------------------
/admin/src/assets/img/icon/iconfont.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StudentWan/ashen-blog/ba4d3e3a5b47c5ab671fe98fe733eadada45d258/admin/src/assets/img/icon/iconfont.eot
--------------------------------------------------------------------------------
/admin/src/assets/img/icon/iconfont.js:
--------------------------------------------------------------------------------
1 | (function(window){var svgSprite='';var script=function(){var scripts=document.getElementsByTagName("script");return scripts[scripts.length-1]}();var shouldInjectCss=script.getAttribute("data-injectcss");var ready=function(fn){if(document.addEventListener){if(~["complete","loaded","interactive"].indexOf(document.readyState)){setTimeout(fn,0)}else{var loadFn=function(){document.removeEventListener("DOMContentLoaded",loadFn,false);fn()};document.addEventListener("DOMContentLoaded",loadFn,false)}}else if(document.attachEvent){IEContentLoaded(window,fn)}function IEContentLoaded(w,fn){var d=w.document,done=false,init=function(){if(!done){done=true;fn()}};var polling=function(){try{d.documentElement.doScroll("left")}catch(e){setTimeout(polling,50);return}init()};polling();d.onreadystatechange=function(){if(d.readyState=="complete"){d.onreadystatechange=null;init()}}}};var before=function(el,target){target.parentNode.insertBefore(el,target)};var prepend=function(el,target){if(target.firstChild){before(el,target.firstChild)}else{target.appendChild(el)}};function appendSvg(){var div,svg;div=document.createElement("div");div.innerHTML=svgSprite;svgSprite=null;svg=div.getElementsByTagName("svg")[0];if(svg){svg.setAttribute("aria-hidden","true");svg.style.position="absolute";svg.style.width=0;svg.style.height=0;svg.style.overflow="hidden";prepend(svg,document.body)}}if(shouldInjectCss&&!window.__iconfont__svg__cssinject__){window.__iconfont__svg__cssinject__=true;try{document.write("")}catch(e){console&&console.log(e)}}ready(appendSvg)})(window)
--------------------------------------------------------------------------------
/admin/src/assets/img/icon/iconfont.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
49 |
--------------------------------------------------------------------------------
/admin/src/assets/img/icon/iconfont.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StudentWan/ashen-blog/ba4d3e3a5b47c5ab671fe98fe733eadada45d258/admin/src/assets/img/icon/iconfont.ttf
--------------------------------------------------------------------------------
/admin/src/assets/img/icon/iconfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StudentWan/ashen-blog/ba4d3e3a5b47c5ab671fe98fe733eadada45d258/admin/src/assets/img/icon/iconfont.woff
--------------------------------------------------------------------------------
/admin/src/assets/img/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StudentWan/ashen-blog/ba4d3e3a5b47c5ab671fe98fe733eadada45d258/admin/src/assets/img/logo.png
--------------------------------------------------------------------------------
/admin/src/assets/img/star-half.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StudentWan/ashen-blog/ba4d3e3a5b47c5ab671fe98fe733eadada45d258/admin/src/assets/img/star-half.png
--------------------------------------------------------------------------------
/admin/src/assets/img/star-off.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StudentWan/ashen-blog/ba4d3e3a5b47c5ab671fe98fe733eadada45d258/admin/src/assets/img/star-off.png
--------------------------------------------------------------------------------
/admin/src/assets/img/star-on.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StudentWan/ashen-blog/ba4d3e3a5b47c5ab671fe98fe733eadada45d258/admin/src/assets/img/star-on.png
--------------------------------------------------------------------------------
/admin/src/assets/img/subhead.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StudentWan/ashen-blog/ba4d3e3a5b47c5ab671fe98fe733eadada45d258/admin/src/assets/img/subhead.png
--------------------------------------------------------------------------------
/admin/src/assets/style/_font.scss:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: 'KaiShu';
3 | src: url(../font/kaishu.ttf) format('truetype');
4 | font-weight: normal;
5 | font-style: normal;
6 | }
7 |
8 | /* latin-ext */
9 | @font-face {
10 | font-family: 'Source Sans Pro';
11 | font-style: normal;
12 | font-weight: 400;
13 | src: local('Source Sans Pro'), local('SourceSansPro-Regular'), url(../font/ODelI1aHBYDBqgeIAH2zlIa1YDtoarzwSXxTHggEXMw.woff2) format('woff2');
14 | unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF;
15 | }
16 |
17 | /* latin */
18 | @font-face {
19 | font-family: 'Source Sans Pro';
20 | font-style: normal;
21 | font-weight: 400;
22 | src: local('Source Sans Pro'), local('SourceSansPro-Regular'), url(../font/ODelI1aHBYDBqgeIAH2zlJbPFduIYtoLzwST68uhz_Y.woff2) format('woff2');
23 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
24 | }
25 |
26 | /* latin-ext */
27 | @font-face {
28 | font-family: 'Source Sans Pro';
29 | font-style: normal;
30 | font-weight: 600;
31 | src: local('Source Sans Pro Semibold'), local('SourceSansPro-Semibold'), url(../font/toadOcfmlt9b38dHJxOBGKyGJhAh-RE0BxGcd_izyev3rGVtsTkPsbDajuO5ueQw.woff2) format('woff2');
32 | unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF;
33 | }
34 |
35 | /* latin */
36 | @font-face {
37 | font-family: 'Source Sans Pro';
38 | font-style: normal;
39 | font-weight: 600;
40 | src: local('Source Sans Pro Semibold'), local('SourceSansPro-Semibold'), url(../font/toadOcfmlt9b38dHJxOBGMzFoXZ-Kj537nB_-9jJhlA.woff2) format('woff2');
41 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
42 | }
43 |
44 | /* latin */
45 | @font-face {
46 | font-family: 'Dosis';
47 | font-style: normal;
48 | font-weight: 500;
49 | src: local('Dosis Medium'), local('Dosis-Medium'), url(../font/mAcLJWdPWDNiDJwJvcWKc3YhjbSpvc47ee6xR_80Hnw.woff2) format('woff2');
50 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
51 | }
52 |
--------------------------------------------------------------------------------
/admin/src/assets/style/_variable.scss:
--------------------------------------------------------------------------------
1 | // 基础配色
2 | $white: #fefdff;
3 | $black: #000;
4 |
5 | // 系统配色
6 | $title: #2c3e50;
7 | $word: #34495e;
8 | $base: #f18f01;
9 | $quote: #99c24d;
10 | $special: #c1bfb5;
11 | $background: #ebeff5;
12 |
13 | @mixin flex($flow: row wrap, $justify: center, $align: center) {
14 | display: flex;
15 | flex-flow: $flow;
16 | justify-content: $justify;
17 | align-items: $align;
18 | }
19 |
--------------------------------------------------------------------------------
/admin/src/assets/style/index.scss:
--------------------------------------------------------------------------------
1 | @import "font";
2 |
3 | .icon {
4 | width: 1em; height: 1em;
5 | vertical-align: -0.15em;
6 | fill: currentColor;
7 | overflow: hidden;
8 | }
9 |
10 | html {
11 | font: {
12 | size: 62.5%;
13 | family: "Source Sans Pro", "Helvetica Neue", Arial, sans-serif;
14 | weight: 400;
15 | }
16 | color: $word;
17 | height: 100%;
18 | }
19 |
20 | body {
21 | margin: 0;
22 | height: 100%;
23 | min-width: 1024px;
24 | min-height: 900px;
25 | }
26 |
27 | body * {
28 | box-sizing: border-box;
29 | }
30 |
31 | h3, h4, h5, h6 {
32 | font-size: 1.6rem;
33 | }
34 |
35 | a {
36 | text-decoration: none;
37 | }
38 |
39 | #app {
40 | height: 100%;
41 | }
42 |
43 | .main {
44 | margin-left: 48px;
45 | background: $background;
46 | padding: .5em 6em;
47 | height: calc(100% - 48px);
48 | h2 {
49 | font-weight: 400;
50 | span {
51 | color: $special;
52 | }
53 | }
54 | }
55 |
56 | .logo {
57 | width: 30px;
58 | height: 30px;
59 | }
60 |
61 | .editor-toolbar {
62 | @include flex($justify: flex-start);
63 | height: 40px;
64 | padding-left: 5px;
65 | font-size: 1.6rem;
66 | color: #adb5bc;
67 | background: $white;
68 | border-top-left-radius: 3px;
69 | border-top-right-radius: 3px;
70 | border: 1px solid $special;
71 | a {
72 | margin-right: 10px;
73 | padding: .2em;
74 | border-radius: 2px;
75 | border: 1px solid $white;
76 | &:hover {
77 | border: 1px solid #adb5bc;
78 | }
79 | }
80 | .separator {
81 | font-style: normal;
82 | margin-right: 10px;
83 | }
84 | .fa-question-circle {
85 | color: #adb5bc;
86 | }
87 | }
88 |
89 | .btn-container {
90 | button {
91 | border-radius: 2px;
92 | outline: none;
93 | cursor: pointer;
94 | margin-right: 10px;
95 | }
96 | button:last-of-type {
97 | margin-right: 0;
98 | }
99 | .delete {
100 | margin-right: 10px;
101 | color: $word;
102 | border: 1px solid $word;
103 | &:hover {
104 | color: $base;
105 | border: 1px solid $base;
106 | }
107 | }
108 | .not-del {
109 | color: $white;
110 | background: $base;
111 | border: 1px solid $base;
112 | &:hover {
113 | background: $white;
114 | color: $base;
115 | border: 1px solid $base;
116 | }
117 | }
118 | }
119 |
120 | .tag, .tag-edit {
121 | sup {
122 | display: inline-block;
123 | visibility: hidden;
124 | cursor: pointer;
125 | color: $white;
126 | line-height: 10px;
127 | text-align: center;
128 | width: 10px;
129 | height: 10px;
130 | border-radius: 50%;
131 | background: $special;
132 | }
133 | &:hover > sup {
134 | visibility: visible;
135 | }
136 | }
137 |
138 | .tag-input {
139 | margin-bottom: 5px;
140 | width: 80px;
141 | background: none;
142 | border: none;
143 | border-bottom: 1px solid $special;
144 | outline: none;
145 | color: $word;
146 | }
147 |
--------------------------------------------------------------------------------
/admin/src/components/About.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
关于我 /
4 | ABOUT ME
5 |
6 |
7 |
8 |
9 |
12 |
13 |
14 |
15 |
16 |
61 |
62 |
74 |
--------------------------------------------------------------------------------
/admin/src/components/List.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
文章列表 /
4 | ARTICLE LIST
5 |
6 |
7 |
8 |
14 |
15 |
16 |
17 |
18 |
19 |
58 |
59 |
85 |
--------------------------------------------------------------------------------
/admin/src/components/Login.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 无火的余灰
5 |
6 |
7 |
15 |
16 |
17 |
18 |
19 |
55 |
56 |
57 |
119 |
120 |
--------------------------------------------------------------------------------
/admin/src/components/ReadingList.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
15 |
16 |
17 |
阅读列表 /
18 | READING LIST
19 |
20 |
21 |
22 |
25 |
26 |
27 | 书名 |
28 | 作者 |
29 | 评分 |
30 | 编辑/删除 |
31 |
32 |
33 | {{ name }} |
34 | {{ author }} |
35 |
36 |
37 | |
38 |
39 |
43 | |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
191 |
192 |
281 |
--------------------------------------------------------------------------------
/admin/src/components/Tag.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
标签筛选 /
4 | CHOOSE BY TAG
5 |
6 |
7 |
8 |
26 |
27 |
28 |
29 |
30 |
31 |
98 |
99 |
147 |
--------------------------------------------------------------------------------
/admin/src/components/common/ArticleList.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | -
4 |
5 |
{{ createTime }}
6 |
7 |
8 |
9 |
10 |
234 |
235 |
270 |
--------------------------------------------------------------------------------
/admin/src/components/common/Editor.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
22 |
标签查询页面只能批量更改标签,修改的文章内容会自动保存。
23 |
24 |
25 |
26 |
27 |
28 |
29 |
144 |
145 |
203 |
--------------------------------------------------------------------------------
/admin/src/components/common/HeaderNav.vue:
--------------------------------------------------------------------------------
1 |
2 |
20 |
21 |
22 |
37 |
38 |
74 |
--------------------------------------------------------------------------------
/admin/src/components/common/SideNav.vue:
--------------------------------------------------------------------------------
1 |
2 |
28 |
29 |
30 |
37 |
38 |
85 |
--------------------------------------------------------------------------------
/admin/src/components/common/Star.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
48 |
49 |
80 |
--------------------------------------------------------------------------------
/admin/src/main.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author {benyuwan@gmail.com}
3 | * @file client端入口文件
4 | */
5 | // The Vue build version to load with the `import` command
6 | // (runtime-only or standalone) has been set in webpack.base.conf with an alias.
7 | import '@/assets/style/index.scss'
8 | import '@/assets/img/icon/iconfont'
9 | import Vue from 'vue'
10 | import App from '@/App'
11 | import router from './router'
12 | import store from './store'
13 |
14 | Vue.config.productionTip = false
15 |
16 | axios.defaults.baseURL = 'http://localhost:3000'
17 | axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded'
18 |
19 | /* eslint-disable no-new */
20 | new Vue({
21 | el: '#app',
22 | router,
23 | store,
24 | template: '',
25 | components: {
26 | App
27 | }
28 | })
29 |
--------------------------------------------------------------------------------
/admin/src/router/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author {benyuwan@gmail.com}
3 | * @file admin端路由文件
4 | */
5 | import Vue from 'vue'
6 | import Router from 'vue-router'
7 | import Login from '@/components/Login'
8 | import List from '@/components/List'
9 | import Tag from '@/components/Tag'
10 | import ReadingList from '@/components/ReadingList'
11 | import About from '@/components/About'
12 |
13 | Vue.use(Router)
14 |
15 | const router = new Router({
16 | routes: [{
17 | path: '/login',
18 | component: Login
19 | },
20 | {
21 | path: '/lists',
22 | component: List
23 | },
24 | {
25 | path: '/tags',
26 | component: Tag
27 | },
28 | {
29 | path: '/readinglists',
30 | component: ReadingList
31 | },
32 | {
33 | path: '/about',
34 | component: About
35 | },
36 | {
37 | path: '*',
38 | redirect: '/login'
39 | }
40 | ]
41 | })
42 |
43 | router.beforeEach((to, from, next) => {
44 | // redirect会重新进行路由守卫,next()不会
45 | if (localStorage.ashenToken) {
46 | axios.get(
47 | '/api/v1/tokens/check', {
48 | headers: {
49 | Authorization: `Bearer ${localStorage.ashenToken}`
50 | }
51 | })
52 | .then(res => {
53 | // token验证通过
54 | const pathArr = ['/lists', '/tags', '/readinglists', '/about']
55 | if (pathArr.indexOf(to.path) === -1) {
56 | next('lists')
57 | }
58 | else {
59 | next()
60 | }
61 | })
62 | .catch(err => {
63 | // token验证不通过
64 | if (to.path !== '/login') {
65 | next('login')
66 | }
67 | else {
68 | next()
69 | }
70 | })
71 | }
72 | else {
73 | if (to.path !== '/login') {
74 | next('login')
75 | }
76 | else {
77 | next()
78 | }
79 | }
80 | })
81 |
82 | export default router
83 |
--------------------------------------------------------------------------------
/admin/src/store/actions.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author {benyuwan@gmail.com}
3 | * @file vuex actions
4 | */
5 |
6 | export async function saveArticle({commit, state}, {id, title, tags, content, isPublished}) {
7 | try {
8 | await axios.put(
9 | `/api/v1/articles/update/${id}`,
10 | {
11 | title,
12 | tags,
13 | content,
14 | isPublished
15 | },
16 | {
17 | headers: {
18 | Authorization: `Bearer ${localStorage.ashenToken}`
19 | }
20 | })
21 | commit('updateArticle', {id, title, tags, content, isPublished})
22 | }
23 | catch (err) {
24 | console.error(err.response.data.error)
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/admin/src/store/getters.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author {benyuwan@gmail.com}
3 | * @file vuex getters
4 | */
5 |
6 | export function getTags({tags}) {
7 | if (tags.length !== 0) {
8 | return tags.split(',')
9 | }
10 | return []
11 | }
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/admin/src/store/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author {benyuwan@gmail.com}
3 | * @file 状态管理的入口文件
4 | */
5 |
6 | import Vue from 'vue'
7 | import Vuex from 'vuex'
8 | import state from './state'
9 | import * as getters from './getters'
10 | import * as actions from './actions'
11 | import * as mutations from './mutations'
12 |
13 | Vue.use(Vuex)
14 |
15 | export default new Vuex.Store({
16 | state,
17 | getters,
18 | mutations,
19 | actions
20 | })
21 |
--------------------------------------------------------------------------------
/admin/src/store/mutations.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author {benyuwan@gmail.com}
3 | * @file vuex mutations
4 | */
5 |
6 | export function updateArticle(state, {id, title, tags, content, isPublished}) {
7 | state.id = id
8 | state.title = title
9 | state.tags = tags
10 | state.content = content
11 | state.isPublished = isPublished
12 | }
13 |
14 | export function deleteArticle(state) {
15 | state.toggleDelete = !state.toggleDelete
16 | }
17 |
18 | export function updatePublishState(state) {
19 | state.isPublished = 1
20 | }
21 |
--------------------------------------------------------------------------------
/admin/src/store/state.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author {benyuwan@gmail.com}
3 | * @file vuex states
4 | */
5 |
6 | export default {
7 | // 正在编辑的文章
8 | id: '',
9 | title: '',
10 | tags: '',
11 | content: '',
12 | isPublished: '',
13 | toggleDelete: false
14 | }
15 |
--------------------------------------------------------------------------------
/client/build/build.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | require('./check-versions')()
3 |
4 | process.env.NODE_ENV = 'production'
5 |
6 | const ora = require('ora')
7 | const rm = require('rimraf')
8 | const path = require('path')
9 | const chalk = require('chalk')
10 | const webpack = require('webpack')
11 | const config = require('../config/index')
12 | const webpackConfig = require('./webpack.prod.conf')
13 |
14 | const spinner = ora('building for production...')
15 | spinner.start()
16 |
17 | rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
18 | if (err) throw err
19 | webpack(webpackConfig, function (err, stats) {
20 | spinner.stop()
21 | if (err) throw err
22 | process.stdout.write(stats.toString({
23 | colors: true,
24 | modules: false,
25 | children: false,
26 | chunks: false,
27 | chunkModules: false
28 | }) + '\n\n')
29 |
30 | if (stats.hasErrors()) {
31 | console.log(chalk.red(' Build failed with errors.\n'))
32 | process.exit(1)
33 | }
34 |
35 | console.log(chalk.cyan(' Build complete.\n'))
36 | console.log(chalk.yellow(
37 | ' Tip: built files are meant to be served over an HTTP server.\n' +
38 | ' Opening index.html over file:// won\'t work.\n'
39 | ))
40 | })
41 | })
42 |
--------------------------------------------------------------------------------
/client/build/check-versions.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const chalk = require('chalk')
3 | const semver = require('semver')
4 | const packageConfig = require('../../package.json')
5 | const shell = require('shelljs')
6 | function exec (cmd) {
7 | return require('child_process').execSync(cmd).toString().trim()
8 | }
9 |
10 | const versionRequirements = [
11 | {
12 | name: 'node',
13 | currentVersion: semver.clean(process.version),
14 | versionRequirement: packageConfig.engines.node
15 | }
16 | ]
17 |
18 | if (shell.which('npm')) {
19 | versionRequirements.push({
20 | name: 'npm',
21 | currentVersion: exec('npm --version'),
22 | versionRequirement: packageConfig.engines.npm
23 | })
24 | }
25 |
26 | module.exports = function () {
27 | const warnings = []
28 | for (let i = 0; i < versionRequirements.length; i++) {
29 | const mod = versionRequirements[i]
30 | if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
31 | warnings.push(mod.name + ': ' +
32 | chalk.red(mod.currentVersion) + ' should be ' +
33 | chalk.green(mod.versionRequirement)
34 | )
35 | }
36 | }
37 |
38 | if (warnings.length) {
39 | console.log('')
40 | console.log(chalk.yellow('To use this template, you must update following to modules:'))
41 | console.log()
42 | for (let i = 0; i < warnings.length; i++) {
43 | const warning = warnings[i]
44 | console.log(' ' + warning)
45 | }
46 | console.log()
47 | process.exit(1)
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/client/build/dev-client.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | 'use strict'
3 | require('eventsource-polyfill')
4 | const hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true')
5 |
6 | hotClient.subscribe(function (event) {
7 | if (event.action === 'reload') {
8 | window.location.reload()
9 | }
10 | })
11 |
--------------------------------------------------------------------------------
/client/build/dev-server.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | require('./check-versions')()
3 |
4 | const config = require('../config/index')
5 | if (!process.env.NODE_ENV) {
6 | process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV)
7 | }
8 |
9 | const opn = require('opn')
10 | const path = require('path')
11 | const express = require('express')
12 | const webpack = require('webpack')
13 | const proxyMiddleware = require('http-proxy-middleware')
14 | const webpackConfig = require('./webpack.dev.conf')
15 |
16 | // default port where dev server listens for incoming traffic
17 | const port = process.env.PORT || config.dev.port
18 | // automatically open browser, if not set will be false
19 | const autoOpenBrowser = !!config.dev.autoOpenBrowser
20 | // Define HTTP proxies to your custom API backend
21 | // https://github.com/chimurai/http-proxy-middleware
22 | const proxyTable = config.dev.proxyTable
23 |
24 | const app = express()
25 | const compiler = webpack(webpackConfig)
26 |
27 | const devMiddleware = require('webpack-dev-middleware')(compiler, {
28 | publicPath: webpackConfig.output.publicPath,
29 | quiet: true
30 | })
31 |
32 | const hotMiddleware = require('webpack-hot-middleware')(compiler, {
33 | log: false,
34 | heartbeat: 2000
35 | })
36 | // force page reload when html-webpack-plugin template changes
37 | // currently disabled until this is resolved:
38 | // https://github.com/jantimon/html-webpack-plugin/issues/680
39 | // compiler.plugin('compilation', function (compilation) {
40 | // compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) {
41 | // hotMiddleware.publish({ action: 'reload' })
42 | // cb()
43 | // })
44 | // })
45 |
46 | // enable hot-reload and state-preserving
47 | // compilation error display
48 | app.use(hotMiddleware)
49 |
50 | // proxy api requests
51 | Object.keys(proxyTable).forEach(function (context) {
52 | const options = proxyTable[context]
53 | if (typeof options === 'string') {
54 | options = { target: options }
55 | }
56 | app.use(proxyMiddleware(options.filter || context, options))
57 | })
58 |
59 | // handle fallback for HTML5 history API
60 | app.use(require('connect-history-api-fallback')())
61 |
62 | // serve webpack bundle output
63 | app.use(devMiddleware)
64 |
65 | // serve pure static assets
66 | const staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory)
67 | app.use(staticPath, express.static('./static'))
68 |
69 | const uri = 'http://localhost:' + port
70 |
71 | var _resolve
72 | var _reject
73 | var readyPromise = new Promise((resolve, reject) => {
74 | _resolve = resolve
75 | _reject = reject
76 | })
77 |
78 | var server
79 | var portfinder = require('portfinder')
80 | portfinder.basePort = port
81 |
82 | console.log('> Starting dev server...')
83 | devMiddleware.waitUntilValid(() => {
84 | portfinder.getPort((err, port) => {
85 | if (err) {
86 | _reject(err)
87 | }
88 | process.env.PORT = port
89 | var uri = 'http://localhost:' + port
90 | console.log('> Listening at ' + uri + '\n')
91 | // when env is testing, don't need open it
92 | if (autoOpenBrowser && process.env.NODE_ENV !== 'testing') {
93 | opn(uri)
94 | }
95 | server = app.listen(port)
96 | _resolve()
97 | })
98 | })
99 |
100 | module.exports = {
101 | ready: readyPromise,
102 | close: () => {
103 | server.close()
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/client/build/utils.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const path = require('path')
3 | const config = require('../config/index')
4 | const ExtractTextPlugin = require('extract-text-webpack-plugin')
5 |
6 | exports.assetsPath = function (_path) {
7 | const assetsSubDirectory = process.env.NODE_ENV === 'production'
8 | ? config.build.assetsSubDirectory
9 | : config.dev.assetsSubDirectory
10 | return path.posix.join(assetsSubDirectory, _path)
11 | }
12 |
13 | exports.cssLoaders = function (options) {
14 | options = options || {}
15 |
16 | const cssLoader = {
17 | loader: 'css-loader',
18 | options: {
19 | minimize: process.env.NODE_ENV === 'production',
20 | sourceMap: options.sourceMap
21 | }
22 | }
23 |
24 | // generate loader string to be used with extract text plugin
25 | function generateLoaders (loader, loaderOptions) {
26 | const loaders = [cssLoader]
27 | if (loader) {
28 | loaders.push({
29 | loader: loader + '-loader',
30 | options: Object.assign({}, loaderOptions, {
31 | sourceMap: options.sourceMap
32 | })
33 | })
34 | }
35 |
36 | // Extract CSS when that option is specified
37 | // (which is the case during production build)
38 | if (options.extract) {
39 | return ExtractTextPlugin.extract({
40 | use: loaders,
41 | fallback: 'vue-style-loader'
42 | })
43 | } else {
44 | return ['vue-style-loader'].concat(loaders)
45 | }
46 | }
47 |
48 | // https://vue-loader.vuejs.org/en/configurations/extract-css.html
49 | return {
50 | css: generateLoaders(),
51 | postcss: generateLoaders(),
52 | less: generateLoaders('less'),
53 | sass: generateLoaders('sass', { indentedSyntax: true }),
54 | scss: generateLoaders('sass').concat({
55 | loader: 'sass-resources-loader',
56 | options: {
57 | resources: path.resolve(__dirname, '../src/assets/style/_variable.scss')
58 | }
59 | }),
60 | stylus: generateLoaders('stylus'),
61 | styl: generateLoaders('stylus')
62 | }
63 | }
64 |
65 | // Generate loaders for standalone style files (outside of .vue)
66 | exports.styleLoaders = function (options) {
67 | const output = []
68 | const loaders = exports.cssLoaders(options)
69 | for (const extension in loaders) {
70 | const loader = loaders[extension]
71 | output.push({
72 | test: new RegExp('\\.' + extension + '$'),
73 | use: loader
74 | })
75 | }
76 | return output
77 | }
78 |
--------------------------------------------------------------------------------
/client/build/vue-loader.conf.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const utils = require('./utils')
3 | const config = require('../config/index')
4 | const isProduction = process.env.NODE_ENV === 'production'
5 |
6 | module.exports = {
7 | loaders: utils.cssLoaders({
8 | sourceMap: isProduction
9 | ? config.build.productionSourceMap
10 | : config.dev.cssSourceMap,
11 | extract: isProduction
12 | }),
13 | transformToRequire: {
14 | video: 'src',
15 | source: 'src',
16 | img: 'src',
17 | image: 'xlink:href'
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/client/build/webpack.base.conf.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const path = require('path')
3 | const utils = require('./utils')
4 | const config = require('../config/index')
5 | const vueLoaderConfig = require('./vue-loader.conf')
6 |
7 | function resolve (dir) {
8 | return path.join(__dirname, '..', dir)
9 | }
10 |
11 | module.exports = {
12 | entry: {
13 | app: './client/src/main.js'
14 | },
15 | output: {
16 | path: config.build.assetsRoot,
17 | filename: '[name].js',
18 | publicPath: process.env.NODE_ENV === 'production'
19 | ? config.build.assetsPublicPath
20 | : config.dev.assetsPublicPath
21 | },
22 | resolve: {
23 | extensions: ['.js', '.vue', '.json'],
24 | alias: {
25 | 'vue$': 'vue/dist/vue.esm.js',
26 | '@': resolve('src')
27 | }
28 | },
29 | module: {
30 | rules: [
31 | // {
32 | // test: /\.(js|vue)$/,
33 | // loader: 'eslint-loader',
34 | // enforce: 'pre',
35 | // include: [resolve('src'), resolve('test')],
36 | // options: {
37 | // formatter: require('eslint-friendly-formatter')
38 | // }
39 | // },
40 | {
41 | test: /\.vue$/,
42 | loader: 'vue-loader',
43 | options: vueLoaderConfig
44 | },
45 | {
46 | test: /\.js$/,
47 | loader: 'babel-loader',
48 | include: [resolve('src'), resolve('test')]
49 | },
50 | {
51 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
52 | loader: 'url-loader',
53 | options: {
54 | limit: 10000,
55 | name: utils.assetsPath('img/[name].[hash:7].[ext]')
56 | }
57 | },
58 | {
59 | test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
60 | loader: 'url-loader',
61 | options: {
62 | limit: 10000,
63 | name: utils.assetsPath('media/[name].[hash:7].[ext]')
64 | }
65 | },
66 | {
67 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
68 | loader: 'url-loader',
69 | options: {
70 | limit: 80000,
71 | name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
72 | }
73 | }
74 | ]
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/client/build/webpack.dev.conf.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const utils = require('./utils')
3 | const webpack = require('webpack')
4 | const config = require('../config/index')
5 | const merge = require('webpack-merge')
6 | const baseWebpackConfig = require('./webpack.base.conf')
7 | const HtmlWebpackPlugin = require('html-webpack-plugin')
8 | const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
9 |
10 | // add hot-reload related code to entry chunks
11 | Object.keys(baseWebpackConfig.entry).forEach(function (name) {
12 | baseWebpackConfig.entry[name] = ['./client/build/dev-client'].concat(baseWebpackConfig.entry[name])
13 | })
14 |
15 | module.exports = merge(baseWebpackConfig, {
16 | module: {
17 | rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap })
18 | },
19 | // cheap-module-eval-source-map is faster for development
20 | devtool: '#cheap-module-eval-source-map',
21 | plugins: [
22 | new webpack.DefinePlugin({
23 | 'process.env': config.dev.env
24 | }),
25 | // https://github.com/glenjamin/webpack-hot-middleware#installation--usage
26 | new webpack.HotModuleReplacementPlugin(),
27 | new webpack.NoEmitOnErrorsPlugin(),
28 | // https://github.com/ampedandwired/html-webpack-plugin
29 | new HtmlWebpackPlugin({
30 | filename: 'index.html',
31 | template: './client/index.html',
32 | favicon: './favicon.ico',
33 | inject: true
34 | }),
35 | new FriendlyErrorsPlugin(),
36 | new webpack.ProvidePlugin({
37 | axios: 'axios'
38 | })
39 | ]
40 | })
41 |
--------------------------------------------------------------------------------
/client/build/webpack.prod.conf.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const path = require('path')
3 | const utils = require('./utils')
4 | const webpack = require('webpack')
5 | const config = require('../config/index')
6 | const merge = require('webpack-merge')
7 | const baseWebpackConfig = require('./webpack.base.conf')
8 | const CopyWebpackPlugin = require('copy-webpack-plugin')
9 | const HtmlWebpackPlugin = require('html-webpack-plugin')
10 | const ExtractTextPlugin = require('extract-text-webpack-plugin')
11 | const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
12 |
13 | const env = config.build.env
14 |
15 | const webpackConfig = merge(baseWebpackConfig, {
16 | module: {
17 | rules: utils.styleLoaders({
18 | sourceMap: config.build.productionSourceMap,
19 | extract: true
20 | })
21 | },
22 | devtool: config.build.productionSourceMap ? '#source-map' : false,
23 | output: {
24 | path: config.build.assetsRoot,
25 | filename: utils.assetsPath('js/[name].[chunkhash].js'),
26 | chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
27 | },
28 | plugins: [
29 | new webpack.ContextReplacementPlugin(/moment[\\\/]locale$/, /^\.\/(zh-cn|en-gb)$/),
30 | new webpack.ProvidePlugin({
31 | axios: 'axios'
32 | }),
33 | // http://vuejs.github.io/vue-loader/en/workflow/production.html
34 | new webpack.DefinePlugin({
35 | 'process.env': env
36 | }),
37 | // UglifyJs do not support ES6+, you can also use babel-minify for better treeshaking: https://github.com/babel/minify
38 | new webpack.optimize.UglifyJsPlugin({
39 | compress: {
40 | warnings: false
41 | }
42 | }),
43 | // extract css into its own file
44 | new ExtractTextPlugin({
45 | filename: utils.assetsPath('css/[name].[contenthash].css')
46 | }),
47 | // Compress extracted CSS. We are using this plugin so that possible
48 | // duplicated CSS from different components can be deduped.
49 | new OptimizeCSSPlugin({
50 | cssProcessorOptions: {
51 | safe: true
52 | }
53 | }),
54 | // generate dist index.html with correct asset hash for caching.
55 | // you can customize output by editing /index.html
56 | // see https://github.com/ampedandwired/html-webpack-plugin
57 | new HtmlWebpackPlugin({
58 | filename: config.build.index,
59 | template: './client/index.html',
60 | favicon: './favicon.ico',
61 | inject: true,
62 | minify: {
63 | removeComments: true,
64 | collapseWhitespace: true,
65 | removeAttributeQuotes: true
66 | // more options:
67 | // https://github.com/kangax/html-minifier#options-quick-reference
68 | },
69 | // necessary to consistently work with multiple chunks via CommonsChunkPlugin
70 | chunksSortMode: 'dependency'
71 | }),
72 | // keep module.id stable when vender modules does not change
73 | new webpack.HashedModuleIdsPlugin(),
74 | // split vendor js into its own file
75 | new webpack.optimize.CommonsChunkPlugin({
76 | name: 'vendor',
77 | minChunks: function (module) {
78 | // any required modules inside node_modules are extracted to vendor
79 | return (
80 | module.resource &&
81 | /\.js$/.test(module.resource) &&
82 | module.resource.indexOf(
83 | path.join(__dirname, '../../node_modules')
84 | ) === 0
85 | )
86 | }
87 | }),
88 | // extract webpack runtime and module manifest to its own file in order to
89 | // prevent vendor hash from being updated whenever app bundle is updated
90 | new webpack.optimize.CommonsChunkPlugin({
91 | name: 'manifest',
92 | chunks: ['vendor']
93 | }),
94 | // copy custom static assets
95 | new CopyWebpackPlugin([
96 | {
97 | from: path.resolve(__dirname, '../../static'),
98 | to: config.build.assetsSubDirectory,
99 | ignore: ['.*']
100 | }
101 | ])
102 | ]
103 | })
104 |
105 | if (config.build.productionGzip) {
106 | const CompressionWebpackPlugin = require('compression-webpack-plugin')
107 | console.log(CompressionWebpackPlugin)
108 | webpackConfig.plugins.push(
109 | new CompressionWebpackPlugin({
110 | asset: '[path].gz[query]',
111 | algorithm: 'gzip',
112 | test: new RegExp(
113 | '\\.(' +
114 | config.build.productionGzipExtensions.join('|') +
115 | ')$'
116 | ),
117 | threshold: 10240,
118 | minRatio: 0.8
119 | })
120 | )
121 | }
122 |
123 | if (config.build.bundleAnalyzerReport) {
124 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
125 | webpackConfig.plugins.push(new BundleAnalyzerPlugin())
126 | }
127 |
128 | module.exports = webpackConfig
129 |
--------------------------------------------------------------------------------
/client/config/dev.env.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const merge = require('webpack-merge')
3 | const prodEnv = require('./prod.env')
4 |
5 | module.exports = merge(prodEnv, {
6 | NODE_ENV: '"development"'
7 | })
8 |
--------------------------------------------------------------------------------
/client/config/index.js:
--------------------------------------------------------------------------------
1 |
2 | 'use strict'
3 | // Template version: 1.1.1
4 | // see http://vuejs-templates.github.io/webpack for documentation.
5 |
6 | const path = require('path')
7 |
8 | module.exports = {
9 | build: {
10 | env: require('./prod.env'),
11 | index: path.resolve(__dirname, '../dist/index.html'),
12 | assetsRoot: path.resolve(__dirname, '../dist'),
13 | assetsSubDirectory: 'static',
14 | assetsPublicPath: './',
15 | productionSourceMap: true,
16 | // Gzip off by default as many popular static hosts such as
17 | // Surge or Netlify already gzip all static assets for you.
18 | // Before setting to `true`, make sure to:
19 | // npm install --save-dev compression-webpack-plugin
20 | productionGzip: false,
21 | productionGzipExtensions: ['js', 'css'],
22 | // Run the build command with an extra argument to
23 | // View the bundle analyzer report after build finishes:
24 | // `npm run build --report`
25 | // Set to `true` or `false` to always turn it on or off
26 | bundleAnalyzerReport: process.env.npm_config_report
27 | },
28 | dev: {
29 | env: require('./dev.env'),
30 | port: process.env.PORT || 8080,
31 | autoOpenBrowser: true,
32 | assetsSubDirectory: 'static',
33 | assetsPublicPath: '/',
34 | proxyTable: {},
35 | // CSS Sourcemaps off by default because relative paths are "buggy"
36 | // with this option, according to the CSS-Loader README
37 | // (https://github.com/webpack/css-loader#sourcemaps)
38 | // In our experience, they generally work as expected,
39 | // just be aware of this issue when enabling this option.
40 | cssSourceMap: false
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/client/config/prod.env.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | module.exports = {
3 | NODE_ENV: '"production"'
4 | }
5 |
--------------------------------------------------------------------------------
/client/config/test.env.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const merge = require('webpack-merge')
3 | const devEnv = require('./dev.env')
4 |
5 | module.exports = merge(devEnv, {
6 | NODE_ENV: '"testing"'
7 | })
8 |
--------------------------------------------------------------------------------
/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 无火的余灰
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/client/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
11 |
12 |
13 |
14 |
47 |
48 |
72 |
--------------------------------------------------------------------------------
/client/src/assets/font/ODelI1aHBYDBqgeIAH2zlIa1YDtoarzwSXxTHggEXMw.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StudentWan/ashen-blog/ba4d3e3a5b47c5ab671fe98fe733eadada45d258/client/src/assets/font/ODelI1aHBYDBqgeIAH2zlIa1YDtoarzwSXxTHggEXMw.woff2
--------------------------------------------------------------------------------
/client/src/assets/font/ODelI1aHBYDBqgeIAH2zlJbPFduIYtoLzwST68uhz_Y.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StudentWan/ashen-blog/ba4d3e3a5b47c5ab671fe98fe733eadada45d258/client/src/assets/font/ODelI1aHBYDBqgeIAH2zlJbPFduIYtoLzwST68uhz_Y.woff2
--------------------------------------------------------------------------------
/client/src/assets/font/mAcLJWdPWDNiDJwJvcWKc3YhjbSpvc47ee6xR_80Hnw.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StudentWan/ashen-blog/ba4d3e3a5b47c5ab671fe98fe733eadada45d258/client/src/assets/font/mAcLJWdPWDNiDJwJvcWKc3YhjbSpvc47ee6xR_80Hnw.woff2
--------------------------------------------------------------------------------
/client/src/assets/font/toadOcfmlt9b38dHJxOBGKyGJhAh-RE0BxGcd_izyev3rGVtsTkPsbDajuO5ueQw.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StudentWan/ashen-blog/ba4d3e3a5b47c5ab671fe98fe733eadada45d258/client/src/assets/font/toadOcfmlt9b38dHJxOBGKyGJhAh-RE0BxGcd_izyev3rGVtsTkPsbDajuO5ueQw.woff2
--------------------------------------------------------------------------------
/client/src/assets/font/toadOcfmlt9b38dHJxOBGMzFoXZ-Kj537nB_-9jJhlA.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StudentWan/ashen-blog/ba4d3e3a5b47c5ab671fe98fe733eadada45d258/client/src/assets/font/toadOcfmlt9b38dHJxOBGMzFoXZ-Kj537nB_-9jJhlA.woff2
--------------------------------------------------------------------------------
/client/src/assets/img/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StudentWan/ashen-blog/ba4d3e3a5b47c5ab671fe98fe733eadada45d258/client/src/assets/img/logo.png
--------------------------------------------------------------------------------
/client/src/assets/img/star-half.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StudentWan/ashen-blog/ba4d3e3a5b47c5ab671fe98fe733eadada45d258/client/src/assets/img/star-half.png
--------------------------------------------------------------------------------
/client/src/assets/img/star-off.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StudentWan/ashen-blog/ba4d3e3a5b47c5ab671fe98fe733eadada45d258/client/src/assets/img/star-off.png
--------------------------------------------------------------------------------
/client/src/assets/img/star-on.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StudentWan/ashen-blog/ba4d3e3a5b47c5ab671fe98fe733eadada45d258/client/src/assets/img/star-on.png
--------------------------------------------------------------------------------
/client/src/assets/style/_font.scss:
--------------------------------------------------------------------------------
1 | /* latin-ext */
2 | @font-face {
3 | font-family: 'Source Sans Pro';
4 | font-style: normal;
5 | font-weight: 400;
6 | src: local('Source Sans Pro'), local('SourceSansPro-Regular'), url(../font/ODelI1aHBYDBqgeIAH2zlIa1YDtoarzwSXxTHggEXMw.woff2) format('woff2');
7 | unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF;
8 | }
9 |
10 | /* latin */
11 | @font-face {
12 | font-family: 'Source Sans Pro';
13 | font-style: normal;
14 | font-weight: 400;
15 | src: local('Source Sans Pro'), local('SourceSansPro-Regular'), url(../font/ODelI1aHBYDBqgeIAH2zlJbPFduIYtoLzwST68uhz_Y.woff2) format('woff2');
16 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
17 | }
18 |
19 | /* latin-ext */
20 | @font-face {
21 | font-family: 'Source Sans Pro';
22 | font-style: normal;
23 | font-weight: 600;
24 | src: local('Source Sans Pro Semibold'), local('SourceSansPro-Semibold'), url(../font/toadOcfmlt9b38dHJxOBGKyGJhAh-RE0BxGcd_izyev3rGVtsTkPsbDajuO5ueQw.woff2) format('woff2');
25 | unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF;
26 | }
27 |
28 | /* latin */
29 | @font-face {
30 | font-family: 'Source Sans Pro';
31 | font-style: normal;
32 | font-weight: 600;
33 | src: local('Source Sans Pro Semibold'), local('SourceSansPro-Semibold'), url(../font/toadOcfmlt9b38dHJxOBGMzFoXZ-Kj537nB_-9jJhlA.woff2) format('woff2');
34 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
35 | }
36 |
37 | /* latin */
38 | @font-face {
39 | font-family: 'Dosis';
40 | font-style: normal;
41 | font-weight: 500;
42 | src: local('Dosis Medium'), local('Dosis-Medium'), url(../font/mAcLJWdPWDNiDJwJvcWKc3YhjbSpvc47ee6xR_80Hnw.woff2) format('woff2');
43 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
44 | }
45 |
--------------------------------------------------------------------------------
/client/src/assets/style/_highlight.scss:
--------------------------------------------------------------------------------
1 | pre {
2 | display: block;
3 | overflow-x: auto;
4 | padding: 0.5em;
5 | color: #383a42;
6 | background: #f8f8f8;
7 | }
8 |
9 | .hljs-comment,
10 | .hljs-quote {
11 | color: #a0a1a7;
12 | font-style: italic;
13 | }
14 |
15 | .hljs-doctag,
16 | .hljs-keyword,
17 | .hljs-formula {
18 | color: #a626a4;
19 | }
20 |
21 | .hljs-section,
22 | .hljs-name,
23 | .hljs-selector-tag,
24 | .hljs-deletion,
25 | .hljs-subst {
26 | color: #e45649;
27 | }
28 |
29 | .hljs-literal {
30 | color: #0184bb;
31 | }
32 |
33 | .hljs-string,
34 | .hljs-regexp,
35 | .hljs-addition,
36 | .hljs-attribute,
37 | .hljs-meta-string {
38 | color: #50a14f;
39 | }
40 |
41 | .hljs-built_in,
42 | .hljs-class .hljs-title {
43 | color: #c18401;
44 | }
45 |
46 | .hljs-attr,
47 | .hljs-variable,
48 | .hljs-template-variable,
49 | .hljs-type,
50 | .hljs-selector-class,
51 | .hljs-selector-attr,
52 | .hljs-selector-pseudo,
53 | .hljs-number {
54 | color: #986801;
55 | }
56 |
57 | .hljs-symbol,
58 | .hljs-bullet,
59 | .hljs-link,
60 | .hljs-meta,
61 | .hljs-selector-id,
62 | .hljs-title {
63 | color: #4078f2;
64 | }
65 |
66 | .hljs-emphasis {
67 | font-style: italic;
68 | }
69 |
70 | .hljs-strong {
71 | font-weight: bold;
72 | }
73 |
74 | .hljs-link {
75 | text-decoration: underline;
76 | }
77 |
--------------------------------------------------------------------------------
/client/src/assets/style/_variable.scss:
--------------------------------------------------------------------------------
1 | // sass-resources-loader加载该文件,用于在每个文件导入mixin和variables
2 | // 但不可用于做全局样式文件,因为在每个文件中导入,路径并不统一
3 |
4 | // 基础配色
5 | $white: #fefdff;
6 | $black: #000;
7 |
8 | // 系统配色
9 | $title: #2c3e50;
10 | $word: #34495e;
11 | $base: #f18f01;
12 | $quote: #99c24d;
13 | $special: #c1bfb5;
14 |
15 | @mixin chosen-item {
16 | border-bottom: 3px solid $base;
17 | }
18 |
19 | @mixin flex($flow: row wrap, $justify: center, $align: center) {
20 | display: flex;
21 | flex-flow: $flow;
22 | justify-content: $justify;
23 | align-items: $align;
24 | }
25 |
26 | @mixin fix($left: 0px, $top: 0px) {
27 | position: fixed;
28 | left: $left;
29 | top: $top;
30 | }
31 |
32 | @mixin clear-fix($element) {
33 | #{$element}:before,
34 | #{$element}:after {
35 | content: "";
36 | display: table;
37 | }
38 | #{$element}:after {
39 | clear: both;
40 | }
41 | #{$element} {
42 | *zoom: 1;
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/client/src/assets/style/index.scss:
--------------------------------------------------------------------------------
1 | @import "font";
2 | @import "highlight";
3 | html {
4 | font: {
5 | size: 62.5%;
6 | family: "Source Sans Pro", "Helvetica Neue", Arial, sans-serif;
7 | }
8 | color: $word;
9 | }
10 |
11 | body {
12 | margin: 0;
13 | min-width: 300px;
14 | }
15 |
16 | body * {
17 | box-sizing: border-box;
18 | }
19 |
20 | a {
21 | text-decoration: none;
22 | color: $word;
23 | }
24 |
25 | #main {
26 | font-size: 1.4rem;
27 | padding: .5em 0 1em 0;
28 | width: 95%;
29 | max-width: 850px;
30 | margin: auto;
31 | }
32 |
33 | h1 {
34 | font-size: 1.7em;
35 | }
36 |
37 | h1,
38 | h2,
39 | h3,
40 | h4 {
41 | color: $title;
42 | }
43 |
44 | h3:before,
45 | h4:before {
46 | content: "# ";
47 | color: $base;
48 | }
49 |
50 | .time {
51 | color: $special;
52 | }
53 |
54 | // 文章解析的全局样式
55 | article {
56 | h2 {
57 | padding-bottom: .6em;
58 | border-bottom: 1px solid $special;
59 | }
60 | .time:before {
61 | content: "";
62 | }
63 | img {
64 | max-width: 100%;
65 | }
66 | a {
67 | color: $base;
68 | }
69 | blockquote {
70 | border-left: 3px solid $base;
71 | margin: 2em 0;
72 | padding-left: 1em;
73 | font-weight: 600;
74 | }
75 | }
76 |
77 | // slide-fade动画效果
78 | .slide-fade-enter-active {
79 | transition: all 0.5s ease;
80 | }
81 |
82 | .slide-fade-leave-active {
83 | transition: all 0.5s cubic-bezier(1.0, 0.5, 0.8, 1.0);;
84 | }
85 |
86 | .slide-fade-enter,
87 | .slide-fade-leave-to {
88 | transform: translateX(235px);
89 | }
90 |
91 | .fade-enter-active, .fade-leave-active {
92 | transition: opacity .2s ease;
93 | }
94 | .fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
95 | opacity: 0;
96 | }
97 |
98 | //.fade-enter-active {
99 | // transition: opacity 0s;
100 | //}
101 |
102 | @media screen and (max-width: 480px) {
103 | #main {
104 | margin-top: 4.7em;
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/client/src/components/About.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
32 |
33 |
34 |
47 |
--------------------------------------------------------------------------------
/client/src/components/Archive.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
文章归档
5 |
6 | {{ year }}
7 |
8 | -
9 | {{ intro.title }}
10 | {{ intro.publishTime }}
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
52 |
53 |
54 |
73 |
--------------------------------------------------------------------------------
/client/src/components/Article.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ title }}
5 | {{ publishTime }}
6 |
7 | {{ tag }}
8 |
9 |
10 |
11 |
12 |
13 |
14 |
51 |
52 |
58 |
--------------------------------------------------------------------------------
/client/src/components/ArticleList.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | {{ title }}
8 |
9 | {{ publishTime }}
10 |
11 |
12 |
15 |
16 |
17 |
24 |
25 |
26 |
27 |
93 |
94 |
95 |
133 |
134 |
--------------------------------------------------------------------------------
/client/src/components/ReadingList.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
阅读列表
4 |
5 |
6 | 书名 |
7 | 作者 |
8 | ashenの评分 |
9 |
10 |
11 | {{ name }} |
12 | {{ author }} |
13 |
14 |
15 | |
16 |
17 |
18 |
19 |
20 |
21 |
47 |
48 |
49 |
81 |
--------------------------------------------------------------------------------
/client/src/components/Tag.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
标签
4 |
11 |
12 | -
13 | {{ tag }}
14 |
15 | -
16 | {{ intro.title }}
17 | {{ intro.publishTime }}
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
73 |
74 |
75 |
99 |
--------------------------------------------------------------------------------
/client/src/components/common/HeaderNav.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
31 |
32 |
51 |
52 |
53 |
54 |
55 |
102 |
103 |
208 |
--------------------------------------------------------------------------------
/client/src/components/common/Star.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
35 |
36 |
62 |
--------------------------------------------------------------------------------
/client/src/main.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author {benyuwan@gmail.com}
3 | * @file client端入口文件
4 | */
5 | // The Vue build version to load with the `import` command
6 | // (runtime-only or standalone) has been set in webpack.base.conf with an alias.
7 | import '@/assets/style/index.scss'
8 | import Vue from 'vue'
9 | import App from '@/App'
10 | import router from './router'
11 |
12 | Vue.config.productionTip = false
13 |
14 | axios.defaults.baseURL = 'http://localhost:3000'
15 | axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded'
16 |
17 | new Vue({
18 | el: '#app',
19 | router,
20 | template: '',
21 | components: {
22 | App
23 | }
24 | })
25 |
--------------------------------------------------------------------------------
/client/src/router/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author {benyuwan@gmail.com}
3 | * @file client端路由文件
4 | */
5 | import Vue from 'vue'
6 | import Router from 'vue-router'
7 |
8 | import ArticleList from '@/components/ArticleList'
9 | import Article from '@/components/Article'
10 | import Archive from '@/components/Archive'
11 | import Tag from '@/components/Tag'
12 | import ReadingList from '@/components/ReadingList'
13 | import About from '@/components/About'
14 |
15 | Vue.use(Router)
16 |
17 | const router = new Router({
18 | routes: [
19 | {
20 | path: '/articles',
21 | component: ArticleList
22 | },
23 | {
24 | path: '/articles/:id',
25 | component: Article
26 | },
27 | {
28 | path: '/archives',
29 | component: Archive
30 | },
31 | {
32 | path: '/tags',
33 | component: Tag
34 | },
35 | {
36 | path: '/lists',
37 | component: ReadingList
38 | },
39 | {
40 | path: '/about',
41 | component: About
42 | },
43 | {
44 | path: '*',
45 | redirect: {
46 | path: 'articles',
47 | query: {
48 | page: 0
49 | }
50 | }
51 | }
52 | ]
53 | })
54 |
55 | router.beforeEach((to, from, next) => {
56 | document.documentElement.scrollTop = 0
57 | document.body.scrollTop = 0
58 | next()
59 | })
60 |
61 | export default router
62 |
--------------------------------------------------------------------------------
/client/src/utils/parseMarkdown.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author {benyuwan@gmail.com}
3 | * @file 解析markdown编写的文章内容
4 | */
5 |
6 | import marked from 'marked'
7 | import highlight from 'highlight.js/lib/highlight'
8 | // 按需加载,解决打包后highlight过大的问题
9 | const languages = ['cpp', 'xml', 'bash', 'coffeescript', 'css', 'markdown', 'http', 'java',
10 | 'javascript', 'json', 'less', 'makefile', 'nginx', 'php', 'python', 'scss', 'sql', 'stylus'
11 | ]
12 | highlight.registerLanguage('cpp', require('highlight.js/lib/languages/cpp'))
13 | highlight.registerLanguage('xml', require('highlight.js/lib/languages/xml'))
14 | highlight.registerLanguage('bash', require('highlight.js/lib/languages/bash'))
15 | highlight.registerLanguage('coffeescript', require('highlight.js/lib/languages/coffeescript'))
16 | highlight.registerLanguage('css', require('highlight.js/lib/languages/css'))
17 | highlight.registerLanguage('markdown', require('highlight.js/lib/languages/markdown'))
18 | highlight.registerLanguage('http', require('highlight.js/lib/languages/http'))
19 | highlight.registerLanguage('java', require('highlight.js/lib/languages/java'))
20 | highlight.registerLanguage('javascript', require('highlight.js/lib/languages/javascript'))
21 | highlight.registerLanguage('json', require('highlight.js/lib/languages/json'))
22 | highlight.registerLanguage('less', require('highlight.js/lib/languages/less'))
23 | highlight.registerLanguage('makefile', require('highlight.js/lib/languages/makefile'))
24 | highlight.registerLanguage('nginx', require('highlight.js/lib/languages/nginx'))
25 | highlight.registerLanguage('php', require('highlight.js/lib/languages/php'))
26 | highlight.registerLanguage('python', require('highlight.js/lib/languages/python'))
27 | highlight.registerLanguage('scss', require('highlight.js/lib/languages/scss'))
28 | highlight.registerLanguage('sql', require('highlight.js/lib/languages/sql'))
29 | highlight.registerLanguage('stylus', require('highlight.js/lib/languages/stylus'))
30 |
31 | marked.setOptions({
32 | renderer: new marked.Renderer(),
33 | gfm: true,
34 | tables: true,
35 | breaks: false,
36 | pedantic: false,
37 | sanitize: false,
38 | smartLists: true,
39 | smartypants: false,
40 | highlight(code, lang) {
41 | if (!~languages.indexOf(lang)) {
42 | return highlight.highlightAuto(code).value
43 | }
44 | return highlight.highlight(lang, code).value
45 | }
46 | })
47 |
48 | export default function (content) {
49 | return marked(content)
50 | }
51 |
--------------------------------------------------------------------------------
/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StudentWan/ashen-blog/ba4d3e3a5b47c5ab671fe98fe733eadada45d258/favicon.ico
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ashen-blog",
3 | "version": "1.0.0",
4 | "description": "ashen's blog, koa 2.x + vue 2.x ",
5 | "author": "StudentWan ",
6 | "private": true,
7 | "scripts": {
8 | "start": "pm2 start server/config/pm2_config.json",
9 | "babel-node": "babel-node --presets=/*a*/ --ignore='foo|bar|baz'",
10 | "dev-client": "node client/build/dev-server.js",
11 | "build-client": "node client/build/build.js",
12 | "dev-admin": "node admin/build/dev-server.js",
13 | "build-admin": "node admin/build/build.js",
14 | "dev-server": "nodemon server/ --exec babel-node",
15 | "lint": "eslint --ext .js,.vue src"
16 | },
17 | "dependencies": {
18 | "axios": "^0.17.1",
19 | "font-awesome": "^4.7.0",
20 | "glob": "^7.1.2",
21 | "highlight.js": "^9.12.0",
22 | "jsonwebtoken": "^8.1.0",
23 | "koa": "^2.3.0",
24 | "koa-bodyparser": "^4.2.0",
25 | "koa-cors": "0.0.16",
26 | "koa-helmet": "^3.2.0",
27 | "koa-logger": "^3.1.0",
28 | "koa-onerror": "^4.0.0",
29 | "koa-router": "^7.2.1",
30 | "lodash": "^4.17.4",
31 | "lodash.debounce": "^4.0.8",
32 | "marked": "^0.3.6",
33 | "md5": "^2.2.1",
34 | "moment": "^2.19.4",
35 | "mysql": "^2.15.0",
36 | "simplemde": "^1.11.2",
37 | "thenify-all": "^1.6.0",
38 | "vue": "^2.4.2",
39 | "vue-router": "^2.7.0",
40 | "vue-simplemde": "^0.4.6",
41 | "vuex": "^3.0.0"
42 | },
43 | "devDependencies": {
44 | "autoprefixer": "^7.1.2",
45 | "babel-cli": "^6.26.0",
46 | "babel-core": "^6.22.1",
47 | "babel-eslint": "^7.1.1",
48 | "babel-loader": "^7.1.1",
49 | "babel-plugin-transform-runtime": "^6.23.0",
50 | "babel-polyfill": "^6.26.0",
51 | "babel-preset-env": "^1.3.2",
52 | "babel-preset-stage-2": "^6.22.0",
53 | "babel-register": "^6.22.0",
54 | "babel-runtime": "^6.26.0",
55 | "chalk": "^2.0.1",
56 | "compression-webpack-plugin": "^1.1.3",
57 | "connect-history-api-fallback": "^1.3.0",
58 | "copy-webpack-plugin": "^4.0.1",
59 | "cross-env": "^5.1.3",
60 | "css-loader": "^0.28.0",
61 | "eslint": "^3.19.0",
62 | "eslint-config-fecs-demo": "^1.0.7",
63 | "eslint-friendly-formatter": "^3.0.0",
64 | "eslint-import-resolver-webpack": "^0.8.3",
65 | "eslint-loader": "^1.7.1",
66 | "eslint-plugin-html": "^3.0.0",
67 | "eventsource-polyfill": "^0.9.6",
68 | "express": "^4.14.1",
69 | "extract-text-webpack-plugin": "^3.0.0",
70 | "file-loader": "^1.1.4",
71 | "friendly-errors-webpack-plugin": "^1.6.1",
72 | "html-webpack-plugin": "^2.30.1",
73 | "http-proxy-middleware": "^0.17.3",
74 | "mocha": "^5.0.0",
75 | "node-sass": "^4.5.3",
76 | "nodemon": "^1.12.1",
77 | "opn": "^5.1.0",
78 | "optimize-css-assets-webpack-plugin": "^3.2.0",
79 | "ora": "^1.2.0",
80 | "portfinder": "^1.0.13",
81 | "rimraf": "^2.6.0",
82 | "sass-loader": "^6.0.6",
83 | "sass-resources-loader": "^1.3.1",
84 | "semver": "^5.3.0",
85 | "shelljs": "^0.7.6",
86 | "source-map-support": "^0.5.0",
87 | "url-loader": "^0.5.8",
88 | "vue-loader": "^13.0.4",
89 | "vue-style-loader": "^3.0.1",
90 | "vue-template-compiler": "^2.4.2",
91 | "webpack": "^3.6.0",
92 | "webpack-bundle-analyzer": "^2.9.1",
93 | "webpack-dev-middleware": "^1.12.0",
94 | "webpack-hot-middleware": "^2.18.2",
95 | "webpack-merge": "^4.1.0"
96 | },
97 | "engines": {
98 | "node": ">= 4.0.0",
99 | "npm": ">= 3.0.0"
100 | },
101 | "browserslist": [
102 | "> 1%",
103 | "last 2 versions",
104 | "not ie <= 8"
105 | ]
106 | }
107 |
--------------------------------------------------------------------------------
/server/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["env"]
3 | }
4 |
--------------------------------------------------------------------------------
/server/config/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author {benyuwan@gmail.com}
3 | * @file server端配置文件
4 | */
5 |
6 | export const db = {
7 | host: '127.0.0.1',
8 | port: '8889',
9 | user: 'root',
10 | password: 'root',
11 | multipleStatements: true
12 | }
13 |
14 | export const dbName = {
15 | database: 'ashen_db'
16 | }
17 |
18 | export const port = 3000
19 |
20 | export const baseApi = 'api/v1'
21 |
22 | export const secret = 'ashen-one'
23 |
--------------------------------------------------------------------------------
/server/config/pm2_config.json:
--------------------------------------------------------------------------------
1 | {
2 | "apps" : [{
3 | "name" : "ashen-server",
4 | "script" : "./server/index.js",
5 | "watch" : true,
6 | "exec_interpreter" : "babel-node",
7 | "exec_mode" : "fork"
8 | }]
9 | }
10 |
--------------------------------------------------------------------------------
/server/controllers/articles.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author {benyuwan@gmail.com}
3 | * @file 关于文章的controller
4 | */
5 |
6 | import Article from '../models/articles'
7 |
8 | class ArticleControllers {
9 |
10 | async addArticle(ctx) {
11 | const res = await Article.addArticle()
12 | ctx.body = res
13 | }
14 |
15 | async getArticleList(ctx) {
16 | const {isPublished = 0, offset = 0, limit = 0} = ctx.query
17 | if (isPublished) {
18 | const res = {
19 | maxPage: '',
20 | articles: ''
21 | }
22 | const promises = []
23 | promises.push(Article.getPagination())
24 | promises.push(Article.getLimitArticles(offset, limit))
25 | const results = await Promise.all(promises)
26 | res.maxPage = Math.ceil(results[0][0]['COUNT(*)'] / limit)
27 | res.articles = results[1]
28 | ctx.body = res
29 | }
30 | else {
31 | ctx.body = await Article.getAllArticles()
32 | }
33 | }
34 |
35 | async getOneArticle(ctx) {
36 | const res = await Article.getOneArticle(ctx.params.id)
37 | if (res.length === 0) {
38 | ctx.throw(404, '没有找到到该文章!')
39 | }
40 | ctx.body = res
41 | }
42 |
43 | async updateArticle(ctx) {
44 | const id = ctx.params.id
45 | const {title, tags, content} = ctx.request.body
46 | ctx.body = await Article.updateArticle(id, {title, tags, content})
47 | }
48 |
49 | async publishArticle(ctx) {
50 | const id = ctx.params.id
51 | const {title, tags, content} = ctx.request.body
52 | ctx.body = await Article.publishArticle(id, {title, tags, content})
53 | }
54 |
55 | async deleteArticle(ctx) {
56 | ctx.body = await Article.deleteArticle(ctx.params.id)
57 | }
58 | }
59 |
60 | export default new ArticleControllers()
61 |
--------------------------------------------------------------------------------
/server/controllers/books.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author {benyuwan@gmail.com}
3 | * @file 关于阅读列表的controller
4 | */
5 |
6 | import Book from '../models/books'
7 |
8 | class BookControllers {
9 | async getBookList(ctx) {
10 | ctx.body = await Book.getAllBooks()
11 | }
12 |
13 | async addBook(ctx) {
14 | const book = ctx.request.body
15 | ctx.body = await Book.addBook(book)
16 | }
17 |
18 | async editBook(ctx) {
19 | const id = ctx.params.id
20 | const book = ctx.request.body
21 | ctx.body = await Book.updateBook(id, book)
22 | }
23 |
24 | async deleteBook(ctx) {
25 | const id = ctx.params.id
26 | ctx.body = await Book.deleteBook(id)
27 | }
28 | }
29 |
30 | export default new BookControllers()
31 |
--------------------------------------------------------------------------------
/server/controllers/briefs.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author {benyuwan@gmail.com}
3 | * @file 关于个人简介的controller
4 | */
5 |
6 | import Brief from '../models/briefs'
7 |
8 | class BriefControllers {
9 | async getBrief(ctx) {
10 | ctx.body = await Brief.getBrief()
11 | }
12 |
13 | async updateBrief(ctx) {
14 | const id = ctx.params.id
15 | const content = ctx.request.body.content
16 | ctx.body = await Brief.updateBrief(id, content)
17 | }
18 | }
19 |
20 | export default new BriefControllers()
21 |
--------------------------------------------------------------------------------
/server/controllers/introductions.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author {benyuwan@gmail.com}
3 | * @file 关于文章介绍信息的controller
4 | */
5 |
6 | import Introduction from '../models/introductions'
7 |
8 | class IntroControllers {
9 |
10 | async getIntroductions(ctx) {
11 | ctx.body = await Introduction.getIntroductions()
12 | }
13 | }
14 |
15 | export default new IntroControllers()
16 |
--------------------------------------------------------------------------------
/server/controllers/tags.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author {benyuwan@gmail.com}
3 | * @file 关于标签的controller
4 | */
5 |
6 | import Tag from '../models/tags'
7 |
8 | class TagsController {
9 | async updateTag(ctx) {
10 | ctx.body = await Tag.updateTag(ctx.params.id, ctx.request.body.tags)
11 | }
12 | }
13 |
14 | export default new TagsController()
15 |
--------------------------------------------------------------------------------
/server/controllers/tokens.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author {benyuwan@gmail.com}
3 | * @file 关于token的controller
4 | */
5 |
6 | import jwt from 'jsonwebtoken'
7 | import User from '../models/users'
8 | import {
9 | secret
10 | } from '../config'
11 |
12 | class TokenControllers {
13 |
14 | async createToken(ctx) {
15 | const {
16 | username,
17 | password
18 | } = ctx.request.body
19 | const res = (await User.findUser(username))[0]
20 | if (res) {
21 | if (password === res.password) {
22 | const token = jwt.sign({
23 | exp: Math.floor(Date.now() / 1000) + 24 * 60 * 60// 一天
24 | }, secret)
25 | ctx.body = token
26 | }
27 | else {
28 | ctx.throw(401, '密码错误')
29 | }
30 | }
31 | else {
32 | ctx.throw(401, '用户名错误')
33 | }
34 | }
35 |
36 | checkToken(ctx) {
37 | ctx.body = '验证通过'
38 | }
39 | }
40 |
41 | export default new TokenControllers()
42 |
--------------------------------------------------------------------------------
/server/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author {benyuwan@gmail.com}
3 | * @file server端的入口文件
4 | */
5 |
6 | import 'source-map-support/register'
7 | import bodyParser from 'koa-bodyparser'
8 | import Koa from 'koa'
9 | import logger from 'koa-logger'
10 | import helmet from 'koa-helmet'
11 | import cors from 'koa-cors'
12 | import onerror from 'koa-onerror'
13 | import routing from './routes/'
14 | import {port} from './config'
15 |
16 | const app = new Koa()
17 |
18 | onerror(app)
19 |
20 | app
21 | .use(cors({
22 | maxAge: 7 * 24 * 60 * 60,
23 | credentials: true,
24 | methods: 'GET, HEAD, OPTIONS, PUT, POST, PATCH, DELETE',
25 | headers: 'Content-Type, Accept, Authorization'
26 | }))
27 | .use(logger())
28 | .use(bodyParser())
29 | .use(helmet())
30 |
31 | routing(app)
32 |
33 | app.listen(port, () => console.log(`✅ The server is running at http://localhost:${port}/`))
34 |
--------------------------------------------------------------------------------
/server/middlewares/check.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @file 检查文章格式的中间件
3 | * @author {benyuwan@gmail.com}
4 | */
5 |
6 | export default async function (ctx, next) {
7 | const {title, tags, content, isPublished} = ctx.request.body
8 | if (isPublished) {
9 | if (tags === '' || title === '') {
10 | ctx.throw(400, '标题或者标签未设置!')
11 | }
12 | else {
13 | const pattern = //i
14 | if (!pattern.test(content)) {
15 | ctx.throw(400, '文章没有设置摘要分界!')
16 | }
17 | }
18 | await next()
19 | }
20 | else {
21 | await next()
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/server/middlewares/verify.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author {benyuwan@gmail.com}
3 | * @file 处理验证的中间件
4 | */
5 |
6 | import jwt from 'jsonwebtoken'
7 | import thenifyAll from 'thenify-all'
8 | import {secret} from '../config'
9 |
10 | thenifyAll(jwt, {}, ['verify'])
11 |
12 | export default async function (ctx, next) {
13 | const auth = ctx.get('Authorization')
14 | const token = auth.split(' ')[1]
15 | try {
16 | await jwt.verify(token, secret)
17 | }
18 | catch (err) {
19 | ctx.throw(401, err)
20 | }
21 | await next()
22 | }
23 |
--------------------------------------------------------------------------------
/server/models/articles.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author {benyuwan@gmail.com}
3 | * @file articles的model
4 | */
5 |
6 | import query from '../utils/query'
7 | import escape from '../utils/escape'
8 |
9 | class Articles {
10 | async addArticle() {
11 | return await query(`INSERT INTO ARTICLE SET title='新文章',tags='',createTime=NOW(),publishTime=NOW(),content=''`)
12 | }
13 |
14 | async getAllArticles() {
15 | return await query(`SELECT * FROM ARTICLE ORDER BY createTime DESC`)
16 | }
17 |
18 | async getLimitArticles(offset, limit) {
19 | return await query(escape`SELECT * FROM ARTICLE WHERE isPublished=1 ORDER BY publishTime DESC LIMIT ${parseInt(offset, 10)},${parseInt(limit, 10)}`)
20 | }
21 |
22 | async getPagination() {
23 | return await query(`SELECT COUNT(*) FROM ARTICLE WHERE isPublished=1`)
24 | }
25 |
26 | async getOneArticle(id) {
27 | return await query(`SELECT * FROM ARTICLE WHERE id=${id}`)
28 | }
29 |
30 | async updateArticle(id, {title, tags, content, isPublished}) {
31 | return await query(escape`UPDATE ARTICLE SET title=${title}, tags=${tags}, content=${content} WHERE id=${id}`)
32 | }
33 |
34 | async publishArticle(id, {title, tags, content}) {
35 | return await query(escape`UPDATE ARTICLE SET title=${title}, tags=${tags}, content=${content}, publishTime=NOW(), isPublished=1 WHERE id=${id}`)
36 | }
37 |
38 | async deleteArticle(id) {
39 | return await query(escape`DELETE FROM ARTICLE WHERE id=${id}`)
40 | }
41 | }
42 |
43 | export default new Articles()
44 |
--------------------------------------------------------------------------------
/server/models/books.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author {benyuwan@gmail.com}
3 | * @file books的model
4 | */
5 |
6 | import query from '../utils/query'
7 | import escape from '../utils/escape'
8 |
9 | class Books {
10 | async getAllBooks() {
11 | return await query(`SELECT * FROM RD_LIST`)
12 | }
13 |
14 | async addBook({name, author, score}) {
15 | return await query(escape`INSERT INTO RD_LIST SET name=${name},author=${author},score=${score}`)
16 | }
17 |
18 | async updateBook(id, {name, author, score}) {
19 | return await query(escape`UPDATE RD_LIST SET name=${name},author=${author},score=${score} WHERE id=${id}`)
20 | }
21 |
22 | async deleteBook(id) {
23 | return await query(escape`DELETE FROM RD_LIST WHERE id=${id}`)
24 | }
25 | }
26 |
27 | export default new Books()
28 |
--------------------------------------------------------------------------------
/server/models/briefs.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author {benyuwan@gmail.com}
3 | * @file briefs的model
4 | */
5 |
6 | import query from '../utils/query'
7 | import escape from '../utils/escape'
8 |
9 | class Briefs {
10 | async getBrief() {
11 | return await query(`SELECT * FROM ABOUT`)
12 | }
13 |
14 | async updateBrief(id, content) {
15 | return await query(escape`UPDATE ABOUT SET content=${content} WHERE id=${id}`)
16 | }
17 | }
18 |
19 | export default new Briefs()
20 |
--------------------------------------------------------------------------------
/server/models/introductions.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author {benyuwan@gmail.com}
3 | * @file introductions的model
4 | */
5 |
6 | import query from '../utils/query'
7 |
8 | class Introductions {
9 | async getIntroductions() {
10 | return await query(`SELECT id,title,tags,publishTime FROM ARTICLE where isPublished=1 ORDER BY publishTime DESC`)
11 | }
12 | }
13 |
14 | export default new Introductions()
15 |
--------------------------------------------------------------------------------
/server/models/tags.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author {benyuwan@gmail.com}
3 | * @file tags的model
4 | */
5 |
6 | import query from '../utils/query'
7 | import escape from '../utils/escape'
8 |
9 | class Tags {
10 | async updateTag(id, tags) {
11 | return await query(escape`UPDATE ARTICLE SET tags=${tags} WHERE id=${id}`)
12 | }
13 | }
14 |
15 | export default new Tags()
16 |
--------------------------------------------------------------------------------
/server/models/users.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author {benyuwan@gmail.com}
3 | * @file users的model
4 | */
5 |
6 | import query from '../utils/query'
7 | import escape from '../utils/escape'
8 |
9 | class Users {
10 | async findUser(username) {
11 | return await query(escape`SELECT user,password FROM USER WHERE user=${username}`)
12 | }
13 | }
14 |
15 | export default new Users()
16 |
--------------------------------------------------------------------------------
/server/routes/articles.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author {benyuwan@gmail.com}
3 | * @file 操作文章的api
4 | */
5 |
6 | import Router from 'koa-router'
7 | import {baseApi} from '../config'
8 | import ArticleController from '../controllers/articles'
9 | import verify from '../middlewares/verify'
10 | import check from '../middlewares/check'
11 |
12 | const api = 'articles'
13 |
14 | const router = new Router()
15 |
16 | router.prefix(`/${baseApi}/${api}`)
17 |
18 | router.post('/', verify, ArticleController.addArticle)
19 | router.put('/update/:id', verify, check, ArticleController.updateArticle)
20 | router.put('/publish/:id', verify, check, ArticleController.publishArticle)
21 | router.get('/', ArticleController.getArticleList)
22 | router.get('/:id', ArticleController.getOneArticle)
23 | router.delete('/:id', verify, ArticleController.deleteArticle)
24 |
25 | module.exports = router
26 |
--------------------------------------------------------------------------------
/server/routes/books.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author {benyuwan@gmail.com}
3 | * @file 操作阅读列表的api
4 | */
5 |
6 | import Router from 'koa-router'
7 | import {baseApi} from '../config'
8 | import BookController from '../controllers/books'
9 | import verify from '../middlewares/verify'
10 |
11 | const api = 'books'
12 |
13 | const router = new Router()
14 |
15 | router.prefix(`/${baseApi}/${api}`)
16 |
17 | router.get('/', BookController.getBookList)
18 | router.post('/', verify, BookController.addBook)
19 | router.put('/:id', verify, BookController.editBook)
20 | router.delete('/:id', verify, BookController.deleteBook)
21 |
22 | module.exports = router
23 |
--------------------------------------------------------------------------------
/server/routes/briefs.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author {benyuwan@gmail.com}
3 | * @file 操作个人简介的api
4 | */
5 |
6 | import Router from 'koa-router'
7 | import {baseApi} from '../config'
8 | import BriefController from '../controllers/briefs'
9 | import verify from '../middlewares/verify'
10 |
11 | const api = 'briefs'
12 |
13 | const router = new Router()
14 |
15 | router.prefix(`/${baseApi}/${api}`)
16 |
17 | router.get('/', BriefController.getBrief)
18 | router.put('/:id', verify, BriefController.updateBrief)
19 |
20 | module.exports = router
21 |
22 |
--------------------------------------------------------------------------------
/server/routes/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author {benyuwan@gmail.com}
3 | * @file 路由根文件,遍历并处理每个路由文件
4 | */
5 |
6 | import routesLoader from '../utils/routesLoader'
7 |
8 | export default function (app) {
9 | routesLoader(`${__dirname}`).then(routers => {
10 | routers.forEach(router => {
11 | app
12 | .use(router.routes())
13 | .use(router.allowedMethods({
14 | throw: true
15 | }))
16 | })
17 | })
18 | }
19 |
--------------------------------------------------------------------------------
/server/routes/introductions.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author {benyuwan@gmail.com}
3 | * @file 操作文章介绍信息的api
4 | */
5 |
6 | import Router from 'koa-router'
7 | import {baseApi} from '../config'
8 | import IntroController from '../controllers/introductions'
9 |
10 | const api = 'introductions'
11 |
12 | const router = new Router()
13 |
14 | router.prefix(`/${baseApi}/${api}`)
15 |
16 | router.get('/', IntroController.getIntroductions)
17 |
18 | module.exports = router
19 |
--------------------------------------------------------------------------------
/server/routes/tags.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author {benyuwan@gmail.com}
3 | * @file 操作标签的api
4 | */
5 |
6 | import Router from 'koa-router'
7 | import {baseApi} from '../config'
8 | import TagController from '../controllers/tags'
9 | import verify from '../middlewares/verify'
10 |
11 | const api = 'tags'
12 |
13 | const router = new Router()
14 |
15 | router.prefix(`/${baseApi}/${api}`)
16 |
17 | router.put('/:id', verify, TagController.updateTag)
18 |
19 | module.exports = router
20 |
--------------------------------------------------------------------------------
/server/routes/tokens.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author {benyuwan@gmail.com}
3 | * @file 操作token的api
4 | */
5 |
6 | import Router from 'koa-router'
7 | import {baseApi} from '../config'
8 | import TokenController from '../controllers/tokens'
9 | import verify from '../middlewares/verify'
10 |
11 | const api = 'tokens'
12 |
13 | const router = new Router()
14 |
15 | router.prefix(`/${baseApi}/${api}`)
16 |
17 | router.post('/', TokenController.createToken)
18 | router.get('/check', verify, TokenController.checkToken)
19 |
20 | module.exports = router
21 |
22 |
--------------------------------------------------------------------------------
/server/sql/ashen_db.sql:
--------------------------------------------------------------------------------
1 | # ************************************************************
2 | # Sequel Pro SQL dump
3 | # Version 4541
4 | #
5 | # http://www.sequelpro.com/
6 | # https://github.com/sequelpro/sequelpro
7 | #
8 | # Host: 127.0.0.1 (MySQL 5.6.35)
9 | # Database: ashen_db
10 | # Generation Time: 2018-01-03 13:21:50 +0000
11 | # ************************************************************
12 |
13 |
14 | /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
15 | /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
16 | /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
17 | /*!40101 SET NAMES utf8 */;
18 | /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
19 | /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
20 | /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
21 |
22 |
23 | # Dump of table ABOUT
24 | # ------------------------------------------------------------
25 |
26 | DROP TABLE IF EXISTS `ABOUT`;
27 |
28 | CREATE TABLE `ABOUT` (
29 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
30 | `content` longtext,
31 | PRIMARY KEY (`id`)
32 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
33 |
34 | LOCK TABLES `ABOUT` WRITE;
35 | /*!40000 ALTER TABLE `ABOUT` DISABLE KEYS */;
36 |
37 | INSERT INTO `ABOUT` (`id`, `content`)
38 | VALUES
39 | (1,'* 独立之精神\n* 自由之思想');
40 |
41 | /*!40000 ALTER TABLE `ABOUT` ENABLE KEYS */;
42 | UNLOCK TABLES;
43 |
44 |
45 | # Dump of table ARTICLE
46 | # ------------------------------------------------------------
47 |
48 | DROP TABLE IF EXISTS `ARTICLE`;
49 |
50 | CREATE TABLE `ARTICLE` (
51 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
52 | `title` varchar(255) DEFAULT '',
53 | `tags` varchar(255) DEFAULT '',
54 | `createTime` datetime NOT NULL,
55 | `publishTime` datetime NOT NULL,
56 | `content` longtext NOT NULL,
57 | `isPublished` tinyint(1) NOT NULL DEFAULT '0',
58 | PRIMARY KEY (`id`)
59 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
60 |
61 | LOCK TABLES `ARTICLE` WRITE;
62 | /*!40000 ALTER TABLE `ARTICLE` DISABLE KEYS */;
63 |
64 | INSERT INTO `ARTICLE` (`id`, `title`, `tags`, `createTime`, `publishTime`, `content`, `isPublished`)
65 | VALUES
66 | (220,'欢迎来到Ashen Blog!','Blog','2018-01-02 16:05:45','2018-01-02 16:07:26','Enjoy ur self here!\n\n\n\n```js\nconsole.log(\'Hello World!\')\n```',1);
67 |
68 | /*!40000 ALTER TABLE `ARTICLE` ENABLE KEYS */;
69 | UNLOCK TABLES;
70 |
71 |
72 | # Dump of table RD_LIST
73 | # ------------------------------------------------------------
74 |
75 | DROP TABLE IF EXISTS `RD_LIST`;
76 |
77 | CREATE TABLE `RD_LIST` (
78 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
79 | `name` varchar(255) NOT NULL DEFAULT '',
80 | `author` varchar(255) NOT NULL DEFAULT '',
81 | `score` float NOT NULL,
82 | PRIMARY KEY (`id`)
83 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
84 |
85 | LOCK TABLES `RD_LIST` WRITE;
86 | /*!40000 ALTER TABLE `RD_LIST` DISABLE KEYS */;
87 |
88 | INSERT INTO `RD_LIST` (`id`, `name`, `author`, `score`)
89 | VALUES
90 | (4,'哈利·波特','J.K.罗琳',5);
91 |
92 | /*!40000 ALTER TABLE `RD_LIST` ENABLE KEYS */;
93 | UNLOCK TABLES;
94 |
95 |
96 | # Dump of table USER
97 | # ------------------------------------------------------------
98 |
99 | DROP TABLE IF EXISTS `USER`;
100 |
101 | CREATE TABLE `USER` (
102 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
103 | `user` varchar(255) NOT NULL DEFAULT '',
104 | `password` varchar(255) NOT NULL DEFAULT '',
105 | PRIMARY KEY (`id`)
106 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
107 |
108 | LOCK TABLES `USER` WRITE;
109 | /*!40000 ALTER TABLE `USER` DISABLE KEYS */;
110 |
111 | INSERT INTO `USER` (`id`, `user`, `password`)
112 | VALUES
113 | (1,'admin','e5d2a815230449badccf00bc67436696');
114 |
115 | /*!40000 ALTER TABLE `USER` ENABLE KEYS */;
116 | UNLOCK TABLES;
117 |
118 |
119 |
120 | /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
121 | /*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
122 | /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
123 | /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
124 | /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
125 | /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
126 |
--------------------------------------------------------------------------------
/server/utils/escape.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author {benyuwan@gmail.com}
3 | * @file 转义输入的字符
4 | */
5 |
6 | import mysql from 'mysql'
7 |
8 | export default function escape(template, ...subs) {
9 | let result = ''
10 | for (let i = 0; i < subs.length; i++) {
11 | result += template[i]
12 | result += mysql.escape(subs[i])
13 | }
14 | result += template[template.length - 1]
15 | return result
16 | }
--------------------------------------------------------------------------------
/server/utils/query.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author {benyuwan@gmail.com}
3 | * @file 初始化数据库并用Promise封装数据库操作
4 | */
5 |
6 | import mysql from 'mysql'
7 | import fs from 'fs'
8 | import path from 'path'
9 | import {db, dbName} from '../config/'
10 |
11 | let pool
12 | const sqlSource = fs.readFileSync(path.resolve(__dirname, '..', './sql/ashen_db.sql'), 'utf-8')
13 | const init = mysql.createConnection(db)
14 |
15 | init.connect()
16 | init.query('CREATE DATABASE ashen_db', err => {
17 | Object.assign(db, dbName)
18 | pool = mysql.createPool(db)
19 | if (err) {
20 | console.log('✅ Ashen Database created already.')
21 | }
22 | else {
23 | console.log('✅ Create Ashen Database')
24 | query(sqlSource).then(res => console.log('✅ Import sql file'))
25 | }
26 | })
27 | init.end()
28 |
29 | export default function query(sql, values) {
30 | return new Promise((resolve, reject) => {
31 | pool.getConnection((err, connection) => {
32 | if (err) {
33 | reject(err)
34 | }
35 | else {
36 | connection.query(sql, values, (err, rows) => {
37 | if (err) {
38 | reject(err)
39 | }
40 | else {
41 | resolve(rows)
42 | }
43 | connection.release()
44 | })
45 | }
46 | })
47 | })
48 | }
49 |
--------------------------------------------------------------------------------
/server/utils/routesLoader.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author {benyuwan@gmail.com}
3 | * @file 遍历加载路由文件
4 | */
5 |
6 | import glob from 'glob'
7 |
8 | export default function (dirname) {
9 | return new Promise((resolve, reject) => {
10 | const routers = []
11 | glob(
12 | `${dirname}/*`,
13 | {ignore: '**/index.js'},
14 | (err, files) => {
15 | if (err) {
16 | reject(err)
17 | }
18 | files.forEach(file => {
19 | const router = require(file)
20 | routers.push(router)
21 | })
22 | resolve(routers)
23 | }
24 | )
25 | })
26 | }
27 |
28 |
--------------------------------------------------------------------------------
/static/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StudentWan/ashen-blog/ba4d3e3a5b47c5ab671fe98fe733eadada45d258/static/.gitkeep
--------------------------------------------------------------------------------