├── .babelrc
├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── .husky
├── commit-msg
└── pre-commit
├── .postcssrc.js
├── .prettierrc
├── README.md
├── build
├── checkPort.js
├── setup-dev-server.js
├── vue-loader.config.js
├── webpack.base.config.js
├── webpack.client.config.js
└── webpack.server.config.js
├── commitlint.config.js
├── config
├── env.dev.js
└── env.prod.js
├── copy.js
├── ecosystem.config.js
├── package.json
├── preinstall.js
├── screenShot
├── front-archive-1.jpg
├── front-archive-2.jpg
├── front-article-1.jpg
├── front-article-2.jpg
├── front-category.jpg
├── front-home.jpg
├── front-leavewords-1.jpg
├── front-leavewords-2.jpg
├── front-movies-1.jpg
├── front-movies-2.jpg
├── front-search.jpg
└── front-tag.png
├── server.js
├── server
├── api
│ ├── articles.js
│ ├── category.js
│ ├── comments.js
│ ├── count.js
│ ├── index.js
│ ├── login.js
│ ├── mongodump.js
│ ├── movies.js
│ ├── msgBoard.js
│ ├── news.js
│ ├── qiniu.js
│ ├── resetpwd.js
│ ├── tags.js
│ ├── viewer.js
│ └── visitor.js
├── db
│ ├── index.js
│ └── schema.js
├── http
│ ├── index.js
│ └── server-api.js
├── middleware
│ ├── confirmToken.js
│ ├── confirmUnpublish.js
│ └── ratelimit.js
├── plugin
│ └── lastMod.js
└── utils
│ ├── getBrowser.js
│ ├── getIp.js
│ ├── getOS.js
│ ├── highLimitApis.js
│ ├── reviseTime.js
│ ├── schedule.js
│ └── verify.js
├── src
├── App.vue
├── api
│ ├── article.js
│ ├── category.js
│ ├── comments.js
│ ├── entertainment.js
│ ├── index.js
│ ├── messageBoard.js
│ ├── qiniu.js
│ ├── tags.js
│ └── visitor.js
├── app.js
├── assets
│ ├── css
│ │ ├── emoji-sprite.css
│ │ ├── prism.css
│ │ └── quill.snow.css
│ ├── font
│ │ └── sf-arch
│ │ │ ├── SF Arch Rival Extended Bold-embed.css
│ │ │ ├── SF Arch Rival Extended Bold.css
│ │ │ ├── SF Arch Rival Extended Bold.eot
│ │ │ ├── SF Arch Rival Extended Bold.svg
│ │ │ ├── SF Arch Rival Extended Bold.ttf
│ │ │ └── SF Arch Rival Extended Bold.woff
│ ├── img
│ │ ├── github.png
│ │ ├── icp.png
│ │ ├── loading.gif
│ │ ├── qq.png
│ │ └── share.png
│ └── js
│ │ ├── emoji-data.js
│ │ └── prism.js
├── components
│ ├── base
│ │ └── miss.vue
│ ├── dot
│ │ └── index.vue
│ ├── emoji
│ │ └── index.vue
│ ├── empty
│ │ └── index.vue
│ ├── note
│ │ └── index.vue
│ ├── rating
│ │ └── index.vue
│ └── splitLine
│ │ └── index.vue
├── entry-client.js
├── entry-server.js
├── http
│ └── index.js
├── index.template.html
├── mixins
│ └── mergeAsyncData.js
├── router
│ └── index.js
├── store
│ ├── actions.js
│ ├── index.js
│ └── mutations.js
├── style
│ ├── function.scss
│ ├── global.scss
│ ├── index.scss
│ ├── mixins.scss
│ ├── reset.scss
│ └── theme
│ │ ├── dark
│ │ ├── color.scss
│ │ ├── font.scss
│ │ └── index.scss
│ │ ├── light
│ │ ├── color.scss
│ │ ├── font.scss
│ │ └── index.scss
│ │ └── map.scss
├── utils
│ ├── cls.js
│ ├── errorCode.js
│ ├── generateTree.js
│ ├── getBrowserInfo.js
│ ├── getElementTop.js
│ ├── getRandomCharacter.js
│ ├── getRandomColor.js
│ ├── getScrollTop.js
│ ├── getUrlParams.js
│ ├── lazyLoad.js
│ ├── loadEle.js
│ ├── requestAnimation.js
│ ├── scrollTo.js
│ ├── siblings.js
│ └── storage.js
└── views
│ ├── archives
│ └── index.vue
│ ├── article-filter
│ └── index.vue
│ ├── article
│ ├── articleDetail.vue
│ └── components
│ │ ├── copyright.vue
│ │ ├── prevnext.vue
│ │ └── share.vue
│ ├── category
│ └── index.vue
│ ├── components
│ ├── article-iterator.vue
│ ├── comments-item.vue
│ ├── comments.vue
│ ├── search.vue
│ ├── site-introduction.vue
│ ├── submit.vue
│ └── tags-iterator.vue
│ ├── home
│ └── index.vue
│ ├── layout
│ ├── components
│ │ ├── header
│ │ │ └── index.vue
│ │ └── navbar
│ │ │ ├── horizontal-navbar.vue
│ │ │ ├── index.vue
│ │ │ └── vertical-navbar.vue
│ └── index.vue
│ ├── messageBoard
│ └── index.vue
│ ├── movies
│ └── index.vue
│ ├── pannel
│ ├── components
│ │ ├── pannel-archives.vue
│ │ ├── pannel-articles.vue
│ │ ├── pannel-catalog.vue
│ │ ├── pannel-category.vue
│ │ ├── pannel-comments.vue
│ │ ├── pannel-introduction.vue
│ │ ├── pannel-tags.vue
│ │ └── tree-folder.vue
│ ├── index.vue
│ └── style
│ │ └── mixins.scss
│ └── tags
│ └── index.vue
├── static
├── .gitkeep
├── 404.html
├── gc_back.html
├── img
│ ├── avatar
│ │ └── avatar.jpeg
│ ├── cover
│ │ ├── archive.jpeg
│ │ ├── articles.jpeg
│ │ ├── category.jpg
│ │ ├── default.jpg
│ │ ├── home.jpg
│ │ ├── movie.jpeg
│ │ ├── movie.jpg
│ │ ├── msgboard.jpeg
│ │ └── tags.jpg
│ └── favicon.ico
├── js
│ ├── qrcode.min.js
│ └── ribbon.js
└── qc_back.html
└── template
└── secret.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [["@babel/preset-env"]],
3 | "plugins": [
4 | "transform-vue-jsx",
5 | "@babel/transform-runtime",
6 | [
7 | "component",
8 | {
9 | "libraryName": "element-ui",
10 | "styleLibraryName": "theme-chalk"
11 | }
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 |
2 | config/*
3 | src/assets
4 | src/utils/lazyLoad.js
5 | node_modules
6 | dist
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | parser: 'vue-eslint-parser',
4 | parserOptions: {
5 | parser: '@babel/eslint-parser',
6 | sourceType: 'module'
7 | },
8 | env: {
9 | browser: true,
10 | node: true,
11 | es6: true
12 | },
13 | extends: ['plugin:vue/essential', 'eslint:recommended'],
14 | // required to lint *.vue files
15 | plugins: ['vue', 'html'],
16 | // check if imports actually resolve
17 |
18 | globals: {
19 | BMap: true,
20 | AMap: true,
21 | AMapUI: true,
22 | processEnv: true,
23 | _: true,
24 | Prism: true,
25 | QRCode: true,
26 | QC: true
27 | },
28 | // add your custom rules here
29 | //it is base on https://github.com/vuejs/eslint-config-vue
30 | rules: {
31 | 'vue/multi-word-component-names': 'off'
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /dist/*
4 | server/db/secret.js
5 | server/db/copy/*
6 | server/files/movies
7 | package-lock.json
8 |
9 | # Log files
10 | npm-debug.log*
11 | yarn-debug.log*
12 | yarn-error.log*
13 | pnpm-debug.log*
14 |
15 | # Editor directories and files
16 | .idea
17 | .vscode
18 | .history
19 | *.suo
20 | *.ntvs*
21 | *.njsproj
22 | *.sln
23 | *.sw?
24 |
--------------------------------------------------------------------------------
/.husky/commit-msg:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | npx --no-install commitlint --edit "$1"
5 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | npx lint-staged --allow-empty "$1"
5 |
--------------------------------------------------------------------------------
/.postcssrc.js:
--------------------------------------------------------------------------------
1 | // https://github.com/michael-ciniawsky/postcss-load-config
2 |
3 | module.exports = {
4 | "plugins": {
5 | "postcss-import": {},
6 | "postcss-url": {},
7 | // to edit target browsers: use "browserslist" field in package.json
8 | "autoprefixer": {}
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "semi": false,
4 | "trailingComma":"none",
5 | "htmlWhitespaceSensitivity": "ignore",
6 | "printWidth": 120,
7 | "proseWrap ": "preserver"
8 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # vue-ssr-blog
3 |
4 |
5 |
6 | 
7 | 
8 | 
9 | 
10 | 
11 | 
12 |
13 |
14 |
15 | 
16 |
17 | ## 介绍
18 | 这是一个使用 vue2.x 开发的,记录学习与生活的 [个人博客](https://mapblog.cn "Marcos's Blog")。
19 | 整站选用 ssr 技术进行服务端渲染,具备良好的 SEO 和首屏性能。
20 | 同时,我为它配备了一个使用 `vue3.x`、`typescript`、`vite` 开发的管理系统,详情请移步 [这里](https://github.com/justJokee/vue-blog-admin) 查看。
21 | ## 技术栈
22 | - vue2.x ssr
23 | - vue-router
24 | - vuex
25 | - nodejs
26 | - mongodb
27 | - mongoose
28 | - express
29 | - pm2
30 | - lazyload
31 | ## 主要功能
32 |
33 | - [x] 首页
34 | - [x] 最新文章
35 | - [x] 文章搜索(高亮关键词)
36 | - [x] 导航
37 | - [x] 文章归档
38 | - [x] 文章标签
39 | - [x] 文章分类
40 | - [x] 娱乐
41 | - [x] 电影([爬虫](https://github.com/justJokee/douban-spider)获取豆瓣的观影记录)
42 | - [x] 看过的影视
43 | - [x] 想看的影视
44 | - [x] 在看的影视
45 | - [x] 留言板
46 | - [x] 留言
47 | - [x] 相互回复
48 | - [x] 点赞、取消赞(ip统计)
49 | - [x] 支持 QQ、Github 第三方登录,以支持评论及留言
50 | - [x] 文章筛选
51 | - [x] 按标签
52 | - [x] 按分类
53 | - [x] 按归档时间
54 | - [x] 文章相关
55 | - [x] 评论
56 | - [x] 回复评论
57 | - [x] 文章/评论的赞、取消赞(ip统计)
58 | - [x] pv、评论、点赞统计
59 | - [x] 文档目录自动生成
60 | - [x] 分享
61 | - [x] 快捷看板
62 | - [x] 本站简介
63 | - [x] 最新文章
64 | - [x] 最新评论
65 | - [x] 标签统计
66 | - [x] 文章分类
67 | - [x] 文章归档
68 | - [x] 响应式
69 | - [x] 图片懒加载
70 | - [ ] 友链(留坑,待开发)
71 | - [ ] 首页名言管理(留坑,待开发)
72 | - [ ] 换肤(留坑,待开发)
73 |
74 | ## fork后必读
75 | 在fork项目后,请务必仔细阅读以下说明:
76 | ### 配置文件
77 | 在 `preinstall` hook中,系统将为你自动生成一份默认的配置文件至 `/server/db/secret.js`,它涵盖了数据库、管理系统、三方登录、七牛云等重要配置信息,以确保本项目正常启动,此文件进行了详细注释,因此你可以在项目启动前根据自己的实际情况进行修改。
78 | ### 依赖
79 | 1. 安装mongodb,推荐5.x版本
80 | 2. mongodb中创建名为`/server/db/secret.js[db.db]`(默认配置为 “blog”)的数据库
81 | 3. mongodb中创建用户为`/server/db/secret.js[db.user/pwd]`(默认配置为 “admin 12345”)
82 | 4. 运行时node版本推荐 14.x,实测16.x安装依赖会失败,初步观察为node-sass版本兼容问题,因时间原因目前不打算排查升级,建议直接安装nvm管理工具对node版本进行灵活切换。
83 |
84 | ## Build Setup
85 | 关于部署至生产环境的详细教程,请查看[这篇文章](https://mapblog.cn/app/article/7)
86 |
87 | ```bash
88 | # install dependencies
89 |
90 | npm install
91 |
92 | # serve with hot reload at localhost:6180
93 |
94 | npm run dev
95 |
96 | # build for production with minification
97 |
98 | # 注意,此命令输出生产包至 dist 目录,部署时将 dist 下 的所有目录上传至你的静态服务目录,例如 /usr/local/nginx/htmls
99 |
100 | npm run build
101 |
102 | # serve for production
103 | # 注意,如果你想在本地试运行生产包,请运行以下命令:
104 |
105 | cd dist
106 | npm run start:local
107 |
108 | # 在生产服务器的静态服务目录中
109 |
110 | npm i
111 |
112 | # 直接使用node启动
113 |
114 | npm run start
115 |
116 | # 使用pm2启动
117 |
118 | npm i pm2 -g
119 | npm run pm2:prod
120 |
121 | # 查看部署相关信息
122 |
123 | pm2 show mapblog
124 |
125 | ```
126 | ## 预览
127 |
128 | ### 首页
129 | 
130 | ### 搜索
131 | 
132 | ### 文章详情
133 | 
134 |
135 | 
136 | ### 留言板
137 | 
138 |
139 | 
140 | ### 电影
141 | 
142 |
143 | 
144 | ### 归档
145 | 
146 |
147 | 
148 | ### 标签
149 | 
150 | ### 分类
151 | 
152 |
153 | ## 其它
154 |
155 | 1. 文章、评论、留言的点赞按照客户端ip进行统计
156 | 2. 评论、留言具备一定的频率限制
157 | 3. 爬虫固定每天凌晨一点开始同步豆瓣上的相关数据
158 | 4. 爬虫获取的数据以json格式存储至 /server/files/movies
159 | 5. Github登录有时会失败,😌 ~ 墙你太高
160 | 6. 搁置开发:友链功能
161 | 7. 搁置开发:首页名言功能的后端管控
162 | 8. 搁置开发:主题切换
163 |
164 | ## 特别致谢
165 |
166 | 本站前台展示端的UI风格参考自 [hexo](https://hexo.io/zh-cn/) 博客 的 [butterfly](https://github.com/jerryc127/hexo-theme-butterfly) 主题,特此致谢。
167 |
168 | 最后的最后,如果你喜欢这个项目,不妨star鼓励一下~
--------------------------------------------------------------------------------
/build/checkPort.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @desc 检查端口占用并返回一个可用端口
3 | */
4 | const detect = require('detect-port')
5 |
6 | module.exports = function checkPort(port = 6180) {
7 | return new Promise((resolve) => {
8 | detect(port, (err, _port) => {
9 | if (err) {
10 | console.log(err)
11 | }
12 | if (port == _port) {
13 | resolve(port)
14 | } else {
15 | resolve(_port)
16 | console.log(`port: ${port} was occupied, try port: ${_port}`)
17 | }
18 | })
19 | })
20 | }
21 |
--------------------------------------------------------------------------------
/build/setup-dev-server.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const webpack = require('webpack')
3 | const MFS = require('memory-fs')
4 | const clientConfig = require('./webpack.client.config')
5 | const serverConfig = require('./webpack.server.config')
6 | const checkPort = require('./checkPort')
7 |
8 | process.env.NODE_ENV === 'development'
9 | const readFile = (fs, file) => {
10 | try {
11 | return fs.readFileSync(path.join(clientConfig.output.path, file), 'utf-8')
12 | } catch (e) {
13 | //
14 | }
15 | }
16 |
17 | module.exports = async function setupDevServer(app, cb) {
18 | let bundle, clientManifest
19 | let resolve
20 |
21 | const readyPromise = new Promise(_resolve => {
22 | resolve = _resolve
23 | })
24 | const ready = (...args) => {
25 | resolve()
26 | cb(...args)
27 | }
28 | const port = await checkPort()
29 | // 注入环境变量
30 | const injectEnv = require(`../config/env.dev.js`)
31 | injectEnv.BASE_URL = `"http://localhost:${port}"`
32 | clientConfig.plugins.push(
33 | // strip dev-only code in Vue source
34 | new webpack.DefinePlugin({
35 | 'process.env': { ...injectEnv, VUE_ENV: '"client"' }
36 | })
37 | )
38 | serverConfig.plugins.push(
39 | // strip dev-only code in Vue source
40 | new webpack.DefinePlugin({
41 | 'process.env': { ...injectEnv, VUE_ENV: '"server"' }
42 | })
43 | )
44 | process.env.__SAFE_PORT__ = port
45 |
46 | /****** client ******/
47 |
48 | // modify client config to work with hot middleware
49 | // see https://github.com/webpack-contrib/webpack-hot-middleware#readme
50 | clientConfig.entry.app = ['webpack-hot-middleware/client', clientConfig.entry.app]
51 | clientConfig.output.filename = '[name].js'
52 | clientConfig.plugins.push(new webpack.HotModuleReplacementPlugin(), new webpack.NoEmitOnErrorsPlugin())
53 | // dev middleware
54 | const clientCompiler = webpack(clientConfig)
55 | const devMiddleware = require('webpack-dev-middleware')(clientCompiler, {
56 | publicPath: clientConfig.output.publicPath,
57 | noInfo: true,
58 | clientLogLevel: 'warning'
59 | })
60 |
61 | app.use(devMiddleware)
62 |
63 | clientCompiler.plugin('done', stats => {
64 | stats = stats.toJson()
65 | stats.errors.forEach(err => console.error(err))
66 | stats.warnings.forEach(err => console.warn(err))
67 | if (stats.errors.length) return
68 |
69 | clientManifest = JSON.parse(readFile(devMiddleware.fileSystem, 'vue-ssr-client-manifest.json'))
70 | if (bundle) {
71 | ready(bundle, {
72 | clientManifest
73 | })
74 | }
75 | })
76 | // hot middleware
77 | app.use(require('webpack-hot-middleware')(clientCompiler, { heartbeat: 5000 }))
78 |
79 | /****** server ******/
80 |
81 | // watch and update server renderer
82 | const serverCompiler = webpack(serverConfig)
83 | // 文件写入内存
84 | const mfs = new MFS()
85 | serverCompiler.outputFileSystem = mfs
86 | serverCompiler.watch({}, (err, stats) => {
87 | if (err) throw err
88 | stats = stats.toJson()
89 | if (stats.errors.length) return
90 |
91 | // read bundle generated by vue-ssr-webpack-plugin
92 | bundle = JSON.parse(readFile(mfs, 'vue-ssr-server-bundle.json'))
93 |
94 | if (clientManifest) {
95 | ready(bundle, {
96 | clientManifest
97 | })
98 | }
99 | })
100 |
101 | return readyPromise
102 | }
103 |
--------------------------------------------------------------------------------
/build/vue-loader.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extractCSS: process.env.NODE_ENV === 'production',
3 | preserveWhitespace: false,
4 | postcss: [
5 | require('autoprefixer')({
6 | browsers: ['last 3 versions']
7 | })
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/build/webpack.base.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable indent */
2 | const path = require('path')
3 | const webpack = require('webpack')
4 | const vueConfig = require('./vue-loader.config')
5 | const ExtractTextPlugin = require('extract-text-webpack-plugin')
6 | const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
7 | // const CopyWebpackPlugin = require('copy-webpack-plugin')
8 | const isProd = process.env.NODE_ENV === 'production'
9 |
10 | module.exports = {
11 | devtool: isProd ? false : '#cheap-module-source-map',
12 | output: {
13 | path: path.resolve(__dirname, '../dist/front/'),
14 | publicPath: '/',
15 | filename: 'js/[name].[chunkhash].js',
16 | chunkFilename: 'js/[name].[chunkhash].js'
17 | },
18 | resolve: {
19 | extensions: ['.js', '.vue', '.json'],
20 | alias: {
21 | // 'echarts': 'echarts/dist/echarts.common.min.js',
22 | // 'vue': 'vue/dist/vue.runtime.min.js',
23 | // 'vue': 'vue/dist/vue.js',
24 | '@': path.resolve('src'),
25 | R: path.resolve('src/components')
26 | }
27 | },
28 | module: {
29 | noParse: /es6-promise\.js$/, // avoid webpack shimming process
30 | rules: [
31 | {
32 | test: /\.vue$/,
33 | loader: 'vue-loader',
34 | options: vueConfig
35 | },
36 | {
37 | test: /\.js$/,
38 | loader: 'babel-loader',
39 | exclude: /node_modules/
40 | },
41 | {
42 | test: /\.(woff|eot|ttf)\??.*$/,
43 | loader: 'url-loader',
44 | options: {
45 | limit: 10000,
46 | name: 'css/fonts/[name].[hash:8].[ext]'
47 | }
48 | },
49 | {
50 | test: /\.(gif|jpg|png|svg|webp)\??.*$/,
51 | loader: 'url-loader',
52 | options: {
53 | limit: 10000,
54 | name: 'img/[name].[hash:8].[ext]'
55 | }
56 | },
57 | {
58 | test: /\.(less|css|scss)$/,
59 | use: isProd
60 | ? ExtractTextPlugin.extract({
61 | use: ['css-loader?minimize', 'less-loader', 'sass-loader'],
62 | fallback: 'vue-style-loader'
63 | })
64 | : ['vue-style-loader', 'css-loader', 'less-loader', 'sass-loader']
65 | }
66 | ]
67 | },
68 | performance: {
69 | maxEntrypointSize: 300000,
70 | hints: isProd ? 'warning' : false
71 | },
72 | plugins: isProd
73 | ? [
74 | new webpack.optimize.UglifyJsPlugin({
75 | compress: {
76 | warnings: false,
77 | drop_debugger: true,
78 | drop_console: true
79 | },
80 | sourceMap: false // true
81 | }),
82 | new ExtractTextPlugin({
83 | filename: 'css/common.[chunkhash].css',
84 | allChunks: true //所有组件的css都打包到一个css文件中
85 | }),
86 | new ExtractTextPlugin({
87 | filename: 'css/common.[chunkhash].less'
88 | })
89 | ]
90 | : [new FriendlyErrorsPlugin()]
91 | }
92 |
--------------------------------------------------------------------------------
/build/webpack.client.config.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack')
2 | const merge = require('webpack-merge')
3 | const base = require('./webpack.base.config')
4 | const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')
5 | const env = process.env.NODE_ENV === 'developemnt' ? 'dev' : 'prod'
6 |
7 | const config = merge(base, {
8 | entry: {
9 | app: './src/entry-client.js',
10 | vendors: ['axios', '@/assets/js/prism.js'],
11 | vues: ['vue', 'vuex', 'vue-router']
12 | },
13 | plugins: [
14 | // extract vendor chunks for better caching
15 | // new webpack.optimize.CommonsChunkPlugin({
16 | // name: ['vendors', 'echarts'],
17 | // minChunks: function(module) {
18 | // // a module is extracted into the vendor chunk if...
19 | // return (
20 | // // it's inside node_modules
21 | // /node_modules/.test(module.context) &&
22 | // // and not a CSS file (due to extract-text-webpack-plugin limitation)
23 | // !/\.css$/.test(module.request)
24 | // )
25 | // }
26 | // }),
27 | new webpack.optimize.CommonsChunkPlugin({
28 | names: ['vendors', 'vues'],
29 | minChunks: 3
30 | }),
31 | // 重要信息:这将 webpack 运行时分离到一个引导 chunk 中,
32 | // 以便可以在之后正确注入异步 chunk。
33 | // 这也为你的 应用程序/vendor 代码提供了更好的缓存。
34 | new webpack.optimize.CommonsChunkPlugin({
35 | name: 'manifest',
36 | // minChunks: Infinity
37 | chunks: ['vendors', 'vues']
38 | }),
39 | // 此插件在输出目录中
40 | // 生成 `vue-ssr-client-manifest.json`。
41 | new VueSSRClientPlugin()
42 | ]
43 | })
44 |
45 | if (process.env.NODE_ENV === 'production') {
46 | const injectEnv = require(`../config/env.prod.js`)
47 | injectEnv.VUE_ENV = '"client"'
48 | config.plugins.push(
49 | // strip dev-only code in Vue source
50 | new webpack.DefinePlugin({
51 | 'process.env': injectEnv
52 | })
53 | )
54 | }
55 |
56 | module.exports = config
57 |
--------------------------------------------------------------------------------
/build/webpack.server.config.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack')
2 | const merge = require('webpack-merge')
3 | const base = require('./webpack.base.config')
4 | const nodeExternals = require('webpack-node-externals')
5 | const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
6 | const config = merge(base, {
7 | // 这允许 webpack 以 Node 适用方式(Node-appropriate fashion)处理动态导入(dynamic import),
8 | // 并且还会在编译 Vue 组件时,
9 | // 告知 `vue-loader` 输送面向服务器代码(server-oriented code)。
10 | target: 'node',
11 | // 对 bundle renderer 提供 source map 支持
12 | devtool: '#source-map',
13 | entry: './src/entry-server.js',
14 | // 此处告知 server bundle 使用 Node 风格导出模块(Node-style exports)
15 | output: {
16 | filename: 'server-bundle.js',
17 | libraryTarget: 'commonjs2'
18 | },
19 | // 外置化应用程序依赖模块。可以使服务器构建速度更快,
20 | // 并生成较小的 bundle 文件。
21 | externals: nodeExternals({
22 | whitelist: /\.css$/
23 | }),
24 | plugins: [
25 | // 这是将服务器的整个输出
26 | // 构建为单个 JSON 文件的插件。
27 | // 默认文件名为 `vue-ssr-server-bundle.json`
28 | new VueSSRServerPlugin()
29 | ]
30 | })
31 | if (process.env.NODE_ENV === 'production') {
32 | const injectEnv = require(`../config/env.prod.js`)
33 | injectEnv.VUE_ENV = '"server"'
34 | config.plugins.push(
35 | // strip dev-only code in Vue source
36 | new webpack.DefinePlugin({
37 | 'process.env': injectEnv
38 | })
39 | )
40 | }
41 |
42 | module.exports = config
43 |
--------------------------------------------------------------------------------
/commitlint.config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * feat:新增功能
3 | * fix:bug 修复
4 | * docs:文档更新
5 | * style:不影响程序逻辑的代码修改(修改空白字符,格式缩进,补全缺失的分号等,没有改变代码逻辑)
6 | * refactor:重构代码(既没有新增功能,也没有修复 bug)
7 | * perf:性能, 体验优化
8 | * test:新增测试用例或是更新现有测试
9 | * build:主要目的是修改项目构建系统(例如 glup,webpack,rollup 的配置等)的提交
10 | * ci:主要目的是修改项目继续集成流程(例如 Travis,Jenkins,GitLab CI,Circle等)的提交
11 | * chore:不属于以上类型的其他类型,比如构建流程, 依赖管理
12 | * revert:回滚某个更早之前的提交
13 | */
14 | module.exports = {
15 | extends: ['@commitlint/config-conventional'],
16 | rules: {
17 | 'type-enum': [2, 'always', ['feat', 'fix', 'docs', 'style', 'perf', 'refactor', 'test', 'chore', 'revert']],
18 | 'subject-full-stop': [0, 'never'],
19 | 'subject-case': [0, 'never']
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/config/env.dev.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @desc 开发环境的变量注入
3 | */
4 | module.exports = {
5 | NODE_ENV: '"development"',
6 | STATS: '"development"',
7 | BASE_URL: '"http://localhost:6180"'
8 | }
9 |
--------------------------------------------------------------------------------
/config/env.prod.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @desc 生产环境的变量注入
3 | */
4 | module.exports = {
5 | NODE_ENV: '"production"',
6 | STATS: '"production"',
7 | BASE_URL: '"https://mapblog.cn"'
8 | }
9 |
--------------------------------------------------------------------------------
/copy.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @desc 复制需要部署的文件、目录等
3 | * @author justJokee
4 | */
5 |
6 | const fs = require('fs-extra')
7 | console.log()
8 | console.log('开始复制部署资源 >>>>')
9 | console.log()
10 | fs.copySync('./static', './dist/front')
11 | fs.copySync('./src/index.template.html', './dist/front/index.template.html')
12 | fs.copySync('./server', './dist/server')
13 | fs.copySync('./server.js', './dist/server.js')
14 | fs.copySync('./ecosystem.config.js', './dist/ecosystem.config.js')
15 | // 处理package.json
16 | const json = fs.readFileSync('./package.json')
17 | const obj = JSON.parse(json)
18 | ;(obj.scripts['start:local'] = '../node_modules/.bin/cross-env NODE_ENV=production node server'),
19 | delete obj.scripts.preinstall
20 | delete obj.scripts.prepare
21 | fs.writeFileSync('./dist/package.json', JSON.stringify(obj, null, '\t'), 'utf8')
22 |
23 | console.log('部署资源复制完毕 >>>>')
24 | console.log()
25 |
--------------------------------------------------------------------------------
/ecosystem.config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @desc pm2 配置文件
3 | * @author justJokee
4 | */
5 | module.exports = {
6 | apps: [
7 | {
8 | name: 'mapblog',
9 | cwd: './',
10 | script: 'server.js',
11 | max_memory_restart: '1024M',
12 | log_date_format: 'YYYY-MM-DD HH:mm Z',
13 | exec_mode: 'cluster',
14 | instances: 'max',
15 | watch: 'false',
16 | env: {
17 | NODE_ENV: 'development'
18 | },
19 | env_production: {
20 | NODE_ENV: 'production' // 环境参数,当前指定为生产环境
21 | }
22 | }
23 | ]
24 |
25 | // deploy: {
26 | // production: {
27 | // user: 'SSH_USERNAME',
28 | // host: 'SSH_HOSTMACHINE',
29 | // ref: 'origin/master',
30 | // repo: 'GIT_REPOSITORY',
31 | // path: 'DESTINATION_PATH',
32 | // 'pre-deploy-local': '',
33 | // 'post-deploy': 'npm install && pm2 reload ecosystem.config.js --env production',
34 | // 'pre-setup': ''
35 | // }
36 | // }
37 | }
38 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ssr",
3 | "version": "1.0.0",
4 | "description": "Marco's Blog",
5 | "author": "justJokee",
6 | "private": true,
7 | "scripts": {
8 | "preinstall": "node ./preinstall.js",
9 | "prepare": "husky install",
10 | "dev": "cross-env NODE_ENV=development node server",
11 | "start": "cross-env NODE_ENV=production node server",
12 | "build": "rimraf dist && npm run build:client && npm run build:server && npm run copy",
13 | "build:client": "cross-env NODE_ENV=production webpack --config build/webpack.client.config.js --progress --hide-modules",
14 | "build:server": "cross-env NODE_ENV=production webpack --config build/webpack.server.config.js --progress --hide-modules",
15 | "pm2:prod": "cross-env NODE_ENV=production pm2 start ecosystem.config.js --env production",
16 | "copy": "node copy.js"
17 | },
18 | "lint-staged": {
19 | "src/**/*.{js,vue}": [
20 | "prettier --write",
21 | "eslint --fix --ext .js,.jsx,.vue"
22 | ]
23 | },
24 | "dependencies": {
25 | "@octokit/core": "^3.5.1",
26 | "archiver": "^2.1.1",
27 | "axios": "^0.17.1",
28 | "element-ui": "^2.15.6",
29 | "emojilib": "^3.0.4",
30 | "express": "^4.16.2",
31 | "lodash": "^4.17.21",
32 | "moment": "^2.29.1",
33 | "qs": "^6.5.1",
34 | "resize-observer-polyfill": "^1.5.1",
35 | "ueditor": "^1.2.3",
36 | "vue": "^2.6.10",
37 | "vue-meta": "^1.5.0",
38 | "vue-router": "^3.5.2",
39 | "vue-server-renderer": "^2.6.10",
40 | "vuex": "^3.0.1"
41 | },
42 | "devDependencies": {
43 | "@babel/core": "^7.17.5",
44 | "@babel/eslint-parser": "^7.17.0",
45 | "@babel/plugin-transform-runtime": "^7.17.0",
46 | "@babel/preset-env": "^7.16.0",
47 | "@commitlint/cli": "^16.2.3",
48 | "@commitlint/config-conventional": "^16.2.1",
49 | "autoprefixer": "^7.1.2",
50 | "babel-helper-vue-jsx-merge-props": "^2.0.3",
51 | "babel-loader": "^8.2.3",
52 | "babel-plugin-component": "^1.1.1",
53 | "babel-plugin-syntax-jsx": "^6.18.0",
54 | "babel-plugin-transform-vue-jsx": "^3.5.0",
55 | "body-parser": "^1.18.2",
56 | "chalk": "^2.0.1",
57 | "compression": "^1.7.4",
58 | "cookie-parser": "^1.4.3",
59 | "copy-webpack-plugin": "^4.0.1",
60 | "cross-env": "^5.1.3",
61 | "cross-spawn": "^5.1.0",
62 | "css-loader": "^0.28.0",
63 | "detect-port": "^1.3.0",
64 | "douban-spider-v": "0.0.4",
65 | "ejs": "^2.5.7",
66 | "eslint": "^8.10.0",
67 | "eslint-plugin-html": "^6.2.0",
68 | "eslint-plugin-import": "^2.20.2",
69 | "eslint-plugin-prettier": "3.4.0",
70 | "eslint-plugin-vue": "^8.5.0",
71 | "extract-text-webpack-plugin": "^3.0.0",
72 | "file-loader": "^1.1.4",
73 | "friendly-errors-webpack-plugin": "^1.6.1",
74 | "fs-extra": "^10.0.1",
75 | "html-webpack-plugin": "^2.30.1",
76 | "husky": "^7.0.4",
77 | "js-md5": "^0.7.3",
78 | "jsonwebtoken": "^8.5.1",
79 | "less": "^2.7.3",
80 | "less-loader": "^4.0.5",
81 | "lint-staged": "^12.3.7",
82 | "lru-cache": "^7.5.1",
83 | "md5": "^2.2.1",
84 | "memory-fs": "^0.5.0",
85 | "mongoose": "^4.13.9",
86 | "morgan": "^1.10.0",
87 | "node-notifier": "^5.1.2",
88 | "node-sass": "^4.7.2",
89 | "node-schedule": "^2.1.0",
90 | "optimize-css-assets-webpack-plugin": "^3.2.0",
91 | "ora": "^1.2.0",
92 | "portfinder": "^1.0.13",
93 | "postcss-import": "^11.0.0",
94 | "postcss-loader": "^2.0.8",
95 | "postcss-url": "^7.2.1",
96 | "prettier": "^2.5.1",
97 | "qiniu": "^7.4.0",
98 | "rimraf": "^2.6.0",
99 | "sass": "^1.0.0-beta.4",
100 | "sass-loader": "^6.0.6",
101 | "semver": "^5.3.0",
102 | "serve-favicon": "^2.4.5",
103 | "sw-precache-webpack-plugin": "^0.11.4",
104 | "uglifyjs-webpack-plugin": "^1.1.1",
105 | "url-loader": "^0.5.8",
106 | "vue-eslint-parser": "^8.3.0",
107 | "vue-loader": "^13.3.0",
108 | "vue-style-loader": "^3.0.1",
109 | "vue-template-compiler": "^2.6.10",
110 | "vuex-router-sync": "^5.0.0",
111 | "webpack": "^3.6.0",
112 | "webpack-bundle-analyzer": "^2.9.0",
113 | "webpack-dev-server": "^2.9.1",
114 | "webpack-hot-middleware": "^2.21.0",
115 | "webpack-merge": "^4.1.0",
116 | "webpack-node-externals": "^1.6.0"
117 | },
118 | "engines": {
119 | "node": ">= 6.0.0",
120 | "npm": ">= 3.0.0"
121 | },
122 | "browserslist": [
123 | "> 1%",
124 | "last 2 versions",
125 | "not ie <= 8"
126 | ]
127 | }
128 |
--------------------------------------------------------------------------------
/preinstall.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @desc 创建数据库/第三方登录等配置文件
3 | * @author justJokee
4 | */
5 |
6 | const fs = require('fs')
7 | const path = './server/db/secret.js'
8 |
9 | if (!fs.existsSync(path)) {
10 | const str = fs.readFileSync('./template/secret.js')
11 | fs.writeFileSync(path, str, 'utf8')
12 |
13 | console.log()
14 | console.log('配置文件创建成功 >>>>')
15 | console.log()
16 | }
17 |
--------------------------------------------------------------------------------
/screenShot/front-archive-1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justJokee/vue-ssr-blog/f944768f8a87f10841b121fecd3055ca876579ad/screenShot/front-archive-1.jpg
--------------------------------------------------------------------------------
/screenShot/front-archive-2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justJokee/vue-ssr-blog/f944768f8a87f10841b121fecd3055ca876579ad/screenShot/front-archive-2.jpg
--------------------------------------------------------------------------------
/screenShot/front-article-1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justJokee/vue-ssr-blog/f944768f8a87f10841b121fecd3055ca876579ad/screenShot/front-article-1.jpg
--------------------------------------------------------------------------------
/screenShot/front-article-2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justJokee/vue-ssr-blog/f944768f8a87f10841b121fecd3055ca876579ad/screenShot/front-article-2.jpg
--------------------------------------------------------------------------------
/screenShot/front-category.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justJokee/vue-ssr-blog/f944768f8a87f10841b121fecd3055ca876579ad/screenShot/front-category.jpg
--------------------------------------------------------------------------------
/screenShot/front-home.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justJokee/vue-ssr-blog/f944768f8a87f10841b121fecd3055ca876579ad/screenShot/front-home.jpg
--------------------------------------------------------------------------------
/screenShot/front-leavewords-1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justJokee/vue-ssr-blog/f944768f8a87f10841b121fecd3055ca876579ad/screenShot/front-leavewords-1.jpg
--------------------------------------------------------------------------------
/screenShot/front-leavewords-2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justJokee/vue-ssr-blog/f944768f8a87f10841b121fecd3055ca876579ad/screenShot/front-leavewords-2.jpg
--------------------------------------------------------------------------------
/screenShot/front-movies-1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justJokee/vue-ssr-blog/f944768f8a87f10841b121fecd3055ca876579ad/screenShot/front-movies-1.jpg
--------------------------------------------------------------------------------
/screenShot/front-movies-2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justJokee/vue-ssr-blog/f944768f8a87f10841b121fecd3055ca876579ad/screenShot/front-movies-2.jpg
--------------------------------------------------------------------------------
/screenShot/front-search.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justJokee/vue-ssr-blog/f944768f8a87f10841b121fecd3055ca876579ad/screenShot/front-search.jpg
--------------------------------------------------------------------------------
/screenShot/front-tag.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justJokee/vue-ssr-blog/f944768f8a87f10841b121fecd3055ca876579ad/screenShot/front-tag.png
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 | const express = require('express')
2 | const path = require('path')
3 | const lruCache = require('lru-cache')
4 | const fs = require('fs')
5 | const cookieParser = require('cookie-parser')
6 | const bodyParser = require('body-parser')
7 | const logger = require('morgan')
8 | const ejs = require('ejs')
9 | const detect = require('detect-port')
10 | const route = require('./server/api/')
11 | const compression = require('compression')
12 | const { createBundleRenderer } = require('vue-server-renderer')
13 | const { startSchedule } = require('./server/utils/schedule')
14 | const isProd = process.env.NODE_ENV === 'production'
15 | const tpl = isProd ? './front/index.template.html' : './src/index.template.html'
16 | const template = fs.readFileSync(tpl, 'utf-8')
17 | const ratelimit = require('./server/middleware/ratelimit')
18 | const getIp = require('./server/utils/getIp')
19 | const server = express()
20 | // nginx 反向代理后获取真实ip
21 | logger.token('remote-addr', (req) => {
22 | return req.headers['x-real-ip'] || req.headers['x-forwarded-for'] || req.connection.remoteAddress
23 | })
24 | // const resolve = (file) => path.resolve(__dirname, file)
25 | // 开启流控
26 | server.use('/api', ratelimit)
27 | // 日志记录中间件
28 | server.use(logger(isProd ? 'combined' : 'tiny'))
29 | server.use(bodyParser.json())
30 | server.use(bodyParser.urlencoded({ extended: true }))
31 | server.use(cookieParser())
32 | // 开启定时任务
33 | startSchedule()
34 | //引入ejs模板引擎
35 | server.set('views', [path.join(__dirname, isProd ? 'front' : 'static')])
36 | server.engine('.html', ejs.__express)
37 | server.set('view engine', 'ejs')
38 | route(server)
39 |
40 | const LRU = new lruCache({
41 | max: 1000,
42 | ttl: 1000 * 60 * 15
43 | })
44 |
45 | function createRenderer(bundle, options) {
46 | return createBundleRenderer(
47 | bundle,
48 | Object.assign(options, {
49 | template: template,
50 | cache: LRU,
51 | // basedir: isProd ? resolve('./front') : resolve('./dist'),
52 | runInNewContext: false
53 | })
54 | )
55 | }
56 | let renderer
57 | // eslint-disable-next-line no-unused-vars
58 | let readyPromise
59 | if (isProd) {
60 | const bundle = require('./front/vue-ssr-server-bundle.json')
61 | const clientManifest = require('./front/vue-ssr-client-manifest.json')
62 | renderer = createRenderer(bundle, {
63 | clientManifest
64 | })
65 | readyPromise = new Promise((resolve) => {
66 | checkPort().then((port) => {
67 | process.env.__SAFE_PORT__ = port
68 | resolve()
69 | })
70 | })
71 | } else {
72 | readyPromise = require('./build/setup-dev-server')(server, (bundle, options) => {
73 | renderer = createRenderer(bundle, options)
74 | })
75 | }
76 | readyPromise.then(() => {
77 | server.use(compression()) //开启gzip压缩
78 | // 伺服静态资源
79 | if (isProd) {
80 | server.use(express.static(path.join(__dirname, 'front')))
81 | } else {
82 | server.use(express.static(path.join(__dirname, 'static')))
83 | }
84 |
85 | // 前端请求
86 | server.get(['/', '/app/*'], (req, res) => {
87 | try {
88 | const context = {
89 | title: 'mapBlog',
90 | url: req.url
91 | }
92 | // 文章详情页、留言板页面请求,在server-render运行时注入客户端IP,
93 | // 否则在在查表时,获取的是生产环境的服务器IP,这将导致根据ip统计的点赞情况失效
94 | if (req.url.startsWith('/app/article/') || req.url === '/app/messageBoard') {
95 | context._ip = getIp(req)
96 | }
97 | renderer.renderToString(context, (err, html) => {
98 | if (err) {
99 | res.status(500).end('Internal Server Error')
100 | return
101 | }
102 | if (context.meta) {
103 | const { title, meta } = context.meta.inject()
104 | html = html.replace(//g, title.text())
105 | html = html.replace(//g, meta.text())
106 | }
107 |
108 | res.end(html)
109 | })
110 | } catch (e) {
111 | res.status(500).end('Internal Server Error')
112 | }
113 | })
114 |
115 | server.get('*', function (req, res) {
116 | res.render('404.html', {
117 | title: 'No Found'
118 | })
119 | })
120 |
121 | const port = process.env.__SAFE_PORT__
122 | const uri = 'http://localhost:' + port
123 | console.log()
124 | console.log('启动服务路径' + uri)
125 | console.log()
126 | server.listen(port, '0.0.0.0')
127 | })
128 | // 检查端口占用情况
129 | function checkPort(port = 6180) {
130 | return new Promise((resolve) => {
131 | detect(port, (err, _port) => {
132 | if (err) {
133 | console.log(err)
134 | }
135 | if (port == _port) {
136 | resolve(port)
137 | } else {
138 | resolve(_port)
139 | console.log(`port: ${port} was occupied, try port: ${_port}`)
140 | }
141 | })
142 | })
143 | }
144 |
--------------------------------------------------------------------------------
/server/api/category.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @desc 文章分类
3 | * @author justJokee
4 | */
5 |
6 | const express = require('express')
7 | const router = express.Router()
8 | const db = require('../db/')
9 |
10 | // 获取分类列表
11 | router.get('/api/front/category/get', async (req, res) => {
12 | try {
13 | const doc = await db.category.find({}).sort({ _id: 1 })
14 | res.json({
15 | status: 200,
16 | data: doc,
17 | total: doc.length,
18 | info: '获取文档分类成功'
19 | })
20 | } catch (e) {
21 | res.status(500).end()
22 | }
23 | })
24 |
25 | module.exports = router
26 |
--------------------------------------------------------------------------------
/server/api/count.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @desc 统计类
3 | * @author justJokee
4 | */
5 | const express = require('express')
6 | const router = express.Router()
7 | const db = require('../db/')
8 | const confirmToken = require('../middleware/confirmToken')
9 |
10 | // 管理端首页统计项
11 | router.get('/api/admin/count', confirmToken, async (req, res) => {
12 | try {
13 | const article = await db.article.find({}).count()
14 | const comment = await db.comment.find({}).count()
15 | const msgBoard = await db.msgBoard.find({}).count()
16 | const visitor = await db.visitor.find({}).count()
17 | res.json({
18 | status: 200,
19 | data: {
20 | total: {
21 | article,
22 | visitor,
23 | comment,
24 | msgBoard
25 | }
26 | },
27 | info: '资源统计成功'
28 | })
29 | } catch (e) {
30 | res.status(500).end()
31 | }
32 | })
33 |
34 | module.exports = router
35 |
--------------------------------------------------------------------------------
/server/api/index.js:
--------------------------------------------------------------------------------
1 | const articles = require('./articles')
2 | const tags = require('./tags')
3 | const comments = require('./comments')
4 | const msgBoard = require('./msgBoard')
5 | const visitor = require('./visitor')
6 | const login = require('./login')
7 | const mongodump = require('./mongodump')
8 | const news = require('./news')
9 | const resetpwd = require('./resetpwd')
10 | const category = require('./category')
11 | const movies = require('./movies')
12 | const qiniu = require('./qiniu')
13 | const viewer = require('./viewer')
14 | const count = require('./count')
15 |
16 | module.exports = (app) => {
17 | app.use(viewer)
18 | app.use(articles)
19 | app.use(tags)
20 | app.use(comments)
21 | app.use(msgBoard)
22 | app.use(login)
23 | app.use(visitor)
24 | app.use(mongodump)
25 | app.use(news)
26 | app.use(resetpwd)
27 | app.use(category)
28 | app.use(movies)
29 | app.use(qiniu)
30 | app.use(count)
31 | }
32 |
--------------------------------------------------------------------------------
/server/api/login.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @desc 登录鉴权
3 | * @author justJokee
4 | */
5 | const express = require('express')
6 | const router = express.Router()
7 | const jwt = require('jsonwebtoken')
8 | const db = require('../db/')
9 | const { userSecret } = require('../db/secret')
10 |
11 | const createToken = (id, account) => {
12 | const secret = `${userSecret.salt}`
13 | return jwt.sign(
14 | {
15 | id: id,
16 | account: account
17 | },
18 | secret,
19 | { expiresIn: '4h' }
20 | )
21 | }
22 | // 登录验证
23 | router.post('/api/admin/login', async (req, res) => {
24 | try {
25 | const user = await db.user.find({ account: req.body.account })
26 | // 用户名不存在,返回401
27 | if (!user.length) {
28 | res.json({
29 | status: 402,
30 | info: '用户名或密码不正确'
31 | })
32 | return
33 | }
34 | const pwd = user[0].password
35 | if (req.body.password === pwd) {
36 | const token = createToken(user[0]._id, user[0].account)
37 | res.json({
38 | status: 200,
39 | data: {
40 | token,
41 | uid: user[0]._id,
42 | account: user[0].account,
43 | avatar: user[0].avatar,
44 | lastLoginTime: user[0].lastLoginTime
45 | },
46 | info: '登陆成功'
47 | })
48 | // 更新登录时间
49 | await db.user.update({ user: req.body.user }, { lastLoginTime: new Date() })
50 | } else {
51 | res.json({
52 | status: 402,
53 | info: '用户名或密码不正确'
54 | })
55 | }
56 | } catch (e) {
57 | res.status(500).end()
58 | }
59 | })
60 |
61 | module.exports = router
62 |
--------------------------------------------------------------------------------
/server/api/mongodump.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @desc 数据库备份
3 | * @author justJokee
4 | */
5 | const express = require('express')
6 | const router = express.Router()
7 | const exec = require('child_process').exec
8 | const archiver = require('archiver')
9 | const fs = require('fs-extra')
10 | const path = require('path')
11 | const confirmToken = require('../middleware/confirmToken')
12 | const { db } = require('../db/secret')
13 | const moment = require('moment')
14 | // 备份文件临时存放路径
15 | const outputPath = path.join(__dirname, '../db/copy/temp')
16 | let toolPath = '/usr/local/mongo-tools/bin/mongodump'
17 | // 生产环境 mongodump 路径
18 | if (process.env.NODE_ENV === 'production') {
19 | toolPath = 'mongodump'
20 | }
21 | router.post('/api/admin/mongodump', confirmToken, async (req, res) => {
22 | try {
23 | // 清空临时存放路径
24 | fs.emptyDirSync(outputPath)
25 | const filename = moment().format('YYYY-MM-DD-HH-mm')
26 | const zipPath = path.join(__dirname, `../db/copy/mongodump-${filename}.zip`)
27 | exec(`${toolPath} -h 127.0.0.1:27017 -u ${db.user} -p ${db.pwd} -d ${db.db} -o ${outputPath}`, (err) => {
28 | if (err) {
29 | res.status(500).end()
30 | } else {
31 | const archive = archiver('zip')
32 | const output = fs.createWriteStream(zipPath)
33 | output.on('close', () => {
34 | const row = fs.readFileSync(zipPath)
35 | res.send(row)
36 | })
37 | archive.pipe(output)
38 | // 第二个参数是压缩包内的层级目录
39 | archive.directory(path.join(__dirname, '../db/copy/temp/mapblog'), '/mapblog')
40 | archive.finalize()
41 | }
42 | })
43 | } catch (e) {
44 | res.status(500).end()
45 | }
46 | })
47 |
48 | module.exports = router
49 |
--------------------------------------------------------------------------------
/server/api/movies.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @desc 豆瓣电影
3 | * @author justJokee
4 | */
5 | const express = require('express')
6 | const router = express.Router()
7 | const path = require('path')
8 | const fs = require('fs-extra')
9 |
10 | // 获取电影列表
11 | router.get('/api/front/douban/get', async (req, res) => {
12 | try {
13 | const page = req.query.page || 1
14 | const type = req.query.type || 'collect'
15 | const tp = path.join(__dirname, '..', `files/movies/${type}/${page}.json`)
16 | const exist = fs.existsSync(tp)
17 |
18 | if (exist) {
19 | const doc = fs.readFileSync(tp, 'utf8')
20 | const pageTotal = fs.readFileSync(path.join(__dirname, '..', `files/movies/${type}/pageTotal.txt`), 'utf8')
21 | res.json({
22 | status: 200,
23 | data: JSON.parse(doc),
24 | pageTotal: parseInt(pageTotal) || 1,
25 | info: '获取影视记录成功'
26 | })
27 | } else {
28 | res.json({
29 | status: 404,
30 | data: [],
31 | info: '数据不存在'
32 | })
33 | }
34 | } catch (e) {
35 | res.status(500).end()
36 | }
37 | })
38 |
39 | module.exports = router
40 |
--------------------------------------------------------------------------------
/server/api/news.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @desc 消息管理
3 | * @author justJokee
4 | */
5 |
6 | const express = require('express')
7 | const router = express.Router()
8 | const db = require('../db/')
9 | const confirmToken = require('../middleware/confirmToken')
10 |
11 | // 获取消息列表
12 | router.get('/api/admin/news/gets', confirmToken, async (req, res) => {
13 | try {
14 | const limit = parseInt(req.query.limit) || 10
15 | const skip = req.query.page * limit - limit
16 | delete req.query.page
17 | delete req.query.limit
18 | const doc = await db.news
19 | .find({ ...req.query })
20 | .sort({ _id: -1 })
21 | .skip(skip)
22 | .limit(limit)
23 |
24 | const total = await db.news.count({ ...req.query })
25 |
26 | res.json({
27 | status: 200,
28 | data: doc,
29 | total,
30 | info: '获取消息成功'
31 | })
32 | } catch (e) {
33 | res.status(500).end()
34 | }
35 | })
36 | // 已读消息
37 | router.patch('/api/admin/news/read', confirmToken, async (req, res) => {
38 | try {
39 | const params = typeof req.body.id === 'string' ? { _id: req.body.id } : { _id: { $in: req.body.id } }
40 | await db.news.updateMany(params, { $set: { read: 1 } })
41 | res.json({
42 | status: 200,
43 | info: '设置已读消息成功'
44 | })
45 | } catch (e) {
46 | res.status(500).end()
47 | }
48 | })
49 | // 获取所有未读消息数量
50 | router.get('/api/admin/news/unReadTotal', confirmToken, async (req, res) => {
51 | try {
52 | const total = await db.news.count({ read: 0 })
53 | res.json({
54 | status: 200,
55 | total,
56 | info: '未读消息数量统计成功'
57 | })
58 | } catch (e) {
59 | res.status(500).end()
60 | }
61 | })
62 | // 将所有消息设置为已读
63 | router.patch('/api/admin/news/readAll', confirmToken, async (req, res) => {
64 | try {
65 | await db.news.updateMany({ read: 0 }, { $set: { read: 1 } })
66 | res.json({
67 | status: 200,
68 | info: '设置所有消息已读成功'
69 | })
70 | } catch (e) {
71 | res.status(500).end()
72 | }
73 | })
74 | // 删除消息
75 | router.delete('/api/admin/news/del', confirmToken, async (req, res) => {
76 | try {
77 | const params = typeof req.query.id === 'string' ? { _id: req.query.id } : { _id: { $in: req.query.id } }
78 | await db.news.remove(params)
79 | res.json({
80 | status: 200,
81 | info: '删除消息成功'
82 | })
83 | } catch (e) {
84 | res.status(500).end()
85 | }
86 | })
87 |
88 | module.exports = router
89 |
--------------------------------------------------------------------------------
/server/api/qiniu.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @desc 文章分类
3 | * @author justJokee
4 | */
5 | const express = require('express')
6 | const router = express.Router()
7 | const qiniu = require('qiniu')
8 | const { qiniuConfig } = require('../db/secret')
9 | const accessKey = qiniuConfig.AccessKey
10 | const secretKey = qiniuConfig.SecretKey
11 | const mac = new qiniu.auth.digest.Mac(accessKey, secretKey)
12 | const options = {
13 | scope: qiniuConfig.Bucket
14 | }
15 | const confirmToken = require('../middleware/confirmToken')
16 |
17 | // 获取分类列表
18 | router.get('/api/front/qiniu/getToken', confirmToken, async (req, res) => {
19 | try {
20 | const putPolicy = new qiniu.rs.PutPolicy(options)
21 | const uploadToken = putPolicy.uploadToken(mac)
22 | res.json({
23 | status: 200,
24 | data: {
25 | token: uploadToken
26 | },
27 | info: '获取七牛云token成功'
28 | })
29 | } catch (e) {
30 | res.status(500).end()
31 | }
32 | })
33 |
34 | module.exports = router
35 |
--------------------------------------------------------------------------------
/server/api/resetpwd.js:
--------------------------------------------------------------------------------
1 | const express = require('express')
2 | const router = express.Router()
3 | const db = require('../db/')
4 | const confirmToken = require('../middleware/confirmToken')
5 |
6 | router.patch('/api/admin/pwd/reset', confirmToken, async (req, res) => {
7 | const user = await db.user.find({ _id: req.body.uid })
8 | if (user.length) {
9 | // 旧密码不正确
10 | if (req.body.oldPassword !== user[0].password) {
11 | res.json({
12 | status: 201,
13 | info: '旧密码不正确'
14 | })
15 | } else {
16 | await db.user.update({ _id: req.body.uid }, { $set: { password: req.body.password } })
17 | res.json({
18 | status: 200,
19 | info: '密码修改成功,请重新登录'
20 | })
21 | }
22 | } else {
23 | res.json({
24 | status: 104,
25 | info: '用户不存在'
26 | })
27 | }
28 | })
29 | module.exports = router
30 |
--------------------------------------------------------------------------------
/server/api/tags.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @desc 文章标签
3 | * @author justJokee
4 | */
5 | const express = require('express')
6 | const router = express.Router()
7 | const db = require('../db/')
8 |
9 | // 获取文档标签统计数量
10 | router.get('/api/front/tags/count', async (req, res) => {
11 | try {
12 | const tags = await db.article.aggregate([
13 | { $match: { publish: 1 } },
14 | { $unwind: '$tag' },
15 | { $group: { _id: '$tag', total: { $sum: 1 } } },
16 | { $project: { tag: '$_id', _id: 0, total: 1 } }
17 | ])
18 | res.json({
19 | status: 200,
20 | data: tags,
21 | total: tags.length,
22 | info: '获取标签统计成功'
23 | })
24 | } catch (e) {
25 | res.status(500).end()
26 | }
27 | })
28 |
29 | module.exports = router
30 |
--------------------------------------------------------------------------------
/server/api/viewer.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @desc 访客信息
3 | * @author justJokee
4 | */
5 |
6 | const express = require('express')
7 | const router = express.Router()
8 | const db = require('../db/')
9 | const getIp = require('../utils/getIp')
10 | const getBrowser = require('../utils/getBrowser')
11 | const getOS = require('../utils/getOS')
12 |
13 | // 统计访客数量、信息(按前端刷新次数计算)
14 | router.get(['/', '/app/*'], async (req, res, next) => {
15 | try {
16 | if (process.env.NODE_ENV === 'production') {
17 | const ip = getIp(req)
18 | const browser = getBrowser(req.headers['user-agent'])
19 | const system = getOS(req.headers['user-agent'])
20 | await new db.viewer({
21 | ip,
22 | count: 1,
23 | browser: browser,
24 | system: system,
25 | date: new Date()
26 | }).save()
27 | }
28 | next()
29 | } catch (e) {
30 | next()
31 | }
32 | })
33 | // 按天返回某一段时间范围内的访客数量
34 | router.get('/api/admin/viewer/history', async (req, res) => {
35 | try {
36 | const doc = await db.viewer.aggregate([
37 | {
38 | $match: {
39 | date: {
40 | $gte: new Date(parseInt(req.query.start)),
41 | $lte: new Date(parseInt(req.query.end))
42 | }
43 | }
44 | },
45 | {
46 | $project: {
47 | date: { $dateToString: { format: '%Y-%m-%d', date: '$date' } }
48 | }
49 | },
50 | {
51 | $group: {
52 | _id: '$date',
53 | total: { $sum: 1 }
54 | }
55 | },
56 | {
57 | $project: {
58 | date: '$_id',
59 | value: '$total',
60 | _id: 0
61 | }
62 | },
63 | { $sort: { date: 1 } }
64 | ])
65 | res.json({
66 | status: 200,
67 | data: doc,
68 | info: '查询访问记录成功'
69 | })
70 | } catch (e) {
71 | res.status(500).end()
72 | }
73 | })
74 | // 查询访客设备信息
75 | router.get('/api/front/viewer/getDevice', async (req, res) => {
76 | try {
77 | const browser = await db.viewer.aggregate([
78 | {
79 | $group: {
80 | _id: '$browser',
81 | total: { $sum: 1 }
82 | }
83 | },
84 | {
85 | $project: {
86 | name: '$_id',
87 | value: '$total',
88 | _id: 0
89 | }
90 | }
91 | ])
92 | const system = await db.viewer.aggregate([
93 | {
94 | $group: {
95 | _id: '$system',
96 | total: { $sum: 1 }
97 | }
98 | },
99 | {
100 | $project: {
101 | name: '$_id',
102 | value: '$total',
103 | _id: 0
104 | }
105 | }
106 | ])
107 |
108 | res.json({
109 | status: 200,
110 | data: { browser, system },
111 | info: '访客设备信息统计成功'
112 | })
113 | } catch (e) {
114 | res.status(500).end()
115 | }
116 | })
117 | // 查询访客总数
118 | router.get('/api/front/admin/count', async (req, res) => {
119 | try {
120 | const total = await db.viewer.find({}).count()
121 | const doc = await db.viewer.find({}).sort({ _id: -1 }).limit(1)
122 | res.json({
123 | status: 200,
124 | data: { total, latest: doc[0] },
125 | info: '访客设备信息统计成功'
126 | })
127 | } catch (e) {
128 | res.status(500).end()
129 | }
130 | })
131 | module.exports = router
132 |
--------------------------------------------------------------------------------
/server/api/visitor.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @desc 访客信息处理
3 | * @author justJokee
4 | */
5 |
6 | const express = require('express')
7 | const { Octokit } = require('@octokit/core')
8 | const api = require('../http/server-api')
9 | const router = express.Router()
10 | const db = require('../db/')
11 | const { githubSecret } = require('../db/secret')
12 |
13 | // 存储访客信息
14 | router.post('/api/front/visitor/save', async (req, res) => {
15 | try {
16 | // 自定义用户
17 | if (req.body.type === '0') {
18 | const exist = await db.visitor.find({ name: req.body.name })
19 | if (exist.length) {
20 | res.json({ status: 100, info: '用户名已存在' })
21 | return
22 | }
23 | }
24 |
25 | const doc = await new db.visitor(req.body).save()
26 |
27 | res.json({
28 | status: 200,
29 | data: doc,
30 | info: '访客信息存储成功'
31 | })
32 | } catch (e) {
33 | res.status(500).end()
34 | }
35 | })
36 |
37 | // 查看访客是否已经被存储
38 | router.post('/api/front/visitor/existed', async (req, res) => {
39 | try {
40 | // QQ用户
41 | if (req.body.type === '1') {
42 | const exist = await db.visitor.find({ qqOpenId: req.body.qqOpenId })
43 | if (exist.length) {
44 | // 仅更新昵称和头像
45 | await db.visitor.update(
46 | { qqOpenId: req.body.qqOpenId },
47 | { $set: { name: req.body.name, imgUrl: req.body.imgUrl } }
48 | )
49 | const doc = await db.visitor.find({ qqOpenId: req.body.qqOpenId })
50 | res.json({ status: 200, info: '访客信息已存在', data: { info: doc[0], _saved: 1 } })
51 | return
52 | }
53 | }
54 | res.json({
55 | status: 200,
56 | data: {
57 | _saved: 0
58 | },
59 | info: '访客信息不存在'
60 | })
61 | } catch (e) {
62 | res.status(500).end()
63 | }
64 | })
65 |
66 | // github登录
67 |
68 | router.get('/login/git', (req, res) => {
69 | const baseUrl = process.env.NODE_ENV === 'production' ? 'https://mapblog.cn' : 'http://localhost:6180'
70 | //请替换为自己的client_id
71 | let path = `https://github.com/login/oauth/authorize?client_id=${githubSecret.clientId}&scope=['user']&redirect_uri=${baseUrl}/login_github`
72 | res.redirect(path)
73 | res.status(200).end()
74 | })
75 | router.get('/login_github', (req, res) => {
76 | try {
77 | const baseUrl = process.env.NODE_ENV === 'production' ? 'https://mapblog.cn' : 'http://localhost:6180'
78 |
79 | const params = {
80 | client_id: githubSecret.clientId,
81 | client_secret: githubSecret.secret,
82 | code: req.query.code,
83 | scope: ['user'],
84 | redirect_uri: `${baseUrl}/login_github`
85 | }
86 | api
87 | .post('https://github.com/login/oauth/access_token', JSON.parse(JSON.stringify(params)))
88 | .then((fullData) => {
89 | const arr1 = fullData.split('&')
90 | const arr2 = arr1[0].split('=')
91 | const token = arr2[1]
92 | return token
93 | })
94 | .then(async (token) => {
95 | let userInfo = {}
96 | const octokit = new Octokit({ auth: `${token}` })
97 | const info = await octokit.request('GET /user')
98 | // 查看访客表是否已经存在此用户
99 | const visitor = await db.visitor.find({ githubId: info.data.id })
100 | // 存在此用户
101 | if (visitor.length) {
102 | await db.visitor.update(
103 | { githubId: info.data.id },
104 | { $set: { name: info.data.login, imgUrl: info.data.avatar_url } }
105 | )
106 | const doc = await db.visitor.find({ githubId: info.data.id })
107 | userInfo = {
108 | ...doc[0].toObject(),
109 | // 前端判断存在的标识
110 | _saved: 1
111 | }
112 | }
113 | res.render('gc_back.html', { title: 'github登陆成功', userInfo: JSON.stringify(userInfo) })
114 | })
115 | } catch (e) {
116 | res.status(500).end()
117 | }
118 | })
119 |
120 | module.exports = router
121 |
--------------------------------------------------------------------------------
/server/db/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @desc 数据库连接相关入口
3 | * @author justjokee
4 | */
5 |
6 | const mongoose = require('mongoose')
7 | const md5 = require('js-md5')
8 | const { db: dbSecret, userSecret } = require('./secret')
9 | const {
10 | userSchema,
11 | visitorsSchema,
12 | categorySchema,
13 | articleSchema,
14 | commentSchema,
15 | msgBoardSchema,
16 | newsSchema,
17 | counterSchema,
18 | commentIpSchema,
19 | viewerSchema
20 | } = require('./schema')
21 |
22 | // 实现articleId自增序列
23 | articleSchema.pre('save', async function (next) {
24 | const that = this
25 | try {
26 | const counterDoc = await db.counter.find({})
27 | if (!counterDoc.length) {
28 | await new db.counter({ _id: 'entityId', seq: 2 }).save()
29 | that.articleId = 1
30 | } else {
31 | const curCountDoc = await db.counter.findByIdAndUpdate(
32 | { _id: 'entityId' },
33 | {
34 | $inc: {
35 | seq: 1
36 | }
37 | }
38 | )
39 | that.articleId = curCountDoc.seq
40 | }
41 | next()
42 | } catch (e) {
43 | next(e)
44 | }
45 | })
46 |
47 | const db = {
48 | user: mongoose.model('user', userSchema),
49 | article: mongoose.model('article', articleSchema),
50 | category: mongoose.model('categorys', categorySchema),
51 | comment: mongoose.model('comment', commentSchema),
52 | msgBoard: mongoose.model('msgBoard', msgBoardSchema),
53 | visitor: mongoose.model('visitor', visitorsSchema),
54 | news: mongoose.model('new', newsSchema),
55 | counter: mongoose.model('counter', counterSchema),
56 | commentIp: mongoose.model('commentIp', commentIpSchema),
57 | viewer: mongoose.model('viewer', viewerSchema)
58 | }
59 |
60 | // 初次启动服务后创建用户
61 | const initUser = async () => {
62 | const user = await db.user.find({})
63 |
64 | if (!user.length) {
65 | const { account, pwd, salt, avatar } = userSecret
66 | await new db.user({
67 | account,
68 | password: md5(pwd),
69 | salt: salt,
70 | avatar,
71 | lastLoginTime: new Date()
72 | }).save()
73 | console.log()
74 | console.log('you have created account now!')
75 | console.log()
76 | }
77 | }
78 | // TODO: 后续会增加分类管理
79 | const initCategorys = async () => {
80 | const category = await db.category.find({})
81 |
82 | if (!category.length) {
83 | await new db.category({
84 | name: '技术文档',
85 | total: 0,
86 | createTime: new Date()
87 | }).save()
88 | await new db.category({
89 | name: '生活感悟',
90 | total: 0,
91 | createTime: new Date()
92 | }).save()
93 | console.log()
94 | console.log('you have created category now!')
95 | console.log()
96 | }
97 | }
98 | // 建立连接
99 | mongoose.Promise = global.Promise
100 | mongoose.connection.openUri(`mongodb://${dbSecret.user}:${dbSecret.pwd}@localhost:27017/${dbSecret.db}`)
101 | mongoose.connection.once('open', () => {
102 | initUser()
103 | initCategorys()
104 | })
105 |
106 | module.exports = db
107 |
--------------------------------------------------------------------------------
/server/db/schema.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @desc DB schema
3 | * @author justJokee
4 | */
5 | const mongoose = require('mongoose')
6 |
7 | const userSchema = new mongoose.Schema({
8 | account: 'string',
9 | password: 'string',
10 | salt: 'string',
11 | avatar: 'string',
12 | lastLoginTime: Date
13 | })
14 | const viewerSchema = new mongoose.Schema({
15 | ip: 'string',
16 | count: 'number',
17 | browser: 'string',
18 | system: 'string',
19 | date: 'date'
20 | })
21 | const visitorsSchema = new mongoose.Schema({
22 | name: 'string',
23 | imgUrl: 'string',
24 | email: 'string',
25 | link: 'string',
26 | // 0: 自定义用户 1: qq 2: github
27 | type: 'number',
28 | githubId: 'number',
29 | qqOpenId: 'string',
30 | date: 'date'
31 | })
32 | // 文章分类
33 | const categorySchema = new mongoose.Schema({
34 | name: {
35 | type: 'string',
36 | required: true
37 | },
38 | total: 'number',
39 | createTime: 'date',
40 | updateTime: 'date'
41 | })
42 | const articleSchema = new mongoose.Schema({
43 | articleId: 'number',
44 | categoryId: mongoose.Schema.Types.ObjectId,
45 | original: 'number',
46 | title: 'string',
47 | abstract: 'string',
48 | content: 'string',
49 | // 当前最新编辑内容,当文档发布后,与content保持严格一致
50 | content_draft: 'string',
51 | content_plain: 'string',
52 | headerPic: 'string',
53 | // 是否发布 0:未发布 1:发布
54 | // 新建文档初始值为 0
55 | // 文档编辑阶段 可保存,可发布/更新;
56 | publish: 'number',
57 | // 是否存在未经发布的编辑内容 0:不存在(即已发布) 1:存在最新编辑内容
58 | editing: 'number',
59 | tag: 'array',
60 | commentNum: 'number',
61 | likeNum: 'number',
62 | pv: 'number',
63 | createTime: 'date',
64 | updateTime: 'date'
65 | })
66 | const commentSchema = new mongoose.Schema({
67 | name: 'string',
68 | imgUrl: 'string',
69 | email: 'string',
70 | content: 'string',
71 | link: 'string',
72 | like: 'number',
73 | aite: 'string',
74 | articleId: 'number',
75 | title: 'string',
76 | date: 'date',
77 | // 是否管理员
78 | admin: 'number',
79 | parentId: mongoose.Schema.Types.ObjectId
80 | })
81 | const msgBoardSchema = new mongoose.Schema({
82 | name: 'string',
83 | imgUrl: 'string',
84 | email: 'string',
85 | content: 'string',
86 | link: 'string',
87 | like: 'number',
88 | aite: 'string',
89 | parentId: mongoose.Schema.Types.ObjectId,
90 | // 是否管理员
91 | admin: 'number',
92 | date: 'date'
93 | })
94 | const newsSchema = new mongoose.Schema({
95 | type: 'string',
96 | // 文章评论/留言板
97 | name: 'string',
98 | ip: 'string',
99 | lng: 'string',
100 | lat: 'string',
101 | date: 'date',
102 | // 国际区域
103 | nation: 'string',
104 | // 省份
105 | province: 'string',
106 | city: 'string',
107 | // 区域
108 | district: 'string',
109 | // 是否已读
110 | read: 'number',
111 | // 存储 _id
112 | articleId: mongoose.Schema.Types.ObjectId,
113 | commentId: mongoose.Schema.Types.ObjectId,
114 | leaveMessageId: mongoose.Schema.Types.ObjectId,
115 | content: 'string'
116 | })
117 | const commentIpSchema = new mongoose.Schema({
118 | ip: {
119 | type: 'string',
120 | required: true
121 | },
122 | // 0: 留言 1: 文章评论 2: 文章赞
123 | type: {
124 | type: 'number',
125 | required: true
126 | },
127 | msgid: {
128 | type: mongoose.Schema.Types.ObjectId,
129 | required: true
130 | },
131 | like: {
132 | type: 'number',
133 | required: true
134 | },
135 | createTime: 'date',
136 | updateTime: 'date'
137 | })
138 |
139 | const counterSchema = new mongoose.Schema({
140 | _id: 'string',
141 | seq: 'number'
142 | })
143 |
144 | module.exports = {
145 | userSchema,
146 | visitorsSchema,
147 | categorySchema,
148 | articleSchema,
149 | commentSchema,
150 | msgBoardSchema,
151 | newsSchema,
152 | counterSchema,
153 | commentIpSchema,
154 | viewerSchema
155 | }
156 |
--------------------------------------------------------------------------------
/server/http/index.js:
--------------------------------------------------------------------------------
1 | const axios = require('axios')
2 | const qs = require('qs')
3 | axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8'
4 | axios.defaults.headers.get['Content-Type'] = 'application/json;charset=UTF-8'
5 |
6 | function ajax(type, url, options) {
7 | return new Promise((resolve, reject) => {
8 | axios({
9 | method: type,
10 | url: url,
11 | // baseURL: "https://www.mapblog.cn",
12 | baseURL: process.env.BASE_URL,
13 | params: type === 'get' ? options : null,
14 | data: type !== 'get' ? qs.stringify(options) : null
15 | })
16 | .then((res) => {
17 | if (res.status === 200) {
18 | resolve(res.data)
19 | } else {
20 | reject('request error in ' + url)
21 | }
22 | })
23 | .catch((err) => {
24 | console.error(err, url)
25 | reject(err)
26 | })
27 | })
28 | }
29 | const config = {
30 | get(url, options) {
31 | return new Promise((resolve, reject) => {
32 | ajax('get', url, options)
33 | .then((data) => {
34 | resolve(data)
35 | })
36 | .catch((e) => {
37 | reject(e)
38 | })
39 | })
40 | },
41 | post(url, options) {
42 | return new Promise((resolve, reject) => {
43 | ajax('post', url, options)
44 | .then((data) => {
45 | resolve(data)
46 | })
47 | .catch((e) => {
48 | reject(e)
49 | })
50 | })
51 | },
52 | patch(url, options) {
53 | return new Promise((resolve, reject) => {
54 | ajax('patch', url, options)
55 | .then((data) => {
56 | resolve(data)
57 | })
58 | .catch((e) => {
59 | reject(e)
60 | })
61 | })
62 | },
63 | put(url, options) {
64 | return new Promise((resolve, reject) => {
65 | ajax('put', url, options)
66 | .then((data) => {
67 | resolve(data)
68 | })
69 | .catch((e) => {
70 | reject(e)
71 | })
72 | })
73 | },
74 | delete(url, options) {
75 | return new Promise((resolve, reject) => {
76 | ajax('delete', url, options)
77 | .then((data) => {
78 | resolve(data)
79 | })
80 | .catch((e) => {
81 | reject(e)
82 | })
83 | })
84 | }
85 | }
86 |
87 | module.exports = config
88 |
--------------------------------------------------------------------------------
/server/http/server-api.js:
--------------------------------------------------------------------------------
1 | const axios = require('axios')
2 | const qs = require('qs')
3 | axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8'
4 | function ajax(type, url, options) {
5 | let _params = {}
6 | if (type === 'get') _params = { params: options }
7 | if (type === 'post') _params = { data: qs.stringify(options) }
8 | return new Promise((resolve, reject) => {
9 | axios({
10 | method: type,
11 | url: url,
12 | ..._params
13 | })
14 | .then((_res) => {
15 | if (_res.status === 200) {
16 | resolve(_res.data)
17 | } else {
18 | reject('request error in ' + url)
19 | }
20 | })
21 | .catch((err) => {
22 | console.log(err, url)
23 | })
24 | })
25 | }
26 | const config = {
27 | get(url, options) {
28 | return ajax('get', url, options)
29 | },
30 | post(url, options) {
31 | return ajax('post', url, options)
32 | },
33 | patch(url, options) {
34 | return ajax('patch', url, options)
35 | }
36 | }
37 |
38 | module.exports = config
39 |
--------------------------------------------------------------------------------
/server/middleware/confirmToken.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @desc api鉴权中间件
3 | * @author justJokee
4 | */
5 | const verify = require('../utils/verify')
6 | const confirmToken = (req, res, next) => {
7 | if (!req.headers.authorization) {
8 | res.json({ status: 401, info: '无访问权限或token已过期' })
9 | } else {
10 | const token = req.headers.authorization
11 |
12 | verify(token, (err) => {
13 | if (err) {
14 | res.json({ status: 401, info: '无访问权限或token已过期' })
15 | } else {
16 | next()
17 | }
18 | })
19 | }
20 | }
21 | module.exports = confirmToken
22 |
--------------------------------------------------------------------------------
/server/middleware/confirmUnpublish.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @desc 对于前后端通用的文章系列接口,阻断前端对草稿箱的访问
3 | * @param {*} req
4 | * @param {*} res
5 | * @param {*} next
6 | * @returns
7 | * @author justJokee
8 | */
9 | const verify = require('../utils/verify')
10 | const confirmUnpublish = (req, res, next) => {
11 | if (req.publish === '0') {
12 | if (!req.headers.authorization) {
13 | res.json({ status: 401, info: '无访问权限或token已过期' })
14 | } else {
15 | const token = req.headers.authorization
16 |
17 | verify(token, (err) => {
18 | if (err) {
19 | res.json({ status: 401, info: '无访问权限或token已过期' })
20 | } else {
21 | next()
22 | }
23 | })
24 | }
25 | } else next()
26 | }
27 | module.exports = confirmUnpublish
28 |
--------------------------------------------------------------------------------
/server/middleware/ratelimit.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @desc 限流中间件(简单实现)
3 | * @author justJokee
4 | */
5 |
6 | const lruCache = require('lru-cache')
7 | const getIp = require('../utils/getIp')
8 | const { highLimitApis, HEIGHLIMIT, HEIGHLIMITTL } = require('../utils/highLimitApis')
9 | // 每个ip一分钟最大限制100次请求
10 | const LIMIT = 100
11 | const LRU = new lruCache({
12 | max: 6000,
13 | // 默认时间窗口1分钟
14 | ttl: 1000 * 60
15 | // 过期后立即删除 Note that this may significantly degrade performance
16 | // ttlAutopurge: true
17 | })
18 |
19 | module.exports = function ratelimit(req, res, next) {
20 | res.header('Access-Control-Allow-Origin', 'admin.mapblog.cn')
21 | res.header('Access-Control-Allow-Headers', 'X-Requested-With')
22 | res.header('Access-Control-Allow-Methods', 'PUT,POST,GET,DELETE,OPTIONS')
23 | const ip = getIp(req)
24 | const url = `${req.baseUrl}${req.url}`
25 | const key = `${ip}:${url}`
26 | const blackKey = `black:${ip}:${url}`
27 | const value = LRU.get(key)
28 | const isHeighLimitApi = highLimitApis.includes(url)
29 | const limit = isHeighLimitApi ? HEIGHLIMIT : LIMIT
30 | const option = isHeighLimitApi ? { ttl: HEIGHLIMITTL } : {}
31 |
32 | // cache中存在当前ip对应的接口信息
33 | if (value) {
34 | if (value < limit) {
35 | LRU.set(key, value + 1, option)
36 | next()
37 | }
38 | // 请求次数超限
39 | else {
40 | LRU.delete(key)
41 | LRU.set(blackKey, Date.now())
42 | // 返回限流状态
43 | response(res)
44 | }
45 | }
46 | // cache中不存在当前请求ip信息
47 | else {
48 | // ip已经在黑名单
49 | if (LRU.get(blackKey)) {
50 | // 返回限流状态
51 | response(res)
52 | } else {
53 | LRU.set(key, 1, option)
54 | next()
55 | }
56 | }
57 | }
58 |
59 | function response(res) {
60 | res.json({
61 | status: 429,
62 | info: '访问次数超限,请稍后再试'
63 | })
64 | }
65 |
--------------------------------------------------------------------------------
/server/plugin/lastMod.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @desc 为每一条存储/更新的数据添加最后修改时间
3 | * @param {*} schema
4 | * @param {*} options
5 | */
6 | const moment = require('moment')
7 | module.exports = function lastModifiePlugin(schema, options) {
8 | schema.add({ lastMod: 'date' })
9 | schema.pre('save', function(next) {
10 | this.lastMod = moment().format()
11 | })
12 | schema.pre('update', function(next) {
13 | this.lastMod = moment().format()
14 | })
15 | }
16 |
--------------------------------------------------------------------------------
/server/utils/getBrowser.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @desc 获取当前浏览类型
3 | */
4 |
5 | module.exports = function getBrowser(userAgent) {
6 | //判断是否Opera浏览器
7 | const isOpera = userAgent.indexOf('Opera') > -1
8 | // 判断是否IE浏览器
9 | const isIE = userAgent.indexOf('compatible') > -1 && userAgent.indexOf('MSIE') > -1 && !isOpera
10 | // 判断是否IE的Edge浏览器
11 | const isEdge = userAgent.indexOf('Windows NT 6.1; Trident/7.0;') > -1 && !isIE
12 | // 判断是否Firefox浏览器
13 | const isFF = userAgent.indexOf('Firefox') > -1
14 | // 判断是否Safari浏览器
15 | const isSafari = userAgent.indexOf('Safari') > -1 && userAgent.indexOf('Chrome') == -1
16 | // 判断Chrome浏览器
17 | const isChrome = userAgent.indexOf('Chrome') > -1 && userAgent.indexOf('Safari') > -1
18 |
19 | if (isChrome) return 'Chrome'
20 | if (isFF) return 'FireFox'
21 | if (isSafari) return 'Safari'
22 | if (isOpera) return 'Opera'
23 | if (isEdge) return 'Edge'
24 | if (isIE) {
25 | const reIE = new RegExp('MSIE (\\d+\\.\\d+);')
26 | reIE.test(userAgent)
27 | const fIEVersion = parseFloat(RegExp['$1'])
28 | if (fIEVersion) return 'IE' + fIEVersion
29 | return 'IE'
30 | }
31 | return '未知'
32 | }
33 |
--------------------------------------------------------------------------------
/server/utils/getIp.js:
--------------------------------------------------------------------------------
1 | const getIp = function(req) {
2 | let ip = req.get('X-Real-IP') || req.get('X-Forwarded-For') || req.ip
3 | if (ip.split(',').length > 0) {
4 | ip = ip.split(',')[0]
5 | }
6 | return ip
7 | }
8 |
9 | module.exports = getIp
10 |
--------------------------------------------------------------------------------
/server/utils/getOS.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @desc 获取当前操作系统
3 | * @returns
4 | */
5 |
6 | module.exports = function getOS(userAgent) {
7 | if (userAgent.indexOf('Window') > 0) {
8 | return 'Windows'
9 | } else if (userAgent.indexOf('Mac OS X') > 0) {
10 | return 'MacOS'
11 | } else if (userAgent.indexOf('Linux') > 0) {
12 | return 'Linux'
13 | } else {
14 | return '未知'
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/server/utils/highLimitApis.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @desc 限制高频访问的api名单
3 | * 1. 时间窗口 10s
4 | * 2. 限制次数 3
5 | * @author justJokee
6 | */
7 |
8 | exports.HEIGHLIMIT = 3
9 | exports.HEIGHLIMITTL = 1000 * 10
10 |
11 | exports.highLimitApis = ['/api/front/comments/save', '/api/front/messageBoard/save']
12 |
--------------------------------------------------------------------------------
/server/utils/reviseTime.js:
--------------------------------------------------------------------------------
1 | //time为Date.now()
2 | const reviseTime = function(time){
3 | //new Data(str)会把时间转换为本地时间,虽然传入的time参数为UTC标准时间,但是在调用方法时,new Data
4 | //应该在内部转换为了本地时间,一开始在time中加上了1000*60*60*8,小时数会比本地时间多了8小时。
5 | //服务器若设置在国外,则应当换算时区
6 | let localStamp = time
7 | let localTime = new Date(localStamp),
8 | year = localTime.getFullYear(),
9 | month = localTime.getMonth()+1,
10 | day = localTime.getDate(),
11 | hours = localTime.getHours(),
12 | minutes = localTime.getMinutes(),
13 | finTime
14 | for(let i = 0;i < 9;i++){
15 | if(i === minutes){
16 | minutes = "0" + minutes
17 | }
18 | }
19 | finTime = year + "年" + month + "月" + day + "日" + hours + "时" + minutes + "分"
20 | return finTime
21 | }
22 |
23 | module.exports = reviseTime
--------------------------------------------------------------------------------
/server/utils/schedule.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @desc 定时任务 - 爬取豆瓣电影等
3 | * @author justJokee
4 | */
5 |
6 | const schedule = require('node-schedule')
7 | const DoubanSpider = require('douban-spider-v')
8 | const fs = require('fs-extra')
9 | const path = require('path')
10 | const { doubanConfig } = require('../db/secret')
11 | // 确保文件目录存在
12 | fs.ensureDirSync(path.join(__dirname, '../files/movies/collect'))
13 | fs.ensureDirSync(path.join(__dirname, '../files/movies/wish'))
14 | fs.ensureDirSync(path.join(__dirname, '../files/movies/do'))
15 |
16 | const moviesPath = {
17 | getMovieCollect: path.join(__dirname, '../files/movies/collect'),
18 | getMovieWish: path.join(__dirname, '../files/movies/wish'),
19 | getMovieDo: path.join(__dirname, '../files/movies/do')
20 | }
21 |
22 | let cache = {
23 | getMovieCollect: [],
24 | getMovieWish: [],
25 | getMovieDo: []
26 | }
27 | const douban = new DoubanSpider({
28 | uid: doubanConfig.uid
29 | })
30 |
31 | function startSchedule() {
32 | // 每天凌晨1点进行爬取
33 | schedule.scheduleJob('0 0 1 * * *', async () => {
34 | console.log('定时任务触发 -->>>>>')
35 | getMovies()
36 | })
37 | }
38 | async function getMovies() {
39 | await handleMovies('getMovieCollect')
40 | await sleep()
41 | await handleMovies('getMovieWish')
42 | await sleep()
43 | await handleMovies('getMovieDo')
44 | }
45 |
46 | async function handleMovies(method) {
47 | try {
48 | const res = await douban[method]()
49 | cache[method].push(res.data)
50 | console.log(`[${method}]: 第1页爬取成功 -->>>>`)
51 | fs.writeFileSync(`${moviesPath[method]}/pageTotal.txt`, res.page.totalPage + '', 'utf8')
52 | if (res.page.totalPage > 1) {
53 | // 保存总页码数
54 |
55 | for (let i = 2; i <= res.page.totalPage; i++) {
56 | // 爬取速度 30s/1页,避免触发反爬机制
57 | await sleep()
58 | const res = await douban[method](i)
59 | cache[method].push(res.data)
60 | console.log(`[${method}]: 第${i}页爬取成功 -->>>>`)
61 | }
62 | }
63 | // 写入json文件
64 | cache[method].forEach((doc, index) => {
65 | fs.ensureDirSync(moviesPath[method])
66 | fs.writeFileSync(`${moviesPath[method]}/${index + 1}.json`, JSON.stringify(doc), 'utf8')
67 | })
68 | // 释放空间
69 | cache[method] = []
70 | } catch (e) {
71 | console.log('爬虫解析错误 -->>>>', e)
72 | cache = {
73 | getMovieCollect: [],
74 | getMovieWish: [],
75 | getMovieDo: []
76 | }
77 | }
78 | }
79 | async function sleep(ms = 1000 * 30) {
80 | await new Promise((resolve) => setTimeout(resolve, ms))
81 | }
82 | exports.startSchedule = startSchedule
83 |
--------------------------------------------------------------------------------
/server/utils/verify.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @desc jwt verify
3 | * @author justJokee
4 | */
5 | const jwt = require('jsonwebtoken')
6 | const { userSecret } = require('../db/secret')
7 |
8 | module.exports = async function verify(token, cb) {
9 | const secret = `${userSecret.salt}`
10 | jwt.verify(token, secret, (err) => {
11 | cb(err)
12 | })
13 | }
14 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
66 |
71 |
--------------------------------------------------------------------------------
/src/api/article.js:
--------------------------------------------------------------------------------
1 | import http from '@/http/'
2 | export default {
3 | // 获取文章列表
4 | getArticles(payload) {
5 | return http.get('/api/front/article/gets', payload).then((data) => {
6 | return data
7 | })
8 | },
9 | // 获取文章详情
10 | getArticle(payload) {
11 | return http.get('/api/front/article/detail', payload).then((data) => {
12 | return data
13 | })
14 | },
15 | // 获取文章评论
16 | getArticleComments(payload) {
17 | return http.get('/api/front/comments/get', payload).then((data) => {
18 | return data
19 | })
20 | },
21 | // 获取上一篇文章和下一篇文章
22 | getPrevnextArticle(payload) {
23 | return http.get('/api/front/article/prevnext', payload).then((data) => {
24 | return data
25 | })
26 | },
27 | // 文章搜索
28 | searchArticle(payload) {
29 | return http.get('/api/front/article/search', payload).then((data) => {
30 | return data
31 | })
32 | },
33 | // 文章归档
34 | getArchives(payload) {
35 | return http.get('/api/front/article/archives', payload).then((data) => {
36 | return data
37 | })
38 | },
39 | // 文章归档
40 | likeArticle(payload) {
41 | return http.patch('/api/front/article/like', payload).then((data) => {
42 | return data
43 | })
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/api/category.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @desc 分类
3 | * @author Justjokee
4 | */
5 | import http from '@/http/'
6 | export default {
7 | // 获取分类
8 | getCategory(payload) {
9 | return http.get('/api/front/category/get', payload).then((data) => {
10 | return data
11 | })
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/api/comments.js:
--------------------------------------------------------------------------------
1 | import http from '@/http/'
2 | export default {
3 | // 获取文章评论
4 | getArticleComments(payload) {
5 | return http.get('/api/front/comments/get', payload).then((data) => {
6 | return data
7 | })
8 | },
9 | // 点赞留言
10 | likeArticleComment(payload) {
11 | return http.patch('/api/front/comments/like', payload).then((data) => {
12 | return data
13 | })
14 | },
15 | // 发表文章评论 /api/front/comments/save
16 | saveArticleComment(payload) {
17 | return http.post('/api/front/comments/save', payload).then((data) => {
18 | return data
19 | })
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/api/entertainment.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @desc 娱乐
3 | * @author Justjokee
4 | */
5 | import http from '@/http/'
6 | export default {
7 | // 获取电影、电视剧等影视记录
8 | getMovies(payload) {
9 | return http.get('/api/front/douban/get', payload).then((data) => {
10 | return data
11 | })
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/api/index.js:
--------------------------------------------------------------------------------
1 | import article from './article'
2 | import visitor from './visitor'
3 | import comments from './comments'
4 | import messageBoard from './messageBoard'
5 | import category from './category'
6 | import tags from './tags'
7 | import entertainment from './entertainment'
8 | import qiniu from './qiniu'
9 |
10 | export default {
11 | ...article,
12 | ...visitor,
13 | ...comments,
14 | ...messageBoard,
15 | ...category,
16 | ...tags,
17 | ...entertainment,
18 | ...qiniu
19 | }
20 |
--------------------------------------------------------------------------------
/src/api/messageBoard.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @desc 留言板
3 | * @author Justjokee
4 | */
5 | import http from '@/http/'
6 | export default {
7 | // 获取留言列表
8 | getMessageBoard(payload) {
9 | return http.get('/api/front/messageBoard/gets', payload).then((data) => {
10 | return data
11 | })
12 | },
13 | // 存储留言
14 | saveMessageBoard(payload) {
15 | return http.post('/api/front/messageBoard/save', payload).then((data) => {
16 | return data
17 | })
18 | },
19 | // 点赞留言
20 | likeMessageBoard(payload) {
21 | return http.patch('/api/front/messageBoard/like', payload).then((data) => {
22 | return data
23 | })
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/api/qiniu.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @desc 获取七牛云token
3 | * @author Justjokee
4 | */
5 |
6 | import http from '@/http/'
7 | export default {
8 | // 获取文章所有的tag
9 | getQiniuToken(payload) {
10 | return http.get('/api/front/qiniu/getToken', payload).then((data) => {
11 | return data
12 | })
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/api/tags.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @desc 获取文章标签
3 | * @author Justjokee
4 | */
5 |
6 | import http from '@/http/'
7 | export default {
8 | // 获取文章所有的tag
9 | getTags(payload) {
10 | return http.get('/api/front/tags/count', payload).then((data) => {
11 | return data
12 | })
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/api/visitor.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @desc 访客信息
3 | * @author Justjokee
4 | */
5 |
6 | import http from '@/http/'
7 | export default {
8 | // 存储访客信息
9 | saveVisitor(payload) {
10 | return http.post('/api/front/visitor/save', payload).then((data) => {
11 | return data
12 | })
13 | },
14 | isExistedVisitor(payload) {
15 | return http.post('/api/front/visitor/existed', payload).then((data) => {
16 | return data
17 | })
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/app.js:
--------------------------------------------------------------------------------
1 | // The Vue build version to load with the `import` command
2 | // (runtime-only or standalone) has been set in webpack.base.conf with an alias.
3 | import Vue from 'vue'
4 | // eslint-disable-next-line no-unused-vars
5 | import loadEle from '@/utils/loadEle'
6 | import App from './App.vue'
7 | import moment from 'moment'
8 | import api from '@/api/'
9 | import empty from '@/components/empty'
10 | import { createRouter } from './router'
11 | import { createStore } from './store'
12 | import { sync } from 'vuex-router-sync'
13 |
14 | import mergeAsyncData from '@/mixins/mergeAsyncData'
15 | // Vue.config.productionTip = false
16 | Vue.filter('formatDate', (val) => {
17 | return moment(val).format('YYYY-MM-DD HH:mm')
18 | })
19 | Vue.mixin(mergeAsyncData)
20 | Vue.prototype.$moment = moment
21 | Vue.prototype.$api = api
22 | Vue.component('empty', empty)
23 | Vue.config.devtools = true
24 | /* eslint-disable no-new */
25 | export function createApp() {
26 | const router = createRouter()
27 | const store = createStore()
28 | sync(store, router)
29 | const app = new Vue({
30 | router,
31 | store,
32 | render: (h) => h(App)
33 | })
34 |
35 | return { app, router, store }
36 | }
37 |
--------------------------------------------------------------------------------
/src/assets/css/prism.css:
--------------------------------------------------------------------------------
1 | /* PrismJS 1.27.0
2 | https://prismjs.com/download.html#themes=prism-tomorrow&languages=markup+css+clike+javascript+bash+http+java+js-extras+pug+pure+python+r+jsx+tsx+ruby+rust+sass+scss+sql+typescript&plugins=line-numbers+toolbar+copy-to-clipboard */
3 | code[class*=language-],pre[class*=language-]{color:#ccc;background:0 0;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#2d2d2d}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.block-comment,.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#999}.token.punctuation{color:#ccc}.token.attr-name,.token.deleted,.token.namespace,.token.tag{color:#e2777a}.token.function-name{color:#6196cc}.token.boolean,.token.function,.token.number{color:#f08d49}.token.class-name,.token.constant,.token.property,.token.symbol{color:#f8c555}.token.atrule,.token.builtin,.token.important,.token.keyword,.token.selector{color:#cc99cd}.token.attr-value,.token.char,.token.regex,.token.string,.token.variable{color:#7ec699}.token.entity,.token.operator,.token.url{color:#67cdcc}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.token.inserted{color:green}
4 | pre[class*=language-].line-numbers{position:relative;padding-left:3.8em;counter-reset:linenumber}pre[class*=language-].line-numbers>code{position:relative;white-space:inherit}.line-numbers .line-numbers-rows{position:absolute;pointer-events:none;top:0;font-size:100%;left:-3.8em;width:3em;letter-spacing:-1px;border-right:1px solid #999;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.line-numbers-rows>span{display:block;counter-increment:linenumber}.line-numbers-rows>span:before{content:counter(linenumber);color:#999;display:block;padding-right:.8em;text-align:right}
5 | div.code-toolbar{position:relative}div.code-toolbar>.toolbar{position:absolute;z-index:10;top:.3em;right:.2em;transition:opacity .3s ease-in-out;opacity:0}div.code-toolbar:hover>.toolbar{opacity:1}div.code-toolbar:focus-within>.toolbar{opacity:1}div.code-toolbar>.toolbar>.toolbar-item{display:inline-block}div.code-toolbar>.toolbar>.toolbar-item>a{cursor:pointer}div.code-toolbar>.toolbar>.toolbar-item>button{background:0 0;border:0;color:inherit;font:inherit;line-height:normal;overflow:visible;padding:0;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none}div.code-toolbar>.toolbar>.toolbar-item>a,div.code-toolbar>.toolbar>.toolbar-item>button,div.code-toolbar>.toolbar>.toolbar-item>span{color:#bbb;font-size:.8em;padding:0 .5em;background:#f5f2f0;background:rgba(224,224,224,.2);box-shadow:0 2px 0 0 rgba(0,0,0,.2);border-radius:.5em}div.code-toolbar>.toolbar>.toolbar-item>a:focus,div.code-toolbar>.toolbar>.toolbar-item>a:hover,div.code-toolbar>.toolbar>.toolbar-item>button:focus,div.code-toolbar>.toolbar>.toolbar-item>button:hover,div.code-toolbar>.toolbar>.toolbar-item>span:focus,div.code-toolbar>.toolbar>.toolbar-item>span:hover{color:inherit;text-decoration:none}
6 |
--------------------------------------------------------------------------------
/src/assets/font/sf-arch/SF Arch Rival Extended Bold.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: "SF Arch Rival Extended Bold";
3 | src: url("SF Arch Rival Extended Bold.eot"); /* IE9 */
4 | src: url("SF Arch Rival Extended Bold.eot?#iefix") format("embedded-opentype"), /* IE6-IE8 */
5 |
6 | url("SF Arch Rival Extended Bold.woff") format("woff"), /* chrome、firefox */
7 | url("SF Arch Rival Extended Bold.ttf") format("truetype"), /* chrome、firefox、opera、Safari, Android, iOS 4.2+ */
8 |
9 | url("SF Arch Rival Extended Bold.svg#SF Arch Rival Extended Bold") format("svg"); /* iOS 4.1- */
10 | font-style: normal;
11 | font-weight: normal;
12 | }
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/assets/font/sf-arch/SF Arch Rival Extended Bold.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justJokee/vue-ssr-blog/f944768f8a87f10841b121fecd3055ca876579ad/src/assets/font/sf-arch/SF Arch Rival Extended Bold.eot
--------------------------------------------------------------------------------
/src/assets/font/sf-arch/SF Arch Rival Extended Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justJokee/vue-ssr-blog/f944768f8a87f10841b121fecd3055ca876579ad/src/assets/font/sf-arch/SF Arch Rival Extended Bold.ttf
--------------------------------------------------------------------------------
/src/assets/font/sf-arch/SF Arch Rival Extended Bold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justJokee/vue-ssr-blog/f944768f8a87f10841b121fecd3055ca876579ad/src/assets/font/sf-arch/SF Arch Rival Extended Bold.woff
--------------------------------------------------------------------------------
/src/assets/img/github.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justJokee/vue-ssr-blog/f944768f8a87f10841b121fecd3055ca876579ad/src/assets/img/github.png
--------------------------------------------------------------------------------
/src/assets/img/icp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justJokee/vue-ssr-blog/f944768f8a87f10841b121fecd3055ca876579ad/src/assets/img/icp.png
--------------------------------------------------------------------------------
/src/assets/img/loading.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justJokee/vue-ssr-blog/f944768f8a87f10841b121fecd3055ca876579ad/src/assets/img/loading.gif
--------------------------------------------------------------------------------
/src/assets/img/qq.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justJokee/vue-ssr-blog/f944768f8a87f10841b121fecd3055ca876579ad/src/assets/img/qq.png
--------------------------------------------------------------------------------
/src/assets/img/share.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justJokee/vue-ssr-blog/f944768f8a87f10841b121fecd3055ca876579ad/src/assets/img/share.png
--------------------------------------------------------------------------------
/src/components/base/miss.vue:
--------------------------------------------------------------------------------
1 |
2 | 网页走丢了
3 |
4 |
--------------------------------------------------------------------------------
/src/components/dot/index.vue:
--------------------------------------------------------------------------------
1 |
2 | @desc: 点状形状包裹器
3 | @author: justJokee
4 |
5 |
6 |
7 |
8 |
9 |
10 |
18 |
43 |
--------------------------------------------------------------------------------
/src/components/emoji/index.vue:
--------------------------------------------------------------------------------
1 |
2 | @desc: 留言、文章评论展示
3 | @author: justJokee
4 |
5 |
6 |
7 |
14 |
15 |
16 |
17 |
18 | {{ emoji }}
19 |
20 |
21 |
22 |
23 | 😀
24 |
25 |
26 |
27 |
80 |
98 |
--------------------------------------------------------------------------------
/src/components/empty/index.vue:
--------------------------------------------------------------------------------
1 |
2 | @desc: 无数据占位符
3 | @author: justJokee
4 |
5 |
6 |
7 | {{ text }}
8 |
9 |
10 |
24 |
38 |
--------------------------------------------------------------------------------
/src/components/note/index.vue:
--------------------------------------------------------------------------------
1 |
2 | @desc: note包裹容器
3 | @author: justJokee
4 |
5 |
6 |
12 |
13 |
21 |
40 |
--------------------------------------------------------------------------------
/src/components/rating/index.vue:
--------------------------------------------------------------------------------
1 |
2 | @desc: 评分
3 | @author: justJokee
4 |
5 |
6 |
7 |
8 |
9 |
10 |
37 |
47 |
--------------------------------------------------------------------------------
/src/components/splitLine/index.vue:
--------------------------------------------------------------------------------
1 |
2 | @desc: 分割线
3 | @author: justJokee
4 |
5 |
6 |
7 |
8 |
9 |
10 |
18 |
51 |
--------------------------------------------------------------------------------
/src/entry-client.js:
--------------------------------------------------------------------------------
1 | import { createApp } from './app'
2 | import { storage } from '@/utils/storage'
3 | import R_O_P from 'resize-observer-polyfill'
4 | import Vue from 'vue'
5 | import VueLazyload from '@/utils/lazyLoad'
6 | const loading = require('@/assets/img/loading.gif')
7 | if (!window.ResizeObserver) {
8 | window.ResizeObserver = R_O_P
9 | }
10 | // eslint-disable-next-line no-undef
11 | Vue.use(VueLazyload, {
12 | loading: loading
13 | })
14 |
15 | Vue.mixin({
16 | beforeRouteUpdate(to, from, next) {
17 | const { asyncData } = this.$options
18 | if (asyncData) {
19 | asyncData({
20 | store: this.$store,
21 | route: to
22 | })
23 | .then((res) => {
24 | Object.assign(this.$data, res)
25 | next()
26 | })
27 | .catch(next)
28 | } else {
29 | next()
30 | }
31 | }
32 | })
33 |
34 | const { app, router, store } = createApp()
35 | Prism.plugins.toolbar.registerButton('macostyle', function () {
36 | const content = document.createElement('div')
37 | content.setAttribute('class', 'toolbar-item__content')
38 | content.innerHTML = ''
39 | return content
40 | })
41 | // 将服务端渲染时的状态写入vuex中
42 | if (window.__INITIAL_STATE__) {
43 | store.replaceState(window.__INITIAL_STATE__.state)
44 | }
45 | // 同步访客登录信息
46 | if (storage.getVisitor()) store.commit('setVisitor', storage.getVisitor())
47 | router.onReady(() => {
48 | router.beforeResolve(async (to, from, next) => {
49 | const matched = router.getMatchedComponents(to)
50 | const prevMatched = router.getMatchedComponents(from)
51 | // 我们只关心之前没有渲染的组件
52 | // 所以我们对比它们,找出两个匹配列表的差异组件
53 | let diffed = false
54 | const activated = matched.filter((c, i) => {
55 | return diffed || (diffed = prevMatched[i] !== c)
56 | })
57 | if (!activated.length) {
58 | return next()
59 | }
60 | try {
61 | // 这里如果有加载指示器(loading indicator),就触发
62 | await Promise.all(
63 | activated.map(async (Component) => {
64 | if (Component.asyncData) {
65 | const res = await Component.asyncData({ store, route: to })
66 | Component.__COMPONENT_ASYNCDATA__ = res || {}
67 | }
68 | })
69 | )
70 | // 停止加载指示器(loading indicator)
71 | next()
72 | } catch (e) {
73 | console.error(`[entry-client]: async data fetch error -> ${e}`)
74 | next(e)
75 | }
76 | })
77 |
78 | app.$mount('#app')
79 | })
80 |
--------------------------------------------------------------------------------
/src/entry-server.js:
--------------------------------------------------------------------------------
1 | import { createApp } from './app'
2 |
3 | export default (context) => {
4 | const { app, router, store } = createApp()
5 | const meta = app.$meta()
6 | return new Promise((resolve, reject) => {
7 | router.push(context.url)
8 | context.meta = meta
9 | router.onReady(async () => {
10 | const matchedComponents = app.$router.getMatchedComponents()
11 | if (!matchedComponents.length) {
12 | return reject({ code: 404 })
13 | }
14 | // 对所有匹配的路由组件调用 `asyncData()`
15 | try {
16 | const componentRes = await Promise.all(
17 | matchedComponents.map(async (Component) => {
18 | if (Component.asyncData) {
19 | const res = await Component.asyncData({
20 | store,
21 | route: router.currentRoute,
22 | isServer: true,
23 | _ip: context._ip
24 | })
25 | return { res, Component }
26 | }
27 | })
28 | )
29 | const __COMPONENT_ASYNCDATA__ = componentRes.map((eRes) => {
30 | if (eRes) {
31 | const { res, Component } = eRes
32 | // 将路由组件的 asyncData 返回值挂载到组件实例的构造项上
33 | // 用于 data & asyncData 的合并策略
34 | Component.__COMPONENT_ASYNCDATA__ = res
35 | return res || {}
36 | }
37 | })
38 |
39 | // 在所有预取钩子(preFetch hook) resolve 后,
40 | // 我们的 store 现在已经填充入渲染应用程序所需的状态。
41 | // 当我们将状态附加到上下文,
42 | // 并且 `template` 选项用于 renderer 时,
43 | // 状态将自动序列化为 `window.__INITIAL_STATE__`,并注入 HTML。
44 | context.state = {
45 | state: store.state,
46 | __COMPONENT_ASYNCDATA__
47 | }
48 | resolve(app)
49 | } catch (e) {
50 | console.error(`[entry-server]: async data fetch error -> ${e}`)
51 | reject(e)
52 | }
53 | }, reject)
54 | })
55 | }
56 |
--------------------------------------------------------------------------------
/src/http/index.js:
--------------------------------------------------------------------------------
1 | const axios = require('axios')
2 | const qs = require('qs')
3 | import { errorCode } from '@/utils/errorCode'
4 | import { Message } from 'element-ui'
5 |
6 | axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8'
7 | //response拦截器
8 | axios.interceptors.response.use(
9 | (config) => {
10 | return config
11 | },
12 | (error) => {
13 | let errRes = JSON.parse(JSON.stringify(error))
14 | if (errRes && errRes.response && errRes.response.status === 500) {
15 | _message('warning', '服务器错误')
16 | }
17 | return Promise.reject(error)
18 | }
19 | )
20 | function ajax(type, url, options) {
21 | return new Promise((resolve, reject) => {
22 | axios({
23 | method: type,
24 | url: url,
25 | baseURL: process.env.BASE_URL,
26 | params: type === 'get' ? options : null,
27 | data: type !== 'get' ? qs.stringify(options) : null
28 | })
29 | .then((res) => {
30 | if (res.status === 200) {
31 | errorCode(res.data.status)
32 | resolve(res.data)
33 | } else {
34 | reject('request error in ' + url)
35 | }
36 | })
37 | .catch((err) => {
38 | console.error(err, url)
39 | reject(err)
40 | })
41 | })
42 | }
43 | const config = {
44 | get(url, options) {
45 | return ajax('get', url, options)
46 | },
47 | post(url, options) {
48 | return ajax('post', url, options)
49 | },
50 | patch(url, options) {
51 | return ajax('patch', url, options)
52 | },
53 | put(url, options) {
54 | return ajax('put', url, options)
55 | },
56 | delete(url, options) {
57 | return ajax('delete', url, options)
58 | }
59 | }
60 |
61 | function _message(t, m) {
62 | Message({
63 | type: t,
64 | message: m,
65 | center: true
66 | })
67 | }
68 |
69 | export default config
70 |
--------------------------------------------------------------------------------
/src/index.template.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Marco's Blog
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/src/mixins/mergeAsyncData.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @desc data & asyncData合并策略
3 | * @author justJokee
4 | */
5 | export default {
6 | created() {
7 | // client 端做同步
8 | if (
9 | !this.$isServer &&
10 | window.__INITIAL_STATE__ &&
11 | window.__INITIAL_STATE__.__COMPONENT_ASYNCDATA__ &&
12 | this.$router &&
13 | this.$router.getMatchedComponents().length
14 | ) {
15 | const matcheComps = this.$router.getMatchedComponents()
16 | matcheComps.forEach((ec, i) => (ec.__COMPONENT_ASYNCDATA__ = window.__INITIAL_STATE__.__COMPONENT_ASYNCDATA__[i]))
17 | window.__INITIAL_STATE__.__COMPONENT_ASYNCDATA__ = null
18 | }
19 | // 保证client端在开发模式下的热更新正常
20 | if (!this.$isServer) HMR(this)
21 | // server 端合并策略 & client 端的首次合并 & client 端接管后的合并
22 | if (this.constructor && this.constructor.extendOptions && this.constructor.extendOptions.__COMPONENT_ASYNCDATA__) {
23 | const data = this.constructor.extendOptions.__COMPONENT_ASYNCDATA__
24 | Object.assign(this.$data, data)
25 | this.constructor.extendOptions.__COMPONENT_ASYNCDATA__ = null
26 | if (process.env.NODE_ENV === 'development') console.log('[data merge]: merge asyncData success')
27 | }
28 | }
29 | }
30 |
31 | async function HMR(vm) {
32 | if (
33 | process.env.NODE_ENV === 'development' &&
34 | vm.constructor &&
35 | vm.constructor.extendOptions &&
36 | vm.constructor.extendOptions.asyncData &&
37 | !vm.constructor.extendOptions.__COMPONENT_ASYNCDATA__
38 | ) {
39 | const matched = vm.$router.getMatchedComponents()
40 | await Promise.all(
41 | matched.map(async (Component) => {
42 | if (Component.asyncData) {
43 | const res = await Component.asyncData({ store: vm.$store, route: vm.$route })
44 | Object.assign(vm.$data, res)
45 | }
46 | })
47 | )
48 | }
49 | return
50 | }
51 |
--------------------------------------------------------------------------------
/src/router/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Router from 'vue-router'
3 | import Meta from 'vue-meta'
4 | const miss = () => import('@/components/base/miss')
5 | const home = () => import('@/views/home/')
6 | const articleDetail = () => import('@/views/article/articleDetail')
7 | const messageBoard = () => import('@/views/messageBoard/')
8 | const archives = () => import('@/views/archives/')
9 | const tags = () => import('@/views/tags/')
10 | const articleFilter = () => import('@/views/article-filter/')
11 | const category = () => import('@/views/category/')
12 | const movies = () => import('@/views/movies/')
13 |
14 | Vue.use(Router)
15 | Vue.use(Meta)
16 | // 避免重复点击相同路由 报错问题
17 | const originalPush = Router.prototype.push
18 | Router.prototype.push = function push(location) {
19 | return originalPush.call(this, location).catch((err) => err)
20 | }
21 | export function createRouter() {
22 | return new Router({
23 | mode: 'history',
24 | routes: [
25 | {
26 | path: '*',
27 | name: 'miss',
28 | component: miss
29 | },
30 | {
31 | path: '/',
32 | name: 'home',
33 | component: home
34 | },
35 | {
36 | path: '/app/article/:id',
37 | name: 'articleDetail',
38 | component: articleDetail
39 | },
40 | {
41 | path: '/app/messageBoard',
42 | name: 'messageBoard',
43 | component: messageBoard
44 | },
45 | {
46 | path: '/app/archives',
47 | name: 'archives',
48 | component: archives
49 | },
50 | {
51 | path: '/app/tags',
52 | name: 'tags',
53 | component: tags
54 | },
55 | {
56 | path: '/app/category',
57 | name: 'category',
58 | component: category
59 | },
60 | {
61 | path: '/app/articles/:type/:param',
62 | name: 'articleFilter',
63 | component: articleFilter
64 | },
65 | {
66 | path: '/app/movies',
67 | name: 'movies',
68 | component: movies
69 | }
70 | ],
71 | scrollBehavior(to, from, savedPosition) {
72 | if (to.hash || to.query.anchor) return false
73 | if (savedPosition) {
74 | return savedPosition
75 | } else {
76 | return { x: 0, y: 0 }
77 | }
78 | }
79 | })
80 | }
81 |
--------------------------------------------------------------------------------
/src/store/actions.js:
--------------------------------------------------------------------------------
1 | export default {}
2 |
--------------------------------------------------------------------------------
/src/store/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Vuex from 'vuex'
3 | import actions from './actions'
4 | import mutations from './mutations'
5 |
6 | Vue.use(Vuex)
7 | export function createStore() {
8 | return new Vuex.Store({
9 | state: {
10 | activeCatalog: '',
11 | rollBack: false,
12 | // 访客信息
13 | visitorInfo: {},
14 | // 文章目录树
15 | catalogs: [],
16 | // 文章归档 按月统计
17 | archives: [],
18 | // 文章分类
19 | category: [],
20 | // 文章标签
21 | tags: [],
22 | // 最新评论
23 | newComments: [],
24 | // 最新文章
25 | newArticles: [],
26 | totals: {
27 | article: 0,
28 | tag: 0,
29 | category: 0
30 | }
31 | },
32 | mutations,
33 | actions
34 | })
35 | }
36 |
--------------------------------------------------------------------------------
/src/store/mutations.js:
--------------------------------------------------------------------------------
1 | export default {
2 | setCatalogs(state, catalogs) {
3 | state.catalogs = catalogs
4 | },
5 | setArchives(state, archives) {
6 | state.archives = archives
7 | },
8 | setCategory(state, category) {
9 | state.category = category
10 | },
11 | setTags(state, tags) {
12 | state.tags = tags
13 | },
14 | setNewComments(state, newComments) {
15 | state.newComments = newComments
16 | },
17 | setNewArticles(state, newArticles) {
18 | state.newArticles = newArticles
19 | },
20 | setActiveCatalog(state, id) {
21 | state.activeCatalog = id
22 | },
23 | setRollBack(state, rollBack) {
24 | state.rollBack = rollBack
25 | },
26 | setVisitor(state, info) {
27 | state.visitorInfo = info
28 | },
29 | setTotals(state, kv) {
30 | state.totals[kv.key] = kv.value
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/style/function.scss:
--------------------------------------------------------------------------------
1 | /**
2 | * @desc 函数
3 | */
4 |
5 | @function merge($maps...) {
6 | $copy: ();
7 | @each $mv in $maps {
8 | @each $k, $v in $mv {
9 | $copy: map-merge(
10 | $copy,
11 | (
12 | $k: $v
13 | )
14 | );
15 | }
16 | }
17 | @return $copy;
18 | }
19 |
--------------------------------------------------------------------------------
/src/style/global.scss:
--------------------------------------------------------------------------------
1 | // 文字
2 |
3 | $font12: 12px;
4 | $font14: 14px;
5 | $font16: 16px;
6 | $font18: 18px;
7 | $font56: 56px;
8 |
9 | // 字体文件
10 | @font-face {
11 | font-family: 'sf-arch';
12 | src: url('~@/assets/font/sf-arch/SF Arch Rival Extended Bold.woff') format('woff'),
13 | url('~@/assets/font/sf-arch/SF Arch Rival Extended Bold.ttf') format('truetype');
14 | }
15 |
--------------------------------------------------------------------------------
/src/style/index.scss:
--------------------------------------------------------------------------------
1 | @import './theme/map.scss';
2 | @import './mixins.scss';
3 | @import './global.scss';
4 |
--------------------------------------------------------------------------------
/src/style/mixins.scss:
--------------------------------------------------------------------------------
1 | // 全局混入宏
2 |
3 | @mixin flex {
4 | display: flex;
5 | }
6 | @mixin flex-box-space {
7 | display: flex;
8 | align-items: center;
9 | justify-content: space-between;
10 | }
11 | @mixin flex-box-center {
12 | display: flex;
13 | align-items: center;
14 | justify-content: center;
15 | }
16 | @mixin flex-box-wrap-space {
17 | display: flex;
18 | flex-wrap: wrap;
19 | justify-content: space-between;
20 | }
21 | @mixin flex-box-end-space {
22 | display: flex;
23 | justify-content: flex-end;
24 | }
25 |
26 | @mixin themeify($themes: $themes) {
27 | @each $key, $theme-map in $themes {
28 | $d_theme: if($key==light, null, '.mapblog-theme-#{$key}');
29 | #{$d_theme} & {
30 | $theme-map-copy: () !global;
31 | @each $theme-map-key, $theme-map-value in $theme-map {
32 | $theme-map-copy: map-merge(
33 | $theme-map-copy,
34 | (
35 | $theme-map-key: $theme-map-value
36 | )
37 | ) !global;
38 | }
39 | @content;
40 | $theme-map-copy: null !global;
41 | }
42 | }
43 | }
44 |
45 | @function themed($key) {
46 | @return map-get($theme-map-copy, $key);
47 | }
48 | $break_points: (
49 | xs: 'screen and (max-width: 768px)',
50 | sm: 'screen and (min-width: 769px)',
51 | md: 'screen and (min-width: 992px)',
52 | lg: 'screen and (min-width: 1200px)'
53 | );
54 |
55 | @mixin respond-to($point) {
56 | $query: map-get($break_points, $point);
57 | @media #{$query} {
58 | @content;
59 | }
60 | }
61 | @mixin clamp($num){
62 | display: -webkit-box;
63 | -webkit-line-clamp: $num;
64 | -webkit-box-orient: vertical;
65 | overflow: hidden;
66 | }
67 |
--------------------------------------------------------------------------------
/src/style/reset.scss:
--------------------------------------------------------------------------------
1 | // 全局样式覆盖
2 | @import './index.scss';
3 | body,
4 | button,
5 | input,
6 | p,
7 | div,
8 | section,
9 | article,
10 | td,
11 | th,
12 | span,
13 | textarea,
14 | form,
15 | footer,
16 | header,
17 | nav,
18 | main,
19 | address,
20 | aside,
21 | pre,
22 | ul,
23 | li,
24 | canvas {
25 | margin: 0;
26 | padding: 0;
27 | outline: none;
28 | box-sizing: border-box;
29 | }
30 | body {
31 | font: 400 14px/20px -apple-system, BlinkMacSystemFont, Arial, Helvetica, Tahoma, '华文细黑', 'Microsoft YaHei',
32 | '微软雅黑', sans-serif;
33 |
34 | @include themeify() {
35 | color: themed('color-default');
36 | }
37 | }
38 | li {
39 | list-style: none;
40 | }
41 | a {
42 | text-decoration: none;
43 | @include themeify() {
44 | color: themed('color-default');
45 | }
46 | }
47 | #app {
48 | overflow: hidden;
49 | }
50 | .code-toolbar {
51 | border-radius: 8px;
52 | padding-top: 24px;
53 | > pre {
54 | border-radius: 0 0 8px 8px;
55 | }
56 | > .toolbar {
57 | opacity: 1 !important;
58 | width: 100%;
59 | top: 0px !important;
60 | right: 0px !important;
61 | background: #30343f;
62 | border-radius: 8px 8px 0 0;
63 | .toolbar-item:first-child {
64 | float: right;
65 | margin-right: 12px;
66 | }
67 | .copy-to-clipboard-button {
68 | color: #bbb !important;
69 | cursor: pointer;
70 | background: transparent !important;
71 | }
72 | .copy-to-clipboard-button:hover {
73 | color: #bbb !important;
74 | }
75 | }
76 | .toolbar-item__content {
77 | width: 100%;
78 | height: 32px;
79 | padding: 0 14px;
80 | display: flex;
81 | align-items: center;
82 | span {
83 | display: inline-block;
84 | width: 12px;
85 | height: 12px;
86 | border-radius: 6px;
87 | background: rgb(252, 98, 93);
88 | box-shadow: rgb(253, 188, 64) 20px 0px, rgb(53, 205, 75) 40px 0px;
89 | }
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/src/style/theme/dark/color.scss:
--------------------------------------------------------------------------------
1 | /**
2 | * @desc: 颜色表
3 | */
4 |
5 | $color: ();
6 |
--------------------------------------------------------------------------------
/src/style/theme/dark/font.scss:
--------------------------------------------------------------------------------
1 | /**
2 | * @desc: 字体表 ,切换主题有文字变化时在此文件补充
3 | */
4 |
5 | $font: (
6 | font12: 12px,
7 | font14: 14px
8 | );
9 |
--------------------------------------------------------------------------------
/src/style/theme/dark/index.scss:
--------------------------------------------------------------------------------
1 | // 暗黑色系
2 | @import './color.scss';
3 | @import './font.scss';
4 | @import '~@/style/function.scss';
5 |
6 | $dark: merge($color, $font);
7 |
--------------------------------------------------------------------------------
/src/style/theme/light/color.scss:
--------------------------------------------------------------------------------
1 | /**
2 | * @desc: 颜色表
3 | */
4 |
5 | $color: (
6 | color-ele-primary: #409eff,
7 | color-ele-success: #67c23a,
8 | color-ele-danger: #f56c6c,
9 | color-ele-holder: #c0c4cc,
10 | color-default: #4c4948,
11 | color-navbar: #eee,
12 | color-navbar-rollup-bg: rgba(255, 255, 255, 0.8),
13 | color-navbar-rollup-color: #4c4948,
14 | color-home-article-detail: #858585,
15 | color-title: #fff,
16 | color-avatar-bg: #c0c4cc,
17 | color-avatar-icon: #fff,
18 | color-hr-border: #a4d8fa,
19 | bg-header-mask: rgba(0, 0, 0, 0.5),
20 | color-copyright-border: #eee,
21 | color-copyright-label: #46adf1,
22 | color-comments: #4c4948,
23 | color-list-hover: #40a0ffb4,
24 | color-catalog-active: #03c1b4
25 | );
26 |
--------------------------------------------------------------------------------
/src/style/theme/light/font.scss:
--------------------------------------------------------------------------------
1 | /**
2 | * @desc: 字体表 ,切换主题有文字变化时在此文件补充
3 | */
4 |
5 | $font: (
6 | font12: 12px,
7 | font14: 14px
8 | );
9 |
--------------------------------------------------------------------------------
/src/style/theme/light/index.scss:
--------------------------------------------------------------------------------
1 | // 亮色,默认色系
2 | @import './color.scss';
3 | @import './font.scss';
4 | @import '~@/style/function.scss';
5 |
6 | $light: merge($color, $font);
7 |
--------------------------------------------------------------------------------
/src/style/theme/map.scss:
--------------------------------------------------------------------------------
1 | /**
2 | * @desc: 主题映射表
3 | * @use key即为主题后缀 .mapblog—theme-${key}
4 | */
5 | @import './dark/index.scss';
6 | @import './light/index.scss';
7 |
8 | $themes: (
9 | dark: $dark,
10 | light: $light
11 | );
12 |
--------------------------------------------------------------------------------
/src/utils/cls.js:
--------------------------------------------------------------------------------
1 | export function cls(cls) {
2 | if (document.getElementsByClassName) {
3 | return document.getElementsByClassName(cls)[0]
4 | } else {
5 | let arr = document.getElementsByTagName('*')
6 | let tempArr = []
7 | for (let i = 0; i < arr.length; i++) {
8 | let clsArr = arr[i].className.split(' ')
9 | for (let k = 0; k < clsArr.length; k++) {
10 | if (clsArr[k] === cls) {
11 | tempArr.push(arr[i])
12 | }
13 | }
14 | }
15 | return tempArr[0]
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/utils/errorCode.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @desc 错误码
3 | * @author justJokee
4 | */
5 | import { Message } from 'element-ui'
6 | export function errorCode(code) {
7 | switch (code) {
8 | case 200:
9 | break
10 | case 100:
11 | _message('warning', '昵称已存在')
12 | break
13 | case 101:
14 | _message('warning', '您已经点过赞了 ~')
15 | break
16 | case 102:
17 | _message('warning', '文章不存在或已删除 ~')
18 | break
19 | case 429:
20 | _message('warning', '访问次数超限,请稍后再试 ~')
21 | break
22 | }
23 | }
24 |
25 | function _message(t, m) {
26 | Message({
27 | type: t,
28 | message: m,
29 | center: true
30 | })
31 | }
32 |
--------------------------------------------------------------------------------
/src/utils/generateTree.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @desc 将打平的用层级关系维护的树状数据,组装成树结构
3 | * @param {*} flatList Object[]
4 | * @param {*} options { level: Number} level,维护层级关系的字段
5 | * @used 目前用于自动生成文章的目录结构
6 | * @returns Array
7 | * @author justJokee
8 | */
9 | export function generateTree(flatList, options = { level: 'level' }) {
10 | const result = []
11 | let stack = []
12 | flatList.forEach((item) => {
13 | // 栈为空,则直接入栈
14 | if (!stack.length) {
15 | item.children = []
16 | stack.push(item)
17 | result.push(item)
18 | } else {
19 | // 当前元素级别大于栈底元素,则倒序遍历栈,找到其父级并挂载
20 | if (item[options.level] > stack[0][options.level]) {
21 | stack.reverse().some((el) => {
22 | if (el[options.level] < item[options.level]) {
23 | if (!el.children) el.children = []
24 | el.children.push(item)
25 | return true
26 | }
27 | })
28 | stack.reverse()
29 | stack.push(item)
30 | }
31 | // 完成子树收集
32 | else {
33 | // 清空栈
34 | stack = []
35 | item.children = []
36 | stack.push(item)
37 | result.push(item)
38 | }
39 | }
40 | })
41 | return result
42 | }
43 |
--------------------------------------------------------------------------------
/src/utils/getBrowserInfo.js:
--------------------------------------------------------------------------------
1 | export function getBrowserInfo() {
2 | let agent = navigator.userAgent.toLowerCase(),
3 | reg_ie = /msie [\d.]+;/gi,
4 | reg_ff = /firefox\/[\d.]+/gi,
5 | reg_chrome = /chrome\/[\d.]+/gi,
6 | reg_saf = /safari\/[\d.]+/gi
7 | //IE11以下版本
8 | if (agent.indexOf('msie') > 0) {
9 | return agent.match(reg_ie)
10 | }
11 | //IE11版本中不包括MSIE字段
12 | if (agent.indexOf('trident') > 0 && agent.indexOf('rv') > 0) {
13 | return 'IE ' + agent.match(/rv:(\d+\.\d+)/)[1]
14 | }
15 | //firefox
16 | if (agent.indexOf('firefox') > 0) {
17 | return agent.match(reg_ff)
18 | }
19 | //Chrome
20 | if (agent.indexOf('chrome') > 0) {
21 | return agent.match(reg_chrome)
22 | }
23 | //Safari
24 | if (agent.indexOf('safari') > 0 && agent.indexOf('chrome') < 0) {
25 | return agent.match(reg_saf)
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/utils/getElementTop.js:
--------------------------------------------------------------------------------
1 | export function getElementTop(ele) {
2 | let actualTop = ele.offsetTop
3 | let current = ele.offsetParent
4 | while (current !== null) {
5 | actualTop += current.offsetTop
6 | current = current.offsetParent
7 | }
8 | return actualTop
9 | }
10 |
--------------------------------------------------------------------------------
/src/utils/getRandomCharacter.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @desc 获取随机字符串
3 | * @param {*} digits: Number 指定位数
4 | * @param {*} join: String 分隔字符串
5 | * @returns String
6 | * @auther justJokee
7 | */
8 | export function getRandomCharacter(digits = 4, join = '') {
9 | const words = [
10 | 'A',
11 | 'B',
12 | 'C',
13 | 'D',
14 | 'E',
15 | 'F',
16 | 'G',
17 | 'H',
18 | 'I',
19 | 'J',
20 | 'K',
21 | 'L',
22 | 'M',
23 | 'N',
24 | 'O',
25 | 'P',
26 | 'Q',
27 | 'R',
28 | 'S',
29 | 'T',
30 | 'U',
31 | 'V',
32 | 'W',
33 | 'X',
34 | 'Y',
35 | 'Z'
36 | ]
37 | if (typeof digits !== 'number') throw new Error('参数必须为 number 类型')
38 | return Array(digits)
39 | .fill()
40 | .map(() => words[Math.ceil(Math.random() * 1000) % 26])
41 | .join(join)
42 | }
43 |
--------------------------------------------------------------------------------
/src/utils/getRandomColor.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @desc 获取随机色
3 | * @returns
4 | * @author justJokee
5 | */
6 |
7 | export function getRandomColor() {
8 | const r = Math.floor(Math.random() * 255)
9 | const g = Math.floor(Math.random() * 255)
10 | const b = Math.floor(Math.random() * 255)
11 | const rgb = `rgb(${r},${g},${b})`
12 | return rgb
13 | }
14 |
--------------------------------------------------------------------------------
/src/utils/getScrollTop.js:
--------------------------------------------------------------------------------
1 | export function getScrollTop() {
2 | let scrollTop = 0
3 | if (document.documentElement && document.documentElement.scrollTop) {
4 | scrollTop = document.documentElement.scrollTop
5 | } else if (document.body) {
6 | scrollTop = document.body.scrollTop
7 | }
8 | return scrollTop
9 | }
10 |
--------------------------------------------------------------------------------
/src/utils/getUrlParams.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @desc 获取search & hash路由上的参数
3 | * @param {*} urlStr
4 | * @param {*} field
5 | * @returns string | Object
6 | * @author Justjokee
7 | */
8 | export function getUrlParams(urlStr, field) {
9 | let searchParams = {}
10 | let hashParams = {}
11 | if (window.location.hash) hashParams = this.getSearchParams(window.location.hash)
12 | searchParams = getSearchParams(urlStr)
13 | if (field) return Object.assign(hashParams, searchParams)[field]
14 | return Object.assign(hashParams, searchParams)
15 | }
16 |
17 | function getSearchParams(urlStr) {
18 | let search = urlStr || document.location.search
19 | search = search.split('?')[1]
20 | let params = {}
21 | if (search) {
22 | if (search.lastIndexOf('/') == search.length - 1) search = search.substr(0, search.length - 1)
23 | let eps = search.split('&')
24 | eps.forEach((ep) => {
25 | let k_v = ep.split('=')
26 | params[k_v[0]] = k_v[1]
27 | })
28 | }
29 | return params
30 | }
31 |
--------------------------------------------------------------------------------
/src/utils/loadEle.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import {
3 | Button,
4 | Select,
5 | Tag,
6 | Card,
7 | Dropdown,
8 | DropdownMenu,
9 | DropdownItem,
10 | Pagination,
11 | Input,
12 | Dialog,
13 | Form,
14 | FormItem,
15 | Popover,
16 | Tabs,
17 | TabPane,
18 | Scrollbar,
19 | Message,
20 | Timeline,
21 | TimelineItem,
22 | RadioGroup,
23 | RadioButton,
24 | Menu,
25 | Submenu,
26 | MenuItem,
27 | drawer
28 | } from 'element-ui'
29 | import layout from '@/views/layout/'
30 | Vue.component('layout', layout)
31 | Vue.use(Button)
32 | Vue.use(Select)
33 | Vue.use(Tag)
34 | Vue.use(Card)
35 | Vue.use(Dropdown)
36 | Vue.use(DropdownMenu)
37 | Vue.use(DropdownItem)
38 | Vue.use(Pagination)
39 | Vue.use(Input)
40 | Vue.use(Dialog)
41 | Vue.use(Form)
42 | Vue.use(FormItem)
43 | Vue.use(Popover)
44 | Vue.use(Tabs)
45 | Vue.use(TabPane)
46 | Vue.use(Scrollbar)
47 | Vue.use(Timeline)
48 | Vue.use(TimelineItem)
49 | Vue.use(RadioGroup)
50 | Vue.use(RadioButton)
51 | Vue.use(Menu)
52 | Vue.use(MenuItem)
53 | Vue.use(Submenu)
54 | Vue.use(drawer)
55 | Vue.prototype.$message = Message
56 |
--------------------------------------------------------------------------------
/src/utils/requestAnimation.js:
--------------------------------------------------------------------------------
1 | var requestAnimation = function () {
2 | var lastTime = 0
3 | var vendors = ['ms', 'moz', 'webkit', 'o']
4 | for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
5 | window.requestAnimationFrame = window[vendors[x] + 'RequestAnimationFrame']
6 | window.cancelAnimationFrame =
7 | window[vendors[x] + 'CancelAnimationFrame'] || window[vendors[x] + 'CancelRequestAnimationFrame']
8 | }
9 |
10 | if (!window.requestAnimationFrame) {
11 | window.requestAnimationFrame = function (callback) {
12 | var currTime = new Date().getTime()
13 | var timeToCall = Math.max(0, 16 - (currTime - lastTime))
14 | var id = window.setTimeout(function () {
15 | callback(currTime + timeToCall)
16 | }, timeToCall)
17 | lastTime = currTime + timeToCall
18 | return id
19 | }
20 | }
21 |
22 | if (!window.cancelAnimationFrame) {
23 | window.cancelAnimationFrame = function (id) {
24 | clearTimeout(id)
25 | }
26 | }
27 | }
28 | export { requestAnimation }
29 |
--------------------------------------------------------------------------------
/src/utils/scrollTo.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @desc 窗口滚动动画
3 | * @param {*} pos 需要设置的scrollTop值
4 | * @param {*} duration 动画时长
5 | * @returns
6 | * @author justJokee
7 | */
8 | import { getScrollTop } from '@/utils/getScrollTop'
9 | export default function scrollTo(pos = 0, duration = 250) {
10 | // 计算需要滚动的距离
11 | const distance = pos - getScrollTop()
12 | let scrollY = getScrollTop()
13 | let oldTimestamp = null
14 | if (distance == 0) return
15 |
16 | function step(newTimestamp) {
17 | if (oldTimestamp !== null) {
18 | // 上移
19 | if (distance < 0) {
20 | scrollY -= (Math.abs(distance) * (newTimestamp - oldTimestamp)) / duration
21 | if (scrollY <= pos) return (document.scrollingElement.scrollTop = pos)
22 | }
23 | // 下移
24 | else {
25 | scrollY += (Math.abs(distance) * (newTimestamp - oldTimestamp)) / duration
26 | if (scrollY >= pos) return (document.scrollingElement.scrollTop = pos)
27 | }
28 | document.scrollingElement.scrollTop = scrollY
29 | }
30 | oldTimestamp = newTimestamp
31 | window.requestAnimationFrame(step)
32 | }
33 | window.requestAnimationFrame(step)
34 | }
35 |
--------------------------------------------------------------------------------
/src/utils/siblings.js:
--------------------------------------------------------------------------------
1 | export function siblings(ele) {
2 | let arr_siblings = []
3 | let ele_par = ele.parentNode
4 | let children = ele_par.childNodes
5 | for (let i = 0; i < children.length; i++) {
6 | if (children[i].nodeType == 1 && children[i] !== ele) {
7 | arr_siblings.push(children[i])
8 | }
9 | }
10 | return arr_siblings
11 | }
12 |
--------------------------------------------------------------------------------
/src/utils/storage.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @desc localStorage信息存储
3 | * @author Justjokee
4 | */
5 |
6 | // 访客信息key
7 | const visitorKey = 'v_k_i'
8 |
9 | export const storage = {
10 | getVisitor() {
11 | if (localStorage.getItem(visitorKey)) {
12 | return JSON.parse(localStorage.getItem(visitorKey))
13 | }
14 | },
15 | setVisitor(info) {
16 | localStorage.setItem(visitorKey, JSON.stringify(info))
17 | },
18 | removeVisitor() {
19 | localStorage.removeItem(visitorKey)
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/views/archives/index.vue:
--------------------------------------------------------------------------------
1 |
2 | @desc: 归档
3 | @author: justJokee
4 |
5 |
6 |
7 |
8 |
9 |
10 |
{{ range.year || range.month }}
11 |
12 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | {{ article.title }}
28 |
29 |
30 |
31 |
32 | {{ article.createTime | formatDate }}
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
49 |
50 |
51 |
52 |
53 |
119 |
185 |
--------------------------------------------------------------------------------
/src/views/article-filter/index.vue:
--------------------------------------------------------------------------------
1 |
2 | @desc: 按条件搜索文章列表
3 | @author: justJokee
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
17 |
18 |
19 |
20 |
21 |
22 |
90 |
99 |
--------------------------------------------------------------------------------
/src/views/article/components/copyright.vue:
--------------------------------------------------------------------------------
1 |
2 | @desc: 文章版权信息提示
3 | @author: justJokee
4 |
5 |
6 |
7 |
8 | -
9 | 文章作者:
10 |
11 | {{ author }}
12 |
13 |
14 | -
15 | 文章链接:
16 |
17 | {{ url }}
18 |
19 |
20 | -
21 | 版权声明:
22 |
23 | 本博客原创系列文章遵守
24 | 创意共享3.0许可证
25 | (自由分享-署名-非商用-禁止演绎)
26 |
27 |
28 |
29 |
30 |
31 |
52 |
85 |
--------------------------------------------------------------------------------
/src/views/article/components/prevnext.vue:
--------------------------------------------------------------------------------
1 |
2 | @desc: 文章上/下一篇
3 | @author: justJokee
4 |
5 |
6 |
28 |
29 |
65 |
144 |
--------------------------------------------------------------------------------
/src/views/article/components/share.vue:
--------------------------------------------------------------------------------
1 |
2 | @desc: 文章分享
3 | @author: justJokee
4 |
5 |
6 |
7 |
8 |
9 | {{ tag }}
10 |
11 |
12 |
13 |
18 |
23 |
28 |
29 |
33 |
34 |
35 |
40 |
41 |
42 |
43 |
119 |
185 |
--------------------------------------------------------------------------------
/src/views/category/index.vue:
--------------------------------------------------------------------------------
1 |
2 | @desc: 分类
3 | @author: justJokee
4 |
5 |
6 |
22 |
23 |
69 |
86 |
--------------------------------------------------------------------------------
/src/views/components/article-iterator.vue:
--------------------------------------------------------------------------------
1 |
2 | @desc: 文章列表迭代器
3 | @author: justJokee
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
![]()
12 |
13 |
14 |
15 | {{ article.title }}
16 |
17 |
18 |
19 |
20 | 发表时间 {{ article.createTime | formatDate }}
21 |
22 | |
23 |
24 |
25 | 评论数 {{ article.commentNum }}
26 |
27 | |
28 |
29 |
30 | 点赞 {{ article.likeNum }}
31 |
32 |
33 |
{{ article.abstract }}
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
59 |
148 |
--------------------------------------------------------------------------------
/src/views/components/comments-item.vue:
--------------------------------------------------------------------------------
1 |
2 | @desc: 留言、文章评论展示列表
3 | @author: justJokee
4 |
5 |
6 |
39 |
40 |
72 |
145 |
--------------------------------------------------------------------------------
/src/views/components/comments.vue:
--------------------------------------------------------------------------------
1 |
2 | @desc: 留言、文章评论展示
3 | @author: justJokee
4 |
5 |
6 |
40 |
41 |
76 |
101 |
--------------------------------------------------------------------------------
/src/views/components/search.vue:
--------------------------------------------------------------------------------
1 |
2 | @desc: 文章搜索
3 | @author: justJokee
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
24 |
Oops~ 暂未找到关键词为”{{ keyword }}“的文章
25 |
26 |
27 |
28 |
35 |
36 |
37 |
38 |
39 |
99 |
189 |
--------------------------------------------------------------------------------
/src/views/components/site-introduction.vue:
--------------------------------------------------------------------------------
1 |
2 | @desc: 本站简介
3 | @author: justJokee
4 |
5 |
6 |
7 |
8 |

9 |
10 |
11 |
Marco
12 |
13 | Hi,欢迎来到 Marco's Blog,这是一个使用 vue ssr 开发的,记录学习与生活的个人博客,你可以在
14 | 这里
15 | 了解到关于本站的更多技术细节。
16 |
17 |
18 |
19 |
20 |
文章
21 |
{{ totals.article }}
22 |
23 |
24 |
分类
25 |
{{ totals.category }}
26 |
27 |
28 |
标签
29 |
{{ totals.tag }}
30 |
31 |
32 |
57 |
58 |
59 |
78 |
154 |
--------------------------------------------------------------------------------
/src/views/components/tags-iterator.vue:
--------------------------------------------------------------------------------
1 |
2 | @desc: 标签迭代容器
3 | @author: justJokee
4 |
5 |
6 |
20 |
21 |
67 |
80 |
--------------------------------------------------------------------------------
/src/views/layout/components/header/index.vue:
--------------------------------------------------------------------------------
1 |
2 | @desc: 布局 - 头部组件
3 | @author: justJokee
4 |
5 |
6 |
15 |
16 |
34 |
67 |
--------------------------------------------------------------------------------
/src/views/layout/components/navbar/horizontal-navbar.vue:
--------------------------------------------------------------------------------
1 |
2 | @desc: navbar 适配大屏幕
3 | @author: justJokee
4 |
5 |
6 |
7 |
11 |
15 |
36 |
37 |
50 |
54 |
58 |
59 |
60 |
77 |
135 |
--------------------------------------------------------------------------------
/src/views/layout/components/navbar/vertical-navbar.vue:
--------------------------------------------------------------------------------
1 |
2 | @desc: navbar 适配移动端
3 | @author: justJokee
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
53 |
54 |
55 |
83 |
90 |
--------------------------------------------------------------------------------
/src/views/layout/index.vue:
--------------------------------------------------------------------------------
1 |
2 | @desc: 布局组件
3 | @author: justJokee
4 |
5 |
6 |
7 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
31 |
32 |
33 |
49 |
50 |
51 |
80 |
199 |
--------------------------------------------------------------------------------
/src/views/movies/index.vue:
--------------------------------------------------------------------------------
1 |
2 | @desc: 电影
3 | @author: justJokee
4 |
5 |
6 |
7 |
8 |
9 | 每一帧,都是热爱
10 |
11 |
12 |
13 | 看过
14 | 想看
15 | 在看
16 |
17 |
18 |
19 |
20 |
25 |
26 |
31 |
演员: {{ movie.intro }}
32 |
33 |
评分:
34 |
35 |
暂无评分
36 |
37 |
标记时间:{{ movie.rating.date }}
38 |
39 |
40 |
暂无数据
41 |
42 |
43 |
50 |
51 |
52 |
53 |
54 |
116 |
182 |
--------------------------------------------------------------------------------
/src/views/pannel/components/pannel-archives.vue:
--------------------------------------------------------------------------------
1 |
2 | @desc: 看板 - 文章归档
3 | @author: justJokee
4 |
5 |
6 |
7 |
8 |
9 |
10 | 归档
11 |
12 |
13 |
14 | -
15 | {{ archive.month }}
16 | {{ archive.total }}
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
47 |
71 |
--------------------------------------------------------------------------------
/src/views/pannel/components/pannel-articles.vue:
--------------------------------------------------------------------------------
1 |
2 | @desc: 看板 - 最新文章
3 | @author: justJokee
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | 最新文章
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | {{ article.title }}
25 |
26 |
27 |
{{ article.createTime | formatDate }}
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
52 |
60 |
--------------------------------------------------------------------------------
/src/views/pannel/components/pannel-catalog.vue:
--------------------------------------------------------------------------------
1 |
2 | @desc: 看板 - 文章目录树
3 | @author: justJokee
4 |
5 |
6 |
7 |
8 |
9 |
10 | 目录
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
39 |
54 |
--------------------------------------------------------------------------------
/src/views/pannel/components/pannel-category.vue:
--------------------------------------------------------------------------------
1 |
2 | @desc: 看板 - 文章分类
3 | @author: justJokee
4 |
5 |
6 |
7 |
8 |
9 |
10 | 文章分类
11 |
12 |
13 |
14 | -
15 | {{ item.name }}
16 | {{ item.total }}
17 |
18 |
19 |
20 |
21 |
22 |
23 |
51 |
74 |
--------------------------------------------------------------------------------
/src/views/pannel/components/pannel-comments.vue:
--------------------------------------------------------------------------------
1 |
2 | @desc: 看板 - 最新文章评论
3 | @author: justJokee
4 |
5 |
6 |
37 |
38 |
53 |
73 |
--------------------------------------------------------------------------------
/src/views/pannel/components/pannel-introduction.vue:
--------------------------------------------------------------------------------
1 |
2 | @desc: 看板 - 本站简介
3 | @author: justJokee
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
26 |
39 |
--------------------------------------------------------------------------------
/src/views/pannel/components/pannel-tags.vue:
--------------------------------------------------------------------------------
1 |
2 | @desc: 看板 - 文章标签
3 | @author: justJokee
4 |
5 |
6 |
17 |
18 |
35 |
42 |
--------------------------------------------------------------------------------
/src/views/pannel/components/tree-folder.vue:
--------------------------------------------------------------------------------
1 |
2 | @desc: 布局组件
3 | @author: justJokee
4 |
5 |
6 |
7 | -
8 |
13 |
14 |
16 | {{ catalog.order }}
17 | {{ catalog.order }}.
18 |
19 |
{{ catalog.name }}
20 |
21 |
22 |
27 |
28 | 暂未匹配到目录
29 |
30 |
31 |
64 |
97 |
--------------------------------------------------------------------------------
/src/views/pannel/style/mixins.scss:
--------------------------------------------------------------------------------
1 | @mixin pannel-frame {
2 | .pannel__item-title {
3 | font-size: 16px;
4 |
5 | > [class*='el-icon-'] {
6 | font-size: 18px;
7 | margin-right: 12px;
8 | }
9 | }
10 | .pannel__item-body {
11 | margin-top: 10px;
12 | }
13 | }
14 |
15 | @mixin articles-comments {
16 | @include pannel-frame;
17 | .pannel__item-body {
18 | .body-item {
19 | display: flex;
20 | padding: 8px 0;
21 | }
22 | .body-pic {
23 | flex: 0 0 auto;
24 | width: 60px;
25 | height: 60px;
26 | overflow: hidden;
27 | img {
28 | transition: all ease 0.38s;
29 | width: 100%;
30 | height: 100%;
31 | object-fit: cover;
32 | }
33 | img:hover {
34 | transform: scale(1.2);
35 | }
36 | }
37 | .body-info {
38 | margin-left: 12px;
39 | &__title a {
40 | transition: all 0.38s ease-in-out;
41 | @include clamp(2);
42 | }
43 | &__title:hover a,
44 | &__title:hover {
45 | @include themeify() {
46 | color: themed('color-ele-primary');
47 | }
48 | }
49 | &__name {
50 | @include themeify() {
51 | color: themed('color-home-article-detail');
52 | }
53 | font-size: 12px;
54 | }
55 | &__date {
56 | @include themeify() {
57 | color: themed('color-home-article-detail');
58 | }
59 | font-size: 12px;
60 | padding: 4px 0;
61 | }
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/views/tags/index.vue:
--------------------------------------------------------------------------------
1 |
2 | @desc: 标签
3 | @author: justJokee
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
44 |
47 |
--------------------------------------------------------------------------------
/static/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justJokee/vue-ssr-blog/f944768f8a87f10841b121fecd3055ca876579ad/static/.gitkeep
--------------------------------------------------------------------------------
/static/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 额~页面走丢了...
6 |
7 |
8 | (⊙o⊙)…页面走丢了
9 |
10 |
--------------------------------------------------------------------------------
/static/gc_back.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%= title %>
5 |
8 |
9 |
10 |
11 |
数据传输中,请稍后...
12 |
13 |
14 |
24 |
25 |
--------------------------------------------------------------------------------
/static/img/avatar/avatar.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justJokee/vue-ssr-blog/f944768f8a87f10841b121fecd3055ca876579ad/static/img/avatar/avatar.jpeg
--------------------------------------------------------------------------------
/static/img/cover/archive.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justJokee/vue-ssr-blog/f944768f8a87f10841b121fecd3055ca876579ad/static/img/cover/archive.jpeg
--------------------------------------------------------------------------------
/static/img/cover/articles.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justJokee/vue-ssr-blog/f944768f8a87f10841b121fecd3055ca876579ad/static/img/cover/articles.jpeg
--------------------------------------------------------------------------------
/static/img/cover/category.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justJokee/vue-ssr-blog/f944768f8a87f10841b121fecd3055ca876579ad/static/img/cover/category.jpg
--------------------------------------------------------------------------------
/static/img/cover/default.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justJokee/vue-ssr-blog/f944768f8a87f10841b121fecd3055ca876579ad/static/img/cover/default.jpg
--------------------------------------------------------------------------------
/static/img/cover/home.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justJokee/vue-ssr-blog/f944768f8a87f10841b121fecd3055ca876579ad/static/img/cover/home.jpg
--------------------------------------------------------------------------------
/static/img/cover/movie.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justJokee/vue-ssr-blog/f944768f8a87f10841b121fecd3055ca876579ad/static/img/cover/movie.jpeg
--------------------------------------------------------------------------------
/static/img/cover/movie.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justJokee/vue-ssr-blog/f944768f8a87f10841b121fecd3055ca876579ad/static/img/cover/movie.jpg
--------------------------------------------------------------------------------
/static/img/cover/msgboard.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justJokee/vue-ssr-blog/f944768f8a87f10841b121fecd3055ca876579ad/static/img/cover/msgboard.jpeg
--------------------------------------------------------------------------------
/static/img/cover/tags.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justJokee/vue-ssr-blog/f944768f8a87f10841b121fecd3055ca876579ad/static/img/cover/tags.jpg
--------------------------------------------------------------------------------
/static/img/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justJokee/vue-ssr-blog/f944768f8a87f10841b121fecd3055ca876579ad/static/img/favicon.ico
--------------------------------------------------------------------------------
/static/js/ribbon.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | /**
3 | * Created by zproo on 2017/4/8.
4 | */
5 | !(function() {
6 | document.addEventListener('touchmove', function(e) {
7 | e.preventDefault()
8 | })
9 |
10 | function getAttr(script, attr, default_val) {
11 | return Number(script.getAttribute(attr)) || default_val
12 | }
13 |
14 | // 获取自定义配置
15 | var ribbon = document.getElementById('ribbon') // 当前加载的script
16 | var config = {
17 | zIndex: getAttr(ribbon, 'zIndex', -1), // z-index
18 | alpha: getAttr(ribbon, 'alpha', 0.6), // alpha
19 | ribbon_width: getAttr(ribbon, 'size', 100) // size
20 | }
21 |
22 | var canvas = document.createElement('canvas')
23 | canvas.style.cssText = 'position:fixed;top:0;left:0;z-index:' + config.zIndex
24 | document.getElementsByTagName('body')[0].appendChild(canvas)
25 |
26 | var canvasRibbon = canvas,
27 | ctx = canvasRibbon.getContext('2d'), // 获取canvas 2d上下文
28 | dpr = window.devicePixelRatio || 1, // the size of one CSS pixel to the size of one physical pixel.
29 | width = window.innerWidth, // 返回窗口的文档显示区的宽高
30 | height = window.innerHeight,
31 | RIBBON_WIDTH = config.ribbon_width,
32 | path,
33 | math = Math,
34 | r = 0,
35 | PI_2 = math.PI * 2, // 圆周率*2
36 | cos = math.cos, // cos函数返回一个数值的余弦值(-1~1)
37 | random = math.random, // 返回0-1随机数
38 | proportion = dpr == 1 ? 0.7 : 0.5
39 | canvasRibbon.width = width * dpr // 返回实际宽高
40 | canvasRibbon.height = height * dpr
41 | ctx.scale(dpr, dpr) // 水平、竖直方向缩放
42 | ctx.globalAlpha = config.alpha // 图形透明度
43 |
44 | function init() {
45 | ctx.clearRect(0, 0, width, height) // 擦除之前绘制内容
46 | path = [
47 | { x: 0, y: height * proportion + RIBBON_WIDTH },
48 | { x: 0, y: height * proportion - RIBBON_WIDTH }
49 | ]
50 | // 路径没有填满屏幕宽度时,绘制路径
51 | while (path[1].x < width + RIBBON_WIDTH) {
52 | draw(path[0], path[1])
53 | }
54 | }
55 |
56 | function draw(start, end) {
57 | ctx.beginPath() // 创建一个新的路径
58 | ctx.moveTo(start.x, start.y) // path起点
59 | ctx.lineTo(end.x, end.y) // path终点
60 | var nextX = end.x + (random() * 2 - 0.25) * RIBBON_WIDTH,
61 | nextY = geneY(end.y)
62 | ctx.lineTo(nextX, nextY)
63 | ctx.closePath()
64 |
65 | r -= PI_2 / -50
66 | // 随机生成并设置canvas路径16进制颜色
67 | ctx.fillStyle =
68 | '#' +
69 | (
70 | ((cos(r) * 127 + 128) << 16) |
71 | ((cos(r + PI_2 / 3) * 127 + 128) << 8) |
72 | (cos(r + (PI_2 / 3) * 2) * 127 + 128)
73 | ).toString(16)
74 | ctx.fill() // 根据当前样式填充路径
75 | path[0] = path[1] // 起点更新为当前终点
76 | path[1] = { x: nextX, y: nextY } // 更新终点
77 | }
78 |
79 | function geneY(y) {
80 | var temp = y + (random() * 2 - 1.1) * RIBBON_WIDTH
81 | return temp > height || temp < 0 ? geneY(y) : temp
82 | }
83 |
84 | document.onclick = init
85 | document.ontouchstart = init
86 | init()
87 | })()
88 |
--------------------------------------------------------------------------------
/static/qc_back.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | QQConnect JSDK - redirectURI
7 |
14 |
15 |
16 |
17 |
数据传输中,请稍后...
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/template/secret.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @desc 密钥管理模板
3 | * @author justJokee
4 | */
5 |
6 | // 数据库配置
7 | exports.db = {
8 | user: 'admin',
9 | pwd: '12345',
10 | db: 'blog'
11 | }
12 | // 用户密码,签证盐值
13 | exports.userSecret = {
14 | account: 'test',
15 | pwd: '12345',
16 | salt: '12345',
17 | avatar: 'https://avatars.githubusercontent.com/u/35912907?v=4'
18 | }
19 | // github三方服务配置
20 | exports.githubSecret = {
21 | clientId: '',
22 | secret: ''
23 | }
24 | // 七牛云配置
25 | exports.qiniuConfig = {
26 | AccessKey: '',
27 | SecretKey: '',
28 | Bucket: '',
29 | Port: 9000,
30 | // demo 启动后会在本地 /uptoken 上提供获取 uptoken 的接口,所以这里可以填 'token'
31 | UptokenUrl: '',
32 | // Bucket 的外链默认域名,在 Bucket 的内容管理里查看
33 | Domain: ''
34 | }
35 | // 豆瓣uid
36 | exports.doubanConfig = {
37 | uid: '173712770'
38 | }
39 |
--------------------------------------------------------------------------------