├── .babelrc ├── .dockerignore ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .eslintrc.js ├── .gitignore ├── .postcssrc.js ├── Dockerfile ├── README.md ├── build ├── build.js ├── check-versions.js ├── dev-client.js ├── dev-server.js ├── logo.png ├── 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 ├── package-lock.json ├── package.json ├── server ├── .env ├── .gitignore ├── api │ └── index.js ├── app.js ├── lib │ └── mongo.js ├── logs │ ├── combined.outerr.log │ ├── err.log │ └── out.log ├── package.json ├── pm2.json ├── routes │ ├── articleList.js │ ├── createArticle.js │ ├── createTag.js │ ├── detail.js │ ├── index.js │ ├── login.js │ ├── register.js │ └── user.js └── utils │ ├── checkToken.js │ └── createToken.js ├── src ├── App.vue ├── api │ └── index.js ├── assets │ ├── css │ │ ├── _variables.styl │ │ ├── default.css │ │ ├── default0.css │ │ ├── editor.styl │ │ ├── highlight.css │ │ ├── markdown.styl │ │ ├── preview.styl │ │ └── reset.css │ ├── images │ │ └── logo.png │ ├── img │ │ └── logo.png │ └── js │ │ ├── commen.js │ │ └── marked.js ├── components │ ├── Comment.vue │ ├── Info.vue │ ├── Item.vue │ ├── ProgressBar.vue │ ├── Spinner.vue │ ├── header.vue │ └── sidebar.vue ├── config │ ├── common.config.js │ ├── development.config.js │ ├── index.js │ └── production.config.js ├── main.js ├── router │ ├── index.js │ └── routes.js ├── store │ ├── actions.js │ ├── getters.js │ ├── index.js │ └── mutations.js ├── utils │ ├── filters.js │ ├── http.js │ ├── index.js │ ├── marked.js │ ├── meta.js │ ├── store.js │ └── title.js ├── view │ ├── index │ │ ├── components │ │ │ ├── ArticleListMView.vue │ │ │ ├── ArticleListVIew.vue │ │ │ ├── ArticleTagView.vue │ │ │ ├── BannerListView.vue │ │ │ └── allTagView.vue │ │ └── indexView.vue │ ├── notfound │ │ └── NotFoundView.vue │ └── user │ │ ├── article │ │ ├── create │ │ │ └── IndexView.vue │ │ ├── detail │ │ │ └── IndexView.vue │ │ ├── list │ │ │ └── IndexView.vue │ │ └── tag │ │ │ └── IndexView.vue │ │ ├── center │ │ └── IndexView.vue │ │ ├── changepass │ │ └── IndexView.vue │ │ ├── index │ │ └── IndexView.vue │ │ ├── login │ │ └── IndexView.vue │ │ └── register │ │ └── IndexView.vue └── views │ ├── admin │ ├── components │ │ ├── article.vue │ │ ├── editor.vue │ │ └── tags.vue │ └── index.vue │ ├── front │ ├── ArticleDetail.vue │ ├── ArticleList.vue │ ├── Index.vue │ ├── Mine.vue │ ├── Tags.vue │ ├── TimeLine.vue │ └── components │ │ └── RelateSkill.vue │ └── hello │ └── HelloView.vue ├── static ├── .gitkeep └── img │ ├── 1.png │ ├── 2.png │ ├── 3.png │ ├── 4.png │ ├── 5.png │ ├── 6.png │ ├── 7.png │ └── 8.png └── test ├── e2e ├── custom-assertions │ └── elementCount.js ├── nightwatch.conf.js ├── runner.js └── specs │ └── test.js └── unit ├── .eslintrc ├── jest.conf.js ├── setup.js └── specs └── HelloWorld.spec.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "modules": false, 5 | "targets": { 6 | "browsers": ["> 1%", "last 2 versions", "not ie <= 8"] 7 | } 8 | }], 9 | "stage-2" 10 | ], 11 | "plugins": ["transform-vue-jsx", "transform-runtime"], 12 | "env": { 13 | "test": { 14 | "presets": ["env", "stage-2"], 15 | "plugins": ["transform-vue-jsx", "transform-es2015-modules-commonjs", "dynamic-import-node"] 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.dockerignore : -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hbbaly/vue-blog/b624636e9ca48174e0bb49ef28d87c3e26be8a60/.dockerignore -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | /config/ 3 | /dist/ 4 | /*.js 5 | /test/unit/coverage/ 6 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "vue-eslint-parser", 3 | "parserOptions": { 4 | "ecmaVersion": 8, 5 | "sourceType": "module", 6 | "allowImportExportEverywhere": false 7 | }, 8 | "extends": [ 9 | "standard", 10 | "plugin:vue/recommended" 11 | ], 12 | "plugins": [ 13 | "jsdoc" 14 | ], 15 | "globals": { 16 | "vue": true 17 | }, 18 | "env": { 19 | "browser": true, 20 | "es6": true 21 | }, 22 | "rules": { 23 | "no-var": 2, 24 | "one-var": 1, 25 | "block-scoped-var": 0, 26 | "no-alert": 0, 27 | "no-console": 0, 28 | "no-const-assign": 2, 29 | "no-dupe-keys": 2, 30 | "no-dupe-args": 2, 31 | "no-eval": 1, 32 | "no-irregular-whitespace": 2, 33 | "no-script-url": 0, 34 | "eqeqeq": 2, 35 | "indent": [2, 2], 36 | "prefer-const": 0, 37 | "prefer-spread": 0, 38 | "space-after-keywords": [0, "always"] 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // https://eslint.org/docs/user-guide/configuring 2 | 3 | module.exports = { 4 | root: true, 5 | parserOptions: { 6 | parser: 'babel-eslint' 7 | }, 8 | env: { 9 | browser: true, 10 | }, 11 | extends: [ 12 | // https://github.com/vuejs/eslint-plugin-vue#priority-a-essential-error-prevention 13 | // consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules. 14 | 'plugin:vue/essential', 15 | // https://github.com/standard/standard/blob/master/docs/RULES-en.md 16 | 'standard' 17 | ], 18 | // required to lint *.vue files 19 | plugins: [ 20 | 'vue' 21 | ], 22 | // add your custom rules here 23 | rules: { 24 | // allow async-await 25 | 'generator-star-spacing': 'off', 26 | // allow debugger during development 27 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off' 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | /dist/ 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | /test/unit/coverage/ 8 | /test/e2e/reports/ 9 | selenium-debug.log 10 | 11 | # Editor directories and files 12 | .idea 13 | .vscode 14 | *.suo 15 | *.ntvs* 16 | *.njsproj 17 | *.sln 18 | -------------------------------------------------------------------------------- /.postcssrc.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | "plugins": { 5 | "postcss-import": {}, 6 | "postcss-url": {}, 7 | // to edit target browsers: use "browserslist" field in package.json 8 | "autoprefixer": {} 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # /usr/src/nodejs/hello-docker/Dockerfile 2 | FROM node:10.0 3 | 4 | # 在容器中创建一个目录 5 | RUN mkdir -p /usr/src/nodejs/ 6 | 7 | # 定位到容器的工作目录 8 | WORKDIR /usr/src/nodejs/ 9 | 10 | # RUN/COPY 是分层的,package.json 提前,只要没修改,就不会重新安装包 11 | COPY package.json /usr/src/app/package.json 12 | RUN cd /usr/src/app/ 13 | RUN npm i 14 | 15 | # 把当前目录下的所有文件拷贝到 Image 的 /usr/src/nodejs/ 目录下 16 | COPY . /usr/src/nodejs/ 17 | 18 | 19 | EXPOSE 3000 20 | CMD npm start -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-blog 2 | 3 | >Vue2 + Vue-router + Axios + Vuex + Node + Express + MongoDB + Webpack 搭建博客 4 | 5 | blog样式已改进 6 | 7 | ## Run Setup 8 | 9 | 注意:先启动MongoDB 10 | 11 | ``` bash 12 | cd server 13 | npm i 14 | pm2 start pm2.json (启动服务端) 15 | ``` 16 | 17 | ## Build Setup 18 | 19 | ``` bash 20 | # install dependencies(安装依赖) 21 | npm install 22 | 23 | # serve with hot reload at localhost:8080(试运行) 24 | npm run dev 25 | 26 | # build for production with minification(项目构建) 27 | npm run build 28 | 29 | # build for production and view the bundle analyzer report(打包分析报告) 30 | npm run build --report 31 | ``` 32 | ## 已完成的功能 33 | - 首页轮播设置及获取 34 | - 文章列表分页获取 35 | - 文章详情 36 | - 登陆 37 | - 注册 38 | - 修改密码 39 | - 编辑文章 40 | - 创建文章 41 | - 创建标签 42 | - 修改标签 43 | - 删除标签 44 | 45 | ## 未完成功能 46 | - 使用vuex管理数据 47 | - 需要登陆状态的页面需要路由守卫 48 | - 删除某篇文章 49 | - 标签搜索文章页面 50 | - 首页完善各个页面入口 51 | - 待添加...... 52 | 53 | 54 | ## 目录结构 55 | 56 | ``` 57 | │ .babelrc 58 | │ .eslintignore 59 | │ .eslintrc.js 60 | │ .gitignore 61 | │ index.html 62 | │ package.json 63 | │ README.md 64 | │ 65 | ├─build 66 | │ build.js 67 | │ check-versions.js 68 | │ dev-client.js 69 | │ dev-server.js 70 | │ utils.js 71 | │ webpack.base.conf.js 72 | │ webpack.dev.conf.js 73 | │ webpack.prod.conf.js 74 | │ 75 | ├─config 76 | │ dev.env.js 77 | │ index.js 78 | │ prod.env.js 79 | │ 80 | ├─server 后端文件夹 81 | │ │ .env 82 | │ │ app.js 后端入口 83 | │ │ 84 | │ ├─api 85 | │ │ index.js 后端api 86 | │ │ 87 | │ ├─lib 88 | │ │ mongo.js 数据库 89 | │ │ 90 | │ ├─utils 91 | │ │ checkToken.js 92 | │ │ createToken.js 93 | │ │ 94 | │ └─routes 后端路由 95 | │ articleList.js 96 | │ createArticle.js 97 | │ createTag.js 98 | │ detail.js 99 | │ index.js 100 | │ login.js 101 | | register.js 102 | | user.js 103 | │ 104 | ├─src 前端文件夹 105 | │ │ App.vue 106 | │ │ main.js 前端入口 107 | │ │ 108 | │ ├─api 前端api 109 | │ │ index.js 110 | │ │ 111 | │ ├─assets 112 | │ │ ├─css 113 | │ │ │ reset.css 114 | │ │ │ default.css 115 | │ │ │ default0.css 116 | │ │ │ highlight.css 117 | │ │ │ 118 | │ │ ├─img 119 | │ │ │ 120 | │ │ └─js 121 | │ │ highlight.pack.js 122 | │ │ hljs.js 123 | │ │ 124 | │ ├─components 125 | │ │ │ info.vue 126 | │ │ 127 | │ |─config 前端请求配置 128 | │ │ common.config.js 129 | │ │ development.config.js 130 | │ │ index.js.vue 131 | │ │ production.config.js 132 | │ |─filter 过滤 133 | │ │ 134 | | | 135 | │ |─mixins 混入 136 | │ │ 137 | │ ├─routes 前端路由vue-router 138 | │ │ index.js 139 | │ │ routes.js 140 | │ │ 141 | │ └─store 前端vuex 142 | │ │ 143 | │ │ 144 | │ │ 145 | │ └─utils 公用函数 146 | │ │ http 147 | │ │ index 148 | │ │ store 149 | │ │ 150 | │ └─view 前端页面 151 | │ │ ├─index 首页 152 | │ │ ├─notfound 404页面 153 | │ │ ├─user 个人中心相关页面 154 | │ │ │ ├─article 155 | │ │ │ │ create 156 | │ │ │ │ detail 157 | │ │ │ │ list 158 | │ │ │ │ tag 159 | │ │ │ ├─center 160 | │ │ │ ├─changepass 161 | │ │ │ ├─index 162 | │ │ │ ├─login 163 | │ │ │ ├─register 164 | ``` 165 | 166 | ## 功能展示展示 167 | **没有太过在意样式,后期计划会进行样式大修改** 168 | ### 首页 169 | http://localhost:8080/ 170 | 171 | 172 | ### 创建文章 173 | http://localhost:8080/article/create 174 | 175 | 176 | ### 文章列表 177 | http://localhost:8080/article/list 178 | 179 | 180 | ### 标签 181 | http://localhost:8080/article/tag 182 | 183 | ### 文章详情 184 | http://localhost:8080/user/article/detail/${_id} 185 | 186 | ### 登陆 187 | http://localhost:8080/article/tag 188 | 189 | ### 注册 190 | http://localhost:8080/article/tag 191 | 192 | 193 | 194 | For a detailed explanation on how things work, check out the [guide](http://vuejs-templates.github.io/webpack/) and [docs for vue-loader](http://vuejs.github.io/vue-loader). 195 | more info, contact with 1422699902@qq.com 196 | -------------------------------------------------------------------------------- /build/build.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | require('./check-versions')() 3 | 4 | process.env.NODE_ENV = 'production' 5 | 6 | const ora = require('ora') 7 | const rm = require('rimraf') 8 | const path = require('path') 9 | const chalk = require('chalk') 10 | const webpack = require('webpack') 11 | const config = require('../config') 12 | const webpackConfig = require('./webpack.prod.conf') 13 | 14 | const spinner = ora('building for production...') 15 | spinner.start() 16 | 17 | rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => { 18 | if (err) throw err 19 | webpack(webpackConfig, (err, stats) => { 20 | spinner.stop() 21 | if (err) throw err 22 | process.stdout.write(stats.toString({ 23 | colors: true, 24 | modules: false, 25 | children: false, // If you are using ts-loader, setting this to true will make TypeScript errors show up during build. 26 | chunks: false, 27 | chunkModules: false 28 | }) + '\n\n') 29 | 30 | if (stats.hasErrors()) { 31 | console.log(chalk.red(' Build failed with errors.\n')) 32 | process.exit(1) 33 | } 34 | 35 | console.log(chalk.cyan(' Build complete.\n')) 36 | console.log(chalk.yellow( 37 | ' Tip: built files are meant to be served over an HTTP server.\n' + 38 | ' Opening index.html over file:// won\'t work.\n' 39 | )) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /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 | 29 | for (let i = 0; i < versionRequirements.length; i++) { 30 | const mod = versionRequirements[i] 31 | 32 | if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) { 33 | warnings.push(mod.name + ': ' + 34 | chalk.red(mod.currentVersion) + ' should be ' + 35 | chalk.green(mod.versionRequirement) 36 | ) 37 | } 38 | } 39 | 40 | if (warnings.length) { 41 | console.log('') 42 | console.log(chalk.yellow('To use this template, you must update following to modules:')) 43 | console.log() 44 | for (let i = 0; i < warnings.length; i++) { 45 | const warning = warnings[i] 46 | console.log(' ' + warning) 47 | } 48 | console.log() 49 | process.exit(1) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /build/dev-client.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 'use strict' 3 | require('eventsource-polyfill') 4 | var 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 | -------------------------------------------------------------------------------- /build/dev-server.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | require('./check-versions')() 3 | 4 | const config = require('../config') 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 | 26 | /*// json数据接口 27 | const router = express.Router(); 28 | const goodsData = require('./../mock/goods.json'); 29 | router.get("/goods", function (req, res, next) { 30 | res.json(goodsData) 31 | }) 32 | app.use(router);*/ 33 | 34 | const compiler = webpack(webpackConfig) 35 | 36 | const devMiddleware = require('webpack-dev-middleware')(compiler, { 37 | publicPath: webpackConfig.output.publicPath, 38 | quiet: true 39 | }) 40 | 41 | const hotMiddleware = require('webpack-hot-middleware')(compiler, { 42 | log: false, 43 | heartbeat: 2000 44 | }) 45 | // force page reload when html-webpack-plugin template changes 46 | // currently disabled until this is resolved: 47 | // https://github.com/jantimon/html-webpack-plugin/issues/680 48 | // compiler.plugin('compilation', function (compilation) { 49 | // compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) { 50 | // hotMiddleware.publish({ action: 'reload' }) 51 | // cb() 52 | // }) 53 | // }) 54 | 55 | // enable hot-reload and state-preserving 56 | // compilation error display 57 | app.use(hotMiddleware) 58 | 59 | // proxy api requests 60 | Object.keys(proxyTable).forEach(function (context) { 61 | const options = proxyTable[context] 62 | if (typeof options === 'string') { 63 | options = { target: options } 64 | } 65 | app.use(proxyMiddleware(options.filter || context, options)) 66 | }) 67 | 68 | // handle fallback for HTML5 history API 69 | app.use(require('connect-history-api-fallback')()) 70 | 71 | // serve webpack bundle output 72 | app.use(devMiddleware) 73 | 74 | // serve pure static assets 75 | const staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory) 76 | app.use(staticPath, express.static('./static')) 77 | 78 | const uri = 'http://localhost:' + port 79 | 80 | var _resolve 81 | var _reject 82 | var readyPromise = new Promise((resolve, reject) => { 83 | _resolve = resolve 84 | _reject = reject 85 | }) 86 | 87 | var server 88 | var portfinder = require('portfinder') 89 | portfinder.basePort = port 90 | 91 | console.log('> Starting dev server...') 92 | devMiddleware.waitUntilValid(() => { 93 | portfinder.getPort((err, port) => { 94 | if (err) { 95 | _reject(err) 96 | } 97 | process.env.PORT = port 98 | var uri = 'http://localhost:' + port 99 | console.log('> Listening at ' + uri + '\n') 100 | // when env is testing, don't need open it 101 | if (autoOpenBrowser && process.env.NODE_ENV !== 'testing') { 102 | opn(uri) 103 | } 104 | server = app.listen(port) 105 | _resolve() 106 | }) 107 | }) 108 | 109 | module.exports = { 110 | ready: readyPromise, 111 | close: () => { 112 | server.close() 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /build/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hbbaly/vue-blog/b624636e9ca48174e0bb49ef28d87c3e26be8a60/build/logo.png -------------------------------------------------------------------------------- /build/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const config = require('../config') 4 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 5 | const packageConfig = require('../package.json') 6 | 7 | exports.assetsPath = function (_path) { 8 | const assetsSubDirectory = process.env.NODE_ENV === 'production' 9 | ? config.build.assetsSubDirectory 10 | : config.dev.assetsSubDirectory 11 | return path.posix.join(assetsSubDirectory, _path) 12 | } 13 | 14 | exports.cssLoaders = function (options) { 15 | options = options || {} 16 | 17 | const cssLoader = { 18 | loader: 'css-loader', 19 | options: { 20 | sourceMap: options.sourceMap 21 | } 22 | } 23 | 24 | const postcssLoader = { 25 | loader: 'postcss-loader', 26 | options: { 27 | sourceMap: options.sourceMap 28 | } 29 | } 30 | 31 | // generate loader string to be used with extract text plugin 32 | function generateLoaders (loader, loaderOptions) { 33 | const loaders = options.usePostCSS ? [cssLoader, postcssLoader] : [cssLoader] 34 | 35 | if (loader) { 36 | loaders.push({ 37 | loader: loader + '-loader', 38 | options: Object.assign({}, loaderOptions, { 39 | sourceMap: options.sourceMap 40 | }) 41 | }) 42 | } 43 | 44 | // Extract CSS when that option is specified 45 | // (which is the case during production build) 46 | if (options.extract) { 47 | return ExtractTextPlugin.extract({ 48 | use: loaders, 49 | fallback: 'vue-style-loader' 50 | }) 51 | } else { 52 | return ['vue-style-loader'].concat(loaders) 53 | } 54 | } 55 | 56 | // https://vue-loader.vuejs.org/en/configurations/extract-css.html 57 | return { 58 | css: generateLoaders(), 59 | postcss: generateLoaders(), 60 | less: generateLoaders('less'), 61 | sass: generateLoaders('sass', { indentedSyntax: true }), 62 | scss: generateLoaders('sass'), 63 | stylus: generateLoaders('stylus'), 64 | styl: generateLoaders('stylus') 65 | } 66 | } 67 | 68 | // Generate loaders for standalone style files (outside of .vue) 69 | exports.styleLoaders = function (options) { 70 | const output = [] 71 | const loaders = exports.cssLoaders(options) 72 | for (const extension in loaders) { 73 | const loader = loaders[extension] 74 | output.push({ 75 | test: new RegExp('\\.' + extension + '$'), 76 | use: loader 77 | }) 78 | } 79 | 80 | return output 81 | } 82 | 83 | exports.createNotifierCallback = () => { 84 | const notifier = require('node-notifier') 85 | 86 | return (severity, errors) => { 87 | if (severity !== 'error') return 88 | 89 | const error = errors[0] 90 | const filename = error.file && error.file.split('!').pop() 91 | 92 | notifier.notify({ 93 | title: packageConfig.name, 94 | message: severity + ': ' + error.name, 95 | subtitle: filename || '', 96 | icon: path.join(__dirname, 'logo.png') 97 | }) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /build/vue-loader.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const utils = require('./utils') 3 | const config = require('../config') 4 | const isProduction = process.env.NODE_ENV === 'production' 5 | const sourceMapEnabled = isProduction 6 | ? config.build.productionSourceMap 7 | : config.dev.cssSourceMap 8 | 9 | module.exports = { 10 | loaders: utils.cssLoaders({ 11 | sourceMap: sourceMapEnabled, 12 | extract: isProduction 13 | }), 14 | cssSourceMap: sourceMapEnabled, 15 | cacheBusting: config.dev.cacheBusting, 16 | transformToRequire: { 17 | video: ['src', 'poster'], 18 | source: 'src', 19 | img: 'src', 20 | image: 'xlink:href' 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /build/webpack.base.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const utils = require('./utils') 4 | const config = require('../config') 5 | const vueLoaderConfig = require('./vue-loader.conf') 6 | 7 | function resolve (dir) { 8 | return path.join(__dirname, '..', dir) 9 | } 10 | 11 | const createLintingRule = () => ({ 12 | // test: /\.(js|vue)$/, 13 | // loader: 'eslint-loader', 14 | // enforce: 'pre', 15 | // include: [resolve('src'), resolve('test')], 16 | // options: { 17 | // formatter: require('eslint-friendly-formatter'), 18 | // emitWarning: !config.dev.showEslintErrorsInOverlay 19 | // } 20 | }) 21 | 22 | module.exports = { 23 | context: path.resolve(__dirname, '../'), 24 | entry: { 25 | app: './src/main.js' 26 | }, 27 | output: { 28 | path: config.build.assetsRoot, 29 | filename: '[name].js', 30 | publicPath: process.env.NODE_ENV === 'production' 31 | ? config.build.assetsPublicPath 32 | : config.dev.assetsPublicPath 33 | }, 34 | resolve: { 35 | extensions: ['.js', '.vue', '.json'], 36 | alias: { 37 | 'vue$': 'vue/dist/vue.esm.js', 38 | '@': resolve('src'), 39 | } 40 | }, 41 | module: { 42 | rules: [ 43 | ...(config.dev.useEslint ? [createLintingRule()] : []), 44 | { 45 | test: /\.vue$/, 46 | loader: 'vue-loader', 47 | options: vueLoaderConfig 48 | }, 49 | { 50 | test: /\.js$/, 51 | loader: 'babel-loader', 52 | include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')] 53 | }, 54 | { 55 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 56 | loader: 'url-loader', 57 | options: { 58 | limit: 10000, 59 | name: utils.assetsPath('img/[name].[hash:7].[ext]') 60 | } 61 | }, 62 | { 63 | test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, 64 | loader: 'url-loader', 65 | options: { 66 | limit: 10000, 67 | name: utils.assetsPath('media/[name].[hash:7].[ext]') 68 | } 69 | }, 70 | { 71 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 72 | loader: 'url-loader', 73 | options: { 74 | limit: 10000, 75 | name: utils.assetsPath('fonts/[name].[hash:7].[ext]') 76 | } 77 | } 78 | ] 79 | }, 80 | node: { 81 | // prevent webpack from injecting useless setImmediate polyfill because Vue 82 | // source contains it (although only uses it if it's native). 83 | setImmediate: false, 84 | // prevent webpack from injecting mocks to Node native modules 85 | // that does not make sense for the client 86 | dgram: 'empty', 87 | fs: 'empty', 88 | net: 'empty', 89 | tls: 'empty', 90 | child_process: 'empty' 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /build/webpack.dev.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const utils = require('./utils') 3 | const webpack = require('webpack') 4 | const config = require('../config') 5 | const merge = require('webpack-merge') 6 | const path = require('path') 7 | const baseWebpackConfig = require('./webpack.base.conf') 8 | const CopyWebpackPlugin = require('copy-webpack-plugin') 9 | const HtmlWebpackPlugin = require('html-webpack-plugin') 10 | const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') 11 | const portfinder = require('portfinder') 12 | 13 | const HOST = process.env.HOST 14 | const PORT = process.env.PORT && Number(process.env.PORT) 15 | 16 | const devWebpackConfig = merge(baseWebpackConfig, { 17 | module: { 18 | rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true }) 19 | }, 20 | // cheap-module-eval-source-map is faster for development 21 | devtool: config.dev.devtool, 22 | 23 | // these devServer options should be customized in /config/index.js 24 | devServer: { 25 | clientLogLevel: 'warning', 26 | historyApiFallback: { 27 | rewrites: [ 28 | { from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') }, 29 | ], 30 | }, 31 | hot: true, 32 | contentBase: false, // since we use CopyWebpackPlugin. 33 | compress: true, 34 | host: HOST || config.dev.host, 35 | port: PORT || config.dev.port, 36 | open: config.dev.autoOpenBrowser, 37 | overlay: config.dev.errorOverlay 38 | ? { warnings: false, errors: true } 39 | : false, 40 | publicPath: config.dev.assetsPublicPath, 41 | proxy: config.dev.proxyTable, 42 | quiet: true, // necessary for FriendlyErrorsPlugin 43 | watchOptions: { 44 | poll: config.dev.poll, 45 | } 46 | }, 47 | plugins: [ 48 | new webpack.DefinePlugin({ 49 | 'process.env': require('../config/dev.env') 50 | }), 51 | new webpack.HotModuleReplacementPlugin(), 52 | new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update. 53 | new webpack.NoEmitOnErrorsPlugin(), 54 | // https://github.com/ampedandwired/html-webpack-plugin 55 | new HtmlWebpackPlugin({ 56 | filename: 'index.html', 57 | template: 'index.html', 58 | inject: true 59 | }), 60 | // copy custom static assets 61 | new CopyWebpackPlugin([ 62 | { 63 | from: path.resolve(__dirname, '../static'), 64 | to: config.dev.assetsSubDirectory, 65 | ignore: ['.*'] 66 | } 67 | ]) 68 | ] 69 | }) 70 | 71 | module.exports = new Promise((resolve, reject) => { 72 | portfinder.basePort = process.env.PORT || config.dev.port 73 | portfinder.getPort((err, port) => { 74 | if (err) { 75 | reject(err) 76 | } else { 77 | // publish the new Port, necessary for e2e tests 78 | process.env.PORT = port 79 | // add port to devServer config 80 | devWebpackConfig.devServer.port = port 81 | 82 | // Add FriendlyErrorsPlugin 83 | devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({ 84 | compilationSuccessInfo: { 85 | messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`], 86 | }, 87 | onErrors: config.dev.notifyOnErrors 88 | ? utils.createNotifierCallback() 89 | : undefined 90 | })) 91 | 92 | resolve(devWebpackConfig) 93 | } 94 | }) 95 | }) 96 | -------------------------------------------------------------------------------- /build/webpack.prod.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const utils = require('./utils') 4 | const webpack = require('webpack') 5 | const config = require('../config') 6 | const merge = require('webpack-merge') 7 | const baseWebpackConfig = require('./webpack.base.conf') 8 | const CopyWebpackPlugin = require('copy-webpack-plugin') 9 | const HtmlWebpackPlugin = require('html-webpack-plugin') 10 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 11 | const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin') 12 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin') 13 | 14 | const env = process.env.NODE_ENV === 'testing' 15 | ? require('../config/test.env') 16 | : require('../config/prod.env') 17 | 18 | const webpackConfig = merge(baseWebpackConfig, { 19 | module: { 20 | rules: utils.styleLoaders({ 21 | sourceMap: config.build.productionSourceMap, 22 | extract: true, 23 | usePostCSS: true 24 | }) 25 | }, 26 | devtool: config.build.productionSourceMap ? config.build.devtool : false, 27 | output: { 28 | path: config.build.assetsRoot, 29 | filename: utils.assetsPath('js/[name].[chunkhash].js'), 30 | chunkFilename: utils.assetsPath('js/[id].[chunkhash].js') 31 | }, 32 | plugins: [ 33 | // http://vuejs.github.io/vue-loader/en/workflow/production.html 34 | new webpack.DefinePlugin({ 35 | 'process.env': env 36 | }), 37 | new UglifyJsPlugin({ 38 | uglifyOptions: { 39 | compress: { 40 | warnings: false 41 | } 42 | }, 43 | sourceMap: config.build.productionSourceMap, 44 | parallel: true 45 | }), 46 | // extract css into its own file 47 | new ExtractTextPlugin({ 48 | filename: utils.assetsPath('css/[name].[contenthash].css'), 49 | // Setting the following option to `false` will not extract CSS from codesplit chunks. 50 | // Their CSS will instead be inserted dynamically with style-loader when the codesplit chunk has been loaded by webpack. 51 | // It's currently set to `true` because we are seeing that sourcemaps are included in the codesplit bundle as well when it's `false`, 52 | // increasing file size: https://github.com/vuejs-templates/webpack/issues/1110 53 | allChunks: true, 54 | }), 55 | // Compress extracted CSS. We are using this plugin so that possible 56 | // duplicated CSS from different components can be deduped. 57 | new OptimizeCSSPlugin({ 58 | cssProcessorOptions: config.build.productionSourceMap 59 | ? { safe: true, map: { inline: false } } 60 | : { safe: true } 61 | }), 62 | // generate dist index.html with correct asset hash for caching. 63 | // you can customize output by editing /index.html 64 | // see https://github.com/ampedandwired/html-webpack-plugin 65 | new HtmlWebpackPlugin({ 66 | filename: process.env.NODE_ENV === 'testing' 67 | ? 'index.html' 68 | : config.build.index, 69 | template: 'index.html', 70 | inject: true, 71 | minify: { 72 | removeComments: true, 73 | collapseWhitespace: true, 74 | removeAttributeQuotes: true 75 | // more options: 76 | // https://github.com/kangax/html-minifier#options-quick-reference 77 | }, 78 | // necessary to consistently work with multiple chunks via CommonsChunkPlugin 79 | chunksSortMode: 'dependency' 80 | }), 81 | // keep module.id stable when vendor modules does not change 82 | new webpack.HashedModuleIdsPlugin(), 83 | // enable scope hoisting 84 | new webpack.optimize.ModuleConcatenationPlugin(), 85 | // split vendor js into its own file 86 | new webpack.optimize.CommonsChunkPlugin({ 87 | name: 'vendor', 88 | minChunks (module) { 89 | // any required modules inside node_modules are extracted to vendor 90 | return ( 91 | module.resource && 92 | /\.js$/.test(module.resource) && 93 | module.resource.indexOf( 94 | path.join(__dirname, '../node_modules') 95 | ) === 0 96 | ) 97 | } 98 | }), 99 | // extract webpack runtime and module manifest to its own file in order to 100 | // prevent vendor hash from being updated whenever app bundle is updated 101 | new webpack.optimize.CommonsChunkPlugin({ 102 | name: 'manifest', 103 | minChunks: Infinity 104 | }), 105 | // This instance extracts shared chunks from code splitted chunks and bundles them 106 | // in a separate chunk, similar to the vendor chunk 107 | // see: https://webpack.js.org/plugins/commons-chunk-plugin/#extra-async-commons-chunk 108 | new webpack.optimize.CommonsChunkPlugin({ 109 | name: 'app', 110 | async: 'vendor-async', 111 | children: true, 112 | minChunks: 3 113 | }), 114 | 115 | // copy custom static assets 116 | new CopyWebpackPlugin([ 117 | { 118 | from: path.resolve(__dirname, '../static'), 119 | to: config.build.assetsSubDirectory, 120 | ignore: ['.*'] 121 | } 122 | ]) 123 | ] 124 | }) 125 | 126 | if (config.build.productionGzip) { 127 | const CompressionWebpackPlugin = require('compression-webpack-plugin') 128 | 129 | webpackConfig.plugins.push( 130 | new CompressionWebpackPlugin({ 131 | asset: '[path].gz[query]', 132 | algorithm: 'gzip', 133 | test: new RegExp( 134 | '\\.(' + 135 | config.build.productionGzipExtensions.join('|') + 136 | ')$' 137 | ), 138 | threshold: 10240, 139 | minRatio: 0.8 140 | }) 141 | ) 142 | } 143 | 144 | if (config.build.bundleAnalyzerReport) { 145 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin 146 | webpackConfig.plugins.push(new BundleAnalyzerPlugin()) 147 | } 148 | 149 | module.exports = webpackConfig 150 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | // Template version: 1.3.1 3 | // see http://vuejs-templates.github.io/webpack for documentation. 4 | 5 | const path = require('path') 6 | 7 | module.exports = { 8 | dev: { 9 | 10 | // Paths 11 | assetsSubDirectory: 'static', 12 | assetsPublicPath: '/', 13 | proxyTable: { 14 | '/api':{ 15 | target:'http://127.0.0.1:3010/api', 16 | changeOrigin:true, 17 | pathRewrite:{ 18 | '^/api':'' 19 | } 20 | } 21 | }, 22 | 23 | // Various Dev Server settings 24 | host: 'localhost', // can be overwritten by process.env.HOST 25 | env: require('./dev.env'), 26 | port: process.env.PORT || 8080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined 27 | autoOpenBrowser: false, 28 | errorOverlay: true, 29 | notifyOnErrors: true, 30 | poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions- 31 | 32 | // Use Eslint Loader? 33 | // If true, your code will be linted during bundling and 34 | // linting errors and warnings will be shown in the console. 35 | useEslint: true, 36 | // If true, eslint errors and warnings will also be shown in the error overlay 37 | // in the browser. 38 | showEslintErrorsInOverlay: false, 39 | 40 | /** 41 | * Source Maps 42 | */ 43 | 44 | // https://webpack.js.org/configuration/devtool/#development 45 | devtool: 'cheap-module-eval-source-map', 46 | 47 | // If you have problems debugging vue-files in devtools, 48 | // set this to false - it *may* help 49 | // https://vue-loader.vuejs.org/en/options.html#cachebusting 50 | cacheBusting: true, 51 | 52 | cssSourceMap: true 53 | }, 54 | 55 | build: { 56 | // Template for index.html 57 | index: path.resolve(__dirname, '../dist/index.html'), 58 | 59 | // Paths 60 | assetsRoot: path.resolve(__dirname, '../dist'), 61 | assetsSubDirectory: 'static', 62 | assetsPublicPath: '/', 63 | 64 | /** 65 | * Source Maps 66 | */ 67 | 68 | productionSourceMap: true, 69 | // https://webpack.js.org/configuration/devtool/#production 70 | devtool: '#source-map', 71 | 72 | // Gzip off by default as many popular static hosts such as 73 | // Surge or Netlify already gzip all static assets for you. 74 | // Before setting to `true`, make sure to: 75 | // npm install --save-dev compression-webpack-plugin 76 | productionGzip: false, 77 | productionGzipExtensions: ['js', 'css'], 78 | // Run the build command with an extra argument to 79 | // View the bundle analyzer report after build finishes: 80 | // `npm run build --report` 81 | // Set to `true` or `false` to always turn it on or off 82 | bundleAnalyzerReport: process.env.npm_config_report 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /config/prod.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = { 3 | NODE_ENV: '"production"' 4 | } 5 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | vue-blog 6 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spa", 3 | "version": "1.0.0", 4 | "description": "blog spa", 5 | "author": "hbbaly <1422699902@qq.com>", 6 | "private": true, 7 | "scripts": { 8 | "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js", 9 | "start": "npm run dev", 10 | "unit": "jest --config test/unit/jest.conf.js --coverage", 11 | "e2e": "node test/e2e/runner.js", 12 | "test": "npm run unit && npm run e2e", 13 | "lint": "eslint --ext .js,.vue src test/unit test/e2e/specs", 14 | "build": "node build/build.js" 15 | }, 16 | "dependencies": { 17 | "axios": "^0.18.0", 18 | "element-ui": "^2.12.0", 19 | "highlight.js": "^9.16.2", 20 | "marked": "^0.7.0", 21 | "qs": "^6.9.1", 22 | "simplemde": "^1.11.2", 23 | "vue": "^2.5.2", 24 | "vue-router": "^3.0.1", 25 | "vuex": "^3.0.1" 26 | }, 27 | "devDependencies": { 28 | "autoprefixer": "^7.1.2", 29 | "babel-core": "^6.22.1", 30 | "babel-eslint": "^8.2.1", 31 | "babel-helper-vue-jsx-merge-props": "^2.0.3", 32 | "babel-jest": "^21.0.2", 33 | "babel-loader": "^7.1.1", 34 | "babel-plugin-dynamic-import-node": "^1.2.0", 35 | "babel-plugin-syntax-jsx": "^6.18.0", 36 | "babel-plugin-transform-es2015-modules-commonjs": "^6.26.0", 37 | "babel-plugin-transform-runtime": "^6.22.0", 38 | "babel-plugin-transform-vue-jsx": "^3.5.0", 39 | "babel-preset-env": "^1.3.2", 40 | "babel-preset-stage-2": "^6.22.0", 41 | "babel-register": "^6.22.0", 42 | "chalk": "^2.0.1", 43 | "chromedriver": "^2.27.2", 44 | "copy-webpack-plugin": "^4.0.1", 45 | "cross-spawn": "^5.0.1", 46 | "css-loader": "^0.28.0", 47 | "eslint": "^4.15.0", 48 | "eslint-config-standard": "^10.2.1", 49 | "eslint-friendly-formatter": "^3.0.0", 50 | "eslint-loader": "^1.7.1", 51 | "eslint-plugin-import": "^2.7.0", 52 | "eslint-plugin-node": "^5.2.0", 53 | "eslint-plugin-promise": "^3.4.0", 54 | "eslint-plugin-standard": "^3.0.1", 55 | "eslint-plugin-vue": "^4.0.0", 56 | "extract-text-webpack-plugin": "^3.0.0", 57 | "file-loader": "^1.1.4", 58 | "friendly-errors-webpack-plugin": "^1.6.1", 59 | "html-webpack-plugin": "^2.30.1", 60 | "jest": "^22.0.4", 61 | "jest-serializer-vue": "^0.3.0", 62 | "nightwatch": "^0.9.12", 63 | "node-notifier": "^5.1.2", 64 | "optimize-css-assets-webpack-plugin": "^3.2.0", 65 | "ora": "^1.2.0", 66 | "portfinder": "^1.0.13", 67 | "postcss-import": "^11.0.0", 68 | "postcss-loader": "^2.0.8", 69 | "postcss-url": "^7.2.1", 70 | "rimraf": "^2.6.0", 71 | "selenium-server": "^3.0.1", 72 | "semver": "^5.3.0", 73 | "shelljs": "^0.7.6", 74 | "stylus": "^0.54.5", 75 | "stylus-loader": "^3.0.1", 76 | "uglifyjs-webpack-plugin": "^1.1.1", 77 | "url-loader": "^0.5.8", 78 | "vue-jest": "^1.0.2", 79 | "vue-loader": "^13.3.0", 80 | "vue-style-loader": "^3.0.1", 81 | "vue-template-compiler": "^2.5.2", 82 | "webpack": "^3.6.0", 83 | "webpack-bundle-analyzer": "^2.9.0", 84 | "webpack-dev-server": "^2.9.1", 85 | "webpack-merge": "^4.1.0" 86 | }, 87 | "engines": { 88 | "node": ">= 6.0.0", 89 | "npm": ">= 3.0.0" 90 | }, 91 | "browserslist": [ 92 | "> 1%", 93 | "last 2 versions", 94 | "not ie <= 8" 95 | ] 96 | } 97 | -------------------------------------------------------------------------------- /server/.env: -------------------------------------------------------------------------------- 1 | JWT_SECRET = hbb 2 | -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | node_modules 3 | coverage 4 | logs -------------------------------------------------------------------------------- /server/api/index.js: -------------------------------------------------------------------------------- 1 | const {Article, SetBannerList, User, Classify} = require('../lib/mongo') 2 | 3 | module.exports = { 4 | setBanner: (params) => { 5 | return SetBannerList.create(params).exec() 6 | }, 7 | getBanner: (params) => { 8 | return SetBannerList.find().exec() 9 | }, 10 | 11 | registerUser: (params) => { 12 | return User.create(params).exec() 13 | }, 14 | getUserLogin: (params) => { 15 | const {name} = params 16 | return User.find({name}).exec() 17 | }, 18 | changePass (params) { 19 | const {name} = params 20 | return User.update({name},{$set:params}).exec() 21 | }, 22 | 23 | createArticle: (params) => { 24 | return Article.create(params).exec() 25 | }, 26 | getArticle: (params) => { 27 | const {page, limit, type} = params 28 | let skip 29 | let data = type ? {type} : {} 30 | if (page && limit) { 31 | skip = (page -1) * limit 32 | return Promise.all([ 33 | Article.find(data).addCreateAt().sort({_id:-1}).skip(skip * 1).limit(limit * 1).exec(), 34 | Article.count(data).exec() 35 | ]) 36 | } else { 37 | return Article.find(data).addCreateAt().sort({_id: -1}).exec() 38 | } 39 | }, 40 | // 获取文章详情 41 | getArticleDetail: (params) => { 42 | return Article.find(params).addCreateAt().sort({_id: -1}).exec() 43 | }, 44 | updateArticle (params) { 45 | const { _id, title, content,type,contentToMark } = params 46 | return Article.update({_id},{$set:{title, type, content,contentToMark }}).exec() 47 | }, 48 | setClassify (params) { 49 | return Classify.create(params).exec() 50 | }, 51 | searchClassify (params = {}) { 52 | return Classify.find(params).exec() 53 | }, 54 | updateClassify (_id, params) { 55 | return Classify.update({_id:_id},{$set:params}).exec() 56 | }, 57 | // remove Classify 58 | removeClassify (params) { 59 | return Classify.remove(params).exec() 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /server/app.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const path = require('path'); 3 | const app = express(); 4 | const bodyParser = require('body-parser'); 5 | const index = require('./routes/index') 6 | const register = require('./routes/register.js') 7 | const login = require('./routes/login') 8 | const user = require('./routes/user') 9 | const createArticle = require('./routes/createArticle') 10 | const articleList = require('./routes/articleList') 11 | const createTag = require('./routes/createTag') 12 | const detail = require('./routes/detail') 13 | app.use(bodyParser.urlencoded({ extended: true })); 14 | app.use(bodyParser.json()); 15 | // 跨域 16 | app.all('*', function(req, res, next) { 17 | res.header("Access-Control-Allow-Origin", "*"); 18 | res.header("Access-Control-Allow-Headers", "X-Requested-With,Content-Type");//预检请求使用 19 | res.header("Access-Control-Allow-Methods","PUT,POST,GET,DELETE,OPTIONS");//预检请求使用 20 | next(); 21 | }); 22 | app.use('/api', index) 23 | app.use('/api', register) 24 | app.use('/api', login) 25 | app.use('/api', user) 26 | app.use('/api', createArticle) 27 | app.use('/api', articleList) 28 | app.use('/api', createTag) 29 | app.use('/api', detail) 30 | app.set('port', process.env.PORT || 3010); 31 | app.listen(app.get('port'), function() { 32 | console.log('Express server listening on port ' + app.get('port')); 33 | }); 34 | 35 | -------------------------------------------------------------------------------- /server/lib/mongo.js: -------------------------------------------------------------------------------- 1 | const Mongolass = require('mongolass'); 2 | const mongolass = new Mongolass(); 3 | mongolass.connect('mongodb://localhost:27017/test'); 4 | const moment = require('moment');//时间格式化(前后台都可以用的npm包) 5 | const objectIdToTimestamp = require('objectid-to-timestamp');// 根据_id生成时间戳 6 | /* 7 | mongolass插件系统,语法: 8 | mongolass.plugin(插件名字,{ 9 | before(方法) 10 | after(方法) 11 | }) 12 | */ 13 | mongolass.plugin('addCreateAt',{ 14 | // 只要查询所有条件,那么一定会有最终结果 15 | afterFind:results => { 16 | results.forEach(item => { 17 | item.created_at =item.created_at = moment(objectIdToTimestamp(item._id)).format('YYYY-MM-DD HH:mm:ss'); 18 | }) 19 | return results 20 | }, 21 | // 单个查询有可能是null,所以要加if 22 | afterFindOne: (result) => { 23 | if(result){ 24 | result.created_at =result.created_at = moment(objectIdToTimestamp(result._id)).format('YYYY-MM-DD HH:mm:ss'); 25 | } 26 | return result 27 | } 28 | }) 29 | // 文章 30 | exports.Article = mongolass.model('article',{ 31 | title: {type: 'string'}, 32 | content: {type: 'string'}, 33 | type: {type: 'string'}, 34 | contentToMark: {type: 'string'}, 35 | }) 36 | exports.SetBannerList = mongolass.model('banner', { 37 | title: {type: 'string'}, 38 | description: {type: 'string'}, 39 | url: {type: 'string'}, 40 | img: {type: 'string'}, 41 | }) 42 | // 用户集合 43 | exports.User = mongolass.model('User',{ 44 | name: {type: 'string'}, 45 | password:{type:'string'} 46 | }) 47 | // 分类 48 | exports.Classify = mongolass.model('Classify',{ 49 | type: {type:'string'} 50 | }) -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "cross-env NODE_ENV=production node app.js" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "body-parser": "^1.17.1", 14 | "cross-env": "^5.2.0", 15 | "dotenv": "^8.0.0", 16 | "express": "^4.15.2", 17 | "jsonwebtoken": "^8.5.1", 18 | "moment": "^2.24.0", 19 | "mongolass": "^2.4.5", 20 | "objectid-to-timestamp": "^1.3.0", 21 | "sha1": "^1.1.1" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /server/pm2.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-blog", 3 | "script": "npm", 4 | "args": "start", 5 | "watch": [ 6 | "dist" 7 | ], 8 | "ignore_watch": [ 9 | "node_modules", 10 | "public", 11 | "logs" 12 | ], 13 | "watch_options": { 14 | "followSymlinks": false 15 | }, 16 | "log_file": "./logs/combined.outerr.log", 17 | "out_file": "./logs/out.log", 18 | "err_file": "./logs/err.log" 19 | } -------------------------------------------------------------------------------- /server/routes/articleList.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const router = express.Router() 3 | const api = require('../api/index') 4 | const checkToken = require('../utils/checkToken') 5 | router.post('/get/article',checkToken, (req, res, next) => { 6 | api.getArticle(req.body).then(response => { 7 | res.send({ 8 | code: 200, 9 | message: 'ok', 10 | data: response[0], 11 | total: response[1] 12 | }) 13 | }).catch(err => { 14 | res.send({ 15 | code: -200, 16 | message: err.toString() 17 | }) 18 | }) 19 | }) 20 | router.get('/get/type/article',checkToken, (req, res, next) => { 21 | api.getArticleByTag(req.body).then(response => { 22 | res.send({ 23 | code: 200, 24 | message: 'ok', 25 | data: response[0], 26 | total: response[1] 27 | }) 28 | }).catch(err => { 29 | res.send({ 30 | code: -200, 31 | message: err.toString() 32 | }) 33 | }) 34 | }) 35 | module.exports = router -------------------------------------------------------------------------------- /server/routes/createArticle.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const router = express.Router() 3 | const checkToken = require('../utils/checkToken') 4 | const api = require('../api/index') 5 | router.post('/create/article', checkToken, (req, res, next) => { 6 | api.createArticle(req.body).then(response => { 7 | if (response.result.ok && response.result.n > 0){ 8 | res.send({ 9 | code: 200, 10 | message: '发布成功' 11 | }); 12 | } else { 13 | res.send({ 14 | code: -200, 15 | message: '发布失败' 16 | }) 17 | } 18 | }).catch(err => { 19 | res.send({ 20 | code: -200, 21 | message: err.toString() 22 | }) 23 | }) 24 | }) 25 | module.exports = router -------------------------------------------------------------------------------- /server/routes/createTag.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const router = express.Router() 3 | const api = require('../api/index') 4 | const checkToken = require('../utils/checkToken') 5 | 6 | router.post('/create/tag',checkToken, (req, res, next) => { 7 | api.searchClassify(req.body).then(resp => { 8 | if (resp.length) { 9 | //更新 10 | api.updateClassify(resp[0]._id, req.body).then(response => { 11 | if (response.result.ok && response.result.n > 0){ 12 | res.send({ 13 | code: 200, 14 | message: '更新成功' 15 | }); 16 | } else { 17 | res.send({ 18 | code: -200, 19 | message: '更新失败' 20 | }) 21 | } 22 | }).catch(err => { 23 | res.send({ 24 | code: -200, 25 | message: err.toString() 26 | }) 27 | }) 28 | } else { 29 | // 创建 30 | api.setClassify(req.body).then(response => { 31 | if (response.result.ok && response.result.n > 0){ 32 | res.send({ 33 | code: 200, 34 | message: '添加成功' 35 | }); 36 | } else { 37 | res.send({ 38 | code: -200, 39 | message: '添加失败' 40 | }) 41 | } 42 | }).catch(err => { 43 | res.send({ 44 | code: -200, 45 | message: err.toString() 46 | }) 47 | }) 48 | } 49 | }) 50 | }) 51 | router.post('/get/all/tag', (req, res, next) => { 52 | api.searchClassify(req.body).then(response => { 53 | if (response.length){ 54 | res.send({ 55 | code: 200, 56 | message: '成功', 57 | data: response 58 | }); 59 | } else { 60 | res.send({ 61 | code: -200, 62 | message: '获取失败' 63 | }) 64 | } 65 | }).catch(err => { 66 | res.send({ 67 | code: -200, 68 | message: err.toString() 69 | }) 70 | }) 71 | }) 72 | router.post('/remove/tag',checkToken, (req, res, next) => { 73 | api.removeClassify(req.body).then(response => { 74 | if (response.result.ok && response.result.n > 0){ 75 | res.send({ 76 | code: 200, 77 | message: '删除成功' 78 | }) 79 | } else { 80 | throw new Error('该分类不存在') 81 | } 82 | }).catch(err => { 83 | res.send({ 84 | code: -200, 85 | message: err.toString() 86 | }) 87 | }) 88 | }) 89 | module.exports = router -------------------------------------------------------------------------------- /server/routes/detail.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const router = express.Router() 3 | const api = require('../api/index') 4 | router.post('/get/article/detail', (req, res, next) => { 5 | api.getArticleDetail(req.body).then(response => { 6 | if (response.length) { 7 | res.send({ 8 | code: 200, 9 | message: 'ok', 10 | data: response[0] 11 | }) 12 | } else { 13 | res.send({ 14 | code: -200, 15 | message: '没有找到改文章', 16 | }) 17 | } 18 | }).catch(err => { 19 | res.send({ 20 | code: -200, 21 | message: err.toString() 22 | }) 23 | }) 24 | }) 25 | module.exports = router -------------------------------------------------------------------------------- /server/routes/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const router = express.Router() 3 | const api = require('../api/index') 4 | router.get('/get/topics', (req, res) => { 5 | api.getTopics().then(data => { 6 | res.send({ 7 | code: 200, 8 | message: 'ok', 9 | data 10 | }) 11 | }) 12 | }); 13 | // 设置banner 14 | router.post('/set/banner', (req, res, next) => { 15 | const data = req.body 16 | api.setBanner(data).then(response => { 17 | res.send({ 18 | code: 200, 19 | message: 'ok', 20 | data: response 21 | }) 22 | }) 23 | }) 24 | router.get('/get/banner',(req, res, next) => { 25 | api.getBanner().then(response => { 26 | res.send({ 27 | code: 200, 28 | message: 'ok', 29 | data: response 30 | }) 31 | }) 32 | }) 33 | module.exports = router -------------------------------------------------------------------------------- /server/routes/login.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config('../.env') 2 | const express = require('express') 3 | const router = express.Router() 4 | const api = require('../api/index') 5 | const sha1 = require('sha1') 6 | const createToken = require('../utils/createToken') 7 | router.post('/user/login', (req, res, next) => { 8 | const {name, password} = req.body 9 | let encodePassword = sha1(password), 10 | user = { 11 | name, 12 | password: encodePassword 13 | } 14 | 15 | api.getUserLogin(user).then(response => { 16 | if (response && response.length > 0 && response[0].password === encodePassword) { 17 | res.send({ 18 | // 登陆成功 19 | code:200, 20 | message: '登陆成功', 21 | data: { 22 | token: createToken(name), 23 | name: name 24 | } 25 | }) 26 | } else { 27 | res.send({ 28 | // 登陆成功 29 | code: -200, 30 | message: '用户名或密码错误', 31 | }) 32 | } 33 | }).catch(err => { 34 | next(err) 35 | // 服务器发生错误(例如status:) 36 | return res.json({ 37 | code:-200, 38 | message:err.toString() 39 | }) 40 | }) 41 | }) 42 | module.exports = router -------------------------------------------------------------------------------- /server/routes/register.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config('../.env') 2 | const express = require('express') 3 | const router = express.Router() 4 | const api = require('../api/index') 5 | const sha1 = require('sha1') 6 | const createToken = require('../utils/createToken') 7 | const checkToken = require('../utils/checkToken') 8 | router.post('/user/register', (req, res, next) => { 9 | const {name, password} = req.body 10 | let encodePassword = sha1(password), 11 | user = { 12 | name, 13 | password: encodePassword 14 | } 15 | 16 | api.registerUser(user).then(response => { 17 | res.send({ 18 | // 创建用户成功 19 | code:200, 20 | message: 'ok', 21 | data: { 22 | token: createToken(name) 23 | } 24 | }) 25 | }).catch(err => { 26 | if(err.message.match('E11000 duplicate key')){ 27 | return res.json({ 28 | code:-200, 29 | message:'用户名重复' 30 | }) 31 | } 32 | // 服务器发生错误(例如status:) 33 | return res.send({ 34 | code:-200, 35 | message:err.toString() 36 | }) 37 | }) 38 | }) 39 | router.post('/change/pass', checkToken, (req, res, next) => { 40 | let encodePassword = sha1(password), 41 | user = { 42 | name, 43 | password: encodePassword 44 | } 45 | api.changePass(user).then(response => { 46 | if (response.result.ok && response.result.n > 0){ 47 | res.send({ 48 | code: 200, 49 | message: '修改成功' 50 | }) 51 | } else { 52 | throw new Error('没有找到该用户'); 53 | } 54 | }).catch(err => { 55 | return res.json({ 56 | code:-200, 57 | message: err.toString() 58 | }) 59 | }) 60 | }) 61 | module.exports = router -------------------------------------------------------------------------------- /server/routes/user.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const router = express.Router() 3 | const checkToken = require('../utils/checkToken') 4 | router.post('/user/info', checkToken, (req, res, next) => { 5 | console.log(req.headers['authorization'], 'headers authorization') 6 | const {name} = req.body 7 | res.send({ 8 | code: 200, 9 | message: '用户中心', 10 | data: { 11 | name 12 | } 13 | }); 14 | }) 15 | module.exports = router -------------------------------------------------------------------------------- /server/utils/checkToken.js: -------------------------------------------------------------------------------- 1 | // 验证token中间件 2 | const jwt = require('jsonwebtoken') 3 | module.exports = function (req, res, next) { 4 | if(req.headers['authorization']){ 5 | const token = req.headers['authorization'].split(' ')[1] 6 | const decoded = jwt.decode(token, process.env.JWT_SECRET) 7 | // 如果过期了就重新登录 8 | // 验证token也需要优化 9 | if(token&&decoded.exp<=Date.now()/1000){ 10 | return res.send({ 11 | code: 401, 12 | message:"授权已经过期,请重新登陆" 13 | }) 14 | } 15 | } 16 | next(); 17 | } -------------------------------------------------------------------------------- /server/utils/createToken.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken') 2 | module.exports =function (name){ 3 | const expiry = new Date(); 4 | expiry.setDate(expiry.getDate()+7);//有效期设置为七天 5 | const token = jwt.sign({ 6 | name:name, 7 | exp:parseInt(expiry.getTime()/1000)//除以1000以后表示的是秒数 8 | },process.env.JWT_SECRET) 9 | return token; 10 | } -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 18 | -------------------------------------------------------------------------------- /src/api/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hbbaly/vue-blog/b624636e9ca48174e0bb49ef28d87c3e26be8a60/src/api/index.js -------------------------------------------------------------------------------- /src/assets/css/_variables.styl: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: hbbaly 3 | * @Date: 2019-11-16 17:43:56 4 | * @GitHub: https://github.com/hbbaly 5 | * @Description: variables 6 | */ 7 | 8 | /* 水平间距 */ 9 | $row-smest= 5px; 10 | $row-sm= 10px; 11 | $row-base= 15px; 12 | $row-lg= 20px; 13 | $row-lger= 25px; 14 | $row-lgest= 30px; 15 | 16 | /* 垂直间距 */ 17 | $col-smest= 5px; 18 | $col-sm= 10px; 19 | $col-base= 15px; 20 | $col-lg= 20px; 21 | $col-lger= 25px; 22 | $col-lgest= 30px; 23 | // colors 24 | $grey-bg = #fefefe 25 | $blue = #0288D1 26 | $orange = #409EFF 27 | $color = #fff 28 | 29 | // font-size= 30 | $fs-smest=12px; 31 | $fs-smer=14px; 32 | $fs-sm=16px; 33 | $fs-base=18px; 34 | $fs-lg=20px; 35 | $fs-lger=22px; 36 | $fs-lgest=24px; -------------------------------------------------------------------------------- /src/assets/css/default.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Original highlight.js style (c) Ivan Sagalaev 4 | 5 | */ 6 | 7 | .hljs { 8 | display: block; 9 | overflow-x: auto; 10 | padding: 0.5em; 11 | background: #F0F0F0; 12 | } 13 | 14 | 15 | /* Base color: saturation 0; */ 16 | 17 | .hljs, 18 | .hljs-subst { 19 | color: #444; 20 | } 21 | 22 | .hljs-comment { 23 | color: #888888; 24 | } 25 | 26 | .hljs-keyword, 27 | .hljs-attribute, 28 | .hljs-selector-tag, 29 | .hljs-meta-keyword, 30 | .hljs-doctag, 31 | .hljs-name { 32 | font-weight: bold; 33 | } 34 | 35 | 36 | /* User color: hue: 0 */ 37 | 38 | .hljs-type, 39 | .hljs-string, 40 | .hljs-number, 41 | .hljs-selector-id, 42 | .hljs-selector-class, 43 | .hljs-quote, 44 | .hljs-template-tag, 45 | .hljs-deletion { 46 | color: #880000; 47 | } 48 | 49 | .hljs-title, 50 | .hljs-section { 51 | color: #880000; 52 | font-weight: bold; 53 | } 54 | 55 | .hljs-regexp, 56 | .hljs-symbol, 57 | .hljs-variable, 58 | .hljs-template-variable, 59 | .hljs-link, 60 | .hljs-selector-attr, 61 | .hljs-selector-pseudo { 62 | color: #BC6060; 63 | } 64 | 65 | 66 | /* Language color: hue: 90; */ 67 | 68 | .hljs-literal { 69 | color: #78A960; 70 | } 71 | 72 | .hljs-built_in, 73 | .hljs-bullet, 74 | .hljs-code, 75 | .hljs-addition { 76 | color: #397300; 77 | } 78 | 79 | 80 | /* Meta color: hue: 200 */ 81 | 82 | .hljs-meta { 83 | color: #1f7199; 84 | } 85 | 86 | .hljs-meta-string { 87 | color: #4d99bf; 88 | } 89 | 90 | 91 | /* Misc effects */ 92 | 93 | .hljs-emphasis { 94 | font-style: italic; 95 | } 96 | 97 | .hljs-strong { 98 | font-weight: bold; 99 | } 100 | -------------------------------------------------------------------------------- /src/assets/css/default0.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Original highlight.js style (c) Ivan Sagalaev 4 | 5 | */ 6 | 7 | .hljs { 8 | display: block; 9 | overflow-x: auto; 10 | padding: 0.5em; 11 | background: #F0F0F0; 12 | } 13 | 14 | 15 | /* Base color: saturation 0; */ 16 | 17 | .hljs, 18 | .hljs-subst { 19 | color: #444; 20 | } 21 | 22 | .hljs-comment { 23 | color: #888888; 24 | } 25 | 26 | .hljs-keyword, 27 | .hljs-attribute, 28 | .hljs-selector-tag, 29 | .hljs-meta-keyword, 30 | .hljs-doctag, 31 | .hljs-name { 32 | font-weight: bold; 33 | } 34 | 35 | 36 | /* User color: hue: 0 */ 37 | 38 | .hljs-type, 39 | .hljs-string, 40 | .hljs-number, 41 | .hljs-selector-id, 42 | .hljs-selector-class, 43 | .hljs-quote, 44 | .hljs-template-tag, 45 | .hljs-deletion { 46 | color: #880000; 47 | } 48 | 49 | .hljs-title, 50 | .hljs-section { 51 | color: #880000; 52 | font-weight: bold; 53 | } 54 | 55 | .hljs-regexp, 56 | .hljs-symbol, 57 | .hljs-variable, 58 | .hljs-template-variable, 59 | .hljs-link, 60 | .hljs-selector-attr, 61 | .hljs-selector-pseudo { 62 | color: #BC6060; 63 | } 64 | 65 | 66 | /* Language color: hue: 90; */ 67 | 68 | .hljs-literal { 69 | color: #78A960; 70 | } 71 | 72 | .hljs-built_in, 73 | .hljs-bullet, 74 | .hljs-code, 75 | .hljs-addition { 76 | color: #397300; 77 | } 78 | 79 | 80 | /* Meta color: hue: 200 */ 81 | 82 | .hljs-meta { 83 | color: #1f7199; 84 | } 85 | 86 | .hljs-meta-string { 87 | color: #4d99bf; 88 | } 89 | 90 | 91 | /* Misc effects */ 92 | 93 | .hljs-emphasis { 94 | font-style: italic; 95 | } 96 | 97 | .hljs-strong { 98 | font-weight: bold; 99 | } 100 | -------------------------------------------------------------------------------- /src/assets/css/editor.styl: -------------------------------------------------------------------------------- 1 | .gutter pre 2 | color #999 3 | pre 4 | padding 10px 5 | color: #525252 6 | background: #ddd 7 | .function .keyword, 8 | .constant 9 | color: #0092db 10 | .keyword, 11 | .attribute 12 | color: #e96900 13 | .number, 14 | .literal 15 | color: #AE81FF 16 | .tag, 17 | .tag .title, 18 | .change, 19 | .winutils, 20 | .flow, 21 | .lisp .title, 22 | .clojure .built_in, 23 | .nginx .title, 24 | .tex .special 25 | color: #2973b7 26 | .class .title 27 | color: #42b983 28 | .symbol, 29 | .symbol .string, 30 | .value, 31 | .regexp 32 | color: $blue 33 | .title 34 | color: #A6E22E 35 | .tag .value, 36 | .string, 37 | .subst, 38 | .haskell .type, 39 | .preprocessor, 40 | .ruby .class .parent, 41 | .built_in, 42 | .sql .aggregate, 43 | .django .template_tag, 44 | .django .variable, 45 | .smalltalk .class, 46 | .javadoc, 47 | .django .filter .argument, 48 | .smalltalk .localvars, 49 | .smalltalk .array, 50 | .attr_selector, 51 | .pseudo, 52 | .addition, 53 | .stream, 54 | .envvar, 55 | .apache .tag, 56 | .apache .cbracket, 57 | .tex .command, 58 | .prompt 59 | color: #0288D1 60 | .comment, 61 | .java .annotation, 62 | .python .decorator, 63 | .template_comment, 64 | .pi, 65 | .doctype, 66 | .deletion, 67 | .shebang, 68 | .apache .sqbracket, 69 | .tex .formula 70 | color: #b3b3b3 71 | .coffeescript .javascript, 72 | .javascript .xml, 73 | .tex .formula, 74 | .xml .javascript, 75 | .xml .vbscript, 76 | .xml .css, 77 | .xml .cdata 78 | opacity: 0.5 -------------------------------------------------------------------------------- /src/assets/css/highlight.css: -------------------------------------------------------------------------------- 1 | .hljs-comment, 2 | .hljs-quote { 3 | color: #8e908c; 4 | } 5 | 6 | .hljs-variable, 7 | .hljs-template-variable, 8 | .hljs-tag, 9 | .hljs-name, 10 | .hljs-selector-id, 11 | .hljs-selector-class, 12 | .hljs-regexp, 13 | .hljs-deletion { 14 | color: #c82829; 15 | } 16 | 17 | .hljs-number, 18 | .hljs-built_in, 19 | .hljs-builtin-name, 20 | .hljs-literal, 21 | .hljs-type, 22 | .hljs-params, 23 | .hljs-meta, 24 | .hljs-link { 25 | color: #f5871f; 26 | } 27 | 28 | .hljs-attribute { 29 | color: #eab700; 30 | } 31 | 32 | .hljs-string, 33 | .hljs-symbol, 34 | .hljs-bullet, 35 | .hljs-addition { 36 | color: #718c00; 37 | } 38 | 39 | .hljs-title, 40 | .hljs-section { 41 | color: #4271ae; 42 | } 43 | 44 | .hljs-keyword, 45 | .hljs-selector-tag { 46 | color: #8959a8; 47 | } 48 | 49 | .hljs { 50 | display: block; 51 | overflow-x: auto; 52 | background: white; 53 | color: #4d4d4c; 54 | padding: 0.5em; 55 | } 56 | 57 | .hljs-emphasis { 58 | font-style: italic; 59 | } 60 | 61 | .hljs-strong { 62 | font-weight: bold; 63 | } -------------------------------------------------------------------------------- /src/assets/css/markdown.styl: -------------------------------------------------------------------------------- 1 | .markdown-body { 2 | -ms-text-size-adjust: 100%; 3 | -webkit-text-size-adjust: 100%; 4 | line-height: 1.5; 5 | color: #24292e; 6 | font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 7 | font-size: 16px; 8 | line-height: 1.5; 9 | word-wrap: break-word; 10 | } 11 | 12 | .markdown-body .pl-c { 13 | color: #969896; 14 | } 15 | 16 | .markdown-body .pl-c1, 17 | .markdown-body .pl-s .pl-v { 18 | color: #0086b3; 19 | } 20 | 21 | .markdown-body .pl-e, 22 | .markdown-body .pl-en { 23 | color: #795da3; 24 | } 25 | 26 | .markdown-body .pl-smi, 27 | .markdown-body .pl-s .pl-s1 { 28 | color: #333; 29 | } 30 | 31 | .markdown-body .pl-ent { 32 | color: #63a35c; 33 | } 34 | 35 | .markdown-body .pl-k { 36 | color: #a71d5d; 37 | } 38 | 39 | .markdown-body .pl-s, 40 | .markdown-body .pl-pds, 41 | .markdown-body .pl-s .pl-pse .pl-s1, 42 | .markdown-body .pl-sr, 43 | .markdown-body .pl-sr .pl-cce, 44 | .markdown-body .pl-sr .pl-sre, 45 | .markdown-body .pl-sr .pl-sra { 46 | color: #183691; 47 | } 48 | 49 | .markdown-body .pl-v, 50 | .markdown-body .pl-smw { 51 | color: #ed6a43; 52 | } 53 | 54 | .markdown-body .pl-bu { 55 | color: #b52a1d; 56 | } 57 | 58 | .markdown-body .pl-ii { 59 | color: #f8f8f8; 60 | background-color: #b52a1d; 61 | } 62 | 63 | .markdown-body .pl-c2 { 64 | color: #f8f8f8; 65 | background-color: #b52a1d; 66 | } 67 | 68 | .markdown-body .pl-c2::before { 69 | content: "^M"; 70 | } 71 | 72 | .markdown-body .pl-sr .pl-cce { 73 | font-weight: bold; 74 | color: #63a35c; 75 | } 76 | 77 | .markdown-body .pl-ml { 78 | color: #693a17; 79 | } 80 | 81 | .markdown-body .pl-mh, 82 | .markdown-body .pl-mh .pl-en, 83 | .markdown-body .pl-ms { 84 | font-weight: bold; 85 | color: #1d3e81; 86 | } 87 | 88 | .markdown-body .pl-mq { 89 | color: #008080; 90 | } 91 | 92 | .markdown-body .pl-mi { 93 | font-style: italic; 94 | color: #333; 95 | } 96 | 97 | .markdown-body .pl-mb { 98 | font-weight: bold; 99 | color: #333; 100 | } 101 | 102 | .markdown-body .pl-md { 103 | color: #bd2c00; 104 | background-color: #ffecec; 105 | } 106 | 107 | .markdown-body .pl-mi1 { 108 | color: #55a532; 109 | background-color: #eaffea; 110 | } 111 | 112 | .markdown-body .pl-mc { 113 | color: #ef9700; 114 | background-color: #ffe3b4; 115 | } 116 | 117 | .markdown-body .pl-mi2 { 118 | color: #d8d8d8; 119 | background-color: #808080; 120 | } 121 | 122 | .markdown-body .pl-mdr { 123 | font-weight: bold; 124 | color: #795da3; 125 | } 126 | 127 | .markdown-body .pl-mo { 128 | color: #1d3e81; 129 | } 130 | 131 | .markdown-body .pl-ba { 132 | color: #595e62; 133 | } 134 | 135 | .markdown-body .pl-sg { 136 | color: #c0c0c0; 137 | } 138 | 139 | .markdown-body .pl-corl { 140 | text-decoration: underline; 141 | color: #183691; 142 | } 143 | 144 | .markdown-body .octicon { 145 | display: inline-block; 146 | vertical-align: text-top; 147 | fill: currentColor; 148 | } 149 | 150 | .markdown-body a { 151 | background-color: transparent; 152 | -webkit-text-decoration-skip: objects; 153 | } 154 | 155 | .markdown-body a:active, 156 | .markdown-body a:hover { 157 | outline-width: 0; 158 | } 159 | 160 | .markdown-body strong { 161 | font-weight: inherit; 162 | } 163 | 164 | .markdown-body strong { 165 | font-weight: bolder; 166 | } 167 | 168 | .markdown-body h1 { 169 | font-size: 2em; 170 | margin: 0.67em 0; 171 | } 172 | 173 | .markdown-body img { 174 | border-style: none; 175 | } 176 | 177 | .markdown-body svg:not(:root) { 178 | overflow: hidden; 179 | } 180 | 181 | .markdown-body code, 182 | .markdown-body kbd, 183 | .markdown-body pre { 184 | font-family: monospace, monospace; 185 | font-size: 1em; 186 | } 187 | 188 | .markdown-body hr { 189 | box-sizing: content-box; 190 | height: 0; 191 | overflow: visible; 192 | } 193 | 194 | .markdown-body input { 195 | font: inherit; 196 | margin: 0; 197 | } 198 | 199 | .markdown-body input { 200 | overflow: visible; 201 | } 202 | 203 | .markdown-body [type="checkbox"] { 204 | box-sizing: border-box; 205 | padding: 0; 206 | } 207 | 208 | .markdown-body * { 209 | box-sizing: border-box; 210 | } 211 | 212 | .markdown-body input { 213 | font-family: inherit; 214 | font-size: inherit; 215 | line-height: inherit; 216 | } 217 | 218 | .markdown-body a { 219 | color: #0366d6; 220 | text-decoration: none; 221 | } 222 | 223 | .markdown-body a:hover { 224 | text-decoration: underline; 225 | } 226 | 227 | .markdown-body strong { 228 | font-weight: 600; 229 | } 230 | 231 | .markdown-body hr { 232 | height: 0; 233 | margin: 15px 0; 234 | overflow: hidden; 235 | background: transparent; 236 | border: 0; 237 | border-bottom: 1px solid #dfe2e5; 238 | } 239 | 240 | .markdown-body hr::before { 241 | display: table; 242 | content: ""; 243 | } 244 | 245 | .markdown-body hr::after { 246 | display: table; 247 | clear: both; 248 | content: ""; 249 | } 250 | 251 | .markdown-body table { 252 | border-spacing: 0; 253 | border-collapse: collapse; 254 | } 255 | 256 | .markdown-body td, 257 | .markdown-body th { 258 | padding: 0; 259 | } 260 | 261 | .markdown-body h1, 262 | .markdown-body h2, 263 | .markdown-body h3, 264 | .markdown-body h4, 265 | .markdown-body h5, 266 | .markdown-body h6 { 267 | margin-top: 0; 268 | margin-bottom: 0; 269 | } 270 | 271 | .markdown-body h1 { 272 | font-size: 32px; 273 | font-weight: 600; 274 | } 275 | 276 | .markdown-body h2 { 277 | font-size: 24px; 278 | font-weight: 600; 279 | } 280 | 281 | .markdown-body h3 { 282 | font-size: 20px; 283 | font-weight: 600; 284 | } 285 | 286 | .markdown-body h4 { 287 | font-size: 16px; 288 | font-weight: 600; 289 | } 290 | 291 | .markdown-body h5 { 292 | font-size: 14px; 293 | font-weight: 600; 294 | } 295 | 296 | .markdown-body h6 { 297 | font-size: 12px; 298 | font-weight: 600; 299 | } 300 | 301 | .markdown-body p { 302 | margin-top: 0; 303 | margin-bottom: 10px; 304 | } 305 | 306 | .markdown-body blockquote { 307 | margin: 0; 308 | } 309 | 310 | .markdown-body ul, 311 | .markdown-body ol { 312 | padding-left: 0; 313 | margin-top: 0; 314 | margin-bottom: 0; 315 | } 316 | 317 | .markdown-body ol ol, 318 | .markdown-body ul ol { 319 | list-style-type: lower-roman; 320 | } 321 | 322 | .markdown-body ul ul ol, 323 | .markdown-body ul ol ol, 324 | .markdown-body ol ul ol, 325 | .markdown-body ol ol ol { 326 | list-style-type: lower-alpha; 327 | } 328 | 329 | .markdown-body dd { 330 | margin-left: 0; 331 | } 332 | 333 | .markdown-body code { 334 | font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; 335 | font-size: 12px; 336 | } 337 | 338 | .markdown-body pre { 339 | margin-top: 0; 340 | margin-bottom: 0; 341 | font: 12px "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; 342 | } 343 | 344 | .markdown-body .octicon { 345 | vertical-align: text-bottom; 346 | } 347 | 348 | .markdown-body .pl-0 { 349 | padding-left: 0 !important; 350 | } 351 | 352 | .markdown-body .pl-1 { 353 | padding-left: 4px !important; 354 | } 355 | 356 | .markdown-body .pl-2 { 357 | padding-left: 8px !important; 358 | } 359 | 360 | .markdown-body .pl-3 { 361 | padding-left: 16px !important; 362 | } 363 | 364 | .markdown-body .pl-4 { 365 | padding-left: 24px !important; 366 | } 367 | 368 | .markdown-body .pl-5 { 369 | padding-left: 32px !important; 370 | } 371 | 372 | .markdown-body .pl-6 { 373 | padding-left: 40px !important; 374 | } 375 | 376 | .markdown-body::before { 377 | display: table; 378 | content: ""; 379 | } 380 | 381 | .markdown-body::after { 382 | display: table; 383 | //clear: both; 384 | content: ""; 385 | } 386 | 387 | .markdown-body>*:first-child { 388 | margin-top: 0 !important; 389 | } 390 | 391 | .markdown-body>*:last-child { 392 | margin-bottom: 0 !important; 393 | } 394 | 395 | .markdown-body a:not([href]) { 396 | color: inherit; 397 | text-decoration: none; 398 | } 399 | 400 | .markdown-body .anchor { 401 | float: left; 402 | padding-right: 4px; 403 | margin-left: -20px; 404 | line-height: 1; 405 | } 406 | 407 | .markdown-body .anchor:focus { 408 | outline: none; 409 | } 410 | 411 | .markdown-body p, 412 | .markdown-body blockquote, 413 | .markdown-body ul, 414 | .markdown-body ol, 415 | .markdown-body dl, 416 | .markdown-body table, 417 | .markdown-body pre { 418 | margin-top: 0; 419 | margin-bottom: 16px; 420 | } 421 | 422 | .markdown-body hr { 423 | height: 0.25em; 424 | padding: 0; 425 | margin: 24px 0; 426 | background-color: #e1e4e8; 427 | border: 0; 428 | } 429 | 430 | .markdown-body blockquote { 431 | padding: 0 1em; 432 | color: #6a737d; 433 | border-left: 0.25em solid #dfe2e5; 434 | } 435 | 436 | .markdown-body blockquote>:first-child { 437 | margin-top: 0; 438 | } 439 | 440 | .markdown-body blockquote>:last-child { 441 | margin-bottom: 0; 442 | } 443 | 444 | .markdown-body kbd { 445 | display: inline-block; 446 | padding: 3px 5px; 447 | font-size: 11px; 448 | line-height: 10px; 449 | color: #444d56; 450 | vertical-align: middle; 451 | background-color: #fafbfc; 452 | border: solid 1px #c6cbd1; 453 | border-bottom-color: #959da5; 454 | border-radius: 3px; 455 | box-shadow: inset 0 -1px 0 #959da5; 456 | } 457 | 458 | .markdown-body h1, 459 | .markdown-body h2, 460 | .markdown-body h3, 461 | .markdown-body h4, 462 | .markdown-body h5, 463 | .markdown-body h6 { 464 | margin-top: 24px; 465 | margin-bottom: 16px; 466 | font-weight: 600; 467 | line-height: 1.25; 468 | } 469 | 470 | .markdown-body h1 .octicon-link, 471 | .markdown-body h2 .octicon-link, 472 | .markdown-body h3 .octicon-link, 473 | .markdown-body h4 .octicon-link, 474 | .markdown-body h5 .octicon-link, 475 | .markdown-body h6 .octicon-link { 476 | color: #1b1f23; 477 | vertical-align: middle; 478 | visibility: hidden; 479 | } 480 | 481 | .markdown-body h1:hover .anchor, 482 | .markdown-body h2:hover .anchor, 483 | .markdown-body h3:hover .anchor, 484 | .markdown-body h4:hover .anchor, 485 | .markdown-body h5:hover .anchor, 486 | .markdown-body h6:hover .anchor { 487 | text-decoration: none; 488 | } 489 | 490 | .markdown-body h1:hover .anchor .octicon-link, 491 | .markdown-body h2:hover .anchor .octicon-link, 492 | .markdown-body h3:hover .anchor .octicon-link, 493 | .markdown-body h4:hover .anchor .octicon-link, 494 | .markdown-body h5:hover .anchor .octicon-link, 495 | .markdown-body h6:hover .anchor .octicon-link { 496 | visibility: visible; 497 | } 498 | 499 | .markdown-body h1 { 500 | padding-bottom: 0.3em; 501 | font-size: 2em; 502 | border-bottom: 1px solid #eaecef; 503 | } 504 | 505 | .markdown-body h2 { 506 | padding-bottom: 0.3em; 507 | font-size: 1.5em; 508 | border-bottom: 1px solid #eaecef; 509 | } 510 | 511 | .markdown-body h3 { 512 | font-size: 1.25em; 513 | } 514 | 515 | .markdown-body h4 { 516 | font-size: 1em; 517 | } 518 | 519 | .markdown-body h5 { 520 | font-size: 0.875em; 521 | } 522 | 523 | .markdown-body h6 { 524 | font-size: 0.85em; 525 | color: #6a737d; 526 | } 527 | 528 | .markdown-body ul, 529 | .markdown-body ol { 530 | padding-left: 2em; 531 | } 532 | 533 | .markdown-body ul ul, 534 | .markdown-body ul ol, 535 | .markdown-body ol ol, 536 | .markdown-body ol ul { 537 | margin-top: 0; 538 | margin-bottom: 0; 539 | } 540 | 541 | .markdown-body li>p { 542 | margin-top: 16px; 543 | } 544 | 545 | .markdown-body li+li { 546 | margin-top: 0.25em; 547 | } 548 | 549 | .markdown-body dl { 550 | padding: 0; 551 | } 552 | 553 | .markdown-body dl dt { 554 | padding: 0; 555 | margin-top: 16px; 556 | font-size: 1em; 557 | font-style: italic; 558 | font-weight: 600; 559 | } 560 | 561 | .markdown-body dl dd { 562 | padding: 0 16px; 563 | margin-bottom: 16px; 564 | } 565 | 566 | .markdown-body table { 567 | display: block; 568 | width: 100%; 569 | overflow: auto; 570 | } 571 | 572 | .markdown-body table th { 573 | font-weight: 600; 574 | } 575 | 576 | .markdown-body table th, 577 | .markdown-body table td { 578 | padding: 6px 13px; 579 | border: 1px solid #dfe2e5; 580 | } 581 | 582 | .markdown-body table tr { 583 | background-color: #fff; 584 | border-top: 1px solid #c6cbd1; 585 | } 586 | 587 | .markdown-body table tr:nth-child(2n) { 588 | background-color: #f6f8fa; 589 | } 590 | 591 | .markdown-body img { 592 | max-width: 100%; 593 | box-sizing: content-box; 594 | background-color: #fff; 595 | } 596 | 597 | .markdown-body code { 598 | padding: 0; 599 | padding-top: 0.2em; 600 | padding-bottom: 0.2em; 601 | margin: 0; 602 | font-size: 85%; 603 | background-color: rgba(27,31,35,0.05); 604 | border-radius: 3px; 605 | } 606 | 607 | .markdown-body code::before, 608 | .markdown-body code::after { 609 | letter-spacing: -0.2em; 610 | content: "\00a0"; 611 | } 612 | 613 | .markdown-body pre { 614 | word-wrap: normal; 615 | } 616 | 617 | .markdown-body pre>code { 618 | padding: 0; 619 | margin: 0; 620 | font-size: 100%; 621 | word-break: normal; 622 | white-space: pre; 623 | background: transparent; 624 | border: 0; 625 | } 626 | 627 | .markdown-body .highlight { 628 | margin-bottom: 16px; 629 | } 630 | 631 | .markdown-body .highlight pre { 632 | margin-bottom: 0; 633 | word-break: normal; 634 | } 635 | 636 | .markdown-body .highlight pre, 637 | .markdown-body pre { 638 | padding: 16px; 639 | overflow: auto; 640 | font-size: 85%; 641 | line-height: 1.45; 642 | background-color: #f6f8fa; 643 | border-radius: 3px; 644 | } 645 | 646 | .markdown-body pre code { 647 | display: inline; 648 | max-width: auto; 649 | padding: 0; 650 | margin: 0; 651 | overflow: visible; 652 | line-height: inherit; 653 | word-wrap: normal; 654 | background-color: transparent; 655 | border: 0; 656 | } 657 | 658 | .markdown-body pre code::before, 659 | .markdown-body pre code::after { 660 | content: normal; 661 | } 662 | 663 | .markdown-body .full-commit .btn-outline:not(:disabled):hover { 664 | color: #005cc5; 665 | border-color: #005cc5; 666 | } 667 | 668 | .markdown-body kbd { 669 | display: inline-block; 670 | padding: 3px 5px; 671 | font: 11px "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; 672 | line-height: 10px; 673 | color: #444d56; 674 | vertical-align: middle; 675 | background-color: #fcfcfc; 676 | border: solid 1px #c6cbd1; 677 | border-bottom-color: #959da5; 678 | border-radius: 3px; 679 | box-shadow: inset 0 -1px 0 #959da5; 680 | } 681 | 682 | .markdown-body :checked+.radio-label { 683 | position: relative; 684 | z-index: 1; 685 | border-color: #0366d6; 686 | } 687 | 688 | .markdown-body .task-list-item { 689 | list-style-type: none; 690 | } 691 | 692 | .markdown-body .task-list-item+.task-list-item { 693 | margin-top: 3px; 694 | } 695 | 696 | .markdown-body .task-list-item input { 697 | margin: 0 0.2em 0.25em -1.6em; 698 | vertical-align: middle; 699 | } 700 | 701 | .markdown-body hr { 702 | border-bottom-color: #eee; 703 | } -------------------------------------------------------------------------------- /src/assets/css/reset.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * ress.css • v1.1.2 3 | * MIT License 4 | * github.com/filipelinhares/ress 5 | */ 6 | 7 | /* # ================================================================= 8 | # Global selectors 9 | # ================================================================= */ 10 | 11 | html { 12 | box-sizing: border-box; 13 | -webkit-text-size-adjust: 100%; /* iOS 8+ */ 14 | } 15 | *{ 16 | margin: 0; 17 | padding:0 18 | } 19 | *, 20 | ::before, 21 | ::after { 22 | box-sizing: inherit; 23 | } 24 | 25 | ::before, 26 | ::after { 27 | text-decoration: inherit; /* Inherit text-decoration and vertical align to ::before and ::after pseudo elements */ 28 | vertical-align: inherit; 29 | } 30 | 31 | /* Remove margin, padding of all elements and set background-no-repeat as default */ 32 | * { 33 | background-repeat: no-repeat; /* Set `background-repeat: no-repeat` to all elements */ 34 | padding: 0; /* Reset `padding` and `margin` of all elements */ 35 | margin: 0; 36 | } 37 | 38 | /* # ================================================================= 39 | # General elements 40 | # ================================================================= */ 41 | 42 | /* Add the correct display in iOS 4-7.*/ 43 | audio:not([controls]) { 44 | display: none; 45 | height: 0; 46 | } 47 | 48 | hr { 49 | overflow: visible; /* Show the overflow in Edge and IE */ 50 | } 51 | 52 | /* 53 | * Correct `block` display not defined for any HTML5 element in IE 8/9 54 | * Correct `block` display not defined for `details` or `summary` in IE 10/11 55 | * and Firefox 56 | * Correct `block` display not defined for `main` in IE 11 57 | */ 58 | article, 59 | aside, 60 | details, 61 | figcaption, 62 | figure, 63 | footer, 64 | header, 65 | main, 66 | menu, 67 | nav, 68 | section, 69 | summary { 70 | display: block; 71 | } 72 | 73 | summary { 74 | display: list-item; /* Add the correct display in all browsers */ 75 | } 76 | 77 | small { 78 | font-size: 80%; /* Set font-size to 80% in `small` elements */ 79 | } 80 | 81 | [hidden], 82 | template { 83 | display: none; /* Add the correct display in IE */ 84 | } 85 | 86 | abbr[title] { 87 | border-bottom: 1px dotted; /* Add a bordered underline effect in all browsers */ 88 | text-decoration: none; /* Remove text decoration in Firefox 40+ */ 89 | } 90 | 91 | a { 92 | color: #000; 93 | text-decoration: none; 94 | background-color: transparent; /* Remove the gray background on active links in IE 10 */ 95 | -webkit-text-decoration-skip: objects; /* Remove gaps in links underline in iOS 8+ and Safari 8+ */ 96 | } 97 | 98 | a:active, 99 | a:hover { 100 | outline-width: 0; /* Remove the outline when hovering in all browsers */ 101 | } 102 | 103 | code, 104 | kbd, 105 | pre, 106 | samp { 107 | font-family: monospace, monospace; /* Specify the font family of code elements */ 108 | } 109 | 110 | b, 111 | strong { 112 | font-weight: bolder; /* Correct style set to `bold` in Edge 12+, Safari 6.2+, and Chrome 18+ */ 113 | } 114 | 115 | dfn { 116 | font-style: italic; /* Address styling not present in Safari and Chrome */ 117 | } 118 | 119 | /* Address styling not present in IE 8/9 */ 120 | mark { 121 | background-color: #ff0; 122 | color: #000; 123 | } 124 | 125 | /* https://gist.github.com/unruthless/413930 */ 126 | sub, 127 | sup { 128 | font-size: 75%; 129 | line-height: 0; 130 | position: relative; 131 | vertical-align: baseline; 132 | } 133 | 134 | sub { 135 | bottom: -0.25em; 136 | } 137 | 138 | sup { 139 | top: -0.5em; 140 | } 141 | 142 | /* # ================================================================= 143 | # Forms 144 | # ================================================================= */ 145 | 146 | input { 147 | border-radius: 0; 148 | } 149 | 150 | /* Apply cursor pointer to button elements */ 151 | button, 152 | [type="button"], 153 | [type="reset"], 154 | [type="submit"], 155 | [role="button"] { 156 | cursor: pointer; 157 | } 158 | 159 | /* Replace pointer cursor in disabled elements */ 160 | [disabled] { 161 | cursor: default; 162 | } 163 | 164 | [type="number"] { 165 | width: auto; /* Firefox 36+ */ 166 | } 167 | 168 | [type="search"] { 169 | -webkit-appearance: textfield; /* Safari 8+ */ 170 | } 171 | 172 | [type="search"]::-webkit-search-cancel-button, 173 | [type="search"]::-webkit-search-decoration { 174 | -webkit-appearance: none; /* Safari 8 */ 175 | } 176 | 177 | textarea { 178 | overflow: auto; /* Internet Explorer 11+ */ 179 | resize: vertical; /* Specify textarea resizability */ 180 | } 181 | 182 | button, 183 | input, 184 | optgroup, 185 | select, 186 | textarea { 187 | font: inherit; /* Specify font inheritance of form elements */ 188 | } 189 | 190 | optgroup { 191 | font-weight: bold; /* Restore the font weight unset by the previous rule. */ 192 | } 193 | 194 | button { 195 | overflow: visible; /* Address `overflow` set to `hidden` in IE 8/9/10/11 */ 196 | } 197 | 198 | /* Remove inner padding and border in Firefox 4+ */ 199 | button::-moz-focus-inner, 200 | [type="button"]::-moz-focus-inner, 201 | [type="reset"]::-moz-focus-inner, 202 | [type="submit"]::-moz-focus-inner { 203 | border-style: 0; 204 | padding: 0; 205 | } 206 | 207 | /* Replace focus style removed in the border reset above */ 208 | button:-moz-focusring, 209 | [type="button"]::-moz-focus-inner, 210 | [type="reset"]::-moz-focus-inner, 211 | [type="submit"]::-moz-focus-inner { 212 | outline: 1px dotted ButtonText; 213 | } 214 | 215 | button, 216 | html [type="button"], /* Prevent a WebKit bug where (2) destroys native `audio` and `video`controls in Android 4 */ 217 | [type="reset"], 218 | [type="submit"] { 219 | -webkit-appearance: button; /* Correct the inability to style clickable types in iOS */ 220 | } 221 | 222 | button, 223 | select { 224 | text-transform: none; /* Firefox 40+, Internet Explorer 11- */ 225 | } 226 | 227 | /* Remove the default button styling in all browsers */ 228 | button, 229 | input, 230 | select, 231 | textarea { 232 | background-color: transparent; 233 | border-style: none; 234 | color: inherit; 235 | } 236 | 237 | /* Style select like a standard input */ 238 | select { 239 | -moz-appearance: none; /* Firefox 36+ */ 240 | -webkit-appearance: none; /* Chrome 41+ */ 241 | } 242 | 243 | select::-ms-expand { 244 | display: none; /* Internet Explorer 11+ */ 245 | } 246 | 247 | select::-ms-value { 248 | color: currentColor; /* Internet Explorer 11+ */ 249 | } 250 | 251 | legend { 252 | border: 0; /* Correct `color` not being inherited in IE 8/9/10/11 */ 253 | color: inherit; /* Correct the color inheritance from `fieldset` elements in IE */ 254 | display: table; /* Correct the text wrapping in Edge and IE */ 255 | max-width: 100%; /* Correct the text wrapping in Edge and IE */ 256 | white-space: normal; /* Correct the text wrapping in Edge and IE */ 257 | } 258 | 259 | ::-webkit-file-upload-button { 260 | -webkit-appearance: button; /* Correct the inability to style clickable types in iOS and Safari */ 261 | font: inherit; /* Change font properties to `inherit` in Chrome and Safari */ 262 | } 263 | 264 | [type="search"] { 265 | -webkit-appearance: textfield; /* Correct the odd appearance in Chrome and Safari */ 266 | outline-offset: -2px; /* Correct the outline style in Safari */ 267 | } 268 | 269 | /* # ================================================================= 270 | # Specify media element style 271 | # ================================================================= */ 272 | 273 | img { 274 | border-style: none; /* Remove border when inside `a` element in IE 8/9/10 */ 275 | } 276 | 277 | /* Add the correct vertical alignment in Chrome, Firefox, and Opera */ 278 | progress { 279 | vertical-align: baseline; 280 | } 281 | 282 | svg:not(:root) { 283 | overflow: hidden; /* Internet Explorer 11- */ 284 | } 285 | 286 | audio, 287 | canvas, 288 | progress, 289 | video { 290 | display: inline-block; /* Internet Explorer 11+, Windows Phone 8.1+ */ 291 | } 292 | 293 | /* # ================================================================= 294 | # Accessibility 295 | # ================================================================= */ 296 | 297 | /* Hide content from screens but not screenreaders */ 298 | @media screen { 299 | [hidden~="screen"] { 300 | display: inherit; 301 | } 302 | [hidden~="screen"]:not(:active):not(:focus):not(:target) { 303 | position: absolute !important; 304 | clip: rect(0 0 0 0) !important; 305 | } 306 | } 307 | 308 | /* Specify the progress cursor of updating elements */ 309 | [aria-busy="true"] { 310 | cursor: progress; 311 | } 312 | 313 | /* Specify the pointer cursor of trigger elements */ 314 | [aria-controls] { 315 | cursor: pointer; 316 | } 317 | 318 | /* Specify the unstyled cursor of disabled, not-editable, or otherwise inoperable elements */ 319 | [aria-disabled] { 320 | cursor: default; 321 | } 322 | 323 | /* # ================================================================= 324 | # Selection 325 | # ================================================================= */ 326 | 327 | /* Specify text selection background color and omit drop shadow */ 328 | 329 | ::-moz-selection { 330 | background-color: #b3d4fc; /* Required when declaring ::selection */ 331 | color: #000; 332 | text-shadow: none; 333 | } 334 | 335 | ::selection { 336 | background-color: #b3d4fc; /* Required when declaring ::selection */ 337 | color: #000; 338 | text-shadow: none; 339 | } 340 | 341 | -------------------------------------------------------------------------------- /src/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hbbaly/vue-blog/b624636e9ca48174e0bb49ef28d87c3e26be8a60/src/assets/images/logo.png -------------------------------------------------------------------------------- /src/assets/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hbbaly/vue-blog/b624636e9ca48174e0bb49ef28d87c3e26be8a60/src/assets/img/logo.png -------------------------------------------------------------------------------- /src/assets/js/commen.js: -------------------------------------------------------------------------------- 1 | export function sub(obj,res){ 2 | obj.btnText="提交"; 3 | obj.editLoading = false; 4 | 5 | if(res.data.code==200){ 6 | obj.$notify({ 7 | title:'成功', 8 | message:'提交成功', 9 | type:'success' 10 | }) 11 | }else if(res.data.code==401){ 12 | this.$notify({ 13 | title:'失败', 14 | message, 15 | type:'error' 16 | }) 17 | setTimeout(()=>{ 18 | this.$router.replace({path:'/login'}) 19 | },500) 20 | return false//阻止继续执行 21 | // 需要优化 22 | } 23 | obj.formVisible = false; 24 | obj.getLists(); 25 | } 26 | -------------------------------------------------------------------------------- /src/assets/js/marked.js: -------------------------------------------------------------------------------- 1 | 2 | import highlight from 'highlight.js'; 3 | import marked from 'marked'; 4 | import 'highlight.js/styles/monokai-sublime.css' 5 | const languages = ['cpp', 'xml', 'bash', 'coffeescript', 'css', 'markdown', 'http', 'java', 'javascript', 'json', 'less', 'makefile', 'nginx', 'php', 'python', 'scss', 'sql', 'stylus']; 6 | highlight.registerLanguage('cpp', require('highlight.js/lib/languages/cpp')); 7 | highlight.registerLanguage('xml', require('highlight.js/lib/languages/xml')); 8 | highlight.registerLanguage('bash', require('highlight.js/lib/languages/bash')); 9 | highlight.registerLanguage('coffeescript', require('highlight.js/lib/languages/coffeescript')); 10 | highlight.registerLanguage('css', require('highlight.js/lib/languages/css')); 11 | highlight.registerLanguage('markdown', require('highlight.js/lib/languages/markdown')); 12 | highlight.registerLanguage('http', require('highlight.js/lib/languages/http')); 13 | highlight.registerLanguage('java', require('highlight.js/lib/languages/java')); 14 | highlight.registerLanguage('javascript', require('highlight.js/lib/languages/javascript')); 15 | highlight.registerLanguage('json', require('highlight.js/lib/languages/json')); 16 | highlight.registerLanguage('less', require('highlight.js/lib/languages/less')); 17 | highlight.registerLanguage('makefile', require('highlight.js/lib/languages/makefile')); 18 | highlight.registerLanguage('nginx', require('highlight.js/lib/languages/nginx')); 19 | highlight.registerLanguage('php', require('highlight.js/lib/languages/php')); 20 | highlight.registerLanguage('python', require('highlight.js/lib/languages/python')); 21 | highlight.registerLanguage('scss', require('highlight.js/lib/languages/scss')); 22 | highlight.registerLanguage('sql', require('highlight.js/lib/languages/sql')); 23 | highlight.registerLanguage('stylus', require('highlight.js/lib/languages/stylus')); 24 | highlight.configure({ 25 | classPrefix: '', // don't append class prefix 26 | }); 27 | // https://github.com/chjj/marked 28 | marked.setOptions({ 29 | renderer: new marked.Renderer(), 30 | gfm: true, 31 | pedantic: false, 32 | sanitize: false, 33 | tables: true, 34 | breaks: true, 35 | smartLists: true, 36 | smartypants: true, 37 | highlight: function (code, lang) { 38 | if (!lang) { 39 | return; 40 | } 41 | if (!~languages.indexOf(lang)) { 42 | return highlight.highlightAuto(code).value; 43 | } 44 | return highlight.highlight(lang, code).value; 45 | }, 46 | }); 47 | export default marked; 48 | -------------------------------------------------------------------------------- /src/components/Comment.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 40 | 41 | 75 | -------------------------------------------------------------------------------- /src/components/Info.vue: -------------------------------------------------------------------------------- 1 | 40 | 123 | 131 | -------------------------------------------------------------------------------- /src/components/Item.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 41 | 42 | 68 | -------------------------------------------------------------------------------- /src/components/ProgressBar.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | 88 | 89 | 102 | -------------------------------------------------------------------------------- /src/components/Spinner.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 16 | 17 | 55 | -------------------------------------------------------------------------------- /src/components/header.vue: -------------------------------------------------------------------------------- 1 | 13 | 57 | -------------------------------------------------------------------------------- /src/components/sidebar.vue: -------------------------------------------------------------------------------- 1 | 19 | 55 | -------------------------------------------------------------------------------- /src/config/common.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'APP_NAME': 'vue-blog', 3 | 'APP_DESC': '' 4 | } 5 | -------------------------------------------------------------------------------- /src/config/development.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | SERVER: '' 3 | } 4 | 5 | -------------------------------------------------------------------------------- /src/config/index.js: -------------------------------------------------------------------------------- 1 | import commonConfig from './common.config' 2 | import developmentConfig from './development.config' 3 | import productionConfig from './production.config' 4 | 5 | function buildConfig (type) { 6 | let envConfig = process.env.NODE_ENV === 'production' ? productionConfig : developmentConfig 7 | return Object.assign(commonConfig, envConfig) 8 | } 9 | 10 | export default buildConfig() 11 | -------------------------------------------------------------------------------- /src/config/production.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | } 3 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | // The Vue build version to load with the `import` command 2 | // (runtime-only or standalone) has been set in webpack.base.conf with an alias. 3 | import Vue from 'vue' 4 | import App from './App' 5 | import router from './router' 6 | import ElementUI from 'element-ui' 7 | import 'element-ui/lib/theme-chalk/index.css' 8 | import './assets/css/reset.css' 9 | // import ProgressBar from './components/ProgressBar.vue' 10 | Vue.use(ElementUI) 11 | Vue.config.productionTip = false 12 | // global progress bar 13 | // const bar = Vue.prototype.$bar = new Vue(ProgressBar).$mount() 14 | // document.body.appendChild(bar.$el) 15 | /* eslint-disable no-new */ 16 | new Vue({ 17 | el: '#app', 18 | router, 19 | components: { App }, 20 | template: '' 21 | }) 22 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | // route-level code splitting 4 | const Index = () => import('../views/front/Index.vue') 5 | const AdminView = () => import('../views/admin/index.vue') 6 | const ArticleDetail = () => import('../views/front/ArticleDetail.vue') 7 | const TimeLine = () => import('../views/front/TimeLine.vue') 8 | const Tags = () => import('../views/front/Tags.vue') 9 | const ArticleList = () => import('../views/front/ArticleList.vue') 10 | Vue.use(Router) 11 | 12 | export default new Router({ 13 | routes: [ 14 | { 15 | path: '/', 16 | name: 'Index', 17 | component: Index 18 | }, 19 | { 20 | path: '/admin', 21 | name: 'Admin', 22 | component: AdminView 23 | }, 24 | { 25 | path: '/article/:_id', 26 | name: 'ArticleDetail', 27 | component: ArticleDetail 28 | }, 29 | { 30 | path: '/timeline', 31 | name: 'TimeLine', 32 | component: TimeLine 33 | }, 34 | { 35 | path: '/tags', 36 | name: 'Tags', 37 | component: Tags 38 | }, 39 | { 40 | path: '/:type/article', 41 | name: 'ArticleList', 42 | component: ArticleList 43 | } 44 | ] 45 | }) 46 | -------------------------------------------------------------------------------- /src/router/routes.js: -------------------------------------------------------------------------------- 1 | import Index from '@/view/index/IndexView' 2 | import Register from '@/view/user/register/IndexView' 3 | import Login from '@/view/user/login/IndexView' 4 | import User from '@/view/user/index/IndexView' 5 | import CreateArticle from '@/view/user/article/create/IndexView' 6 | import ArticleList from '@/view/user/article/list/IndexView' 7 | import Tag from '@/view/user/article/tag/IndexView' 8 | import ChangePass from '@/view/user/changepass/IndexView' 9 | import Detail from '@/view/user/article/detail/IndexView' 10 | import NotFound from '@/view/notfound/NotFoundView' 11 | export default [ 12 | { 13 | path:'/', 14 | component:Index, 15 | }, 16 | { 17 | path: '/user', 18 | component: User, 19 | children: [ 20 | { 21 | path:'',redirect: {name:'ArticleCreate'} 22 | }, 23 | { 24 | name: 'ArticleCreate', 25 | path: '/article/create', 26 | component: CreateArticle 27 | }, 28 | { 29 | path: '/article/list', 30 | component: ArticleList 31 | }, 32 | { 33 | path: '/article/tag', 34 | component: Tag 35 | }, 36 | { 37 | path: '/settings/changepass', 38 | component: ChangePass 39 | } 40 | ] 41 | }, 42 | { 43 | path: '/user/register', 44 | component: Register 45 | }, 46 | { 47 | path: '/user/login', 48 | component: Login 49 | }, 50 | { 51 | path: '/user/article/detail/:_id', 52 | component: Detail 53 | }, 54 | { 55 | name: 'NotFound', 56 | path:'*', 57 | component: NotFound 58 | } 59 | ] 60 | -------------------------------------------------------------------------------- /src/store/actions.js: -------------------------------------------------------------------------------- 1 | import API from '../api' 2 | 3 | export default { 4 | GET_TOPICS: ({ 5 | commit 6 | }) => { 7 | return API.home 8 | .getTopics({page:1,limit:20,tab:'share'}) 9 | .then((res) => { 10 | if (res.status !== -404 && res.data && res.data) { 11 | commit('GET_TOPICS', res.data.data) 12 | } 13 | }).catch(() => { 14 | 15 | }) 16 | }, 17 | } 18 | -------------------------------------------------------------------------------- /src/store/getters.js: -------------------------------------------------------------------------------- 1 | export default { 2 | // ids of the items that should be currently displayed based on 3 | // current list type and current pagination 4 | topicList (state) { 5 | return state.topicList 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import actions from './actions' 4 | import mutations from './mutations' 5 | import getters from './getters' 6 | 7 | Vue.use(Vuex) 8 | 9 | export function createStore () { 10 | return new Vuex.Store({ 11 | state: { 12 | topicList:[] 13 | }, 14 | actions, 15 | mutations, 16 | getters 17 | }) 18 | } 19 | -------------------------------------------------------------------------------- /src/store/mutations.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | export default { 4 | GET_TOPICS: (state, list) => { 5 | state.topicList = list 6 | }, 7 | } 8 | -------------------------------------------------------------------------------- /src/utils/filters.js: -------------------------------------------------------------------------------- 1 | export function host (url) { 2 | const host = url.replace(/^https?:\/\//, '').replace(/\/.*$/, '') 3 | const parts = host.split('.').slice(-3) 4 | if (parts[0] === 'www') parts.shift() 5 | return parts.join('.') 6 | } 7 | 8 | export function timeAgo (time) { 9 | const between = Date.now() / 1000 - Number(time) 10 | if (between < 3600) { 11 | return pluralize(~~(between / 60), ' minute') 12 | } else if (between < 86400) { 13 | return pluralize(~~(between / 3600), ' hour') 14 | } else { 15 | return pluralize(~~(between / 86400), ' day') 16 | } 17 | } 18 | 19 | function pluralize (time, label) { 20 | if (time === 1) { 21 | return time + label 22 | } 23 | return time + label + 's' 24 | } 25 | -------------------------------------------------------------------------------- /src/utils/http.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | import axios from 'axios' 3 | import qs from 'qs' 4 | import config from '../config/index' 5 | axios.interceptors.request.use( 6 | config => { 7 | // loading 8 | return config 9 | }, 10 | error => { 11 | return Promise.reject(error) 12 | } 13 | ) 14 | 15 | axios.interceptors.response.use( 16 | response => { 17 | return response 18 | }, 19 | error => { 20 | return Promise.resolve(error.response) 21 | } 22 | ) 23 | 24 | function checkStatus(response) { 25 | // loading 26 | // 如果http状态码正常,则直接返回数据 27 | if ( 28 | response && 29 | (response.status === 200 || 30 | response.status === 304 || 31 | response.status === 400) 32 | ) { 33 | if (response.data.code === 200) { 34 | return response 35 | // 如果不需要除了data之外的数据,可以直接 return response.data 36 | } 37 | return response 38 | } 39 | // 异常状态下,把错误信息返回去 40 | return { 41 | status: -404, 42 | msg: '网络异常' 43 | } 44 | } 45 | 46 | function checkCode(res) { 47 | // 如果code异常(这里已经包括网络错误,服务器错误,后端抛出的错误),可以弹出一个错误提示,告诉用户 48 | if (res.status === -404) { 49 | } 50 | if (res.data && !res.data.success) { 51 | } 52 | return res 53 | } 54 | 55 | export default { 56 | post(url, data) { 57 | return axios({ 58 | method: 'post', 59 | baseURL: config.API_SERVER, 60 | url, 61 | data: qs.stringify(data), 62 | timeout: 10000, 63 | headers: { 64 | 'X-Requested-With': 'XMLHttpRequest', 65 | 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' 66 | } 67 | }) 68 | .then(response => { 69 | return checkStatus(response) 70 | }) 71 | .then(res => { 72 | return checkCode(res) 73 | }) 74 | }, 75 | get(url, params) { 76 | return axios({ 77 | method: 'get', 78 | baseURL: config.API_SERVER, 79 | url, 80 | params, // get 请求时带的参数 81 | timeout: 10000, 82 | headers: { 83 | 'X-Requested-With': 'XMLHttpRequest' 84 | } 85 | }) 86 | .then(response => { 87 | return checkStatus(response) 88 | }) 89 | .then(res => { 90 | return checkCode(res) 91 | }) 92 | .catch(err => { 93 | if (err) { 94 | } 95 | }) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hbbaly/vue-blog/b624636e9ca48174e0bb49ef28d87c3e26be8a60/src/utils/index.js -------------------------------------------------------------------------------- /src/utils/marked.js: -------------------------------------------------------------------------------- 1 | import highlight from 'highlight.js'; 2 | import marked from 'marked'; 3 | import 'highlight.js/styles/darcula.css' 4 | const languages = ['cpp', 'xml', 'bash', 'coffeescript', 'css', 'markdown', 'http', 'java', 'javascript', 'json', 'less', 'makefile', 'nginx', 'php', 'python', 'scss', 'sql', 'stylus']; 5 | highlight.registerLanguage('cpp', require('highlight.js/lib/languages/cpp')); 6 | highlight.registerLanguage('xml', require('highlight.js/lib/languages/xml')); 7 | highlight.registerLanguage('bash', require('highlight.js/lib/languages/bash')); 8 | highlight.registerLanguage('coffeescript', require('highlight.js/lib/languages/coffeescript')); 9 | highlight.registerLanguage('css', require('highlight.js/lib/languages/css')); 10 | highlight.registerLanguage('markdown', require('highlight.js/lib/languages/markdown')); 11 | highlight.registerLanguage('http', require('highlight.js/lib/languages/http')); 12 | highlight.registerLanguage('java', require('highlight.js/lib/languages/java')); 13 | highlight.registerLanguage('javascript', require('highlight.js/lib/languages/javascript')); 14 | highlight.registerLanguage('json', require('highlight.js/lib/languages/json')); 15 | highlight.registerLanguage('less', require('highlight.js/lib/languages/less')); 16 | highlight.registerLanguage('makefile', require('highlight.js/lib/languages/makefile')); 17 | highlight.registerLanguage('nginx', require('highlight.js/lib/languages/nginx')); 18 | highlight.registerLanguage('php', require('highlight.js/lib/languages/php')); 19 | highlight.registerLanguage('python', require('highlight.js/lib/languages/python')); 20 | highlight.registerLanguage('scss', require('highlight.js/lib/languages/scss')); 21 | highlight.registerLanguage('sql', require('highlight.js/lib/languages/sql')); 22 | highlight.registerLanguage('stylus', require('highlight.js/lib/languages/stylus')); 23 | highlight.configure({ 24 | classPrefix: '', // don't append class prefix 25 | }); 26 | // https://github.com/chjj/marked 27 | marked.setOptions({ 28 | renderer: new marked.Renderer(), 29 | gfm: true, 30 | pedantic: false, 31 | sanitize: false, 32 | tables: true, 33 | breaks: true, 34 | smartLists: true, 35 | smartypants: true, 36 | highlight: function (code, lang) { 37 | if (!lang) { 38 | return; 39 | } 40 | if (!~languages.indexOf(lang)) { 41 | return highlight.highlightAuto(code).value; 42 | } 43 | return highlight.highlight(lang, code).value; 44 | }, 45 | }); 46 | export default marked; 47 | -------------------------------------------------------------------------------- /src/utils/meta.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Page meta message operate 3 | */ 4 | function getMeta (vm) { 5 | const { 6 | meta 7 | } = vm.$options 8 | return typeof meta === 'function' 9 | ? meta.call(vm) 10 | : meta 11 | } 12 | 13 | const serverMetaMixin = { 14 | created () { 15 | const meta = getMeta(this) 16 | if (meta) { 17 | let metaTags = [] 18 | 19 | meta.forEach(item => { 20 | metaTags.push(``) 21 | }) 22 | 23 | this.$ssrContext.meta = metaTags.join('\r\n') 24 | } 25 | } 26 | } 27 | 28 | const clientMetaMixin = { 29 | mounted () { 30 | // const title = getMeta(this) 31 | // if (title) { 32 | // document.title = title 33 | // } 34 | } 35 | } 36 | 37 | // 可以通过 `webpack.DefinePlugin` 注入 `VUE_ENV` 38 | export default process.env.VUE_ENV === 'server' 39 | ? serverMetaMixin 40 | : clientMetaMixin 41 | -------------------------------------------------------------------------------- /src/utils/store.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 全局 store 操作方法 3 | * 4 | * store : sessionStorage 、 localStorage、cookie 5 | * params: key 值 6 | * type 是类型,sessionStorage 、 localStorage、cookie三选一 7 | * day 此参数只在type === cookie 的时候存在 8 | */ 9 | function saveWithKey (type, params, val, day = 7) { 10 | if (type === 'localStorage') localStorage.setItem(params, val) 11 | if (type === 'sessionStorage') sessionStorage.setItem(params, val) 12 | } 13 | 14 | function getWithKey (type, params, day = 7) { 15 | if (type === 'localStorage') return localStorage.getItem(params) 16 | if (type === 'sessionStorage') return sessionStorage.getItem(params) 17 | } 18 | 19 | function clearWithKey (type, params) { 20 | if (type === 'localStorage') localStorage.removeItem(params) 21 | if (type === 'sessionStorage') sessionStorage.removeItem(params) 22 | } 23 | export default { 24 | saveWithKey, 25 | getWithKey, 26 | clearWithKey 27 | } 28 | -------------------------------------------------------------------------------- /src/utils/title.js: -------------------------------------------------------------------------------- 1 | function getTitle (vm) { 2 | const { title } = vm.$options 3 | if (title) { 4 | return typeof title === 'function' 5 | ? title.call(vm) 6 | : title 7 | } 8 | } 9 | 10 | const serverTitleMixin = { 11 | created () { 12 | const title = getTitle(this) 13 | if (title) { 14 | this.$ssrContext.title = `${title}` 15 | } 16 | } 17 | } 18 | 19 | const clientTitleMixin = { 20 | mounted () { 21 | const title = getTitle(this) 22 | if (title) { 23 | document.title = `${title}` 24 | } 25 | } 26 | } 27 | 28 | export default process.env.VUE_ENV === 'server' 29 | ? serverTitleMixin 30 | : clientTitleMixin 31 | -------------------------------------------------------------------------------- /src/view/index/components/ArticleListMView.vue: -------------------------------------------------------------------------------- 1 | 10 | 49 | -------------------------------------------------------------------------------- /src/view/index/components/ArticleListVIew.vue: -------------------------------------------------------------------------------- 1 | 19 | 44 | -------------------------------------------------------------------------------- /src/view/index/components/ArticleTagView.vue: -------------------------------------------------------------------------------- 1 | 13 | 29 | 76 | -------------------------------------------------------------------------------- /src/view/index/components/BannerListView.vue: -------------------------------------------------------------------------------- 1 | 12 | 30 | -------------------------------------------------------------------------------- /src/view/index/components/allTagView.vue: -------------------------------------------------------------------------------- 1 | 13 | 29 | 67 | -------------------------------------------------------------------------------- /src/view/index/indexView.vue: -------------------------------------------------------------------------------- 1 | 34 | 161 | 222 | -------------------------------------------------------------------------------- /src/view/notfound/NotFoundView.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 20 | 21 | 43 | -------------------------------------------------------------------------------- /src/view/user/article/create/IndexView.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 195 | -------------------------------------------------------------------------------- /src/view/user/article/detail/IndexView.vue: -------------------------------------------------------------------------------- 1 | 17 | 56 | -------------------------------------------------------------------------------- /src/view/user/article/list/IndexView.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 107 | 124 | -------------------------------------------------------------------------------- /src/view/user/article/tag/IndexView.vue: -------------------------------------------------------------------------------- 1 | 30 | 152 | -------------------------------------------------------------------------------- /src/view/user/center/IndexView.vue: -------------------------------------------------------------------------------- 1 | 19 | -------------------------------------------------------------------------------- /src/view/user/changepass/IndexView.vue: -------------------------------------------------------------------------------- 1 | 6 | 39 | 47 | -------------------------------------------------------------------------------- /src/view/user/index/IndexView.vue: -------------------------------------------------------------------------------- 1 | 48 | 74 | 83 | 84 | -------------------------------------------------------------------------------- /src/view/user/login/IndexView.vue: -------------------------------------------------------------------------------- 1 | 33 | 113 | 121 | -------------------------------------------------------------------------------- /src/view/user/register/IndexView.vue: -------------------------------------------------------------------------------- 1 | 6 | 46 | 54 | -------------------------------------------------------------------------------- /src/views/admin/components/article.vue: -------------------------------------------------------------------------------- 1 | 7 | 26 | 61 | 121 | -------------------------------------------------------------------------------- /src/views/admin/components/editor.vue: -------------------------------------------------------------------------------- 1 | 2 | 34 | 165 | -------------------------------------------------------------------------------- /src/views/admin/components/tags.vue: -------------------------------------------------------------------------------- 1 | 2 | 8 | 41 | 104 | -------------------------------------------------------------------------------- /src/views/admin/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 8 | 24 | 54 | -------------------------------------------------------------------------------- /src/views/front/ArticleDetail.vue: -------------------------------------------------------------------------------- 1 | 15 | 70 | 81 | -------------------------------------------------------------------------------- /src/views/front/ArticleList.vue: -------------------------------------------------------------------------------- 1 | 7 | 22 | 64 | 101 | -------------------------------------------------------------------------------- /src/views/front/Index.vue: -------------------------------------------------------------------------------- 1 | 50 | 96 | -------------------------------------------------------------------------------- /src/views/front/Mine.vue: -------------------------------------------------------------------------------- 1 | 22 | 39 | -------------------------------------------------------------------------------- /src/views/front/Tags.vue: -------------------------------------------------------------------------------- 1 | 9 | 38 | -------------------------------------------------------------------------------- /src/views/front/TimeLine.vue: -------------------------------------------------------------------------------- 1 | 2 | 8 | 41 | 161 | -------------------------------------------------------------------------------- /src/views/front/components/RelateSkill.vue: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 37 | 38 | -------------------------------------------------------------------------------- /src/views/hello/HelloView.vue: -------------------------------------------------------------------------------- 1 | 2 | 7 | 18 | -------------------------------------------------------------------------------- /static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hbbaly/vue-blog/b624636e9ca48174e0bb49ef28d87c3e26be8a60/static/.gitkeep -------------------------------------------------------------------------------- /static/img/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hbbaly/vue-blog/b624636e9ca48174e0bb49ef28d87c3e26be8a60/static/img/1.png -------------------------------------------------------------------------------- /static/img/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hbbaly/vue-blog/b624636e9ca48174e0bb49ef28d87c3e26be8a60/static/img/2.png -------------------------------------------------------------------------------- /static/img/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hbbaly/vue-blog/b624636e9ca48174e0bb49ef28d87c3e26be8a60/static/img/3.png -------------------------------------------------------------------------------- /static/img/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hbbaly/vue-blog/b624636e9ca48174e0bb49ef28d87c3e26be8a60/static/img/4.png -------------------------------------------------------------------------------- /static/img/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hbbaly/vue-blog/b624636e9ca48174e0bb49ef28d87c3e26be8a60/static/img/5.png -------------------------------------------------------------------------------- /static/img/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hbbaly/vue-blog/b624636e9ca48174e0bb49ef28d87c3e26be8a60/static/img/6.png -------------------------------------------------------------------------------- /static/img/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hbbaly/vue-blog/b624636e9ca48174e0bb49ef28d87c3e26be8a60/static/img/7.png -------------------------------------------------------------------------------- /static/img/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hbbaly/vue-blog/b624636e9ca48174e0bb49ef28d87c3e26be8a60/static/img/8.png -------------------------------------------------------------------------------- /test/e2e/custom-assertions/elementCount.js: -------------------------------------------------------------------------------- 1 | // A custom Nightwatch assertion. 2 | // The assertion name is the filename. 3 | // Example usage: 4 | // 5 | // browser.assert.elementCount(selector, count) 6 | // 7 | // For more information on custom assertions see: 8 | // http://nightwatchjs.org/guide#writing-custom-assertions 9 | 10 | exports.assertion = function (selector, count) { 11 | this.message = 'Testing if element <' + selector + '> has count: ' + count 12 | this.expected = count 13 | this.pass = function (val) { 14 | return val === this.expected 15 | } 16 | this.value = function (res) { 17 | return res.value 18 | } 19 | this.command = function (cb) { 20 | var self = this 21 | return this.api.execute(function (selector) { 22 | return document.querySelectorAll(selector).length 23 | }, [selector], function (res) { 24 | cb.call(self, res) 25 | }) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /test/e2e/nightwatch.conf.js: -------------------------------------------------------------------------------- 1 | require('babel-register') 2 | var config = require('../../config') 3 | 4 | // http://nightwatchjs.org/gettingstarted#settings-file 5 | module.exports = { 6 | src_folders: ['test/e2e/specs'], 7 | output_folder: 'test/e2e/reports', 8 | custom_assertions_path: ['test/e2e/custom-assertions'], 9 | 10 | selenium: { 11 | start_process: true, 12 | server_path: require('selenium-server').path, 13 | host: '127.0.0.1', 14 | port: 4444, 15 | cli_args: { 16 | 'webdriver.chrome.driver': require('chromedriver').path 17 | } 18 | }, 19 | 20 | test_settings: { 21 | default: { 22 | selenium_port: 4444, 23 | selenium_host: 'localhost', 24 | silent: true, 25 | globals: { 26 | devServerURL: 'http://localhost:' + (process.env.PORT || config.dev.port) 27 | } 28 | }, 29 | 30 | chrome: { 31 | desiredCapabilities: { 32 | browserName: 'chrome', 33 | javascriptEnabled: true, 34 | acceptSslCerts: true 35 | } 36 | }, 37 | 38 | firefox: { 39 | desiredCapabilities: { 40 | browserName: 'firefox', 41 | javascriptEnabled: true, 42 | acceptSslCerts: true 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test/e2e/runner.js: -------------------------------------------------------------------------------- 1 | // 1. start the dev server using production config 2 | process.env.NODE_ENV = 'testing' 3 | 4 | const webpack = require('webpack') 5 | const DevServer = require('webpack-dev-server') 6 | 7 | const webpackConfig = require('../../build/webpack.prod.conf') 8 | const devConfigPromise = require('../../build/webpack.dev.conf') 9 | 10 | let server 11 | 12 | devConfigPromise.then(devConfig => { 13 | const devServerOptions = devConfig.devServer 14 | const compiler = webpack(webpackConfig) 15 | server = new DevServer(compiler, devServerOptions) 16 | const port = devServerOptions.port 17 | const host = devServerOptions.host 18 | return server.listen(port, host) 19 | }) 20 | .then(() => { 21 | // 2. run the nightwatch test suite against it 22 | // to run in additional browsers: 23 | // 1. add an entry in test/e2e/nightwatch.conf.js under "test_settings" 24 | // 2. add it to the --env flag below 25 | // or override the environment flag, for example: `npm run e2e -- --env chrome,firefox` 26 | // For more information on Nightwatch's config file, see 27 | // http://nightwatchjs.org/guide#settings-file 28 | let opts = process.argv.slice(2) 29 | if (opts.indexOf('--config') === -1) { 30 | opts = opts.concat(['--config', 'test/e2e/nightwatch.conf.js']) 31 | } 32 | if (opts.indexOf('--env') === -1) { 33 | opts = opts.concat(['--env', 'chrome']) 34 | } 35 | 36 | const spawn = require('cross-spawn') 37 | const runner = spawn('./node_modules/.bin/nightwatch', opts, { stdio: 'inherit' }) 38 | 39 | runner.on('exit', function (code) { 40 | server.close() 41 | process.exit(code) 42 | }) 43 | 44 | runner.on('error', function (err) { 45 | server.close() 46 | throw err 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /test/e2e/specs/test.js: -------------------------------------------------------------------------------- 1 | // For authoring Nightwatch tests, see 2 | // http://nightwatchjs.org/guide#usage 3 | 4 | module.exports = { 5 | 'default e2e tests': function (browser) { 6 | // automatically uses dev Server port from /config.index.js 7 | // default: http://localhost:8080 8 | // see nightwatch.conf.js 9 | const devServer = browser.globals.devServerURL 10 | 11 | browser 12 | .url(devServer) 13 | .waitForElementVisible('#app', 5000) 14 | .assert.elementPresent('.hello') 15 | .assert.containsText('h1', 'Welcome to Your Vue.js App') 16 | .assert.elementCount('img', 1) 17 | .end() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/unit/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jest": true 4 | }, 5 | "globals": { 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/unit/jest.conf.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | rootDir: path.resolve(__dirname, '../../'), 5 | moduleFileExtensions: [ 6 | 'js', 7 | 'json', 8 | 'vue' 9 | ], 10 | moduleNameMapper: { 11 | '^@/(.*)$': '/src/$1' 12 | }, 13 | transform: { 14 | '^.+\\.js$': '/node_modules/babel-jest', 15 | '.*\\.(vue)$': '/node_modules/vue-jest' 16 | }, 17 | testPathIgnorePatterns: [ 18 | '/test/e2e' 19 | ], 20 | snapshotSerializers: ['/node_modules/jest-serializer-vue'], 21 | setupFiles: ['/test/unit/setup'], 22 | mapCoverage: true, 23 | coverageDirectory: '/test/unit/coverage', 24 | collectCoverageFrom: [ 25 | 'src/**/*.{js,vue}', 26 | '!src/main.js', 27 | '!src/router/index.js', 28 | '!**/node_modules/**' 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /test/unit/setup.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | Vue.config.productionTip = false 4 | -------------------------------------------------------------------------------- /test/unit/specs/HelloWorld.spec.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import HelloWorld from '@/components/HelloWorld' 3 | 4 | describe('HelloWorld.vue', () => { 5 | it('should render correct contents', () => { 6 | const Constructor = Vue.extend(HelloWorld) 7 | const vm = new Constructor().$mount() 8 | expect(vm.$el.querySelector('.hello h1').textContent) 9 | .toEqual('Welcome to Your Vue.js App') 10 | }) 11 | }) 12 | --------------------------------------------------------------------------------