├── LICENSE ├── README.md ├── node-server-api ├── api.js ├── db.js ├── index.js └── package.json ├── vue-blog-admin ├── .editorconfig ├── .env.development ├── .env.production ├── .env.staging ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── .travis.yml ├── babel.config.js ├── build │ └── index.js ├── jest.config.js ├── package.json ├── postcss.config.js ├── public │ ├── favicon.ico │ └── index.html ├── src │ ├── App.vue │ ├── api │ │ ├── article.js │ │ ├── food.js │ │ ├── updateLog.js │ │ ├── user.js │ │ └── whisper.js │ ├── components │ │ ├── Breadcrumb │ │ │ └── index.vue │ │ ├── Hamburger │ │ │ └── index.vue │ │ ├── Pagination │ │ │ └── index.vue │ │ └── SvgIcon │ │ │ └── index.vue │ ├── directive │ │ ├── clipboard │ │ │ ├── clipboard.js │ │ │ └── index.js │ │ ├── el-drag-dialog │ │ │ ├── drag.js │ │ │ └── index.js │ │ ├── el-table │ │ │ ├── adaptive.js │ │ │ └── index.js │ │ ├── permission │ │ │ ├── index.js │ │ │ └── permission.js │ │ ├── sticky.js │ │ └── waves │ │ │ ├── index.js │ │ │ ├── waves.css │ │ │ └── waves.js │ ├── icons │ │ ├── index.js │ │ ├── svg │ │ │ ├── dashboard.svg │ │ │ ├── example.svg │ │ │ ├── eye-open.svg │ │ │ ├── eye.svg │ │ │ ├── form.svg │ │ │ ├── link.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 │ │ └── modules │ │ │ └── basicData.js │ ├── settings.js │ ├── store │ │ ├── getters.js │ │ ├── index.js │ │ └── modules │ │ │ ├── app.js │ │ │ ├── settings.js │ │ │ └── user.js │ ├── styles │ │ ├── element-ui.scss │ │ ├── index.scss │ │ ├── mixin.scss │ │ ├── sidebar.scss │ │ ├── transition.scss │ │ └── variables.scss │ ├── utils │ │ ├── auth.js │ │ ├── get-page-title.js │ │ ├── index.js │ │ ├── request.js │ │ ├── scroll-to.js │ │ └── validate.js │ └── views │ │ ├── 404.vue │ │ ├── basicData │ │ ├── articleList.vue │ │ ├── foodList.vue │ │ ├── updateLogList.vue │ │ └── whisperList.vue │ │ ├── dashboard │ │ └── index.vue │ │ └── login │ │ └── index.vue └── vue.config.js ├── vue-blog-h5 ├── .browserslistrc ├── .editorconfig ├── .env.development ├── .env.production ├── .env.staging ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .postcssrc.js ├── .prettierrc ├── babel.config.js ├── package.json ├── public │ ├── favicon.ico │ └── index.html ├── src │ ├── App.vue │ ├── api │ │ ├── common.js │ │ ├── index.js │ │ └── user.js │ ├── assets │ │ ├── css │ │ │ ├── index.scss │ │ │ ├── mixin.scss │ │ │ └── variables.scss │ │ └── logo.png │ ├── components │ │ ├── Header.vue │ │ └── TabBar.vue │ ├── config │ │ ├── env.development.js │ │ ├── env.production.js │ │ ├── env.staging.js │ │ └── index.js │ ├── filters │ │ ├── filter.js │ │ └── index.js │ ├── main.js │ ├── plugins │ │ └── vant.js │ ├── router │ │ ├── index.js │ │ └── router.config.js │ ├── store │ │ ├── getters.js │ │ ├── index.js │ │ └── modules │ │ │ └── app.js │ ├── utils │ │ ├── index.js │ │ ├── request.js │ │ └── validate.js │ └── views │ │ ├── about │ │ └── index.vue │ │ ├── album │ │ └── index.vue │ │ ├── home │ │ └── index.vue │ │ ├── layouts │ │ └── index.vue │ │ ├── updateLog │ │ └── index.vue │ │ └── whisper │ │ └── index.vue └── vue.config.js └── vue-blog-template ├── .browserslistrc ├── .eslintrc.js ├── .gitignore ├── babel.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── favicon.ico └── index.html ├── src ├── App.vue ├── assets │ └── styles │ │ ├── index.scss │ │ └── normalize.scss ├── main.js ├── router │ └── index.js ├── store.js ├── utils │ └── index.js └── views │ ├── about │ └── About.vue │ ├── album │ └── Album.vue │ ├── home │ └── Home.vue │ ├── layout │ ├── components │ │ ├── Header.vue │ │ ├── Main.vue │ │ └── index.js │ └── index.vue │ ├── updateLog │ └── UpdateLog.vue │ └── whisper │ └── Whisper.vue └── vue.config.js /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Jason Chen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | ## 简介 6 | 7 | [vue-blog-template](https://github.com/chenyicai622/vue-blog-template) 是基于vue2.0,包含了vue-blog-template(PC版)、vue-blog-h5(H5版)、vue-blog-admin(blog后台管理系统)、node-server-api(blog服务端API),用于个人博客的开发和管理。 8 | 9 | - [在线预览](http://cycblog.cn) 10 | - [H5版在线预览](http://m.cycblog.cn) (浏览器访问请切换移动模式) 11 | 12 | **该项目为本人自主设计与开发,一切以学习为主,如有改进及优化建议,请发送本人邮箱chenyicai622@qq.com** 13 | 14 | **如果觉得不错,别忘了右上角,点个星星哟~~** 15 | 16 | ## 前序准备 17 | 18 | 你需要在本地安装 [node](http://nodejs.org/) 和 [git](https://git-scm.com/)。本项目技术栈基于 [ES2015+](http://es6.ruanyifeng.com/)、[vue](https://cn.vuejs.org/index.html)、[vuex](https://vuex.vuejs.org/zh-cn/)、[vue-router](https://router.vuejs.org/zh-cn/) 、[vue-cli](https://github.com/vuejs/vue-cli) 、[axios](https://github.com/axios/axios) 、[element-ui](https://github.com/ElemeFE/element)、[vant](https://vant-contrib.gitee.io/vant/#/zh-CN/)、[express](https://expressjs.com/)、[mongodb](https://www.mongodb.com/3)、[mongoose](https://mongoosejs.com/),提前了解和学习这些知识会对使用本项目有很大的帮助。 19 | 20 |

21 | 22 |

23 | 24 |

25 | 26 |

27 | 28 |

29 | 30 |

31 | 32 |

33 | 34 |

35 | 36 |

37 | 38 |

39 | 40 |

41 | 42 |

43 | 44 |

45 | 46 |

47 | 48 |

49 | 50 |

51 | 52 |

53 | 54 |

