├── .babelrc
├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── .postcssrc.js
├── LICENSE
├── README.md
├── build
├── build.js
├── check-versions.js
├── dev-client.js
├── dev-server.js
├── utils.js
├── vue-loader.conf.js
├── webpack.base.conf.js
├── webpack.dev.conf.js
└── webpack.prod.conf.js
├── config
├── dev.env.js
├── index.js
└── prod.env.js
├── index.html
├── package.json
├── server
├── api
│ ├── article.js
│ ├── comment.js
│ ├── draft.js
│ ├── index.js
│ ├── login.js
│ ├── tag.js
│ └── user.js
├── app.js
├── db
│ ├── data.json
│ ├── db.js
│ └── sequence.js
├── email.js
├── middlewares
│ └── confirmToken.js
└── package.json
├── src
├── App.vue
├── assets
│ ├── css
│ │ ├── animate.min.css
│ │ ├── common.scss
│ │ ├── highlight.scss
│ │ ├── icon.scss
│ │ ├── index.scss
│ │ ├── marked.scss
│ │ └── normalize.css
│ └── js
│ │ └── highlight.pack.js
├── components
│ ├── back
│ │ ├── account.vue
│ │ ├── admin.vue
│ │ ├── component
│ │ │ ├── ArticleContent.vue
│ │ │ └── TagInput.vue
│ │ ├── drafts.vue
│ │ ├── editor.vue
│ │ ├── login.vue
│ │ ├── posts.vue
│ │ └── search.vue
│ ├── front
│ │ ├── AboutMe.vue
│ │ ├── Articles.vue
│ │ ├── Home.vue
│ │ ├── SearchResult.vue
│ │ ├── article.vue
│ │ ├── component
│ │ │ ├── ArticleComment.vue
│ │ │ ├── ArticleList.vue
│ │ │ ├── MyFooter.vue
│ │ │ └── MyHeader.vue
│ │ ├── contact.vue
│ │ └── index.vue
│ └── share
│ │ ├── DialogBox.vue
│ │ ├── FireCanvas.vue
│ │ └── spinner.vue
├── lib
│ └── utils.js
├── main.js
├── router
│ └── index.js
└── store
│ ├── actions.js
│ ├── getters.js
│ ├── index.js
│ └── mutations.js
└── static
├── .gitkeep
├── favicon.ico
├── me.jpg
├── reviewer.jpg
└── sunset.jpg
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | ["env", { "modules": false }],
4 | "stage-2"
5 | ],
6 | "plugins": ["transform-runtime"],
7 | "comments": false,
8 | "env": {
9 | "test": {
10 | "presets": ["env", "stage-2"],
11 | "plugins": [ "istanbul" ]
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/.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/*.js
2 | config/*.js
3 | src/assets/js/*.js
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | // http://eslint.org/docs/user-guide/configuring
2 |
3 | module.exports = {
4 | root: true,
5 | parser: 'babel-eslint',
6 | parserOptions: {
7 | sourceType: 'module'
8 | },
9 | env: {
10 | browser: true,
11 | },
12 | // https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style
13 | extends: 'standard',
14 | // required to lint *.vue files
15 | plugins: [
16 | 'html'
17 | ],
18 | // add your custom rules here
19 | 'rules': {
20 | // allow paren-less arrow functions
21 | 'arrow-parens': 0,
22 | // allow async-await
23 | 'generator-star-spacing': 0,
24 | // allow debugger during development
25 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
26 | 'indent': ["error", 4],
27 | 'no-return-assign': ["error", "always"],
28 | 'no-multi-spaces': ["error", { exceptions: { "ImportDeclaration": true } }]
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules/
3 | dist/
4 | npm-debug.log
5 | yarn-error.log
6 | .idea/
7 |
--------------------------------------------------------------------------------
/.postcssrc.js:
--------------------------------------------------------------------------------
1 | // https://github.com/michael-ciniawsky/postcss-load-config
2 |
3 | module.exports = {
4 | "plugins": {
5 | // to edit target browsers: use "browserlist" field in package.json
6 | "autoprefixer": {}
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 许浩东
4 |
5 | Inspired by ycwalker link: https://github.com/ycwalker/CMS-of-Blog
6 |
7 | Permission is hereby granted, free of charge, to any person obtaining a copy
8 | of this software and associated documentation files (the "Software"), to deal
9 | in the Software without restriction, including without limitation the rights
10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | copies of the Software, and to permit persons to whom the Software is
12 | furnished to do so, subject to the following conditions:
13 |
14 | The above copyright notice and this permission notice shall be included in all
15 | copies or substantial portions of the Software.
16 |
17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | SOFTWARE.
24 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # vue-blog
2 |
3 | > Vue.js+Node.js+Mongodb+Express的前后端分离的个人博客
4 |
5 |
6 | ## 主要技术构成
7 | 前端主要技术栈为vue.js、vue-router、 vue-resource、 vuex
8 |
9 | 后端主要技术栈为node.js、 mongodb、 Express
10 |
11 | ## 博客功能
12 | ### 前台页面
13 | - canvas粒子效果
14 | - 搜索文章
15 | - 动态显示文章
16 | - 留言
17 | - 文章分类显示
18 | - 评论文章
19 | - 文章目录
20 |
21 | ### 后台管理页面
22 | - 发布文章
23 | - 存为草稿
24 | - 搜索文章
25 | - 修改账户
26 | - 权限验证
27 | - 登录验证
28 | - markdown编辑器
29 |
30 | ## Setup
31 |
32 | 运行环境
33 | - node.js
34 | - mongoDB
35 |
36 | 克隆远程库
37 | ```
38 | git clone https://github.com/FatDong1/VueBlog.git
39 | ```
40 | 进入项目目录VueBlog后,安装依赖
41 | ```
42 | npm install
43 | ```
44 | 安装完数据库后,启动mongodb。(不要关闭当前窗口,然后重新打开另外一个dos窗口,进行下一个步骤)
45 | ```
46 | mongod --dbpath d:\data // d:\data 为数据库文件夹位置,可自行设置
47 | ```
48 | 运行服务器。(确保数据库mongodb已经启动,不要关闭当前窗口,然后重新打开另外一个dos窗口,进行下一个步骤)
49 | ```
50 | npm run start
51 | ```
52 | 在8082端口热重载开发,等待一会后,会自动弹出浏览器窗口,加载会比较慢,请耐心等待
53 | ```
54 | npm run dev
55 | ```
56 | ### 注意
57 | - 账户: boss 密码: 123456
58 | - 配置: 请将server/app.js和/server/api/comment.js里面的xxx@qq.com改为自己的邮箱,来测试邮箱通知功能
59 | - 邮箱通知功能,已经为大家注册了一个公用126邮箱去发送邮件,如果频繁发送邮件到一固定邮箱,则会被当成垃圾邮件被系统拦截。如果需要频繁发送邮件测试,可以在接收邮箱中设置邮箱白名单,也可以在接收邮箱中添加公用的126邮箱为联系人。
60 | - 登录界面在:最底下的——‘站长登录’
61 |
62 | ### 效果展示
63 | #### 前台效果
64 |
65 | 首页
66 |
67 | 
68 |
69 | 博客所有文章
70 |
71 | 
72 |
73 | 最近更新
74 |
75 | 
76 |
77 | 某一篇文章
78 |
79 | 
80 |
81 | 留言
82 |
83 | 
84 |
85 | 评论
86 |
87 | 
88 |
89 | #### 后台效果
90 |
91 | 所有文章
92 |
93 | 
94 |
95 | 搜索文章
96 |
97 | 
98 |
99 | 修改账户
100 |
101 | 
102 |
103 | markdown编辑器
104 |
105 | 
106 |
107 | 移动端演示
108 |
109 | 
110 | ### 目录
111 | ```
112 | │ .babelrc babel配置
113 | │ .editorconfig 编辑器配置
114 | │ .eslintignore eslint忽略
115 | │ .eslintrc.js eslintrc配置
116 | │ .gitignore git上传忽略
117 | │ .postcssrc.js
118 | │ debug.log
119 | │ index.html 打包模板
120 | │ package.json
121 | │ README.md
122 | │ LICENSE
123 | │
124 | ├─build
125 | │
126 | ├─server 服务端
127 | │ │
128 | │ ├─ api Restful接口
129 | │ │
130 | │ ├─ db 数据库
131 | │ │
132 | │ ├─ middlewares 中间件
133 | │ │
134 | │ ├─app.js
135 | │ └─email.js
136 | │
137 | ├─src
138 | │ │ main.js 项目入口
139 | │ │ App.vue 根组件
140 | │ │
141 | │ ├─assets 外部引用文件
142 | │ │ ├─css
143 | │ │ └─js
144 | │ │
145 | │ ├─components vue组件
146 | │ │ ├─back 后台组件
147 | │ │ ├─front 前台组件
148 | │ │ └─share 共享组件
149 | │ │
150 | │ ├─ lib
151 | │ │
152 | │ ├─router 路由
153 | │ │
154 | │ └─store vuex文件
155 | │
156 | └─static 静态文件
157 | ```
158 |
159 |
160 | ### TODO
161 | - 添加服务器配置教程
162 | - 简化webpack配置
163 | - 添加代码规范。BEM + JsDoc
164 | - 重构代码
165 |
166 |
167 |
--------------------------------------------------------------------------------
/build/build.js:
--------------------------------------------------------------------------------
1 | require('./check-versions')()
2 |
3 | process.env.NODE_ENV = 'production'
4 |
5 | var ora = require('ora')
6 | var rm = require('rimraf')
7 | var path = require('path')
8 | var chalk = require('chalk')
9 | var webpack = require('webpack')
10 | var config = require('../config')
11 | var webpackConfig = require('./webpack.prod.conf')
12 |
13 | var spinner = ora('building for production...')
14 | spinner.start()
15 |
16 | rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
17 | if (err) throw err
18 | webpack(webpackConfig, function (err, stats) {
19 | spinner.stop()
20 | if (err) throw err
21 | process.stdout.write(stats.toString({
22 | colors: true,
23 | modules: false,
24 | children: false,
25 | chunks: false,
26 | chunkModules: false
27 | }) + '\n\n')
28 |
29 | console.log(chalk.cyan(' Build complete.\n'))
30 | console.log(chalk.yellow(
31 | ' Tip: built files are meant to be served over an HTTP server.\n' +
32 | ' Opening index.html over file:// won\'t work.\n'
33 | ))
34 | })
35 | })
36 |
--------------------------------------------------------------------------------
/build/check-versions.js:
--------------------------------------------------------------------------------
1 | var chalk = require('chalk')
2 | var semver = require('semver')
3 | var packageConfig = require('../package.json')
4 |
5 | function exec (cmd) {
6 | return require('child_process').execSync(cmd).toString().trim()
7 | }
8 |
9 | var versionRequirements = [
10 | {
11 | name: 'node',
12 | currentVersion: semver.clean(process.version),
13 | versionRequirement: packageConfig.engines.node
14 | },
15 | {
16 | name: 'npm',
17 | currentVersion: exec('npm --version'),
18 | versionRequirement: packageConfig.engines.npm
19 | }
20 | ]
21 |
22 | module.exports = function () {
23 | var warnings = []
24 | for (var i = 0; i < versionRequirements.length; i++) {
25 | var mod = versionRequirements[i]
26 | if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
27 | warnings.push(mod.name + ': ' +
28 | chalk.red(mod.currentVersion) + ' should be ' +
29 | chalk.green(mod.versionRequirement)
30 | )
31 | }
32 | }
33 |
34 | if (warnings.length) {
35 | console.log('')
36 | console.log(chalk.yellow('To use this template, you must update following to modules:'))
37 | console.log()
38 | for (var i = 0; i < warnings.length; i++) {
39 | var warning = warnings[i]
40 | console.log(' ' + warning)
41 | }
42 | console.log()
43 | process.exit(1)
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/build/dev-client.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | require('eventsource-polyfill')
3 | var hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true')
4 |
5 | hotClient.subscribe(function (event) {
6 | if (event.action === 'reload') {
7 | window.location.reload()
8 | }
9 | })
10 |
--------------------------------------------------------------------------------
/build/dev-server.js:
--------------------------------------------------------------------------------
1 | require('./check-versions')()
2 |
3 | var config = require('../config')
4 | if (!process.env.NODE_ENV) {
5 | process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV)
6 | }
7 |
8 | var opn = require('opn')
9 | var path = require('path')
10 | var express = require('express')
11 | var webpack = require('webpack')
12 | var proxyMiddleware = require('http-proxy-middleware')
13 | var webpackConfig = require('./webpack.dev.conf')
14 |
15 | // default port where dev server listens for incoming traffic
16 | var port = process.env.PORT || config.dev.port
17 | // automatically open browser, if not set will be false
18 | var autoOpenBrowser = !!config.dev.autoOpenBrowser
19 | // Define HTTP proxies to your custom API backend
20 | // https://github.com/chimurai/http-proxy-middleware
21 | var proxyTable = config.dev.proxyTable
22 |
23 | var app = express()
24 | var compiler = webpack(webpackConfig)
25 |
26 | var devMiddleware = require('webpack-dev-middleware')(compiler, {
27 | publicPath: webpackConfig.output.publicPath,
28 | quiet: true
29 | })
30 |
31 | var hotMiddleware = require('webpack-hot-middleware')(compiler, {
32 | log: () => {}
33 | })
34 | // force page reload when html-webpack-plugin template changes
35 | compiler.plugin('compilation', function (compilation) {
36 | compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) {
37 | hotMiddleware.publish({ action: 'reload' })
38 | cb()
39 | })
40 | })
41 |
42 | // proxy api requests
43 | Object.keys(proxyTable).forEach(function (context) {
44 | var options = proxyTable[context]
45 | if (typeof options === 'string') {
46 | options = { target: options }
47 | }
48 | app.use(proxyMiddleware(options.filter || context, options))
49 | })
50 |
51 | // handle fallback for HTML5 history API
52 | app.use(require('connect-history-api-fallback')())
53 |
54 | // serve webpack bundle output
55 | app.use(devMiddleware)
56 |
57 | // enable hot-reload and state-preserving
58 | // compilation error display
59 | app.use(hotMiddleware)
60 |
61 | // serve pure static assets
62 | var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory)
63 | app.use(staticPath, express.static('./static'))
64 |
65 | var uri = 'http://localhost:' + port
66 |
67 | devMiddleware.waitUntilValid(function () {
68 | console.log('> Listening at ' + uri + '\n')
69 | })
70 |
71 | module.exports = app.listen(port, function (err) {
72 | if (err) {
73 | console.log(err)
74 | return
75 | }
76 |
77 | // when env is testing, don't need open it
78 | if (autoOpenBrowser && process.env.NODE_ENV !== 'testing') {
79 | opn(uri)
80 | }
81 | })
82 |
--------------------------------------------------------------------------------
/build/utils.js:
--------------------------------------------------------------------------------
1 | var path = require('path')
2 | var config = require('../config')
3 | var ExtractTextPlugin = require('extract-text-webpack-plugin')
4 |
5 | exports.assetsPath = function (_path) {
6 | var assetsSubDirectory = process.env.NODE_ENV === 'production'
7 | ? config.build.assetsSubDirectory
8 | : config.dev.assetsSubDirectory
9 | return path.posix.join(assetsSubDirectory, _path)
10 | }
11 |
12 | exports.cssLoaders = function (options) {
13 | options = options || {}
14 |
15 | var cssLoader = {
16 | loader: 'css-loader',
17 | options: {
18 | minimize: process.env.NODE_ENV === 'production',
19 | sourceMap: options.sourceMap
20 | }
21 | }
22 |
23 | // generate loader string to be used with extract text plugin
24 | function generateLoaders (loader, loaderOptions) {
25 | var loaders = [cssLoader]
26 | if (loader) {
27 | loaders.push({
28 | loader: loader + '-loader',
29 | options: Object.assign({}, loaderOptions, {
30 | sourceMap: options.sourceMap
31 | })
32 | })
33 | }
34 |
35 | // Extract CSS when that option is specified
36 | // (which is the case during production build)
37 | if (options.extract) {
38 | return ExtractTextPlugin.extract({
39 | use: loaders,
40 | fallback: 'vue-style-loader'
41 | })
42 | } else {
43 | return ['vue-style-loader'].concat(loaders)
44 | }
45 | }
46 |
47 | // http://vuejs.github.io/vue-loader/en/configurations/extract-css.html
48 | return {
49 | css: generateLoaders(),
50 | postcss: generateLoaders(),
51 | less: generateLoaders('less'),
52 | sass: generateLoaders('sass', { indentedSyntax: true }),
53 | scss: generateLoaders('sass'),
54 | stylus: generateLoaders('stylus'),
55 | styl: generateLoaders('stylus')
56 | }
57 | }
58 |
59 | // Generate loaders for standalone style files (outside of .vue)
60 | exports.styleLoaders = function (options) {
61 | var output = []
62 | var loaders = exports.cssLoaders(options)
63 | for (var extension in loaders) {
64 | var loader = loaders[extension]
65 | output.push({
66 | test: new RegExp('\\.' + extension + '$'),
67 | use: loader
68 | })
69 | }
70 | return output
71 | }
72 |
--------------------------------------------------------------------------------
/build/vue-loader.conf.js:
--------------------------------------------------------------------------------
1 | var utils = require('./utils')
2 | var config = require('../config')
3 | var isProduction = process.env.NODE_ENV === 'production'
4 |
5 | module.exports = {
6 | loaders: utils.cssLoaders({
7 | sourceMap: isProduction
8 | ? config.build.productionSourceMap
9 | : config.dev.cssSourceMap,
10 | extract: isProduction
11 | })
12 | }
13 |
--------------------------------------------------------------------------------
/build/webpack.base.conf.js:
--------------------------------------------------------------------------------
1 | var path = require('path')
2 | var utils = require('./utils')
3 | var config = require('../config')
4 | var vueLoaderConfig = require('./vue-loader.conf')
5 |
6 | function resolve (dir) {
7 | return path.join(__dirname, '..', dir)
8 | }
9 |
10 | module.exports = {
11 | entry: {
12 | app: './src/main.js'
13 | },
14 | output: {
15 | path: config.build.assetsRoot,
16 | filename: '[name].js',
17 | publicPath: process.env.NODE_ENV === 'production'
18 | ? config.build.assetsPublicPath
19 | : config.dev.assetsPublicPath
20 | },
21 | resolve: {
22 | extensions: ['.js', '.vue', '.json'],
23 | alias: {
24 | 'vue$': 'vue/dist/vue.esm.js',
25 | '@': resolve('src'),
26 | }
27 | },
28 | module: {
29 | rules: [
30 | {
31 | test: /\.(js|vue)$/,
32 | loader: 'eslint-loader',
33 | enforce: "pre",
34 | include: [resolve('src'), resolve('test')],
35 | options: {
36 | formatter: require('eslint-friendly-formatter')
37 | }
38 | },
39 | {
40 | test: /\.vue$/,
41 | loader: 'vue-loader',
42 | options: vueLoaderConfig
43 | },
44 | {
45 | test: /\.js$/,
46 | loader: 'babel-loader',
47 | include: [resolve('src'), resolve('test')]
48 | },
49 | {
50 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
51 | loader: 'url-loader',
52 | query: {
53 | limit: 10000,
54 | name: utils.assetsPath('img/[name].[hash:7].[ext]')
55 | }
56 | },
57 | {
58 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
59 | loader: 'url-loader',
60 | query: {
61 | limit: 10000,
62 | name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
63 | }
64 | }
65 | ]
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/build/webpack.dev.conf.js:
--------------------------------------------------------------------------------
1 | var utils = require('./utils')
2 | var webpack = require('webpack')
3 | var config = require('../config')
4 | var merge = require('webpack-merge')
5 | var baseWebpackConfig = require('./webpack.base.conf')
6 | var HtmlWebpackPlugin = require('html-webpack-plugin')
7 | var FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
8 |
9 | // add hot-reload related code to entry chunks
10 | Object.keys(baseWebpackConfig.entry).forEach(function (name) {
11 | baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(baseWebpackConfig.entry[name])
12 | })
13 |
14 | module.exports = merge(baseWebpackConfig, {
15 | module: {
16 | rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap })
17 | },
18 | // cheap-module-eval-source-map is faster for development
19 | devtool: '#cheap-module-eval-source-map',
20 | plugins: [
21 | new webpack.DefinePlugin({
22 | 'process.env': config.dev.env
23 | }),
24 | // https://github.com/glenjamin/webpack-hot-middleware#installation--usage
25 | new webpack.HotModuleReplacementPlugin(),
26 | new webpack.NoEmitOnErrorsPlugin(),
27 | // https://github.com/ampedandwired/html-webpack-plugin
28 | new HtmlWebpackPlugin({
29 | filename: 'index.html',
30 | template: 'index.html',
31 | inject: true
32 | }),
33 | new FriendlyErrorsPlugin()
34 | ]
35 | })
36 |
--------------------------------------------------------------------------------
/build/webpack.prod.conf.js:
--------------------------------------------------------------------------------
1 | var path = require('path')
2 | var utils = require('./utils')
3 | var webpack = require('webpack')
4 | var config = require('../config')
5 | var merge = require('webpack-merge')
6 | var baseWebpackConfig = require('./webpack.base.conf')
7 | var CopyWebpackPlugin = require('copy-webpack-plugin')
8 | var HtmlWebpackPlugin = require('html-webpack-plugin')
9 | var ExtractTextPlugin = require('extract-text-webpack-plugin')
10 | var OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
11 |
12 | var env = config.build.env
13 |
14 | var webpackConfig = merge(baseWebpackConfig, {
15 | module: {
16 | rules: utils.styleLoaders({
17 | sourceMap: config.build.productionSourceMap,
18 | extract: false
19 | })
20 | },
21 | devtool: config.build.productionSourceMap ? '#source-map' : false,
22 | output: {
23 | path: config.build.assetsRoot,
24 | filename: utils.assetsPath('js/[name].[chunkhash].js'),
25 | chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
26 | },
27 | plugins: [
28 | // http://vuejs.github.io/vue-loader/en/workflow/production.html
29 | new webpack.DefinePlugin({
30 | 'process.env': env
31 | }),
32 | new webpack.optimize.UglifyJsPlugin({
33 | compress: {
34 | warnings: false
35 | },
36 | sourceMap: true
37 | }),
38 | // extract css into its own file
39 | new ExtractTextPlugin({
40 | filename: utils.assetsPath('css/[name].[contenthash].css')
41 | }),
42 | // Compress extracted CSS. We are using this plugin so that possible
43 | // duplicated CSS from different components can be deduped.
44 | new OptimizeCSSPlugin(),
45 | // generate dist index.html with correct asset hash for caching.
46 | // you can customize output by editing /index.html
47 | // see https://github.com/ampedandwired/html-webpack-plugin
48 | new HtmlWebpackPlugin({
49 | filename: config.build.index,
50 | template: 'index.html',
51 | inject: true,
52 | minify: {
53 | removeComments: true,
54 | collapseWhitespace: true,
55 | removeAttributeQuotes: true
56 | // more options:
57 | // https://github.com/kangax/html-minifier#options-quick-reference
58 | },
59 | // necessary to consistently work with multiple chunks via CommonsChunkPlugin
60 | chunksSortMode: 'dependency'
61 | }),
62 | // split vendor js into its own file
63 | new webpack.optimize.CommonsChunkPlugin({
64 | name: 'vendor',
65 | minChunks: function (module, count) {
66 | // any required modules inside node_modules are extracted to vendor
67 | return (
68 | module.resource &&
69 | /\.js$/.test(module.resource) &&
70 | module.resource.indexOf(
71 | path.join(__dirname, '../node_modules')
72 | ) === 0
73 | )
74 | }
75 | }),
76 | // extract webpack runtime and module manifest to its own file in order to
77 | // prevent vendor hash from being updated whenever app bundle is updated
78 | new webpack.optimize.CommonsChunkPlugin({
79 | name: 'manifest',
80 | chunks: ['vendor']
81 | }),
82 | // copy custom static assets
83 | new CopyWebpackPlugin([
84 | {
85 | from: path.resolve(__dirname, '../static'),
86 | to: config.build.assetsSubDirectory,
87 | ignore: ['.*']
88 | }
89 | ])
90 | ]
91 | })
92 |
93 | if (config.build.productionGzip) {
94 | var CompressionWebpackPlugin = require('compression-webpack-plugin')
95 |
96 | webpackConfig.plugins.push(
97 | new CompressionWebpackPlugin({
98 | asset: '[path].gz[query]',
99 | algorithm: 'gzip',
100 | test: new RegExp(
101 | '\\.(' +
102 | config.build.productionGzipExtensions.join('|') +
103 | ')$'
104 | ),
105 | threshold: 10240,
106 | minRatio: 0.8
107 | })
108 | )
109 | }
110 |
111 | if (config.build.bundleAnalyzerReport) {
112 | var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
113 | webpackConfig.plugins.push(new BundleAnalyzerPlugin())
114 | }
115 |
116 | module.exports = webpackConfig
117 |
--------------------------------------------------------------------------------
/config/dev.env.js:
--------------------------------------------------------------------------------
1 | var merge = require('webpack-merge')
2 | var prodEnv = require('./prod.env')
3 |
4 | module.exports = merge(prodEnv, {
5 | NODE_ENV: '"development"'
6 | })
7 |
--------------------------------------------------------------------------------
/config/index.js:
--------------------------------------------------------------------------------
1 | // see http://vuejs-templates.github.io/webpack for documentation.
2 | var path = require('path')
3 |
4 | module.exports = {
5 | build: {
6 | env: require('./prod.env'),
7 | index: path.resolve(__dirname, '../dist/index.html'),
8 | assetsRoot: path.resolve(__dirname, '../dist'),
9 | assetsSubDirectory: 'static',
10 | assetsPublicPath: '/',
11 | productionSourceMap: true,
12 | // Gzip off by default as many popular static hosts such as
13 | // Surge or Netlify already gzip all static assets for you.
14 | // Before setting to `true`, make sure to:
15 | // npm install --save-dev compression-webpack-plugin
16 | productionGzip: false,
17 | productionGzipExtensions: ['js', 'css'],
18 | // Run the build command with an extra argument to
19 | // View the bundle analyzer report after build finishes:
20 | // `npm run build --report`
21 | // Set to `true` or `false` to always turn it on or off
22 | bundleAnalyzerReport: process.env.npm_config_report
23 | },
24 | dev: {
25 | env: require('./dev.env'),
26 | port: 8082,
27 | autoOpenBrowser: true,
28 | assetsSubDirectory: 'static',
29 | assetsPublicPath: '/',
30 | proxyTable: {
31 | '/api': {
32 | target: 'http://localhost:3003',
33 | changeOrigin: true,
34 | pathRewrite: {
35 | '^/api': '/api'
36 | }
37 | }
38 | },
39 | // CSS Sourcemaps off by default because relative paths are "buggy"
40 | // with this option, according to the CSS-Loader README
41 | // (https://github.com/webpack/css-loader#sourcemaps)
42 | // In our experience, they generally work as expected,
43 | // just be aware of this issue when enabling this option.
44 | cssSourceMap: false
45 | },
46 | jwt: {
47 | cert: '123'
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/config/prod.env.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | NODE_ENV: '"production"'
3 | }
4 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | vue-blog
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vueblog",
3 | "version": "1.0.0",
4 | "description": "A Vue.js project",
5 | "author": "FatDong1 ",
6 | "private": true,
7 | "scripts": {
8 | "start": "cross-env NODE_ENV=development supervisor -i src server/app.js",
9 | "dev": "cross-env NODE_ENV=development node build/dev-server.js",
10 | "build": "cross-env NODE_ENV=production node build/build.js",
11 | "lint": "eslint --ext .js,.vue src"
12 | },
13 | "dependencies": {
14 | "body-parser": "^1.17.1",
15 | "csprng": "^0.1.2",
16 | "express": "^4.15.2",
17 | "highlight.js": "^9.10.0",
18 | "jsonwebtoken": "^7.3.0",
19 | "marked": "^0.3.6",
20 | "mongoose": "^4.9.1",
21 | "nodemailer": "^4.0.0",
22 | "sha1": "^1.1.1",
23 | "smooth-scroll": "github:cferdinandi/smooth-scroll",
24 | "vue": "^2.2.2",
25 | "vue-router": "^2.2.0"
26 | },
27 | "devDependencies": {
28 | "autoprefixer": "^6.7.2",
29 | "babel-core": "^6.22.1",
30 | "babel-eslint": "^7.1.1",
31 | "babel-loader": "^6.2.10",
32 | "babel-plugin-transform-runtime": "^6.22.0",
33 | "babel-preset-env": "^1.2.1",
34 | "babel-preset-stage-2": "^6.22.0",
35 | "babel-register": "^6.22.0",
36 | "chalk": "^1.1.3",
37 | "connect-history-api-fallback": "^1.3.0",
38 | "copy-webpack-plugin": "^4.0.1",
39 | "cross-env": "^3.2.4",
40 | "css-loader": "^0.26.1",
41 | "eslint": "^3.14.1",
42 | "eslint-config-standard": "^6.2.1",
43 | "eslint-friendly-formatter": "^2.0.7",
44 | "eslint-loader": "^1.6.1",
45 | "eslint-plugin-html": "^2.0.0",
46 | "eslint-plugin-promise": "^3.4.0",
47 | "eslint-plugin-standard": "^2.0.1",
48 | "eventsource-polyfill": "^0.9.6",
49 | "extract-text-webpack-plugin": "^2.0.0",
50 | "file-loader": "^0.10.0",
51 | "friendly-errors-webpack-plugin": "^1.1.3",
52 | "function-bind": "^1.1.0",
53 | "html-webpack-plugin": "^2.28.0",
54 | "http-proxy-middleware": "^0.17.3",
55 | "node-sass": "^4.5.0",
56 | "opn": "^4.0.2",
57 | "optimize-css-assets-webpack-plugin": "^1.3.0",
58 | "ora": "^1.1.0",
59 | "rimraf": "^2.6.0",
60 | "sass-loader": "^6.0.3",
61 | "semver": "^5.3.0",
62 | "supervisor": "^0.12.0",
63 | "url-loader": "^0.5.7",
64 | "vue-loader": "^11.1.4",
65 | "vue-resource": "^1.2.1",
66 | "vue-style-loader": "^2.0.0",
67 | "vue-template-compiler": "^2.2.4",
68 | "vuex": "^2.2.1",
69 | "webpack": "^2.2.1",
70 | "webpack-bundle-analyzer": "^2.2.1",
71 | "webpack-dev-middleware": "^1.10.0",
72 | "webpack-hot-middleware": "^2.16.1",
73 | "webpack-merge": "^2.6.1"
74 | },
75 | "engines": {
76 | "node": ">= 4.0.0",
77 | "npm": ">= 3.0.0"
78 | },
79 | "browserslist": [
80 | "> 1%",
81 | "last 2 versions",
82 | "not ie <= 8"
83 | ]
84 | }
85 |
--------------------------------------------------------------------------------
/server/api/article.js:
--------------------------------------------------------------------------------
1 | const express = require('express')
2 | const router = express.Router()
3 | const db = require('../db/db.js')
4 | const confirmToken = require('../middlewares/confirmToken')
5 |
6 | // 发布文章
7 | router.post('/api/article', confirmToken, (req, res) => {
8 | const article = {
9 | comment_n: 0,
10 | title: req.body.title,
11 | content: req.body.content,
12 | date: Date(),
13 | tags: req.body.tags,
14 | isPublish: true
15 | }
16 | new db.Article(article).save()
17 | res.status(200).send('succeed in saving new passage.')
18 | })
19 |
20 | // 获取某篇文章
21 | router.get('/api/article/:aid', (req, res) => {
22 | db.Article.findOne({aid: req.params.aid}, (err, doc) => {
23 | if (err) {
24 | console.log(err)
25 | } else {
26 | res.status(200).send(doc)
27 | }
28 | })
29 | })
30 |
31 | // 删除文章并删除文章下面的评论
32 | router.delete('/api/article/:aid', confirmToken, (req, res) => {
33 | db.Article.remove({aid: req.params.aid}, (err, data) => {
34 | if (err) {
35 | console.log(err)
36 | } else {
37 | db.Comment.remove({articleId: req.params.aid}, (err, data) => {
38 | if (err) {
39 | console.log(err)
40 | } else {
41 | res.status(200).send('succeed in deleting ---' + data)
42 | }
43 | })
44 | }
45 | })
46 |
47 | })
48 |
49 | // 更新文章
50 | router.patch('/api/article/:aid', confirmToken, (req, res) => {
51 | const aid = req.params.aid
52 | const article = {
53 | title: req.body.title,
54 | tags: req.body.tags,
55 | date: Date(),
56 | content: req.body.content,
57 | isPublish: true
58 | }
59 | db.Article.update({aid: aid}, article, (err, data) => {
60 | if (err) {
61 | console.log(err)
62 | } else {
63 | res.status(200).send('succeed in updating ---' + data.title)
64 | }
65 | })
66 | })
67 |
68 | // 获取很多文章
69 | router.get('/api/articles', (req, res) => {
70 | const page = req.query.payload.page
71 | const value =req.query.payload.value
72 | const limit = req.query.payload.limit - 0 || 4
73 | const skip = limit * (page - 1 )
74 | if (value && value !== '全部') {
75 | db.Article.find({tags: value, isPublish: true}).sort({date: -1}).limit(limit).skip(skip).exec()
76 | .then((articles) => {
77 | res.send(articles)
78 | })
79 | } else {
80 | db.Article.find({isPublish: true}).sort({date: -1}).limit(limit).skip(skip).exec().then((articles) => {
81 | res.send(articles)
82 | })
83 | }
84 | })
85 |
86 | // 搜索一些文章
87 | router.get('/api/someArticles', (req, res) => {
88 | const key = req.query.payload.key
89 | const value = req.query.payload.value
90 | const page = req.query.payload.page || 1
91 | const skip = 4 * (page - 1 )
92 | const re = new RegExp(value,'i')
93 | if (key === 'tags') { // 根据标签来搜索文章
94 | const arr = value.split(' ')
95 | db.Article.find({tags: {$all: arr}})
96 | .sort({date: -1}).limit(4).skip(skip).exec()
97 | .then((articles) => {
98 | res.send(articles)
99 | })
100 | } else if (key === 'title') { // 根据标题的部分内容来搜索文章
101 | db.Article.find({title: re, isPublish: true})
102 | .sort({date: -1}).limit(4).skip(skip).exec()
103 | .then((articles) => {
104 | res.send(articles)
105 | })
106 | } else if (key === 'date') { // 根据日期来搜索文章
107 | const nextDay = value + 'T24:00:00'
108 | db.Article.find({date: {$gte: new Date(value), $lt: new Date(nextDay)}})
109 | .sort({date: -1}).limit(4).skip(skip).exec()
110 | .then((articles) => {
111 | res.send(articles)
112 | })
113 | }
114 | })
115 |
116 | module.exports = router
117 |
--------------------------------------------------------------------------------
/server/api/comment.js:
--------------------------------------------------------------------------------
1 | const express = require('express')
2 | const router = express.Router()
3 | const mail = require('../email')
4 | const db = require('../db/db.js')
5 |
6 | const emailForm = (title, name, otherName, message, content, url) => {
7 | let string = `
8 |
9 |
${title}
10 |
hello,${name} 😈
11 |
${otherName}${message}
12 |
${content}
13 |
前往查看
14 |
15 | `
16 | return string
17 | }
18 |
19 | // 发布评论并通知站长和评论者
20 | router.post('/api/comment', (req, res) => {
21 | db.Comment.findOne({name: req.body.name, articleId: req.body.articleId}, (err, doc) => {
22 | if (doc && doc.address !== req.body.address) {
23 | res.status(403).end('用户名已存在')
24 | } else if(!doc || doc.address === req.body.address) {
25 | const comment = {
26 | imgName: req.body.imgName,
27 | name: req.body.name,
28 | address: req.body.address,
29 | date: Date(),
30 | content: req.body.content,
31 | articleId: req.body.articleId,
32 | like: 0
33 | }
34 | if (/^@(.*):/.test(req.body.content)) {
35 | const reviewer = /^@(.*):/.exec(req.body.content)[1] // 评论者的名字
36 | db.Comment.findOne({name: reviewer, articleId: req.body.articleId}, (err, doc) => {
37 | const url = 'https://www.xxx.cn' + req.body.curPath
38 | const replyEmail = doc.address
39 | const content = emailForm('欢迎常来我的博客', reviewer, req.body.name, '回复了你的评论',req.body.content, url)
40 | mail.send(replyEmail, '您在FatDong的博客有一条新评论', content, res)
41 | })
42 | }
43 | new db.Comment(comment).save().then(() => {
44 | const url = 'https://www.xxx.cn' + req.body.curPath
45 | const content = emailForm('MyBlog Message', '站长', req.body.name, '评论了你的文章',req.body.content, url)
46 | mail.send('xxx@qq.com', '您的博客有一条新评论', content, res)
47 | res.status(200).send('send email successfully')
48 | }).catch(err => { console.log(err) })
49 | db.Article.update({aid: req.body.articleId},{$inc: {comment_n: 1}}, (err, data) => {
50 | if (err) {
51 | console.log(err)
52 | }
53 | })
54 | }
55 | })
56 | })
57 |
58 | // 获取某一篇文章的所有评论
59 | router.get('/api/comments', (req, res) => {
60 | const articleId = req.query.payload.id
61 | if (req.query.payload.sort === 'date') { // 根据时间排序评论
62 | db.Comment.find({articleId: articleId}, 'name date content like imgName').sort({date: -1}).exec()
63 | .then((comments) => {
64 | res.send(comments)
65 | })
66 | } else if (req.query.payload.sort === 'like') { // 根据点赞数量排序评论
67 | db.Comment.find({articleId: articleId}, 'name date content like imgName').sort({like: -1}).exec()
68 | .then((comments) => {
69 | res.send(comments)
70 | })
71 | } else { // 根据文章的aid获取所有评论
72 | db.Comment.find({articleId: articleId}, 'name date content like imgName').exec().then((comments) => {
73 | res.send(comments)
74 | })
75 | }
76 | })
77 |
78 | // 更新评论的点赞数
79 | router.patch('/api/comments/:id', (req, res) => {
80 | const id = req.params.id
81 | if (req.body.option === 'add') {
82 | db.Comment.update({_id: id}, {$inc: {like: 1}}, (err, data) => {
83 | if (err) {
84 | console.log(err)
85 | } else {
86 | res.status(200).send('succeed in updating like')
87 | }
88 | })
89 | } else if (req.body.option === 'drop') {
90 | db.Comment.update({_id: id}, {$inc: {like: -1}}, (err, data) => {
91 | if (err) {
92 | console.log(err)
93 | } else {
94 | res.status(200).send('succeed in updating like')
95 | }})
96 | }
97 | })
98 |
99 | module.exports = router
100 |
--------------------------------------------------------------------------------
/server/api/draft.js:
--------------------------------------------------------------------------------
1 | const express = require('express')
2 | const router = express.Router()
3 | const confirmToken = require('../middlewares/confirmToken')
4 | const db = require('../db/db.js')
5 |
6 | // 保存草稿
7 | router.post('/api/draft', confirmToken, (req, res) => {
8 | const article = {
9 | title: req.body.title,
10 | content: req.body.content,
11 | date: Date(),
12 | tags: req.body.tags,
13 | isPublish: false
14 | }
15 | new db.Article(article).save()
16 | res.status(200).send('succeed in saving new draft')
17 | })
18 |
19 | // 更新草稿
20 | router.patch('/api/draft/:aid', confirmToken, (req, res) => {
21 | const aid = req.params.aid
22 | const article = {
23 | title: req.body.title,
24 | tags: req.body.tags,
25 | date: Date(),
26 | content: req.body.content,
27 | isPublish: false
28 | }
29 | db.Article.update({aid: aid}, article, (err, data) => {
30 | if (err) {
31 | console.log(err)
32 | } else {
33 | res.status(200).send('succeed in updating ---' + data.title)
34 | }
35 | })
36 | })
37 |
38 | // 获取所有的草稿
39 | router.get('/api/drafts', (req, res) => {
40 | const page = req.query.payload.page
41 | const limit = req.query.payload.limit - 0 || 8
42 | const skip = limit * (page - 1 )
43 | db.Article.find({isPublish: false}).sort({date: -1}).limit(limit).skip(skip).exec().then((articles) => {
44 | res.send(articles)
45 | }).catch((err) => {
46 | console.log(err)
47 | })
48 | })
49 |
50 | module.exports = router
51 |
--------------------------------------------------------------------------------
/server/api/index.js:
--------------------------------------------------------------------------------
1 | const article = require('./article.js')
2 | const draft = require('./draft.js')
3 | const tag = require('./tag.js')
4 | const comment = require('./comment')
5 | const login = require('./login')
6 | const user = require('./user')
7 |
8 | module.exports = (app) => {
9 | app.use(article)
10 | app.use(draft)
11 | app.use(tag)
12 | app.use(comment)
13 | app.use(login)
14 | app.use(user)
15 | }
--------------------------------------------------------------------------------
/server/api/login.js:
--------------------------------------------------------------------------------
1 | const express = require('express')
2 | const router = express.Router()
3 | const secret = require('../../config').jwt
4 | const jwt = require('jsonwebtoken')
5 | const db = require('../db/db.js')
6 | const sha1 = require('sha1')
7 |
8 | const creatToken = (id, name) => {
9 | return jwt.sign({
10 | id: id,
11 | name: name
12 | }, secret.cert, { expiresIn: '7d' })
13 | }
14 |
15 | router.post('/api/login', (req, res) => {
16 | db.User.findOne({name: req.body.name}, (err, doc) => {
17 | if (err) {
18 | console.log(err)
19 | } else if (doc){
20 | const salt = doc.salt
21 | if (doc.password === sha1(req.body.password + salt)) {
22 | const token = creatToken(doc._id, doc.name)
23 | res.status(200).send({
24 | id: doc._id,
25 | name: doc.name,
26 | token: token
27 | })
28 | } else {
29 | res.status(401).end()
30 | }
31 | }
32 | })
33 | })
34 |
35 | module.exports = router
36 |
--------------------------------------------------------------------------------
/server/api/tag.js:
--------------------------------------------------------------------------------
1 | const express = require('express')
2 | const router = express.Router()
3 | const db = require('../db/db.js')
4 |
5 | router.get('/api/tags', (req, res) => {
6 | db.Article.find({isPublish: true}).distinct('tags', (err, doc) => {
7 | if (err) {
8 | console.log(err)
9 | } else if (doc) {
10 | res.send(doc)
11 | }
12 | })
13 | })
14 |
15 | module.exports = router
16 |
--------------------------------------------------------------------------------
/server/api/user.js:
--------------------------------------------------------------------------------
1 | const express = require('express')
2 | const router = express.Router()
3 | const db = require('../db/db.js')
4 | const confirmToken = require('../middlewares/confirmToken')
5 | const rand = require('csprng')
6 | const sha1 = require('sha1')
7 |
8 | // 修改账户
9 | router.post('/api/user', confirmToken, (req, res) => {
10 | const salt = rand(160, 36)
11 | const user = {
12 | salt: salt,
13 | name: req.body.name,
14 | password: sha1(req.body.password + salt)
15 | }
16 | db.User.update({_id: req.body.id}, user, (err) => {
17 | if (err) {
18 | console.log(err)
19 | } else {
20 | res.status(200).send('update successfully')
21 | }
22 | })
23 | })
24 |
25 | module.exports = router
26 |
--------------------------------------------------------------------------------
/server/app.js:
--------------------------------------------------------------------------------
1 | const express = require('express')
2 | const bodyParser = require('body-parser')
3 | const route = require('./api/index.js')
4 | const mail = require('./email')
5 | const app = express()
6 |
7 | app.set('port', (process.env.port || 3003))
8 | app.use(bodyParser.urlencoded({ extended: true }))
9 | app.use(bodyParser.json())
10 |
11 | route(app)
12 |
13 | // 发送邮件通知站长
14 | app.post('/api/mail', (req, res) => {
15 | const content = `
16 |
17 |
MyBlog Message
18 |
hello,站长 😈
19 |
你有一条新留言
20 |
主题: ${req.body.subject}
21 | 内容: ${req.body.content}
22 | 邮箱: ${req.body.address}
23 |
24 |
回到博客
25 |
26 | `
27 | mail.send('xxx@qq.com', '您的博客有一条新留言', content, res)
28 | res.status(200).send('send email successfully')
29 | })
30 |
31 | app.listen(app.get('port'), function () {
32 | console.log('GetData http://localhost:' + app.get('port'))
33 | })
34 |
--------------------------------------------------------------------------------
/server/db/data.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "title" : "啦啦啦",
4 | "date" : "2017-04-03T02:46:54.000Z",
5 | "content" : "`啦啦啦啦啦`",
6 | "isPublish" : false,
7 | "comment_n" : 0,
8 | "tags" : ["随笔"]
9 | },
10 | {
11 | "title" : "hello world",
12 | "date" : "2017-04-25T06:01:29.000Z",
13 | "content" : "### 你好,世界。\n\nHi, my name is Haodong Xu. Welcome to my blog. Nice to meet you! Hi, my name is Haodong Xu. Welcome to my blog. Nice to meet you! Hi, my name is Haodong Xu. Welcome to my blog. Nice to meet you! Hi, my name is Haodong Xu. Welcome to my blog. Nice to meet you! Hi, my name is Haodong Xu. Welcome to my blog. Nice to meet you! Hi, my name is Haodong Xu. Welcome to my blog. Nice to meet you! Hi, my name is Haodong Xu. Welcome to my blog. Nice to meet you! Hi, my name is Haodong Xu. Welcome to my blog. Nice to meet you! Hi, my name is Haodong Xu. Welcome to my blog. Nice to meet you! \n\n```html\n hello world
\n```\n\n```js\nconsole.log('hello world')\nalert('hello world')\n```",
14 | "isPublish" : true,
15 | "comment_n" : 0,
16 | "tags" : [
17 | "html",
18 | "css"
19 | ]
20 | },
21 | {
22 | "title" : "markdown编辑器介绍",
23 | "date" : "2017-04-25T07:14:09.000Z",
24 | "content" : "## markdown编辑器\n---\n使用插件为marked.js 和 highlight.js\n\n### 插件介绍\n1. `marked.js` 解析markdown语法为html\n2. `highlight.js` 将代码高亮显示\n\n## 功能介绍\n---\n### 特点\n1. 标签处使用vue数据驱动的思想,将所有的标签作为一个数组驱动视图更新\n2. 如果内容修改后没有保存就跳转到其他页面,则会弹出提示框;如果内容没有修改过,则不会提示。\n3. 标签输入处自动去除首尾空格,如果标签重复(包括大小写),则自动弹窗警告并删除\n \n\n### 快捷键\n如有快捷键冲突,可自行修改代码\n- tab键切换输入框\n- ctrl + ↓ 来回切换查看模式\n- 标签书写处回车enter,添加新标签\n- 光标在文章书写框处, ctrl + enter 存为草稿\n- 发布文章没有设置快捷键\n\n### 代码高亮\n```js\nlet user = {\n name: 'shuaige',\n age: 21\n};\n```\n> 1. 如果要修改代码高亮样式,可先按F12查看代码的className,然后修改highlight.css\n2. 如果要修改解析的html样式,如margin、border-left、border-radius等,可在marked.scss修改\n",
25 | "isPublish" : true,
26 | "comment_n" : 0,
27 | "tags" : [
28 | "marked.js",
29 | "highlight.js"
30 | ]
31 | },
32 | {
33 | "title" : "learing css",
34 | "date" : "2017-04-03T02:45:18.000Z",
35 | "content" : "> I'm learing css.",
36 | "isPublish" : false,
37 | "comment_n" : 0,
38 | "tags" : [
39 | "css",
40 | "html"
41 | ]
42 | },
43 | {
44 | "title" : "writing draft",
45 | "date" : "2017-04-03T02:45:07.000Z",
46 | "content" : "### I'm writing a draft.",
47 | "isPublish" : false,
48 | "comment_n" : 0,
49 | "tags" : [
50 | "draft"
51 | ]
52 | },
53 | {
54 | "title" : "learing js",
55 | "date" : "2017-04-03T02:45:47.000Z",
56 | "content" : "> I'm learing js.",
57 | "isPublish" : false,
58 | "comment_n" : 0,
59 | "tags" : [
60 | "javascript"
61 | ]
62 | },
63 | {
64 | "title" : "测试--文章目录",
65 | "content" : "# 一级标题 \n## 二级标题 a\n### 三级标题1\n催眠术!这是一段内容。这是一段内容。这是一段内容。这是一段内容。这是一段内容。这是一段内容。这是一段内容。这是一段内容。这是一段内容。这是一段内容。这是一段内容。这是一段内容。这是一段内容。这是一段内容。这是一段内容。这是一段内容。这是一段内容。这是一段内容。\n### 三级标题2\n催眠术!这是一段内容。这是一段内容。这是一段内容。这是一段内容。这是一段内容。这是一段内容。这是一段内容。这是一段内容。这是一段内容。这是一段内容。这是一段内容。这是一段内容。这是一段内容。这是一段内容。这是一段内容。这是一段内容。这是一段内容。这是一段内容。\n## 二级标题b\n#### 四级标题1\n催眠术!这是一段内容。这是一段内容。这是一段内容。这是一段内容。这是一段内容。这是一段内容。这是一段内容。这是一段内容。这是一段内容。这是一段内容。这是一段内容。这是一段内容。这是一段内容。这是一段内容。这是一段内容。这是一段内容。这是一段内容。这是一段内容。\n\n## 二级标题2.1\n#### 四级标题2\n催眠术!这是一段内容。这是一段内容。这是一段内容。这是一段内容。这是一段内容。这是一段内容。这是一段内容。这是一段内容。这是一段内容。\n\n\n",
66 | "date" : "2017-04-25T02:18:48.000Z",
67 | "isPublish" : true,
68 | "comment_n" : 0,
69 | "tags" : [
70 | "test"
71 | ]
72 | },
73 | {
74 | "title" : "关于我",
75 | "date" : "2017-04-25T07:55:40.000Z",
76 | "content" : "## 随笔\n---\n### 感悟\n不知不觉已经花了一个月的时间去做这个这个博客项目了,这个过程有苦有乐,虽然这个简单的项目在别人眼里可能没什么特点,但是对于我自己来说,能完成这个项目真是太好了,自己做了一回设计者和体验者,感受到了编程创造的乐趣,虽然每次调试完一个bug后,短暂的开心之后又会迎来下一个bug,无穷无尽的bug让我心力交瘁,还好现在看起来是没什么bug,我得趁现在还没什么bug,去找找暑假的实习了,不然得一直陷在这个博客项目里了,对于一个非科班的学生来说,暑假可没什么地方可以去,以后有时间精力再对这个博客进行完善了。Goodbye,myblog!\n\n### 联系方式\n- 广州大学大三在读, 2018毕业\n- QQ: 3552116732\n- 邮箱: xuhaodong66@gmail.com\n\n\n\n",
77 | "isPublish" : true,
78 | "comment_n" : 0,
79 | "tags" : [
80 | "个人介绍"
81 | ]
82 | },
83 | {
84 | "title" : "测试--标签去重",
85 | "content" : "## 测试标签去重\n---\n### 文章的schema\n```js\nconst ArticleSchema = new Schema(\n {\n aid: { type : Number, index: { unique: true } },\n title: String,\n content: String,\n tags: [String],\n date: Date,\n isPublish: Boolean,\n comment_n: Number\n },\n { versionKey: false }\n)\n```\n\n### 标签去重\n主要是使用了mongodb里面的distinct方法,就可以方便把文章schema里的tags合并",
86 | "date" : "2017-04-25T05:59:09.000Z",
87 | "isPublish" : true,
88 | "comment_n" : 0,
89 | "tags" : [
90 | "html",
91 | "css"
92 | ]
93 | },
94 | {
95 | "title" : "test--draft",
96 | "content" : "> hi",
97 | "date" : "2017-04-25T06:04:12.000Z",
98 | "isPublish" : false,
99 | "comment_n" : 0,
100 | "tags" : [
101 | "test"
102 | ]
103 | },
104 | {
105 | "comment_n" : 0,
106 | "title" : "如何生成文章目录",
107 | "content" : "## 实现的需求\n---\n1. 点击目录对应标题后, 会自动跳到相应的锚点\n2. 不同层次的标题目录左边距不同\n3. 向下滚动后一段距离后,目录相对屏幕固定\n\n## 如何生成文章目录 \n--- \n\n### 重写原本marked解析h标签的格式 \n \n原本marked解析h标签会将除了英文和数字以外的字符替换为'-',包括中文字符,如果用中文写h标签,这样生成的锚点和id都为'-'字符,除非用英文数字写标题,所以需要对h标签的格式重写。\n\n通过设置`renderer.heading`后, `marked`解析h标签的格式可以重写。具体可以看/src/components/back/editor.vue\n\n```html\n一级标题 \n\n\n\n\n 一级标题 \n \n```\n上面的data-scroll是使smooth-scroll,可以是跳转锚点平滑滚动\n\n### 获取所有h标签生成目录数组\n首先将从后台获取到的内容用marked解析成html字符串,再用正则表达式取出h标签中的重要信息,h标签的等级和id值,然后通过一点简单的算法算出不同标签对应的层级, 不同的层级在目录里的左边距不同,最后push到目录list数组中。下面是目录数组的格式。\n```js\nlet list = [\n {\n level: 1,\n size: 2,\n content: '二级标题'\n },\n {\n level: 2,\n size: 3,\n content: '三级标题'\n }\n];\n```\n具体的算法可以看/src/store/getters.js",
108 | "date" : "2017-04-25T06:23:16.000Z",
109 | "isPublish" : true,
110 | "tags" : [
111 | "模块介绍",
112 | "marked.js"
113 | ]
114 | },
115 | {
116 | "comment_n" : 0,
117 | "title" : "动态展示页面",
118 | "content" : "## 前言\n\n---\n最近一直在鼓捣怎么给自己的个人博客添加一个功能---向下滚动的时候,用一些动画展示页面,最终自己瞎折腾用Vue的自定义指令找出了看起来比较优雅的方法,先上代码!\n\n### 代码\n\n```html\n\n\n```\n```css\n.newBlog {\n min-height: 500px;\n}\n.headline, .posts {\n display: none;\n}\n```\n```js\nexport default {\n directives: {\n scrollShow: {\n bind: (el) => {\n window.addEventListener('scroll', () => {\n if (document.body.scrollTop + 400 > el.offsetTop) {\n for (let i = 0; i < el.children.length; i++) {\n setTimeout(() => { \n el.children[i].style.display = 'block' \n }, 1000 * i)\n }\n }\n })\n }\n }\n }\n}\n```\n\n### 简单介绍实现过程\n主要使用Vue的自定义指令,监听滚动事件,当滚动条的scrollTop等于要动态展示节点的offsetTop - 400时,延时展示该节点的子节点children\n\n## 结尾\n\n---\n这是自己想的一个方法,如果各位有什么更优雅的方法去实现的话,欢迎告知,先谢谢啦!",
119 | "date" : "2017-04-25T07:11:41.000Z",
120 | "isPublish" : true,
121 | "tags" : [
122 | "模块介绍",
123 | "Vue"
124 | ]
125 | },
126 | {
127 | "comment_n" : 0,
128 | "title" : "平滑滚动",
129 | "content" : "### 前言\n之前给自己的博客中`回到顶部`功能主要用的setTimeout延时滚动,看到vue文档中点击对应标题,就可以平滑滚动到锚点,就很好奇是怎么实现的,看了一下网页的源码,发现是使用了一个叫做smooth-scroll的轻量插件,所以自己也鼓捣使用了这个插件,把原本用setTimeout回到顶部的功能也直接用这个插件实现。\n### 使用插件\n- smooth-scroll\n",
130 | "date" : "2017-04-25T07:40:34.000Z",
131 | "isPublish" : true,
132 | "tags" : [
133 | "模块介绍"
134 | ]
135 | },
136 | {
137 | "comment_n" : 0,
138 | "title" : "canvas粒子效果",
139 | "content" : "## 前言\n\n---\n在这个博客啥都还没搭的时候,就已经决定要用canvas来给自己的博客加一点炫酷的效果了,在网上找了一两天的时间,看有没有上面比较炫酷的canvas效果,顺便简单学习一下canvas,毕竟canvas还没学过,最终选择这样一个canvas粒子效果,从而也决定我的博客是为主题为暗色系列的。 \n\n\n## canvas粒子效果\n\n---\n### 看懂源代码\n看了很多的canvas炫酷的效果,最终还是被粒子效果吸引。在一次google搜索以后,找到了一个[非常炫酷的canvas效果](http://www.mrspeaker.net/dev/parcycle/),当时就想着一定要把这个炫酷的效果放到自己的博客上,于是就右键点击查看源代码,一点一点的去理解这个效果的实现原理,经过一两天的苦思冥想/(ㄒoㄒ)/~~,终于理解了这个网站的源代码,虽然这个过程很累,不过实在是这个canvas效果太吸引我了,所以对这个canvas粒子效果也很执着。\n\n### 后期改良\n\n\n后期经过不断修改,给这个canvas添加了以下的功能\n- 鼠标点击后,随机变化颜色\n- 鼠标跟随canvas粒子效果\n- 鼠标离开页面后,从左到右循环移动\n\n## 结尾\n自己的github里有对源代码进行改良后的代码,与这个博客的效果有一些不一样,有兴趣可以去看一下----[github链接](https://fatdong1.github.io/canvas/particle/index.html)\n",
140 | "date" : "2017-04-25T07:10:51.000Z",
141 | "isPublish" : true,
142 | "tags" : [
143 | "模块介绍",
144 | "canvas"
145 | ]
146 | },
147 | {
148 | "comment_n" : 0,
149 | "title" : "评论模块",
150 | "content" : "## 前言\n---\n自己写了一个简单评论模块,实现下面的需求:点赞,回复,头像,通知等。\n\n## 简单介绍\n---\n### 通知功能实现\n当有访问者评论后,会通过邮件通知评论者和站长。使用插件为`nodemailer`,尝试了gmail、126、163邮箱,最终决定了126邮箱,因为gmail的安全机制导致有时候有些邮件发不出去,而用163邮箱容易被放到邮箱的垃圾箱中。在这个项目中,我自己注册了一个公用的126邮箱供开发者使用来发送邮件,如果想要使用自己的邮箱来发送邮件,在配置nodemailer时会用到授权码,注意这个不是邮箱密码,可以到网上搜索设置授权码。\n\n但是,如果频繁使用126邮箱发送邮件到固定的邮箱中,超过一定次数会邮件会被放入接收者邮箱的垃圾箱中。解决方法:设置接受者邮箱的白名单为该126邮箱,添加该126邮箱为邮箱联系人。\n### 点赞功能\n将访问者在每篇文章点赞的数量记录在客户端的浏览器localStorage中,如果不清除浏览器缓存,在下次同一个访问者访问的时候,会从浏览器中的localStorage读取点赞记录。\n### 头像功能\n通过判断state.users来判断是站长还是访问者,从而设置不同的头像\n### 评论者重名\n在提交评论的时候,会在后台鉴别该评论名字是否对应特定邮箱,如果数据库已有该评论者名字且邮箱不对应,则无法提交评论,防止评论者重名。\n",
151 | "date" : "2017-04-25T07:39:03.000Z",
152 | "isPublish" : true,
153 | "tags" : [
154 | "模块介绍",
155 | "nodemailer"
156 | ]
157 | }
158 | ]
--------------------------------------------------------------------------------
/server/db/db.js:
--------------------------------------------------------------------------------
1 | const mongoose =require('mongoose')
2 | const Schema = mongoose.Schema
3 | const data = require('./data')
4 | const sha1 = require('sha1')
5 | const rand = require('csprng')
6 | const Sequence = require('./sequence')
7 |
8 | const UserSchema = new Schema(
9 | {
10 | name: String,
11 | password: String,
12 | salt: String // 使用csprng随机生成的盐
13 | },
14 | { versionKey: false }
15 | )
16 |
17 | const ArticleSchema = new Schema(
18 | {
19 | aid: { type : Number, index: { unique: true } },
20 | title: String,
21 | content: String,
22 | tags: [String],
23 | date: Date,
24 | isPublish: Boolean,
25 | comment_n: Number
26 | },
27 | { versionKey: false }
28 | )
29 | const CommentSchema = new Schema(
30 | {
31 | imgName: String,
32 | name: String,
33 | address: String,
34 | content: String,
35 | articleId: Number,
36 | date: Date,
37 | like: Number
38 | },
39 | { versionKey: false }
40 | )
41 |
42 | // 生成从0开始自增长的文章aid
43 | ArticleSchema.pre('save', function(next) {
44 | var self = this;
45 | if( self.isNew ) {
46 | Sequence.increment('Article',function (err, result) {
47 | if (err)
48 | throw err;
49 | self.aid = result.value.next;
50 | next();
51 | });
52 | } else {
53 | next();
54 | }
55 | })
56 |
57 | const Models = {
58 | User: mongoose.model('User', UserSchema),
59 | Article: mongoose.model('Article', ArticleSchema),
60 | Comment: mongoose.model('Comment', CommentSchema)
61 | }
62 |
63 | // 初始化数据
64 | const initialize = () => {
65 | console.log('beginning to initialize data...')
66 | Models.User.find({}, (err, doc) => {
67 | if (err) {
68 | console.log(err)
69 | console.log('initialize failed')
70 | } else if (!doc.length) {
71 | const salt = rand(160, 36)
72 | // 第一次创建站长账户
73 | new Models['User']({name: 'boss', password: sha1('123456' + salt), salt: salt}).save()
74 | Promise.all(data.map((item) => { new Models['Article'](item).save() }))
75 | .then(() => { console.log('initialize successfully') })
76 | .catch(() => { console.log('initialize failed') })
77 | } else {
78 | console.log('initialize successfully')
79 | }
80 | })
81 | }
82 |
83 | mongoose.connect('mongodb://127.0.0.1/my-blog')
84 |
85 | const db = mongoose.connection
86 |
87 | db.on('error',console.error.bind(console,'Database connection error.'));
88 | db.once('open', () => {
89 | console.log('The database has connected.')
90 | initialize()
91 | });
92 |
93 | module.exports = Models
94 |
--------------------------------------------------------------------------------
/server/db/sequence.js:
--------------------------------------------------------------------------------
1 | // 用于生成自增长的序列段,此处用于生成文章的aid
2 | var mongoose = require('mongoose');
3 |
4 | var Schema = mongoose.Schema;
5 | /**
6 | * 存储ID的序列值
7 | */
8 | SequenceSchema = new Schema({
9 | _id: String,
10 | next: Number
11 | });
12 |
13 | SequenceSchema.statics.findAndModify = function (query, sort, doc, options, callback) {
14 | return this.collection.findAndModify(query, sort, doc, options, callback);
15 | };
16 |
17 | SequenceSchema.statics.increment = function (schemaName, callback) {
18 | return this.collection.findAndModify({_id: schemaName}, [],
19 | { $inc: { next: 1 } }, {"new":true, upsert:true}, callback);
20 | };
21 |
22 | var Sequence = mongoose.model('Sequence', SequenceSchema);
23 |
24 | module.exports = Sequence
25 |
26 |
--------------------------------------------------------------------------------
/server/email.js:
--------------------------------------------------------------------------------
1 | const nodemailer = require('nodemailer')
2 |
3 | let transporter = nodemailer.createTransport({
4 | service: '126',
5 | auth: {
6 | user: 'blogbutler@126.com',
7 | pass: '123456abc'
8 | }
9 | })
10 |
11 | exports.send = function(to, subject, html, res) {
12 | const mailOptions = {
13 | from: '"博客小管家" ',
14 | to : to,
15 | subject : subject,
16 | html : html
17 | }
18 |
19 | transporter.sendMail(mailOptions, function(error, info){
20 | if (error) {
21 | console.log(error)
22 | res.status(504).end("通知邮件发送失败")
23 | } else {
24 | console.log("Message sent: " + info.response)
25 | }
26 | })
27 | }
28 |
--------------------------------------------------------------------------------
/server/middlewares/confirmToken.js:
--------------------------------------------------------------------------------
1 | const jwt =require('jsonwebtoken')
2 | const secret = require('../../config').jwt
3 |
4 | // 检查token是否正确
5 | const confirmToken = (req, res, next) => {
6 | if (!req.headers.authorization) {
7 | res.status(401).end('no token')
8 | } else {
9 | const token = req.headers.authorization.split(' ')[1]
10 | jwt.verify(token, secret.cert, function (err) {
11 | if (err) {
12 | res.status(401).end(err.message)
13 | } else {
14 | // 通过验证才执行 next()
15 | next()
16 | }
17 | })
18 | }
19 | // 如果 next() 写在这里不管验证结果如何 虽然提示报错 但是依然会执行后面的逻辑
20 | // next()
21 | }
22 |
23 | module.exports = confirmToken
24 |
--------------------------------------------------------------------------------
/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "blog_server",
3 | "version": "1.0.0",
4 | "description": "node.js",
5 | "main": "app.js",
6 | "author": "FD_one",
7 | "license": "ISC",
8 | "dependencies": {
9 | "body-parser": "^1.17.1",
10 | "csprng": "^0.1.2",
11 | "express": "^4.15.2",
12 | "highlight.js": "^9.10.0",
13 | "jsonwebtoken": "^7.3.0",
14 | "marked": "^0.3.6",
15 | "mongoose": "^4.9.1",
16 | "nodemailer": "^4.0.0",
17 | "sha1": "^1.1.1"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
40 |
41 |
58 |
--------------------------------------------------------------------------------
/src/assets/css/common.scss:
--------------------------------------------------------------------------------
1 | * {
2 | margin: 0;
3 | padding: 0;
4 | }
5 | html {
6 | height: 100%;
7 | font-size: 16px;
8 | }
9 | body {
10 | height: 100%;
11 | font-family: "Microsoft YaHei", "微软雅黑", STXihei, "华文细黑", serif;
12 | }
13 | a {
14 | text-decoration: none;
15 | color: #55ffff;
16 | }
17 | button {
18 | height: 2.5rem;
19 | border: none;
20 | outline: none;
21 | color: #00193a;
22 | background: rgb(129, 216, 208);
23 | margin-top: 1.875rem;
24 | padding-left: 0.4375rem;
25 | cursor: pointer;
26 | border-radius: 0.25rem;
27 | transition: 0.5s;
28 | font-size: 0.875rem;
29 | span {
30 | display: inline-block;
31 | height: 2.5rem;
32 | line-height: 2.5rem;
33 | position: relative;
34 | transition: 0.5s;
35 | }
36 | &:hover span {
37 | padding-right: 0.625rem;
38 | transition: 0.5s;
39 | }
40 | span:after {
41 | content: '\00bb';
42 | opacity: 0;
43 | transition: 0.5s;
44 | font-size: 1.25rem;
45 | position: relative;
46 | right: 0;
47 | }
48 | &:hover span:after{
49 | opacity: 1;
50 | right: -0.5rem;
51 | }
52 | }
53 | .hashTitle {
54 | color: white;
55 | }
56 |
57 | h2, h3, h4, h5, h6 {
58 | padding-top: 0.8rem;
59 | color: #F0F0F0;
60 | }
61 |
62 |
63 | @media screen and (max-width: 400px) {
64 | html {font-size: 18px;}
65 | }
66 |
67 | @media screen and (max-width: 360px) {
68 | html {font-size: 17px;}
69 | }
70 |
71 | @media screen and (max-width: 320px) {
72 | html {font-size: 16px;}
73 | }
74 |
75 |
--------------------------------------------------------------------------------
/src/assets/css/highlight.scss:
--------------------------------------------------------------------------------
1 | .lang-js {
2 | padding: 0;
3 | }
4 | .hljs-comment,
5 | .hljs-quote {
6 | color: #8e908c;
7 | }
8 |
9 | .hljs-variable,
10 | .hljs-template-variable,
11 | .hljs-tag,
12 | .hljs-name,
13 | .hljs-selector-id,
14 | .hljs-selector-class,
15 | .hljs-regexp,
16 | .hljs-deletion {
17 | color: darkturquoise;
18 | }
19 |
20 | .hljs-number,
21 | .hljs-built_in,
22 | .hljs-builtin-name,
23 | .hljs-literal,
24 | .hljs-type,
25 | .hljs-params,
26 | .hljs-meta,
27 | .hljs-link {
28 | color: #f5871f;
29 | }
30 |
31 | .hljs-attribute {
32 | color: #eab700;
33 | }
34 |
35 | .hljs-string,
36 | .hljs-symbol,
37 | .hljs-bullet,
38 | .hljs-addition {
39 | color: greenyellow;
40 | }
41 |
42 | .hljs-title,
43 | .hljs-section {
44 | color: #55ffff;
45 | }
46 |
47 | .hljs-keyword,
48 | .hljs-selector-tag {
49 | color: rgb(207, 32, 255);
50 | }
51 |
52 | .hljs {
53 | display: block;
54 | overflow-x: auto;
55 | background: white;
56 | color: #4d4d4c;
57 | }
58 |
59 | .hljs-emphasis {
60 | font-style: italic;
61 | }
62 |
63 | .hljs-strong {
64 | font-weight: bold;
65 | }
--------------------------------------------------------------------------------
/src/assets/css/icon.scss:
--------------------------------------------------------------------------------
1 |
2 | @font-face {font-family: "iconfont";
3 | src: url('//at.alicdn.com/t/font_hvlvsgwsxwm6lxr.eot?t=1492504246115'); /* IE9*/
4 | src: url('//at.alicdn.com/t/font_hvlvsgwsxwm6lxr.eot?t=1492504246115#iefix') format('embedded-opentype'), /* IE6-IE8 */
5 | url('//at.alicdn.com/t/font_hvlvsgwsxwm6lxr.woff?t=1492504246115') format('woff'), /* chrome, firefox */
6 | url('//at.alicdn.com/t/font_hvlvsgwsxwm6lxr.ttf?t=1492504246115') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/
7 | url('//at.alicdn.com/t/font_hvlvsgwsxwm6lxr.svg?t=1492504246115#iconfont') format('svg'); /* iOS 4.1- */
8 | }
9 |
10 | .iconfont {
11 | font-family:"iconfont" !important;
12 | font-size:16px;
13 | font-style:normal;
14 | -webkit-font-smoothing: antialiased;
15 | -moz-osx-font-smoothing: grayscale;
16 | }
17 |
18 | .icon-zhanghu:before { content: "\e600"; }
19 |
20 | .icon-fanhui:before { content: "\e607"; }
21 |
22 | .icon-shanchu:before { content: "\e636"; }
23 |
24 | .icon-zhuye:before { content: "\e610"; }
25 |
26 | .icon-label:before { content: "\e61b"; }
27 |
28 | .icon-iconsf:before { content: "\e602"; }
29 |
30 | .icon-like:before { content: "\e609"; }
31 |
32 | .icon-zengjia:before { content: "\e6a7"; }
33 |
34 | .icon-shanchu1:before { content: "\e620"; }
35 |
36 | .icon-github:before { content: "\e69f"; }
37 |
38 | .icon-shijian:before { content: "\e626"; }
39 |
40 | .icon-icon13:before { content: "\e627"; }
41 |
42 | .icon-draft:before { content: "\e679"; }
43 |
44 | .icon-biji-copy:before { content: "\e601"; }
45 |
46 | .icon-mulu:before { content: "\e638"; }
47 |
48 | .icon-cion15:before { content: "\e60f"; }
49 |
50 | .icon-huojian:before { content: "\e617"; }
51 |
52 | .icon-icon69:before { content: "\e786"; }
53 |
54 | .icon-yuechi:before { content: "\e676"; }
55 |
56 | .icon-search:before { content: "\e603"; }
57 |
58 | .icon-left:before { content: "\e604"; }
59 |
60 | .icon-right:before { content: "\e605"; }
61 |
62 | .icon-huifu:before { content: "\e61c"; }
63 |
64 | .icon-out:before { content: "\e611"; }
65 |
--------------------------------------------------------------------------------
/src/assets/css/index.scss:
--------------------------------------------------------------------------------
1 | @import './common.scss';
2 | @import './marked.scss';
3 | @import '../../assets/css/animate.min.css';
4 | @import 'highlight';
5 | @import '../../assets/css/icon.scss';
6 | @import './normalize.css'
7 |
--------------------------------------------------------------------------------
/src/assets/css/marked.scss:
--------------------------------------------------------------------------------
1 | blockquote {
2 | margin-top: 1.25rem;
3 | border-left: 0.1875rem solid rgb(129, 216, 208);
4 | padding: 0 0.625rem;
5 | background: rgba(204, 204, 204, 0.5);
6 | }
7 | ul, ol{
8 | margin-top: 0;
9 | margin-bottom: -2rem;
10 | padding-left: 1.25rem;
11 | line-height: 1.125rem;
12 | }
13 |
14 | img:not(.bgImage) {
15 | border: 1px solid #ccc;
16 | }
17 | code {
18 | color: rgb(147, 240, 216);
19 | background: rgb(71, 73,73);
20 | font-family: Consolas;
21 | padding: 0 0.3125rem;
22 | border-radius: 0.3125rem;
23 | overflow-x: auto;
24 | }
25 | hr {
26 | margin: 0.625rem 0 0;
27 | }
28 | pre {
29 | padding: 0.625rem 0.625rem;
30 | background: rgb(71, 73,73);
31 | border-radius: 0.3125rem;
32 | overflow-x: auto;
33 | }
34 | h1, h2, h3, h4, h5, h6 {
35 | margin-bottom: 0.625rem;
36 | }
--------------------------------------------------------------------------------
/src/assets/css/normalize.css:
--------------------------------------------------------------------------------
1 | /*! normalize.css v7.0.0 | MIT License | github.com/necolas/normalize.css */
2 |
3 | /* Document
4 | ========================================================================== */
5 |
6 | /**
7 | * 1. Correct the line height in all browsers.
8 | * 2. Prevent adjustments of font size after orientation changes in
9 | * IE on Windows Phone and in iOS.
10 | */
11 |
12 | html {
13 | -ms-text-size-adjust: 100%; /* 2 */
14 | -webkit-text-size-adjust: 100%; /* 2 */
15 | }
16 |
17 | /* Sections
18 | ========================================================================== */
19 |
20 | /**
21 | * Remove the margin in all browsers (opinionated).
22 | */
23 |
24 | body {
25 | margin: 0;
26 | }
27 |
28 | /**
29 | * Add the correct display in IE 9-.
30 | */
31 |
32 | article,
33 | aside,
34 | footer,
35 | header,
36 | nav,
37 | section {
38 | display: block;
39 | }
40 |
41 | /**
42 | * Correct the font size and margin on `h1` elements within `section` and
43 | * `article` contexts in Chrome, Firefox, and Safari.
44 | */
45 |
46 | h1 {
47 | font-size: 2em;
48 | margin: 0.67em 0;
49 | }
50 |
51 | /* Grouping content
52 | ========================================================================== */
53 |
54 | /**
55 | * Add the correct display in IE 9-.
56 | * 1. Add the correct display in IE.
57 | */
58 |
59 | figcaption,
60 | figure,
61 | main { /* 1 */
62 | display: block;
63 | }
64 |
65 | /**
66 | * Add the correct margin in IE 8.
67 | */
68 |
69 | figure {
70 | margin: 1em 40px;
71 | }
72 |
73 | /**
74 | * 1. Add the correct box sizing in Firefox.
75 | * 2. Show the overflow in Edge and IE.
76 | */
77 |
78 | hr {
79 | box-sizing: content-box; /* 1 */
80 | overflow: visible; /* 2 */
81 | }
82 |
83 | /**
84 | * 1. Correct the inheritance and scaling of font size in all browsers.
85 | * 2. Correct the odd `em` font sizing in all browsers.
86 | */
87 |
88 | pre {
89 | font-size: 1em; /* 2 */
90 | }
91 |
92 | /* Text-level semantics
93 | ========================================================================== */
94 |
95 | /**
96 | * 1. Remove the gray background on active links in IE 10.
97 | * 2. Remove gaps in links underline in iOS 8+ and Safari 8+.
98 | */
99 |
100 | a {
101 | background-color: transparent; /* 1 */
102 | -webkit-text-decoration-skip: objects; /* 2 */
103 | }
104 |
105 | /**
106 | * 1. Remove the bottom border in Chrome 57- and Firefox 39-.
107 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
108 | */
109 |
110 | abbr[title] {
111 | border-bottom: none; /* 1 */
112 | text-decoration: underline; /* 2 */
113 | text-decoration: underline dotted; /* 2 */
114 | }
115 |
116 | /**
117 | * Prevent the duplicate application of `bolder` by the next rule in Safari 6.
118 | */
119 |
120 | b,
121 | strong {
122 | font-weight: inherit;
123 | }
124 |
125 | /**
126 | * Add the correct font weight in Chrome, Edge, and Safari.
127 | */
128 |
129 | b,
130 | strong {
131 | font-weight: bolder;
132 | }
133 |
134 | /**
135 | * 1. Correct the inheritance and scaling of font size in all browsers.
136 | * 2. Correct the odd `em` font sizing in all browsers.
137 | */
138 |
139 | code,
140 | kbd,
141 | samp {
142 | font-size: 1em; /* 2 */
143 | }
144 |
145 | /**
146 | * Add the correct font style in Android 4.3-.
147 | */
148 |
149 | dfn {
150 | font-style: italic;
151 | }
152 |
153 | /**
154 | * Add the correct background and color in IE 9-.
155 | */
156 |
157 | mark {
158 | background-color: #ff0;
159 | color: #000;
160 | }
161 |
162 | /**
163 | * Add the correct font size in all browsers.
164 | */
165 |
166 | small {
167 | font-size: 80%;
168 | }
169 |
170 | /**
171 | * Prevent `sub` and `sup` elements from affecting the line height in
172 | * all browsers.
173 | */
174 |
175 | sub,
176 | sup {
177 | font-size: 75%;
178 | line-height: 0;
179 | position: relative;
180 | vertical-align: baseline;
181 | }
182 |
183 | sub {
184 | bottom: -0.25em;
185 | }
186 |
187 | sup {
188 | top: -0.5em;
189 | }
190 |
191 | /* Embedded content
192 | ========================================================================== */
193 |
194 | /**
195 | * Add the correct display in IE 9-.
196 | */
197 |
198 | audio,
199 | video {
200 | display: inline-block;
201 | }
202 |
203 | /**
204 | * Add the correct display in iOS 4-7.
205 | */
206 |
207 | audio:not([controls]) {
208 | display: none;
209 | height: 0;
210 | }
211 |
212 | /**
213 | * Remove the border on images inside links in IE 10-.
214 | */
215 |
216 | img {
217 | border-style: none;
218 | }
219 |
220 | /**
221 | * Hide the overflow in IE.
222 | */
223 |
224 | svg:not(:root) {
225 | overflow: hidden;
226 | }
227 |
228 | /* Forms
229 | ========================================================================== */
230 |
231 | /**
232 | * 1. Change the font styles in all browsers (opinionated).
233 | * 2. Remove the margin in Firefox and Safari.
234 | */
235 |
236 | input,
237 | optgroup,
238 | select,
239 | textarea {
240 | font-family: sans-serif; /* 1 */
241 | font-size: 100%; /* 1 */
242 | line-height: 1.15; /* 1 */
243 | margin: 0; /* 2 */
244 | }
245 |
246 | /**
247 | * Show the overflow in IE.
248 | * 1. Show the overflow in Edge.
249 | */
250 |
251 | button,
252 | input { /* 1 */
253 | overflow: visible;
254 | }
255 |
256 | /**
257 | * Remove the inheritance of text transform in Edge, Firefox, and IE.
258 | * 1. Remove the inheritance of text transform in Firefox.
259 | */
260 |
261 | button,
262 | select { /* 1 */
263 | text-transform: none;
264 | }
265 |
266 | /**
267 | * 1. Prevent a WebKit bug where (2) destroys native `audio` and `video`
268 | * controls in Android 4.
269 | * 2. Correct the inability to style clickable types in iOS and Safari.
270 | */
271 |
272 | button,
273 | html [type="button"], /* 1 */
274 | [type="reset"],
275 | [type="submit"] {
276 | -webkit-appearance: button; /* 2 */
277 | }
278 |
279 | /**
280 | * Remove the inner border and padding in Firefox.
281 | */
282 |
283 | button::-moz-focus-inner,
284 | [type="button"]::-moz-focus-inner,
285 | [type="reset"]::-moz-focus-inner,
286 | [type="submit"]::-moz-focus-inner {
287 | border-style: none;
288 | padding: 0;
289 | }
290 |
291 | /**
292 | * Restore the focus styles unset by the previous rule.
293 | */
294 |
295 | button:-moz-focusring,
296 | [type="button"]:-moz-focusring,
297 | [type="reset"]:-moz-focusring,
298 | [type="submit"]:-moz-focusring {
299 | outline: 1px dotted ButtonText;
300 | }
301 |
302 | /**
303 | * Correct the padding in Firefox.
304 | */
305 |
306 | fieldset {
307 | padding: 0.35em 0.75em 0.625em;
308 | }
309 |
310 | /**
311 | * 1. Correct the text wrapping in Edge and IE.
312 | * 2. Correct the color inheritance from `fieldset` elements in IE.
313 | * 3. Remove the padding so developers are not caught out when they zero out
314 | * `fieldset` elements in all browsers.
315 | */
316 |
317 | legend {
318 | box-sizing: border-box; /* 1 */
319 | color: inherit; /* 2 */
320 | display: table; /* 1 */
321 | max-width: 100%; /* 1 */
322 | padding: 0; /* 3 */
323 | white-space: normal; /* 1 */
324 | }
325 |
326 | /**
327 | * 1. Add the correct display in IE 9-.
328 | * 2. Add the correct vertical alignment in Chrome, Firefox, and Opera.
329 | */
330 |
331 | progress {
332 | display: inline-block; /* 1 */
333 | vertical-align: baseline; /* 2 */
334 | }
335 |
336 | /**
337 | * Remove the default vertical scrollbar in IE.
338 | */
339 |
340 | textarea {
341 | overflow: auto;
342 | }
343 |
344 | /**
345 | * 1. Add the correct box sizing in IE 10-.
346 | * 2. Remove the padding in IE 10-.
347 | */
348 |
349 | [type="checkbox"],
350 | [type="radio"] {
351 | box-sizing: border-box; /* 1 */
352 | padding: 0; /* 2 */
353 | }
354 |
355 | /**
356 | * Correct the cursor style of increment and decrement buttons in Chrome.
357 | */
358 |
359 | [type="number"]::-webkit-inner-spin-button,
360 | [type="number"]::-webkit-outer-spin-button {
361 | height: auto;
362 | }
363 |
364 | /**
365 | * 1. Correct the odd appearance in Chrome and Safari.
366 | * 2. Correct the outline style in Safari.
367 | */
368 |
369 | [type="search"] {
370 | -webkit-appearance: textfield; /* 1 */
371 | outline-offset: -2px; /* 2 */
372 | }
373 |
374 | /**
375 | * Remove the inner padding and cancel buttons in Chrome and Safari on macOS.
376 | */
377 |
378 | [type="search"]::-webkit-search-cancel-button,
379 | [type="search"]::-webkit-search-decoration {
380 | -webkit-appearance: none;
381 | }
382 |
383 | /**
384 | * 1. Correct the inability to style clickable types in iOS and Safari.
385 | * 2. Change font properties to `inherit` in Safari.
386 | */
387 |
388 | ::-webkit-file-upload-button {
389 | -webkit-appearance: button; /* 1 */
390 | font: inherit; /* 2 */
391 | }
392 |
393 | /* Interactive
394 | ========================================================================== */
395 |
396 | /*
397 | * Add the correct display in IE 9-.
398 | * 1. Add the correct display in Edge, IE, and Firefox.
399 | */
400 |
401 | details, /* 1 */
402 | menu {
403 | display: block;
404 | }
405 |
406 | /*
407 | * Add the correct display in all browsers.
408 | */
409 |
410 | summary {
411 | display: list-item;
412 | }
413 |
414 | /* Scripting
415 | ========================================================================== */
416 |
417 | /**
418 | * Add the correct display in IE 9-.
419 | */
420 |
421 | canvas {
422 | display: inline-block;
423 | }
424 |
425 | /**
426 | * Add the correct display in IE.
427 | */
428 |
429 | template {
430 | display: none;
431 | }
432 |
433 | /* Hidden
434 | ========================================================================== */
435 |
436 | /**
437 | * Add the correct display in IE 10-.
438 | */
439 |
440 | [hidden] {
441 | display: none;
442 | }
--------------------------------------------------------------------------------
/src/assets/js/highlight.pack.js:
--------------------------------------------------------------------------------
1 | /*! highlight.js v9.10.0 | BSD3 License | git.io/hljslicense */
2 | !function(e){var n="object"==typeof window&&window||"object"==typeof self&&self;"undefined"!=typeof exports?e(exports):n&&(n.hljs=e({}),"function"==typeof define&&define.amd&&define([],function(){return n.hljs}))}(function(e){function n(e){return e.replace(/[&<>]/gm,function(e){return j[e]})}function t(e){return e.nodeName.toLowerCase()}function r(e,n){var t=e&&e.exec(n);return t&&0===t.index}function a(e){return k.test(e)}function i(e){var n,t,r,i,o=e.className+" ";if(o+=e.parentNode?e.parentNode.className:"",t=B.exec(o))return w(t[1])?t[1]:"no-highlight";for(o=o.split(/\s+/),n=0,r=o.length;r>n;n++)if(i=o[n],a(i)||w(i))return i}function o(e){var n,t={},r=Array.prototype.slice.call(arguments,1);for(n in e)t[n]=e[n];return r.forEach(function(e){for(n in e)t[n]=e[n]}),t}function u(e){var n=[];return function r(e,a){for(var i=e.firstChild;i;i=i.nextSibling)3===i.nodeType?a+=i.nodeValue.length:1===i.nodeType&&(n.push({event:"start",offset:a,node:i}),a=r(i,a),t(i).match(/br|hr|img|input/)||n.push({event:"stop",offset:a,node:i}));return a}(e,0),n}function c(e,r,a){function i(){return e.length&&r.length?e[0].offset!==r[0].offset?e[0].offset"}function u(e){l+=""+t(e)+">"}function c(e){("start"===e.event?o:u)(e.node)}for(var s=0,l="",f=[];e.length||r.length;){var g=i();if(l+=n(a.substring(s,g[0].offset)),s=g[0].offset,g===e){f.reverse().forEach(u);do c(g.splice(0,1)[0]),g=i();while(g===e&&g.length&&g[0].offset===s);f.reverse().forEach(o)}else"start"===g[0].event?f.push(g[0].node):f.pop(),c(g.splice(0,1)[0])}return l+n(a.substr(s))}function s(e){return e.v&&!e.cached_variants&&(e.cached_variants=e.v.map(function(n){return o(e,{v:null},n)})),e.cached_variants||e.eW&&[o(e)]||[e]}function l(e){function n(e){return e&&e.source||e}function t(t,r){return new RegExp(n(t),"m"+(e.cI?"i":"")+(r?"g":""))}function r(a,i){if(!a.compiled){if(a.compiled=!0,a.k=a.k||a.bK,a.k){var o={},u=function(n,t){e.cI&&(t=t.toLowerCase()),t.split(" ").forEach(function(e){var t=e.split("|");o[t[0]]=[n,t[1]?Number(t[1]):1]})};"string"==typeof a.k?u("keyword",a.k):x(a.k).forEach(function(e){u(e,a.k[e])}),a.k=o}a.lR=t(a.l||/\w+/,!0),i&&(a.bK&&(a.b="\\b("+a.bK.split(" ").join("|")+")\\b"),a.b||(a.b=/\B|\b/),a.bR=t(a.b),a.e||a.eW||(a.e=/\B|\b/),a.e&&(a.eR=t(a.e)),a.tE=n(a.e)||"",a.eW&&i.tE&&(a.tE+=(a.e?"|":"")+i.tE)),a.i&&(a.iR=t(a.i)),null==a.r&&(a.r=1),a.c||(a.c=[]),a.c=Array.prototype.concat.apply([],a.c.map(function(e){return s("self"===e?a:e)})),a.c.forEach(function(e){r(e,a)}),a.starts&&r(a.starts,i);var c=a.c.map(function(e){return e.bK?"\\.?("+e.b+")\\.?":e.b}).concat([a.tE,a.i]).map(n).filter(Boolean);a.t=c.length?t(c.join("|"),!0):{exec:function(){return null}}}}r(e)}function f(e,t,a,i){function o(e,n){var t,a;for(t=0,a=n.c.length;a>t;t++)if(r(n.c[t].bR,e))return n.c[t]}function u(e,n){if(r(e.eR,n)){for(;e.endsParent&&e.parent;)e=e.parent;return e}return e.eW?u(e.parent,n):void 0}function c(e,n){return!a&&r(n.iR,e)}function s(e,n){var t=N.cI?n[0].toLowerCase():n[0];return e.k.hasOwnProperty(t)&&e.k[t]}function p(e,n,t,r){var a=r?"":I.classPrefix,i='',i+n+o}function h(){var e,t,r,a;if(!E.k)return n(k);for(a="",t=0,E.lR.lastIndex=0,r=E.lR.exec(k);r;)a+=n(k.substring(t,r.index)),e=s(E,r),e?(B+=e[1],a+=p(e[0],n(r[0]))):a+=n(r[0]),t=E.lR.lastIndex,r=E.lR.exec(k);return a+n(k.substr(t))}function d(){var e="string"==typeof E.sL;if(e&&!L[E.sL])return n(k);var t=e?f(E.sL,k,!0,x[E.sL]):g(k,E.sL.length?E.sL:void 0);return E.r>0&&(B+=t.r),e&&(x[E.sL]=t.top),p(t.language,t.value,!1,!0)}function b(){y+=null!=E.sL?d():h(),k=""}function v(e){y+=e.cN?p(e.cN,"",!0):"",E=Object.create(e,{parent:{value:E}})}function m(e,n){if(k+=e,null==n)return b(),0;var t=o(n,E);if(t)return t.skip?k+=n:(t.eB&&(k+=n),b(),t.rB||t.eB||(k=n)),v(t,n),t.rB?0:n.length;var r=u(E,n);if(r){var a=E;a.skip?k+=n:(a.rE||a.eE||(k+=n),b(),a.eE&&(k=n));do E.cN&&(y+=C),E.skip||(B+=E.r),E=E.parent;while(E!==r.parent);return r.starts&&v(r.starts,""),a.rE?0:n.length}if(c(n,E))throw new Error('Illegal lexeme "'+n+'" for mode "'+(E.cN||"")+'"');return k+=n,n.length||1}var N=w(e);if(!N)throw new Error('Unknown language: "'+e+'"');l(N);var R,E=i||N,x={},y="";for(R=E;R!==N;R=R.parent)R.cN&&(y=p(R.cN,"",!0)+y);var k="",B=0;try{for(var M,j,O=0;;){if(E.t.lastIndex=O,M=E.t.exec(t),!M)break;j=m(t.substring(O,M.index),M[0]),O=M.index+j}for(m(t.substr(O)),R=E;R.parent;R=R.parent)R.cN&&(y+=C);return{r:B,value:y,language:e,top:E}}catch(T){if(T.message&&-1!==T.message.indexOf("Illegal"))return{r:0,value:n(t)};throw T}}function g(e,t){t=t||I.languages||x(L);var r={r:0,value:n(e)},a=r;return t.filter(w).forEach(function(n){var t=f(n,e,!1);t.language=n,t.r>a.r&&(a=t),t.r>r.r&&(a=r,r=t)}),a.language&&(r.second_best=a),r}function p(e){return I.tabReplace||I.useBR?e.replace(M,function(e,n){return I.useBR&&"\n"===e?" ":I.tabReplace?n.replace(/\t/g,I.tabReplace):""}):e}function h(e,n,t){var r=n?y[n]:t,a=[e.trim()];return e.match(/\bhljs\b/)||a.push("hljs"),-1===e.indexOf(r)&&a.push(r),a.join(" ").trim()}function d(e){var n,t,r,o,s,l=i(e);a(l)||(I.useBR?(n=document.createElementNS("http://www.w3.org/1999/xhtml","div"),n.innerHTML=e.innerHTML.replace(/\n/g,"").replace(/ /g,"\n")):n=e,s=n.textContent,r=l?f(l,s,!0):g(s),t=u(n),t.length&&(o=document.createElementNS("http://www.w3.org/1999/xhtml","div"),o.innerHTML=r.value,r.value=c(t,u(o),s)),r.value=p(r.value),e.innerHTML=r.value,e.className=h(e.className,l,r.language),e.result={language:r.language,re:r.r},r.second_best&&(e.second_best={language:r.second_best.language,re:r.second_best.r}))}function b(e){I=o(I,e)}function v(){if(!v.called){v.called=!0;var e=document.querySelectorAll("pre code");E.forEach.call(e,d)}}function m(){addEventListener("DOMContentLoaded",v,!1),addEventListener("load",v,!1)}function N(n,t){var r=L[n]=t(e);r.aliases&&r.aliases.forEach(function(e){y[e]=n})}function R(){return x(L)}function w(e){return e=(e||"").toLowerCase(),L[e]||L[y[e]]}var E=[],x=Object.keys,L={},y={},k=/^(no-?highlight|plain|text)$/i,B=/\blang(?:uage)?-([\w-]+)\b/i,M=/((^(<[^>]+>|\t|)+|(?:\n)))/gm,C=" ",I={classPrefix:"hljs-",tabReplace:null,useBR:!1,languages:void 0},j={"&":"&","<":"<",">":">"};return e.highlight=f,e.highlightAuto=g,e.fixMarkup=p,e.highlightBlock=d,e.configure=b,e.initHighlighting=v,e.initHighlightingOnLoad=m,e.registerLanguage=N,e.listLanguages=R,e.getLanguage=w,e.inherit=o,e.IR="[a-zA-Z]\\w*",e.UIR="[a-zA-Z_]\\w*",e.NR="\\b\\d+(\\.\\d+)?",e.CNR="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",e.BNR="\\b(0b[01]+)",e.RSR="!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",e.BE={b:"\\\\[\\s\\S]",r:0},e.ASM={cN:"string",b:"'",e:"'",i:"\\n",c:[e.BE]},e.QSM={cN:"string",b:'"',e:'"',i:"\\n",c:[e.BE]},e.PWM={b:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|like)\b/},e.C=function(n,t,r){var a=e.inherit({cN:"comment",b:n,e:t,c:[]},r||{});return a.c.push(e.PWM),a.c.push({cN:"doctag",b:"(?:TODO|FIXME|NOTE|BUG|XXX):",r:0}),a},e.CLCM=e.C("//","$"),e.CBCM=e.C("/\\*","\\*/"),e.HCM=e.C("#","$"),e.NM={cN:"number",b:e.NR,r:0},e.CNM={cN:"number",b:e.CNR,r:0},e.BNM={cN:"number",b:e.BNR,r:0},e.CSSNM={cN:"number",b:e.NR+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",r:0},e.RM={cN:"regexp",b:/\//,e:/\/[gimuy]*/,i:/\n/,c:[e.BE,{b:/\[/,e:/\]/,r:0,c:[e.BE]}]},e.TM={cN:"title",b:e.IR,r:0},e.UTM={cN:"title",b:e.UIR,r:0},e.METHOD_GUARD={b:"\\.\\s*"+e.UIR,r:0},e});hljs.registerLanguage("xml",function(s){var e="[A-Za-z0-9\\._:-]+",t={eW:!0,i:/,r:0,c:[{cN:"attr",b:e,r:0},{b:/=\s*/,r:0,c:[{cN:"string",endsParent:!0,v:[{b:/"/,e:/"/},{b:/'/,e:/'/},{b:/[^\s"'=<>`]+/}]}]}]};return{aliases:["html","xhtml","rss","atom","xjb","xsd","xsl","plist"],cI:!0,c:[{cN:"meta",b:"",r:10,c:[{b:"\\[",e:"\\]"}]},s.C("",{r:10}),{b:"<\\!\\[CDATA\\[",e:"\\]\\]>",r:10},{b:/<\?(php)?/,e:/\?>/,sL:"php",c:[{b:"/\\*",e:"\\*/",skip:!0}]},{cN:"tag",b:"",rE:!0,sL:["css","xml"]}},{cN:"tag",b:"",rE:!0,sL:["actionscript","javascript","handlebars","xml"]}},{cN:"meta",v:[{b:/<\?xml/,e:/\?>/,r:10},{b:/<\?\w+/,e:/\?>/}]},{cN:"tag",b:"?",e:"/?>",c:[{cN:"name",b:/[^\/><\s]+/,r:0},t]}]}});hljs.registerLanguage("markdown",function(e){return{aliases:["md","mkdown","mkd"],c:[{cN:"section",v:[{b:"^#{1,6}",e:"$"},{b:"^.+?\\n[=-]{2,}$"}]},{b:"<",e:">",sL:"xml",r:0},{cN:"bullet",b:"^([*+-]|(\\d+\\.))\\s+"},{cN:"strong",b:"[*_]{2}.+?[*_]{2}"},{cN:"emphasis",v:[{b:"\\*.+?\\*"},{b:"_.+?_",r:0}]},{cN:"quote",b:"^>\\s+",e:"$"},{cN:"code",v:[{b:"^```w*s*$",e:"^```s*$"},{b:"`.+?`"},{b:"^( {4}| )",e:"$",r:0}]},{b:"^[-\\*]{3,}",e:"$"},{b:"\\[.+?\\][\\(\\[].*?[\\)\\]]",rB:!0,c:[{cN:"string",b:"\\[",e:"\\]",eB:!0,rE:!0,r:0},{cN:"link",b:"\\]\\(",e:"\\)",eB:!0,eE:!0},{cN:"symbol",b:"\\]\\[",e:"\\]",eB:!0,eE:!0}],r:10},{b:/^\[[^\n]+\]:/,rB:!0,c:[{cN:"symbol",b:/\[/,e:/\]/,eB:!0,eE:!0},{cN:"link",b:/:\s*/,e:/$/,eB:!0}]}]}});hljs.registerLanguage("coffeescript",function(e){var c={keyword:"in if for while finally new do return else break catch instanceof throw try this switch continue typeof delete debugger super yield import export from as default await then unless until loop of by when and or is isnt not",literal:"true false null undefined yes no on off",built_in:"npm require console print module global window document"},n="[A-Za-z$_][0-9A-Za-z$_]*",r={cN:"subst",b:/#\{/,e:/}/,k:c},i=[e.BNM,e.inherit(e.CNM,{starts:{e:"(\\s*/)?",r:0}}),{cN:"string",v:[{b:/'''/,e:/'''/,c:[e.BE]},{b:/'/,e:/'/,c:[e.BE]},{b:/"""/,e:/"""/,c:[e.BE,r]},{b:/"/,e:/"/,c:[e.BE,r]}]},{cN:"regexp",v:[{b:"///",e:"///",c:[r,e.HCM]},{b:"//[gim]*",r:0},{b:/\/(?![ *])(\\\/|.)*?\/[gim]*(?=\W|$)/}]},{b:"@"+n},{sL:"javascript",eB:!0,eE:!0,v:[{b:"```",e:"```"},{b:"`",e:"`"}]}];r.c=i;var s=e.inherit(e.TM,{b:n}),t="(\\(.*\\))?\\s*\\B[-=]>",o={cN:"params",b:"\\([^\\(]",rB:!0,c:[{b:/\(/,e:/\)/,k:c,c:["self"].concat(i)}]};return{aliases:["coffee","cson","iced"],k:c,i:/\/\*/,c:i.concat([e.C("###","###"),e.HCM,{cN:"function",b:"^\\s*"+n+"\\s*=\\s*"+t,e:"[-=]>",rB:!0,c:[s,o]},{b:/[:\(,=]\s*/,r:0,c:[{cN:"function",b:t,e:"[-=]>",rB:!0,c:[o]}]},{cN:"class",bK:"class",e:"$",i:/[:="\[\]]/,c:[{bK:"extends",eW:!0,i:/[:="\[\]]/,c:[s]},s]},{b:n+":",e:":",rB:!0,rE:!0,r:0}])}});hljs.registerLanguage("javascript",function(e){var r="[A-Za-z$_][0-9A-Za-z$_]*",t={keyword:"in of if for while finally var new function do return void else break catch instanceof with throw case default try this switch continue typeof delete let yield const export super debugger as async await static import from as",literal:"true false null undefined NaN Infinity",built_in:"eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape Object Function Boolean Error EvalError InternalError RangeError ReferenceError StopIteration SyntaxError TypeError URIError Number Math Date String RegExp Array Float32Array Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array Uint8Array Uint8ClampedArray ArrayBuffer DataView JSON Intl arguments require module console window document Symbol Set Map WeakSet WeakMap Proxy Reflect Promise"},a={cN:"number",v:[{b:"\\b(0[bB][01]+)"},{b:"\\b(0[oO][0-7]+)"},{b:e.CNR}],r:0},n={cN:"subst",b:"\\$\\{",e:"\\}",k:t,c:[]},c={cN:"string",b:"`",e:"`",c:[e.BE,n]};n.c=[e.ASM,e.QSM,c,a,e.RM];var s=n.c.concat([e.CBCM,e.CLCM]);return{aliases:["js","jsx"],k:t,c:[{cN:"meta",r:10,b:/^\s*['"]use (strict|asm)['"]/},{cN:"meta",b:/^#!/,e:/$/},e.ASM,e.QSM,c,e.CLCM,e.CBCM,a,{b:/[{,]\s*/,r:0,c:[{b:r+"\\s*:",rB:!0,r:0,c:[{cN:"attr",b:r,r:0}]}]},{b:"("+e.RSR+"|\\b(case|return|throw)\\b)\\s*",k:"return throw case",c:[e.CLCM,e.CBCM,e.RM,{cN:"function",b:"(\\(.*?\\)|"+r+")\\s*=>",rB:!0,e:"\\s*=>",c:[{cN:"params",v:[{b:r},{b:/\(\s*\)/},{b:/\(/,e:/\)/,eB:!0,eE:!0,k:t,c:s}]}]},{b:/,e:/(\/\w+|\w+\/)>/,sL:"xml",c:[{b:/<\w+\s*\/>/,skip:!0},{b:/<\w+/,e:/(\/\w+|\w+\/)>/,skip:!0,c:[{b:/<\w+\s*\/>/,skip:!0},"self"]}]}],r:0},{cN:"function",bK:"function",e:/\{/,eE:!0,c:[e.inherit(e.TM,{b:r}),{cN:"params",b:/\(/,e:/\)/,eB:!0,eE:!0,c:s}],i:/\[|%/},{b:/\$[(.]/},e.METHOD_GUARD,{cN:"class",bK:"class",e:/[{;=]/,eE:!0,i:/[:"\[\]]/,c:[{bK:"extends"},e.UTM]},{bK:"constructor",e:/\{/,eE:!0}],i:/#(?!!)/}});hljs.registerLanguage("css",function(e){var c="[a-zA-Z-][a-zA-Z0-9_-]*",t={b:/[A-Z\_\.\-]+\s*:/,rB:!0,e:";",eW:!0,c:[{cN:"attribute",b:/\S/,e:":",eE:!0,starts:{eW:!0,eE:!0,c:[{b:/[\w-]+\(/,rB:!0,c:[{cN:"built_in",b:/[\w-]+/},{b:/\(/,e:/\)/,c:[e.ASM,e.QSM]}]},e.CSSNM,e.QSM,e.ASM,e.CBCM,{cN:"number",b:"#[0-9A-Fa-f]+"},{cN:"meta",b:"!important"}]}}]};return{cI:!0,i:/[=\/|'\$]/,c:[e.CBCM,{cN:"selector-id",b:/#[A-Za-z0-9_-]+/},{cN:"selector-class",b:/\.[A-Za-z0-9_-]+/},{cN:"selector-attr",b:/\[/,e:/\]/,i:"$"},{cN:"selector-pseudo",b:/:(:)?[a-zA-Z0-9\_\-\+\(\)"'.]+/},{b:"@(font-face|page)",l:"[a-z-]+",k:"font-face page"},{b:"@",e:"[{;]",i:/:/,c:[{cN:"keyword",b:/\w+/},{b:/\s/,eW:!0,eE:!0,r:0,c:[e.ASM,e.QSM,e.CSSNM]}]},{cN:"selector-tag",b:c,r:0},{b:"{",e:"}",i:/\S/,c:[e.CBCM,t]}]}});hljs.registerLanguage("json",function(e){var i={literal:"true false null"},n=[e.QSM,e.CNM],r={e:",",eW:!0,eE:!0,c:n,k:i},t={b:"{",e:"}",c:[{cN:"attr",b:/"/,e:/"/,c:[e.BE],i:"\\n"},e.inherit(r,{b:/:/})],i:"\\S"},c={b:"\\[",e:"\\]",c:[e.inherit(r)],i:"\\S"};return n.splice(n.length,0,t,c),{c:n,k:i,i:"\\S"}});hljs.registerLanguage("python",function(e){var r={keyword:"and elif is global as in if from raise for except finally print import pass return exec else break not with class assert yield try while continue del or def lambda async await nonlocal|10 None True False",built_in:"Ellipsis NotImplemented"},b={cN:"meta",b:/^(>>>|\.\.\.) /},c={cN:"subst",b:/\{/,e:/\}/,k:r,i:/#/},a={cN:"string",c:[e.BE],v:[{b:/(u|b)?r?'''/,e:/'''/,c:[b],r:10},{b:/(u|b)?r?"""/,e:/"""/,c:[b],r:10},{b:/(fr|rf|f)'''/,e:/'''/,c:[b,c]},{b:/(fr|rf|f)"""/,e:/"""/,c:[b,c]},{b:/(u|r|ur)'/,e:/'/,r:10},{b:/(u|r|ur)"/,e:/"/,r:10},{b:/(b|br)'/,e:/'/},{b:/(b|br)"/,e:/"/},{b:/(fr|rf|f)'/,e:/'/,c:[c]},{b:/(fr|rf|f)"/,e:/"/,c:[c]},e.ASM,e.QSM]},s={cN:"number",r:0,v:[{b:e.BNR+"[lLjJ]?"},{b:"\\b(0o[0-7]+)[lLjJ]?"},{b:e.CNR+"[lLjJ]?"}]},i={cN:"params",b:/\(/,e:/\)/,c:["self",b,s,a]};return c.c=[a,s,b],{aliases:["py","gyp"],k:r,i:/(<\/|->|\?)|=>/,c:[b,s,a,e.HCM,{v:[{cN:"function",bK:"def"},{cN:"class",bK:"class"}],e:/:/,i:/[${=;\n,]/,c:[e.UTM,i,{b:/->/,eW:!0,k:"None"}]},{cN:"meta",b:/^[\t ]*@/,e:/$/},{b:/\b(print|exec)\(/}]}});
--------------------------------------------------------------------------------
/src/components/back/account.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
{{msg}}
17 |
确认修改
18 |
19 |
20 |
21 |
81 |
82 |
149 |
--------------------------------------------------------------------------------
/src/components/back/admin.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {{time}}好,{{name}}
7 |
8 |
9 |
10 | 登出
11 |
12 |
13 |
14 |
15 | 文章
16 | 搜索
17 | 草稿
18 | 账户
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
58 |
59 |
152 |
--------------------------------------------------------------------------------
/src/components/back/component/ArticleContent.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 标题
6 | 标签
7 | 日期
8 | 操作
9 |
10 |
11 |
12 |
13 | {{article.title}}
14 | {{article.tags | toTag}}
15 | {{article.date | toDate}}
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | 上一页
25 | 第 {{page}} 页
26 | 下一页
27 |
28 |
29 |
30 |
31 |
32 |
77 |
78 |
122 |
--------------------------------------------------------------------------------
/src/components/back/component/TagInput.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
12 |
13 |
14 |
15 |
16 |
17 |
71 |
72 |
121 |
--------------------------------------------------------------------------------
/src/components/back/drafts.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
所有草稿
4 |
5 |
添加草稿
9 |
10 |
11 |
12 |
48 |
49 |
79 |
--------------------------------------------------------------------------------
/src/components/back/editor.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
13 |
14 |
27 |
发布文章
28 |
存为草稿
29 |
30 |
31 |
32 |
153 |
154 |
276 |
--------------------------------------------------------------------------------
/src/components/back/login.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
{{info}}
14 |
登录
15 |
16 |
17 |
18 |
19 |
53 |
120 |
--------------------------------------------------------------------------------
/src/components/back/posts.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
所有文章
4 |
5 |
添加文章
9 |
10 |
11 |
12 |
45 |
46 |
77 |
--------------------------------------------------------------------------------
/src/components/back/search.vue:
--------------------------------------------------------------------------------
1 |
2 |
20 |
21 |
22 |
65 |
66 |
127 |
--------------------------------------------------------------------------------
/src/components/front/AboutMe.vue:
--------------------------------------------------------------------------------
1 |
2 |
16 |
17 |
18 |
31 |
32 |
111 |
--------------------------------------------------------------------------------
/src/components/front/Articles.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
15 |
16 |
{{article.title}}
17 |
{{article.date | toDate}}
18 |
{{article.tags | toTag}}
19 |
20 |
{{article.content}}
21 |
22 | Continue reading
23 |
24 |
25 |
下拉加载更多
26 |
已经到底了,别扯了
27 |
28 |
29 |
30 |
31 |
32 |
91 |
92 |
179 |
--------------------------------------------------------------------------------
/src/components/front/Home.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 最近更新
6 |
7 |
8 |
9 |
10 |
11 | {{article.date | toDate}}
12 |
13 |
14 |
{{article.title}}
15 |
{{article.content}}
16 |
Read More
17 |
18 |
19 |
20 |
21 |
34 |
35 |
36 |
37 |
101 |
102 |
232 |
--------------------------------------------------------------------------------
/src/components/front/SearchResult.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
{{article.title}}
5 |
{{article.date | toDate}}
6 |
{{article.tags | toTag}}
7 |
8 |
{{article.content}}
9 |
10 | Continue reading
11 |
12 |
13 |
14 |
下拉加载更多
15 |
没啦没啦,别扯了
16 |
17 |
18 |
19 |
77 |
78 |
140 |
--------------------------------------------------------------------------------
/src/components/front/article.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
{{article.title}}
5 |
6 |
7 | {{article.date | toDate}}
8 |
9 |
10 | {{article.tags | toTag}}
11 |
12 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | {{articles[prePage].title}}
22 |
23 | {{articles[nextPage].title}}
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
117 |
118 |
248 |
--------------------------------------------------------------------------------
/src/components/front/component/ArticleComment.vue:
--------------------------------------------------------------------------------
1 |
2 |
45 |
46 |
47 |
189 |
190 |
413 |
--------------------------------------------------------------------------------
/src/components/front/component/ArticleList.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | 文章目录
8 |
16 |
17 |
18 |
19 |
20 |
67 |
68 |
141 |
--------------------------------------------------------------------------------
/src/components/front/component/MyFooter.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
Copyright @ 2017 Powered by Vue. Designed by me | 站长登录
8 |
9 |
10 |
11 |
15 |
16 |
51 |
--------------------------------------------------------------------------------
/src/components/front/component/MyHeader.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
11 |
12 |
13 |
14 |
15 | 首页
16 | 博客
17 | 联系站长
18 | 关于我
19 |
20 |
21 |
22 |
23 |
24 |
25 | {{headline.content}}
26 |
27 |
28 |
29 |
30 |
31 |
53 |
54 |
181 |
--------------------------------------------------------------------------------
/src/components/front/contact.vue:
--------------------------------------------------------------------------------
1 |
2 |
15 |
16 |
17 |
78 |
79 |
138 |
--------------------------------------------------------------------------------
/src/components/front/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
43 |
44 |
77 |
--------------------------------------------------------------------------------
/src/components/share/DialogBox.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
{{dialog.info}}
6 |
7 | 确定
8 | 取消
9 |
10 |
11 |
12 |
13 |
14 |
40 |
41 |
117 |
--------------------------------------------------------------------------------
/src/components/share/FireCanvas.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
191 |
192 |
199 |
--------------------------------------------------------------------------------
/src/components/share/spinner.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
14 |
15 |
60 |
--------------------------------------------------------------------------------
/src/lib/utils.js:
--------------------------------------------------------------------------------
1 | // 让一个高频触发的函数在一定时间内只触发一次
2 | export function _debounce (func, wait) {
3 | let _timestamp, _timer
4 | return function () {
5 | let now = Date.now()
6 | if (_timestamp && ((now - _timestamp) < wait)) {
7 | clearTimeout(_timer)
8 | }
9 | _timestamp = now
10 | _timer = setTimeout(func.bind(this, ...arguments), wait)
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import VueResource from 'vue-resource'
3 | import App from './App'
4 | import router from './router'
5 | import store from './store'
6 | import './assets/css/index.scss'
7 |
8 | Vue.config.productionTip = false
9 |
10 | Vue.use(VueResource)
11 |
12 | Vue.filter('toDate', (date) => { // 2017年5月10日15:35
13 | if (date) {
14 | const d = new Date(date)
15 | const minutes = d.getMinutes() < 10 ? '0' + d.getMinutes() : d.getMinutes()
16 | const hours = d.getHours() < 10 ? '0' + d.getHours() : d.getHours()
17 | return d.getFullYear() + '年' + (d.getMonth() + 1) + '月' +
18 | d.getDate() + '日 ' + hours + ' : ' + minutes
19 | }
20 | })
21 |
22 | Vue.filter('to_date', (date) => { // 2017-5-10 at 15:35
23 | if (date) {
24 | const d = new Date(date)
25 | const minutes = d.getMinutes() < 10 ? '0' + d.getMinutes() : d.getMinutes()
26 | const hours = d.getHours() < 10 ? '0' + d.getHours() : d.getHours()
27 | return d.getFullYear() + '-' + (d.getMonth() + 1) + '-' +
28 | d.getDate() + ' ' + hours + ': ' + minutes
29 | }
30 | })
31 |
32 | Vue.filter('toTag', (arr) => {
33 | if (arr) {
34 | return arr.join(',')
35 | }
36 | })
37 |
38 | Vue.http.interceptors.push((request, next) => {
39 | if (window.localStorage.getItem('token')) {
40 | request.headers.set('authorization', 'Bearer ' + window.localStorage.getItem('token'))
41 | }
42 | next((response) => {
43 | if (response.status === 401) {
44 | store.commit('unset_user')
45 | router.go({name: 'login'})
46 | }
47 | return response
48 | })
49 | })
50 |
51 | /* eslint-disable no-new */
52 | new Vue({
53 | el: '#app',
54 | router,
55 | store,
56 | template: ' ',
57 | components: { App }
58 | })
59 |
--------------------------------------------------------------------------------
/src/router/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Store from '../store'
3 | import Router from 'vue-router'
4 |
5 | const index = resolve => require(['@/components/front/index'], resolve)
6 | const AboutMe = resolve => require(['@/components/front/AboutMe'], resolve)
7 | const Home = resolve => require(['@/components/front/Home'], resolve)
8 | const Articles = resolve => require(['@/components/front/Articles'], resolve)
9 | const contact = resolve => require(['@/components/front/contact'], resolve)
10 | const login = resolve => require(['@/components/back/login'], resolve)
11 | const admin = resolve => require(['@/components/back/admin'], resolve)
12 | const posts = resolve => require(['@/components/back/posts'], resolve)
13 | const editor = resolve => require(['@/components/back/editor'], resolve)
14 | const drafts = resolve => require(['@/components/back/drafts'], resolve)
15 | const search = resolve => require(['@/components/back/search'], resolve)
16 | const article = resolve => require(['@/components/front/article'], resolve)
17 | const account = resolve => require(['@/components/back/account'], resolve)
18 | const SearchResult = resolve => require(['@/components/front/SearchResult'], resolve)
19 |
20 | Vue.use(Router)
21 |
22 | const router = new Router({
23 | mode: 'history',
24 | scrollBehavior (to, from, savedPosition) {
25 | if (to.hash) {
26 | return {
27 | selector: to.hash
28 | }
29 | } else {
30 | return {x: 0, y: 0}
31 | }
32 | },
33 | routes: [
34 | {
35 | path: '/',
36 | redirect: 'home',
37 | component: index,
38 | children: [
39 | {path: 'home', name: 'home', component: Home, meta: {title: '博客首页'}},
40 | {path: 'about', name: 'about', component: AboutMe, meta: {title: '关于我'}},
41 | {path: 'articles', name: 'articles', component: Articles, meta: {title: '学习笔记分享'}},
42 | {path: 'articles/:id', name: 'article', component: article},
43 | {path: 'contact', name: 'contact', component: contact, meta: {title: '联系站长'}},
44 | {path: 'search/:text', name: 'SearchResult', component: SearchResult, meta: {title: '搜索结果'}}
45 | ]
46 | },
47 | {
48 | path: '/login',
49 | name: 'login',
50 | component: login,
51 | meta: {title: '登录页面'}
52 | },
53 | {
54 | path: '/admin',
55 | redirect: '/admin/posts',
56 | component: admin,
57 | children: [
58 | {path: 'posts', name: 'posts', component: posts, meta: {requireAuth: true, title: '博客文章'}},
59 | {path: 'editor', name: 'editor', component: editor, meta: {requireAuth: true, title: '编辑文章'}},
60 | {path: 'drafts', name: 'drafts', component: drafts, meta: {requireAuth: true, title: '博客草稿'}},
61 | {path: 'search', name: 'search', component: search, meta: {requireAuth: true, title: '搜索结果'}},
62 | {path: 'account', name: 'account', component: account, meta: {requireAuth: true, title: '修改账户'}}
63 | ]
64 | }
65 | ]
66 | })
67 |
68 | router.beforeEach((to, from, next) => {
69 | document.title = to.meta.title
70 | if (Store.state.user.token && to.name === 'login') {
71 | next({name: 'posts'})
72 | } else if (!Store.state.user.token && to.meta.requireAuth) {
73 | next({name: 'login'})
74 | } else {
75 | next()
76 | }
77 | })
78 |
79 | export default router
80 |
--------------------------------------------------------------------------------
/src/store/actions.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import router from '../router'
3 |
4 | const beginLoading = (commit, add) => {
5 | add ? commit('loadMore_toggle', true) : commit('isLoading_toggle', true)
6 | return Date.now()
7 | }
8 |
9 | const endLoading = (commit, startTime, toggle) => {
10 | const leftTime = 500 - (Date.now() - startTime)
11 | leftTime > 0 ? setTimeout(commit(toggle, false), leftTime) : commit(toggle, false)
12 | }
13 |
14 | export default {
15 | login ({commit}, payload) {
16 | return Vue.http.post('/api/login', payload).catch((err) => { console.log(err) })
17 | },
18 | resetUser ({commit}, payload) {
19 | return Vue.http.post('/api/user', payload)
20 | .then(() => {
21 | commit('unset_user')
22 | router.go({name: 'login'})
23 | }).catch((err) => { console.log(err) })
24 | },
25 | // article的http请求
26 | saveArticle ({state, commit}, aid) {
27 | commit('isSaving_toggle', false)
28 | if (aid) {
29 | return Vue.http.patch('/api/article/' + aid, state.article)
30 | .then(() => {
31 | commit('isSaving_toggle', true)
32 | router.push({name: 'posts'})
33 | }, () => { alert('保存失败') }).catch((err) => { console.log(err) })
34 | } else {
35 | return Vue.http.post('/api/article', state.article)
36 | .then(() => {
37 | commit('isSaving_toggle', true)
38 | router.push({name: 'posts'})
39 | }, () => { alert('保存失败') }).catch((err) => { console.log(err) })
40 | }
41 | },
42 | getAllArticles ({commit}, payload) {
43 | commit('moreArticle_toggle', true)
44 | const startTime = beginLoading(commit, payload.add)
45 | if (payload.value) {
46 | commit('isLoading_toggle', false)
47 | }
48 | return Vue.http.get('/api/articles', {params: {payload}})
49 | .then(response => response.json())
50 | .then(articles => {
51 | if (articles.length === 0) {
52 | commit('moreArticle_toggle', false)
53 | commit('noMore_toggle', true)
54 | } else {
55 | commit('noMore_toggle', false)
56 | }
57 | if (payload.add) {
58 | commit('add_articles', articles)
59 | endLoading(commit, startTime, 'loadMore_toggle')
60 | } else {
61 | commit('set_all_articles', articles)
62 | endLoading(commit, startTime, 'isLoading_toggle')
63 | }
64 | }).catch((err) => { console.log(err) })
65 | },
66 | getArticle ({commit, state}, aid) {
67 | const startTime = beginLoading(commit, false)
68 | if (router.currentRoute.hash) {
69 | commit('isLoading_toggle', false)
70 | }
71 | document.title = '加载中...'
72 | return Vue.http.get('/api/article/' + aid)
73 | .then(response => {
74 | commit('set_article', response.data)
75 | commit('set_headline', {content: state.article.title, animation: 'animated rotateIn'})
76 | document.title = state.article.title
77 | endLoading(commit, startTime, 'isLoading_toggle')
78 | }).catch((err) => { console.log(err) })
79 | },
80 | delArticle ({dispatch}, payload) {
81 | return Vue.http.delete('/api/article/' + payload.aid)
82 | .then(() => {
83 | if (payload.route.name === 'posts') dispatch('getAllArticles', {page: payload.page, limit: 8})
84 | if (payload.route.name === 'drafts') dispatch('getAllDrafts', {page: payload.page, limit: 8})
85 | if (payload.route.name === 'search') router.push({name: 'posts'})
86 | }).catch((err) => { console.log(err) })
87 | },
88 | // draft的http请求
89 | saveDraft ({state, commit}, aid) {
90 | // 可能要改saveArticle
91 | if (aid) {
92 | return Vue.http.patch('/api/draft/' + aid, state.article)
93 | .then(() => {
94 | commit('isSaving_toggle', true)
95 | router.push({name: 'drafts'})
96 | }, () => { alert('保存失败') }).catch((err) => { console.log(err) })
97 | } else {
98 | return Vue.http.post('/api/draft', state.article)
99 | .then(() => {
100 | commit('isSaving_toggle', true)
101 | router.push({name: 'drafts'})
102 | }, () => { alert('保存失败') }).catch((err) => { console.log(err) })
103 | }
104 | },
105 | getAllDrafts ({commit}, payload) {
106 | return Vue.http.get('/api/drafts', {params: {payload}})
107 | .then(response => response.json())
108 | .then(articles => {
109 | commit('set_all_articles', articles)
110 | }).catch((err) => { console.log(err) })
111 | },
112 | // search
113 | searchArticles ({commit}, payload) {
114 | document.title = '搜索中...'
115 | commit('moreArticle_toggle', true)
116 | const startTime = beginLoading(commit, payload.add)
117 | return Vue.http.get('/api/someArticles', {params: {payload}})
118 | .then(response => response.json())
119 | .then(articles => {
120 | if (articles.length === 0) {
121 | commit('moreArticle_toggle', false)
122 | commit('noMore_toggle', true)
123 | } else {
124 | commit('noMore_toggle', false)
125 | }
126 | if (payload.add) {
127 | commit('add_articles', articles)
128 | endLoading(commit, startTime, 'loadMore_toggle')
129 | } else {
130 | commit('set_all_articles', articles)
131 | endLoading(commit, startTime, 'isLoading_toggle')
132 | }
133 | document.title = '搜索成功'
134 | }).catch((err) => { console.log(err) })
135 | },
136 | // tags
137 | getAllTags ({commit}) {
138 | return Vue.http.get('/api/tags')
139 | .then(response => {
140 | commit('set_tags', response.data)
141 | }).catch((err) => { console.log(err) })
142 | },
143 | // email
144 | sendMail ({commit}, payload) {
145 | return Vue.http.post('/api/mail', payload).catch((err) => { console.log(err) })
146 | },
147 | // comment
148 | summitComment ({commit}, payload) {
149 | return Vue.http.post('/api/comment', payload)
150 | },
151 | getAllComments ({commit}, payload) {
152 | return Vue.http.get('/api/comments', {params: {payload}})
153 | .then(response => response.json())
154 | .then(comments => {
155 | commit('set_comments', comments)
156 | }).catch((err) => { console.log(err) })
157 | },
158 | updateLike ({commit}, payload) {
159 | return Vue.http.patch('/api/comments/' + payload.id, {option: payload.option})
160 | .catch((err) => { console.log(err) })
161 | }
162 | }
163 |
--------------------------------------------------------------------------------
/src/store/getters.js:
--------------------------------------------------------------------------------
1 | import marked from 'marked'
2 |
3 | const unescapeHTML = (a) => {
4 | a = '' + a
5 | return a.replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&').replace(/"/g, '"').replace(/'/g, '"')
6 | }
7 |
8 | const renderer = new marked.Renderer()
9 | renderer.heading = function (text, level) {
10 | return '' + text + ' '
12 | }
13 |
14 | export default {
15 | reducedArticles: (state) => {
16 | const articles = state.articles.map(article => {
17 | let newArticle = {}
18 | for (let i in article) { newArticle[i] = article[i] }
19 | newArticle.content = marked(article.content || '').replace(/<[^>]*>/g, '').slice(0, 200) + '......'
20 | return newArticle
21 | })
22 | return articles
23 | },
24 | allTags: (state) => {
25 | state.tags.unshift('全部')
26 | return state.tags
27 | },
28 | articleList: (state) => {
29 | const strHtml = unescapeHTML(marked(state.article.content || '', { renderer: renderer }))
30 | if (strHtml) {
31 | const Re = //g
32 | let arr = Re.exec(strHtml)
33 | let list = []
34 | let index = 0
35 | const sizeTop = arr[1]
36 | list.push({
37 | level: 1,
38 | size: sizeTop,
39 | content: arr[2]
40 | })
41 | while ((arr = Re.exec(strHtml)) !== null) {
42 | var level
43 | if (arr[1] > list[index].size) {
44 | level = list[index].level + 1
45 | } else if (arr[1] === list[index].size) {
46 | level = list[index].level
47 | } else if (arr[1] < list[index].size) {
48 | let i = index
49 | while (i--) {
50 | if (arr[1] === list[i].size) {
51 | level = list[i].level
52 | break
53 | }
54 | }
55 | }
56 | list.push({
57 | level: level,
58 | size: arr[1],
59 | content: arr[2]
60 | })
61 | index = index + 1
62 | }
63 | return list
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/store/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Vuex from 'vuex'
3 | import actions from './actions.js'
4 | import getters from './getters.js'
5 | import mutations from './mutations.js'
6 |
7 | Vue.use(Vuex)
8 |
9 | const store = new Vuex.Store({
10 | state: {
11 | user: {},
12 | headline: {},
13 | isLoading: false,
14 | moreArticle: true,
15 | loadMore: false,
16 | isSaving: false,
17 | noMore: false,
18 | dialog: {
19 | show: false,
20 | hasTwoBtn: false,
21 | info: 'hey',
22 | resolveFn: () => {},
23 | rejectFn: () => {}
24 | },
25 | tags: [],
26 | curTag: '',
27 | article: {},
28 | articles: [],
29 | draft: {},
30 | drafts: {},
31 | comments: []
32 | },
33 | getters,
34 | actions,
35 | mutations
36 | })
37 | export default store
38 |
--------------------------------------------------------------------------------
/src/store/mutations.js:
--------------------------------------------------------------------------------
1 | export default {
2 | // login
3 | set_user: (state, user) => {
4 | localStorage.setItem('token', user.token)
5 | localStorage.setItem('userName', user.name)
6 | state.user = user
7 | },
8 | unset_user: (state) => {
9 | localStorage.removeItem('token')
10 | localStorage.removeItem('userName')
11 | state.user = {}
12 | },
13 | // 设置页面标题
14 | set_headline: (state, headline) => {
15 | state.headline = headline
16 | },
17 | isSaving_toggle: (state, isSaving) => {
18 | state.isSaving = isSaving
19 | },
20 | isLoading_toggle: (state, isLoading) => {
21 | state.isLoading = isLoading
22 | },
23 | loadMore_toggle: (state, loadMore) => {
24 | state.loadMore = loadMore
25 | },
26 | moreArticle_toggle: (state, flag) => {
27 | state.moreArticle = flag
28 | },
29 | noMore_toggle: (state, flag) => {
30 | state.noMore = flag
31 | },
32 | // tags
33 | set_tags: (state, tags) => {
34 | state.tags = tags
35 | },
36 | set_curtag: (state, tag) => {
37 | state.curTag = tag
38 | },
39 | // article
40 | update_post_title: (state, title) => {
41 | state.article.title = title
42 | },
43 | update_post_content: (state, content) => {
44 | state.article.content = content
45 | },
46 | set_article: (state, article) => {
47 | state.article = article
48 | },
49 | set_all_articles: (state, articles) => {
50 | state.articles = articles
51 | },
52 | add_articles: (state, articles) => {
53 | state.articles = state.articles.concat(articles)
54 | },
55 | // comment
56 | set_comments: (state, comments) => {
57 | state.comments = comments
58 | },
59 | // 对话框
60 | set_dialog: (state, payload) => {
61 | state.dialog = payload
62 | state.dialog.resolveFn = () => {}
63 | state.dialog.rejectFn = () => {}
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/static/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rudyxu1102/vue-blog/fba4009577a49192078313c15f49712070374425/static/.gitkeep
--------------------------------------------------------------------------------
/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rudyxu1102/vue-blog/fba4009577a49192078313c15f49712070374425/static/favicon.ico
--------------------------------------------------------------------------------
/static/me.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rudyxu1102/vue-blog/fba4009577a49192078313c15f49712070374425/static/me.jpg
--------------------------------------------------------------------------------
/static/reviewer.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rudyxu1102/vue-blog/fba4009577a49192078313c15f49712070374425/static/reviewer.jpg
--------------------------------------------------------------------------------
/static/sunset.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rudyxu1102/vue-blog/fba4009577a49192078313c15f49712070374425/static/sunset.jpg
--------------------------------------------------------------------------------
#{{index + 1}} {{comment.name}}
26 |{{comment.content}}
27 |{{comment.date | to_date}}
29 | 30 | 31 | 回复 32 | 33 | 34 |35 | 36 | {{comment.like}} 37 |
38 |