├── .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 | ![management_1.png](https://i.loli.net/2019/11/03/45zPwF1mWMSuxaA.png) 285 | ![management_2.png](https://i.loli.net/2019/11/03/9Z4XQVTBwUaG3H6.png) 286 | ![management_3.png](https://i.loli.net/2019/11/03/DmlxzToWGr47PXi.png) 287 | ![management_4.png](https://i.loli.net/2019/11/03/R4ojHfI7D698gW1.png) 288 | ![management_5.png](https://i.loli.net/2019/11/03/rPjNQhf7C4X2yWe.png) 289 | ![management_6.png](https://i.loli.net/2019/11/03/7REC9DkLGOVgmMu.png) 290 | ![management_7.png](https://i.loli.net/2019/11/03/9qNp18nkmhDOevc.png) 291 | ![management_8.png](https://i.loli.net/2019/11/03/GYnST3ztLK5al8v.png) 292 | ![management_9.png](https://i.loli.net/2019/11/03/e5lSFJWiAtQ6Y2g.png) 293 | ![management_10.png](https://i.loli.net/2019/11/03/QX7b63mdGwgfpBO.png) 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 | 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 | 23 | 24 | 93 | 94 | 107 | -------------------------------------------------------------------------------- /src/components/Hamburger/index.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 35 | 36 | 48 | -------------------------------------------------------------------------------- /src/components/SvgIcon/index.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 63 | 64 | 79 | -------------------------------------------------------------------------------- /src/components/markdown/index.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 43 | 44 | -------------------------------------------------------------------------------- /src/components/postTable/index.vue: -------------------------------------------------------------------------------- 1 | 55 | 56 | -------------------------------------------------------------------------------- /src/components/project/index.vue: -------------------------------------------------------------------------------- 1 | 102 | 103 | 217 | 218 | 271 | -------------------------------------------------------------------------------- /src/components/upload/index.vue: -------------------------------------------------------------------------------- 1 | 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 | 11 | 12 | 22 | 23 | 35 | 36 | 44 | -------------------------------------------------------------------------------- /src/layout/components/Navbar.vue: -------------------------------------------------------------------------------- 1 | 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 | 8 | 9 | 37 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/Logo.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 53 | 54 | 103 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/SidebarItem.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 118 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/index.vue: -------------------------------------------------------------------------------- 1 | 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 | 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 | 38 | 39 | 44 | 45 | 239 | -------------------------------------------------------------------------------- /src/views/articleList/index.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 62 | 63 | -------------------------------------------------------------------------------- /src/views/dashboard/index.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 50 | 51 | 62 | -------------------------------------------------------------------------------- /src/views/labelManager/index.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 195 | 196 | -------------------------------------------------------------------------------- /src/views/login/index.vue: -------------------------------------------------------------------------------- 1 | 81 | 82 | 201 | 202 | 245 | 246 | 309 | -------------------------------------------------------------------------------- /src/views/myProject/index.vue: -------------------------------------------------------------------------------- 1 | 75 | 76 | 182 | 183 | -------------------------------------------------------------------------------- /src/views/sortManager/index.vue: -------------------------------------------------------------------------------- 1 | 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 | --------------------------------------------------------------------------------