55 | 56 | 57 | 58 | ## 文件 59 | 60 | ```bash 61 | # node-server-api 62 | - api.js API接口 63 | - db.js MongoDB连接配置及接口建表 64 | - index.js 相关配置 65 | 66 | # vue-blog-template/vue-blog-h5 67 | - src 68 | - views 69 | - about 关于 70 | - album 相册 71 | - home 文章 72 | - updateLog 更新 73 | - whisper 留言 74 | 75 | # vue-blog-admin 76 | - src 77 | - views 78 | - basiceData 79 | - articleList.vue 文章 80 | - foodList.vue 图片 81 | - updateLogList.vue 更新 82 | - whisperList.vue 留言 83 | ``` 84 | 85 | ## 开发 86 | 87 | ```bash 88 | # 克隆项目 89 | git clone https://github.com/chenyicai622/vue-blog-template.git 90 | 91 | # 进入项目目录 92 | cd vue-blog-template 93 | 94 | # 进入各版本目录 95 | # PC版 96 | cd vue-blog-template 97 | # H5版 98 | cd vue-blog-h5 99 | # Admin 100 | cd vue-blog-admin 101 | # Server API 102 | cd node-server-api 103 | 104 | # 安装依赖 105 | npm install 106 | 107 | # 建议不要直接使用 cnpm 安装依赖,会有各种诡异的 bug。可以通过如下操作解决 npm 下载速度慢的问题 108 | npm install --registry=https://registry.npm.taobao.org 109 | 110 | # 启动服务 111 | # PC版 112 | npm run serve 113 | # H5版 114 | npm run serve 115 | # Admin 116 | npm run dev 117 | # Server API 118 | node index.js 119 | ``` 120 | 121 | ## 感谢 122 | 123 | - [vue-element-admin](https://github.com/PanJiaChen/vue-element-admin) 124 | - [vue-h5-template](https://github.com/sunniejs/vue-h5-template) 125 | 126 | ## License 127 | 128 | [MIT](https://github.com/chenyicai622/vue-blog-template/blob/main/LICENSE) 129 | 130 | Copyright (c) 2021 Jason Chen 131 | -------------------------------------------------------------------------------- /node-server-api/db.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | // 连接数据库 如果不自己创建 默认test数据库会自动生成 3 | mongoose.connect( 4 | "mongodb://127.0.0.1:27017/blog", //mongodb默认27017端口,自行创建数据库文件 5 | { useNewUrlParser: true, useUnifiedTopology: true }, 6 | function(err) { 7 | if (err) { 8 | console.log("连接失败:" + err); 9 | } else { 10 | console.log("连接成功"); 11 | } 12 | } 13 | ); 14 | 15 | /************** 定义模式Schema **************/ 16 | //登录 17 | const userSchema = new mongoose.Schema( 18 | { 19 | username: String, 20 | password: String, 21 | date: { 22 | type: Date, 23 | default: Date.now 24 | } 25 | }, 26 | { collection: "user", versionKey: false } 27 | ); 28 | 29 | //文章 30 | const articleSchema = new mongoose.Schema( 31 | { 32 | date: String, 33 | title: String, 34 | content: String, 35 | url: String 36 | }, 37 | { collection: "articles", versionKey: false } 38 | ); 39 | 40 | //留言 41 | const whisperSchema = new mongoose.Schema( 42 | { 43 | userIP: String, 44 | userCity: String, 45 | date: String, 46 | content: String, 47 | avatar: String 48 | }, 49 | { collection: "whispers", versionKey: false } 50 | ); 51 | 52 | //更新记录 53 | const updateLogSchema = new mongoose.Schema( 54 | { 55 | date: String, 56 | info: String, 57 | content: String 58 | }, 59 | { collection: "updateLogs", versionKey: false } 60 | ); 61 | 62 | //美食 63 | const foodSchema = new mongoose.Schema( 64 | { 65 | date: String, 66 | url: String, 67 | name: String 68 | }, 69 | { collection: "foods", versionKey: false } 70 | ); 71 | 72 | /************** 定义模型Model **************/ 73 | const Models = { 74 | Food: mongoose.model("Food", foodSchema), 75 | User: mongoose.model("User", userSchema), 76 | Article: mongoose.model("Article", articleSchema), 77 | Whisper: mongoose.model("Whisper", whisperSchema), 78 | UpdateLog: mongoose.model("UpdateLog", updateLogSchema) 79 | }; 80 | 81 | module.exports = Models; 82 | -------------------------------------------------------------------------------- /node-server-api/index.js: -------------------------------------------------------------------------------- 1 | // 引入编写好的api 2 | const api = require('./api') 3 | // 引入文件模块 4 | const fs = require('fs') 5 | // 引入处理路径的模块 6 | const path = require('path') 7 | // 引入处理post数据的模块 8 | const bodyParser = require('body-parser') 9 | // 引入Express 10 | const express = require('express') 11 | const app = express() 12 | 13 | var cors = require('cors') 14 | app.use(cors()) 15 | app.use(bodyParser.json()) 16 | app.use(bodyParser.urlencoded({ extended: false })) 17 | app.use(api) 18 | 19 | // 访问静态资源文件 这里是访问所有dist目录下的静态资源文件 20 | app.use(express.static(path.resolve(__dirname, '../web'))) 21 | 22 | // 因为是单页应用 所有请求都走/dist/index.html 23 | app.get('*', function (req, res) { 24 | const html = fs.readFileSync( 25 | path.resolve(__dirname, '../web/index.html'), 26 | 'utf-8' 27 | ) 28 | res.send(html) 29 | }) 30 | 31 | app.listen(8090) 32 | console.log('success listen…………') 33 | -------------------------------------------------------------------------------- /node-server-api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "blog-server", 3 | "author": "Jason Chen", 4 | "version": "1.0.0", 5 | "description": "a blog server", 6 | "main": "index.js", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "prd": "pm2 start index.js --name='server'" 10 | }, 11 | "license": "MIT", 12 | "dependencies": { 13 | "body-parser": "^1.19.0", 14 | "cors": "^2.8.5", 15 | "express": "^4.17.1", 16 | "jsonwebtoken": "^8.5.1", 17 | "mongoose": "^5.6.4" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /vue-blog-admin/.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 | -------------------------------------------------------------------------------- /vue-blog-admin/.env.development: -------------------------------------------------------------------------------- 1 | # just a flag 2 | ENV = 'development' 3 | 4 | # base api 5 | VUE_APP_BASE_API = 'http://localhost:8090' 6 | 7 | # vue-cli uses the VUE_CLI_BABEL_TRANSPILE_MODULES environment variable, 8 | # to control whether the babel-plugin-dynamic-import-node plugin is enabled. 9 | # It only does one thing by converting all import() to require(). 10 | # This configuration can significantly increase the speed of hot updates, 11 | # when you have a large number of pages. 12 | # Detail: https://github.com/vuejs/vue-cli/blob/dev/packages/@vue/babel-preset-app/index.js 13 | 14 | VUE_CLI_BABEL_TRANSPILE_MODULES = true 15 | -------------------------------------------------------------------------------- /vue-blog-admin/.env.production: -------------------------------------------------------------------------------- 1 | # just a flag 2 | ENV = 'production' 3 | 4 | # base api 5 | VUE_APP_BASE_API = 'http://127.0.0.1:8090' 6 | 7 | -------------------------------------------------------------------------------- /vue-blog-admin/.env.staging: -------------------------------------------------------------------------------- 1 | NODE_ENV = production 2 | 3 | # just a flag 4 | ENV = 'staging' 5 | 6 | # base api 7 | VUE_APP_BASE_API = '/stage-api' 8 | 9 | -------------------------------------------------------------------------------- /vue-blog-admin/.eslintignore: -------------------------------------------------------------------------------- 1 | build/*.js 2 | src/assets 3 | public 4 | dist 5 | -------------------------------------------------------------------------------- /vue-blog-admin/.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 | -------------------------------------------------------------------------------- /vue-blog-admin/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "eslintIntegration": true, 3 | "singleQuote": true, 4 | "semi": false 5 | } 6 | -------------------------------------------------------------------------------- /vue-blog-admin/.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 10 3 | script: npm run test 4 | notifications: 5 | email: false 6 | -------------------------------------------------------------------------------- /vue-blog-admin/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/app' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /vue-blog-admin/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 = 9526 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 | } else { 33 | run(`vue-cli-service build ${args}`) 34 | } 35 | -------------------------------------------------------------------------------- /vue-blog-admin/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleFileExtensions: ['js', 'jsx', 'json', 'vue'], 3 | transform: { 4 | '^.+\\.vue$': 'vue-jest', 5 | '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 6 | 'jest-transform-stub', 7 | '^.+\\.jsx?$': 'babel-jest' 8 | }, 9 | moduleNameMapper: { 10 | '^@/(.*)$': '/src/$1' 11 | }, 12 | snapshotSerializers: ['jest-serializer-vue'], 13 | testMatch: [ 14 | '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)' 15 | ], 16 | collectCoverageFrom: ['src/utils/**/*.{js,vue}', '!src/utils/auth.js', '!src/utils/request.js', 'src/components/**/*.{js,vue}'], 17 | coverageDirectory: '/tests/unit/coverage', 18 | // 'collectCoverage': true, 19 | 'coverageReporters': [ 20 | 'lcov', 21 | 'text-summary' 22 | ], 23 | testURL: 'http://localhost/' 24 | } 25 | -------------------------------------------------------------------------------- /vue-blog-admin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "blog-admin", 3 | "version": "1.0.0", 4 | "description": "a blog admin", 5 | "author": "Jason Chen", 6 | "license": "MIT", 7 | "scripts": { 8 | "dev": "vue-cli-service serve", 9 | "build:prod": "vue-cli-service build", 10 | "build:stage": "vue-cli-service build --mode staging", 11 | "preview": "node build/index.js --preview", 12 | "lint": "eslint --ext .js,.vue src", 13 | "test:unit": "jest --clearCache && vue-cli-service test:unit", 14 | "test:ci": "npm run lint && npm run test:unit", 15 | "svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml" 16 | }, 17 | "dependencies": { 18 | "axios": "0.18.1", 19 | "element-ui": "2.7.2", 20 | "js-cookie": "2.2.0", 21 | "normalize.css": "7.0.0", 22 | "nprogress": "0.2.0", 23 | "path-to-regexp": "2.4.0", 24 | "vue": "2.6.10", 25 | "vue-router": "3.0.6", 26 | "vuex": "3.1.0" 27 | }, 28 | "devDependencies": { 29 | "@babel/core": "7.0.0", 30 | "@babel/register": "7.0.0", 31 | "@vue/cli-plugin-babel": "3.6.0", 32 | "@vue/cli-plugin-eslint": "^3.9.1", 33 | "@vue/cli-plugin-unit-jest": "3.6.3", 34 | "@vue/cli-service": "3.6.0", 35 | "@vue/test-utils": "1.0.0-beta.29", 36 | "autoprefixer": "^9.5.1", 37 | "babel-core": "7.0.0-bridge.0", 38 | "babel-eslint": "10.0.1", 39 | "babel-jest": "23.6.0", 40 | "chalk": "2.4.2", 41 | "connect": "3.6.6", 42 | "eslint": "5.15.3", 43 | "eslint-plugin-vue": "5.2.2", 44 | "html-webpack-plugin": "3.2.0", 45 | "mockjs": "1.0.1-beta3", 46 | "node-sass": "^4.9.0", 47 | "runjs": "^4.3.2", 48 | "sass-loader": "^7.1.0", 49 | "script-ext-html-webpack-plugin": "2.1.3", 50 | "script-loader": "0.7.2", 51 | "serve-static": "^1.13.2", 52 | "svg-sprite-loader": "4.1.3", 53 | "svgo": "1.2.2", 54 | "vue-template-compiler": "2.6.10" 55 | }, 56 | "engines": { 57 | "node": ">=8.9", 58 | "npm": ">= 3.0.0" 59 | }, 60 | "browserslist": [ 61 | "> 1%", 62 | "last 2 versions" 63 | ] 64 | } 65 | -------------------------------------------------------------------------------- /vue-blog-admin/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 | -------------------------------------------------------------------------------- /vue-blog-admin/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcyicai/vue-blog-template/525110674b567994f807ac23ed23556a3f12b043/vue-blog-admin/public/favicon.ico -------------------------------------------------------------------------------- /vue-blog-admin/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | <%= webpackConfig.name %> 12 | 164 | 165 | 166 | 172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 | 188 | 189 | 190 | -------------------------------------------------------------------------------- /vue-blog-admin/src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /vue-blog-admin/src/api/article.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function fetchList(query) { 4 | return request({ 5 | url: '/article/list', 6 | method: 'get', 7 | params: query 8 | }) 9 | } 10 | 11 | export function fetchArticle(id) { 12 | return request({ 13 | url: '/article/detail', 14 | method: 'get', 15 | params: { id } 16 | }) 17 | } 18 | 19 | export function createArticle(data) { 20 | return request({ 21 | url: '/article/create', 22 | method: 'post', 23 | data 24 | }) 25 | } 26 | 27 | export function updateArticle(data) { 28 | return request({ 29 | url: '/article/update', 30 | method: 'post', 31 | data 32 | }) 33 | } 34 | 35 | export function deleteArticle(data) { 36 | return request({ 37 | url: '/article/delete', 38 | method: 'post', 39 | data 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /vue-blog-admin/src/api/food.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function fetchList(query) { 4 | return request({ 5 | url: '/food/list', 6 | method: 'get', 7 | params: query 8 | }) 9 | } 10 | 11 | export function fetchFood(id) { 12 | return request({ 13 | url: '/food/detail', 14 | method: 'get', 15 | params: { id } 16 | }) 17 | } 18 | 19 | export function createFood(data) { 20 | return request({ 21 | url: '/food/create', 22 | method: 'post', 23 | data 24 | }) 25 | } 26 | 27 | export function updateFood(data) { 28 | return request({ 29 | url: '/food/update', 30 | method: 'post', 31 | data 32 | }) 33 | } 34 | 35 | export function deleteFood(data) { 36 | return request({ 37 | url: '/food/delete', 38 | method: 'post', 39 | data 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /vue-blog-admin/src/api/updateLog.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function fetchList(query) { 4 | return request({ 5 | url: '/updateLog/list', 6 | method: 'get', 7 | params: query 8 | }) 9 | } 10 | 11 | export function fetchLog(id) { 12 | return request({ 13 | url: '/updateLog/detail', 14 | method: 'get', 15 | params: { id } 16 | }) 17 | } 18 | 19 | export function createLog(data) { 20 | return request({ 21 | url: '/updateLog/create', 22 | method: 'post', 23 | data 24 | }) 25 | } 26 | 27 | export function updateLog(data) { 28 | return request({ 29 | url: '/updateLog/update', 30 | method: 'post', 31 | data 32 | }) 33 | } 34 | 35 | export function deleteLog(data) { 36 | return request({ 37 | url: '/updateLog/delete', 38 | method: 'post', 39 | data 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /vue-blog-admin/src/api/user.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function login(data) { 4 | return request({ 5 | url: '/user/login', 6 | method: 'post', 7 | data 8 | }) 9 | } 10 | 11 | export function getInfo(token) { 12 | return request({ 13 | url: '/user/info', 14 | method: 'get', 15 | params: { token } 16 | }) 17 | } 18 | 19 | export function logout() { 20 | return request({ 21 | url: '/user/logout', 22 | method: 'post' 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /vue-blog-admin/src/api/whisper.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function fetchList(query) { 4 | return request({ 5 | url: '/whisper/list', 6 | method: 'get', 7 | params: query 8 | }) 9 | } 10 | 11 | export function fetchWhisper(id) { 12 | return request({ 13 | url: '/whisper/detail', 14 | method: 'get', 15 | params: { id } 16 | }) 17 | } 18 | 19 | export function createWhisper(data) { 20 | return request({ 21 | url: '/whisper/create', 22 | method: 'post', 23 | data 24 | }) 25 | } 26 | 27 | export function updateWhisper(data) { 28 | return request({ 29 | url: '/whisper/update', 30 | method: 'post', 31 | data 32 | }) 33 | } 34 | 35 | export function deleteWhisper(data) { 36 | return request({ 37 | url: '/whisper/delete', 38 | method: 'post', 39 | data 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /vue-blog-admin/src/components/Breadcrumb/index.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 65 | 66 | 79 | -------------------------------------------------------------------------------- /vue-blog-admin/src/components/Hamburger/index.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 32 | 33 | 45 | -------------------------------------------------------------------------------- /vue-blog-admin/src/components/Pagination/index.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 92 | 93 | 102 | -------------------------------------------------------------------------------- /vue-blog-admin/src/components/SvgIcon/index.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 47 | 48 | 63 | -------------------------------------------------------------------------------- /vue-blog-admin/src/directive/clipboard/clipboard.js: -------------------------------------------------------------------------------- 1 | // Inspired by https://github.com/Inndy/vue-clipboard2 2 | const Clipboard = require('clipboard') 3 | if (!Clipboard) { 4 | throw new Error('you should npm install `clipboard` --save at first ') 5 | } 6 | 7 | export default { 8 | bind(el, binding) { 9 | if (binding.arg === 'success') { 10 | el._v_clipboard_success = binding.value 11 | } else if (binding.arg === 'error') { 12 | el._v_clipboard_error = binding.value 13 | } else { 14 | const clipboard = new Clipboard(el, { 15 | text() { return binding.value }, 16 | action() { return binding.arg === 'cut' ? 'cut' : 'copy' } 17 | }) 18 | clipboard.on('success', e => { 19 | const callback = el._v_clipboard_success 20 | callback && callback(e) // eslint-disable-line 21 | }) 22 | clipboard.on('error', e => { 23 | const callback = el._v_clipboard_error 24 | callback && callback(e) // eslint-disable-line 25 | }) 26 | el._v_clipboard = clipboard 27 | } 28 | }, 29 | update(el, binding) { 30 | if (binding.arg === 'success') { 31 | el._v_clipboard_success = binding.value 32 | } else if (binding.arg === 'error') { 33 | el._v_clipboard_error = binding.value 34 | } else { 35 | el._v_clipboard.text = function() { return binding.value } 36 | el._v_clipboard.action = function() { return binding.arg === 'cut' ? 'cut' : 'copy' } 37 | } 38 | }, 39 | unbind(el, binding) { 40 | if (binding.arg === 'success') { 41 | delete el._v_clipboard_success 42 | } else if (binding.arg === 'error') { 43 | delete el._v_clipboard_error 44 | } else { 45 | el._v_clipboard.destroy() 46 | delete el._v_clipboard 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /vue-blog-admin/src/directive/clipboard/index.js: -------------------------------------------------------------------------------- 1 | import Clipboard from './clipboard' 2 | 3 | const install = function(Vue) { 4 | Vue.directive('Clipboard', Clipboard) 5 | } 6 | 7 | if (window.Vue) { 8 | window.clipboard = Clipboard 9 | Vue.use(install); // eslint-disable-line 10 | } 11 | 12 | Clipboard.install = install 13 | export default Clipboard 14 | -------------------------------------------------------------------------------- /vue-blog-admin/src/directive/el-drag-dialog/drag.js: -------------------------------------------------------------------------------- 1 | export default { 2 | bind(el, binding, vnode) { 3 | const dialogHeaderEl = el.querySelector('.el-dialog__header') 4 | const dragDom = el.querySelector('.el-dialog') 5 | dialogHeaderEl.style.cssText += ';cursor:move;' 6 | dragDom.style.cssText += ';top:0px;' 7 | 8 | // 获取原有属性 ie dom元素.currentStyle 火狐谷歌 window.getComputedStyle(dom元素, null); 9 | const getStyle = (function() { 10 | if (window.document.currentStyle) { 11 | return (dom, attr) => dom.currentStyle[attr] 12 | } else { 13 | return (dom, attr) => getComputedStyle(dom, false)[attr] 14 | } 15 | })() 16 | 17 | dialogHeaderEl.onmousedown = (e) => { 18 | // 鼠标按下,计算当前元素距离可视区的距离 19 | const disX = e.clientX - dialogHeaderEl.offsetLeft 20 | const disY = e.clientY - dialogHeaderEl.offsetTop 21 | 22 | const dragDomWidth = dragDom.offsetWidth 23 | const dragDomHeight = dragDom.offsetHeight 24 | 25 | const screenWidth = document.body.clientWidth 26 | const screenHeight = document.body.clientHeight 27 | 28 | const minDragDomLeft = dragDom.offsetLeft 29 | const maxDragDomLeft = screenWidth - dragDom.offsetLeft - dragDomWidth 30 | 31 | const minDragDomTop = dragDom.offsetTop 32 | const maxDragDomTop = screenHeight - dragDom.offsetTop - dragDomHeight 33 | 34 | // 获取到的值带px 正则匹配替换 35 | let styL = getStyle(dragDom, 'left') 36 | let styT = getStyle(dragDom, 'top') 37 | 38 | if (styL.includes('%')) { 39 | styL = +document.body.clientWidth * (+styL.replace(/\%/g, '') / 100) 40 | styT = +document.body.clientHeight * (+styT.replace(/\%/g, '') / 100) 41 | } else { 42 | styL = +styL.replace(/\px/g, '') 43 | styT = +styT.replace(/\px/g, '') 44 | } 45 | 46 | document.onmousemove = function(e) { 47 | // 通过事件委托,计算移动的距离 48 | let left = e.clientX - disX 49 | let top = e.clientY - disY 50 | 51 | // 边界处理 52 | if (-(left) > minDragDomLeft) { 53 | left = -minDragDomLeft 54 | } else if (left > maxDragDomLeft) { 55 | left = maxDragDomLeft 56 | } 57 | 58 | if (-(top) > minDragDomTop) { 59 | top = -minDragDomTop 60 | } else if (top > maxDragDomTop) { 61 | top = maxDragDomTop 62 | } 63 | 64 | // 移动当前元素 65 | dragDom.style.cssText += `;left:${left + styL}px;top:${top + styT}px;` 66 | 67 | // emit onDrag event 68 | vnode.child.$emit('dragDialog') 69 | } 70 | 71 | document.onmouseup = function(e) { 72 | document.onmousemove = null 73 | document.onmouseup = null 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /vue-blog-admin/src/directive/el-drag-dialog/index.js: -------------------------------------------------------------------------------- 1 | import drag from './drag' 2 | 3 | const install = function(Vue) { 4 | Vue.directive('el-drag-dialog', drag) 5 | } 6 | 7 | if (window.Vue) { 8 | window['el-drag-dialog'] = drag 9 | Vue.use(install); // eslint-disable-line 10 | } 11 | 12 | drag.install = install 13 | export default drag 14 | -------------------------------------------------------------------------------- /vue-blog-admin/src/directive/el-table/adaptive.js: -------------------------------------------------------------------------------- 1 | import { addResizeListener, removeResizeListener } from 'element-ui/src/utils/resize-event' 2 | 3 | /** 4 | * How to use 5 | * ... 6 | * el-table height is must be set 7 | * bottomOffset: 30(default) // The height of the table from the bottom of the page. 8 | */ 9 | 10 | const doResize = (el, binding, vnode) => { 11 | const { componentInstance: $table } = vnode 12 | 13 | const { value } = binding 14 | 15 | if (!$table.height) { 16 | throw new Error(`el-$table must set the height. Such as height='100px'`) 17 | } 18 | const bottomOffset = (value && value.bottomOffset) || 30 19 | 20 | if (!$table) return 21 | 22 | const height = window.innerHeight - el.getBoundingClientRect().top - bottomOffset 23 | $table.layout.setHeight(height) 24 | $table.doLayout() 25 | } 26 | 27 | export default { 28 | bind(el, binding, vnode) { 29 | el.resizeListener = () => { 30 | doResize(el, binding, vnode) 31 | } 32 | // parameter 1 is must be "Element" type 33 | addResizeListener(window.document.body, el.resizeListener) 34 | }, 35 | inserted(el, binding, vnode) { 36 | doResize(el, binding, vnode) 37 | }, 38 | unbind(el) { 39 | removeResizeListener(window.document.body, el.resizeListener) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /vue-blog-admin/src/directive/el-table/index.js: -------------------------------------------------------------------------------- 1 | import adaptive from './adaptive' 2 | 3 | const install = function(Vue) { 4 | Vue.directive('el-height-adaptive-table', adaptive) 5 | } 6 | 7 | if (window.Vue) { 8 | window['el-height-adaptive-table'] = adaptive 9 | Vue.use(install); // eslint-disable-line 10 | } 11 | 12 | adaptive.install = install 13 | export default adaptive 14 | -------------------------------------------------------------------------------- /vue-blog-admin/src/directive/permission/index.js: -------------------------------------------------------------------------------- 1 | import permission from './permission' 2 | 3 | const install = function(Vue) { 4 | Vue.directive('permission', permission) 5 | } 6 | 7 | if (window.Vue) { 8 | window['permission'] = permission 9 | Vue.use(install); // eslint-disable-line 10 | } 11 | 12 | permission.install = install 13 | export default permission 14 | -------------------------------------------------------------------------------- /vue-blog-admin/src/directive/permission/permission.js: -------------------------------------------------------------------------------- 1 | import store from '@/store' 2 | 3 | export default { 4 | inserted(el, binding, vnode) { 5 | const { value } = binding 6 | const roles = store.getters && store.getters.roles 7 | if (value && value instanceof Array && value.length > 0) { 8 | const permissionRoles = value 9 | 10 | const hasPermission = roles.some(role => { 11 | return permissionRoles.includes(role) 12 | }) 13 | 14 | if (!hasPermission) { 15 | el.parentNode && el.parentNode.removeChild(el) 16 | } 17 | } else { 18 | throw new Error(`need roles! Like v-permission="['admin','editor']"`) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /vue-blog-admin/src/directive/sticky.js: -------------------------------------------------------------------------------- 1 | const vueSticky = {} 2 | let listenAction 3 | vueSticky.install = Vue => { 4 | Vue.directive('sticky', { 5 | inserted(el, binding) { 6 | const params = binding.value || {} 7 | const stickyTop = params.stickyTop || 0 8 | const zIndex = params.zIndex || 1000 9 | const elStyle = el.style 10 | 11 | elStyle.position = '-webkit-sticky' 12 | elStyle.position = 'sticky' 13 | // if the browser support css sticky(Currently Safari, Firefox and Chrome Canary) 14 | // if (~elStyle.position.indexOf('sticky')) { 15 | // elStyle.top = `${stickyTop}px`; 16 | // elStyle.zIndex = zIndex; 17 | // return 18 | // } 19 | const elHeight = el.getBoundingClientRect().height 20 | const elWidth = el.getBoundingClientRect().width 21 | elStyle.cssText = `top: ${stickyTop}px; z-index: ${zIndex}` 22 | 23 | const parentElm = el.parentNode || document.documentElement 24 | const placeholder = document.createElement('div') 25 | placeholder.style.display = 'none' 26 | placeholder.style.width = `${elWidth}px` 27 | placeholder.style.height = `${elHeight}px` 28 | parentElm.insertBefore(placeholder, el) 29 | 30 | let active = false 31 | 32 | const getScroll = (target, top) => { 33 | const prop = top ? 'pageYOffset' : 'pageXOffset' 34 | const method = top ? 'scrollTop' : 'scrollLeft' 35 | let ret = target[prop] 36 | if (typeof ret !== 'number') { 37 | ret = window.document.documentElement[method] 38 | } 39 | return ret 40 | } 41 | 42 | const sticky = () => { 43 | if (active) { 44 | return 45 | } 46 | if (!elStyle.height) { 47 | elStyle.height = `${el.offsetHeight}px` 48 | } 49 | 50 | elStyle.position = 'fixed' 51 | elStyle.width = `${elWidth}px` 52 | placeholder.style.display = 'inline-block' 53 | active = true 54 | } 55 | 56 | const reset = () => { 57 | if (!active) { 58 | return 59 | } 60 | 61 | elStyle.position = '' 62 | placeholder.style.display = 'none' 63 | active = false 64 | } 65 | 66 | const check = () => { 67 | const scrollTop = getScroll(window, true) 68 | const offsetTop = el.getBoundingClientRect().top 69 | if (offsetTop < stickyTop) { 70 | sticky() 71 | } else { 72 | if (scrollTop < elHeight + stickyTop) { 73 | reset() 74 | } 75 | } 76 | } 77 | listenAction = () => { 78 | check() 79 | } 80 | 81 | window.addEventListener('scroll', listenAction) 82 | }, 83 | 84 | unbind() { 85 | window.removeEventListener('scroll', listenAction) 86 | } 87 | }) 88 | } 89 | 90 | export default vueSticky 91 | 92 | -------------------------------------------------------------------------------- /vue-blog-admin/src/directive/waves/index.js: -------------------------------------------------------------------------------- 1 | import waves from './waves' 2 | 3 | const install = function(Vue) { 4 | Vue.directive('waves', waves) 5 | } 6 | 7 | if (window.Vue) { 8 | window.waves = waves 9 | Vue.use(install); // eslint-disable-line 10 | } 11 | 12 | waves.install = install 13 | export default waves 14 | -------------------------------------------------------------------------------- /vue-blog-admin/src/directive/waves/waves.css: -------------------------------------------------------------------------------- 1 | .waves-ripple { 2 | position: absolute; 3 | border-radius: 100%; 4 | background-color: rgba(0, 0, 0, 0.15); 5 | background-clip: padding-box; 6 | pointer-events: none; 7 | -webkit-user-select: none; 8 | -moz-user-select: none; 9 | -ms-user-select: none; 10 | user-select: none; 11 | -webkit-transform: scale(0); 12 | -ms-transform: scale(0); 13 | transform: scale(0); 14 | opacity: 1; 15 | } 16 | 17 | .waves-ripple.z-active { 18 | opacity: 0; 19 | -webkit-transform: scale(2); 20 | -ms-transform: scale(2); 21 | transform: scale(2); 22 | -webkit-transition: opacity 1.2s ease-out, -webkit-transform 0.6s ease-out; 23 | transition: opacity 1.2s ease-out, -webkit-transform 0.6s ease-out; 24 | transition: opacity 1.2s ease-out, transform 0.6s ease-out; 25 | transition: opacity 1.2s ease-out, transform 0.6s ease-out, -webkit-transform 0.6s ease-out; 26 | } -------------------------------------------------------------------------------- /vue-blog-admin/src/directive/waves/waves.js: -------------------------------------------------------------------------------- 1 | import './waves.css' 2 | 3 | const context = '@@wavesContext' 4 | 5 | function handleClick(el, binding) { 6 | function handle(e) { 7 | const customOpts = Object.assign({}, binding.value) 8 | const opts = Object.assign({ 9 | ele: el, // 波纹作用元素 10 | type: 'hit', // hit 点击位置扩散 center中心点扩展 11 | color: 'rgba(0, 0, 0, 0.15)' // 波纹颜色 12 | }, 13 | customOpts 14 | ) 15 | const target = opts.ele 16 | if (target) { 17 | target.style.position = 'relative' 18 | target.style.overflow = 'hidden' 19 | const rect = target.getBoundingClientRect() 20 | let ripple = target.querySelector('.waves-ripple') 21 | if (!ripple) { 22 | ripple = document.createElement('span') 23 | ripple.className = 'waves-ripple' 24 | ripple.style.height = ripple.style.width = Math.max(rect.width, rect.height) + 'px' 25 | target.appendChild(ripple) 26 | } else { 27 | ripple.className = 'waves-ripple' 28 | } 29 | switch (opts.type) { 30 | case 'center': 31 | ripple.style.top = rect.height / 2 - ripple.offsetHeight / 2 + 'px' 32 | ripple.style.left = rect.width / 2 - ripple.offsetWidth / 2 + 'px' 33 | break 34 | default: 35 | ripple.style.top = 36 | (e.pageY - rect.top - ripple.offsetHeight / 2 - document.documentElement.scrollTop || 37 | document.body.scrollTop) + 'px' 38 | ripple.style.left = 39 | (e.pageX - rect.left - ripple.offsetWidth / 2 - document.documentElement.scrollLeft || 40 | document.body.scrollLeft) + 'px' 41 | } 42 | ripple.style.backgroundColor = opts.color 43 | ripple.className = 'waves-ripple z-active' 44 | return false 45 | } 46 | } 47 | 48 | if (!el[context]) { 49 | el[context] = { 50 | removeHandle: handle 51 | } 52 | } else { 53 | el[context].removeHandle = handle 54 | } 55 | 56 | return handle 57 | } 58 | 59 | export default { 60 | bind(el, binding) { 61 | el.addEventListener('click', handleClick(el, binding), false) 62 | }, 63 | update(el, binding) { 64 | el.removeEventListener('click', el[context].removeHandle, false) 65 | el.addEventListener('click', handleClick(el, binding), false) 66 | }, 67 | unbind(el) { 68 | el.removeEventListener('click', el[context].removeHandle, false) 69 | el[context] = null 70 | delete el[context] 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /vue-blog-admin/src/icons/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import SvgIcon from '@/components/SvgIcon'// svg component 3 | 4 | // register globally 5 | Vue.component('svg-icon', SvgIcon) 6 | 7 | const req = require.context('./svg', false, /\.svg$/) 8 | const requireAll = requireContext => requireContext.keys().map(requireContext) 9 | requireAll(req) 10 | -------------------------------------------------------------------------------- /vue-blog-admin/src/icons/svg/dashboard.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vue-blog-admin/src/icons/svg/example.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vue-blog-admin/src/icons/svg/eye-open.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vue-blog-admin/src/icons/svg/eye.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vue-blog-admin/src/icons/svg/form.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vue-blog-admin/src/icons/svg/link.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vue-blog-admin/src/icons/svg/nested.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vue-blog-admin/src/icons/svg/password.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vue-blog-admin/src/icons/svg/table.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vue-blog-admin/src/icons/svg/tree.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vue-blog-admin/src/icons/svg/user.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vue-blog-admin/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 | -------------------------------------------------------------------------------- /vue-blog-admin/src/layout/components/AppMain.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 19 | 20 | 32 | 33 | 41 | -------------------------------------------------------------------------------- /vue-blog-admin/src/layout/components/Navbar.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 56 | 57 | 135 | -------------------------------------------------------------------------------- /vue-blog-admin/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 | // In order to fix the click on menu on the ios device will trigger the mouseleave bug 9 | // https://github.com/PanJiaChen/vue-element-admin/issues/1135 10 | this.fixBugIniOS() 11 | }, 12 | methods: { 13 | fixBugIniOS() { 14 | const $subMenu = this.$refs.subMenu 15 | if ($subMenu) { 16 | const handleMouseleave = $subMenu.handleMouseleave 17 | $subMenu.handleMouseleave = (e) => { 18 | if (this.device === 'mobile') { 19 | return 20 | } 21 | handleMouseleave(e) 22 | } 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /vue-blog-admin/src/layout/components/Sidebar/Item.vue: -------------------------------------------------------------------------------- 1 | 30 | -------------------------------------------------------------------------------- /vue-blog-admin/src/layout/components/Sidebar/Link.vue: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 37 | -------------------------------------------------------------------------------- /vue-blog-admin/src/layout/components/Sidebar/Logo.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 33 | 34 | 83 | -------------------------------------------------------------------------------- /vue-blog-admin/src/layout/components/Sidebar/SidebarItem.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 96 | -------------------------------------------------------------------------------- /vue-blog-admin/src/layout/components/Sidebar/index.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 57 | -------------------------------------------------------------------------------- /vue-blog-admin/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 | -------------------------------------------------------------------------------- /vue-blog-admin/src/layout/index.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 52 | 53 | 97 | -------------------------------------------------------------------------------- /vue-blog-admin/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 | -------------------------------------------------------------------------------- /vue-blog-admin/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Cookies from 'js-cookie' 3 | import 'normalize.css/normalize.css' // A modern alternative to CSS resets 4 | 5 | import ElementUI from 'element-ui' 6 | import 'element-ui/lib/theme-chalk/index.css' 7 | import locale from 'element-ui/lib/locale/lang/zh-CN' // lang i18n 8 | 9 | import '@/styles/index.scss' // global css 10 | 11 | import App from './App' 12 | import store from './store' 13 | import router from './router' 14 | 15 | import '@/icons' // icon 16 | import '@/permission' // permission control 17 | 18 | // set ElementUI lang to EN 19 | Vue.use(ElementUI, { locale, size: Cookies.get('size') || 'medium' }) 20 | 21 | Vue.config.productionTip = false 22 | 23 | new Vue({ 24 | el: '#app', 25 | router, 26 | store, 27 | render: h => h(App) 28 | }) 29 | -------------------------------------------------------------------------------- /vue-blog-admin/src/permission.js: -------------------------------------------------------------------------------- 1 | import router from './router' 2 | import store from './store' 3 | import { Message } from 'element-ui' 4 | import NProgress from 'nprogress' // progress bar 5 | import 'nprogress/nprogress.css' // progress bar style 6 | import { getToken } from '@/utils/auth' // get token from cookie 7 | import getPageTitle from '@/utils/get-page-title' 8 | 9 | NProgress.configure({ showSpinner: false }) // NProgress Configuration 10 | 11 | const whiteList = ['/login'] // no redirect whitelist 12 | 13 | router.beforeEach(async(to, from, next) => { 14 | // start progress bar 15 | NProgress.start() 16 | 17 | // set page title 18 | document.title = getPageTitle(to.meta.title) 19 | 20 | // determine whether the user has logged in 21 | const hasToken = getToken() 22 | 23 | if (hasToken) { 24 | if (to.path === '/login') { 25 | // if is logged in, redirect to the home page 26 | next({ path: '/' }) 27 | NProgress.done() 28 | } else { 29 | const hasGetUserInfo = store.getters.name 30 | if (hasGetUserInfo) { 31 | next() 32 | } else { 33 | try { 34 | // get user info 35 | await store.dispatch('user/getInfo') 36 | next() 37 | } catch (error) { 38 | // remove token and go to login page to re-login 39 | await store.dispatch('user/resetToken') 40 | Message.error(error || 'Has Error') 41 | next(`/login?redirect=${to.path}`) 42 | NProgress.done() 43 | } 44 | } 45 | } 46 | } else { 47 | /* has no token*/ 48 | 49 | if (whiteList.indexOf(to.path) !== -1) { 50 | // in the free login whitelist, go directly 51 | next() 52 | } else { 53 | // other pages that do not have permission to access are redirected to the login page. 54 | next(`/login?redirect=${to.path}`) 55 | NProgress.done() 56 | } 57 | } 58 | }) 59 | 60 | router.afterEach(() => { 61 | // finish progress bar 62 | NProgress.done() 63 | }) 64 | -------------------------------------------------------------------------------- /vue-blog-admin/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 | import basicDataRouter from './modules/basicData' 9 | /** 10 | * Note: sub-menu only appear when route children.length >= 1 11 | * Detail see: https://panjiachen.github.io/vue-element-admin-site/guide/essentials/router-and-nav.html 12 | * 13 | * hidden: true if set true, item will not show in the sidebar(default is false) 14 | * alwaysShow: true if set true, will always show the root menu 15 | * if not set alwaysShow, when item has more than one children route, 16 | * it will becomes nested mode, otherwise not show the root menu 17 | * redirect: noRedirect if set noRedirect will no redirect in the breadcrumb 18 | * name:'router-name' the name is used by (must set!!!) 19 | * meta : { 20 | roles: ['admin','editor'] control the page roles (you can set multiple roles) 21 | title: 'title' the name show in sidebar and breadcrumb (recommend set) 22 | icon: 'svg-name' the icon show in the sidebar 23 | breadcrumb: false if set false, the item will hidden in breadcrumb(default is true) 24 | activeMenu: '/example/list' if set path, the sidebar will highlight the path you set 25 | } 26 | */ 27 | 28 | /** 29 | * constantRoutes 30 | * a base page that does not have permission requirements 31 | * all roles can be accessed 32 | */ 33 | export const constantRoutes = [ 34 | { 35 | path: '/login', 36 | component: () => import('@/views/login/index'), 37 | hidden: true 38 | }, 39 | 40 | { 41 | path: '/404', 42 | component: () => import('@/views/404'), 43 | hidden: true 44 | }, 45 | { 46 | path: '/', 47 | component: Layout, 48 | redirect: '/dashboard', 49 | children: [{ 50 | path: 'dashboard', 51 | name: '首页', 52 | component: () => import('@/views/dashboard/index'), 53 | meta: { title: '首页', icon: 'dashboard' } 54 | }] 55 | }, 56 | basicDataRouter, 57 | // 404 page must be placed at the end !!! 58 | { path: '*', redirect: '/404', hidden: true } 59 | ] 60 | 61 | const createRouter = () => new Router({ 62 | mode: 'history', // require service support 63 | scrollBehavior: () => ({ y: 0 }), 64 | routes: constantRoutes 65 | }) 66 | 67 | const router = createRouter() 68 | 69 | export function resetRouter() { 70 | const newRouter = createRouter() 71 | router.matcher = newRouter.matcher // reset router 72 | } 73 | 74 | export default router 75 | -------------------------------------------------------------------------------- /vue-blog-admin/src/router/modules/basicData.js: -------------------------------------------------------------------------------- 1 | import Layout from '@/layout' 2 | 3 | const basicDataRouter = { 4 | path: '/basicData', 5 | component: Layout, 6 | redirect: '/basicData/articleList', 7 | name: '数据管理', 8 | meta: { title: '数据管理', icon: 'example' }, 9 | alwaysShow: true, 10 | children: [ 11 | { 12 | path: 'articleList', 13 | name: '文章', 14 | component: () => import('@/views/basicData/articleList'), 15 | meta: { title: '文章' } 16 | }, 17 | { 18 | path: 'whisperList', 19 | name: '留言', 20 | component: () => import('@/views/basicData/whisperList'), 21 | meta: { title: '留言' } 22 | }, 23 | { 24 | path: 'updateLogList', 25 | name: '更新记录', 26 | component: () => import('@/views/basicData/updateLogList'), 27 | meta: { title: '更新记录' } 28 | }, 29 | { 30 | path: 'foodList', 31 | name: '美食', 32 | component: () => import('@/views/basicData/foodList'), 33 | meta: { title: '美食' } 34 | } 35 | ] 36 | } 37 | 38 | export default basicDataRouter 39 | -------------------------------------------------------------------------------- /vue-blog-admin/src/settings.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | title: 'CYC Blog Admin', 4 | 5 | /** 6 | * @type {boolean} true | false 7 | * @description Whether fix the header 8 | */ 9 | fixedHeader: true, 10 | 11 | /** 12 | * @type {boolean} true | false 13 | * @description Whether show the logo in sidebar 14 | */ 15 | sidebarLogo: true 16 | } 17 | -------------------------------------------------------------------------------- /vue-blog-admin/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 | -------------------------------------------------------------------------------- /vue-blog-admin/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 | import user from './modules/user' 7 | 8 | Vue.use(Vuex) 9 | 10 | const store = new Vuex.Store({ 11 | modules: { 12 | app, 13 | settings, 14 | user 15 | }, 16 | getters 17 | }) 18 | 19 | export default store 20 | -------------------------------------------------------------------------------- /vue-blog-admin/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 | TOGGLE_SIDEBAR: state => { 13 | state.sidebar.opened = !state.sidebar.opened 14 | state.sidebar.withoutAnimation = false 15 | if (state.sidebar.opened) { 16 | Cookies.set('sidebarStatus', 1) 17 | } else { 18 | Cookies.set('sidebarStatus', 0) 19 | } 20 | }, 21 | CLOSE_SIDEBAR: (state, withoutAnimation) => { 22 | Cookies.set('sidebarStatus', 0) 23 | state.sidebar.opened = false 24 | state.sidebar.withoutAnimation = withoutAnimation 25 | }, 26 | TOGGLE_DEVICE: (state, device) => { 27 | state.device = device 28 | } 29 | } 30 | 31 | const actions = { 32 | toggleSideBar({ commit }) { 33 | commit('TOGGLE_SIDEBAR') 34 | }, 35 | closeSideBar({ commit }, { withoutAnimation }) { 36 | commit('CLOSE_SIDEBAR', withoutAnimation) 37 | }, 38 | toggleDevice({ commit }, device) { 39 | commit('TOGGLE_DEVICE', device) 40 | } 41 | } 42 | 43 | export default { 44 | namespaced: true, 45 | state, 46 | mutations, 47 | actions 48 | } 49 | -------------------------------------------------------------------------------- /vue-blog-admin/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 | -------------------------------------------------------------------------------- /vue-blog-admin/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 { username, avatar } = data 53 | 54 | commit('SET_NAME', username) 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 | -------------------------------------------------------------------------------- /vue-blog-admin/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 | -------------------------------------------------------------------------------- /vue-blog-admin/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, Microsoft YaHei, Arial, sans-serif; 13 | } 14 | 15 | label { 16 | font-weight: 700; 17 | } 18 | 19 | html { 20 | height: 100%; 21 | box-sizing: border-box; 22 | } 23 | 24 | #app { 25 | height: 100%; 26 | } 27 | 28 | *, 29 | *:before, 30 | *:after { 31 | box-sizing: inherit; 32 | } 33 | 34 | a:focus, 35 | a:active { 36 | outline: none; 37 | } 38 | 39 | a, 40 | a:focus, 41 | a:hover { 42 | cursor: pointer; 43 | color: inherit; 44 | text-decoration: none; 45 | } 46 | 47 | div:focus { 48 | outline: none; 49 | } 50 | 51 | .clearfix { 52 | &:after { 53 | visibility: hidden; 54 | display: block; 55 | font-size: 0; 56 | content: " "; 57 | clear: both; 58 | height: 0; 59 | } 60 | } 61 | 62 | // main-container global css 63 | .app-container { 64 | padding: 20px; 65 | } 66 | -------------------------------------------------------------------------------- /vue-blog-admin/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 | -------------------------------------------------------------------------------- /vue-blog-admin/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 | -------------------------------------------------------------------------------- /vue-blog-admin/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 | -------------------------------------------------------------------------------- /vue-blog-admin/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 | -------------------------------------------------------------------------------- /vue-blog-admin/src/utils/auth.js: -------------------------------------------------------------------------------- 1 | import Cookies from 'js-cookie' 2 | 3 | const TokenKey = 'cyc_blog_admin_token' 4 | 5 | export function getToken() { 6 | return Cookies.get(TokenKey) 7 | } 8 | 9 | export function setToken(token) { 10 | return Cookies.set(TokenKey, token) 11 | } 12 | 13 | export function removeToken() { 14 | return Cookies.remove(TokenKey) 15 | } 16 | -------------------------------------------------------------------------------- /vue-blog-admin/src/utils/get-page-title.js: -------------------------------------------------------------------------------- 1 | import defaultSettings from '@/settings' 2 | 3 | const title = defaultSettings.title || 'CYC Blog Admin' 4 | 5 | export default function getPageTitle(pageTitle) { 6 | if (pageTitle) { 7 | return `${pageTitle} - ${title}` 8 | } 9 | return `${title}` 10 | } 11 | -------------------------------------------------------------------------------- /vue-blog-admin/src/utils/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by PanJiaChen on 16/11/18. 3 | */ 4 | 5 | /** 6 | * Parse the time to string 7 | * @param {(Object|string|number)} time 8 | * @param {string} cFormat 9 | * @returns {string} 10 | */ 11 | export function parseTime(time, cFormat) { 12 | if (arguments.length === 0) { 13 | return null 14 | } 15 | const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}' 16 | let date 17 | if (typeof time === 'object') { 18 | date = time 19 | } else { 20 | if ((typeof time === 'string') && (/^[0-9]+$/.test(time))) { 21 | time = parseInt(time) 22 | } 23 | if ((typeof time === 'number') && (time.toString().length === 10)) { 24 | time = time * 1000 25 | } 26 | date = new Date(time) 27 | } 28 | const formatObj = { 29 | y: date.getFullYear(), 30 | m: date.getMonth() + 1, 31 | d: date.getDate(), 32 | h: date.getHours(), 33 | i: date.getMinutes(), 34 | s: date.getSeconds(), 35 | a: date.getDay() 36 | } 37 | const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => { 38 | let value = formatObj[key] 39 | // Note: getDay() returns 0 on Sunday 40 | if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value ] } 41 | if (result.length > 0 && value < 10) { 42 | value = '0' + value 43 | } 44 | return value || 0 45 | }) 46 | return time_str 47 | } 48 | 49 | /** 50 | * @param {number} time 51 | * @param {string} option 52 | * @returns {string} 53 | */ 54 | export function formatTime(time, option) { 55 | if (('' + time).length === 10) { 56 | time = parseInt(time) * 1000 57 | } else { 58 | time = +time 59 | } 60 | const d = new Date(time) 61 | const now = Date.now() 62 | 63 | const diff = (now - d) / 1000 64 | 65 | if (diff < 30) { 66 | return '刚刚' 67 | } else if (diff < 3600) { 68 | // less 1 hour 69 | return Math.ceil(diff / 60) + '分钟前' 70 | } else if (diff < 3600 * 24) { 71 | return Math.ceil(diff / 3600) + '小时前' 72 | } else if (diff < 3600 * 24 * 2) { 73 | return '1天前' 74 | } 75 | if (option) { 76 | return parseTime(time, option) 77 | } else { 78 | return ( 79 | d.getMonth() + 80 | 1 + 81 | '月' + 82 | d.getDate() + 83 | '日' + 84 | d.getHours() + 85 | '时' + 86 | d.getMinutes() + 87 | '分' 88 | ) 89 | } 90 | } 91 | 92 | /** 93 | * @param {string} url 94 | * @returns {Object} 95 | */ 96 | export function param2Obj(url) { 97 | const search = url.split('?')[1] 98 | if (!search) { 99 | return {} 100 | } 101 | return JSON.parse( 102 | '{"' + 103 | decodeURIComponent(search) 104 | .replace(/"/g, '\\"') 105 | .replace(/&/g, '","') 106 | .replace(/=/g, '":"') 107 | .replace(/\+/g, ' ') + 108 | '"}' 109 | ) 110 | } 111 | -------------------------------------------------------------------------------- /vue-blog-admin/src/utils/request.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { MessageBox, Message } from 'element-ui' 3 | import store from '@/store' 4 | import { getToken } from '@/utils/auth' 5 | 6 | // create an axios instance 7 | const service = axios.create({ 8 | baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url 9 | // withCredentials: true, // send cookies when cross-domain requests 10 | timeout: 5000 // request timeout 11 | }) 12 | 13 | // request interceptor 14 | service.interceptors.request.use( 15 | config => { 16 | // do something before request is sent 17 | 18 | if (store.getters.token) { 19 | // let each request carry token 20 | // ['X-Token'] is a custom headers key 21 | // please modify it according to the actual situation 22 | config.headers['X-Token'] = getToken() 23 | } 24 | return config 25 | }, 26 | error => { 27 | // do something with request error 28 | console.log(error) // for debug 29 | return Promise.reject(error) 30 | } 31 | ) 32 | 33 | // response interceptor 34 | service.interceptors.response.use( 35 | /** 36 | * If you want to get http information such as headers or status 37 | * Please return response => response 38 | */ 39 | 40 | /** 41 | * Determine the request status by custom code 42 | * Here is just an example 43 | * You can also judge the status by HTTP Status Code 44 | */ 45 | response => { 46 | const res = response.data 47 | 48 | // if the custom code is not 20000, it is judged as an error. 49 | if (res.code !== 200) { 50 | Message({ 51 | message: res.message || 'Error', 52 | type: 'error', 53 | duration: 5 * 1000 54 | }) 55 | 56 | // 50008: Illegal token; 50012: Other clients logged in; 50014: Token expired; 57 | if ( 58 | res.code === 50008 || 59 | res.code === 50012 || 60 | res.code === 50014 61 | ) { 62 | // to re-login 63 | MessageBox.confirm( 64 | '您已经注销,您可以取消以停留在此页面,或再次登录', 65 | '确认注销', 66 | { 67 | confirmButtonText: '重新登录', 68 | cancelButtonText: '确认', 69 | type: 'warning' 70 | } 71 | ).then(() => { 72 | store.dispatch('user/resetToken').then(() => { 73 | location.reload() 74 | }) 75 | }) 76 | } 77 | return Promise.reject(new Error(res.message || 'Error')) 78 | } else { 79 | return res 80 | } 81 | }, 82 | error => { 83 | console.log('err' + error) // for debug 84 | Message({ 85 | message: error.message, 86 | type: 'error', 87 | duration: 5 * 1000 88 | }) 89 | return Promise.reject(error) 90 | } 91 | ) 92 | 93 | export default service 94 | -------------------------------------------------------------------------------- /vue-blog-admin/src/utils/scroll-to.js: -------------------------------------------------------------------------------- 1 | Math.easeInOutQuad = function(t, b, c, d) { 2 | t /= d / 2 3 | if (t < 1) { 4 | return c / 2 * t * t + b 5 | } 6 | t-- 7 | return -c / 2 * (t * (t - 2) - 1) + b 8 | } 9 | 10 | // requestAnimationFrame for Smart Animating http://goo.gl/sx5sts 11 | var requestAnimFrame = (function() { 12 | return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || function(callback) { window.setTimeout(callback, 1000 / 60) } 13 | })() 14 | 15 | /** 16 | * Because it's so fucking difficult to detect the scrolling element, just move them all 17 | * @param {number} amount 18 | */ 19 | function move(amount) { 20 | document.documentElement.scrollTop = amount 21 | document.body.parentNode.scrollTop = amount 22 | document.body.scrollTop = amount 23 | } 24 | 25 | function position() { 26 | return document.documentElement.scrollTop || document.body.parentNode.scrollTop || document.body.scrollTop 27 | } 28 | 29 | /** 30 | * @param {number} to 31 | * @param {number} duration 32 | * @param {Function} callback 33 | */ 34 | export function scrollTo(to, duration, callback) { 35 | const start = position() 36 | const change = to - start 37 | const increment = 20 38 | let currentTime = 0 39 | duration = (typeof (duration) === 'undefined') ? 500 : duration 40 | var animateScroll = function() { 41 | // increment the time 42 | currentTime += increment 43 | // find the value with the quadratic in-out easing function 44 | var val = Math.easeInOutQuad(currentTime, start, change, duration) 45 | // move the document.body 46 | move(val) 47 | // do the animation unless its over 48 | if (currentTime < duration) { 49 | requestAnimFrame(animateScroll) 50 | } else { 51 | if (callback && typeof (callback) === 'function') { 52 | // the animation is done so lets callback 53 | callback() 54 | } 55 | } 56 | } 57 | animateScroll() 58 | } 59 | -------------------------------------------------------------------------------- /vue-blog-admin/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 | * @param {string} str 11 | * @returns {Boolean} 12 | */ 13 | export function validUsername(str) { 14 | const valid_map = ['admin', 'editor'] 15 | return valid_map.indexOf(str.trim()) >= 0 16 | } 17 | -------------------------------------------------------------------------------- /vue-blog-admin/src/views/404.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 31 | 32 | 226 | -------------------------------------------------------------------------------- /vue-blog-admin/src/views/dashboard/index.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 55 | 56 | 86 | -------------------------------------------------------------------------------- /vue-blog-admin/src/views/login/index.vue: -------------------------------------------------------------------------------- 1 | 65 | 66 | 143 | 144 | 190 | 191 | 262 | -------------------------------------------------------------------------------- /vue-blog-admin/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 || 'CYC Blog Admin' // page title 10 | module.exports = { 11 | publicPath: '/', 12 | outputDir: 'dist', 13 | assetsDir: 'static', 14 | productionSourceMap: false, 15 | configureWebpack: { 16 | // provide the app's title in webpack's name field, so that 17 | // it can be accessed in index.html to inject the correct title. 18 | name: name, 19 | resolve: { 20 | alias: { 21 | '@': resolve('src') 22 | } 23 | }, 24 | performance: { 25 | hints: 'warning', 26 | //入口起点的最大体积 整数类型(以字节为单位) 27 | maxEntrypointSize: 50000000, 28 | //生成文件的最大体积 整数类型(以字节为单位 300k) 29 | maxAssetSize: 30000000, 30 | //只给出 js 文件的性能提示 31 | assetFilter: function(assetFilename) { 32 | return assetFilename.endsWith('.js') 33 | } 34 | } 35 | }, 36 | chainWebpack(config) { 37 | config.plugins.delete('preload') // TODO: need test 38 | config.plugins.delete('prefetch') // TODO: need test 39 | 40 | // set svg-sprite-loader 41 | config.module 42 | .rule('svg') 43 | .exclude.add(resolve('src/icons')) 44 | .end() 45 | config.module 46 | .rule('icons') 47 | .test(/\.svg$/) 48 | .include.add(resolve('src/icons')) 49 | .end() 50 | .use('svg-sprite-loader') 51 | .loader('svg-sprite-loader') 52 | .options({ 53 | symbolId: 'icon-[name]' 54 | }) 55 | .end() 56 | 57 | // set preserveWhitespace 58 | config.module 59 | .rule('vue') 60 | .use('vue-loader') 61 | .loader('vue-loader') 62 | .tap(options => { 63 | options.compilerOptions.preserveWhitespace = true 64 | return options 65 | }) 66 | .end() 67 | 68 | config 69 | // https://webpack.js.org/configuration/devtool/#development 70 | .when(process.env.NODE_ENV === 'development', config => 71 | config.devtool('cheap-source-map') 72 | ) 73 | 74 | config.when(process.env.NODE_ENV !== 'development', config => { 75 | config 76 | .plugin('ScriptExtHtmlWebpackPlugin') 77 | .after('html') 78 | .use('script-ext-html-webpack-plugin', [ 79 | { 80 | // `runtime` must same as runtimeChunk name. default is `runtime` 81 | inline: /runtime\..*\.js$/ 82 | } 83 | ]) 84 | .end() 85 | config.optimization.splitChunks({ 86 | chunks: 'all', 87 | cacheGroups: { 88 | libs: { 89 | name: 'chunk-libs', 90 | test: /[\\/]node_modules[\\/]/, 91 | priority: 10, 92 | chunks: 'initial' // only package third parties that are initially dependent 93 | }, 94 | elementUI: { 95 | name: 'chunk-elementUI', // split elementUI into a single package 96 | priority: 20, // the weight needs to be larger than libs and app or it will be packaged into libs or app 97 | test: /[\\/]node_modules[\\/]_?element-ui(.*)/ // in order to adapt to cnpm 98 | }, 99 | commons: { 100 | name: 'chunk-commons', 101 | test: resolve('src/components'), // can customize your rules 102 | minChunks: 3, // minimum common number 103 | priority: 5, 104 | reuseExistingChunk: true 105 | } 106 | } 107 | }) 108 | config.optimization.runtimeChunk('single') 109 | }) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /vue-blog-h5/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | -------------------------------------------------------------------------------- /vue-blog-h5/.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 | -------------------------------------------------------------------------------- /vue-blog-h5/.env.development: -------------------------------------------------------------------------------- 1 | NODE_ENV='development' 2 | # must start with VUE_APP_ 3 | VUE_APP_ENV = 'development' 4 | 5 | -------------------------------------------------------------------------------- /vue-blog-h5/.env.production: -------------------------------------------------------------------------------- 1 | NODE_ENV='production' 2 | # must start with VUE_APP_ 3 | VUE_APP_ENV = 'production' 4 | -------------------------------------------------------------------------------- /vue-blog-h5/.env.staging: -------------------------------------------------------------------------------- 1 | NODE_ENV='production' 2 | # must start with VUE_APP_ 3 | VUE_APP_ENV = 'staging' 4 | 5 | -------------------------------------------------------------------------------- /vue-blog-h5/.eslintignore: -------------------------------------------------------------------------------- 1 | build/*.js 2 | src/assets 3 | public 4 | dist 5 | -------------------------------------------------------------------------------- /vue-blog-h5/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | extends: ['plugin:vue/essential', 'eslint:recommended', '@vue/prettier'], 7 | parserOptions: { 8 | parser: 'babel-eslint' 9 | }, 10 | rules: { 11 | "vue/max-attributes-per-line": [2, { 12 | "singleline": 10, 13 | "multiline": { 14 | "max": 1, 15 | "allowFirstLine": false 16 | } 17 | }], 18 | "vue/singleline-html-element-content-newline": "off", 19 | "vue/multiline-html-element-content-newline":"off", 20 | "vue/name-property-casing": ["error", "PascalCase"], 21 | "vue/no-v-html": "off", 22 | 'accessor-pairs': 2, 23 | 'arrow-spacing': [2, { 24 | 'before': true, 25 | 'after': true 26 | }], 27 | 'block-spacing': [2, 'always'], 28 | 'brace-style': [2, '1tbs', { 29 | 'allowSingleLine': true 30 | }], 31 | 'camelcase': [0, { 32 | 'properties': 'always' 33 | }], 34 | 'comma-dangle': [2, 'never'], 35 | 'comma-spacing': [2, { 36 | 'before': false, 37 | 'after': true 38 | }], 39 | 'comma-style': [2, 'last'], 40 | 'constructor-super': 2, 41 | 'curly': [2, 'multi-line'], 42 | 'dot-location': [2, 'property'], 43 | 'eol-last': 2, 44 | 'eqeqeq': ["error", "always", {"null": "ignore"}], 45 | 'generator-star-spacing': [2, { 46 | 'before': true, 47 | 'after': true 48 | }], 49 | 'handle-callback-err': [2, '^(err|error)$'], 50 | 'indent': [2, 2, { 51 | 'SwitchCase': 1 52 | }], 53 | 'jsx-quotes': [2, 'prefer-single'], 54 | 'key-spacing': [2, { 55 | 'beforeColon': false, 56 | 'afterColon': true 57 | }], 58 | 'keyword-spacing': [2, { 59 | 'before': true, 60 | 'after': true 61 | }], 62 | 'new-cap': [2, { 63 | 'newIsCap': true, 64 | 'capIsNew': false 65 | }], 66 | 'new-parens': 2, 67 | 'no-array-constructor': 2, 68 | 'no-caller': 2, 69 | 'no-console': 'off', 70 | 'no-class-assign': 2, 71 | 'no-cond-assign': 2, 72 | 'no-const-assign': 2, 73 | 'no-control-regex': 0, 74 | 'no-delete-var': 2, 75 | 'no-dupe-args': 2, 76 | 'no-dupe-class-members': 2, 77 | 'no-dupe-keys': 2, 78 | 'no-duplicate-case': 2, 79 | 'no-empty-character-class': 2, 80 | 'no-empty-pattern': 2, 81 | 'no-eval': 2, 82 | 'no-ex-assign': 2, 83 | 'no-extend-native': 2, 84 | 'no-extra-bind': 2, 85 | 'no-extra-boolean-cast': 2, 86 | 'no-extra-parens': [2, 'functions'], 87 | 'no-fallthrough': 2, 88 | 'no-floating-decimal': 2, 89 | 'no-func-assign': 2, 90 | 'no-implied-eval': 2, 91 | 'no-inner-declarations': [2, 'functions'], 92 | 'no-invalid-regexp': 2, 93 | 'no-irregular-whitespace': 2, 94 | 'no-iterator': 2, 95 | 'no-label-var': 2, 96 | 'no-labels': [2, { 97 | 'allowLoop': false, 98 | 'allowSwitch': false 99 | }], 100 | 'no-lone-blocks': 2, 101 | 'no-mixed-spaces-and-tabs': 2, 102 | 'no-multi-spaces': 2, 103 | 'no-multi-str': 2, 104 | 'no-multiple-empty-lines': [2, { 105 | 'max': 1 106 | }], 107 | 'no-native-reassign': 2, 108 | 'no-negated-in-lhs': 2, 109 | 'no-new-object': 2, 110 | 'no-new-require': 2, 111 | 'no-new-symbol': 2, 112 | 'no-new-wrappers': 2, 113 | 'no-obj-calls': 2, 114 | 'no-octal': 2, 115 | 'no-octal-escape': 2, 116 | 'no-path-concat': 2, 117 | 'no-proto': 2, 118 | 'no-redeclare': 2, 119 | 'no-regex-spaces': 2, 120 | 'no-return-assign': [2, 'except-parens'], 121 | 'no-self-assign': 2, 122 | 'no-self-compare': 2, 123 | 'no-sequences': 2, 124 | 'no-shadow-restricted-names': 2, 125 | 'no-spaced-func': 2, 126 | 'no-sparse-arrays': 2, 127 | 'no-this-before-super': 2, 128 | 'no-throw-literal': 2, 129 | 'no-trailing-spaces': 2, 130 | 'no-undef': 2, 131 | 'no-undef-init': 2, 132 | 'no-unexpected-multiline': 2, 133 | 'no-unmodified-loop-condition': 2, 134 | 'no-unneeded-ternary': [2, { 135 | 'defaultAssignment': false 136 | }], 137 | 'no-unreachable': 2, 138 | 'no-unsafe-finally': 2, 139 | 'no-unused-vars': [2, { 140 | 'vars': 'all', 141 | 'args': 'none' 142 | }], 143 | 'no-useless-call': 2, 144 | 'no-useless-computed-key': 2, 145 | 'no-useless-constructor': 2, 146 | 'no-useless-escape': 0, 147 | 'no-whitespace-before-property': 2, 148 | 'no-with': 2, 149 | 'one-var': [2, { 150 | 'initialized': 'never' 151 | }], 152 | 'operator-linebreak': [2, 'after', { 153 | 'overrides': { 154 | '?': 'before', 155 | ':': 'before' 156 | } 157 | }], 158 | 'padded-blocks': [2, 'never'], 159 | 'quotes': [2, 'single', { 160 | 'avoidEscape': true, 161 | 'allowTemplateLiterals': true 162 | }], 163 | 'semi': [2, 'never'], 164 | 'semi-spacing': [2, { 165 | 'before': false, 166 | 'after': true 167 | }], 168 | 'space-before-blocks': [2, 'always'], 169 | 'space-before-function-paren': [2, 'never'], 170 | 'space-in-parens': [2, 'never'], 171 | 'space-infix-ops': 2, 172 | 'space-unary-ops': [2, { 173 | 'words': true, 174 | 'nonwords': false 175 | }], 176 | 'spaced-comment': [2, 'always', { 177 | 'markers': ['global', 'globals', 'eslint', 'eslint-disable', '*package', '!', ','] 178 | }], 179 | 'template-curly-spacing': [2, 'never'], 180 | 'use-isnan': 2, 181 | 'valid-typeof': 2, 182 | 'wrap-iife': [2, 'any'], 183 | 'yield-star-spacing': [2, 'both'], 184 | 'yoda': [2, 'never'], 185 | 'prefer-const': 2, 186 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, 187 | 'object-curly-spacing': [2, 'always', { 188 | objectsInObjects: false 189 | }], 190 | 'array-bracket-spacing': [2, 'never'] 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /vue-blog-h5/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | /docs 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | 23 | package-lock.json 24 | yarn.lock -------------------------------------------------------------------------------- /vue-blog-h5/.postcssrc.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | module.exports = { 3 | plugins: { 4 | autoprefixer: { 5 | overrideBrowserslist: ['Android 4.1', 'iOS 7.1', 'Chrome > 31', 'ff > 31', 'ie >= 8'] 6 | }, 7 | 'postcss-pxtorem': { 8 | rootValue: 37.5, 9 | propList: ['*'], 10 | //selectorBlackList: ['van-'] 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /vue-blog-h5/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "tabWidth": 2, 4 | "singleQuote": true, 5 | "trailingComma": "none", 6 | "semi": false, 7 | "wrap_line_length": 120, 8 | "wrap_attributes": "auto", 9 | "proseWrap": "always", 10 | "arrowParens": "avoid", 11 | "bracketSpacing": true, 12 | "jsxBracketSameLine": true, 13 | "useTabs": false, 14 | "eslintIntegration":true, 15 | "overrides": [ 16 | { 17 | "files": ".prettierrc", 18 | "options": { 19 | "parser": "json" 20 | } 21 | } 22 | ], 23 | "endOfLine": "auto" 24 | } 25 | -------------------------------------------------------------------------------- /vue-blog-h5/babel.config.js: -------------------------------------------------------------------------------- 1 | // 获取 VUE_APP_ENV 非 NODE_ENV,测试环境依然 console 2 | const IS_PROD = ['production', 'prod'].includes(process.env.VUE_APP_ENV) 3 | const plugins = [ 4 | [ 5 | 'import', 6 | { 7 | libraryName: 'vant', 8 | libraryDirectory: 'es', 9 | style: true 10 | }, 11 | 'vant' 12 | ] 13 | ] 14 | // 去除 console.log 15 | if (IS_PROD) { 16 | plugins.push('transform-remove-console') 17 | } 18 | 19 | module.exports = { 20 | presets: [['@vue/cli-plugin-babel/preset', {useBuiltIns: 'usage', corejs: 3}]], 21 | plugins 22 | } 23 | -------------------------------------------------------------------------------- /vue-blog-h5/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "blog-h5", 3 | "version": "2.0.0", 4 | "description": "a vue blog h5", 5 | "author": "Jason Chen", 6 | "private": true, 7 | "scripts": { 8 | "serve": "vue-cli-service serve --open", 9 | "build": "vue-cli-service build", 10 | "stage": "vue-cli-service build --mode staging", 11 | "lint": "vue-cli-service lint" 12 | }, 13 | "dependencies": { 14 | "axios": "^0.19.2", 15 | "core-js": "^3.6.4", 16 | "lib-flexible": "^0.3.2", 17 | "lodash": "^4.17.15", 18 | "regenerator-runtime": "^0.13.5", 19 | "vant": "^2.4.7", 20 | "vue": "^2.6.11", 21 | "vue-router": "^3.1.5", 22 | "vuex": "^3.1.2" 23 | }, 24 | "devDependencies": { 25 | "@vue/cli-plugin-babel": "~4.3.0", 26 | "@vue/cli-plugin-eslint": "~4.3.0", 27 | "@vue/cli-service": "~4.3.0", 28 | "@vue/eslint-config-prettier": "^6.0.0", 29 | "babel-eslint": "^10.0.3", 30 | "babel-plugin-import": "^1.13.0", 31 | "babel-plugin-transform-remove-console": "^6.9.4", 32 | "eslint": "^6.7.2", 33 | "eslint-plugin-prettier": "^3.1.1", 34 | "eslint-plugin-vue": "^6.2.2", 35 | "node-sass": "^4.13.1", 36 | "postcss-pxtorem": "^4.0.1", 37 | "prettier": "^1.19.1", 38 | "sass-loader": "^8.0.2", 39 | "script-ext-html-webpack-plugin": "^2.1.4", 40 | "vue-template-compiler": "^2.6.11", 41 | "webpack-bundle-analyzer": "^3.7.0" 42 | } 43 | } -------------------------------------------------------------------------------- /vue-blog-h5/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcyicai/vue-blog-template/525110674b567994f807ac23ed23556a3f12b043/vue-blog-h5/public/favicon.ico -------------------------------------------------------------------------------- /vue-blog-h5/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 14 | 15 | <%= webpackConfig.name %> 16 | 17 | 171 | 172 | 173 | 174 | 178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 | 194 | 200 | 201 | 202 | -------------------------------------------------------------------------------- /vue-blog-h5/src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 21 | 22 | -------------------------------------------------------------------------------- /vue-blog-h5/src/api/common.js: -------------------------------------------------------------------------------- 1 | import api from './index' 2 | // axios 3 | import request from '@/utils/request' 4 | 5 | //获取文章 6 | export function getArticleList(data) { 7 | return request({ 8 | url: api.Article, 9 | method: 'get', 10 | params: data, 11 | hideloading: true 12 | }) 13 | } 14 | 15 | //获取照片 16 | export function getAlbumList(data) { 17 | return request({ 18 | url: api.Album, 19 | method: 'get', 20 | params: data, 21 | hideloading: true 22 | }) 23 | } 24 | 25 | //获取照片 26 | export function getUpdateLogList(data) { 27 | return request({ 28 | url: api.UpdateLog, 29 | method: 'get', 30 | params: data, 31 | hideloading: true 32 | }) 33 | } 34 | 35 | //发送留言 36 | export function postSendWhisper(data) { 37 | return request({ 38 | url: api.SendWhisper, 39 | method: 'post', 40 | data, 41 | hideloading: true 42 | }) 43 | } 44 | 45 | //获取留言 46 | export function getWhisperList(data) { 47 | return request({ 48 | url: api.Whisper, 49 | method: 'get', 50 | params: data, 51 | hideloading: true 52 | }) 53 | } -------------------------------------------------------------------------------- /vue-blog-h5/src/api/index.js: -------------------------------------------------------------------------------- 1 | const api = { 2 | Login: '/user/login', 3 | UserInfo: '/user/userinfo', 4 | UserName: '/user/name', 5 | 6 | Article: '/blogArticle/list', //文章 7 | Album: '/blogAlbum/list', //照片 8 | UpdateLog: '/blogUpdateLog/list', //更新日志 9 | Whisper: '/blogWhisper/list', //留言 10 | SendWhisper: '/blogWhisper/insert', //发送留言 11 | } 12 | 13 | export default api 14 | -------------------------------------------------------------------------------- /vue-blog-h5/src/api/user.js: -------------------------------------------------------------------------------- 1 | import api from './index' 2 | // axios 3 | import request from '@/utils/request' 4 | 5 | // 登录 6 | export function login(data) { 7 | return request({ 8 | url: api.Login, 9 | method: 'post', 10 | data 11 | }) 12 | } 13 | 14 | // 用户信息 post 方法 15 | export function getUserInfo(data) { 16 | return request({ 17 | url: api.UserInfo, 18 | method: 'post', 19 | data, 20 | hideloading: true 21 | }) 22 | } 23 | 24 | // 用户名称 get 方法 25 | export function getUserName(params) { 26 | return request({ 27 | url: api.UserName, 28 | method: 'get', 29 | params, 30 | hideloading: true 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /vue-blog-h5/src/assets/css/index.scss: -------------------------------------------------------------------------------- 1 | @import './variables.scss'; 2 | @import './mixin.scss'; 3 | 4 | html, 5 | body .app { 6 | color: #333333; 7 | font-family: Arial, Helvetica, 'STHeiti STXihei', 'Microsoft YaHei', Tohoma, sans-serif; 8 | background-color: $background-color; 9 | } 10 | 11 | .app-container { 12 | padding-bottom: 50px; 13 | } 14 | -------------------------------------------------------------------------------- /vue-blog-h5/src/assets/css/mixin.scss: -------------------------------------------------------------------------------- 1 | // mixin 2 | // 清除浮动 3 | @mixin clearfix { 4 | &:after { 5 | content: ""; 6 | display: table; 7 | clear: both; 8 | } 9 | } 10 | 11 | // 多行隐藏 12 | @mixin textoverflow($clamp:1) { 13 | display: block; 14 | overflow: hidden; 15 | text-overflow: ellipsis; 16 | -o-text-overflow: ellipsis; 17 | display: -webkit-box; 18 | -webkit-line-clamp: $clamp; 19 | /*! autoprefixer: ignore next */ 20 | -webkit-box-orient: vertical; 21 | } 22 | 23 | //flex box 24 | @mixin flexbox($jc:space-between, $ai:center, $fd:row, $fw:nowrap) { 25 | display: flex; 26 | display: -webkit-flex; 27 | flex: 1; 28 | justify-content: $jc; 29 | -webkit-justify-content: $jc; 30 | align-items: $ai; 31 | -webkit-align-items: $ai; 32 | flex-direction: $fd; 33 | -webkit-flex-direction: $fd; 34 | flex-wrap: $fw; 35 | -webkit-flex-wrap: $fw; 36 | } 37 | -------------------------------------------------------------------------------- /vue-blog-h5/src/assets/css/variables.scss: -------------------------------------------------------------------------------- 1 | 2 | // variables 3 | $background-color: #f8f8f8; 4 | -------------------------------------------------------------------------------- /vue-blog-h5/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcyicai/vue-blog-template/525110674b567994f807ac23ed23556a3f12b043/vue-blog-h5/src/assets/logo.png -------------------------------------------------------------------------------- /vue-blog-h5/src/components/Header.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 24 | 25 | 33 | -------------------------------------------------------------------------------- /vue-blog-h5/src/components/TabBar.vue: -------------------------------------------------------------------------------- 1 | 10 | 37 | 38 | 39 | 58 | -------------------------------------------------------------------------------- /vue-blog-h5/src/config/env.development.js: -------------------------------------------------------------------------------- 1 | // 本地环境配置 2 | module.exports = { 3 | title: 'CYC Blog', 4 | baseUrl: 'http://127.0.0.1:8090', // 项目地址 5 | baseApi: 'http://127.0.0.1:8090/api', // 本地api请求地址,注意:如果你使用了代理,请设置成'/' 6 | APPID: '', 7 | APPSECRET: '', 8 | $cdn: '' 9 | } 10 | -------------------------------------------------------------------------------- /vue-blog-h5/src/config/env.production.js: -------------------------------------------------------------------------------- 1 | // 正式 2 | module.exports = { 3 | title: 'CYC Blog', 4 | baseUrl: 'http://127.0.0.1:8090', // 正式项目地址 5 | baseApi: 'http://127.0.0.1:8090/api', // 正式api请求地址 6 | APPID: '', 7 | APPSECRET: '', 8 | $cdn: '' 9 | } 10 | -------------------------------------------------------------------------------- /vue-blog-h5/src/config/env.staging.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | title: 'CYC Blog', 3 | baseUrl: 'https://test.xxx.com', // 测试项目地址 4 | baseApi: 'https://test.xxx.com/api', // 测试api请求地址 5 | APPID: '', 6 | APPSECRET: '', 7 | $cdn: '' 8 | } 9 | -------------------------------------------------------------------------------- /vue-blog-h5/src/config/index.js: -------------------------------------------------------------------------------- 1 | // 根据环境引入不同配置 process.env.NODE_ENV 2 | const config = require('./env.' + process.env.VUE_APP_ENV) 3 | module.exports = config 4 | -------------------------------------------------------------------------------- /vue-blog-h5/src/filters/filter.js: -------------------------------------------------------------------------------- 1 | /** 2 | *格式化时间 3 | *yyyy-MM-dd hh:mm:ss 4 | */ 5 | export function formatDate(time, fmt) { 6 | if (time === undefined || '') { 7 | return 8 | } 9 | const date = new Date(time) 10 | if (/(y+)/.test(fmt)) { 11 | fmt = fmt.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length)) 12 | } 13 | const o = { 14 | 'M+': date.getMonth() + 1, 15 | 'd+': date.getDate(), 16 | 'h+': date.getHours(), 17 | 'm+': date.getMinutes(), 18 | 's+': date.getSeconds() 19 | } 20 | for (const k in o) { 21 | if (new RegExp(`(${k})`).test(fmt)) { 22 | const str = o[k] + '' 23 | fmt = fmt.replace(RegExp.$1, RegExp.$1.length === 1 ? str : padLeftZero(str)) 24 | } 25 | } 26 | return fmt 27 | } 28 | 29 | function padLeftZero(str) { 30 | return ('00' + str).substr(str.length) 31 | } 32 | /* 33 | * 隐藏用户手机号中间四位 34 | */ 35 | export function hidePhone(phone) { 36 | return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2') 37 | } 38 | -------------------------------------------------------------------------------- /vue-blog-h5/src/filters/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import * as filter from './filter' 3 | 4 | Object.keys(filter).forEach(k => Vue.filter(k, filter[k])) 5 | 6 | Vue.prototype.$formatDate = Vue.filter('formatDate') 7 | Vue.prototype.$hidePhone = Vue.filter('hidePhone') 8 | -------------------------------------------------------------------------------- /vue-blog-h5/src/main.js: -------------------------------------------------------------------------------- 1 | // 兼容 IE 2 | // https://github.com/zloirock/core-js/blob/master/docs/2019-03-19-core-js-3-babel-and-a-look-into-the-future.md#babelpolyfill 3 | import 'core-js/stable' 4 | import 'regenerator-runtime/runtime' 5 | 6 | import Vue from 'vue' 7 | import App from './App.vue' 8 | import router from './router' 9 | import store from './store' 10 | 11 | // 设置 js中可以访问 $cdn 12 | import { $cdn } from '@/config' 13 | Vue.prototype.$cdn = $cdn 14 | 15 | // 全局引入按需引入UI库 vant 16 | import '@/plugins/vant' 17 | // 引入全局样式 18 | import '@/assets/css/index.scss' 19 | // 移动端适配 20 | import 'lib-flexible/flexible.js' 21 | 22 | // filters 23 | import './filters' 24 | Vue.config.productionTip = false 25 | 26 | new Vue({ 27 | el: '#app', 28 | router, 29 | store, 30 | render: h => h(App) 31 | }) 32 | -------------------------------------------------------------------------------- /vue-blog-h5/src/plugins/vant.js: -------------------------------------------------------------------------------- 1 | // 按需全局引入 vant组件 2 | import Vue from 'vue' 3 | import { 4 | Button, 5 | List, 6 | Cell, 7 | Tabbar, 8 | TabbarItem, 9 | NavBar, 10 | Swipe, 11 | SwipeItem, 12 | Lazyload, 13 | Col, 14 | Row, 15 | Image as VanImage, 16 | Step, 17 | Steps, 18 | Dialog, 19 | Notify, 20 | Field, 21 | Form, 22 | Skeleton 23 | } from 'vant' 24 | 25 | Vue.use(Button) 26 | Vue.use(Cell) 27 | Vue.use(List) 28 | Vue.use(Tabbar).use(TabbarItem) 29 | Vue.use(NavBar) 30 | Vue.use(Swipe).use(SwipeItem) 31 | Vue.use(Lazyload) 32 | Vue.use(Col) 33 | Vue.use(Row) 34 | Vue.use(VanImage) 35 | Vue.use(Step) 36 | Vue.use(Steps) 37 | Vue.use(Dialog) 38 | Vue.use(Notify) 39 | Vue.use(Field) 40 | Vue.use(Form) 41 | Vue.use(Skeleton) -------------------------------------------------------------------------------- /vue-blog-h5/src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import { constantRouterMap } from './router.config.js' 4 | 5 | // hack router push callback 6 | const originalPush = Router.prototype.push 7 | Router.prototype.push = function push(location, onResolve, onReject) { 8 | if (onResolve || onReject) return originalPush.call(this, location, onResolve, onReject) 9 | return originalPush.call(this, location).catch(err => err) 10 | } 11 | 12 | Vue.use(Router) 13 | 14 | const createRouter = () => 15 | new Router({ 16 | // mode: 'history', // 如果你是 history模式 需要配置vue.config.js publicPath 17 | // base: process.env.BASE_URL, 18 | scrollBehavior: () => ({ y: 0 }), 19 | routes: constantRouterMap 20 | }) 21 | 22 | const router = createRouter() 23 | 24 | // Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465 25 | export function resetRouter() { 26 | const newRouter = createRouter() 27 | router.matcher = newRouter.matcher // reset router 28 | } 29 | 30 | export default router 31 | -------------------------------------------------------------------------------- /vue-blog-h5/src/router/router.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 基础路由 3 | * @type { *[] } 4 | */ 5 | export const constantRouterMap = [ 6 | { 7 | path: '/', 8 | component: () => import('@/views/layouts/index'), 9 | redirect: '/home', 10 | meta: { 11 | title: '首页', 12 | keepAlive: false 13 | }, 14 | children: [ 15 | { 16 | path: '/home', 17 | name: 'Home', 18 | component: () => import('@/views/home/index'), 19 | meta: { title: 'CYC Blog', keepAlive: false } 20 | }, 21 | { 22 | path: '/whisper', 23 | name: 'Whisper', 24 | component: () => import('@/views/whisper/index'), 25 | meta: { title: '留言', keepAlive: false } 26 | }, 27 | { 28 | path: '/album', 29 | name: 'Album', 30 | component: () => import('@/views/album/index'), 31 | meta: { title: '相册', keepAlive: false } 32 | }, 33 | { 34 | path: '/updateLog', 35 | name: 'UpdateLog', 36 | component: () => import('@/views/updateLog/index'), 37 | meta: { title: '更新', keepAlive: false } 38 | }, 39 | { 40 | path: '/about', 41 | name: 'About', 42 | component: () => import('@/views/about/index'), 43 | meta: { title: '关于', keepAlive: false } 44 | } 45 | ] 46 | } 47 | ] 48 | -------------------------------------------------------------------------------- /vue-blog-h5/src/store/getters.js: -------------------------------------------------------------------------------- 1 | const getters = { 2 | userName: state => state.app.userName 3 | } 4 | export default getters 5 | -------------------------------------------------------------------------------- /vue-blog-h5/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 | 6 | Vue.use(Vuex) 7 | 8 | const store = new Vuex.Store({ 9 | modules: { 10 | app 11 | }, 12 | getters 13 | }) 14 | 15 | export default store 16 | -------------------------------------------------------------------------------- /vue-blog-h5/src/store/modules/app.js: -------------------------------------------------------------------------------- 1 | const state = { 2 | userName: '' 3 | } 4 | const mutations = { 5 | SET_USER_NAME(state, name) { 6 | state.userName = name 7 | } 8 | } 9 | const actions = { 10 | // 设置name 11 | setUserName({ commit }, name) { 12 | commit('SET_USER_NAME', name) 13 | } 14 | } 15 | export default { 16 | state, 17 | mutations, 18 | actions 19 | } 20 | -------------------------------------------------------------------------------- /vue-blog-h5/src/utils/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by PanJiaChen on 16/11/18. 3 | */ 4 | 5 | /** 6 | * Parse the time to string 7 | * @param {(Object|string|number)} time 8 | * @param {string} cFormat 9 | * @returns {string} 10 | */ 11 | export function parseTime(time, cFormat) { 12 | if (arguments.length === 0) { 13 | return null 14 | } 15 | const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}' 16 | let date 17 | if (typeof time === 'object') { 18 | date = time 19 | } else { 20 | if ((typeof time === 'string') && (/^[0-9]+$/.test(time))) { 21 | time = parseInt(time) 22 | } 23 | if ((typeof time === 'number') && (time.toString().length === 10)) { 24 | time = time * 1000 25 | } 26 | date = new Date(time) 27 | } 28 | const formatObj = { 29 | y: date.getFullYear(), 30 | m: date.getMonth() + 1, 31 | d: date.getDate(), 32 | h: date.getHours(), 33 | i: date.getMinutes(), 34 | s: date.getSeconds(), 35 | a: date.getDay() 36 | } 37 | const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => { 38 | let value = formatObj[key] 39 | // Note: getDay() returns 0 on Sunday 40 | if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value ] } 41 | if (result.length > 0 && value < 10) { 42 | value = '0' + value 43 | } 44 | return value || 0 45 | }) 46 | return time_str 47 | } 48 | 49 | /** 50 | * @param {number} time 51 | * @param {string} option 52 | * @returns {string} 53 | */ 54 | export function formatTime(time, option) { 55 | if (('' + time).length === 10) { 56 | time = parseInt(time) * 1000 57 | } else { 58 | time = +time 59 | } 60 | const d = new Date(time) 61 | const now = Date.now() 62 | 63 | const diff = (now - d) / 1000 64 | 65 | if (diff < 30) { 66 | return '刚刚' 67 | } else if (diff < 3600) { 68 | // less 1 hour 69 | return Math.ceil(diff / 60) + '分钟前' 70 | } else if (diff < 3600 * 24) { 71 | return Math.ceil(diff / 3600) + '小时前' 72 | } else if (diff < 3600 * 24 * 2) { 73 | return '1天前' 74 | } 75 | if (option) { 76 | return parseTime(time, option) 77 | } else { 78 | return ( 79 | d.getMonth() + 80 | 1 + 81 | '月' + 82 | d.getDate() + 83 | '日' + 84 | d.getHours() + 85 | '时' + 86 | d.getMinutes() + 87 | '分' 88 | ) 89 | } 90 | } 91 | 92 | /** 93 | * @param {string} url 94 | * @returns {Object} 95 | */ 96 | export function param2Obj(url) { 97 | const search = url.split('?')[1] 98 | if (!search) { 99 | return {} 100 | } 101 | return JSON.parse( 102 | '{"' + 103 | decodeURIComponent(search) 104 | .replace(/"/g, '\\"') 105 | .replace(/&/g, '","') 106 | .replace(/=/g, '":"') 107 | .replace(/\+/g, ' ') + 108 | '"}' 109 | ) 110 | } 111 | -------------------------------------------------------------------------------- /vue-blog-h5/src/utils/request.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import store from '@/store' 3 | import { Toast } from 'vant' 4 | // 根据环境不同引入不同api地址 5 | import { baseApi } from '@/config' 6 | // create an axios instance 7 | const service = axios.create({ 8 | baseURL: baseApi, // url = base api url + request url 9 | // withCredentials: true, // send cookies when cross-domain requests 10 | timeout: 5000 // request timeout 11 | }) 12 | 13 | // request拦截器 request interceptor 14 | service.interceptors.request.use( 15 | config => { 16 | // 不传递默认开启loading 17 | if (!config.hideloading) { 18 | // loading 19 | Toast.loading({ 20 | forbidClick: true 21 | }) 22 | } 23 | if (store.getters.token) { 24 | config.headers['X-Token'] = '' 25 | } 26 | return config 27 | }, 28 | error => { 29 | // do something with request error 30 | console.log(error) // for debug 31 | return Promise.reject(error) 32 | } 33 | ) 34 | // respone拦截器 35 | service.interceptors.response.use( 36 | response => { 37 | Toast.clear() 38 | const res = response.data 39 | if (res.code && res.code !== 200) { 40 | // 登录超时,重新登录 41 | if (res.code === 401) { 42 | store.dispatch('FedLogOut').then(() => { 43 | location.reload() 44 | }) 45 | } 46 | return Promise.reject(res || 'error') 47 | } else { 48 | return Promise.resolve(res) 49 | } 50 | }, 51 | error => { 52 | Toast.clear() 53 | console.log('err' + error) // for debug 54 | return Promise.reject(error) 55 | } 56 | ) 57 | 58 | export default service 59 | -------------------------------------------------------------------------------- /vue-blog-h5/src/utils/validate.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Sunnie on 19/06/04. 3 | */ 4 | 5 | /** 6 | * @param {string} path 7 | * @returns {Boolean} 8 | */ 9 | export function isExternal(path) { 10 | return /^(https?:|mailto:|tel:)/.test(path) 11 | } 12 | 13 | /** 14 | * @param {string} str 15 | * @returns {Boolean} 16 | */ 17 | export function validUsername(str) { 18 | const valid_map = ['admin', 'editor'] 19 | return valid_map.indexOf(str.trim()) >= 0 20 | } 21 | -------------------------------------------------------------------------------- /vue-blog-h5/src/views/about/index.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 45 | 46 | 76 | -------------------------------------------------------------------------------- /vue-blog-h5/src/views/album/index.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 81 | 82 | 91 | -------------------------------------------------------------------------------- /vue-blog-h5/src/views/home/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 21 | 22 | 73 | 74 | 90 | -------------------------------------------------------------------------------- /vue-blog-h5/src/views/layouts/index.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 87 | -------------------------------------------------------------------------------- /vue-blog-h5/src/views/updateLog/index.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 35 | 36 | 42 | -------------------------------------------------------------------------------- /vue-blog-h5/src/views/whisper/index.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 117 | 118 | 133 | -------------------------------------------------------------------------------- /vue-blog-h5/vue.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const defaultSettings = require('./src/config/index.js') 4 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin 5 | 6 | const resolve = dir => path.join(__dirname, dir) 7 | // page title 8 | const name = defaultSettings.title || 'CYC Blog' 9 | const IS_PROD = ['production', 'prod'].includes(process.env.NODE_ENV) 10 | 11 | module.exports = { 12 | publicPath: './', // 署应用包时的基本 URL。 vue-router hash 模式使用 13 | outputDir: 'dist', // 生产环境构建文件的目录 14 | assetsDir: 'static', // outputDir的静态资源(js、css、img、fonts)目录 15 | lintOnSave: !IS_PROD, 16 | productionSourceMap: false, // 如果你不需要生产环境的 source map,可以将其设置为 false 以加速生产环境构建。 17 | devServer: { 18 | port: 8080, // 端口 19 | open: false, // 启动后打开浏览器 20 | overlay: { 21 | // 当出现编译器错误或警告时,在浏览器中显示全屏覆盖层 22 | warnings: false, 23 | errors: true 24 | }, 25 | /* proxy: { 26 | //配置跨域 27 | "/api": { 28 | target: "http://127.0.0.1:8090/api", 29 | changeOrigin: true, 30 | ws: true, 31 | pathRewrite: { 32 | "^/api": "/" 33 | } 34 | } 35 | } */ 36 | }, 37 | css: { 38 | extract: IS_PROD, //是否将组件中的 CSS 提取至一个独立的 CSS 文件中 (而不是动态注入到 JavaScript 中的 inline 代码)。 39 | sourceMap: false, 40 | loaderOptions: { 41 | scss: { 42 | // 向全局sass样式传入共享的全局变量, $src可以配置图片cdn前缀 43 | // 详情: https://cli.vuejs.org/guide/css.html#passing-options-to-pre-processor-loaders 44 | prependData: ` 45 | @import "assets/css/mixin.scss"; 46 | @import "assets/css/variables.scss"; 47 | $cdn: "${defaultSettings.$cdn}"; 48 | ` 49 | } 50 | } 51 | }, 52 | configureWebpack: config => { 53 | config.name = name 54 | if (process.env.NODE_ENV === 'production') {// 为生产环境修改配置... 55 | config.mode = 'production'; 56 | config["performance"] = {//打包文件大小配置 57 | "maxEntrypointSize": 10000000, 58 | "maxAssetSize": 30000000 59 | } 60 | } 61 | 62 | // 为生产环境修改配置... 63 | // if (IS_PROD) { 64 | // // externals 65 | // config.externals = externals 66 | // } 67 | }, 68 | 69 | chainWebpack: config => { 70 | config.plugins.delete('preload') // TODO: need test 71 | config.plugins.delete('prefetch') // TODO: need test 72 | 73 | // 别名 alias 74 | config.resolve.alias 75 | .set('@', resolve('src')) 76 | .set('assets', resolve('src/assets')) 77 | .set('api', resolve('src/api')) 78 | .set('views', resolve('src/views')) 79 | .set('components', resolve('src/components')) 80 | 81 | /** 82 | * 添加CDN参数到htmlWebpackPlugin配置中 83 | */ 84 | // config.plugin('html').tap(args => { 85 | // if (IS_PROD) { 86 | // args[0].cdn = cdn.build 87 | // } else { 88 | // args[0].cdn = cdn.dev 89 | // } 90 | // return args 91 | // }) 92 | 93 | /** 94 | * 设置保留空格 95 | */ 96 | config.module 97 | .rule('vue') 98 | .use('vue-loader') 99 | .loader('vue-loader') 100 | .tap(options => { 101 | options.compilerOptions.preserveWhitespace = true 102 | return options 103 | }) 104 | .end() 105 | /** 106 | * 打包分析 107 | */ 108 | if (IS_PROD) { 109 | config.plugin('webpack-report').use(BundleAnalyzerPlugin, [ 110 | { 111 | analyzerMode: 'static' 112 | } 113 | ]) 114 | } 115 | config 116 | // https://webpack.js.org/configuration/devtool/#development 117 | .when(!IS_PROD, config => config.devtool('cheap-source-map')) 118 | 119 | config.when(IS_PROD, config => { 120 | config 121 | .plugin('ScriptExtHtmlWebpackPlugin') 122 | .after('html') 123 | .use('script-ext-html-webpack-plugin', [ 124 | { 125 | // 将 runtime 作为内联引入不单独存在 126 | inline: /runtime\..*\.js$/ 127 | } 128 | ]) 129 | .end() 130 | config.optimization.splitChunks({ 131 | chunks: 'all', 132 | cacheGroups: { 133 | // cacheGroups 下可以可以配置多个组,每个组根据test设置条件,符合test条件的模块 134 | commons: { 135 | name: 'chunk-commons', 136 | test: resolve('src/components'), 137 | minChunks: 3, // 被至少用三次以上打包分离 138 | priority: 5, // 优先级 139 | reuseExistingChunk: true // 表示是否使用已有的 chunk,如果为 true 则表示如果当前的 chunk 包含的模块已经被抽取出去了,那么将不会重新生成新的。 140 | }, 141 | node_vendors: { 142 | name: 'chunk-libs', 143 | chunks: 'initial', // 只打包初始时依赖的第三方 144 | test: /[\\/]node_modules[\\/]/, 145 | priority: 10 146 | }, 147 | vantUI: { 148 | name: 'chunk-vantUI', // 单独将 vantUI 拆包 149 | priority: 20, // 数字大权重到,满足多个 cacheGroups 的条件时候分到权重高的 150 | test: /[\\/]node_modules[\\/]_?vant(.*)/ 151 | } 152 | } 153 | }) 154 | config.optimization.runtimeChunk('single') 155 | }) 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /vue-blog-template/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | -------------------------------------------------------------------------------- /vue-blog-template/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | extends: ["plugin:vue/essential", "@vue/prettier"], 7 | rules: { 8 | "no-console": process.env.NODE_ENV === "production" ? "error" : "off", 9 | "no-debugger": process.env.NODE_ENV === "production" ? "error" : "off" 10 | }, 11 | parserOptions: { 12 | parser: "babel-eslint" 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /vue-blog-template/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | -------------------------------------------------------------------------------- /vue-blog-template/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ["@vue/app"] 3 | }; 4 | -------------------------------------------------------------------------------- /vue-blog-template/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-blog-template", 3 | "author": "Jason Chen", 4 | "version": "1.0.0", 5 | "private": true, 6 | "scripts": { 7 | "serve": "vue-cli-service serve", 8 | "build": "vue-cli-service build", 9 | "lint": "vue-cli-service lint" 10 | }, 11 | "dependencies": { 12 | "axios": "^0.19.0", 13 | "element-ui": "^2.11.1", 14 | "epic-spinners": "^1.1.0", 15 | "postcss-pxtorem": "^4.0.1", 16 | "viewerjs": "^1.3.6", 17 | "vue": "^2.6.10", 18 | "vue-ba": "^1.2.5", 19 | "vue-router": "^3.0.3", 20 | "vuex": "^3.0.1" 21 | }, 22 | "devDependencies": { 23 | "@vue/cli-plugin-babel": "^3.9.0", 24 | "@vue/cli-plugin-eslint": "^3.9.0", 25 | "@vue/cli-service": "^3.9.0", 26 | "@vue/eslint-config-prettier": "^4.0.1", 27 | "babel-eslint": "^10.0.1", 28 | "babel-plugin-component": "^1.1.1", 29 | "babel-polyfill": "^6.26.0", 30 | "eslint": "^5.16.0", 31 | "eslint-plugin-vue": "^5.0.0", 32 | "node-sass": "^4.9.0", 33 | "sass-loader": "^7.1.0", 34 | "vue-template-compiler": "^2.6.10" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /vue-blog-template/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {} 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /vue-blog-template/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcyicai/vue-blog-template/525110674b567994f807ac23ed23556a3f12b043/vue-blog-template/public/favicon.ico -------------------------------------------------------------------------------- /vue-blog-template/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | 10 | CYC Blog 11 | 165 | 166 | 167 | 168 | 172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 | 188 | 189 | 196 | 197 | 198 | 199 | -------------------------------------------------------------------------------- /vue-blog-template/src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 24 | 25 | 55 | -------------------------------------------------------------------------------- /vue-blog-template/src/assets/styles/index.scss: -------------------------------------------------------------------------------- 1 | body { 2 | height: 100%; 3 | -moz-osx-font-smoothing: grayscale; 4 | -webkit-font-smoothing: antialiased; 5 | text-rendering: optimizeLegibility; 6 | font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, 7 | Microsoft YaHei, Arial, sans-serif; 8 | } 9 | 10 | html { 11 | height: 100%; 12 | box-sizing: border-box; 13 | } 14 | 15 | #app { 16 | height: 100%; 17 | } 18 | 19 | .layout { 20 | width: 1200px; 21 | margin: 0 auto; 22 | } 23 | 24 | *, 25 | *:before, 26 | *:after { 27 | box-sizing: inherit; 28 | } 29 | 30 | .no-padding { 31 | padding: 0px !important; 32 | } 33 | 34 | .padding-content { 35 | padding: 4px 0; 36 | } 37 | 38 | a:focus, 39 | a:active { 40 | outline: none; 41 | } 42 | 43 | a, 44 | a:focus, 45 | a:hover { 46 | cursor: pointer; 47 | color: inherit; 48 | text-decoration: none; 49 | } 50 | 51 | div:focus { 52 | outline: none; 53 | } 54 | 55 | .fr { 56 | float: right; 57 | } 58 | 59 | .fl { 60 | float: left; 61 | } 62 | 63 | .block { 64 | display: block; 65 | } 66 | 67 | .pointer { 68 | cursor: pointer; 69 | } 70 | 71 | .inlineBlock { 72 | display: block; 73 | } 74 | 75 | .clearfix { 76 | &:after { 77 | visibility: hidden; 78 | display: block; 79 | font-size: 0; 80 | content: " "; 81 | clear: both; 82 | height: 0; 83 | } 84 | } 85 | 86 | //main-container全局样式 87 | .app-container { 88 | padding: 20px; 89 | } 90 | 91 | .text-center { 92 | text-align: center; 93 | } 94 | 95 | /*滚动条样式修改*/ 96 | ::-webkit-scrollbar { 97 | width: 8px; 98 | height: 8px; 99 | } 100 | 101 | ::-webkit-scrollbar-button:vertical { 102 | display: none; 103 | } 104 | 105 | ::-webkit-scrollbar-corner, 106 | ::-webkit-scrollbar-track { 107 | background-color: #f4f5f7; 108 | } 109 | 110 | ::-webkit-scrollbar-thumb { 111 | border-radius: 5px; 112 | background-color: #d7dbe5; 113 | } 114 | -------------------------------------------------------------------------------- /vue-blog-template/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import router from "./router"; 3 | import store from "./store"; 4 | import ElementUI from "element-ui"; 5 | import axios from "axios"; 6 | import ba from "vue-ba"; //引入百度统计 7 | import "element-ui/lib/theme-chalk/index.css"; 8 | import "@/assets/styles/normalize.scss"; 9 | import "@/assets/styles/index.scss"; 10 | import App from "./App.vue"; 11 | 12 | Vue.config.productionTip = false; 13 | Vue.use(ElementUI); 14 | Vue.prototype.$axios = axios; 15 | Vue.use(ba, "c5d78"); //百度统计账户ID码,自行注册后更改 16 | Vue.use(ba, { siteId: "c5d78" }); 17 | 18 | new Vue({ 19 | router, 20 | store, 21 | render: h => h(App) 22 | }).$mount("#app"); 23 | -------------------------------------------------------------------------------- /vue-blog-template/src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import Router from "vue-router"; 3 | import Layout from "@/views/layout"; 4 | 5 | Vue.use(Router); 6 | 7 | export default new Router({ 8 | mode: "history", 9 | base: process.env.BASE_URL, 10 | routes: [ 11 | { 12 | path: "/", 13 | redirect: { 14 | path: "/home" 15 | } 16 | }, 17 | { 18 | path: "", 19 | component: Layout, 20 | children: [ 21 | { 22 | path: "/home", 23 | component: () => import("@/views/home/Home"), 24 | name: "home", 25 | meta: { title: "文章" } 26 | }, 27 | { 28 | path: "/whisper", 29 | component: () => import("@/views/whisper/Whisper"), 30 | name: "whisper", 31 | meta: { title: "留言" } 32 | }, 33 | { 34 | path: "/album", 35 | component: () => import("@/views/album/Album"), 36 | name: "album", 37 | meta: { title: "相册" } 38 | }, 39 | { 40 | path: "/updateLog", 41 | component: () => import("@/views/updateLog/UpdateLog"), 42 | name: "updateLog", 43 | meta: { title: "更新" } 44 | }, 45 | { 46 | path: "/about", 47 | component: () => import("@/views/about/About"), 48 | name: "about", 49 | meta: { title: "关于" } 50 | } 51 | ] 52 | } 53 | ], 54 | scrollBehavior() { 55 | return { x: 0, y: 0 }; 56 | } 57 | }); 58 | -------------------------------------------------------------------------------- /vue-blog-template/src/store.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import Vuex from "vuex"; 3 | 4 | Vue.use(Vuex); 5 | 6 | export default new Vuex.Store({ 7 | state: {}, 8 | mutations: {}, 9 | actions: {} 10 | }); 11 | -------------------------------------------------------------------------------- /vue-blog-template/src/utils/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Parse the time to string 3 | * @param {(Object|string|number)} time 4 | * @param {string} cFormat 5 | * @returns {string} 6 | */ 7 | export function parseTime(time, cFormat) { 8 | if (arguments.length === 0) { 9 | return null; 10 | } 11 | const format = cFormat || "{y}-{m}-{d} {h}:{i}:{s}"; 12 | let date; 13 | if (typeof time === "object") { 14 | date = time; 15 | } else { 16 | if (typeof time === "string" && /^[0-9]+$/.test(time)) { 17 | time = parseInt(time); 18 | } 19 | if (typeof time === "number" && time.toString().length === 10) { 20 | time = time * 1000; 21 | } 22 | date = new Date(time); 23 | } 24 | const formatObj = { 25 | y: date.getFullYear(), 26 | m: date.getMonth() + 1, 27 | d: date.getDate(), 28 | h: date.getHours(), 29 | i: date.getMinutes(), 30 | s: date.getSeconds(), 31 | a: date.getDay() 32 | }; 33 | const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => { 34 | let value = formatObj[key]; 35 | // Note: getDay() returns 0 on Sunday 36 | if (key === "a") { 37 | return ["日", "一", "二", "三", "四", "五", "六"][value]; 38 | } 39 | if (result.length > 0 && value < 10) { 40 | value = "0" + value; 41 | } 42 | return value || 0; 43 | }); 44 | return time_str; 45 | } 46 | -------------------------------------------------------------------------------- /vue-blog-template/src/views/about/About.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 45 | 46 | 65 | -------------------------------------------------------------------------------- /vue-blog-template/src/views/album/Album.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 128 | 129 | 206 | -------------------------------------------------------------------------------- /vue-blog-template/src/views/layout/components/Header.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 23 | 24 | 48 | -------------------------------------------------------------------------------- /vue-blog-template/src/views/layout/components/Main.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /vue-blog-template/src/views/layout/components/index.js: -------------------------------------------------------------------------------- 1 | export { default as Header } from "./Header"; 2 | export { default as Main } from "./Main"; 3 | -------------------------------------------------------------------------------- /vue-blog-template/src/views/layout/index.vue: -------------------------------------------------------------------------------- 1 | 61 | 62 | 72 | 73 | 130 | -------------------------------------------------------------------------------- /vue-blog-template/src/views/updateLog/UpdateLog.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 57 | 58 | 75 | -------------------------------------------------------------------------------- /vue-blog-template/vue.config.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | module.exports = { 3 | publicPath: "./", 4 | outputDir: "dist", 5 | assetsDir: "static", 6 | productionSourceMap: false, 7 | pluginOptions: { 8 | returnCitySN: "returnCitySN" 9 | }, 10 | devServer: { 11 | open: true, 12 | host: "0.0.0.0", 13 | port: 8080, 14 | proxy: { 15 | "/api": { 16 | target: "http://127.0.0.1:8090/api", //node端口自行更改 17 | changeOrigin: true, 18 | ws: true, 19 | pathRewrite: { 20 | "^/api": "" 21 | } 22 | } 23 | } 24 | }, 25 | configureWebpack: { 26 | performance: { 27 | hints: "warning", 28 | //入口起点的最大体积 整数类型(以字节为单位) 29 | maxEntrypointSize: 50000000, 30 | //生成文件的最大体积 整数类型(以字节为单位 300k) 31 | maxAssetSize: 30000000, 32 | //只给出 js 文件的性能提示 33 | assetFilter: function(assetFilename) { 34 | return assetFilename.endsWith(".js"); 35 | } 36 | } 37 | } 38 | }; 39 | --------------------------------------------------------------------------------