├── .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 | ![client](http://ashenone.cn/blog-client.png) 26 | 客户端界面 27 | 28 | ![admin](http://ashenone.cn/blog-admin.png) 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 | 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 |
<link rel="stylesheet" type="text/css" href="./iconfont.css">
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 |
<script src="./iconfont.js"></script>
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 |
    &#xe772;
    39 |
  • 40 | 41 |
  • 42 | 43 |
    会员 男
    44 |
    &#xe77b;
    45 |
  • 46 | 47 |
  • 48 | 49 |
    全部
    50 |
    &#xe783;
    51 |
  • 52 | 53 |
  • 54 | 55 |
    在线指导
    56 |
    &#xe786;
    57 |
  • 58 | 59 |
  • 60 | 61 |
    退出
    62 |
    &#xe792;
    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">&#x33;</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 | 7 | 8 | Created by iconfont 9 | 10 | 11 | 12 | 13 | 21 | 22 | 23 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 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 | 15 | 16 | 61 | 62 | 74 | -------------------------------------------------------------------------------- /admin/src/components/List.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 58 | 59 | 85 | -------------------------------------------------------------------------------- /admin/src/components/Login.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 55 | 56 | 57 | 119 | 120 | -------------------------------------------------------------------------------- /admin/src/components/ReadingList.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 191 | 192 | 281 | -------------------------------------------------------------------------------- /admin/src/components/Tag.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 98 | 99 | 147 | -------------------------------------------------------------------------------- /admin/src/components/common/ArticleList.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 234 | 235 | 270 | -------------------------------------------------------------------------------- /admin/src/components/common/Editor.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 144 | 145 | 203 | -------------------------------------------------------------------------------- /admin/src/components/common/HeaderNav.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 37 | 38 | 74 | -------------------------------------------------------------------------------- /admin/src/components/common/SideNav.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 37 | 38 | 85 | -------------------------------------------------------------------------------- /admin/src/components/common/Star.vue: -------------------------------------------------------------------------------- 1 | 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 | 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 | 7 | 8 | 32 | 33 | 34 | 47 | -------------------------------------------------------------------------------- /client/src/components/Archive.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 52 | 53 | 54 | 73 | -------------------------------------------------------------------------------- /client/src/components/Article.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 51 | 52 | 58 | -------------------------------------------------------------------------------- /client/src/components/ArticleList.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 93 | 94 | 95 | 133 | 134 | -------------------------------------------------------------------------------- /client/src/components/ReadingList.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 47 | 48 | 49 | 81 | -------------------------------------------------------------------------------- /client/src/components/Tag.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 73 | 74 | 75 | 99 | -------------------------------------------------------------------------------- /client/src/components/common/HeaderNav.vue: -------------------------------------------------------------------------------- 1 | 54 | 55 | 102 | 103 | 208 | -------------------------------------------------------------------------------- /client/src/components/common/Star.vue: -------------------------------------------------------------------------------- 1 | 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 --------------------------------------------------------------------------------