├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── README.md
├── babel.config.js
├── build
└── index.js
├── dump.rdb
├── jsconfig.json
├── my_vue_blog.sql
├── my_vue_blog2.sql
├── package-lock.json
├── package.json
├── postcss.config.js
├── public
└── index.html
├── screenshot
├── 1.png
├── 10.png
├── 2.png
├── 3.png
├── 4.png
├── 5.png
├── 6.png
├── 7.png
├── 8.png
└── 9.png
├── server
├── api
│ ├── admin
│ │ ├── admin.controller.js
│ │ ├── admin.js
│ │ ├── category.controller.js
│ │ ├── laboratory.controller.js
│ │ ├── post.controller.js
│ │ └── tag.controller.js
│ ├── oauth
│ │ ├── github.controller.js
│ │ └── index.js
│ ├── post
│ │ ├── comment.controller.js
│ │ ├── post.controller.js
│ │ └── post.js
│ └── track
│ │ ├── track.controller.js
│ │ └── track.js
├── app.js
├── config
│ ├── environment
│ │ ├── development.js
│ │ └── index.js
│ ├── koa.js
│ └── src
│ │ └── uploads
│ │ └── 20191103
│ │ ├── 1572753935066.png
│ │ └── 1572753942038.jpg
├── middlreware
│ └── tokenError.js
├── routes
│ └── index.js
└── util
│ ├── admin-account.js
│ ├── draft-redis.js
│ ├── draft-socketio.js
│ ├── helper.js
│ ├── mysql-async.js
│ ├── redis-mysql.js
│ └── redis-store.js
├── src
├── App.vue
├── api
│ ├── blog
│ │ ├── category.js
│ │ ├── config.js
│ │ ├── post.js
│ │ ├── project.js
│ │ ├── tag.js
│ │ └── user.js
│ └── index.js
├── assets
│ ├── 404_images
│ │ ├── 404.png
│ │ └── 404_cloud.png
│ └── user
│ │ ├── admin.png
│ │ └── user.png
├── components
│ ├── Breadcrumb
│ │ └── index.vue
│ ├── Hamburger
│ │ └── index.vue
│ ├── SvgIcon
│ │ └── index.vue
│ ├── markdown
│ │ └── index.vue
│ ├── postTable
│ │ └── index.vue
│ ├── project
│ │ └── index.vue
│ └── upload
│ │ └── index.vue
├── icons
│ ├── index.js
│ ├── svg
│ │ ├── add.svg
│ │ ├── button.svg
│ │ ├── dashboard.svg
│ │ ├── delete.svg
│ │ ├── edit.svg
│ │ ├── example.svg
│ │ ├── eye-open.svg
│ │ ├── eye.svg
│ │ ├── form.svg
│ │ ├── link.svg
│ │ ├── list.svg
│ │ ├── nested.svg
│ │ ├── password.svg
│ │ ├── table.svg
│ │ ├── tree.svg
│ │ └── user.svg
│ └── svgo.yml
├── layout
│ ├── components
│ │ ├── AppMain.vue
│ │ ├── Navbar.vue
│ │ ├── Sidebar
│ │ │ ├── FixiOSBug.js
│ │ │ ├── Item.vue
│ │ │ ├── Link.vue
│ │ │ ├── Logo.vue
│ │ │ ├── SidebarItem.vue
│ │ │ └── index.vue
│ │ └── index.js
│ ├── index.vue
│ └── mixin
│ │ └── ResizeHandler.js
├── main.js
├── permission.js
├── router
│ └── index.js
├── settings.js
├── store
│ ├── getters.js
│ ├── index.js
│ └── modules
│ │ ├── app.js
│ │ ├── settings.js
│ │ └── user.js
├── styles
│ ├── element-ui.scss
│ ├── index.scss
│ ├── markdown.css
│ ├── mixin.scss
│ ├── sidebar.scss
│ ├── transition.scss
│ └── variables.scss
├── utils
│ ├── auth.js
│ ├── axios.js
│ ├── get-page-title.js
│ └── validate.js
└── views
│ ├── 404.vue
│ ├── articleList
│ └── index.vue
│ ├── dashboard
│ └── index.vue
│ ├── editArticle
│ └── index.vue
│ ├── labelManager
│ └── index.vue
│ ├── login
│ └── index.vue
│ ├── myProject
│ └── index.vue
│ └── sortManager
│ └── index.vue
└── vue.config.js
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | indent_style = space
7 | indent_size = 2
8 | end_of_line = lf
9 | insert_final_newline = true
10 | trim_trailing_whitespace = true
11 |
12 | [*.md]
13 | insert_final_newline = false
14 | trim_trailing_whitespace = false
15 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | build/*.js
2 | # src/assets
3 | src
4 | public
5 | dist
6 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | parserOptions: {
4 | parser: 'babel-eslint',
5 | sourceType: 'module'
6 | },
7 | env: {
8 | browser: true,
9 | node: true,
10 | es6: true,
11 | },
12 | extends: ['plugin:vue/recommended', 'eslint:recommended'],
13 |
14 | // add your custom rules here
15 | //it is base on https://github.com/vuejs/eslint-config-vue
16 | rules: {
17 | "vue/max-attributes-per-line": [2, {
18 | "singleline": 10,
19 | "multiline": {
20 | "max": 1,
21 | "allowFirstLine": false
22 | }
23 | }],
24 | "vue/singleline-html-element-content-newline": "off",
25 | "vue/multiline-html-element-content-newline":"off",
26 | "vue/name-property-casing": ["error", "PascalCase"],
27 | "vue/no-v-html": "off",
28 | 'accessor-pairs': 2,
29 | 'arrow-spacing': [2, {
30 | 'before': true,
31 | 'after': true
32 | }],
33 | 'block-spacing': [2, 'always'],
34 | 'brace-style': [2, '1tbs', {
35 | 'allowSingleLine': true
36 | }],
37 | 'camelcase': [0, {
38 | 'properties': 'always'
39 | }],
40 | 'comma-dangle': [2, 'never'],
41 | 'comma-spacing': [2, {
42 | 'before': false,
43 | 'after': true
44 | }],
45 | 'comma-style': [2, 'last'],
46 | 'constructor-super': 2,
47 | 'curly': [2, 'multi-line'],
48 | 'dot-location': [2, 'property'],
49 | 'eol-last': 2,
50 | 'eqeqeq': ["error", "always", {"null": "ignore"}],
51 | 'generator-star-spacing': [2, {
52 | 'before': true,
53 | 'after': true
54 | }],
55 | 'handle-callback-err': [2, '^(err|error)$'],
56 | 'indent': [2, 2, {
57 | 'SwitchCase': 1
58 | }],
59 | 'jsx-quotes': [2, 'prefer-single'],
60 | 'key-spacing': [2, {
61 | 'beforeColon': false,
62 | 'afterColon': true
63 | }],
64 | 'keyword-spacing': [2, {
65 | 'before': true,
66 | 'after': true
67 | }],
68 | 'new-cap': [2, {
69 | 'newIsCap': true,
70 | 'capIsNew': false
71 | }],
72 | 'new-parens': 2,
73 | 'no-array-constructor': 2,
74 | 'no-caller': 2,
75 | 'no-console': 'off',
76 | 'no-class-assign': 2,
77 | 'no-cond-assign': 2,
78 | 'no-const-assign': 2,
79 | 'no-control-regex': 0,
80 | 'no-delete-var': 2,
81 | 'no-dupe-args': 2,
82 | 'no-dupe-class-members': 2,
83 | 'no-dupe-keys': 2,
84 | 'no-duplicate-case': 2,
85 | 'no-empty-character-class': 2,
86 | 'no-empty-pattern': 2,
87 | 'no-eval': 2,
88 | 'no-ex-assign': 2,
89 | 'no-extend-native': 2,
90 | 'no-extra-bind': 2,
91 | 'no-extra-boolean-cast': 2,
92 | 'no-extra-parens': [2, 'functions'],
93 | 'no-fallthrough': 2,
94 | 'no-floating-decimal': 2,
95 | 'no-func-assign': 2,
96 | 'no-implied-eval': 2,
97 | 'no-inner-declarations': [2, 'functions'],
98 | 'no-invalid-regexp': 2,
99 | 'no-irregular-whitespace': 2,
100 | 'no-iterator': 2,
101 | 'no-label-var': 2,
102 | 'no-labels': [2, {
103 | 'allowLoop': false,
104 | 'allowSwitch': false
105 | }],
106 | 'no-lone-blocks': 2,
107 | 'no-mixed-spaces-and-tabs': 2,
108 | 'no-multi-spaces': 2,
109 | 'no-multi-str': 2,
110 | 'no-multiple-empty-lines': [2, {
111 | 'max': 1
112 | }],
113 | 'no-native-reassign': 2,
114 | 'no-negated-in-lhs': 2,
115 | 'no-new-object': 2,
116 | 'no-new-require': 2,
117 | 'no-new-symbol': 2,
118 | 'no-new-wrappers': 2,
119 | 'no-obj-calls': 2,
120 | 'no-octal': 2,
121 | 'no-octal-escape': 2,
122 | 'no-path-concat': 2,
123 | 'no-proto': 2,
124 | 'no-redeclare': 2,
125 | 'no-regex-spaces': 2,
126 | 'no-return-assign': [2, 'except-parens'],
127 | 'no-self-assign': 2,
128 | 'no-self-compare': 2,
129 | 'no-sequences': 2,
130 | 'no-shadow-restricted-names': 2,
131 | 'no-spaced-func': 2,
132 | 'no-sparse-arrays': 2,
133 | 'no-this-before-super': 2,
134 | 'no-throw-literal': 2,
135 | 'no-trailing-spaces': 2,
136 | 'no-undef': 2,
137 | 'no-undef-init': 2,
138 | 'no-unexpected-multiline': 2,
139 | 'no-unmodified-loop-condition': 2,
140 | 'no-unneeded-ternary': [2, {
141 | 'defaultAssignment': false
142 | }],
143 | 'no-unreachable': 2,
144 | 'no-unsafe-finally': 2,
145 | 'no-unused-vars': [2, {
146 | 'vars': 'all',
147 | 'args': 'none'
148 | }],
149 | 'no-useless-call': 2,
150 | 'no-useless-computed-key': 2,
151 | 'no-useless-constructor': 2,
152 | 'no-useless-escape': 0,
153 | 'no-whitespace-before-property': 2,
154 | 'no-with': 2,
155 | 'one-var': [2, {
156 | 'initialized': 'never'
157 | }],
158 | 'operator-linebreak': [2, 'after', {
159 | 'overrides': {
160 | '?': 'before',
161 | ':': 'before'
162 | }
163 | }],
164 | 'padded-blocks': [2, 'never'],
165 | 'quotes': [2, 'single', {
166 | 'avoidEscape': true,
167 | 'allowTemplateLiterals': true
168 | }],
169 | 'semi': [2, 'never'],
170 | 'semi-spacing': [2, {
171 | 'before': false,
172 | 'after': true
173 | }],
174 | 'space-before-blocks': [2, 'always'],
175 | 'space-before-function-paren': [2, 'never'],
176 | 'space-in-parens': [2, 'never'],
177 | 'space-infix-ops': 2,
178 | 'space-unary-ops': [2, {
179 | 'words': true,
180 | 'nonwords': false
181 | }],
182 | 'spaced-comment': [2, 'always', {
183 | 'markers': ['global', 'globals', 'eslint', 'eslint-disable', '*package', '!', ',']
184 | }],
185 | 'template-curly-spacing': [2, 'never'],
186 | 'use-isnan': 2,
187 | 'valid-typeof': 2,
188 | 'wrap-iife': [2, 'any'],
189 | 'yield-star-spacing': [2, 'both'],
190 | 'yoda': [2, 'never'],
191 | 'prefer-const': 2,
192 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
193 | 'object-curly-spacing': [2, 'always', {
194 | objectsInObjects: false
195 | }],
196 | 'array-bracket-spacing': [2, 'never']
197 | }
198 | }
199 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules/
3 | dist/
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | package-lock.json
8 | tests/**/coverage/
9 |
10 | # Editor directories and files
11 | .idea
12 | .vscode
13 | *.suo
14 | *.ntvs*
15 | *.njsproj
16 | *.sln
17 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 后台管理系统
2 |
3 | ## 技术栈
4 |
5 | 1. vue/vue-router/vuex/webpack4
6 | 2. koa
7 | 3. mysql
8 | 4. redis
9 | 5. websocket
10 | 6. element-ui
11 |
12 | ## 如何使用
13 |
14 | ```bash
15 | # 安装依赖
16 | npm install
17 | # 开启redis服务
18 | npm run redis
19 | # 开启后端服务 http://127.0.0.1:9001
20 | npm run server
21 | # 开启前端服务 http://127.0.0.1:9002
22 | npm run dev
23 | ```
24 |
25 | ## 版本
26 |
27 | `v2.1`
28 |
29 | ## 更新记录
30 |
31 | 1. 2019-03-16
32 | - 增加 github 授权认证
33 | 2. 2019-03-24
34 | - 增加留言功能
35 | 3. 2019-03-31
36 | - 帐号登录时前端密码加密
37 | 4. 2019-06-02
38 | - 升级到 vue-cli3
39 | - 优化页面
40 | - 修复已知的 bug
41 | - 增加了 vuex 状态管理
42 | 5. 2019-11-03
43 | - 优化项目,项目 UI 使用 element ui
44 | - 修复已知的 bug
45 | - 不仅仅是博客后台
46 | - css 预编译改用 sass
47 |
48 | ## 完成的功能
49 |
50 | 1. 登录页面
51 | 2. 文章列表,文章的编辑,发布,下线,socket 实时保存
52 | 3. 新增文章编辑
53 | 4. 分类列表,分类的删除,编辑,新增
54 | 5. 标签列表,标签的删除,编辑,新增
55 | 6. 项目列表,项目的删除,编辑,新增
56 | 7. 留言板与 github 认证
57 |
58 | ### 待完成的功能
59 |
60 | 1. ~~github 认证~~
61 | 2. ~~评论管理~~
62 | 3. 编辑器的优化
63 | 4. ~~首页增加可视化图表~~
64 | 5. 获取用户位置
65 | 6. 留言管理功能
66 | 7. ...
67 |
68 | ## 注意事项
69 |
70 | 1. vue.socket.io 版本必须是`^2.1.1-a`
71 | 2. 安装 mysql,用 `navicat` 连接 mysql,导入项目中 sql 文件,`my_vue_blog` 是有数据和结构的文件,`my_vue_blog2` 是只有结构,注意数据库名字和数据库登录的帐号密码
72 | 3. 安装 redis, 用 `RedisDesktopManager` 连接 redis,前提是开启了 redis 服务(如果配置环境变量,则 cmd 中输入 `redis-server`,否则进入 redis 安装的目录里输入 `redis-server.exe` `redis.windows.conf`)
73 | 4. 登录帐号是 `admin/123456`
74 |
75 | ## 常见问题
76 |
77 | ```
78 | 1. Warning: no config file specified, using the default config. In order to specify a config file use redis-server.exe /path/to/redis.conf
79 |
80 | 解决方法:在redis目录下输入redis-server.exe redis.windows.conf
81 |
82 | 2. Creating Server TCP listening socket 127.0.0.1:6379: bind: No error
83 |
84 | 解决方法:
85 | 在redis目录下输入
86 | redis-cli.exe
87 | shutdown
88 | exit
89 | 然后重新运行redis-server.exe redis.windows.conf
90 |
91 | ```
92 |
93 | ## 目录
94 |
95 | ```txt
96 | |-- README.md
97 | |-- babel.config.js
98 | |-- build
99 | | `-- index.js
100 | |-- dump.rdb
101 | |-- jsconfig.json
102 | |-- package-lock.json
103 | |-- package.json
104 | |-- postcss.config.js
105 | |-- public
106 | | `-- index.html
107 | |-- server
108 | | |-- api
109 | | | |-- admin
110 | | | | |-- admin.controller.js
111 | | | | |-- admin.js
112 | | | | |-- category.controller.js
113 | | | | |-- laboratory.controller.js
114 | | | | |-- post.controller.js
115 | | | | `-- tag.controller.js
116 | | | |-- oauth
117 | | | | |-- github.controller.js
118 | | | | `-- index.js
119 | | | |-- post
120 | | | | |-- comment.controller.js
121 | | | | |-- post.controller.js
122 | | | | `-- post.js
123 | | | `-- track
124 | | | |-- track.controller.js
125 | | | `-- track.js
126 | | |-- app.js
127 | | |-- config
128 | | | |-- environment
129 | | | | |-- development.js
130 | | | | `-- index.js
131 | | | |-- koa.js
132 | | | `-- src
133 | | | |-- tempUploads
134 | | | `-- uploads
135 | | |-- middlreware
136 | | | `-- tokenError.js
137 | | |-- routes
138 | | | `-- index.js
139 | | `-- util
140 | | |-- admin-account.js
141 | | |-- draft-redis.js
142 | | |-- draft-socketio.js
143 | | |-- helper.js
144 | | |-- mysql-async.js
145 | | |-- redis-mysql.js
146 | | `-- redis-store.js
147 | |-- src
148 | | |-- App.vue
149 | | |-- api
150 | | | |-- blog
151 | | | | |-- category.js
152 | | | | |-- config.js
153 | | | | |-- post.js
154 | | | | |-- project.js
155 | | | | |-- tag.js
156 | | | | `-- user.js
157 | | | `-- index.js
158 | | |-- assets
159 | | | |-- 404_images
160 | | | | |-- 404.png
161 | | | | `-- 404_cloud.png
162 | | | `-- user
163 | | | |-- admin.png
164 | | | `-- user.png
165 | | |-- components
166 | | | |-- Breadcrumb
167 | | | | `-- index.vue
168 | | | |-- Hamburger
169 | | | | `-- index.vue
170 | | | |-- SvgIcon
171 | | | | `-- index.vue
172 | | | |-- markdown
173 | | | | `-- index.vue
174 | | | |-- postTable
175 | | | | `-- index.vue
176 | | | |-- project
177 | | | | `-- index.vue
178 | | | `-- upload
179 | | | `-- index.vue
180 | | |-- icons
181 | | | |-- index.js
182 | | | |-- svg
183 | | | | |-- add.svg
184 | | | | |-- button.svg
185 | | | | |-- dashboard.svg
186 | | | | |-- delete.svg
187 | | | | |-- edit.svg
188 | | | | |-- example.svg
189 | | | | |-- eye-open.svg
190 | | | | |-- eye.svg
191 | | | | |-- form.svg
192 | | | | |-- link.svg
193 | | | | |-- list.svg
194 | | | | |-- nested.svg
195 | | | | |-- password.svg
196 | | | | |-- table.svg
197 | | | | |-- tree.svg
198 | | | | `-- user.svg
199 | | | `-- svgo.yml
200 | | |-- layout
201 | | | |-- components
202 | | | | |-- AppMain.vue
203 | | | | |-- Navbar.vue
204 | | | | |-- Sidebar
205 | | | | | |-- FixiOSBug.js
206 | | | | | |-- Item.vue
207 | | | | | |-- Link.vue
208 | | | | | |-- Logo.vue
209 | | | | | |-- SidebarItem.vue
210 | | | | | `-- index.vue
211 | | | | `-- index.js
212 | | | |-- index.vue
213 | | | `-- mixin
214 | | | `-- ResizeHandler.js
215 | | |-- main.js
216 | | |-- permission.js
217 | | |-- router
218 | | | `-- index.js
219 | | |-- settings.js
220 | | |-- store
221 | | | |-- getters.js
222 | | | |-- index.js
223 | | | `-- modules
224 | | | |-- app.js
225 | | | |-- settings.js
226 | | | `-- user.js
227 | | |-- styles
228 | | | |-- element-ui.scss
229 | | | |-- index.scss
230 | | | |-- markdown.css
231 | | | |-- mixin.scss
232 | | | |-- sidebar.scss
233 | | | |-- transition.scss
234 | | | `-- variables.scss
235 | | |-- utils
236 | | | |-- auth.js
237 | | | |-- axios.js
238 | | | |-- get-page-title.js
239 | | | `-- validate.js
240 | | `-- views
241 | | |-- 404.vue
242 | | |-- articleList
243 | | | `-- index.vue
244 | | |-- dashboard
245 | | | `-- index.vue
246 | | |-- editArticle
247 | | | `-- index.vue
248 | | |-- labelManager
249 | | | `-- index.vue
250 | | |-- login
251 | | | `-- index.vue
252 | | |-- myProject
253 | | | `-- index.vue
254 | | `-- sortManager
255 | | `-- index.vue
256 | `-- vue.config.js
257 | ```
258 |
259 | ## 关键技术点
260 |
261 | 1. [nodejs 熟悉,http,fs,path,Buffer 的使用](https://github.com/dirkhe1051931999/hjBlog/blob/master/blog-management/lessons/01.md)
262 | 2. [koa 的原理与 koa-static、koa-router 等中间件的实现](https://github.com/dirkhe1051931999/hjBlog/blob/master/blog-management/lessons/02.md)
263 | 3. [服务器目录,API 接口管理](https://github.com/dirkhe1051931999/hjBlog/blob/master/blog-management/lessons/03.md)
264 | 4. [使用 koa-session 实现用户认证](https://github.com/dirkhe1051931999/hjBlog/blob/master/blog-management/lessons/04.md)
265 | 5. [使用 jwt 实现用户认证](https://github.com/dirkhe1051931999/hjBlog/blob/master/blog-management/lessons/05.md)
266 | 6. [node+koa 如何连接数据库](https://github.com/dirkhe1051931999/hjBlog/blob/master/blog-management/lessons/06.md)
267 | 7. [node+koa 如何连接 redis,mysql 和 redis 联调](https://github.com/dirkhe1051931999/hjBlog/blob/master/blog-management/lessons/07.md)
268 | 8. [node+koa+vue+soket 的使用](https://github.com/dirkhe1051931999/hjBlog/blob/master/blog-management/lessons/08.md)
269 | 9. [node+koa 的文章的增删改查及文章的表结构](https://github.com/dirkhe1051931999/hjBlog/blob/master/blog-management/lessons/09.md)
270 | 10. [node+koa 分类的增删改查及分类的表结构](https://github.com/dirkhe1051931999/hjBlog/blob/master/blog-management/lessons/10.md)
271 | 11. [node+koa 标签的增删改查及标签的表结构](https://github.com/dirkhe1051931999/hjBlog/blob/master/blog-management/lessons/11.md)
272 | 12. [node+koa 项目的增删改查及项目的表结构](https://github.com/dirkhe1051931999/hjBlog/blob/master/blog-management/lessons/12.md)
273 | 13. [node+koa 图片的上传、form 标签的处理和静态服务器](https://github.com/dirkhe1051931999/hjBlog/blob/master/blog-management/lessons/13.md)
274 | 14. [vue 实现全局的组件、分页组件](https://github.com/dirkhe1051931999/hjBlog/blob/master/blog-management/lessons/14.md)
275 | 15. [vue+koa 实现 github 登录授权](https://github.com/dirkhe1051931999/hjBlog/blob/master/blog-management/lessons/15.md)
276 | 16. [留言功能](https://github.com/dirkhe1051931999/hjBlog/blob/master/blog-management/lessons/16.md)
277 | 17. [vue 组件通信](https://github.com/dirkhe1051931999/hjBlog/blob/master/blog-vue/lessons/06.md)
278 | 18. [vue-cli3.x 的配置文件详细介绍](https://github.com/dirkhe1051931999/hjBlog/tree/master/blog-vue/lessons/11.md)
279 | 19. [rem 与 em](https://github.com/dirkhe1051931999/hjBlog/blob/master/blog-css/lessons/03.md)
280 | 20. [webpack 优化相关](https://github.com/dirkhe1051931999/common-demo/tree/master/webpack-study-notes)
281 |
282 | ## 实现效果
283 |
284 | 
285 | 
286 | 
287 | 
288 | 
289 | 
290 | 
291 | 
292 | 
293 | 
294 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | '@vue/app'
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/build/index.js:
--------------------------------------------------------------------------------
1 | const { run } = require('runjs')
2 | const chalk = require('chalk')
3 | const config = require('../vue.config.js')
4 | const rawArgv = process.argv.slice(2)
5 | const args = rawArgv.join(' ')
6 |
7 | if (process.env.npm_config_preview || rawArgv.includes('--preview')) {
8 | const report = rawArgv.includes('--report')
9 |
10 | run(`vue-cli-service build ${args}`)
11 |
12 | const port = 9003
13 | const publicPath = config.publicPath
14 |
15 | var connect = require('connect')
16 | var serveStatic = require('serve-static')
17 | const app = connect()
18 |
19 | app.use(
20 | publicPath,
21 | serveStatic('./dist', {
22 | index: ['index.html', '/']
23 | })
24 | )
25 |
26 | app.listen(port, function () {
27 | console.log(chalk.green(`> Preview at http://localhost:${port}${publicPath}`))
28 | if (report) {
29 | console.log(chalk.green(`> Report at http://localhost:${port}${publicPath}report.html`))
30 | }
31 |
32 | })
33 | } else {
34 | run(`vue-cli-service build ${args}`)
35 | }
36 |
--------------------------------------------------------------------------------
/dump.rdb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dirkhe1051931999/vue-management/dd7f085e9921106430e1ca87ceaa77b58406f52b/dump.rdb
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "./",
4 | "paths": {
5 | "@/*": ["src/*"]
6 | }
7 | },
8 | "exclude": ["node_modules", "dist"]
9 | }
10 |
--------------------------------------------------------------------------------
/my_vue_blog2.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Navicat MySQL Data Transfer
3 |
4 | Source Server : localhost_3306
5 | Source Server Version : 50524
6 | Source Host : localhost:3306
7 | Source Database : my_vue_blog
8 |
9 | Target Server Type : MYSQL
10 | Target Server Version : 50524
11 | File Encoding : 65001
12 |
13 | Date: 2019-03-16 17:15:12
14 | */
15 |
16 | SET FOREIGN_KEY_CHECKS=0;
17 |
18 | -- ----------------------------
19 | -- Table structure for category
20 | -- ----------------------------
21 | DROP TABLE IF EXISTS `category`;
22 | CREATE TABLE `category` (
23 | `id` int(11) NOT NULL AUTO_INCREMENT,
24 | `name` varchar(100) DEFAULT NULL COMMENT '文章分类名称',
25 | PRIMARY KEY (`id`)
26 | ) ENGINE=InnoDB AUTO_INCREMENT=20 DEFAULT CHARSET=utf8 COMMENT='文章分类表';
27 |
28 | -- ----------------------------
29 | -- Table structure for draft_post_redis
30 | -- ----------------------------
31 | DROP TABLE IF EXISTS `draft_post_redis`;
32 | CREATE TABLE `draft_post_redis` (
33 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
34 | `redisKey` varchar(100) DEFAULT NULL,
35 | `postId` int(11) DEFAULT NULL,
36 | `title` varchar(200) DEFAULT NULL,
37 | `content` text,
38 | `categoryId` int(11) DEFAULT NULL,
39 | `poster` varchar(200) DEFAULT NULL,
40 | `tags` varchar(200) DEFAULT NULL,
41 | PRIMARY KEY (`id`)
42 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
43 |
44 | -- ----------------------------
45 | -- Table structure for laboratory
46 | -- ----------------------------
47 | DROP TABLE IF EXISTS `laboratory`;
48 | CREATE TABLE `laboratory` (
49 | `id` int(11) NOT NULL AUTO_INCREMENT,
50 | `name` varchar(100) DEFAULT NULL COMMENT '项目名称',
51 | `description` varchar(1000) DEFAULT NULL COMMENT '项目说明',
52 | `link` varchar(500) DEFAULT NULL COMMENT '项目地址',
53 | `poster` varchar(500) DEFAULT NULL COMMENT '项目海报',
54 | `createTime` datetime DEFAULT NULL COMMENT '创建时间',
55 | `github` varchar(500) DEFAULT NULL COMMENT '项目Github地址',
56 | PRIMARY KEY (`id`)
57 | ) ENGINE=InnoDB AUTO_INCREMENT=25 DEFAULT CHARSET=utf8 COMMENT='实验室表,记录个人项目';
58 |
59 | -- ----------------------------
60 | -- Table structure for post
61 | -- ----------------------------
62 | DROP TABLE IF EXISTS `post`;
63 | CREATE TABLE `post` (
64 | `id` int(11) NOT NULL AUTO_INCREMENT,
65 | `title` varchar(200) DEFAULT NULL COMMENT '文章标题',
66 | `content` text COMMENT '文章内容',
67 | `categoryId` int(11) DEFAULT NULL COMMENT '文章分类表主键',
68 | `status` enum('DRAFT','PUBLISHED','OFFLINE') DEFAULT NULL COMMENT '文章状态(DRAFT: 草稿, PUBLISHED: 发布,OFFLINE: 下线)',
69 | `poster` varchar(200) DEFAULT NULL COMMENT '海报图片',
70 | `createTime` datetime DEFAULT NULL COMMENT '创建时间',
71 | `viewTotal` int(11) DEFAULT NULL COMMENT '文章查看次数',
72 | PRIMARY KEY (`id`)
73 | ) ENGINE=InnoDB AUTO_INCREMENT=42 DEFAULT CHARSET=utf8 COMMENT='文章表';
74 |
75 | -- ----------------------------
76 | -- Table structure for post_tag
77 | -- ----------------------------
78 | DROP TABLE IF EXISTS `post_tag`;
79 | CREATE TABLE `post_tag` (
80 | `id` int(11) NOT NULL AUTO_INCREMENT,
81 | `postId` int(11) NOT NULL COMMENT '文章表主键',
82 | `tagId` int(11) NOT NULL COMMENT '标签表主键',
83 | PRIMARY KEY (`id`)
84 | ) ENGINE=InnoDB AUTO_INCREMENT=175 DEFAULT CHARSET=utf8 COMMENT='文章-标签对应表';
85 |
86 | -- ----------------------------
87 | -- Table structure for tag
88 | -- ----------------------------
89 | DROP TABLE IF EXISTS `tag`;
90 | CREATE TABLE `tag` (
91 | `id` int(11) NOT NULL AUTO_INCREMENT,
92 | `name` varchar(100) DEFAULT NULL COMMENT '标签名称',
93 | PRIMARY KEY (`id`)
94 | ) ENGINE=InnoDB AUTO_INCREMENT=63 DEFAULT CHARSET=utf8 COMMENT='标签表';
95 |
96 | -- ----------------------------
97 | -- Table structure for user
98 | -- ----------------------------
99 | DROP TABLE IF EXISTS `user`;
100 | CREATE TABLE `user` (
101 | `id` int(11) NOT NULL AUTO_INCREMENT,
102 | `userName` varchar(50) DEFAULT NULL COMMENT '用户名',
103 | `hashedPassword` varchar(1024) DEFAULT NULL COMMENT '加密后的密码',
104 | `salt` varchar(128) DEFAULT NULL COMMENT '加密的盐',
105 | `avatar` varchar(500) DEFAULT NULL COMMENT '用户头像',
106 | `role` enum('ADMIN','GUEST') NOT NULL COMMENT '用户角色(ADMIN:管理员,GUEST:游客)',
107 | `createTime` datetime DEFAULT NULL COMMENT '创建时间',
108 | `email` varchar(50) DEFAULT NULL COMMENT '邮箱',
109 | PRIMARY KEY (`id`)
110 | ) ENGINE=InnoDB AUTO_INCREMENT=44 DEFAULT CHARSET=utf8 COMMENT='用户表';
111 |
112 | -- ----------------------------
113 | -- Table structure for comments
114 | -- ----------------------------
115 | DROP TABLE IF EXISTS `comments`;
116 | CREATE TABLE `comments` (
117 | `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id',
118 | `postId` int(11) DEFAULT NULL COMMENT '文章id',
119 | `content` text COMMENT '评论内容',
120 | `fromUserId` int(11) DEFAULT NULL COMMENT '评论用户',
121 | `toUserId` int(11) DEFAULT NULL COMMENT '目标用户',
122 | `createdTime` datetime DEFAULT NULL COMMENT '创建时间',
123 | `number` int(11) DEFAULT NULL COMMENT 'number',
124 | PRIMARY KEY (`id`)
125 | ) ENGINE=InnoDB AUTO_INCREMENT=28 DEFAULT CHARSET=utf8;
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vue-blog-admin-template",
3 | "version": "4.2.1",
4 | "description": "后台管理平台",
5 | "author": "hejiandirk@163.com",
6 | "license": "MIT",
7 | "script": {
8 | "redis": "第一步:开启redis",
9 | "server": "第二步:开启后端服务",
10 | "dev": "第三步:开启前端服务器",
11 | "build": "生成环境",
12 | "preview": "预发布环境",
13 | "lint": "开启eslint校验",
14 | "test": "开启单元测试",
15 | "svgo": "svg压缩"
16 | },
17 | "scripts": {
18 | "redis": "redis-server",
19 | "server": "nodemon --inspect server/app.js",
20 | "dev": "vue-cli-service serve",
21 | "build": "vue-cli-service build",
22 | "preview": "node build/index.js --preview",
23 | "lint": "eslint --ext .js,.vue src",
24 | "svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml"
25 | },
26 | "dependencies": {
27 | "axios": "^0.19.0",
28 | "core-js": "^2.6.5",
29 | "crypto": "^1.0.1",
30 | "echarts": "^4.4.0",
31 | "element-ui": "^2.12.0",
32 | "formidable": "^1.2.1",
33 | "ioredis": "^4.6.2",
34 | "js-cookie": "2.2.0",
35 | "jsonwebtoken": "^8.1.0",
36 | "koa": "^2.7.0",
37 | "koa-bodyparser": "^4.2.1",
38 | "koa-json": "^2.0.2",
39 | "koa-jwt": "^3.5.1",
40 | "koa-router": "^7.4.0",
41 | "koa-session2": "^2.2.8",
42 | "koa-static": "^5.0.0",
43 | "marked": "^0.6.1",
44 | "moment": "^2.24.0",
45 | "mysql": "^2.16.0",
46 | "node-fetch": "^2.3.0",
47 | "node-schedule": "^1.3.2",
48 | "normalize.css": "7.0.0",
49 | "nprogress": "0.2.0",
50 | "path-to-regexp": "2.4.0",
51 | "socket.io": "^2.0.4",
52 | "socket.io-client": "^2.0.4",
53 | "v-charts": "^1.19.0",
54 | "vue": "2.6.10",
55 | "vue-router": "3.0.6",
56 | "vue-socket.io": "^2.1.1-a",
57 | "vuex": "3.1.0"
58 | },
59 | "devDependencies": {
60 | "@babel/core": "7.0.0",
61 | "@babel/register": "7.0.0",
62 | "@vue/cli-plugin-babel": "3.6.0",
63 | "@vue/cli-plugin-eslint": "^3.9.1",
64 | "@vue/cli-plugin-unit-jest": "3.6.3",
65 | "@vue/cli-service": "3.6.0",
66 | "@vue/test-utils": "1.0.0-beta.29",
67 | "autoprefixer": "^9.5.1",
68 | "babel-core": "7.0.0-bridge.0",
69 | "babel-eslint": "10.0.1",
70 | "babel-jest": "23.6.0",
71 | "chalk": "2.4.2",
72 | "connect": "3.6.6",
73 | "eslint": "5.15.3",
74 | "eslint-plugin-vue": "5.2.2",
75 | "html-webpack-plugin": "3.2.0",
76 | "node-sass": "^4.9.0",
77 | "runjs": "^4.3.2",
78 | "sass-loader": "^7.1.0",
79 | "script-ext-html-webpack-plugin": "2.1.3",
80 | "script-loader": "0.7.2",
81 | "serve-static": "^1.13.2",
82 | "svg-sprite-loader": "4.1.3",
83 | "svgo": "1.2.2",
84 | "vue-template-compiler": "2.6.10"
85 | },
86 | "engines": {
87 | "node": ">=8.9",
88 | "npm": ">= 3.0.0"
89 | },
90 | "browserslist": [
91 | "> 1%",
92 | "last 2 versions"
93 | ]
94 | }
95 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | // https://github.com/michael-ciniawsky/postcss-load-config
2 |
3 | module.exports = {
4 | plugins: {
5 | // to edit target browsers: use "browserslist" field in package.json
6 | autoprefixer: {}
7 | }
8 | };
9 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | <%= webpackConfig.name %>
9 |
10 |
11 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/screenshot/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dirkhe1051931999/vue-management/dd7f085e9921106430e1ca87ceaa77b58406f52b/screenshot/1.png
--------------------------------------------------------------------------------
/screenshot/10.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dirkhe1051931999/vue-management/dd7f085e9921106430e1ca87ceaa77b58406f52b/screenshot/10.png
--------------------------------------------------------------------------------
/screenshot/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dirkhe1051931999/vue-management/dd7f085e9921106430e1ca87ceaa77b58406f52b/screenshot/2.png
--------------------------------------------------------------------------------
/screenshot/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dirkhe1051931999/vue-management/dd7f085e9921106430e1ca87ceaa77b58406f52b/screenshot/3.png
--------------------------------------------------------------------------------
/screenshot/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dirkhe1051931999/vue-management/dd7f085e9921106430e1ca87ceaa77b58406f52b/screenshot/4.png
--------------------------------------------------------------------------------
/screenshot/5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dirkhe1051931999/vue-management/dd7f085e9921106430e1ca87ceaa77b58406f52b/screenshot/5.png
--------------------------------------------------------------------------------
/screenshot/6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dirkhe1051931999/vue-management/dd7f085e9921106430e1ca87ceaa77b58406f52b/screenshot/6.png
--------------------------------------------------------------------------------
/screenshot/7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dirkhe1051931999/vue-management/dd7f085e9921106430e1ca87ceaa77b58406f52b/screenshot/7.png
--------------------------------------------------------------------------------
/screenshot/8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dirkhe1051931999/vue-management/dd7f085e9921106430e1ca87ceaa77b58406f52b/screenshot/8.png
--------------------------------------------------------------------------------
/screenshot/9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dirkhe1051931999/vue-management/dd7f085e9921106430e1ca87ceaa77b58406f52b/screenshot/9.png
--------------------------------------------------------------------------------
/server/api/admin/admin.controller.js:
--------------------------------------------------------------------------------
1 | const jwt = require('jsonwebtoken');
2 | const helper = require('../../util/helper');
3 | const config = require('../../config/environment');
4 | // 登录
5 | exports.login = async (ctx) => {
6 | let userName = ctx.request.body.userName || '',
7 | password = ctx.request.body.password || '';
8 | if (!userName || !password) {
9 | ctx.body = {
10 | success: 0,
11 | message: '用户名或密码不能为空'
12 | };
13 | return;
14 | }
15 | try {
16 | let results = await ctx.execSql(`SELECT id, hashedPassword, salt FROM user WHERE role='ADMIN' and userName = ?`, userName);
17 | if (results.length > 0) {
18 | let hashedPassword = results[0].hashedPassword;
19 | let salt = results[0].salt;
20 | let hashPassword = helper.encryptInputPassword(password, salt);
21 | if (hashedPassword === hashPassword) {
22 | ctx.session.user = userName;
23 | // 用户token
24 | const userToken = {
25 | name: userName,
26 | id: results[0].id
27 | };
28 | // 签发token
29 | const token = jwt.sign(userToken, config.tokenSecret, {
30 | expiresIn: '24h'
31 | });
32 | ctx.body = {
33 | success: 1,
34 | token: token,
35 | message: ''
36 | };
37 | } else {
38 | ctx.body = {
39 | success: 0,
40 | message: '用户名或密码错误'
41 | };
42 | }
43 | } else {
44 | ctx.body = {
45 | success: 0,
46 | message: '用户名或密码错误'
47 | };
48 | }
49 | } catch (error) {
50 | console.log(error);
51 | ctx.body = {
52 | success: 0,
53 | message: '查询数据出错'
54 | };
55 | }
56 | }
57 | // 登出
58 | exports.signOut = async (ctx) => {
59 | ctx.session.user = null;
60 | ctx.body = {
61 | success: 1,
62 | message: ''
63 | };
64 | }
65 |
--------------------------------------------------------------------------------
/server/api/admin/admin.js:
--------------------------------------------------------------------------------
1 | const router = require('koa-router')();
2 | const admin = require('./admin.controller');
3 | const post = require('./post.controller');
4 | const category = require('./category.controller');
5 | const tag = require('./tag.controller');
6 | const laboratory = require('./laboratory.controller');
7 |
8 | // 登录/登出
9 | router.post('/login', admin.login);
10 | router.get('/signOut', admin.signOut);
11 |
12 | // 文章路由路由
13 | // 获取文章详情
14 | router.get('/getPostById/:id', post.getPostById);
15 | // 添加文章
16 | router.post('/addPost', post.addPost);
17 | // 更新文章
18 | router.post('/updatePost/:id', post.updatePost);
19 | // 获取文章列表
20 | router.get('/getPostList', post.getPostList);
21 | // 获取文章总数
22 | router.get('/getPostTotal', post.getPostTotal);
23 | // 下架文章
24 | router.put('/offlinePost/:id', post.offlinePost);
25 | // 发布文章
26 | router.put('/publishPost/:id', post.publishPost);
27 | // 删除文章
28 | router.delete('/deletePost/:id', post.deletePost);
29 | // 获取当前的文章的类别
30 | router.get('/getPostsByCatId/:id', category.getPostsByCatId);
31 | // 获取当前文章的标签
32 | router.get('/getPostsByTagId/:id', tag.getPostsByTagId);
33 |
34 | // 文章分类路由
35 | // 获取所有类别
36 | router.get('/getCategories', category.getCategories);
37 | // 添加文章类别
38 | router.put('/addNewCategory/:name', category.addNewCategory);
39 | // 更新文章类别
40 | router.put('/updateCategory/:id', category.updateCategory);
41 | // 删除当前文章类别
42 | router.delete('/deleteCategory/:id', category.deleteCategory);
43 |
44 | // 文章标签路由
45 | // 获取所有标签
46 | router.get('/getTags', tag.getTags);
47 | // 写文章的时候添加文章标签
48 | router.put('/addNewTagWhenPost/:name', tag.addNewTagWhenPost);
49 | // 添加文章标签
50 | router.put('/addNewTag/:name', tag.addNewTag);
51 | // 更新文章标签
52 | router.put('/updateTag/:id', tag.updateTag);
53 | // 删除文章标签
54 | router.delete('/deleteTag/:id', tag.deleteTag);
55 | // 搜索文章标签
56 | router.get('/searchTagByName/:name', tag.searchTagByName);
57 |
58 | // 个人项目路由
59 | // 获取所有个人项目
60 | router.get('/getLaboratories', laboratory.getLaboratories);
61 | // 添加个人项目
62 | router.post('/createNewLaboratory', laboratory.createNewLaboratory);
63 | // 更新个人项目
64 | router.post('/updateLaboratory', laboratory.updateLaboratory);
65 | // 删除个人项目
66 | router.delete('/deleteLaboratory/:id', laboratory.deleteLaboratory);
67 |
68 | module.exports = router;
69 |
--------------------------------------------------------------------------------
/server/api/admin/category.controller.js:
--------------------------------------------------------------------------------
1 | // 获取所有文章类别
2 | exports.getCategories = async (ctx) => {
3 | let sql = ` SELECT category.id, category.name, COUNT(post.id) AS count
4 | FROM category LEFT JOIN post ON post.categoryId = category.id
5 | GROUP BY category.id`;
6 | try {
7 | let results = await ctx.execSql(sql);
8 | ctx.body = {
9 | success: 1,
10 | message: '',
11 | categories: results.length > 0 ? results : []
12 | };
13 | } catch (error) {
14 | console.log(error);
15 | ctx.body = {
16 | success: 0,
17 | message: '查询数据出错'
18 | };
19 | }
20 | }
21 | // 获取当前文章类型
22 | exports.getPostsByCatId = async (ctx) => {
23 | let id = ctx.params.id || 0,
24 | page = ctx.query.page || 1,
25 | pageNum = ctx.query.pageNum || 10,
26 | pageIndex = (page - 1) * pageNum < 0 ? 0 : (page - 1) * pageNum,
27 | fliter = id == 0 ? '' : ` WHERE post.categoryId = ${id} `,
28 | sql = ` SELECT post.id, post.title, post.createTime, post.status, post.categoryId,
29 | category.name AS categoryName FROM post LEFT JOIN category
30 | ON post.categoryId = category.id ${fliter}
31 | ORDER BY post.createTime DESC LIMIT ${pageIndex}, ${pageNum}`;
32 | try {
33 | let results = await ctx.execSql(sql);
34 | ctx.body = {
35 | success: 1,
36 | message: '',
37 | posts: results
38 | };
39 | } catch (error) {
40 | console.log(error);
41 | ctx.body = {
42 | success: 0,
43 | message: '查询数据出错',
44 | posts: null
45 | };
46 | }
47 | }
48 | // 添加分类
49 | exports.addNewCategory = async(ctx) => {
50 | let name = ctx.params.name || 0;
51 | try {
52 | let existName = await ctx.execSql(`SELECT * FROM category WHERE name = ?`, name);
53 | if (existName.length > 0) {
54 | ctx.body = {
55 | success: 0,
56 | message: '分类名称已存在!'
57 | };
58 | return false;
59 | }
60 | let results = await ctx.execSql(`INSERT INTO category SET name = ?`, name);
61 | ctx.body = {
62 | success: 1,
63 | message: '',
64 | newId: results.insertId
65 | };
66 | } catch (error) {
67 | console.log(error);
68 | ctx.body = {
69 | success: 0,
70 | message: '添加新分类出错',
71 | newId: 0
72 | };
73 | }
74 | }
75 | // 更新分类
76 | exports.updateCategory = async(ctx) => {
77 | let id = ctx.params.id || 0,
78 | name = ctx.query.name || '';
79 | try {
80 | let existName = await ctx.execSql(`SELECT * FROM category WHERE name = ? AND id <> ?`, [name, id]);
81 | if (existName.length > 0) {
82 | ctx.body = {
83 | success: 0,
84 | message: '分类名称已存在!'
85 | };
86 | return false;
87 | }
88 | let results = await ctx.execSql(`UPDATE category SET name = ? WHERE id = ?`, [name, id]);
89 | ctx.body = {
90 | success: 1,
91 | message: ''
92 | };
93 | } catch (error) {
94 | console.log(error);
95 | ctx.body = {
96 | success: 0,
97 | message: '更新分类出错'
98 | };
99 | }
100 | }
101 | // 删除分类
102 | exports.deleteCategory = async(ctx) => {
103 | let id = ctx.params.id || 0;
104 | try {
105 | let results = await ctx.execSql(`DELETE FROM category WHERE id = ?`, id);
106 | ctx.body = {
107 | success: 1,
108 | message: ''
109 | };
110 | } catch (error) {
111 | console.log(error);
112 | ctx.body = {
113 | success: 0,
114 | message: '删除分类出错'
115 | };
116 | }
117 | }
--------------------------------------------------------------------------------
/server/api/admin/laboratory.controller.js:
--------------------------------------------------------------------------------
1 | const helper = require('../../util/helper');
2 | const moment = require('moment');
3 | // 获取个人项目
4 | exports.getLaboratories = async (ctx) => {
5 | let sql = `SELECT * FROM laboratory ORDER BY createTime desc`;
6 | try {
7 | let results = await ctx.execSql(sql);
8 | ctx.body = {
9 | success: 1,
10 | message: '',
11 | laboratories: results.length > 0 ? results : []
12 | };
13 | } catch (error) {
14 | console.log(error);
15 | ctx.body = {
16 | success: 0,
17 | message: '查询数据出错'
18 | };
19 | }
20 | }
21 | // 添加个人项目
22 | exports.createNewLaboratory = async (ctx) => {
23 | let result;
24 | try {
25 | result = await helper.uploadFile(ctx);
26 | let fields = result.fields;
27 | let laboratory = {
28 | name: fields.name,
29 | link: fields.link,
30 | github: fields.github,
31 | description: fields.description,
32 | poster: result.filePath,
33 | createTime: moment().format('YYYY-MM-DD HH:mm:ss')
34 | };
35 | let insert = await ctx.execSql('INSERT INTO laboratory SET ?', laboratory);
36 | if (insert.affectedRows > 0) {
37 | ctx.body = {
38 | success: 1,
39 | id: insert.insertId,
40 | poster: laboratory.poster
41 | };
42 | } else {
43 | ctx.body = {
44 | success: 0,
45 | message: '创建项目出错'
46 | };
47 | }
48 | } catch (error) {
49 | console.log('error', error);
50 | ctx.body = {
51 | success: 0,
52 | message: '参数错误'
53 | };
54 | }
55 | }
56 | // 更新个人项目
57 | exports.updateLaboratory = async (ctx) => {
58 | let result;
59 | try {
60 | result = await helper.uploadFile(ctx);
61 | console.log(result)
62 | let fields = result.fields;
63 | let laboratory = {
64 | name: fields.name,
65 | link: fields.link,
66 | github: fields.github,
67 | description: fields.description,
68 | poster: result.filePath,
69 | createTime: moment().format('YYYY-MM-DD HH:mm:ss')
70 | };
71 | let update = await ctx.execSql('UPDATE laboratory SET ? WHERE id = ?', [laboratory, fields.id]);
72 | ctx.body = {
73 | success: 1,
74 | poster: laboratory.poster
75 | };
76 | } catch (error) {
77 | console.log('error', error);
78 | ctx.body = {
79 | success: 0,
80 | message: '参数错误'
81 | };
82 | }
83 | }
84 | // 删除个人项目
85 | exports.deleteLaboratory = async (ctx) => {
86 | let id = ctx.params.id || 0;
87 | console.log(id)
88 | try {
89 | let result = await ctx.execSql('DELETE FROM laboratory WHERE id = ?', id);
90 | ctx.body = {
91 | success: 1
92 | };
93 | } catch (error) {
94 | console.log('error', error);
95 | ctx.body = {
96 | success: 0,
97 | message: '删除项目出错'
98 | };
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/server/api/admin/post.controller.js:
--------------------------------------------------------------------------------
1 | const moment = require('moment');
2 |
3 | // 获取具体文章
4 | exports.getPostById = async (ctx) => {
5 | let id = ctx.params.id || 0,
6 | sql = ` SELECT post.id, post.title, post.content, post.poster, post.createTime,
7 | post.categoryId, category.name AS categoryName, viewTotal
8 | FROM post LEFT JOIN category ON post.categoryId = category.id
9 | WHERE post.id = ${id}`,
10 | tagSql = ` SELECT tag.id, tag.name from post_tag a LEFT JOIN tag on a.tagId = tag.id
11 | WHERE a.postId = ${id}`;
12 | try {
13 | let results = await ctx.execSql(sql);
14 | if (results.length > 0) {
15 | let tagResults = await ctx.execSql(tagSql);
16 | ctx.body = {
17 | success: 1,
18 | message: '',
19 | post: results[0],
20 | tags: tagResults.length > 0 ? tagResults : []
21 | };
22 | } else {
23 | ctx.body = {
24 | success: 1,
25 | message: '',
26 | post: null,
27 | tags: []
28 | };
29 | }
30 | } catch (error) {
31 | console.log(error);
32 | ctx.body = {
33 | success: 0,
34 | message: '查询数据出错'
35 | };
36 | }
37 | }
38 | // 添加文章
39 | exports.addPost = async (ctx) => {
40 | let postData = ctx.request.body,
41 | tags = postData.tags,
42 | newPost = {
43 | title: postData.title,
44 | content: postData.content,
45 | categoryId: postData.categoryId,
46 | viewTotal: 0,
47 | status: postData.status,
48 | poster: '',
49 | createTime: moment().format('YYYY-MM-DD HH:mm:ss')
50 | };
51 | try {
52 | let insert = await ctx.execSql('INSERT INTO post SET ?', newPost);
53 | if (insert.affectedRows > 0) {
54 | let id = insert.insertId;
55 | if (tags.length > 0) {
56 | let updateTag = 'INSERT INTO post_tag (postId, tagId) values ';
57 | for (let tag of Object.values(tags)) {
58 | updateTag += `(${id}, ${tag.id}),`;
59 | }
60 | let tagSql = updateTag.substring(0, updateTag.length - 1);
61 | let insertTag = await ctx.execSql(tagSql);
62 | }
63 | ctx.body = {
64 | success: 1,
65 | id: id
66 | };
67 | } else {
68 | ctx.body = {
69 | success: 0,
70 | message: '添加文章出错'
71 | };
72 | }
73 | } catch (error) {
74 | console.log(error);
75 | ctx.body = {
76 | success: 0,
77 | message: '添加文章出错'
78 | };
79 | }
80 | }
81 | // 更新文章
82 | exports.updatePost = async (ctx) => {
83 | let id = ctx.params.id || 0,
84 | postData = ctx.request.body,
85 | tags = postData.tags,
86 | newPost = {
87 | title: postData.title,
88 | content: postData.content,
89 | categoryId: postData.categoryId,
90 | status: postData.status,
91 | poster: postData.poster
92 | };
93 | try {
94 | let result = await ctx.execSql('UPDATE post SET ? WHERE id = ?', [newPost, id]);
95 | let delResult = await ctx.execSql('DELETE FROM post_tag WHERE postId = ?', id);
96 | if (tags.length > 0) {
97 | let updateTag = 'INSERT INTO post_tag (postId, tagId) values ';
98 | for (let tag of Object.values(tags)) {
99 | updateTag += `(${id}, ${tag.id}),`;
100 | }
101 | let tagSql = updateTag.substring(0, updateTag.length - 1);
102 | let insertTag = await ctx.execSql(tagSql);
103 | }
104 | ctx.body = {
105 | success: 1
106 | };
107 | } catch (error) {
108 | console.log(error);
109 | ctx.body = {
110 | success: 0,
111 | message: '添加文章出错'
112 | };
113 | }
114 | }
115 | // 获取文章列表
116 | exports.getPostList = async (ctx) => {
117 | let page = ctx.query.page || 1,
118 | pageNum = ctx.query.pageNum || 10;
119 | let pageIndex = (page - 1) * pageNum < 0 ? 0 : (page - 1) * pageNum;
120 | let sql = ` SELECT post.id, post.title, post.createTime, post.status, post.categoryId,
121 | category.name AS categoryName FROM post
122 | LEFT JOIN category ON post.categoryId = category.id
123 | ORDER BY post.createTime DESC LIMIT ${pageIndex}, ${pageNum}`;
124 | try {
125 | let results = await ctx.execSql(sql);
126 | ctx.body = {
127 | success: 1,
128 | message: '',
129 | posts: results
130 | };
131 | } catch (error) {
132 | console.log(error);
133 | ctx.body = {
134 | success: 0,
135 | message: '查询数据出错',
136 | posts: null
137 | };
138 | }
139 | }
140 | // 获取文章总数
141 | exports.getPostTotal = async(ctx) => {
142 | try {
143 | let results = await ctx.execSql(`SELECT * FROM post`);
144 | ctx.body = {
145 | success: 1,
146 | message: '',
147 | total: results.length || 0
148 | };
149 | } catch (error) {
150 | console.log(error);
151 | ctx.body = {
152 | success: 0,
153 | message: '查询数据出错',
154 | total: 0
155 | };
156 | }
157 | }
158 | // 下架文章
159 | exports.offlinePost = async(ctx) => {
160 | let id = ctx.params.id || 0;
161 | try {
162 | let results = await ctx.execSql(`UPDATE post SET status = 'OFFLINE' WHERE id = ?`, id);
163 | ctx.body = {
164 | success: 1,
165 | message: ''
166 | };
167 | } catch (error) {
168 | console.log(error);
169 | ctx.body = {
170 | success: 0,
171 | message: '文章下线出错'
172 | };
173 | }
174 | }
175 | // 发布文章
176 | exports.publishPost = async(ctx) => {
177 | let id = ctx.params.id || 0;
178 | try {
179 | let results = await ctx.execSql(`UPDATE post SET status = 'PUBLISHED' WHERE id = ?`, id);
180 | ctx.body = {
181 | success: 1,
182 | message: ''
183 | };
184 | } catch (error) {
185 | console.log(error);
186 | ctx.body = {
187 | success: 0,
188 | message: '文章发布出错'
189 | };
190 | }
191 | }
192 | // 删除文章
193 | exports.deletePost = async(ctx) => {
194 | let id = ctx.params.id || 0;
195 | try {
196 | let results = await ctx.execSql(`DELETE FROM post WHERE id = ?`, id);
197 | ctx.body = {
198 | success: 1,
199 | message: ''
200 | };
201 | } catch (error) {
202 | console.log(error);
203 | ctx.body = {
204 | success: 0,
205 | message: '文章删除出错'
206 | };
207 | }
208 | }
--------------------------------------------------------------------------------
/server/api/admin/tag.controller.js:
--------------------------------------------------------------------------------
1 | // 获取所有标签
2 | exports.getTags = async (ctx) => {
3 | let sql = ` SELECT tag.id, tag.name, COUNT(post.id) AS count FROM tag
4 | LEFT JOIN post_tag ON tag.id = post_tag.tagId
5 | LEFT JOIN post ON post_tag.postId = post.id AND post.status = 'PUBLISHED'
6 | GROUP BY tag.id`;
7 | try {
8 | let results = await ctx.execSql(sql);
9 | let totalResult = await ctx.execSql(`SELECT COUNT(*) AS total FROM post`);
10 | ctx.body = {
11 | success: 1,
12 | message: '',
13 | tags: results.length > 0 ? results : [],
14 | total: totalResult.length > 0 ? totalResult[0].total : 0
15 | };
16 | } catch (error) {
17 | console.log(error);
18 | ctx.body = {
19 | success: 0,
20 | message: '查询数据出错'
21 | };
22 | }
23 | }
24 | // 获取当前文章的标签
25 | exports.getPostsByTagId = async (ctx) => {
26 | let id = ctx.params.id || 0,
27 | page = ctx.query.page || 1,
28 | pageNum = ctx.query.pageNum || 10,
29 | pageIndex = (page - 1) * pageNum < 0 ? 0 : (page - 1) * pageNum,
30 | fliter = id == 0 ? '' : ` WHERE post_tag.tagId = ${id} `,
31 | sql = ` SELECT post.id, post.title, post.createTime, post.status,
32 | post.categoryId, category.name AS categoryName
33 | FROM post LEFT JOIN category ON post.categoryId = category.id
34 | LEFT JOIN post_tag ON post.id = post_tag.postId ${fliter}
35 | GROUP BY post.id, post.title, post.createTime, post.status,
36 | post.categoryId, category.name
37 | ORDER BY post.createTime DESC LIMIT ${pageIndex}, ${pageNum}`;
38 | try {
39 | let results = await ctx.execSql(sql);
40 | ctx.body = {
41 | success: 1,
42 | message: '',
43 | posts: results
44 | };
45 | } catch (error) {
46 | console.log(error);
47 | ctx.body = {
48 | success: 0,
49 | message: '查询数据出错',
50 | posts: null
51 | };
52 | }
53 | }
54 | // 新增或修改文章时添加新标签(如果标签已存在,则返回标签id)
55 | exports.addNewTagWhenPost = async (ctx) => {
56 | let name = ctx.params.name || 0;
57 | try {
58 | let existName = await ctx.execSql(`SELECT * FROM tag WHERE name = ?`, name);
59 | if (existName.length > 0) {
60 | ctx.body = {
61 | success: 1,
62 | message: '',
63 | newId: existName[0].id
64 | };
65 | return false;
66 | }
67 | let results = await ctx.execSql(`INSERT INTO tag SET name = ?`, name);
68 | ctx.body = {
69 | success: 1,
70 | message: '',
71 | newId: results.insertId
72 | };
73 | } catch (error) {
74 | console.log(error);
75 | ctx.body = {
76 | success: 0,
77 | message: '添加新标签出错',
78 | newId: 0
79 | };
80 | }
81 | }
82 | // 添加文章标签
83 | exports.addNewTag = async (ctx) => {
84 | let name = ctx.params.name || 0;
85 | try {
86 | let existName = await ctx.execSql(`SELECT * FROM tag WHERE name = ?`, name);
87 | if (existName.length > 0) {
88 | ctx.body = {
89 | success: 0,
90 | message: '标签名称已存在!'
91 | };
92 | return false;
93 | }
94 | let results = await ctx.execSql(`INSERT INTO tag SET name = ?`, name);
95 | ctx.body = {
96 | success: 1,
97 | message: '',
98 | newId: results.insertId
99 | };
100 | } catch (error) {
101 | console.log(error);
102 | ctx.body = {
103 | success: 0,
104 | message: '添加新标签出错',
105 | newId: 0
106 | };
107 | }
108 | }
109 | // 更新文章标签
110 | exports.updateTag = async (ctx) => {
111 | let id = ctx.params.id || 0,
112 | name = ctx.query.name || '';
113 | try {
114 | let existName = await ctx.execSql(`SELECT * FROM tag WHERE name = ? AND id <> ?`, [name, id]);
115 | if (existName.length > 0) {
116 | ctx.body = {
117 | success: 0,
118 | message: '标签名称已存在!'
119 | };
120 | return false;
121 | }
122 | let results = await ctx.execSql(`UPDATE tag SET name = ? WHERE id = ?`, [name, id]);
123 | ctx.body = {
124 | success: 1,
125 | message: ''
126 | };
127 | } catch (error) {
128 | console.log(error);
129 | ctx.body = {
130 | success: 0,
131 | message: '更新标签出错'
132 | };
133 | }
134 | }
135 | // 删除标签
136 | exports.deleteTag = async (ctx) => {
137 | let id = ctx.params.id || 0;
138 | try {
139 | let results = await ctx.execSql(`DELETE FROM tag WHERE id = ?`, id);
140 | let results2 = await ctx.execSql(`DELETE FROM post_tag WHERE tagId = ?`, id);
141 | ctx.body = {
142 | success: 1,
143 | message: ''
144 | };
145 | } catch (error) {
146 | console.log(error);
147 | ctx.body = {
148 | success: 0,
149 | message: '删除标签出错'
150 | };
151 | }
152 | }
153 | // 搜索标签
154 | exports.searchTagByName = async (ctx) => {
155 | let name = ctx.params.name;
156 | try {
157 | let result = await ctx.execSql(`SELECT id, name FROM tag WHERE name like '${name}%'`);
158 | ctx.body = {
159 | success: 1,
160 | tags: result.length > 0 ? result : []
161 | };
162 | } catch (error) {
163 | console.log(error);
164 | ctx.body = {
165 | success: 0,
166 | message: '搜索标签出错'
167 | };
168 | }
169 | }
170 |
--------------------------------------------------------------------------------
/server/api/oauth/github.controller.js:
--------------------------------------------------------------------------------
1 | const config = require('../../config/environment');
2 | const sqlQuery = require('../../util/mysql-async');
3 | const fetch = require('node-fetch');
4 | const moment = require('moment');
5 | const jwt = require('jsonwebtoken');
6 |
7 | // 吊起github登录
8 | exports.githubOAuth = async (ctx) => {
9 | const code = ctx.query.code;
10 | // 接口
11 | let path = 'https://github.com/login/oauth/access_token';
12 | // 参数
13 | const params = {
14 | client_id: config.oAuth.github.client_id,
15 | client_secret: config.oAuth.github.client_secret,
16 | code: code
17 | };
18 | // 请求接口
19 | await fetch(path, {
20 | method: 'POST',
21 | headers: {
22 | 'Content-Type': 'application/json'
23 | },
24 | body: JSON.stringify(params)
25 | })
26 | .then(res => {
27 | return res.text();
28 | })
29 | .then(body => {
30 | const args = body.split('&');
31 | let arg = args[0].split('=');
32 | return arg[1];
33 | })
34 | .then(async (token) => {
35 | const url = ' https://api.github.com/user?access_token=' + token;
36 | await fetch(url)
37 | .then(res => {
38 | return res.json();
39 | })
40 | .then(async (res) => {
41 | let userId = 0;
42 | let selectGuest = await sqlQuery(`SELECT * FROM user WHERE role = 'GUEST' AND userName = ?`, [res.login]);
43 | if (selectGuest.length > 0) {
44 | userId = selectGuest[0].id;
45 | await sqlQuery(`UPDATE user SET avatar = ?, email = ? WHERE id = ?`, [res.avatar_url, res.email, userId]);
46 | } else {
47 | let newGuest = {
48 | userName: res.login,
49 | avatar: res.avatar_url,
50 | email: res.email,
51 | role: 'GUEST',
52 | createTime: moment().format('YYYY-MM-DD HH:mm:ss')
53 | };
54 | let insertGuest = await sqlQuery(`INSERT INTO user SET ?`, newGuest);
55 | if (insertGuest.affectedRows > 0) {
56 | userId = insertGuest.insertId;
57 | }
58 | }
59 | if (userId > 0) {
60 | ctx.session.user = res.login;
61 | // 用户token
62 | const userToken = {
63 | name: res.login,
64 | id: userId
65 | };
66 | // 签发token
67 | const token = jwt.sign(userToken, config.tokenSecret, {
68 | expiresIn: '2h'
69 | });
70 | ctx.body = {
71 | success: 1,
72 | token: token,
73 | id:userId,
74 | userName: res.login,
75 | avatar: res.avatar_url,
76 | message: ''
77 | };
78 | } else {
79 | ctx.body = {
80 | success: 0,
81 | token: '',
82 | message: 'GitHub授权登录失败'
83 | };
84 | }
85 | })
86 | })
87 | .catch(e => {
88 | console.log(e);
89 | ctx.body = {
90 | success: 0,
91 | token: '',
92 | message: 'GitHub授权登录失败'
93 | };
94 | });
95 | }
96 |
--------------------------------------------------------------------------------
/server/api/oauth/index.js:
--------------------------------------------------------------------------------
1 | const router = require('koa-router')();
2 | const github = require('./github.controller');
3 |
4 | // 获取github认证
5 | router.get('/github/github_oauth', github.githubOAuth);
6 |
7 | module.exports = router;
8 |
--------------------------------------------------------------------------------
/server/api/post/comment.controller.js:
--------------------------------------------------------------------------------
1 | const moment = require('moment');
2 | const jwt = require('jsonwebtoken');
3 | const config = require('../../config/environment');
4 | const util = require('util');
5 | const verify = util.promisify(jwt.verify);
6 |
7 | // 添加评论
8 | exports.addComment = async (ctx) => {
9 | let commentData = ctx.request.body.comment;
10 | commentData.createdTime = moment().format('YYYY-MM-DD HH:mm:ss');
11 | try {
12 | // 解密payload,获取用户名和ID
13 | let payload = await verify(ctx.request.body.token.split(' ')[1], config.tokenSecret);
14 | commentData.fromUserId = payload.id;
15 | } catch (error) {
16 | ctx.body = {
17 | success: -1,
18 | message: '用户登录信息已过期,请重新登录'
19 | };
20 | return;
21 | }
22 | try {
23 | let maxData = await ctx.execSql('SELECT IFNULL(max(number), 0) AS maxNumber FROM comments WHERE postId = ?', commentData.postId);
24 | let maxNumber = maxData[0].maxNumber + 1;
25 | commentData.number = maxNumber;
26 | let insert = await ctx.execSql('INSERT INTO comments SET ?', commentData);
27 | if (insert.affectedRows > 0) {
28 | ctx.body = {
29 | success: 1,
30 | data: {
31 | fromUserId: commentData.fromUserId,
32 | toUserId: commentData.toUserId,
33 | number: maxNumber
34 | }
35 | };
36 | } else {
37 | ctx.body = {
38 | success: 0,
39 | message: '添加评论错误'
40 | };
41 | }
42 | } catch (error) {
43 | console.log(error);
44 | ctx.body = {
45 | success: 0,
46 | message: '添加评论错误'
47 | };
48 | }
49 | }
50 | // 获取文章的评论
51 | exports.getCommentsByPostId = async (ctx) => {
52 | let postId = ctx.params.postId || 0;
53 | sql = `SELECT comments.*, user.avatar as fromAvatar, user.userName as fromUserName
54 | FROM comments
55 | LEFT JOIN user ON comments.fromUserId = user.id
56 | WHERE comments.postId = ? ORDER BY comments.createdTime DESC`
57 | try {
58 | let comments = await ctx.execSql(sql, postId);
59 | ctx.body = {
60 | success: 1,
61 | message: '',
62 | comments: comments
63 | };
64 | } catch (error) {
65 | console.log(error)
66 | ctx.body = {
67 | success: 0,
68 | message: '获取评论错误'
69 | };
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/server/api/post/post.js:
--------------------------------------------------------------------------------
1 | const router = require('koa-router')();
2 | const post = require('./post.controller');
3 | const comment = require('./comment.controller');
4 |
5 | // 获取文章列表
6 | router.get('/getPostList', post.getPostList);
7 | // 获取文章内容
8 | router.get('/getPost/:id', post.getPost);
9 | // 获取归档
10 | router.get('/getArchive', post.getArchive);
11 | // 根据类型搜索
12 | router.get('/category/:id', post.getPostsByCatId);
13 | // 根据标签搜索
14 | router.get('/tag/:id', post.getPostsByTagId);
15 | // 根据关键字搜索
16 | router.get('/keyword/:keyword', post.getPostsByKeyword);
17 | // 获取个人项目内容
18 | router.get('/getLaboratory', post.getLaboratory);
19 | // 添加评论
20 | router.post('/addComment', comment.addComment);
21 | // 获取文章评论
22 | router.get('/getCommentsByPostId/:postId', comment.getCommentsByPostId);
23 |
24 | module.exports = router;
--------------------------------------------------------------------------------
/server/api/track/track.controller.js:
--------------------------------------------------------------------------------
1 | const moment = require('moment');
2 | const fetch = require('node-fetch');
3 | const config = require('../../config/environment');
4 |
5 | // 添加事件踪迹
6 | exports.addEventTrack = async (ctx) => {
7 | let ip = ctx.request.ip.replace('::ffff:', '');
8 | let address = await fetch(config.aliCloudApi + ip, {
9 | method: 'GET',
10 | headers: {
11 | 'Authorization': `APPCODE ${config.aliCloud_APPCODE}`
12 | }
13 | })
14 | .then(res => {
15 | return res.json();
16 | })
17 | .catch(e => {
18 | console.log('addEventTrack(aliCloudApi-error):', e);
19 | });
20 | try {
21 | let postData = ctx.request.body;
22 | let eventTrack = {
23 | key: postData.key,
24 | ip: ip || '',
25 | data_id: postData.id || '',
26 | keyword: postData.keyword || '',
27 | province: address.province || '',
28 | city: address.city || '',
29 | createTime: moment().format('YYYY-MM-DD HH:mm:ss')
30 | };
31 | let insert = await ctx.execSql('INSERT INTO event_track SET ?', eventTrack);
32 | if (insert.affectedRows > 0) {
33 | ctx.body = {
34 | success: 1,
35 | message: ''
36 | };
37 | }
38 | } catch (error) {
39 | console.log('addEventTrack(insert-error):', error);
40 | ctx.body = {
41 | success: 0,
42 | message: '上传数据出错'
43 | };
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/server/api/track/track.js:
--------------------------------------------------------------------------------
1 | const router = require('koa-router')();
2 | const track = require('./track.controller');
3 |
4 | router.post('/addEventTrack', track.addEventTrack);
5 |
6 | module.exports = router;
--------------------------------------------------------------------------------
/server/app.js:
--------------------------------------------------------------------------------
1 | const session = require('koa-session2');
2 | const http = require('http');
3 | const fs = require('fs');
4 | const path = require('path');
5 | const app = require('./config/koa');
6 | const config = require('./config/environment');
7 | const query = require('./util/mysql-async');
8 | const Store = require('./util/redis-store');
9 | const draftSocket = require('./util/draft-socketio');
10 | const redisMysql = require('./util/redis-mysql');
11 |
12 | app.use(session({
13 | store: new Store(config.db.redis),
14 | ttl: 2 * 60 * 60 * 1000
15 | }));
16 |
17 | app.use(async (ctx, next) => {
18 | ctx.execSql = query;
19 | ctx.set('Access-Control-Allow-Origin', config.accessControlAllowOrigin);
20 | await next();
21 | });
22 |
23 | // routes
24 | fs.readdirSync(path.join(__dirname, 'routes')).forEach(function (file) {
25 | if (~file.indexOf('.js')) {
26 | app.use(require(path.join(__dirname, 'routes', file)).routes());
27 | }
28 | });
29 |
30 | app.use(function (ctx, next) {
31 | ctx.redirect('/404.html');
32 | });
33 |
34 | app.on('error', (error, ctx) => {
35 | console.log('something error ' + JSON.stringify(ctx.onerror));
36 | ctx.redirect('/500.html');
37 | })
38 |
39 | const server = http.createServer(app.callback())
40 | .listen(config.port)
41 | .on('listening', function () {
42 | console.log('server listening on: ' + config.port);
43 | });
44 | // 初始化websocket
45 | draftSocket.initSocket(server);
46 | // 初始化定时任务
47 | redisMysql.redisToMysqlTask();
--------------------------------------------------------------------------------
/server/config/environment/development.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | module.exports = {
3 | db: {
4 | mysql: {
5 | host: '127.0.0.1',
6 | user: 'root',
7 | password: '1234',
8 | database: 'my_vue_blog',
9 | connectionLimit: 10
10 | },
11 | redis: {
12 | port: 6379,
13 | host: '127.0.0.1',
14 | db: 3,
15 | options: {
16 | return_buffers: false,
17 | auth_pass: ''
18 | }
19 | }
20 | },
21 | oAuth: {
22 | github: {
23 | client_id: 'b0fbc6a7d4ff2b320158',
24 | client_secret: 'a02a9f6bac91f3acee2dc8aae86513bc2a94a6b6'
25 | }
26 | },
27 | root: path.normalize(__dirname + '/..'),
28 | appPath: 'src',
29 | tempUploads: 'tempUploads',
30 | uploads: 'uploads',
31 | port: 9001,
32 | tokenSecret: 'test',
33 | isUpdateAdmin: false,
34 | accessControlAllowOrigin: 'http://127.0.0.1:3000',
35 | adminName: 'admin',
36 | adminPassword: '123456',
37 | socketioPath: '/testsocketiopath',
38 | draftPostRedisKey: 'DRAFTPSOTKEY'
39 | };
40 |
--------------------------------------------------------------------------------
/server/config/environment/index.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | // 通过NODE_ENV来设置环境变量
3 | let env = process.env.NODE_ENV || 'development';
4 | env = env.toLowerCase();
5 | // 载入配置文件
6 | const file = path.resolve(__dirname, env);
7 | try {
8 | module.exports = require(file);
9 | } catch (error) {
10 | throw new Error(`You must set the environment variable: ${error}`);
11 | }
12 |
--------------------------------------------------------------------------------
/server/config/koa.js:
--------------------------------------------------------------------------------
1 | const Koa = require('koa');
2 | const koaJson = require('koa-json');
3 | const bodyParser = require('koa-bodyparser');
4 | const resource = require('koa-static');
5 | const path = require('path');
6 | const jwt = require('koa-jwt');
7 | const config = require('./environment');
8 | const tokenError = require('../middlreware/tokenError');
9 | const adminAccout = require('../util/admin-account');
10 | // admin账号通过配置写入到数据库中
11 |
12 | if (config.isUpdateAdmin) {
13 | adminAccout.saveAdminAccount();
14 | }
15 | const app = new Koa();
16 | app.use(tokenError());
17 | app.use(bodyParser());
18 | app.use(koaJson());
19 | app.use(resource(path.join(config.root, config.appPath)));
20 | app.use(jwt({
21 | secret: config.tokenSecret
22 | }).unless({
23 | path: [/^\/backapi\/admin\/login/, /^\/blogapi\//, /^\/blogapi\/oauth\//]
24 | }));
25 |
26 | module.exports = app
27 |
--------------------------------------------------------------------------------
/server/config/src/uploads/20191103/1572753935066.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dirkhe1051931999/vue-management/dd7f085e9921106430e1ca87ceaa77b58406f52b/server/config/src/uploads/20191103/1572753935066.png
--------------------------------------------------------------------------------
/server/config/src/uploads/20191103/1572753942038.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dirkhe1051931999/vue-management/dd7f085e9921106430e1ca87ceaa77b58406f52b/server/config/src/uploads/20191103/1572753942038.jpg
--------------------------------------------------------------------------------
/server/middlreware/tokenError.js:
--------------------------------------------------------------------------------
1 | const jwt = require('jsonwebtoken');
2 | const config = require('../config/environment');
3 | const util = require('util');
4 | const verify = util.promisify(jwt.verify);
5 | /**
6 | * 判断token是否可用
7 | */
8 | module.exports = function () {
9 | return async function (ctx, next) {
10 | if (ctx.path == '/favicon.ico') {
11 | ctx.res.end();
12 | }
13 | try {
14 | // 获取jwt
15 | const token = ctx.header.authorization;
16 | if (token) {
17 | try {
18 | // 解密payload,获取用户名和ID
19 | let payload = await verify(token.split(' ')[1], config.tokenSecret);
20 | ctx.user = {
21 | name: payload.name,
22 | id: payload.id
23 | };
24 | } catch (err) {
25 | console.log('token verify fail: ', err)
26 | }
27 | }
28 | await next();
29 | } catch (err) {
30 | if (err.status === 401) {
31 | ctx.status = 401;
32 | ctx.body = {
33 | success: 0,
34 | message: '认证失败'
35 | };
36 | } else {
37 | err.status = 404;
38 | ctx.body = {
39 | success: 0,
40 | message: '404'
41 | };
42 | }
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/server/routes/index.js:
--------------------------------------------------------------------------------
1 | const router = require('koa-router')();
2 | const post = require('../api/post/post');
3 | const admin = require('../api/admin/admin');
4 | const oauth = require('../api/oauth/index');
5 | // const track = require('../api/track/track');
6 |
7 | // 获取已经发布的内容
8 | router.use('/blogapi/post', post.routes(), post.allowedMethods());
9 | // 后台管理系统正在修改内容
10 | router.use('/backapi/admin', admin.routes(), admin.allowedMethods());
11 | // github吊起权限
12 | router.use('/blogapi/oauth', oauth.routes(), oauth.allowedMethods());
13 | // 获取用户地址
14 | // router.use('/blogapi/track', track.routes(), track.allowedMethods());
15 | module.exports = router;
16 |
--------------------------------------------------------------------------------
/server/util/admin-account.js:
--------------------------------------------------------------------------------
1 | const moment = require('moment');
2 | const helper = require('./helper');
3 | const sqlQuery = require('./mysql-async')
4 | const config = require('../config/environment');
5 | const account = {
6 | name: config.adminName,
7 | password: config.adminPassword
8 | };
9 | exports.saveAdminAccount = async function () {
10 | try {
11 | let salt = helper.makeSalt();
12 | let hashedPassword = helper.encryptPassword(account.password, salt);
13 |
14 | let selectAdmin = await sqlQuery(`SELECT * FROM user WHERE role='ADMIN'`);
15 | if (selectAdmin.length > 0) {
16 | let id = selectAdmin[0].id;
17 | await sqlQuery(`UPDATE user SET hashedPassword = ?, salt = ? WHERE id = ?`, [hashedPassword, salt, id]);
18 | } else {
19 | let newAdmin = {
20 | userName: account.name,
21 | hashedPassword: hashedPassword,
22 | salt: salt,
23 | role: 'ADMIN',
24 | createTime: moment().format('YYYY-MM-DD HH:mm:ss')
25 | };
26 | await sqlQuery(`INSERT INTO user SET ?`, newAdmin);
27 | }
28 | } catch (error) {
29 | console.log('saveAdminAccount', error);
30 | }
31 | };
32 |
--------------------------------------------------------------------------------
/server/util/draft-redis.js:
--------------------------------------------------------------------------------
1 | const Redis = require('ioredis');
2 |
3 | class DraftRedis {
4 | constructor(redisConfig) {
5 | this.redis = new Redis(redisConfig);
6 | }
7 |
8 | async get(key) {
9 | let data = await this.redis.get(key);
10 | return JSON.parse(data);
11 | }
12 |
13 | async set(key, data, maxAge = 7 * 24 * 60 * 60 * 1000) {
14 | try {
15 | // Use redis set EX to automatically drop expired sessions
16 | await this.redis.set(key, JSON.stringify(data), 'EX', maxAge / 1000);
17 | } catch (e) {}
18 | return 'success';
19 | }
20 |
21 | async destroy(key) {
22 | return await this.redis.del(key);
23 | }
24 | }
25 |
26 | module.exports = DraftRedis;
27 |
--------------------------------------------------------------------------------
/server/util/draft-socketio.js:
--------------------------------------------------------------------------------
1 | const SocketIO = require('socket.io');
2 | const config = require('../config/environment');
3 | const DraftRedis = require('./draft-redis');
4 | const redisMysql = require('./redis-mysql');
5 | const draftPostRedisKey = config.draftPostRedisKey;
6 |
7 | exports.initSocket = function (server) {
8 | console.log('init websocket');
9 | //socket.io
10 | let socketHandle = SocketIO(server, {
11 | serveClient: true,
12 | path: config.socketioPath
13 | });
14 |
15 | let draftRedis = new DraftRedis(config.db.redis);
16 | socketHandle.on('connection', function (socket) {
17 | console.log('socket connected');
18 | // 离开编辑文章页面
19 | socket.on('disconnect', function () {
20 | console.info('[%s] DISCONNECTED', socket.sid);
21 | });
22 | // 进入新增文章页面,获取已保存的草稿(可以为空)
23 | socket.on('getDraftPost', async function () {
24 | let data = await draftRedis.get(draftPostRedisKey);
25 | if (!data) {
26 | data = await redisMysql.getDraftPostFromMysql();
27 | socket.emit('getDraftPost', data);
28 | await draftRedis.set(draftPostRedisKey, data);
29 | } else {
30 | socket.emit('getDraftPost', data);
31 | }
32 | })
33 | // 实时保存文章内容
34 | socket.on('saveDraftPost', async function (data) {
35 | let res = await draftRedis.set(draftPostRedisKey, data);
36 | socket.emit('saveDraftPost', res);
37 | })
38 | // 保存后清空已保存的文章草稿
39 | socket.on('clearDraftPost', async function () {
40 | await draftRedis.destroy(draftPostRedisKey);
41 | await redisMysql.clearDraftPostOfMysql();
42 | socket.emit('clearDraftPost', true);
43 | })
44 | })
45 | }
46 |
--------------------------------------------------------------------------------
/server/util/helper.js:
--------------------------------------------------------------------------------
1 | const crypto = require('crypto');
2 | const formidable = require('formidable');
3 | const fs = require('fs');
4 | const path = require('path');
5 | const config = require('../config/environment');
6 | const moment = require('moment');
7 |
8 | /**
9 | * Make salt
10 | *
11 | * @return {String}
12 | * @api public
13 | */
14 | exports.makeSalt = () => {
15 | return crypto.randomBytes(16).toString('base64');
16 | };
17 |
18 | /**
19 | * Encrypt password
20 | *
21 | * @param {String} password
22 | * @return {String}
23 | * @api public
24 | */
25 | exports.encryptPassword = (password, salt) => {
26 | if (!password || !salt) return '';
27 | var salt = new Buffer(salt, 'base64');
28 | let sugar = "!@A#$Q%W^E&R*T()_+a_1";
29 | let newPassword = crypto.createHash("md5").update(sugar + password).digest('hex');
30 | return crypto
31 | .pbkdf2Sync(newPassword, salt, 10000, 64, 'sha1')
32 | .toString('base64');
33 | };
34 | /**
35 | * Encrypt input password
36 | *
37 | * @param {String} password
38 | * @return {String}
39 | * @api public
40 | */
41 | exports.encryptInputPassword = (password, salt) => {
42 | if (!password || !salt) return '';
43 | var salt = new Buffer(salt, 'base64');
44 | return crypto
45 | .pbkdf2Sync(password, salt, 10000, 64, 'sha1')
46 | .toString('base64');
47 | };
48 |
49 | function mkdirsSync(dirname) {
50 | if (fs.existsSync(dirname)) {
51 | return true;
52 | } else {
53 | if (mkdirsSync(path.dirname(dirname))) {
54 | fs.mkdirSync(dirname);
55 | return true;
56 | }
57 | }
58 | }
59 | /**
60 | * 上传文件
61 | */
62 | exports.uploadFile = (ctx) => {
63 | return new Promise((resolve, reject) => {
64 | let form = new formidable.IncomingForm();
65 | form.encoding = 'utf-8';
66 | form.keepExtensions = true; // 保留后缀
67 | form.maxFieldsSize = 2 * 1024 * 1024; // 文件大小2M
68 | form.multiples = true;
69 |
70 | form.uploadDir = path.join(config.root, config.appPath, config.tempUploads);
71 | mkdirsSync(form.uploadDir);
72 | // 解析文件
73 | form.parse(ctx.req, (err, fields, files) => {
74 | console.log("->>>>>>>", fields)
75 | if (err) {
76 | reject(err)
77 | }
78 | let data = JSON.parse(fields.data);
79 | // 更新时未修改图片的情况
80 | if (files.uploadFile === undefined && data.poster !== '') {
81 | return resolve({
82 | fields: data,
83 | filePath: data.poster
84 | })
85 | }
86 | let filePath = '';
87 | // 如果提交文件的form中将上传文件的input名设置为uploadFile,就从uploadFile中取上传文件。否则取for in循环第一个上传的文件。
88 | if (files.uploadFile) {
89 | filePath = files.uploadFile.path;
90 | } else {
91 | for (let key in files) {
92 | if (files[key].path && filePath === '') {
93 | filePath = files[key].path;
94 | break;
95 | }
96 | }
97 | }
98 | //文件移动的目录文件夹,不存在时创建目标文件夹
99 | let dirName = moment().format('YYYYMMDD');
100 | let targetDir = path.join(config.root, config.appPath, config.uploads, dirName);
101 | mkdirsSync(targetDir);
102 |
103 | //以当前时间戳对上传文件进行重命名
104 | let fileExt = filePath.substring(filePath.lastIndexOf('.'));
105 | let fileName = new Date().getTime() + fileExt;
106 | let targetFile = path.join(targetDir, fileName);
107 | //移动文件
108 | fs.rename(filePath, targetFile, function (err) {
109 | if (err) {
110 | reject(err);
111 | } else {
112 | //上传成功,返回文件的相对路径
113 | return resolve({
114 | fields: data,
115 | filePath: path.join(path.sep, config.uploads, dirName, fileName)
116 | })
117 | }
118 | });
119 | })
120 | ctx.session.fileUploadProgress = 0
121 | // 文件上传中事件
122 | form.on('progress', (bytesReceived, bytesExpected) => {
123 | // 百分比
124 | let percent = Math.round(bytesReceived / bytesExpected * 100)
125 | console.log('precent', percent)
126 | ctx.session.fileUploadProgress = percent
127 | })
128 | form.on('end', () => {
129 | ctx.session.fileUploadProgress = 100
130 | })
131 | })
132 | }
133 |
--------------------------------------------------------------------------------
/server/util/mysql-async.js:
--------------------------------------------------------------------------------
1 | const mysql = require('mysql');
2 | const config = require('../config/environment');
3 |
4 | const pool = mysql.createPool({
5 | host : config.db.mysql.host,
6 | user : config.db.mysql.user,
7 | password : config.db.mysql.password,
8 | database : config.db.mysql.database,
9 | connectionLimit: config.db.mysql.connectionLimit
10 | })
11 | let query = function( sql, values ) {
12 | return new Promise(( resolve, reject ) => {
13 | pool.getConnection(function(err, connection) {
14 | if (err) {
15 | return reject(err);
16 | } else {
17 | connection.query(sql, values, (err,rows) => {
18 | connection.release();
19 | if (err) {
20 | return reject(err)
21 | } else {
22 | return resolve(rows);
23 | }
24 | })
25 | }
26 | })
27 | })
28 | }
29 |
30 | module.exports = query;
--------------------------------------------------------------------------------
/server/util/redis-mysql.js:
--------------------------------------------------------------------------------
1 | const schedule = require('node-schedule');
2 | const moment = require('moment');
3 | const sqlQuery = require('./mysql-async');
4 | const DraftRedis = require('./draft-redis');
5 | const config = require('../config/environment');
6 | const draftPostRedisKey = config.draftPostRedisKey;
7 |
8 | // redis向mysql中写入数据定时任务
9 | exports.redisToMysqlTask = function () {
10 | let draftRedis = new DraftRedis(config.db.redis);
11 | // 每天凌晨3点执行任务
12 | let rule = new schedule.RecurrenceRule();
13 | rule.hour = 3;
14 | rule.minute = 0;
15 | schedule.scheduleJob(rule, async function () {
16 | console.log('定时任务开始执行!', moment().format('YYYY-MM-DD HH:mm:ss'));
17 | let redisPost = await draftRedis.get(draftPostRedisKey);
18 | if (redisPost) {
19 | let redisPostData = JSON.parse(redisPost);
20 | let sqlPost = {
21 | postId: redisPostData.id,
22 | title: redisPostData.title,
23 | content: redisPostData.content,
24 | categoryId: redisPostData.categoryId,
25 | tags: JSON.stringify(redisPostData.tags),
26 | poster: redisPostData.poster
27 | };
28 | let selectResult = await sqlQuery('SELECT * FROM draft_post_redis WHERE redisKey = ?', draftPostRedisKey);
29 | if (selectResult && selectResult.length > 0) {
30 | console.log('update');
31 | await sqlQuery('UPDATE draft_post_redis SET ? WHERE id = ?', [sqlPost, selectResult[0].id]);
32 | } else {
33 | console.log('insert');
34 | sqlPost.redisKey = draftPostRedisKey;
35 | await sqlQuery('INSERT INTO draft_post_redis SET ?', sqlPost);
36 | }
37 | }
38 | console.log('redis向mysql中写入数据完成!', moment().format('YYYY-MM-DD HH:mm:ss'));
39 | })
40 | }
41 | exports.getDraftPostFromMysql = async function () {
42 | let selectResult = await sqlQuery('SELECT * FROM draft_post_redis WHERE redisKey = ?', draftPostRedisKey);
43 | if (selectResult && selectResult.length > 0) {
44 | var redisPost = selectResult[0];
45 | return {
46 | id: redisPost.postId,
47 | title: redisPost.title,
48 | content: redisPost.content,
49 | categoryId: redisPost.categoryId,
50 | tags: JSON.parse(redisPost.tags),
51 | poster: redisPost.poster
52 | };
53 | }
54 | return {};
55 | }
56 |
57 | exports.clearDraftPostOfMysql = async function () {
58 | let selectResult = await sqlQuery('DELETE FROM draft_post_redis WHERE redisKey = ?', draftPostRedisKey);
59 | return true;
60 | }
61 |
--------------------------------------------------------------------------------
/server/util/redis-store.js:
--------------------------------------------------------------------------------
1 | const Redis = require('ioredis');
2 | const {
3 | Store
4 | } = require('koa-session2');
5 | class RedisStore extends Store {
6 | constructor(redisConfig) {
7 | super();
8 | this.redis = new Redis(redisConfig);
9 | }
10 |
11 | async get(sid, ctx) {
12 | let data = await this.redis.get(`SESSION:${sid}`);
13 | return JSON.parse(data);
14 | }
15 |
16 | async set(session, {
17 | sid = this.getID(24),
18 | maxAge = 1000000
19 | } = {}, ctx) {
20 | try {
21 | // Use redis set EX to automatically drop expired sessions
22 | await this.redis.set(`SESSION:${sid}`, JSON.stringify(session), 'EX', maxAge / 1000);
23 | return true;
24 | } catch (e) {
25 | return false;
26 | }
27 | }
28 |
29 | async destroy(sid, ctx) {
30 | return await this.redis.del(`SESSION:${sid}`);
31 | }
32 | }
33 |
34 | module.exports = RedisStore;
35 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
15 |
--------------------------------------------------------------------------------
/src/api/blog/category.js:
--------------------------------------------------------------------------------
1 | import _axios from "@/utils/axios";
2 | import { settings } from "./config";
3 | const baseUrl = settings.baseUrl;
4 | export default {
5 | getCategories() {
6 | return _axios(`${baseUrl}/getCategories`);
7 | },
8 | addNewCategory(name) {
9 | return _axios(`${baseUrl}/addNewCategory/${name}`, {}, "put");
10 | },
11 | updateCategory(id, name) {
12 | return _axios(`${baseUrl}/updateCategory/${id}?name=${name}`, {}, "put");
13 | },
14 | deleteCategory(id) {
15 | return _axios(`${baseUrl}/deleteCategory/${id}`, {}, "delete");
16 | }
17 | };
18 |
--------------------------------------------------------------------------------
/src/api/blog/config.js:
--------------------------------------------------------------------------------
1 | let settings = {
2 | baseUrl: "/backapi/admin"
3 | };
4 | export { settings };
5 |
--------------------------------------------------------------------------------
/src/api/blog/post.js:
--------------------------------------------------------------------------------
1 | import _axios from "@/utils/axios";
2 | import { settings } from "./config";
3 | const baseUrl = settings.baseUrl;
4 | export default {
5 | getPostById(id) {
6 | return _axios(`${baseUrl}/getPostById/${id}`);
7 | },
8 | addPost(params) {
9 | return _axios(`${baseUrl}/addPost`, params, "post");
10 | },
11 | updatePost(id, params) {
12 | return _axios(`${baseUrl}/updatePost/${id}`, params, "post");
13 | },
14 | getPostList(page = 1, pageNum = 10) {
15 | return _axios(`${baseUrl}/getPostList`, {
16 | page: page,
17 | pageNum: pageNum
18 | });
19 | },
20 | getPostTotal() {
21 | return _axios(`${baseUrl}/getPostTotal`);
22 | },
23 | offlinePost(id) {
24 | return _axios(`${baseUrl}/offlinePost/${id}`, {}, "put");
25 | },
26 | publishPost(id) {
27 | return _axios(`${baseUrl}/publishPost/${id}`, {}, "put");
28 | },
29 | deletePost(id) {
30 | return _axios(`${baseUrl}/deletePost/${id}`, {}, "delete");
31 | },
32 | getPostsByCatId(id, page = 1, pageNum = 10) {
33 | return _axios(`${baseUrl}/getPostsByCatId/${id}`, {
34 | page: page,
35 | pageNum: pageNum
36 | });
37 | },
38 | getPostsByTagId(id, page = 1, pageNum = 10) {
39 | return _axios(`${baseUrl}/getPostsByTagId/${id}`, {
40 | page: page,
41 | pageNum: pageNum
42 | });
43 | }
44 | };
45 |
--------------------------------------------------------------------------------
/src/api/blog/project.js:
--------------------------------------------------------------------------------
1 | import _axios from "@/utils/axios";
2 | import { settings } from "./config";
3 | const baseUrl = settings.baseUrl;
4 | export default {
5 | getLaboratories() {
6 | return _axios(`${baseUrl}/getLaboratories`);
7 | },
8 | createNewLaboratory(params) {
9 | return _axios(`${baseUrl}/createNewLaboratory`, params, "post", true);
10 | },
11 | updateLaboratory(params) {
12 | return _axios(`${baseUrl}/updateLaboratory`, params, "post", true);
13 | },
14 | deleteLaboratory(id) {
15 | return _axios(`${baseUrl}/deleteLaboratory/${id}`, {}, "delete");
16 | }
17 | };
18 |
--------------------------------------------------------------------------------
/src/api/blog/tag.js:
--------------------------------------------------------------------------------
1 | import _axios from "@/utils/axios";
2 | import { settings } from "./config";
3 | const baseUrl = settings.baseUrl;
4 |
5 | export default {
6 | getTags() {
7 | return _axios(`${baseUrl}/getTags`);
8 | },
9 | addNewTagWhenPost(name) {
10 | return _axios(`${baseUrl}/addNewTagWhenPost/${name}`, {}, "put");
11 | },
12 | addNewTag(name) {
13 | return _axios(`${baseUrl}/addNewTag/${name}`, {}, "put");
14 | },
15 | updateTag(id, name) {
16 | return _axios(`${baseUrl}/updateTag/${id}?name=${name}`, {}, "put");
17 | },
18 | deleteTag(id) {
19 | return _axios(`${baseUrl}/deleteTag/${id}`, {}, "delete");
20 | },
21 | searchTagByName(name) {
22 | return _axios(`${baseUrl}/searchTagByName/${name}`);
23 | }
24 | };
25 |
--------------------------------------------------------------------------------
/src/api/blog/user.js:
--------------------------------------------------------------------------------
1 | import _axios from "@/utils/axios";
2 | import { settings } from "./config";
3 | const baseUrl = settings.baseUrl;
4 | export default {
5 | singUp(userName, password) {
6 | return _axios(
7 | `${baseUrl}/login`,
8 | {
9 | userName: userName,
10 | password: password
11 | },
12 | "post"
13 | );
14 | },
15 | signOut() {
16 | return _axios(`${baseUrl}/signOut`);
17 | }
18 | };
19 |
--------------------------------------------------------------------------------
/src/api/index.js:
--------------------------------------------------------------------------------
1 | import category from "./blog/category";
2 | import project from "./blog/project";
3 | import post from "./blog/post";
4 | import user from "./blog/user";
5 | import tag from "./blog/tag";
6 | export default {
7 | ...category,
8 | ...project,
9 | ...post,
10 | ...user,
11 | ...tag
12 | };
13 |
--------------------------------------------------------------------------------
/src/assets/404_images/404.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dirkhe1051931999/vue-management/dd7f085e9921106430e1ca87ceaa77b58406f52b/src/assets/404_images/404.png
--------------------------------------------------------------------------------
/src/assets/404_images/404_cloud.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dirkhe1051931999/vue-management/dd7f085e9921106430e1ca87ceaa77b58406f52b/src/assets/404_images/404_cloud.png
--------------------------------------------------------------------------------
/src/assets/user/admin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dirkhe1051931999/vue-management/dd7f085e9921106430e1ca87ceaa77b58406f52b/src/assets/user/admin.png
--------------------------------------------------------------------------------
/src/assets/user/user.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dirkhe1051931999/vue-management/dd7f085e9921106430e1ca87ceaa77b58406f52b/src/assets/user/user.png
--------------------------------------------------------------------------------
/src/components/Breadcrumb/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
11 | {{ item.meta.title }}
15 | {{ item.meta.title }}
19 |
20 |
21 |
22 |
23 |
24 |
93 |
94 |
107 |
--------------------------------------------------------------------------------
/src/components/Hamburger/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 |
35 |
36 |
48 |
--------------------------------------------------------------------------------
/src/components/SvgIcon/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
18 |
19 |
20 |
63 |
64 |
79 |
--------------------------------------------------------------------------------
/src/components/markdown/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
43 |
44 |
--------------------------------------------------------------------------------
/src/components/postTable/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | {{ scope.row.createTime | formatTime }}
11 |
12 |
13 |
14 |
15 | {{scope.row.status | setStatus}}
16 | {{scope.row.status | setStatus}}
17 | {{scope.row.status | setStatus}}
21 |
22 |
23 |
24 |
25 | 编辑
31 | 下线
38 | 上线
45 | 删除
51 |
52 |
53 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/src/components/project/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
![项目图片]()
8 |
9 |
10 |
11 |
12 |
19 |
26 |
27 |
28 |
29 |
30 |
31 |
项目名称
32 |
{{project.name}}
33 |
34 |
35 |
项目描述
36 |
{{project.description}}
37 |
38 |
39 |
40 |
41 |
42 |
49 |
94 |
95 |
99 |
100 |
101 |
102 |
103 |
217 |
218 |
271 |
--------------------------------------------------------------------------------
/src/components/upload/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
![]()
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
129 |
130 |
178 |
--------------------------------------------------------------------------------
/src/icons/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import SvgIcon from '@/components/SvgIcon'; // svg component
3 |
4 | // main.js中注册一个全局 svg-icon组件,可根据.svg文件生成
5 | Vue.component('svg-icon', SvgIcon);
6 |
7 | const req = require.context('./svg', false, /\.svg$/);
8 | const requireAll = requireContext => {
9 | return requireContext.keys().map(requireContext);
10 | };
11 | requireAll(req);
12 |
--------------------------------------------------------------------------------
/src/icons/svg/add.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/svg/button.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/svg/dashboard.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/svg/delete.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/svg/edit.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/svg/example.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/svg/eye-open.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/svg/eye.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/svg/form.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/svg/link.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/svg/list.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/svg/nested.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/svg/password.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/svg/table.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/svg/tree.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/svg/user.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/svgo.yml:
--------------------------------------------------------------------------------
1 | # replace default config
2 |
3 | # multipass: true
4 | # full: true
5 |
6 | plugins:
7 |
8 | # - name
9 | #
10 | # or:
11 | # - name: false
12 | # - name: true
13 | #
14 | # or:
15 | # - name:
16 | # param1: 1
17 | # param2: 2
18 |
19 | - removeAttrs:
20 | attrs:
21 | - 'fill'
22 | - 'fill-rule'
23 |
--------------------------------------------------------------------------------
/src/layout/components/AppMain.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 |
22 |
23 |
35 |
36 |
44 |
--------------------------------------------------------------------------------
/src/layout/components/Navbar.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
11 |
12 |
29 |
30 |
31 |
32 |
79 |
80 |
158 |
--------------------------------------------------------------------------------
/src/layout/components/Sidebar/FixiOSBug.js:
--------------------------------------------------------------------------------
1 | export default {
2 | computed: {
3 | device() {
4 | return this.$store.state.app.device;
5 | }
6 | },
7 | mounted() {
8 | this.fixBugIniOS();
9 | },
10 | methods: {
11 | fixBugIniOS() {
12 | // 为了修复ios设备上的菜单单击,将触发mouseleave错误
13 | const $subMenu = this.$refs.subMenu;
14 | if ($subMenu) {
15 | const handleMouseleave = $subMenu.handleMouseleave;
16 | $subMenu.handleMouseleave = e => {
17 | if (this.device === 'mobile') {
18 | return;
19 | }
20 | handleMouseleave(e);
21 | };
22 | }
23 | }
24 | }
25 | };
26 |
--------------------------------------------------------------------------------
/src/layout/components/Sidebar/Item.vue:
--------------------------------------------------------------------------------
1 |
31 |
--------------------------------------------------------------------------------
/src/layout/components/Sidebar/Link.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
37 |
--------------------------------------------------------------------------------
/src/layout/components/Sidebar/Logo.vue:
--------------------------------------------------------------------------------
1 |
2 |
35 |
36 |
37 |
53 |
54 |
103 |
--------------------------------------------------------------------------------
/src/layout/components/Sidebar/SidebarItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
48 |
49 |
50 |
118 |
--------------------------------------------------------------------------------
/src/layout/components/Sidebar/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
10 |
20 |
21 |
27 |
28 |
29 |
30 |
31 |
32 |
66 |
--------------------------------------------------------------------------------
/src/layout/components/index.js:
--------------------------------------------------------------------------------
1 | export { default as Navbar } from './Navbar'
2 | export { default as Sidebar } from './Sidebar'
3 | export { default as AppMain } from './AppMain'
4 |
--------------------------------------------------------------------------------
/src/layout/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
16 |
17 |
18 |
56 |
57 |
98 |
--------------------------------------------------------------------------------
/src/layout/mixin/ResizeHandler.js:
--------------------------------------------------------------------------------
1 | import store from '@/store';
2 |
3 | const { body } = document;
4 | const WIDTH = 992; // refer to Bootstrap's responsive design
5 |
6 | export default {
7 | watch: {
8 | $route(route) {
9 | if (this.device === 'mobile' && this.sidebar.opened) {
10 | store.dispatch('app/closeSideBar', { withoutAnimation: false });
11 | }
12 | }
13 | },
14 | beforeMount() {
15 | window.addEventListener('resize', this.$_resizeHandler);
16 | },
17 | beforeDestroy() {
18 | window.removeEventListener('resize', this.$_resizeHandler);
19 | },
20 | mounted() {
21 | const isMobile = this.$_isMobile();
22 | if (isMobile) {
23 | store.dispatch('app/toggleDevice', 'mobile');
24 | store.dispatch('app/closeSideBar', { withoutAnimation: true });
25 | }
26 | },
27 | methods: {
28 | // use $_ for mixins properties
29 | // https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential
30 | $_isMobile() {
31 | const rect = body.getBoundingClientRect();
32 | return rect.width - 1 < WIDTH;
33 | },
34 | $_resizeHandler() {
35 | if (!document.hidden) {
36 | const isMobile = this.$_isMobile();
37 | store.dispatch('app/toggleDevice', isMobile ? 'mobile' : 'desktop');
38 |
39 | if (isMobile) {
40 | store.dispatch('app/closeSideBar', { withoutAnimation: true });
41 | }
42 | }
43 | }
44 | }
45 | };
46 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | import Vue from "vue";
2 | // CSS重置的现代替代方法
3 | import "normalize.css/normalize.css";
4 | // ElementUI
5 | import ElementUI from "element-ui";
6 | import "element-ui/lib/theme-chalk/index.css";
7 | // global css
8 | import "@/styles/index.scss";
9 | import App from "./App";
10 | import store from "./store";
11 | import router from "./router";
12 | // socket
13 | import VueSocketio from "vue-socket.io";
14 | import socketio from "socket.io-client";
15 | // VCharts
16 | import VCharts from "v-charts";
17 | // icon
18 | import "@/icons";
19 | // 权限验证
20 | import "@/permission";
21 | Vue.use(ElementUI);
22 | Vue.use(
23 | VueSocketio,
24 | socketio("http://127.0.0.1:9001", {
25 | path: "/testsocketiopath"
26 | })
27 | );
28 | Vue.use(VCharts);
29 | Vue.config.productionTip = false;
30 | new Vue({
31 | el: "#app",
32 | router,
33 | store,
34 | render: h => h(App)
35 | });
36 |
--------------------------------------------------------------------------------
/src/permission.js:
--------------------------------------------------------------------------------
1 | import router from "./router";
2 | import api from "@/api";
3 | import { Message } from "element-ui";
4 | import NProgress from "nprogress";
5 | import "nprogress/nprogress.css";
6 | import getPageTitle from "@/utils/get-page-title";
7 |
8 | NProgress.configure({ showSpinner: true });
9 | const whiteList = ["/login"];
10 | router.beforeEach(async (to, from, next) => {
11 | NProgress.start();
12 | document.title = getPageTitle(to.meta.title);
13 | try {
14 | const result = await api.getCategories();
15 | if (to.path === "/login") {
16 | next({ path: "/" });
17 | NProgress.done();
18 | } else {
19 | next();
20 | }
21 | } catch (e) {
22 | if (e.response.status === 401) {
23 | if (whiteList.indexOf(to.path) !== -1) {
24 | next();
25 | } else {
26 | next(`/login?redirect=${to.path}`);
27 | Message.error("无权限访问页面");
28 | NProgress.done();
29 | }
30 | }
31 | }
32 | });
33 |
34 | router.afterEach(() => {
35 | NProgress.done();
36 | });
37 |
--------------------------------------------------------------------------------
/src/router/index.js:
--------------------------------------------------------------------------------
1 | import Vue from "vue";
2 | import Router from "vue-router";
3 |
4 | Vue.use(Router);
5 |
6 | /* Layout */
7 | import Layout from "@/layout";
8 |
9 | /**
10 | * Note: sub-menu only appear when route children.length >= 1
11 | *
12 | * hidden: true if set true, item will not show in the sidebar(default is false)
13 | * alwaysShow: true if set true, will always show the root menu
14 | * if not set alwaysShow, when item has more than one children route,
15 | * it will becomes nested mode, otherwise not show the root menu
16 | * redirect: noRedirect if set noRedirect will no redirect in the breadcrumb
17 | * name:'router-name' the name is used by (must set!!!)
18 | * meta : {
19 | roles: ['admin','editor'] control the page roles (you can set multiple roles)
20 | title: 'title' the name show in sidebar and breadcrumb (recommend set)
21 | icon: 'svg-name' the icon show in the sidebar
22 | breadcrumb: false if set false, the item will hidden in breadcrumb(default is true)
23 | activeMenu: '/example/list' if set path, the sidebar will highlight the path you set
24 | }
25 | */
26 |
27 | /**
28 | * constantRoutes
29 | * a base page that does not have permission requirements
30 | * all roles can be accessed
31 | */
32 | export const constantRoutes = [
33 | // 404页
34 | {
35 | path: "/404",
36 | component: () => import("@/views/404"),
37 | hidden: true
38 | },
39 | { path: "*", redirect: "/404", hidden: true },
40 | // 登录页
41 | {
42 | path: "/login",
43 | component: () => import("@/views/login/index"),
44 | hidden: true
45 | },
46 | // 仪表盘
47 | {
48 | path: "/",
49 | component: Layout,
50 | redirect: "/dashboard",
51 | children: [
52 | {
53 | path: "dashboard",
54 | name: "Dashboard",
55 | component: () => import("@/views/dashboard/index"),
56 | meta: { title: "仪表盘", icon: "dashboard" }
57 | }
58 | ]
59 | },
60 | // 博客后台
61 | {
62 | path: "/blog",
63 | component: Layout,
64 | redirect: "/blog/articleList",
65 | name: "博客后台",
66 | meta: {
67 | title: "博客后台",
68 | icon: "example",
69 | activeMenu: "/blog/articleList"
70 | },
71 | children: [
72 | // 文章列表
73 | {
74 | path: "articleList",
75 | name: "ArticleList",
76 | component: () => import("@/views/articleList/index"),
77 | meta: { title: "文章列表", icon: "list" }
78 | },
79 | // 编辑文章
80 | {
81 | path: "editArticle",
82 | name: "EditArticle",
83 | component: () => import("@/views/editArticle/index"),
84 | meta: { title: "编辑文章", icon: "list" }
85 | },
86 | // 分类管理
87 | {
88 | path: "sortManager",
89 | name: "SortManager",
90 | component: () => import("@/views/sortManager/index"),
91 | meta: { title: "分类管理", icon: "list" }
92 | },
93 | // 标签管理
94 | {
95 | path: "labelManager",
96 | name: "LabelManager",
97 | component: () => import("@/views/labelManager/index"),
98 | meta: { title: "标签管理", icon: "list" }
99 | },
100 | // 个人项目
101 | {
102 | path: "myProject",
103 | name: "MyProject",
104 | component: () => import("@/views/myProject/index"),
105 | meta: { title: "个人项目", icon: "list" }
106 | }
107 | ]
108 | }
109 | ];
110 |
111 | const createRouter = () =>
112 | new Router({
113 | mode: "hash", // require service support
114 | scrollBehavior: () => ({ y: 0 }),
115 | routes: constantRoutes
116 | });
117 |
118 | const router = createRouter();
119 |
120 | // Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465
121 | export function resetRouter() {
122 | const newRouter = createRouter();
123 | router.matcher = newRouter.matcher; // reset router
124 | }
125 |
126 | export default router;
127 |
--------------------------------------------------------------------------------
/src/settings.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | title: "后台管理平台",
3 |
4 | /**
5 | * @type {boolean} true | false
6 | * @description Whether fix the header
7 | */
8 | fixedHeader: true,
9 |
10 | /**
11 | * @type {boolean} true | false
12 | * @description Whether show the logo in sidebar
13 | */
14 | sidebarLogo: false
15 | };
16 |
--------------------------------------------------------------------------------
/src/store/getters.js:
--------------------------------------------------------------------------------
1 | const getters = {
2 | sidebar: state => state.app.sidebar,
3 | device: state => state.app.device,
4 | token: state => state.user.token,
5 | avatar: state => state.user.avatar,
6 | name: state => state.user.name
7 | }
8 | export default getters
9 |
--------------------------------------------------------------------------------
/src/store/index.js:
--------------------------------------------------------------------------------
1 | import Vue from "vue";
2 | import Vuex from "vuex";
3 | import getters from "./getters";
4 | import app from "./modules/app";
5 | import settings from "./modules/settings";
6 |
7 | Vue.use(Vuex);
8 |
9 | const store = new Vuex.Store({
10 | modules: {
11 | app,
12 | settings
13 | },
14 | getters
15 | });
16 |
17 | export default store;
18 |
--------------------------------------------------------------------------------
/src/store/modules/app.js:
--------------------------------------------------------------------------------
1 | import Cookies from 'js-cookie';
2 |
3 | const state = {
4 | sidebar: {
5 | opened: Cookies.get('sidebarStatus') ? !!+Cookies.get('sidebarStatus') : true,
6 | withoutAnimation: false
7 | },
8 | device: 'desktop'
9 | };
10 |
11 | const mutations = {
12 | // 折叠面板(使用cookie控制)
13 | TOGGLE_SIDEBAR: state => {
14 | state.sidebar.opened = !state.sidebar.opened;
15 | state.sidebar.withoutAnimation = false;
16 | if (state.sidebar.opened) {
17 | Cookies.set('sidebarStatus', 1);
18 | } else {
19 | Cookies.set('sidebarStatus', 0);
20 | }
21 | },
22 | // 直接关闭面板(使用cookie控制)
23 | CLOSE_SIDEBAR: (state, withoutAnimation) => {
24 | Cookies.set('sidebarStatus', 0);
25 | state.sidebar.opened = false;
26 | state.sidebar.withoutAnimation = withoutAnimation;
27 | },
28 | TOGGLE_DEVICE: (state, device) => {
29 | state.device = device;
30 | }
31 | };
32 |
33 | const actions = {
34 | // 折叠面板
35 | toggleSideBar({ commit }) {
36 | commit('TOGGLE_SIDEBAR');
37 | },
38 | // 关闭面板
39 | closeSideBar({ commit }, { withoutAnimation }) {
40 | commit('CLOSE_SIDEBAR', withoutAnimation);
41 | },
42 | toggleDevice({ commit }, device) {
43 | commit('TOGGLE_DEVICE', device);
44 | }
45 | };
46 |
47 | export default {
48 | namespaced: true,
49 | state,
50 | mutations,
51 | actions
52 | };
53 |
--------------------------------------------------------------------------------
/src/store/modules/settings.js:
--------------------------------------------------------------------------------
1 | import defaultSettings from '@/settings'
2 |
3 | const { showSettings, fixedHeader, sidebarLogo } = defaultSettings
4 |
5 | const state = {
6 | showSettings: showSettings,
7 | fixedHeader: fixedHeader,
8 | sidebarLogo: sidebarLogo
9 | }
10 |
11 | const mutations = {
12 | CHANGE_SETTING: (state, { key, value }) => {
13 | if (state.hasOwnProperty(key)) {
14 | state[key] = value
15 | }
16 | }
17 | }
18 |
19 | const actions = {
20 | changeSetting({ commit }, data) {
21 | commit('CHANGE_SETTING', data)
22 | }
23 | }
24 |
25 | export default {
26 | namespaced: true,
27 | state,
28 | mutations,
29 | actions
30 | }
31 |
32 |
--------------------------------------------------------------------------------
/src/store/modules/user.js:
--------------------------------------------------------------------------------
1 | import { login, logout, getInfo } from '@/api/user';
2 | import { getToken, setToken, removeToken } from '@/utils/auth';
3 | import { resetRouter } from '@/router';
4 |
5 | const state = {
6 | token: getToken(),
7 | name: '',
8 | avatar: ''
9 | };
10 |
11 | const mutations = {
12 | SET_TOKEN: (state, token) => {
13 | state.token = token;
14 | },
15 | SET_NAME: (state, name) => {
16 | state.name = name;
17 | },
18 | SET_AVATAR: (state, avatar) => {
19 | state.avatar = avatar;
20 | }
21 | };
22 |
23 | const actions = {
24 | // user login
25 | login({ commit }, userInfo) {
26 | const { username, password } = userInfo;
27 | return new Promise((resolve, reject) => {
28 | login({ username: username.trim(), password: password })
29 | .then(response => {
30 | const { data } = response;
31 | commit('SET_TOKEN', data.token);
32 | setToken(data.token);
33 | resolve();
34 | })
35 | .catch(error => {
36 | reject(error);
37 | });
38 | });
39 | },
40 |
41 | // get user info
42 | getInfo({ commit, state }) {
43 | return new Promise((resolve, reject) => {
44 | getInfo(state.token)
45 | .then(response => {
46 | const { data } = response;
47 |
48 | if (!data) {
49 | reject('验证失败,请重新登录。 ');
50 | }
51 |
52 | const { name, avatar } = data;
53 |
54 | commit('SET_NAME', name);
55 | commit('SET_AVATAR', avatar);
56 | resolve(data);
57 | })
58 | .catch(error => {
59 | reject(error);
60 | });
61 | });
62 | },
63 |
64 | // user logout
65 | logout({ commit, state }) {
66 | return new Promise((resolve, reject) => {
67 | logout(state.token)
68 | .then(() => {
69 | commit('SET_TOKEN', '');
70 | removeToken();
71 | resetRouter();
72 | resolve();
73 | })
74 | .catch(error => {
75 | reject(error);
76 | });
77 | });
78 | },
79 |
80 | // remove token
81 | resetToken({ commit }) {
82 | return new Promise(resolve => {
83 | commit('SET_TOKEN', '');
84 | removeToken();
85 | resolve();
86 | });
87 | }
88 | };
89 |
90 | export default {
91 | namespaced: true,
92 | state,
93 | mutations,
94 | actions
95 | };
96 |
--------------------------------------------------------------------------------
/src/styles/element-ui.scss:
--------------------------------------------------------------------------------
1 | // cover some element-ui styles
2 |
3 | .el-breadcrumb__inner,
4 | .el-breadcrumb__inner a {
5 | font-weight: 400 !important;
6 | }
7 |
8 | .el-upload {
9 | input[type="file"] {
10 | display: none !important;
11 | }
12 | }
13 |
14 | .el-upload__input {
15 | display: none;
16 | }
17 |
18 |
19 | // to fixed https://github.com/ElemeFE/element/issues/2461
20 | .el-dialog {
21 | transform: none;
22 | left: 0;
23 | position: relative;
24 | margin: 0 auto;
25 | }
26 |
27 | // refine element ui upload
28 | .upload-container {
29 | .el-upload {
30 | width: 100%;
31 |
32 | .el-upload-dragger {
33 | width: 100%;
34 | height: 200px;
35 | }
36 | }
37 | }
38 |
39 | // dropdown
40 | .el-dropdown-menu {
41 | a {
42 | display: block
43 | }
44 | }
45 |
46 | // to fix el-date-picker css style
47 | .el-range-separator {
48 | box-sizing: content-box;
49 | }
50 |
--------------------------------------------------------------------------------
/src/styles/index.scss:
--------------------------------------------------------------------------------
1 | @import "./variables.scss";
2 | @import "./mixin.scss";
3 | @import "./transition.scss";
4 | @import "./element-ui.scss";
5 | @import "./sidebar.scss";
6 |
7 | body {
8 | height: 100%;
9 | -moz-osx-font-smoothing: grayscale;
10 | -webkit-font-smoothing: antialiased;
11 | text-rendering: optimizeLegibility;
12 | font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB,
13 | Microsoft YaHei, Arial, sans-serif;
14 | }
15 |
16 | label {
17 | font-weight: 700;
18 | }
19 |
20 | html {
21 | height: 100%;
22 | box-sizing: border-box;
23 | }
24 |
25 | #app {
26 | height: 100%;
27 | }
28 |
29 | *,
30 | *:before,
31 | *:after {
32 | box-sizing: inherit;
33 | }
34 |
35 | a:focus,
36 | a:active {
37 | outline: none;
38 | }
39 |
40 | a,
41 | a:focus,
42 | a:hover {
43 | cursor: pointer;
44 | color: inherit;
45 | text-decoration: none;
46 | }
47 |
48 | div:focus {
49 | outline: none;
50 | }
51 |
52 | .clearfix {
53 | &:after {
54 | visibility: hidden;
55 | display: block;
56 | font-size: 0;
57 | content: " ";
58 | clear: both;
59 | height: 0;
60 | }
61 | }
62 |
63 | // main-container global css
64 | .app-container {
65 | padding: 20px;
66 | }
67 |
--------------------------------------------------------------------------------
/src/styles/mixin.scss:
--------------------------------------------------------------------------------
1 | @mixin clearfix {
2 | &:after {
3 | content: "";
4 | display: table;
5 | clear: both;
6 | }
7 | }
8 |
9 | @mixin scrollBar {
10 | &::-webkit-scrollbar-track-piece {
11 | background: #d3dce6;
12 | }
13 |
14 | &::-webkit-scrollbar {
15 | width: 6px;
16 | }
17 |
18 | &::-webkit-scrollbar-thumb {
19 | background: #99a9bf;
20 | border-radius: 20px;
21 | }
22 | }
23 |
24 | @mixin relative {
25 | position: relative;
26 | width: 100%;
27 | height: 100%;
28 | }
29 |
--------------------------------------------------------------------------------
/src/styles/sidebar.scss:
--------------------------------------------------------------------------------
1 | #app {
2 |
3 | .main-container {
4 | min-height: 100%;
5 | transition: margin-left .28s;
6 | margin-left: $sideBarWidth;
7 | position: relative;
8 | }
9 |
10 | .sidebar-container {
11 | transition: width 0.28s;
12 | width: $sideBarWidth !important;
13 | background-color: $menuBg;
14 | height: 100%;
15 | position: fixed;
16 | font-size: 0px;
17 | top: 0;
18 | bottom: 0;
19 | left: 0;
20 | z-index: 1001;
21 | overflow: hidden;
22 |
23 | // reset element-ui css
24 | .horizontal-collapse-transition {
25 | transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out;
26 | }
27 |
28 | .scrollbar-wrapper {
29 | overflow-x: hidden !important;
30 | }
31 |
32 | .el-scrollbar__bar.is-vertical {
33 | right: 0px;
34 | }
35 |
36 | .el-scrollbar {
37 | height: 100%;
38 | }
39 |
40 | &.has-logo {
41 | .el-scrollbar {
42 | height: calc(100% - 50px);
43 | }
44 | }
45 |
46 | .is-horizontal {
47 | display: none;
48 | }
49 |
50 | a {
51 | display: inline-block;
52 | width: 100%;
53 | overflow: hidden;
54 | }
55 |
56 | .svg-icon {
57 | margin-right: 16px;
58 | }
59 |
60 | .el-menu {
61 | border: none;
62 | height: 100%;
63 | width: 100% !important;
64 | }
65 |
66 | // menu hover
67 | .submenu-title-noDropdown,
68 | .el-submenu__title {
69 | &:hover {
70 | background-color: $menuHover !important;
71 | }
72 | }
73 |
74 | .is-active>.el-submenu__title {
75 | color: $subMenuActiveText !important;
76 | }
77 |
78 | & .nest-menu .el-submenu>.el-submenu__title,
79 | & .el-submenu .el-menu-item {
80 | min-width: $sideBarWidth !important;
81 | background-color: $subMenuBg !important;
82 |
83 | &:hover {
84 | background-color: $subMenuHover !important;
85 | }
86 | }
87 | }
88 |
89 | .hideSidebar {
90 | .sidebar-container {
91 | width: 54px !important;
92 | }
93 |
94 | .main-container {
95 | margin-left: 54px;
96 | }
97 |
98 | .submenu-title-noDropdown {
99 | padding: 0 !important;
100 | position: relative;
101 |
102 | .el-tooltip {
103 | padding: 0 !important;
104 |
105 | .svg-icon {
106 | margin-left: 20px;
107 | }
108 | }
109 | }
110 |
111 | .el-submenu {
112 | overflow: hidden;
113 |
114 | &>.el-submenu__title {
115 | padding: 0 !important;
116 |
117 | .svg-icon {
118 | margin-left: 20px;
119 | }
120 |
121 | .el-submenu__icon-arrow {
122 | display: none;
123 | }
124 | }
125 | }
126 |
127 | .el-menu--collapse {
128 | .el-submenu {
129 | &>.el-submenu__title {
130 | &>span {
131 | height: 0;
132 | width: 0;
133 | overflow: hidden;
134 | visibility: hidden;
135 | display: inline-block;
136 | }
137 | }
138 | }
139 | }
140 | }
141 |
142 | .el-menu--collapse .el-menu .el-submenu {
143 | min-width: $sideBarWidth !important;
144 | }
145 |
146 | // mobile responsive
147 | .mobile {
148 | .main-container {
149 | margin-left: 0px;
150 | }
151 |
152 | .sidebar-container {
153 | transition: transform .28s;
154 | width: $sideBarWidth !important;
155 | }
156 |
157 | &.hideSidebar {
158 | .sidebar-container {
159 | pointer-events: none;
160 | transition-duration: 0.3s;
161 | transform: translate3d(-$sideBarWidth, 0, 0);
162 | }
163 | }
164 | }
165 |
166 | .withoutAnimation {
167 |
168 | .main-container,
169 | .sidebar-container {
170 | transition: none;
171 | }
172 | }
173 | }
174 |
175 | // when menu collapsed
176 | .el-menu--vertical {
177 | &>.el-menu {
178 | .svg-icon {
179 | margin-right: 16px;
180 | }
181 | }
182 |
183 | .nest-menu .el-submenu>.el-submenu__title,
184 | .el-menu-item {
185 | &:hover {
186 | // you can use $subMenuHover
187 | background-color: $menuHover !important;
188 | }
189 | }
190 |
191 | // the scroll bar appears when the subMenu is too long
192 | >.el-menu--popup {
193 | max-height: 100vh;
194 | overflow-y: auto;
195 |
196 | &::-webkit-scrollbar-track-piece {
197 | background: #d3dce6;
198 | }
199 |
200 | &::-webkit-scrollbar {
201 | width: 6px;
202 | }
203 |
204 | &::-webkit-scrollbar-thumb {
205 | background: #99a9bf;
206 | border-radius: 20px;
207 | }
208 | }
209 | }
210 |
--------------------------------------------------------------------------------
/src/styles/transition.scss:
--------------------------------------------------------------------------------
1 | // global transition css
2 |
3 | /* fade */
4 | .fade-enter-active,
5 | .fade-leave-active {
6 | transition: opacity 0.28s;
7 | }
8 |
9 | .fade-enter,
10 | .fade-leave-active {
11 | opacity: 0;
12 | }
13 |
14 | /* fade-transform */
15 | .fade-transform-leave-active,
16 | .fade-transform-enter-active {
17 | transition: all .5s;
18 | }
19 |
20 | .fade-transform-enter {
21 | opacity: 0;
22 | transform: translateX(-30px);
23 | }
24 |
25 | .fade-transform-leave-to {
26 | opacity: 0;
27 | transform: translateX(30px);
28 | }
29 |
30 | /* breadcrumb transition */
31 | .breadcrumb-enter-active,
32 | .breadcrumb-leave-active {
33 | transition: all .5s;
34 | }
35 |
36 | .breadcrumb-enter,
37 | .breadcrumb-leave-active {
38 | opacity: 0;
39 | transform: translateX(20px);
40 | }
41 |
42 | .breadcrumb-move {
43 | transition: all .5s;
44 | }
45 |
46 | .breadcrumb-leave-active {
47 | position: absolute;
48 | }
49 |
--------------------------------------------------------------------------------
/src/styles/variables.scss:
--------------------------------------------------------------------------------
1 | // sidebar
2 | $menuText:#bfcbd9;
3 | $menuActiveText:#409EFF;
4 | $subMenuActiveText:#f4f4f5; //https://github.com/ElemeFE/element/issues/12951
5 |
6 | $menuBg:#304156;
7 | $menuHover:#263445;
8 |
9 | $subMenuBg:#1f2d3d;
10 | $subMenuHover:#001528;
11 |
12 | $sideBarWidth: 210px;
13 |
14 | // the :export directive is the magic sauce for webpack
15 | // https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass
16 | :export {
17 | menuText: $menuText;
18 | menuActiveText: $menuActiveText;
19 | subMenuActiveText: $subMenuActiveText;
20 | menuBg: $menuBg;
21 | menuHover: $menuHover;
22 | subMenuBg: $subMenuBg;
23 | subMenuHover: $subMenuHover;
24 | sideBarWidth: $sideBarWidth;
25 | }
26 |
--------------------------------------------------------------------------------
/src/utils/auth.js:
--------------------------------------------------------------------------------
1 | import Cookies from "js-cookie";
2 |
3 | const TokenKey = "management_token";
4 |
5 | export function getToken() {
6 | return Cookies.get(TokenKey);
7 | }
8 |
9 | export function setToken(token) {
10 | return Cookies.set(TokenKey, token, { expires: 1, path: "" });
11 | }
12 |
13 | export function removeToken() {
14 | return Cookies.remove(TokenKey);
15 | }
16 |
--------------------------------------------------------------------------------
/src/utils/axios.js:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 | import { getToken } from "./auth";
3 | // 需要使用代理来解决跨域问题
4 | axios.defaults.headers.post["Content-Type"] =
5 | "application/x-www-form-urlencoded";
6 | axios.defaults.timeout = 20000;
7 | const codeMessage = {
8 | 200: "服务器成功返回请求的数据。",
9 | 201: "新建或修改数据成功。",
10 | 202: "一个请求已经进入后台排队(异步任务)。",
11 | 204: "删除数据成功。",
12 | 400: "发出的请求有错误,服务器没有进行新建或修改数据的操作。",
13 | 401: "用户没有权限(令牌、用户名、密码错误)。",
14 | 403: "用户得到授权,但是访问是被禁止的。",
15 | 404: "发出的请求针对的是不存在的记录,服务器没有进行操作。",
16 | 406: "请求的格式不可得。",
17 | 410: "请求的资源被永久删除,且不会再得到的。",
18 | 422: "当创建一个对象时,发生一个验证错误。",
19 | 500: "服务器发生错误,请检查服务器。",
20 | 502: "网关错误。",
21 | 503: "服务不可用,服务器暂时过载或维护。",
22 | 504: "网关超时。"
23 | };
24 | // Add a request interceptor
25 | axios.interceptors.request.use(
26 | config => {
27 | const token = getToken();
28 | if (token) {
29 | // Bearer是JWT的认证头部信息
30 | config.headers.common["Authorization"] = "Bearer " + token;
31 | }
32 | return config;
33 | },
34 | error => {
35 | return Promise.reject(error);
36 | }
37 | );
38 | export default async (
39 | url = "",
40 | params = {},
41 | method = "get",
42 | isUpload = false
43 | ) => {
44 | method = method.toLowerCase();
45 | if (method === "get") {
46 | let paramArr = [];
47 | for (let [key, value] of Object.entries(params)) {
48 | paramArr.push(key + "=" + value);
49 | }
50 | if (paramArr.length > 0) {
51 | url += "?" + paramArr.join("&").replace(/#/g, "%23");
52 | }
53 | return new Promise((resolve, reject) => {
54 | axios
55 | .get(url)
56 | .then(
57 | response => {
58 | resolve(response.data);
59 | },
60 | err => {
61 | reject(err);
62 | }
63 | )
64 | .catch(error => {
65 | reject(error);
66 | });
67 | });
68 | } else if (method === "post") {
69 | let config = {};
70 | if (isUpload) {
71 | config = {
72 | headers: {
73 | "Content-Type": "multipart/form-data"
74 | }
75 | };
76 | }
77 | return new Promise((resolve, reject) => {
78 | axios
79 | .post(url, params, config)
80 | .then(
81 | response => {
82 | resolve(response.data);
83 | },
84 | err => {
85 | reject(err);
86 | }
87 | )
88 | .catch(error => {
89 | reject(error);
90 | });
91 | });
92 | } else if (method === "put") {
93 | return new Promise((resolve, reject) => {
94 | axios
95 | .put(url, params)
96 | .then(
97 | response => {
98 | resolve(response.data);
99 | },
100 | err => {
101 | reject(err);
102 | }
103 | )
104 | .catch(error => {
105 | reject(error);
106 | });
107 | });
108 | } else if (method === "delete") {
109 | return new Promise((resolve, reject) => {
110 | axios
111 | .delete(url)
112 | .then(
113 | response => {
114 | resolve(response.data);
115 | },
116 | err => {
117 | reject(err);
118 | }
119 | )
120 | .catch(error => {
121 | reject(error);
122 | });
123 | });
124 | } else {
125 | let error = "传递的参数错误";
126 | return Promise.reject(error);
127 | }
128 | };
129 |
--------------------------------------------------------------------------------
/src/utils/get-page-title.js:
--------------------------------------------------------------------------------
1 | import defaultSettings from '@/settings';
2 |
3 | const title = defaultSettings.title || '后台管理';
4 |
5 | export default function getPageTitle(pageTitle) {
6 | if (pageTitle) {
7 | return `${pageTitle} - ${title}`;
8 | }
9 | return `${title}`;
10 | }
11 |
--------------------------------------------------------------------------------
/src/utils/validate.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @param {string} path
3 | * @returns {Boolean}
4 | */
5 | export function isExternal(path) {
6 | return /^(https?:|mailto:|tel:)/.test(path);
7 | }
8 |
9 | /**
10 | * 有效的账户名
11 | * @param {string} str
12 | * @returns {Boolean}
13 | */
14 | export function validUsername(str) {
15 | const valid_map = ['admin', 'editor'];
16 | return valid_map.indexOf(str.trim()) >= 0;
17 | }
18 |
--------------------------------------------------------------------------------
/src/views/404.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
26 |
27 |
404!
28 |
来到了未知世界!
29 |
请检查您输入的URL是否正确,或单击下面的按钮返回首页。
30 |
回到首页
34 |
35 |
36 |
37 |
38 |
39 |
44 |
45 |
239 |
--------------------------------------------------------------------------------
/src/views/articleList/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
16 |
62 |
63 |
--------------------------------------------------------------------------------
/src/views/dashboard/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
50 |
51 |
62 |
--------------------------------------------------------------------------------
/src/views/labelManager/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {{ item.name}} ({{ item.count }})
17 |
18 |
19 |
20 | + 添加标签
21 |
22 |
23 |
24 |
33 |
34 |
35 |
36 |
195 |
196 |
--------------------------------------------------------------------------------
/src/views/login/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 |
19 |
20 |
21 |
后台管理平台
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
58 |
59 |
60 |
61 |
62 |
63 |
67 | 登录
73 |
74 |
75 | username: admin
76 | password: 123456
77 |
78 |
79 |
80 |
81 |
82 |
201 |
202 |
245 |
246 |
309 |
--------------------------------------------------------------------------------
/src/views/myProject/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | + 增加项目
6 |
7 |
8 |
16 |
17 | 空空如也~
18 |
19 |
26 |
67 |
68 |
72 |
73 |
74 |
75 |
76 |
182 |
183 |
--------------------------------------------------------------------------------
/src/views/sortManager/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {{ item.name}} ({{ item.count }})
17 |
18 |
19 |
20 | + 添加分类
21 |
22 |
23 |
24 |
33 |
34 |
35 |
36 |
199 |
200 |
--------------------------------------------------------------------------------
/vue.config.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | const path = require("path");
3 | const defaultSettings = require("./src/settings.js");
4 |
5 | function resolve(dir) {
6 | return path.join(__dirname, dir);
7 | }
8 |
9 | const name = defaultSettings.title || "后台管理"; // page title
10 |
11 | const port = process.env.port || process.env.npm_config_port || 9002; // dev port
12 |
13 | module.exports = {
14 | publicPath: "./",
15 | outputDir: "dist",
16 | assetsDir: "static",
17 | indexPath: "index.html",
18 | lintOnSave: false,
19 | // 否为生产环境构建生成 source map
20 | productionSourceMap: false,
21 | // 默认在生成的静态资源文件名中包含hash以控制缓存
22 | filenameHashing: true,
23 | devServer: {
24 | port: port,
25 | host: "127.0.0.1",
26 | open: false,
27 | overlay: {
28 | warnings: false,
29 | errors: true
30 | },
31 | proxy: {
32 | "/backapi": {
33 | target: "http://127.0.0.1:9001",
34 | changeOrigin: true,
35 | secure: false,
36 | pathRewrite: {
37 | "^/backapi": "/backapi"
38 | }
39 | },
40 | "/uploads": {
41 | target: "http://127.0.0.1:9001",
42 | changeOrigin: true,
43 | secure: false,
44 | pathRewrite: {
45 | "^/uploads": "/uploads"
46 | }
47 | }
48 | }
49 | },
50 | configureWebpack: {
51 | name: name,
52 | resolve: {
53 | alias: {
54 | "@": resolve("src")
55 | }
56 | }
57 | },
58 | chainWebpack(config) {
59 | config.plugins.delete("preload"); // TODO: need test
60 | config.plugins.delete("prefetch"); // TODO: need test
61 |
62 | // set svg-sprite-loader
63 | config.module
64 | .rule("svg")
65 | .exclude.add(resolve("src/icons"))
66 | .end();
67 | config.module
68 | .rule("icons")
69 | .test(/\.svg$/)
70 | .include.add(resolve("src/icons"))
71 | .end()
72 | .use("svg-sprite-loader")
73 | .loader("svg-sprite-loader")
74 | .options({
75 | symbolId: "icon-[name]"
76 | })
77 | .end();
78 |
79 | // set preserveWhitespace
80 | config.module
81 | .rule("vue")
82 | .use("vue-loader")
83 | .loader("vue-loader")
84 | .tap(options => {
85 | options.compilerOptions.preserveWhitespace = true;
86 | return options;
87 | })
88 | .end();
89 |
90 | config
91 | // https://webpack.js.org/configuration/devtool/#development
92 | .when(process.env.NODE_ENV === "development", config =>
93 | config.devtool("cheap-source-map")
94 | );
95 |
96 | config.when(process.env.NODE_ENV !== "development", config => {
97 | config
98 | .plugin("ScriptExtHtmlWebpackPlugin")
99 | .after("html")
100 | .use("script-ext-html-webpack-plugin", [
101 | {
102 | // `runtime` must same as runtimeChunk name. default is `runtime`
103 | inline: /runtime\..*\.js$/
104 | }
105 | ])
106 | .end();
107 | config.optimization.splitChunks({
108 | chunks: "all",
109 | cacheGroups: {
110 | libs: {
111 | name: "chunk-libs",
112 | test: /[\\/]node_modules[\\/]/,
113 | priority: 10,
114 | chunks: "initial" // only package third parties that are initially dependent
115 | },
116 | elementUI: {
117 | name: "chunk-elementUI", // split elementUI into a single package
118 | priority: 20, // the weight needs to be larger than libs and app or it will be packaged into libs or app
119 | test: /[\\/]node_modules[\\/]_?element-ui(.*)/ // in order to adapt to cnpm
120 | },
121 | commons: {
122 | name: "chunk-commons",
123 | test: resolve("src/components"), // can customize your rules
124 | minChunks: 3, // minimum common number
125 | priority: 5,
126 | reuseExistingChunk: true
127 | }
128 | }
129 | });
130 | config.optimization.runtimeChunk("single");
131 | });
132 | }
133 | };
134 |
--------------------------------------------------------------------------------