upload for gitea
3
.browserslistrc
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
> 1%
|
||||||
|
last 2 versions
|
||||||
|
not dead
|
||||||
27
.cz-config.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
module.exports = {
|
||||||
|
types: [
|
||||||
|
{ value: 'feat', name: 'feat: 增加新功能' },
|
||||||
|
{ value: 'fix', name: 'fix: 修复bug' },
|
||||||
|
{ value: 'docs', name: 'docs: 修改文档' },
|
||||||
|
{ value: 'style', name: 'style: 样式修改不影响逻辑' },
|
||||||
|
{ value: 'refactor', name: 'refactor: 代码重构' },
|
||||||
|
{ value: 'perf', name: 'perf: 性能优化' },
|
||||||
|
{ value: 'test', name: 'test: 增删测试' },
|
||||||
|
{ value: 'build', name: 'build: 修改构建或外部依赖' },
|
||||||
|
{ value: 'ci', name: 'ci: 修改ci配置或脚本' },
|
||||||
|
{ value: 'chore', name: 'chore: 除src目录或测试文件以外的修改' },
|
||||||
|
{ value: 'revert', name: 'revert: 版本回退' },
|
||||||
|
],
|
||||||
|
scopes: [],
|
||||||
|
messages: {
|
||||||
|
type: '选择更改类型:\n',
|
||||||
|
customScope: '更改的范围:\n', // ( 'Denote the SCOPE of this change:') allowcustomscopes为true,key为:customScope, 否则为scope
|
||||||
|
subject: '简短描述:\n',
|
||||||
|
body: '详细描述. 使用"|"换行:\n',
|
||||||
|
breaking: 'Breaking Changes列表:\n',
|
||||||
|
footer: '关闭的issues列表. E.g.: #31, #34:\n',
|
||||||
|
confirmCommit: '确认提交?',
|
||||||
|
},
|
||||||
|
allowCustomScopes: true,
|
||||||
|
allowBreakingChanges: ['feat', 'fix'],
|
||||||
|
}
|
||||||
2
.env.development
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
NODE_ENV=development
|
||||||
|
VUE_APP_API_BASE_URL=/proxy
|
||||||
2
.env.production
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
NODE_ENV=production
|
||||||
|
VUE_APP_API_BASE_URL=''
|
||||||
4
.eslintignore
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
node_modules
|
||||||
|
dist/
|
||||||
|
public/
|
||||||
|
build/
|
||||||
95
.eslintrc.js
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
module.exports = {
|
||||||
|
root: true, // 根配置文件
|
||||||
|
parserOptions: {
|
||||||
|
// 语法解析器选项
|
||||||
|
parser: 'babel-eslint', // es6, 兼容箭头函数等
|
||||||
|
sourceType: 'module',
|
||||||
|
},
|
||||||
|
env: {
|
||||||
|
// 执行环境
|
||||||
|
browser: true,
|
||||||
|
node: true,
|
||||||
|
es6: true,
|
||||||
|
},
|
||||||
|
extends: ['plugin:vue/essential', 'eslint:recommended', 'prettier'], // 基础配置进行扩展
|
||||||
|
plugins: ['prettier'],
|
||||||
|
rules: {
|
||||||
|
// 校验规则
|
||||||
|
'vue/component-definition-name-casing': [2, 'PascalCase'], // vue组件name, PascalCase为大驼峰
|
||||||
|
'vue/require-prop-types': 2, // prop必须声明类型
|
||||||
|
'vue/v-bind-style': [1, 'shorthand'], // 动态绑定用缩写:
|
||||||
|
'vue/v-on-style': [1, 'shorthand'], // 事件绑定用缩写@
|
||||||
|
'vue/component-tags-order': [
|
||||||
|
2,
|
||||||
|
{
|
||||||
|
order: ['template', 'script', 'style'],
|
||||||
|
},
|
||||||
|
], // 模板顺序
|
||||||
|
|
||||||
|
// todo 根据env环境设置是否允许console和debugger
|
||||||
|
'no-console': process.env.NODE_ENV === 'production' ? 1 : 0, // 禁用 console
|
||||||
|
'no-debugger': process.env.NODE_ENV === 'production' ? 1 : 0, // 禁用 debugger
|
||||||
|
'require-await': 2, // 禁止使用不带 await 表达式的 async 函数
|
||||||
|
'constructor-super': 2, // 要求在构造函数中有 super() 的调用
|
||||||
|
'handle-callback-err': [2, '^(err|error)$'], // 要求回调函数中有容错处理
|
||||||
|
'new-cap': [
|
||||||
|
2,
|
||||||
|
{
|
||||||
|
newIsCap: true,
|
||||||
|
capIsNew: false,
|
||||||
|
},
|
||||||
|
], // 要求构造函数首字母大写
|
||||||
|
'no-caller': 2, // 禁用 arguments.caller 或 arguments.callee
|
||||||
|
'no-eval': 2, // 禁用 eval()
|
||||||
|
'no-extend-native': 2, // 禁止扩展原生类型
|
||||||
|
'no-extra-bind': 2, // 禁止不必要的 .bind() 调用
|
||||||
|
'no-extra-parens': [2, 'functions'], // 禁止不必要的括号
|
||||||
|
'no-floating-decimal': 2, // 禁止数字字面量中使用前导和末尾小数点
|
||||||
|
'no-implied-eval': 2, // 禁止使用类似 eval() 的方法
|
||||||
|
'no-iterator': 2, // 禁用 __iterator__ 属性
|
||||||
|
'no-label-var': 2, // 不允许标签与变量同名
|
||||||
|
'no-labels': [
|
||||||
|
2,
|
||||||
|
{
|
||||||
|
allowLoop: false,
|
||||||
|
allowSwitch: false,
|
||||||
|
},
|
||||||
|
], // 禁用标签语句
|
||||||
|
'no-lone-blocks': 2, // 禁用不必要的嵌套块
|
||||||
|
'no-multi-str': 2, // 禁止使用多行字符串
|
||||||
|
'no-array-constructor': 2, // 禁用 Array 构造函数
|
||||||
|
'no-new-object': 2, // 禁止使用new Object()
|
||||||
|
'no-new-require': 2, // 禁止调用 require 时使用 new 操作符
|
||||||
|
'no-new-wrappers': 2, // 禁止对 String,Number 和 Boolean 使用 new 操作符
|
||||||
|
'no-octal-escape': 2, // 禁止在字符串中使用八进制转义序列
|
||||||
|
'no-path-concat': 2, // 禁止对 __dirname 和 __filename 进行字符串连接
|
||||||
|
'no-proto': 2, // 禁用__proto__, 在 ECMAScript 3.1 中已经被弃用, 使用 Object.getPrototypeOf 和 Object.setPrototypeOf 代替
|
||||||
|
'no-return-assign': [2, 'except-parens'], // 禁止在 return 语句中使用赋值语句
|
||||||
|
'no-self-compare': 2, // 禁止自身比较
|
||||||
|
'no-sequences': 2, // 禁用逗号操作符
|
||||||
|
'no-throw-literal': 2, // 禁止抛出异常字面量
|
||||||
|
'no-unmodified-loop-condition': 2, // 禁用一成不变的循环条件
|
||||||
|
'no-unneeded-ternary': [
|
||||||
|
2,
|
||||||
|
{
|
||||||
|
defaultAssignment: false,
|
||||||
|
},
|
||||||
|
], // 禁止可以在有更简单的可替代的表达式时使用三元操作符
|
||||||
|
'no-useless-call': 2, // 禁止不必要的 .call() 和 .apply()
|
||||||
|
'no-useless-computed-key': 2, // 禁止在对象中使用不必要的计算属性
|
||||||
|
'no-useless-constructor': 2, // 禁用不必要的构造函数
|
||||||
|
'no-useless-escape': 0, // 禁用不必要的转义字符
|
||||||
|
'no-whitespace-before-property': 2, // 禁止属性前有空白
|
||||||
|
'operator-linebreak': [
|
||||||
|
2,
|
||||||
|
'after',
|
||||||
|
{
|
||||||
|
overrides: {
|
||||||
|
'?': 'before',
|
||||||
|
':': 'before',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
], // 强制操作符使用一致的换行符风格,
|
||||||
|
'spaced-comment': [2, 'always'], // 强制在注释中 // 或 /* 使用一致的空格
|
||||||
|
},
|
||||||
|
}
|
||||||
23
.gitignore
vendored
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
/dist
|
||||||
|
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Log files
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.idea
|
||||||
|
!.vscode
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
8
.prettierignore
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
node_modules/
|
||||||
|
package.json
|
||||||
|
package-lock.json
|
||||||
|
*.md
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
*.vue
|
||||||
|
color.less
|
||||||
27
.prettierrc.js
Executable file
@ -0,0 +1,27 @@
|
|||||||
|
module.exports = {
|
||||||
|
printWidth: 100, // 设置prettier单行输出(不折行)的(最大)长度
|
||||||
|
proseWrap: 'preserve', // 使用默认的折行标准
|
||||||
|
tabWidth: 2, // 设置工具每一个水平缩进的空格数
|
||||||
|
endOfLine: 'auto', // 文件换行格式 LF/CRLF
|
||||||
|
useTabs: false, // 使用tab(制表位)缩进而非空格
|
||||||
|
semi: false, // 在语句末尾添加分号
|
||||||
|
singleQuote: true, // 使用单引号代替双引号
|
||||||
|
quoteProps: 'as-needed', // 对象的 key 仅在必要时用引号
|
||||||
|
trailingComma: 'es5', // 在对象或数组最后一个元素后面是否加逗号
|
||||||
|
bracketSpacing: true, // 在对象字面量声明所使用的的花括号后({)和前(})输出空格
|
||||||
|
arrowParens: 'avoid', // 为单行箭头函数的参数添加圆括号,参数个数为1时可以省略圆括号
|
||||||
|
// parser: 'babylon', // Prettier 会自动从输入文件路径推断解析器
|
||||||
|
jsxBracketSameLine: true, // 在多行JSX元素最后一行的末尾添加 > 而使 > 单独一行(不适用于自闭和元素)
|
||||||
|
filepath: 'none', // 指定文件的输入路径,这将被用于解析器参照
|
||||||
|
requirePragma: false, // (v1.7.0+) Prettier可以严格按照按照文件顶部的一些特殊的注释格式化代码,这些注释称为“require pragma”(必须杂注)
|
||||||
|
insertPragma: false, // (v1.8.0+) Prettier可以在文件的顶部插入一个 @format的特殊注释,以表明改文件已经被Prettier格式化过了。
|
||||||
|
// 配置覆盖
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: ['*.json', '*.css', '*.less', '*.sass'],
|
||||||
|
options: {
|
||||||
|
singleQuote: false, // 使用单引号代替双引号
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
8
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"octref.vetur",
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
"esbenp.prettier-vscode",
|
||||||
|
"ant-design-vue.vscode-ant-design-vue-helper"
|
||||||
|
]
|
||||||
|
}
|
||||||
69
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
{
|
||||||
|
/* html 标签自动补全 */
|
||||||
|
"emmet.triggerExpansionOnTab": true, // 启用后,按下TAB键,将展开Emmet缩写
|
||||||
|
"emmet.syntaxProfiles": {
|
||||||
|
"vue-html": "html",
|
||||||
|
"vue": "html"
|
||||||
|
},
|
||||||
|
"emmet.includeLanguages": {
|
||||||
|
"vue-html": "html",
|
||||||
|
"vue": "html"
|
||||||
|
},
|
||||||
|
/* */
|
||||||
|
"editor.formatOnSave": true, // 代码在保存的时候自动格式化
|
||||||
|
"editor.formatOnSaveMode": "file", // 设置保存文件时格式化整个文件还是仅被修改处。该配置项仅在 "editor.formatOnSave" 为 true时生效
|
||||||
|
"editor.formatOnType": true, // 设置输入完成后是否自动格式化当前行
|
||||||
|
/* */
|
||||||
|
"search.followSymlinks": false, // 防止VSCode启动带有node_modules的项目的时候很卡的问题
|
||||||
|
/* 不显示文件 */
|
||||||
|
"files.exclude": {
|
||||||
|
"**/node_modules/": true,
|
||||||
|
"dist": true
|
||||||
|
},
|
||||||
|
/* vetur */
|
||||||
|
// "vetur.validation.template": false, // 不使用esLint-Plugin-vue校验template
|
||||||
|
"vetur.format.defaultFormatter.html": "prettier",
|
||||||
|
"vetur.format.defaultFormatter.js": "prettier",
|
||||||
|
"vetur.format.defaultFormatter.css": "prettier",
|
||||||
|
"vetur.format.defaultFormatter.less": "prettier",
|
||||||
|
"vetur.format.defaultFormatter.scss": "prettier",
|
||||||
|
"vetur.format.defaultFormatter.stylus": "stylus-supremacy",
|
||||||
|
"vetur.format.defaultFormatter.sass": "sass-formatter",
|
||||||
|
"vetur.format.defaultFormatter.postcss": "prettier",
|
||||||
|
"vetur.format.defaultFormatter.ts": "prettier",
|
||||||
|
/* eslint配置 */
|
||||||
|
"eslint.alwaysShowStatus": true, // 设置状态栏是否一直显示ESlint图标项,true表示一直显示
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll.eslint": true
|
||||||
|
},
|
||||||
|
"eslint.codeAction.showDocumentation": {
|
||||||
|
"enable": true
|
||||||
|
},
|
||||||
|
"eslint.run": "onType",
|
||||||
|
"eslint.options": {
|
||||||
|
"extensions": [".js", ".vue", ".jsx", ".tsx"]
|
||||||
|
},
|
||||||
|
/* 设置各种代码的默认格式化器//以下为默认配置 */
|
||||||
|
"[vue]": {
|
||||||
|
// 指定 *.vue 文件的格式化工具为vetur
|
||||||
|
"editor.defaultFormatter": "octref.vetur"
|
||||||
|
},
|
||||||
|
"[html]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[css]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[less]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[javascript]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[json]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[jsonc]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
}
|
||||||
|
}
|
||||||
732
README.md
Normal file
@ -0,0 +1,732 @@
|
|||||||
|
[TOC]
|
||||||
|
|
||||||
|
# 前端通用框架
|
||||||
|
|
||||||
|
## 框架介绍
|
||||||
|
|
||||||
|
基于`vue@2.6`, `ant-design-vue@1.7`, `axios`, `vue-router`, `vuex`, `webpack@4.x` 的通用后台管理系统
|
||||||
|
|
||||||
|
功能:
|
||||||
|
|
||||||
|
1. 根据路由动态生成侧边导航
|
||||||
|
2. 使用`mock.js`和`mock-serve`进行本地mock
|
||||||
|
3. 集成`eslint`和`prettier`配置
|
||||||
|
4. 集成`commit插件`
|
||||||
|
5. 集成`git`钩子, 必须通过`eslit`代码检查和`commit`提交规范检查才能提交代码
|
||||||
|
6. `ant-design-vue` 组件按需引入, ant-design icon 按需引入
|
||||||
|
7. 响应错误统一处理
|
||||||
|
8. `vue-ls`本地持久化
|
||||||
|
9. 支持设置主题
|
||||||
|
10. 权限自定义指令
|
||||||
|
|
||||||
|
## not Support
|
||||||
|
|
||||||
|
1. ie11 兼容
|
||||||
|
2. h5 兼容
|
||||||
|
3. 响应式
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
推荐使用vscode开发, 需要安装`prettier`, `eslint`, `vetur`插件
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 需要全局安装commitizen才能使用 git cz
|
||||||
|
npm install commitizen -g
|
||||||
|
# 克隆项目
|
||||||
|
git clone xxx
|
||||||
|
git cd xxx
|
||||||
|
# 安装插件
|
||||||
|
yarn
|
||||||
|
# 启动服务
|
||||||
|
yarn run dev
|
||||||
|
# 打包
|
||||||
|
yarn run build
|
||||||
|
```
|
||||||
|
|
||||||
|
## 目录结构
|
||||||
|
|
||||||
|
```json
|
||||||
|
├─.browserslistrc ------------------------ // 目标浏览器配置
|
||||||
|
├─.env.development ----------------------- // 开发环境变量配置
|
||||||
|
├─.env.production ------------------------ // 开发环境变量配置
|
||||||
|
├─.eslintignore -------------------------- // eslint忽略文件
|
||||||
|
├─.eslintrc.js --------------------------- // eslisnt配置
|
||||||
|
├─.gitignore ----------------------------- // git忽略配置
|
||||||
|
├─.prettierignore ------------------------ // prettier忽略配置
|
||||||
|
├─.prettierrc.js ------------------------- // 代码风格配置
|
||||||
|
├─.vscode -------------------------------- // vscode项目设置
|
||||||
|
│ └─settings.json ------------------------ // vscode配置
|
||||||
|
├─README.md
|
||||||
|
├─babel.config.js ------------------------ // js编译器设置
|
||||||
|
├─commitlint.config.js ------------------- // git commit lint
|
||||||
|
├─jsconfig.json -------------------------- // JavaScript项目的根目录配置
|
||||||
|
├─package.json --------------------------- // npm包管理
|
||||||
|
├─public --------------------------------- // 静态资源文件夹
|
||||||
|
│ ├─color.less --------------------------- // 主题颜色配置
|
||||||
|
│ ├─favicon.ico -------------------------- // 网站图标
|
||||||
|
│ └─index.html --------------------------- // 主页
|
||||||
|
├─src ------------------------------------ // 代码目录
|
||||||
|
│ ├─App.vue ------------------------------ // 项目根组件
|
||||||
|
│ ├─api ---------------------------------- // api目录
|
||||||
|
│ │ └─user.js ---------------------------- // 用户相关接口
|
||||||
|
│ ├─assets ------------------------------- // 资源文件夹
|
||||||
|
│ │ ├─avatar.jpg ------------------------- // 默认头像
|
||||||
|
│ │ ├─background.svg --------------------- // 登录页背景
|
||||||
|
│ │ ├─checkcode.png ---------------------- // 验证码默认图
|
||||||
|
│ │ ├─icons ------------------------------ // 图标相关
|
||||||
|
│ │ │ ├─index.js
|
||||||
|
│ │ │ ├─svg ------------------------------ // svg图标存放路径
|
||||||
|
│ │ │ │ ├─develop.svg
|
||||||
|
│ │ │ │ └─svgtest.svg
|
||||||
|
│ │ │ └─svgo.yml ------------------------- // svg图标格式化配置
|
||||||
|
│ │ ├─less ------------------------------- // 样式
|
||||||
|
│ │ │ └─common.less ---------------------- // 公共样式
|
||||||
|
│ │ └─logo.png
|
||||||
|
│ ├─components --------------------------- // 公共组件
|
||||||
|
│ │ ├─Header.vue ------------------------- // 头部
|
||||||
|
│ │ ├─SettingDrawer ---------------------- // 系统设置(主题)
|
||||||
|
│ │ │ ├─index.vue
|
||||||
|
│ │ │ └─settingConfig.js ----------------- // 主题颜色生成
|
||||||
|
│ │ ├─SvgIcon.vue ------------------------ // svg图标组件
|
||||||
|
│ │ ├─TopTabs.vue ------------------------ // tab栏导航
|
||||||
|
│ │ ├─UserPassword.vue
|
||||||
|
│ │ ├─layouts ---------------------------- // 布局相关
|
||||||
|
│ │ │ ├─AggregateLayout(可以删除).vue
|
||||||
|
│ │ │ ├─BlankLayout.vue ------------------ // 空白布局
|
||||||
|
│ │ │ ├─GlobalLayout.vue ----------------- // 全局部局
|
||||||
|
│ │ │ ├─SideTabsLayout.vue --------------- // 侧边切换tab布局
|
||||||
|
│ │ │ └─UserLayout.vue ------------------- // 登录/注册/找回密码共用布局
|
||||||
|
│ │ └─menu
|
||||||
|
│ │ ├─Contextmenu.vue ------------------ // 上下文菜单(在顶部导航鼠标右键菜单用到)
|
||||||
|
│ │ └─SideMenu.js ---------------------- // 左侧导航栏生成
|
||||||
|
│ ├─config
|
||||||
|
│ │ └─defaultSettings.js ----------------- // 全局主题颜色/vue-ls配置
|
||||||
|
│ ├─main.js ------------------------------ // 入口
|
||||||
|
│ ├─mock --------------------------------- // mock
|
||||||
|
│ │ ├─constant --------------------------- // 存放较大的数据
|
||||||
|
│ │ │ └─userPermission.js ---------------- // 权限模拟数据
|
||||||
|
│ │ ├─index.js
|
||||||
|
│ │ ├─mock-server.js --------------------- // mock-server本地服务
|
||||||
|
│ │ ├─table.js --------------------------- // mock语法示例
|
||||||
|
│ │ ├─user.js ---------------------------- // 模拟接口
|
||||||
|
│ │ └─utils.js --------------------------- // mock服务工具方法
|
||||||
|
│ ├─router ------------------------------- // 路由
|
||||||
|
│ │ ├─generateIndexRouter.js ------------- // 动态路由生成
|
||||||
|
│ │ └─index.js
|
||||||
|
│ ├─store -------------------------------- // vuex
|
||||||
|
│ │ ├─index.js
|
||||||
|
│ │ └─modules ---------------------------- // vuex模块
|
||||||
|
│ │ ├─app.js --------------------------- // 全局
|
||||||
|
│ │ └─user.js -------------------------- // 用户相关
|
||||||
|
│ ├─utils -------------------------------- // 工具类
|
||||||
|
│ │ ├─lazy ------------------------------- // 按需加载
|
||||||
|
│ │ │ ├─icons.js ------------------------- // 图标
|
||||||
|
│ │ │ └─lazyAntd.js ---------------------- // ant-design-vue组件
|
||||||
|
│ │ └─request.js ------------------------- // axios封装
|
||||||
|
│ └─views -------------------------------- // 页面
|
||||||
|
│ ├─About.vue -------------------------- // 关于
|
||||||
|
│ ├─Home.vue --------------------------- // 首页
|
||||||
|
│ ├─account ---------------------------- // 账号相关
|
||||||
|
│ │ ├─Center.vue ----------------------- // 个人中心页
|
||||||
|
│ │ └─settings ------------------------- // 个人设置页
|
||||||
|
│ │ ├─BaseSetting.vue
|
||||||
|
│ │ ├─Binding.vue
|
||||||
|
│ │ ├─Custom.vue
|
||||||
|
│ │ ├─Notification.vue
|
||||||
|
│ │ └─Security.vue
|
||||||
|
│ ├─exception -------------------------- // 404/403/500等错误页
|
||||||
|
│ │ └─404.vue
|
||||||
|
│ ├─list ------------------------------- // 多级菜单 示例
|
||||||
|
│ │ ├─StandardList.vue
|
||||||
|
│ │ └─TableList.vue
|
||||||
|
│ ├─modules ---------------------------- // 存放页面公共组件
|
||||||
|
│ │ └─online
|
||||||
|
│ │ └─desform ------------------------ // 列表页跳详情页 示例
|
||||||
|
│ │ ├─DesignFormList.vue
|
||||||
|
│ │ └─DesignFormTempletList.vue
|
||||||
|
│ ├─oneLevelMenu ----------------------- // 一级菜单示例页面
|
||||||
|
│ │ └─index.vue
|
||||||
|
│ ├─profile ---------------------------- // 路由聚合示例
|
||||||
|
│ │ ├─Advanced.vue
|
||||||
|
│ │ └─Basic.vue
|
||||||
|
│ ├─sideTabs --------------------------- // 侧边栏tab标签 示例
|
||||||
|
│ │ ├─components
|
||||||
|
│ │ │ ├─Tab1.vue
|
||||||
|
│ │ │ ├─Tab2.vue
|
||||||
|
│ │ │ ├─Tab3.vue
|
||||||
|
│ │ │ └─Tab4.vue
|
||||||
|
│ │ └─index.vue
|
||||||
|
│ ├─title
|
||||||
|
│ │ └─index.vue
|
||||||
|
│ └─user ------------------------------- // 登录/注册/找回密码 相关
|
||||||
|
│ ├─Login.vue ------------------------ // 登录页
|
||||||
|
│ └─LoginSimple.vue
|
||||||
|
├─test ----------------------------------- // 测试用例
|
||||||
|
│ └─test.js
|
||||||
|
├─vue.config.js -------------------------- // vue-cli配置
|
||||||
|
└─yarn.lock ------------------------------ // npm包版本控制
|
||||||
|
```
|
||||||
|
|
||||||
|
### vue组件的存放路径:
|
||||||
|
|
||||||
|
1. 框架相关的组件, 存放在`@/components`
|
||||||
|
2. 布局相关的, 存在在`@/components/layouts`
|
||||||
|
3. 不相关的两个页面的公共组件, 存放在`@/viewmodules`
|
||||||
|
4. 父子关系/兄第关系的页面的公共组件, 且没有其他页面使用, 存放在他们的公共根目录下`@/views/xxx/components`
|
||||||
|
|
||||||
|
## 动态路由
|
||||||
|
|
||||||
|
### 路由菜单不要超过3层
|
||||||
|
|
||||||
|
<img src="https://i.loli.net/2021/09/18/opbsBDxKWn2vMId.png" alt="image.png" style="zoom:50%;" />
|
||||||
|
|
||||||
|
### 路由生成
|
||||||
|
|
||||||
|
<img src="https://i.loli.net/2021/09/18/1XJzZxdUqrluGDN.png" alt="image.png" style="zoom:50%;" />
|
||||||
|
|
||||||
|
### 路由配置项:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
// "hidden": true, // (默认不传)不在左侧菜单中显示
|
||||||
|
// "redirect":null, // (默认不传)重定向
|
||||||
|
// "alwaysShow": false, // (默认不传)路由聚合, 从menu菜单展示变为tab栏展示
|
||||||
|
"path":"/dashboard/analysis", // 路径
|
||||||
|
"component":"dashboard/Analysis", // 组件
|
||||||
|
"name":"dashboard-analysis", // 路由名称, 使用<keep-alive>时必填
|
||||||
|
"meta":{
|
||||||
|
"keepAlive":true, // 是否缓存
|
||||||
|
"icon":"home", // 图标
|
||||||
|
"title":"首页", // 侧边栏和面包屑中展示的名字
|
||||||
|
// "url": "http://www.baidu.com" // (默认不传)当外部链接跳转时必须传, 必须带http:// 或者httPs://
|
||||||
|
},
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
举例:
|
||||||
|
|
||||||
|
"系统管理"为一级菜单
|
||||||
|
|
||||||
|
"用户管理"为子菜单
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"path":"/isystem",
|
||||||
|
"component":"layouts/RouteView",
|
||||||
|
"name":"isystem",
|
||||||
|
"meta":{
|
||||||
|
"keepAlive":false,
|
||||||
|
"icon":"setting",
|
||||||
|
"title":"系统管理"
|
||||||
|
},
|
||||||
|
"children":[
|
||||||
|
{
|
||||||
|
"path":"/isystem/user",
|
||||||
|
"component":"system/UserList",
|
||||||
|
"name":"isystem-user",
|
||||||
|
"meta":{
|
||||||
|
"keepAlive":false,
|
||||||
|
"title":"用户管理"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path":"/isystem/roleUserList",
|
||||||
|
"component":"system/RoleUserList",
|
||||||
|
"name":"isystem-roleUserList",
|
||||||
|
"meta":{
|
||||||
|
"keepAlive":true,
|
||||||
|
"title":"角色管理"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path":"/isystem/newPermissionList",
|
||||||
|
"component":"system/NewPermissionList",
|
||||||
|
"name":"isystem-newPermissionList",
|
||||||
|
"meta":{
|
||||||
|
"keepAlive":true,
|
||||||
|
"title":"菜单管理"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 路由聚合
|
||||||
|
|
||||||
|
<img src="https://i.loli.net/2021/09/18/zZns3Lr89ClJ2iD.png" alt="image.png" style="zoom:50%;" />
|
||||||
|
|
||||||
|
1. children中的路由不会在左侧菜单栏中显示
|
||||||
|
2. children中的路由只能有一层
|
||||||
|
3. children中的路由所对应的组件, 聚合到tab栏组件中, 通过tab栏切换
|
||||||
|
4. redirect无效, tab栏顺序为children顺序, 默认展示第一个
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
path: '/account/settings',
|
||||||
|
redirect: '/account/settings/notification',
|
||||||
|
component: 'layouts/BlankLayout',
|
||||||
|
alwaysShow: true,
|
||||||
|
meta: {
|
||||||
|
title: '个人设置',
|
||||||
|
},
|
||||||
|
name: 'account-settings-Index',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '/account/settings/notification',
|
||||||
|
component: 'account/settings/Notification',
|
||||||
|
meta: {
|
||||||
|
title: '新消息通知',
|
||||||
|
},
|
||||||
|
name: 'account-settings-notification',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/account/settings/base-setting',
|
||||||
|
component: 'account/settings/BaseSetting',
|
||||||
|
meta: {
|
||||||
|
title: '基本设置',
|
||||||
|
},
|
||||||
|
name: 'account-settings-base',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/account/settings/binding',
|
||||||
|
component: 'account/settings/Binding',
|
||||||
|
meta: {
|
||||||
|
title: '账户绑定',
|
||||||
|
},
|
||||||
|
name: 'account-settings-binding',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/account/settings/security',
|
||||||
|
component: 'account/settings/Security',
|
||||||
|
meta: {
|
||||||
|
title: '安全设置',
|
||||||
|
},
|
||||||
|
name: 'account-settings-security',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/account/settings/custom',
|
||||||
|
component: 'account/settings/Custom',
|
||||||
|
meta: {
|
||||||
|
title: '个性化设置',
|
||||||
|
},
|
||||||
|
name: 'account-settings-custom',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 列表页和详情页
|
||||||
|
|
||||||
|
1. 简单的详情页, 直接通过`抽屉`/ `模态框`渲染
|
||||||
|
2. 复杂的详情页, 通过`/id`跳转到新页面
|
||||||
|
|
||||||
|
<img src="https://i.loli.net/2021/09/18/FL21JyDI5mWwh3s.png" alt="image.png" style="zoom:50%;" />
|
||||||
|
|
||||||
|
<img src="https://i.loli.net/2021/09/18/HgJaSM5ik9vwFCR.png" alt="image.png" style="zoom:50%;" />
|
||||||
|
|
||||||
|
```json
|
||||||
|
// 路由结构 举例
|
||||||
|
{
|
||||||
|
"path": "/test",
|
||||||
|
// 加载默认子路由, 不能有name
|
||||||
|
"component":"layouts/BlankLayout",
|
||||||
|
"meta": {
|
||||||
|
"title":"测试页面",
|
||||||
|
redirectDefaultChild: true, // 子路由不在菜单栏中显示, 进入子路由显示父路由高亮
|
||||||
|
},
|
||||||
|
"children":[
|
||||||
|
{
|
||||||
|
"path": "", // 默认子路由
|
||||||
|
"name": "test-list",
|
||||||
|
"component":"test/List",
|
||||||
|
"meta": {
|
||||||
|
"title":"测试页面列表页",
|
||||||
|
// "keepAlive": true // 可以设置keepAlive使详情页回到列表页, 不重新渲染组件
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "/test/:id", // 详情页
|
||||||
|
"name": "test-detail",
|
||||||
|
"component":"test/Detail",
|
||||||
|
"meta": {
|
||||||
|
"title":"测试页面详情页"
|
||||||
|
},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 外链url跳转
|
||||||
|
|
||||||
|
<img src="https://i.loli.net/2021/09/18/efP12VRlLObi36v.png" alt="image.png" style="zoom:50%;" />
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
path: '/urllink',
|
||||||
|
component: 'layouts/BlankLayout',
|
||||||
|
name: 'baidu',
|
||||||
|
meta: {
|
||||||
|
title: 'url跳转',
|
||||||
|
url: 'https://www.baidu.com', // url必须完整, 带http://或者https://
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 动态权限
|
||||||
|
|
||||||
|
待补充完善
|
||||||
|
|
||||||
|
## mock
|
||||||
|
|
||||||
|
### (本地)开发模式
|
||||||
|
|
||||||
|
1. 启动`yarn run dev`时, 会同时启动`mock-serve`, 与mock相关的接口会转发到`mock-serve`
|
||||||
|
|
||||||
|
2. mock文件示例
|
||||||
|
|
||||||
|
```js
|
||||||
|
// /mock/user.js
|
||||||
|
const tokens = {
|
||||||
|
admin: {
|
||||||
|
token: 'admin-token',
|
||||||
|
},
|
||||||
|
editor: {
|
||||||
|
token: 'editor-token',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = [
|
||||||
|
// 登录接口
|
||||||
|
{
|
||||||
|
url: '/user/login',
|
||||||
|
type: 'post',
|
||||||
|
response: config => {
|
||||||
|
const { username } = config.body
|
||||||
|
const token = tokens[username]
|
||||||
|
|
||||||
|
// mock error
|
||||||
|
if (!token) {
|
||||||
|
return {
|
||||||
|
errcode: 1,
|
||||||
|
errmsg: 'Account and password are incorrect.',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
errcode: 0,
|
||||||
|
data: token,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// 其他接口...
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
```js
|
||||||
|
// /mock/index.js
|
||||||
|
// ...
|
||||||
|
const user = require('./user')
|
||||||
|
const mocks = [
|
||||||
|
...user,
|
||||||
|
]
|
||||||
|
// ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### (本地)生产模式
|
||||||
|
|
||||||
|
生产模式只能通过`mock.js`模拟数据, 与`mock-serve`共同使用同一份模拟数据
|
||||||
|
|
||||||
|
相关配置:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// /src/main.js
|
||||||
|
if (process.env.NODE_ENV === 'production') {
|
||||||
|
const { mockXHR } = require('./mock')
|
||||||
|
mockXHR()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
请求路径与mock.js中匹配的会被拦截, 但因为是重写了xhr方法, 无法在浏览器的network中看到请求, 可以通过控制台打印
|
||||||
|
|
||||||
|
==所以上线前需要在`/mock/index.js`把后端已完成的相关接口注释掉, 或者直接在`src/main.js`注释掉mock功能==
|
||||||
|
|
||||||
|
### 数据切换
|
||||||
|
|
||||||
|
1. 本地开发模式:
|
||||||
|
|
||||||
|
修改`vue.config.js`中的`mockUrls`
|
||||||
|
|
||||||
|
2. 本地生产模式
|
||||||
|
|
||||||
|
注释掉`/mock/index.js`中的对应接口
|
||||||
|
|
||||||
|
```js
|
||||||
|
const mocks = [
|
||||||
|
...user,
|
||||||
|
// ...table
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
全部取消:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// src/main.js
|
||||||
|
// 注释掉这部分
|
||||||
|
// if (process.env.NODE_ENV === 'production') {
|
||||||
|
// const { mockXHR } = require('../mock')
|
||||||
|
// mockXHR()
|
||||||
|
// }
|
||||||
|
```
|
||||||
|
|
||||||
|
## ui组件, icon按需引入
|
||||||
|
|
||||||
|
1. ui组件在`src/utils/lazy/lazyAntd.js`中按需引入
|
||||||
|
|
||||||
|
2. ant-design-icon在`src/utils/lazy/icons.js`中按需引入
|
||||||
|
|
||||||
|
3. 其他icon:
|
||||||
|
|
||||||
|
1. 使用`Iconfont(阿里巴巴图标库)`下载svg图标, 统一放在`@/assets/icon/svg中`
|
||||||
|
|
||||||
|
2. 执行`yarn run svgo`去掉多余信息格式化图标
|
||||||
|
|
||||||
|
3. 使用`<svg-icon icon-class="xxx" />`调用
|
||||||
|
|
||||||
|
```html
|
||||||
|
<svg-icon style="font-size: 18px; color: pink" icon-class="svgtest" />
|
||||||
|
```
|
||||||
|
|
||||||
|
## 公共js , css, 工具js
|
||||||
|
|
||||||
|
* 公共js, css: 框架里只保留最小使用
|
||||||
|
|
||||||
|
* 工具js: 通用性高的工具js和框架一起维护, 需要保持工具js的单一性, 完善注释
|
||||||
|
|
||||||
|
## 体积较大的第三方js插件
|
||||||
|
|
||||||
|
例如 "excel"的导出插件, 统一放在`/src/vender`中
|
||||||
|
|
||||||
|
## 泛用组件
|
||||||
|
|
||||||
|
例如"季度选择组件", 存放到gitlab公共组件仓库中
|
||||||
|
|
||||||
|
todo:
|
||||||
|
|
||||||
|
- [ ] 打包成npm包
|
||||||
|
- [ ] git submodules
|
||||||
|
|
||||||
|
## 开发规范
|
||||||
|
|
||||||
|
### 数据处理
|
||||||
|
|
||||||
|
* 后端接口获取的数据, 只取页面需要的, 不能直接存到data中
|
||||||
|
|
||||||
|
* 当某个接口的数据有多个页面使用时, 考虑存放到vuex/localstorage中
|
||||||
|
* 退出登录时, 需要考虑清空localStorage/sessionStorage中的敏感数据, 例如用户的登录信息, 需要更新的信息
|
||||||
|
|
||||||
|
### 列表页跳详情页
|
||||||
|
|
||||||
|
优先使用抽屉, 模态框, 无法满足需求时才考虑跳转新页面
|
||||||
|
|
||||||
|
### vue单文件的可维护性
|
||||||
|
|
||||||
|
建议不超过500行, 超过的需要考虑是否可以优化
|
||||||
|
|
||||||
|
> 1.抽离组件再引入
|
||||||
|
> 2.将css通过@import方式抽离再引入
|
||||||
|
> 3.将css/js/html全部抽离,只保留模块主入口文件再引入
|
||||||
|
|
||||||
|
### ui组件
|
||||||
|
|
||||||
|
优先使用ant-design-vue, 按需引用
|
||||||
|
|
||||||
|
### icon
|
||||||
|
|
||||||
|
优先使用ant-design icon , 按需引用
|
||||||
|
|
||||||
|
### ui风格
|
||||||
|
|
||||||
|
1. 滚动条/滚动范围:
|
||||||
|
|
||||||
|
以表格为例: 横向溢出时, 内容加滚动条
|
||||||
|
|
||||||
|
纵向溢出, 随页面滚动, 表头页脚不固定(优先考虑分页)
|
||||||
|
|
||||||
|
### 组件通讯
|
||||||
|
|
||||||
|
* 父 -> 子 : props
|
||||||
|
|
||||||
|
```js
|
||||||
|
//App.vue父组件
|
||||||
|
<template>
|
||||||
|
<div id="app">
|
||||||
|
<users :users="users"></users> <!-- 前者自定义名称便于子组件调用,后者要传递数据名 -->
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import Users from "./components/Users"
|
||||||
|
export default {
|
||||||
|
name: 'App',
|
||||||
|
data(){
|
||||||
|
return{
|
||||||
|
users:["Henry","Bucky","Emily"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components:{
|
||||||
|
"users":Users
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//users子组件
|
||||||
|
<template>
|
||||||
|
<div class="hello">
|
||||||
|
<ul>
|
||||||
|
<li v-for="user in users">{{user}}</li> <!-- 遍历传递过来的值,然后呈现到页面 -->
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'HelloWorld',
|
||||||
|
props:{
|
||||||
|
users:{ //这个就是父组件中子标签自定义名字
|
||||||
|
type:Array,
|
||||||
|
required:true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
* 子 -> 父 : $emit
|
||||||
|
|
||||||
|
```js
|
||||||
|
// 子组件
|
||||||
|
<template>
|
||||||
|
<header>
|
||||||
|
<h1 @click="changeTitle">{{title}}</h1> <!-- 绑定一个点击事件 -->
|
||||||
|
</header>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'app-header',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
title:"Vue.js Demo"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods:{
|
||||||
|
changeTitle() {
|
||||||
|
this.$emit("titleChanged","子向父组件传值");//自定义事件 传递值“子向父组件传值”
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
// 父组件
|
||||||
|
<template>
|
||||||
|
<div id="app">
|
||||||
|
<app-header v-on:titleChanged="updateTitle" ></app-header>
|
||||||
|
<!-- 与子组件titleChanged自定义事件保持一致 -->
|
||||||
|
<!-- updateTitle($event)接受传递过来的文字 -->
|
||||||
|
<h2>{{title}}</h2>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import Header from "./components/Header"
|
||||||
|
export default {
|
||||||
|
name: 'App',
|
||||||
|
data(){
|
||||||
|
return{
|
||||||
|
title:"传递的是一个值"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods:{
|
||||||
|
updateTitle(e){ //声明这个函数
|
||||||
|
this.title = e;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components:{
|
||||||
|
"app-header":Header,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
* 兄弟1 -> 兄弟2 :
|
||||||
|
* 如果兄弟组件有共同的父组件(子 - 父 - 子), 通过父组件传递
|
||||||
|
* 如果链路传递超过2层, 优先考虑vuex模块( 孙 - 子 - 父 - 子)
|
||||||
|
|
||||||
|
```js
|
||||||
|
// 参考 子 -> 父 父 -> 子
|
||||||
|
```
|
||||||
|
|
||||||
|
* 链路传递超过2层, 优先考虑vuex模块
|
||||||
|
|
||||||
|
* 避免使用事件总线(待补充)
|
||||||
|
|
||||||
|
* 使用插槽向引用组件传递模板
|
||||||
|
|
||||||
|
* 普通插槽
|
||||||
|
|
||||||
|
```js
|
||||||
|
// 父组件
|
||||||
|
<MyButton>注册</MyButton>
|
||||||
|
|
||||||
|
// 自定义组件
|
||||||
|
<button>
|
||||||
|
<slot>按钮</slot>
|
||||||
|
</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
* 作用域插槽
|
||||||
|
|
||||||
|
```js
|
||||||
|
<current-user>
|
||||||
|
<template v-slot:default="slotProps">
|
||||||
|
{{ slotProps.user.firstName }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-slot:other="otherSlotProps">
|
||||||
|
...
|
||||||
|
</template>
|
||||||
|
</current-user>
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 数据持久化
|
||||||
|
|
||||||
|
### localStorage
|
||||||
|
|
||||||
|
**在vuex里数据改变的时候把数据拷贝一份保存到localStorage里面,刷新之后,如果localStorage里有保存的数据,取出来再替换store里的state。**
|
||||||
|
|
||||||
|
```js
|
||||||
|
let defaultCity = "上海"
|
||||||
|
try { // 用户关闭了本地存储功能,此时在外层加个try...catch
|
||||||
|
if (!defaultCity){
|
||||||
|
defaultCity = JSON.parse(window.localStorage.getItem('defaultCity'))
|
||||||
|
}
|
||||||
|
}catch(e){}
|
||||||
|
export default new Vuex.Store({
|
||||||
|
state: {
|
||||||
|
city: defaultCity
|
||||||
|
},
|
||||||
|
mutations: {
|
||||||
|
changeCity(state, city) {
|
||||||
|
state.city = city
|
||||||
|
try {
|
||||||
|
window.localStorage.setItem('defaultCity', JSON.stringify(state.city));
|
||||||
|
// 数据改变的时候把数据拷贝一份保存到localStorage里面
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### sessionStorage
|
||||||
|
|
||||||
|
参考`localStorage`
|
||||||
|
|
||||||
8
babel.config.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// module.exports = {
|
||||||
|
// presets: ["@vue/cli-plugin-babel/preset"]
|
||||||
|
// };
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
presets: ['@vue/app'],
|
||||||
|
plugins: [['import', { libraryName: 'ant-design-vue', libraryDirectory: 'es', style: true }]],
|
||||||
|
}
|
||||||
1
commitlint.config.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
module.exports = { extends: ['@commitlint/config-conventional'] }
|
||||||
9
jsconfig.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": "./",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
291
mock/constant/userPermission.js
Normal file
@ -0,0 +1,291 @@
|
|||||||
|
const mockRoutesResource = [
|
||||||
|
{
|
||||||
|
path: '/home',
|
||||||
|
component: 'Home',
|
||||||
|
name: 'home',
|
||||||
|
meta: {
|
||||||
|
icon: 'home',
|
||||||
|
title: '首页',
|
||||||
|
keepAlive: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/sideTabs',
|
||||||
|
component: 'sideTabs/index',
|
||||||
|
name: 'sideTabs',
|
||||||
|
meta: {
|
||||||
|
icon: 'github',
|
||||||
|
title: '侧边栏tab标签',
|
||||||
|
keepAlive: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/oneLevelMenu',
|
||||||
|
name: 'oneLevelMenu',
|
||||||
|
component: 'oneLevelMenu/index',
|
||||||
|
meta: {
|
||||||
|
icon: 'table',
|
||||||
|
title: '一级菜单页面',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/list',
|
||||||
|
component: 'layouts/BlankLayout',
|
||||||
|
name: 'list',
|
||||||
|
meta: {
|
||||||
|
icon: 'table',
|
||||||
|
title: '多级菜单',
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '/list/basic-list',
|
||||||
|
component: 'list/StandardList',
|
||||||
|
meta: {
|
||||||
|
title: '二级菜单页',
|
||||||
|
},
|
||||||
|
name: 'list-basic-list',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/list/search',
|
||||||
|
component: 'layouts/BlankLayout',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '/list/search/application',
|
||||||
|
component: 'list/TableList',
|
||||||
|
meta: {
|
||||||
|
title: '三级菜单页',
|
||||||
|
},
|
||||||
|
name: 'list-search-application',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
meta: {
|
||||||
|
title: '二级菜单',
|
||||||
|
},
|
||||||
|
name: 'list-search',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hidden: true,
|
||||||
|
path: '/account',
|
||||||
|
component: 'layouts/BlankLayout',
|
||||||
|
name: 'account',
|
||||||
|
meta: {
|
||||||
|
icon: 'user',
|
||||||
|
title: '个人页',
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '/account/center',
|
||||||
|
component: 'account/Center',
|
||||||
|
name: 'account-center',
|
||||||
|
meta: {
|
||||||
|
title: '个人中心',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
alwaysShow: true,
|
||||||
|
path: '/account/settings',
|
||||||
|
redirect: '/account/settings/notification',
|
||||||
|
component: 'layouts/BlankLayout',
|
||||||
|
name: 'account-settings-Index',
|
||||||
|
meta: {
|
||||||
|
title: '个人设置',
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '/account/settings/notification',
|
||||||
|
component: 'account/settings/Notification',
|
||||||
|
name: 'account-settings-notification',
|
||||||
|
meta: {
|
||||||
|
title: '新消息通知',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/account/settings/base-setting',
|
||||||
|
component: 'account/settings/BaseSetting',
|
||||||
|
name: 'account-settings-base',
|
||||||
|
meta: {
|
||||||
|
title: '基本设置',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/account/settings/binding',
|
||||||
|
component: 'account/settings/Binding',
|
||||||
|
name: 'account-settings-binding',
|
||||||
|
meta: {
|
||||||
|
title: '账户绑定',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/account/settings/security',
|
||||||
|
component: 'account/settings/Security',
|
||||||
|
name: 'account-settings-security',
|
||||||
|
meta: {
|
||||||
|
title: '安全设置',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/account/settings/custom',
|
||||||
|
component: 'account/settings/Custom',
|
||||||
|
name: 'account-settings-custom',
|
||||||
|
meta: {
|
||||||
|
title: '个性化设置',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
alwaysShow: true,
|
||||||
|
path: '/profile',
|
||||||
|
component: 'layouts/BlankLayout',
|
||||||
|
name: 'profile',
|
||||||
|
meta: {
|
||||||
|
icon: 'profile',
|
||||||
|
title: '路由聚合',
|
||||||
|
// keepAlive: true,
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '/profile/basic',
|
||||||
|
component: 'profile/Basic',
|
||||||
|
name: 'profile-basic',
|
||||||
|
meta: {
|
||||||
|
title: '子路由1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/profile/advanced',
|
||||||
|
component: 'profile/Advanced',
|
||||||
|
name: 'profile-advanced',
|
||||||
|
meta: {
|
||||||
|
title: '子路由2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/desform',
|
||||||
|
component: 'layouts/BlankLayout',
|
||||||
|
meta: {
|
||||||
|
icon: 'gold',
|
||||||
|
title: '列表页跳详情页',
|
||||||
|
redirectDefaultChild: true,
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: 'modules/online/desform/DesignFormList',
|
||||||
|
name: 'online-desform',
|
||||||
|
meta: {
|
||||||
|
keepAlive: true,
|
||||||
|
title: '列表页',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/desform/:id',
|
||||||
|
component: 'modules/online/desform/DesignFormTempletList',
|
||||||
|
name: 'online-desformTemplate',
|
||||||
|
meta: {
|
||||||
|
title: '详情页',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/list2detail',
|
||||||
|
component: 'layouts/BlankLayout',
|
||||||
|
name: 'list2detail',
|
||||||
|
meta: {
|
||||||
|
title: '列表页跳详情页2',
|
||||||
|
icon: 'gold',
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '/list2detail/menu2',
|
||||||
|
component: 'layouts/BlankLayout',
|
||||||
|
meta: {
|
||||||
|
title: '二级菜单',
|
||||||
|
redirectDefaultChild: true,
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: 'modules/online/desform/DesignFormList',
|
||||||
|
name: 'list2detail-menu2-online-desform',
|
||||||
|
meta: {
|
||||||
|
keepAlive: true,
|
||||||
|
title: '列表页',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/list2detail/menu2/:id',
|
||||||
|
component: 'modules/online/desform/DesignFormTempletList',
|
||||||
|
name: 'list2detail-menu2-online-desformTemplate',
|
||||||
|
meta: {
|
||||||
|
title: '详情页',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/bing',
|
||||||
|
component: 'layouts/BlankLayout',
|
||||||
|
name: 'bing',
|
||||||
|
meta: {
|
||||||
|
title: '必应首页',
|
||||||
|
url: 'https://cn.bing.com/',
|
||||||
|
icon: 'table',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/dashboard',
|
||||||
|
component: 'layouts/BlankLayout',
|
||||||
|
name: 'dashboard3',
|
||||||
|
meta: {
|
||||||
|
icon: 'dashboard',
|
||||||
|
title: '系统监控',
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '/baidu',
|
||||||
|
component: 'layouts/BlankLayout',
|
||||||
|
name: 'baidu',
|
||||||
|
meta: {
|
||||||
|
title: '百度跳转',
|
||||||
|
url: 'http://www.baidu.com',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/isps/userAnnouncement',
|
||||||
|
component: 'title/index',
|
||||||
|
name: 'isps-userAnnouncement',
|
||||||
|
meta: {
|
||||||
|
title: '我的消息',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const buttonAuth = [
|
||||||
|
{
|
||||||
|
action: 'user:add',
|
||||||
|
describe: '添加用户',
|
||||||
|
type: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: 'user:edit',
|
||||||
|
describe: '编辑用户',
|
||||||
|
type: 1,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
mockRoutesResource,
|
||||||
|
buttonAuth,
|
||||||
|
}
|
||||||
56
mock/index.js
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
const Mock = require('mockjs')
|
||||||
|
const { param2Obj } = require('./utils')
|
||||||
|
|
||||||
|
const user = require('./user')
|
||||||
|
// const table = require('./table')
|
||||||
|
|
||||||
|
const mocks = [
|
||||||
|
...user,
|
||||||
|
// ...table
|
||||||
|
]
|
||||||
|
|
||||||
|
// for front mock
|
||||||
|
// please use it cautiously, it will redefine XMLHttpRequest,
|
||||||
|
// which will cause many of your third-party libraries to be invalidated(like progress event).
|
||||||
|
function mockXHR() {
|
||||||
|
// mock patch
|
||||||
|
// https://github.com/nuysoft/Mock/issues/300
|
||||||
|
Mock.XHR.prototype.proxy_send = Mock.XHR.prototype.send
|
||||||
|
Mock.XHR.prototype.send = function() {
|
||||||
|
if (this.custom.xhr) {
|
||||||
|
this.custom.xhr.withCredentials = this.withCredentials || false
|
||||||
|
|
||||||
|
if (this.responseType) {
|
||||||
|
this.custom.xhr.responseType = this.responseType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.proxy_send(...arguments)
|
||||||
|
}
|
||||||
|
|
||||||
|
function XHR2ExpressReqWrap(respond) {
|
||||||
|
return function(options) {
|
||||||
|
let result = null
|
||||||
|
if (respond instanceof Function) {
|
||||||
|
const { body, type, url } = options
|
||||||
|
// https://expressjs.com/en/4x/api.html#req
|
||||||
|
result = respond({
|
||||||
|
method: type,
|
||||||
|
body: JSON.parse(body),
|
||||||
|
query: param2Obj(url),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
result = respond
|
||||||
|
}
|
||||||
|
return Mock.mock(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const i of mocks) {
|
||||||
|
Mock.mock(new RegExp(i.url), i.type || 'get', XHR2ExpressReqWrap(i.response))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
mocks,
|
||||||
|
mockXHR,
|
||||||
|
}
|
||||||
89
mock/mock-server.js
Executable file
@ -0,0 +1,89 @@
|
|||||||
|
const chokidar = require('chokidar')
|
||||||
|
const bodyParser = require('body-parser')
|
||||||
|
const chalk = require('chalk')
|
||||||
|
const path = require('path')
|
||||||
|
const Mock = require('mockjs')
|
||||||
|
const express = require('express')
|
||||||
|
|
||||||
|
const mockDir = path.join(process.cwd(), '/mock')
|
||||||
|
|
||||||
|
function registerRoutes(app) {
|
||||||
|
let mockLastIndex
|
||||||
|
const { mocks } = require('./index.js')
|
||||||
|
const mocksForServer = mocks.map(route => {
|
||||||
|
return responseFake(route.url, route.type, route.response)
|
||||||
|
})
|
||||||
|
for (const mock of mocksForServer) {
|
||||||
|
const router = express.Router()
|
||||||
|
router.use(bodyParser.json())
|
||||||
|
router.use(
|
||||||
|
bodyParser.urlencoded({
|
||||||
|
extended: true,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
router.use(mock.response)
|
||||||
|
app[mock.type](mock.url, router)
|
||||||
|
mockLastIndex = app._router.stack.length
|
||||||
|
}
|
||||||
|
const mockRoutesLength = Object.keys(mocksForServer).length
|
||||||
|
return {
|
||||||
|
mockRoutesLength: mockRoutesLength,
|
||||||
|
mockStartIndex: mockLastIndex - mockRoutesLength,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function unregisterRoutes() {
|
||||||
|
Object.keys(require.cache).forEach(i => {
|
||||||
|
if (i.includes(mockDir)) {
|
||||||
|
delete require.cache[require.resolve(i)]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// for mock server
|
||||||
|
const responseFake = (url, type, respond) => {
|
||||||
|
return {
|
||||||
|
url: new RegExp(url),
|
||||||
|
type: type || 'get',
|
||||||
|
response(req, res) {
|
||||||
|
console.log('request invoke:' + req.path)
|
||||||
|
res.json(Mock.mock(respond instanceof Function ? respond(req, res) : respond))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = app => {
|
||||||
|
// parse app.body
|
||||||
|
// https://expressjs.com/en/4x/api.html#req.body
|
||||||
|
// eslint-disable-next-line
|
||||||
|
|
||||||
|
const mockRoutes = registerRoutes(app)
|
||||||
|
var mockRoutesLength = mockRoutes.mockRoutesLength
|
||||||
|
var mockStartIndex = mockRoutes.mockStartIndex
|
||||||
|
|
||||||
|
// watch files, hot reload mock server
|
||||||
|
chokidar
|
||||||
|
.watch(mockDir, {
|
||||||
|
ignored: /mock-server/,
|
||||||
|
ignoreInitial: true,
|
||||||
|
})
|
||||||
|
.on('all', (event, path) => {
|
||||||
|
if (event === 'change' || event === 'add') {
|
||||||
|
try {
|
||||||
|
// remove mock routes stack
|
||||||
|
app._router.stack.splice(mockStartIndex, mockRoutesLength)
|
||||||
|
|
||||||
|
// clear routes cache
|
||||||
|
unregisterRoutes()
|
||||||
|
|
||||||
|
const mockRoutes = registerRoutes(app)
|
||||||
|
mockRoutesLength = mockRoutes.mockRoutesLength
|
||||||
|
mockStartIndex = mockRoutes.mockStartIndex
|
||||||
|
|
||||||
|
console.log(chalk.magentaBright(`\n > Mock Server hot reload success! changed ${path}`))
|
||||||
|
} catch (error) {
|
||||||
|
console.log(chalk.redBright(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
33
mock/table.js
Executable file
@ -0,0 +1,33 @@
|
|||||||
|
// 生成随机变量mock示例
|
||||||
|
const Mock = require('mockjs')
|
||||||
|
|
||||||
|
const data = Mock.mock({
|
||||||
|
'items|30': [
|
||||||
|
{
|
||||||
|
id: '@id',
|
||||||
|
title: '@sentence(10, 20)',
|
||||||
|
'status|1': ['published', 'draft', 'deleted'],
|
||||||
|
author: 'name',
|
||||||
|
display_time: '@datetime',
|
||||||
|
pageviews: '@integer(300, 5000)',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports = [
|
||||||
|
{
|
||||||
|
url: '/table/list',
|
||||||
|
type: 'get',
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
response: config => {
|
||||||
|
const items = data.items
|
||||||
|
return {
|
||||||
|
errcode: 0,
|
||||||
|
data: {
|
||||||
|
total: items.length,
|
||||||
|
items: items,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
98
mock/user.js
Executable file
@ -0,0 +1,98 @@
|
|||||||
|
const accounts = {
|
||||||
|
admin: {
|
||||||
|
password: '21232f297a57a5a743894a0e4a801fc3',
|
||||||
|
name: 'super Admin',
|
||||||
|
role: [],
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
password: 'ee11cbb19052e40b07aac0ca060c23ee',
|
||||||
|
name: 'nomal user',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokens = {
|
||||||
|
admin: {
|
||||||
|
token: 'admin-token',
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
token: 'user-token',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const { mockRoutesResource, buttonAuth } = require('./constant/userPermission')
|
||||||
|
const permission = {
|
||||||
|
menu: mockRoutesResource,
|
||||||
|
buttonAuth: buttonAuth,
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = [
|
||||||
|
// 登录接口
|
||||||
|
{
|
||||||
|
url: '/user/login', // 支持正则
|
||||||
|
type: 'post',
|
||||||
|
response: config => {
|
||||||
|
const { username, password } = config.body
|
||||||
|
let token = undefined
|
||||||
|
if (accounts[username] && accounts[username].password === password) {
|
||||||
|
token = tokens[username]
|
||||||
|
}
|
||||||
|
|
||||||
|
// mock error
|
||||||
|
if (!token) {
|
||||||
|
return {
|
||||||
|
errcode: 1,
|
||||||
|
errmsg: '账号或密码不匹配',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
errcode: 0,
|
||||||
|
data: token,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 获取用户信息
|
||||||
|
{
|
||||||
|
url: '/user/info',
|
||||||
|
type: 'get',
|
||||||
|
response: config => {
|
||||||
|
let token = config.headers.authorization.split('Bearer ')[1]
|
||||||
|
if (token === 'admin-token') {
|
||||||
|
return {
|
||||||
|
errcode: 0,
|
||||||
|
data: {
|
||||||
|
isAdmin: true,
|
||||||
|
username: '管理员',
|
||||||
|
avatar: '',
|
||||||
|
info: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else if (token === 'user-token') {
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
isAdmin: false,
|
||||||
|
username: '普通用户',
|
||||||
|
avatar: '',
|
||||||
|
info: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
errcode: 0,
|
||||||
|
errmsg: `token异常`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 获取用户权限制
|
||||||
|
{
|
||||||
|
url: '/permission/getUserPermissionByToken',
|
||||||
|
type: 'get',
|
||||||
|
response: () => {
|
||||||
|
return {
|
||||||
|
errcode: 0,
|
||||||
|
data: permission,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
25
mock/utils.js
Executable file
@ -0,0 +1,25 @@
|
|||||||
|
/**
|
||||||
|
* @param {string} url
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
function param2Obj(url) {
|
||||||
|
const search = decodeURIComponent(url.split('?')[1]).replace(/\+/g, ' ') // url中, 传空格要转成+号, 这里是转回来
|
||||||
|
if (!search) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
const obj = {}
|
||||||
|
const searchArr = search.split('&')
|
||||||
|
searchArr.forEach(v => {
|
||||||
|
const index = v.indexOf('=')
|
||||||
|
if (index !== -1) {
|
||||||
|
const name = v.substring(0, index)
|
||||||
|
const val = v.substring(index + 1, v.length)
|
||||||
|
obj[name] = val
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
param2Obj,
|
||||||
|
}
|
||||||
39128
package-lock.json
generated
Normal file
74
package.json
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
{
|
||||||
|
"name": "vue-spa-scaffold",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vue-cli-service serve",
|
||||||
|
"build:prod": "vue-cli-service build",
|
||||||
|
"format": "prettier --write './**/*.{js,ts,vue,json,css,less,scss}'",
|
||||||
|
"svgo": "svgo -f src/assets/icons/svg --config=src/assets/icons/svgo.yml",
|
||||||
|
"lint": "vue-cli-service lint",
|
||||||
|
"test": "mocha"
|
||||||
|
},
|
||||||
|
"gitHooks": {
|
||||||
|
"pre-commit": "lint-staged",
|
||||||
|
"commit-msg": "commitlint -e $GIT_PARAMS"
|
||||||
|
},
|
||||||
|
"lint-staged": {
|
||||||
|
"*.{js,vue,ts,html}": [
|
||||||
|
"yarn run lint"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"ant-design-vue": "^1.7.4",
|
||||||
|
"axios": "^0.21.1",
|
||||||
|
"core-js": "^3.6.5",
|
||||||
|
"md5": "^2.3.0",
|
||||||
|
"nprogress": "^0.2.0",
|
||||||
|
"vue": "^2.7.14",
|
||||||
|
"vue-ls": "^3.2.2",
|
||||||
|
"vue-router": "^3.5.1",
|
||||||
|
"vuex": "^3.6.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@commitlint/cli": "^13.1.0",
|
||||||
|
"@commitlint/config-conventional": "^13.1.0",
|
||||||
|
"@vue/cli-plugin-babel": "~4.5.0",
|
||||||
|
"@vue/cli-plugin-eslint": "~4.5.0",
|
||||||
|
"@vue/cli-plugin-router": "~4.5.0",
|
||||||
|
"@vue/cli-plugin-vuex": "~4.5.0",
|
||||||
|
"@vue/cli-service": "~4.5.0",
|
||||||
|
"@vue/eslint-config-prettier": "^6.0.0",
|
||||||
|
"add": "^2.0.6",
|
||||||
|
"babel-eslint": "^10.1.0",
|
||||||
|
"babel-plugin-import": "^1.13.3",
|
||||||
|
"chai": "^4.3.4",
|
||||||
|
"chalk": "^4.1.2",
|
||||||
|
"chokidar": "2.1.5",
|
||||||
|
"clear": "^0.1.0",
|
||||||
|
"commitizen": "^4.2.4",
|
||||||
|
"compression-webpack-plugin": "3.1.0",
|
||||||
|
"cz-customizable": "^6.3.0",
|
||||||
|
"eslint": "^6.7.2",
|
||||||
|
"eslint-plugin-prettier": "^3.1.3",
|
||||||
|
"eslint-plugin-vue": "^6.2.2",
|
||||||
|
"html-webpack-plugin": "^3.0.0",
|
||||||
|
"less": "^3.0.4",
|
||||||
|
"less-loader": "^5.0.0",
|
||||||
|
"lint-staged": "^11.1.2",
|
||||||
|
"mocha": "^8.3.2",
|
||||||
|
"mockjs": "^1.1.0",
|
||||||
|
"path": "^0.12.7",
|
||||||
|
"prettier": "^1.19.1",
|
||||||
|
"script-ext-html-webpack-plugin": "^2.1.5",
|
||||||
|
"svg-sprite-loader": "^6.0.9",
|
||||||
|
"vue-template-compiler": "^2.7.14",
|
||||||
|
"webpack": "^4.0.0",
|
||||||
|
"yarn": "^1.22.17"
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"commitizen": {
|
||||||
|
"path": "node_modules/cz-customizable"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
7701
public/color.less
Normal file
BIN
public/favicon.ico
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
272
public/index.html
Normal file
@ -0,0 +1,272 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-cmn-Hans">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
|
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||||
|
<style>
|
||||||
|
.chromeframe {
|
||||||
|
margin: 0.2em 0;
|
||||||
|
background: #ccc;
|
||||||
|
color: #000;
|
||||||
|
padding: 0.2em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#loader-wrapper {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 999999;
|
||||||
|
}
|
||||||
|
|
||||||
|
#loader {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
margin: -75px 0 0 -75px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 3px solid transparent;
|
||||||
|
/* COLOR 1 */
|
||||||
|
border-top-color: #FFF;
|
||||||
|
-webkit-animation: spin 2s linear infinite;
|
||||||
|
/* Chrome, Opera 15+, Safari 5+ */
|
||||||
|
-ms-animation: spin 2s linear infinite;
|
||||||
|
/* Chrome, Opera 15+, Safari 5+ */
|
||||||
|
-moz-animation: spin 2s linear infinite;
|
||||||
|
/* Chrome, Opera 15+, Safari 5+ */
|
||||||
|
-o-animation: spin 2s linear infinite;
|
||||||
|
/* Chrome, Opera 15+, Safari 5+ */
|
||||||
|
animation: spin 2s linear infinite;
|
||||||
|
/* Chrome, Firefox 16+, IE 10+, Opera */
|
||||||
|
z-index: 1001;
|
||||||
|
}
|
||||||
|
|
||||||
|
#loader:before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 5px;
|
||||||
|
left: 5px;
|
||||||
|
right: 5px;
|
||||||
|
bottom: 5px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 3px solid transparent;
|
||||||
|
/* COLOR 2 */
|
||||||
|
border-top-color: #FFF;
|
||||||
|
-webkit-animation: spin 3s linear infinite;
|
||||||
|
/* Chrome, Opera 15+, Safari 5+ */
|
||||||
|
-moz-animation: spin 3s linear infinite;
|
||||||
|
/* Chrome, Opera 15+, Safari 5+ */
|
||||||
|
-o-animation: spin 3s linear infinite;
|
||||||
|
/* Chrome, Opera 15+, Safari 5+ */
|
||||||
|
-ms-animation: spin 3s linear infinite;
|
||||||
|
/* Chrome, Opera 15+, Safari 5+ */
|
||||||
|
animation: spin 3s linear infinite;
|
||||||
|
/* Chrome, Firefox 16+, IE 10+, Opera */
|
||||||
|
}
|
||||||
|
|
||||||
|
#loader:after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 15px;
|
||||||
|
left: 15px;
|
||||||
|
right: 15px;
|
||||||
|
bottom: 15px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 3px solid transparent;
|
||||||
|
border-top-color: #FFF;
|
||||||
|
/* COLOR 3 */
|
||||||
|
-moz-animation: spin 1.5s linear infinite;
|
||||||
|
/* Chrome, Opera 15+, Safari 5+ */
|
||||||
|
-o-animation: spin 1.5s linear infinite;
|
||||||
|
/* Chrome, Opera 15+, Safari 5+ */
|
||||||
|
-ms-animation: spin 1.5s linear infinite;
|
||||||
|
/* Chrome, Opera 15+, Safari 5+ */
|
||||||
|
-webkit-animation: spin 1.5s linear infinite;
|
||||||
|
/* Chrome, Opera 15+, Safari 5+ */
|
||||||
|
animation: spin 1.5s linear infinite;
|
||||||
|
/* Chrome, Firefox 16+, IE 10+, Opera */
|
||||||
|
}
|
||||||
|
|
||||||
|
@-webkit-keyframes spin {
|
||||||
|
0% {
|
||||||
|
-webkit-transform: rotate(0deg);
|
||||||
|
/* Chrome, Opera 15+, Safari 3.1+ */
|
||||||
|
-ms-transform: rotate(0deg);
|
||||||
|
/* IE 9 */
|
||||||
|
transform: rotate(0deg);
|
||||||
|
/* Firefox 16+, IE 10+, Opera */
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
-webkit-transform: rotate(360deg);
|
||||||
|
/* Chrome, Opera 15+, Safari 3.1+ */
|
||||||
|
-ms-transform: rotate(360deg);
|
||||||
|
/* IE 9 */
|
||||||
|
transform: rotate(360deg);
|
||||||
|
/* Firefox 16+, IE 10+, Opera */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% {
|
||||||
|
-webkit-transform: rotate(0deg);
|
||||||
|
/* Chrome, Opera 15+, Safari 3.1+ */
|
||||||
|
-ms-transform: rotate(0deg);
|
||||||
|
/* IE 9 */
|
||||||
|
transform: rotate(0deg);
|
||||||
|
/* Firefox 16+, IE 10+, Opera */
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
-webkit-transform: rotate(360deg);
|
||||||
|
/* Chrome, Opera 15+, Safari 3.1+ */
|
||||||
|
-ms-transform: rotate(360deg);
|
||||||
|
/* IE 9 */
|
||||||
|
transform: rotate(360deg);
|
||||||
|
/* Firefox 16+, IE 10+, Opera */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#loader-wrapper .loader-section {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
width: 51%;
|
||||||
|
height: 100%;
|
||||||
|
background: #49a9ee;
|
||||||
|
/* Old browsers */
|
||||||
|
z-index: 1000;
|
||||||
|
-webkit-transform: translateX(0);
|
||||||
|
/* Chrome, Opera 15+, Safari 3.1+ */
|
||||||
|
-ms-transform: translateX(0);
|
||||||
|
/* IE 9 */
|
||||||
|
transform: translateX(0);
|
||||||
|
/* Firefox 16+, IE 10+, Opera */
|
||||||
|
}
|
||||||
|
|
||||||
|
#loader-wrapper .loader-section.section-left {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#loader-wrapper .loader-section.section-right {
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loaded */
|
||||||
|
.loaded #loader-wrapper .loader-section.section-left {
|
||||||
|
-webkit-transform: translateX(-100%);
|
||||||
|
/* Chrome, Opera 15+, Safari 3.1+ */
|
||||||
|
-ms-transform: translateX(-100%);
|
||||||
|
/* IE 9 */
|
||||||
|
transform: translateX(-100%);
|
||||||
|
/* Firefox 16+, IE 10+, Opera */
|
||||||
|
-webkit-transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
|
||||||
|
transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loaded #loader-wrapper .loader-section.section-right {
|
||||||
|
-webkit-transform: translateX(100%);
|
||||||
|
/* Chrome, Opera 15+, Safari 3.1+ */
|
||||||
|
-ms-transform: translateX(100%);
|
||||||
|
/* IE 9 */
|
||||||
|
transform: translateX(100%);
|
||||||
|
/* Firefox 16+, IE 10+, Opera */
|
||||||
|
-webkit-transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
|
||||||
|
transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loaded #loader {
|
||||||
|
opacity: 0;
|
||||||
|
-webkit-transition: all 0.3s ease-out;
|
||||||
|
transition: all 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loaded #loader-wrapper {
|
||||||
|
visibility: hidden;
|
||||||
|
-webkit-transform: translateY(-100%);
|
||||||
|
/* Chrome, Opera 15+, Safari 3.1+ */
|
||||||
|
-ms-transform: translateY(-100%);
|
||||||
|
/* IE 9 */
|
||||||
|
transform: translateY(-100%);
|
||||||
|
/* Firefox 16+, IE 10+, Opera */
|
||||||
|
-webkit-transition: all 0.3s 1s ease-out;
|
||||||
|
transition: all 0.3s 1s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* JavaScript Turned Off */
|
||||||
|
.no-js #loader-wrapper {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-js h1 {
|
||||||
|
color: #222222;
|
||||||
|
}
|
||||||
|
|
||||||
|
#loader-wrapper .load_title {
|
||||||
|
font-family: 'Open Sans';
|
||||||
|
color: #FFF;
|
||||||
|
font-size: 14px;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
z-index: 9999999999999;
|
||||||
|
position: absolute;
|
||||||
|
top: 60%;
|
||||||
|
opacity: 1;
|
||||||
|
line-height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#loader-wrapper .load_title span {
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: italic;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #FFF;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 滚动条优化 start */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: #f6f6f6;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: #cdcdcd;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #747474;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-corner {
|
||||||
|
background: #f6f6f6;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>
|
||||||
|
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||||
|
</noscript>
|
||||||
|
<div id="app">
|
||||||
|
<!-- <div>正在加载中。。。</div> -->
|
||||||
|
<div id="loader-wrapper">
|
||||||
|
<div id="loader"></div>
|
||||||
|
<div class="loader-section section-left"></div>
|
||||||
|
<div class="loader-section section-right"></div>
|
||||||
|
<div class="load_title">正在加载xxx企业开发平台,请耐心等待
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- built files will be auto injected -->
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
33
src/App.vue
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<template>
|
||||||
|
<a-config-provider :locale="zh_CN">
|
||||||
|
<div id="app">
|
||||||
|
<router-view />
|
||||||
|
</div>
|
||||||
|
</a-config-provider>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import zh_CN from 'ant-design-vue/lib/locale-provider/zh_CN'
|
||||||
|
import moment from 'moment'
|
||||||
|
import 'moment/locale/zh-cn'
|
||||||
|
|
||||||
|
moment.locale('zh-cn')
|
||||||
|
export default {
|
||||||
|
name: 'App',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
zh_CN,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
#app {
|
||||||
|
font-family: Avenir, Helvetica, Arial, sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
color: #2c3e50;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
28
src/api/user.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
const urls = {
|
||||||
|
login: '/user/login', // 登录接口
|
||||||
|
getUserPermission: '/permission/getUserPermissionByToken', // 获取权限接口
|
||||||
|
getUserInfo: '/user/info', // 获取用户信息
|
||||||
|
}
|
||||||
|
|
||||||
|
export const login = params => {
|
||||||
|
return request({
|
||||||
|
url: urls.login,
|
||||||
|
method: 'post',
|
||||||
|
data: params,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getUserPermission = () => {
|
||||||
|
return request({
|
||||||
|
url: urls.getUserPermission,
|
||||||
|
method: 'get',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getUserInfo = () => {
|
||||||
|
return request({
|
||||||
|
url: urls.getUserInfo,
|
||||||
|
method: 'get',
|
||||||
|
})
|
||||||
|
}
|
||||||
BIN
src/assets/avatar.jpg
Executable file
|
After Width: | Height: | Size: 1.8 KiB |
69
src/assets/background.svg
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg width="1361px" height="609px" viewBox="0 0 1361 609" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<!-- Generator: Sketch 46.2 (44496) - http://www.bohemiancoding.com/sketch -->
|
||||||
|
<title>Group 21</title>
|
||||||
|
<desc>Created with Sketch.</desc>
|
||||||
|
<defs></defs>
|
||||||
|
<g id="Ant-Design-Pro-3.0" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||||
|
<g id="账户密码登录-校验" transform="translate(-79.000000, -82.000000)">
|
||||||
|
<g id="Group-21" transform="translate(77.000000, 73.000000)">
|
||||||
|
<g id="Group-18" opacity="0.8" transform="translate(74.901416, 569.699158) rotate(-7.000000) translate(-74.901416, -569.699158) translate(4.901416, 525.199158)">
|
||||||
|
<ellipse id="Oval-11" fill="#CFDAE6" opacity="0.25" cx="63.5748792" cy="32.468367" rx="21.7830479" ry="21.766008"></ellipse>
|
||||||
|
<ellipse id="Oval-3" fill="#CFDAE6" opacity="0.599999964" cx="5.98746479" cy="13.8668601" rx="5.2173913" ry="5.21330997"></ellipse>
|
||||||
|
<path d="M38.1354514,88.3520215 C43.8984227,88.3520215 48.570234,83.6838647 48.570234,77.9254015 C48.570234,72.1669383 43.8984227,67.4987816 38.1354514,67.4987816 C32.3724801,67.4987816 27.7006688,72.1669383 27.7006688,77.9254015 C27.7006688,83.6838647 32.3724801,88.3520215 38.1354514,88.3520215 Z" id="Oval-3-Copy" fill="#CFDAE6" opacity="0.45"></path>
|
||||||
|
<path d="M64.2775582,33.1704963 L119.185836,16.5654915" id="Path-12" stroke="#CFDAE6" stroke-width="1.73913043" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||||
|
<path d="M42.1431708,26.5002681 L7.71190162,14.5640702" id="Path-16" stroke="#E0B4B7" stroke-width="0.702678964" opacity="0.7" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="1.405357899873153,2.108036953469981"></path>
|
||||||
|
<path d="M63.9262187,33.521561 L43.6721326,69.3250951" id="Path-15" stroke="#BACAD9" stroke-width="0.702678964" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="1.405357899873153,2.108036953469981"></path>
|
||||||
|
<g id="Group-17" transform="translate(126.850922, 13.543654) rotate(30.000000) translate(-126.850922, -13.543654) translate(117.285705, 4.381889)" fill="#CFDAE6">
|
||||||
|
<ellipse id="Oval-4" opacity="0.45" cx="9.13482653" cy="9.12768076" rx="9.13482653" ry="9.12768076"></ellipse>
|
||||||
|
<path d="M18.2696531,18.2553615 C18.2696531,13.2142826 14.1798519,9.12768076 9.13482653,9.12768076 C4.08980114,9.12768076 0,13.2142826 0,18.2553615 L18.2696531,18.2553615 Z" id="Oval-4" transform="translate(9.134827, 13.691521) scale(-1, -1) translate(-9.134827, -13.691521) "></path>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g id="Group-14" transform="translate(216.294700, 123.725600) rotate(-5.000000) translate(-216.294700, -123.725600) translate(106.294700, 35.225600)">
|
||||||
|
<ellipse id="Oval-2" fill="#CFDAE6" opacity="0.25" cx="29.1176471" cy="29.1402439" rx="29.1176471" ry="29.1402439"></ellipse>
|
||||||
|
<ellipse id="Oval-2" fill="#CFDAE6" opacity="0.3" cx="29.1176471" cy="29.1402439" rx="21.5686275" ry="21.5853659"></ellipse>
|
||||||
|
<ellipse id="Oval-2-Copy" stroke="#CFDAE6" opacity="0.4" cx="179.019608" cy="138.146341" rx="23.7254902" ry="23.7439024"></ellipse>
|
||||||
|
<ellipse id="Oval-2" fill="#BACAD9" opacity="0.5" cx="29.1176471" cy="29.1402439" rx="10.7843137" ry="10.7926829"></ellipse>
|
||||||
|
<path d="M29.1176471,39.9329268 L29.1176471,18.347561 C23.1616351,18.347561 18.3333333,23.1796097 18.3333333,29.1402439 C18.3333333,35.1008781 23.1616351,39.9329268 29.1176471,39.9329268 Z" id="Oval-2" fill="#BACAD9"></path>
|
||||||
|
<g id="Group-9" opacity="0.45" transform="translate(172.000000, 131.000000)" fill="#E6A1A6">
|
||||||
|
<ellipse id="Oval-2-Copy-2" cx="7.01960784" cy="7.14634146" rx="6.47058824" ry="6.47560976"></ellipse>
|
||||||
|
<path d="M0.549019608,13.6219512 C4.12262681,13.6219512 7.01960784,10.722722 7.01960784,7.14634146 C7.01960784,3.56996095 4.12262681,0.670731707 0.549019608,0.670731707 L0.549019608,13.6219512 Z" id="Oval-2-Copy-2" transform="translate(3.784314, 7.146341) scale(-1, 1) translate(-3.784314, -7.146341) "></path>
|
||||||
|
</g>
|
||||||
|
<ellipse id="Oval-10" fill="#CFDAE6" cx="218.382353" cy="138.685976" rx="1.61764706" ry="1.61890244"></ellipse>
|
||||||
|
<ellipse id="Oval-10-Copy-2" fill="#E0B4B7" opacity="0.35" cx="179.558824" cy="175.381098" rx="1.61764706" ry="1.61890244"></ellipse>
|
||||||
|
<ellipse id="Oval-10-Copy" fill="#E0B4B7" opacity="0.35" cx="180.098039" cy="102.530488" rx="2.15686275" ry="2.15853659"></ellipse>
|
||||||
|
<path d="M28.9985381,29.9671598 L171.151018,132.876024" id="Path-11" stroke="#CFDAE6" opacity="0.8"></path>
|
||||||
|
</g>
|
||||||
|
<g id="Group-10" opacity="0.799999952" transform="translate(1054.100635, 36.659317) rotate(-11.000000) translate(-1054.100635, -36.659317) translate(1026.600635, 4.659317)">
|
||||||
|
<ellipse id="Oval-7" stroke="#CFDAE6" stroke-width="0.941176471" cx="43.8135593" cy="32" rx="11.1864407" ry="11.2941176"></ellipse>
|
||||||
|
<g id="Group-12" transform="translate(34.596774, 23.111111)" fill="#BACAD9">
|
||||||
|
<ellipse id="Oval-7" opacity="0.45" cx="9.18534718" cy="8.88888889" rx="8.47457627" ry="8.55614973"></ellipse>
|
||||||
|
<path d="M9.18534718,17.4450386 C13.8657264,17.4450386 17.6599235,13.6143199 17.6599235,8.88888889 C17.6599235,4.16345787 13.8657264,0.332739156 9.18534718,0.332739156 L9.18534718,17.4450386 Z" id="Oval-7"></path>
|
||||||
|
</g>
|
||||||
|
<path d="M34.6597385,24.809694 L5.71666084,4.76878945" id="Path-2" stroke="#CFDAE6" stroke-width="0.941176471"></path>
|
||||||
|
<ellipse id="Oval" stroke="#CFDAE6" stroke-width="0.941176471" cx="3.26271186" cy="3.29411765" rx="3.26271186" ry="3.29411765"></ellipse>
|
||||||
|
<ellipse id="Oval-Copy" fill="#F7E1AD" cx="2.79661017" cy="61.1764706" rx="2.79661017" ry="2.82352941"></ellipse>
|
||||||
|
<path d="M34.6312443,39.2922712 L5.06366663,59.785082" id="Path-10" stroke="#CFDAE6" stroke-width="0.941176471"></path>
|
||||||
|
</g>
|
||||||
|
<g id="Group-19" opacity="0.33" transform="translate(1282.537219, 446.502867) rotate(-10.000000) translate(-1282.537219, -446.502867) translate(1142.537219, 327.502867)">
|
||||||
|
<g id="Group-17" transform="translate(141.333539, 104.502742) rotate(275.000000) translate(-141.333539, -104.502742) translate(129.333539, 92.502742)" fill="#BACAD9">
|
||||||
|
<circle id="Oval-4" opacity="0.45" cx="11.6666667" cy="11.6666667" r="11.6666667"></circle>
|
||||||
|
<path d="M23.3333333,23.3333333 C23.3333333,16.8900113 18.1099887,11.6666667 11.6666667,11.6666667 C5.22334459,11.6666667 0,16.8900113 0,23.3333333 L23.3333333,23.3333333 Z" id="Oval-4" transform="translate(11.666667, 17.500000) scale(-1, -1) translate(-11.666667, -17.500000) "></path>
|
||||||
|
</g>
|
||||||
|
<circle id="Oval-5-Copy-6" fill="#CFDAE6" cx="201.833333" cy="87.5" r="5.83333333"></circle>
|
||||||
|
<path d="M143.5,88.8126685 L155.070501,17.6038544" id="Path-17" stroke="#BACAD9" stroke-width="1.16666667"></path>
|
||||||
|
<path d="M17.5,37.3333333 L127.466252,97.6449735" id="Path-18" stroke="#BACAD9" stroke-width="1.16666667"></path>
|
||||||
|
<polyline id="Path-19" stroke="#CFDAE6" stroke-width="1.16666667" points="143.902597 120.302281 174.935455 231.571342 38.5 147.510847 126.366941 110.833333"></polyline>
|
||||||
|
<path d="M159.833333,99.7453842 L195.416667,89.25" id="Path-20" stroke="#E0B4B7" stroke-width="1.16666667" opacity="0.6"></path>
|
||||||
|
<path d="M205.333333,82.1372105 L238.719406,36.1666667" id="Path-24" stroke="#BACAD9" stroke-width="1.16666667"></path>
|
||||||
|
<path d="M266.723424,132.231988 L207.083333,90.4166667" id="Path-25" stroke="#CFDAE6" stroke-width="1.16666667"></path>
|
||||||
|
<circle id="Oval-5" fill="#C1D1E0" cx="156.916667" cy="8.75" r="8.75"></circle>
|
||||||
|
<circle id="Oval-5-Copy-3" fill="#C1D1E0" cx="39.0833333" cy="148.75" r="5.25"></circle>
|
||||||
|
<circle id="Oval-5-Copy-2" fill-opacity="0.6" fill="#D1DEED" cx="8.75" cy="33.25" r="8.75"></circle>
|
||||||
|
<circle id="Oval-5-Copy-4" fill-opacity="0.6" fill="#D1DEED" cx="243.833333" cy="30.3333333" r="5.83333333"></circle>
|
||||||
|
<circle id="Oval-5-Copy-5" fill="#E0B4B7" cx="175.583333" cy="232.75" r="5.25"></circle>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 8.7 KiB |
BIN
src/assets/checkcode.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
9
src/assets/icons/index.js
Executable file
@ -0,0 +1,9 @@
|
|||||||
|
import Vue from 'vue'
|
||||||
|
import SvgIcon from '@/components/SvgIcon' // svg组件
|
||||||
|
|
||||||
|
// register globally
|
||||||
|
Vue.component('SvgIcon', SvgIcon)
|
||||||
|
|
||||||
|
const req = require.context('./svg', false, /\.svg$/)
|
||||||
|
const requireAll = requireContext => requireContext.keys().map(requireContext)
|
||||||
|
requireAll(req)
|
||||||
1
src/assets/icons/svg/develop.svg
Executable file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" class="icon" viewBox="0 0 1024 1024"><defs><style/></defs><path d="M529.05 527.616l-30.772-30.746 85.07-85.094 30.77 30.771z"/><path d="M0 340.48l427.52 256L675.84 1024 1024 0 0 340.48zM665.6 921.6L458.24 565.76 102.4 353.28 911.36 81.92l-243.2 243.2 30.72 30.72 243.2-243.2L665.6 921.6z"/></svg>
|
||||||
|
After Width: | Height: | Size: 361 B |
1
src/assets/icons/svg/svgtest.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="200" height="200"><path d="M511.19 700.71c-102.96 0-186.73-83.77-186.73-186.73 0-102.93 83.77-186.7 186.73-186.7 102.93 0 186.7 83.77 186.7 186.7 0 102.96-83.77 186.73-186.7 186.73zm0-299.26c-62.08 0-112.56 50.49-112.56 112.53 0 62.08 50.49 112.56 112.56 112.56 62.04 0 112.53-50.49 112.53-112.56 0-62.05-50.49-112.53-112.53-112.53zM915.01 481.56c-143.78 15.72-255.7 137.46-255.7 285.43 0 35.59 6.57 69.62 18.41 101.08a402.92 402.92 0 0084.23-51.09c-.05-2.24-.42-4.4-.42-6.66 0-108.86 60.57-203.56 149.84-252.28 2.71-18.89 4.6-38.05 4.6-57.7-.01-6.33-.67-12.51-.96-18.78z"/><path d="M253.25 231.34c-10.83 0-21.51-4.71-28.86-13.73-12.89-15.94-10.43-39.3 5.47-52.19 36.07-29.19 76.45-52.59 120.06-69.54 19.05-7.5 40.6 2.03 47.99 21.11 7.42 19.09-2.03 40.56-21.11 47.99-36.43 14.16-70.15 33.72-100.21 58.09-6.89 5.55-15.15 8.27-23.34 8.27zm515.81 0c-8.26 0-16.55-2.75-23.47-8.4-29.55-24.19-63.09-43.71-99.71-57.95-19.09-7.42-28.54-28.9-21.11-47.99 7.39-19.09 28.94-28.65 47.99-21.11 43.89 17.06 84.2 40.49 119.84 69.68 15.83 12.97 18.18 36.33 5.18 52.19-7.32 8.94-17.97 13.58-28.72 13.58zM659.32 934.49c-14.85 0-28.83-8.95-34.55-23.61-7.42-19.09 1.99-40.6 21.08-48.02 36.69-14.31 70.26-33.79 99.74-57.95 15.83-13.04 39.26-10.65 52.19 5.18 13 15.86 10.65 39.22-5.18 52.19-35.53 29.12-75.84 52.55-119.81 69.68a36.863 36.863 0 01-13.47 2.53zM105.2 614.8c-18.18 0-34.04-13.33-36.69-31.83-3.44-24.12-5.18-47.34-5.18-69.07 0-24.19 1.74-46.76 5.29-68.88 3.26-20.25 22.24-34.08 42.48-30.75 20.25 3.26 34.01 22.27 30.75 42.48-2.93 18.22-4.35 36.94-4.35 57.15 0 18.22 1.48 37.92 4.45 58.56 2.9 20.28-11.19 39.08-31.47 41.98-1.77.25-3.54.36-5.28.36zm258.15 319.69c-4.49 0-9.05-.8-13.47-2.54-43.68-17.02-84.06-40.42-120.02-69.54-15.9-12.89-18.36-36.25-5.47-52.19 12.89-15.83 36.22-18.33 52.19-5.47 29.99 24.3 63.71 43.86 100.25 58.09 19.09 7.42 28.5 28.94 21.08 48.02-5.73 14.68-19.74 23.63-34.56 23.63zM917.08 614.8c-1.74 0-3.51-.11-5.29-.36-20.28-2.9-34.37-21.69-31.47-41.98 2.97-20.79 4.49-40.53 4.49-58.56 0-20.28-1.45-38.93-4.38-57.08-3.26-20.21 10.47-39.26 30.68-42.55 20.14-3.33 39.26 10.47 42.55 30.68 3.59 22.09 5.32 44.66 5.32 68.96 0 21.55-1.77 44.8-5.22 69.07-2.64 18.49-18.5 31.82-36.68 31.82z"/><path d="M511.34 242.06c-69.97 0-136.43-33.5-177.83-89.6-12.17-16.48-8.66-39.69 7.82-51.86 16.48-12.17 39.69-8.62 51.86 7.82 27.89 37.81 70.95 59.47 118.14 59.47s90.25-21.66 118.14-59.47c12.17-16.44 35.38-19.99 51.86-7.82s19.99 35.38 7.82 51.86c-41.38 56.1-107.84 89.6-177.81 89.6zm148.02 692.43c-11.37 0-22.6-5.22-29.88-15.07-27.89-37.81-70.95-59.5-118.14-59.5s-90.25 21.69-118.14 59.5c-12.17 16.48-35.38 19.99-51.86 7.82-16.48-12.17-19.99-35.38-7.82-51.86 41.4-56.14 107.89-89.64 177.83-89.64s136.43 33.5 177.83 89.64c12.17 16.48 8.66 39.69-7.82 51.86-6.65 4.89-14.36 7.25-22 7.25zM105.2 487.97c-18.51 0-34.51-13.83-36.8-32.67-2.43-20.35 12.1-38.82 32.41-41.25 46.76-5.58 87.1-32.2 110.64-72.94 23.54-40.53 26.33-88.7 7.68-132.26-8.08-18.83.65-40.64 19.48-48.68 18.65-8.19 40.6.58 48.68 19.48 27.6 64.36 23.22 138.6-11.66 198.61-35.28 61.13-95.83 101.05-165.98 109.45-1.52.19-3 .26-4.45.26zm663.93 382.7c-14.38 0-28.03-8.37-34.08-22.38-27.71-64.1-23.36-138.28 11.63-198.4 34.41-60.34 96.59-101.19 166.2-109.05 20.72-2.03 38.75 12.35 41.03 32.7 2.28 20.35-12.35 38.72-32.7 41.03-46.86 5.25-87.07 31.62-110.24 72.33-23.36 40.16-26.22 89.38-7.82 131.97 8.11 18.8-.54 40.64-19.34 48.75a36.808 36.808 0 01-14.68 3.05zm147.95-382.7c-1.45 0-2.93-.07-4.45-.25-70.15-8.4-130.67-48.31-166.02-109.52-34.84-59.94-39.22-134.22-11.59-198.58 8.08-18.83 29.88-27.6 48.71-19.45 18.83 8.08 27.52 29.88 19.45 48.71-18.69 43.53-15.9 91.7 7.61 132.12 23.61 40.89 63.92 67.47 110.68 73.05 20.32 2.43 34.84 20.9 32.41 41.25-2.29 18.83-18.3 32.67-36.8 32.67zm-663.86 382.7c-4.93 0-9.89-.98-14.7-3.04-18.8-8.11-27.45-29.92-19.34-48.75 18.36-42.59 15.5-91.81-7.64-131.72-23.4-40.96-63.6-67.33-110.46-72.58-20.35-2.32-34.99-20.68-32.7-41.03 2.28-20.35 20.39-34.88 41.03-32.7 69.57 7.86 131.79 48.71 166.38 109.3 34.8 59.87 39.15 134.04 11.48 198.14-6.06 14.02-19.71 22.38-34.05 22.38z"/></svg>
|
||||||
|
After Width: | Height: | Size: 4.0 KiB |
22
src/assets/icons/svgo.yml
Executable file
@ -0,0 +1,22 @@
|
|||||||
|
# replace default config
|
||||||
|
|
||||||
|
# multipass: true
|
||||||
|
# full: true
|
||||||
|
|
||||||
|
plugins:
|
||||||
|
|
||||||
|
# - name
|
||||||
|
#
|
||||||
|
# or:
|
||||||
|
# - name: false
|
||||||
|
# - name: true
|
||||||
|
#
|
||||||
|
# or:
|
||||||
|
# - name:
|
||||||
|
# param1: 1
|
||||||
|
# param2: 2
|
||||||
|
|
||||||
|
- removeAttrs:
|
||||||
|
attrs:
|
||||||
|
- 'fill'
|
||||||
|
- 'fill-rule'
|
||||||
31
src/assets/less/common.less
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
@header-height: 54px;
|
||||||
|
@tab-height: 40px;
|
||||||
|
@gap: 4px;
|
||||||
|
@router-wrapper-1-padding-y: 20px;
|
||||||
|
|
||||||
|
.router-wrapper-1-height-top-tabs(){
|
||||||
|
height: calc(100vh - @header-height - @tab-height - @gap * 3 - @router-wrapper-1-padding-y * 2);
|
||||||
|
}
|
||||||
|
.router-wrapper-1-height-no-top-tabs(){
|
||||||
|
height: calc(100vh - @header-height - @gap * 3 - @router-wrapper-1-padding-y * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-content-height(){
|
||||||
|
.ant-tabs-top-tabs {
|
||||||
|
.router-wrapper-1-height-top-tabs();
|
||||||
|
}
|
||||||
|
.router-wrapper-2-top-tabs {
|
||||||
|
.router-wrapper-1-height-top-tabs();
|
||||||
|
padding-right: 24px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-tabs-no-top-tabs {
|
||||||
|
.router-wrapper-1-height-no-top-tabs();
|
||||||
|
}
|
||||||
|
.router-wrapper-2-no-top-tabs {
|
||||||
|
.router-wrapper-1-height-no-top-tabs();
|
||||||
|
padding-right: 24px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
src/assets/logo.png
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
185
src/components/Header.vue
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
<template>
|
||||||
|
<a-layout-header :style="{ backgroundColor: navTheme === 'light' ? primaryColor : '#fff' }">
|
||||||
|
<div>
|
||||||
|
<a-icon
|
||||||
|
class="trigger"
|
||||||
|
:type="collapsed ? 'menu-unfold' : 'menu-fold'"
|
||||||
|
@click="$emit('update:collapsed', !collapsed)"
|
||||||
|
:style="{ verticalAlign: 'top', ...color }"
|
||||||
|
/>
|
||||||
|
<h1
|
||||||
|
:style="{
|
||||||
|
display: 'inline-block',
|
||||||
|
...color,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
XXX后台管理系统
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<a-space size="small" style="margin-right: 36px">
|
||||||
|
<a href="https://www.baidu.com/" target="_blank" class="header-link"
|
||||||
|
><a-icon type="question-circle" :style="color"
|
||||||
|
/></a>
|
||||||
|
<a-popover trigger="click" class="header-link" placement="bottomRight">
|
||||||
|
<div slot="content">
|
||||||
|
<a-tabs default-active-key="1">
|
||||||
|
<a-tab-pane key="1" tab="通知(0)">
|
||||||
|
<a-button type="dashed" block @click="goToNoticePage('notice')">
|
||||||
|
Content of Tab Pane 1
|
||||||
|
</a-button>
|
||||||
|
</a-tab-pane>
|
||||||
|
<a-tab-pane key="2" tab="系统消息(0)" force-render>
|
||||||
|
<a-button type="dashed" block @click="goToNoticePage('message')">
|
||||||
|
Content of Tab Pane 2
|
||||||
|
</a-button>
|
||||||
|
</a-tab-pane>
|
||||||
|
</a-tabs>
|
||||||
|
</div>
|
||||||
|
<a-icon type="bell" :style="color" />
|
||||||
|
</a-popover>
|
||||||
|
<a-dropdown>
|
||||||
|
<a class="ant-dropdown-link" @click="e => e.preventDefault()">
|
||||||
|
<a-avatar
|
||||||
|
class="avatar"
|
||||||
|
style="margin-right: 12px"
|
||||||
|
src="https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png"
|
||||||
|
/>
|
||||||
|
<span :style="color">欢迎您,{{ 'xxx' }}</span>
|
||||||
|
</a>
|
||||||
|
<a-menu slot="overlay">
|
||||||
|
<a-menu-item key="0">
|
||||||
|
<router-link :to="{ name: 'account-center' }">
|
||||||
|
<a-icon type="user" style="margin-right: 8px" />
|
||||||
|
<span>个人中心</span>
|
||||||
|
</router-link>
|
||||||
|
</a-menu-item>
|
||||||
|
<a-menu-item key="1">
|
||||||
|
<!-- <router-link :to="{ name: 'account-settings-notification' }"> -->
|
||||||
|
<router-link :to="{ name: 'account-settings-Index' }">
|
||||||
|
<a-icon type="setting" style="margin-right: 8px" />
|
||||||
|
<span>账户设置</span>
|
||||||
|
</router-link>
|
||||||
|
</a-menu-item>
|
||||||
|
<a-menu-item key="3" @click="systemSetting">
|
||||||
|
<a-icon type="tool" />
|
||||||
|
<span>系统设置</span>
|
||||||
|
</a-menu-item>
|
||||||
|
<a-menu-item key="4" @click="updatePassword">
|
||||||
|
<a-icon type="setting" />
|
||||||
|
<span>密码修改</span>
|
||||||
|
</a-menu-item>
|
||||||
|
<a-menu-item key="5" @click="updateCurrentDepart">
|
||||||
|
<a-icon type="cluster" />
|
||||||
|
<span>切换部门</span>
|
||||||
|
</a-menu-item>
|
||||||
|
</a-menu>
|
||||||
|
</a-dropdown>
|
||||||
|
<a href="javascript:;" class="header-link" @click="handleLogout">
|
||||||
|
<a-icon type="logout" :style="color" />
|
||||||
|
<span :style="color"> 退出登录</span>
|
||||||
|
</a>
|
||||||
|
<user-password ref="userPassword"></user-password>
|
||||||
|
<setting-drawer :visible.sync="visible" />
|
||||||
|
</a-space>
|
||||||
|
</a-layout-header>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import UserPassword from '@/components/UserPassword'
|
||||||
|
import SettingDrawer from '@/components/SettingDrawer'
|
||||||
|
import { mapState } from 'vuex'
|
||||||
|
export default {
|
||||||
|
name: 'Header',
|
||||||
|
props: {
|
||||||
|
collapsed: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
UserPassword,
|
||||||
|
SettingDrawer,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
visible: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
navTheme: state => state.app.navTheme,
|
||||||
|
primaryColor: state => state.app.primaryColor,
|
||||||
|
}),
|
||||||
|
color() {
|
||||||
|
return {
|
||||||
|
color: this.navTheme === 'light' ? '#fff' : '#000',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
goToNoticePage(type) {
|
||||||
|
// if (
|
||||||
|
// this.$route.name === "isps-userAnnouncement" &&
|
||||||
|
// this.$route.query.type === type
|
||||||
|
// ) {
|
||||||
|
// return;
|
||||||
|
// } else {
|
||||||
|
this.$router.push({
|
||||||
|
name: 'isps-userAnnouncement',
|
||||||
|
query: { type },
|
||||||
|
})
|
||||||
|
// }
|
||||||
|
},
|
||||||
|
systemSetting() {
|
||||||
|
this.visible = true
|
||||||
|
},
|
||||||
|
updatePassword() {
|
||||||
|
this.$refs.userPassword.show()
|
||||||
|
},
|
||||||
|
updateCurrentDepart() {
|
||||||
|
this.$message.warning('您尚未设置部门信息!')
|
||||||
|
},
|
||||||
|
handleLogout() {
|
||||||
|
let that = this
|
||||||
|
this.$confirm({
|
||||||
|
title: '提示',
|
||||||
|
content: '真的要注销登录吗 ?',
|
||||||
|
onOk() {
|
||||||
|
that.$store.dispatch('user/logout')
|
||||||
|
},
|
||||||
|
onCancel() {},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
@import '~@/assets/less/common';
|
||||||
|
.trigger {
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: @header-height;
|
||||||
|
padding: 0 24px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trigger:hover {
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-dropdown-link {
|
||||||
|
height: 100%;
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0 12px;
|
||||||
|
color: #2c3e50;
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.header-link {
|
||||||
|
line-height: 3;
|
||||||
|
.ant-dropdown-link();
|
||||||
|
}
|
||||||
|
</style>
|
||||||
178
src/components/SettingDrawer/index.vue
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
<template>
|
||||||
|
<a-drawer width="300" placement="right" @close="onClose" :closable="false" :visible="visible">
|
||||||
|
<div :style="{ marginBottom: '24px' }">
|
||||||
|
<h3>整体风格设置</h3>
|
||||||
|
|
||||||
|
<div class="setting-drawer-index-blockChecbox">
|
||||||
|
<a-tooltip>
|
||||||
|
<template slot="title"> 暗色菜单风格 </template>
|
||||||
|
<div class="setting-drawer-index-item" @click="handleNavTheme('dark')">
|
||||||
|
<img
|
||||||
|
src="https://gw.alipayobjects.com/zos/rmsportal/LCkqqYNmvBEbokSDscrm.svg"
|
||||||
|
alt="dark"
|
||||||
|
/>
|
||||||
|
<div class="setting-drawer-index-selectIcon" v-if="navTheme === 'dark'">
|
||||||
|
<a-icon type="check" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-tooltip>
|
||||||
|
|
||||||
|
<a-tooltip>
|
||||||
|
<template slot="title"> 亮色菜单风格 </template>
|
||||||
|
<div class="setting-drawer-index-item" @click="handleNavTheme('light')">
|
||||||
|
<img
|
||||||
|
src="https://gw.alipayobjects.com/zos/rmsportal/jpRkZQMyYRryryPNtyIC.svg"
|
||||||
|
alt="light"
|
||||||
|
/>
|
||||||
|
<div class="setting-drawer-index-selectIcon" v-if="navTheme === 'light'">
|
||||||
|
<a-icon type="check" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3>主题色</h3>
|
||||||
|
<div style="height: 20px">
|
||||||
|
<a-tooltip
|
||||||
|
class="setting-drawer-theme-color-colorBlock"
|
||||||
|
v-for="(item, index) in colorList"
|
||||||
|
:key="index"
|
||||||
|
>
|
||||||
|
<template slot="title">
|
||||||
|
{{ item.key }}
|
||||||
|
</template>
|
||||||
|
<a-tag :color="item.color" @click="changeColor(item.color)">
|
||||||
|
<a-icon type="check" v-if="item.color === primaryColor"></a-icon>
|
||||||
|
</a-tag>
|
||||||
|
</a-tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a-divider style="margin-top: 24px" />
|
||||||
|
<div :style="{ marginBottom: '24px' }">
|
||||||
|
<h3>其他设置</h3>
|
||||||
|
<div>
|
||||||
|
<a-list :split="false">
|
||||||
|
<a-list-item>
|
||||||
|
<a-switch slot="actions" size="small" :defaultChecked="multiTab" @change="onMultiTab" />
|
||||||
|
<a-list-item-meta>
|
||||||
|
<div slot="title">多页签模式</div>
|
||||||
|
</a-list-item-meta>
|
||||||
|
</a-list-item>
|
||||||
|
</a-list>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a-divider />
|
||||||
|
<div v-if="showTip">
|
||||||
|
<a-button @click="doCopy" icon="copy" block>打印配置到控制台</a-button>
|
||||||
|
<div :style="{ marginBottom: '24px' }">
|
||||||
|
<a-alert type="warning">
|
||||||
|
<span slot="message">
|
||||||
|
配置栏只在开发环境用于预览,生产环境不会展现,请手动修改配置文件
|
||||||
|
<a
|
||||||
|
href="https://github.com/sendya/ant-design-pro-vue/blob/master/src/config/defaultSettings.js"
|
||||||
|
target="_blank"
|
||||||
|
>src/config/defaultSettings.js</a
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
</a-alert>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-drawer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapState } from 'vuex'
|
||||||
|
import { colorList } from './settingConfig'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'SettingDrawer',
|
||||||
|
props: {
|
||||||
|
visible: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
colorList,
|
||||||
|
showTip: process.env.NODE_ENV === 'development',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
primaryColor: state => state.app.primaryColor,
|
||||||
|
multiTab: state => state.app.multiTab,
|
||||||
|
navTheme: state => state.app.navTheme,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onClose() {
|
||||||
|
this.$emit('update:visible', false)
|
||||||
|
},
|
||||||
|
changeColor(color) {
|
||||||
|
if (this.primaryColor !== color) this.$store.dispatch('app/toggleColor', color)
|
||||||
|
},
|
||||||
|
handleNavTheme(navTheme) {
|
||||||
|
this.$store.commit('app/toggleNavTheme', navTheme)
|
||||||
|
},
|
||||||
|
onMultiTab(isMultiTab) {
|
||||||
|
this.$store.commit('app/setMultiTab', isMultiTab)
|
||||||
|
},
|
||||||
|
doCopy() {
|
||||||
|
const text = `export default {
|
||||||
|
primaryColor: '${this.primaryColor}', // primary color of ant design
|
||||||
|
navTheme: '${this.navTheme}', // theme for nav menu
|
||||||
|
multiTab: ${this.multiTab},
|
||||||
|
production: process.env.NODE_ENV === 'production' && process.env.VUE_APP_PREVIEW !== 'true'}`
|
||||||
|
console.log(text)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.setting-drawer-index-blockChecbox {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.setting-drawer-index-item {
|
||||||
|
margin-right: 16px;
|
||||||
|
position: relative;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-drawer-index-selectIcon {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 100%;
|
||||||
|
padding-top: 15px;
|
||||||
|
padding-left: 24px;
|
||||||
|
height: 100%;
|
||||||
|
color: #1890ff;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-drawer-theme-color-colorBlock {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 2px;
|
||||||
|
float: left;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-right: 8px;
|
||||||
|
padding-left: 0px;
|
||||||
|
padding-right: 0px;
|
||||||
|
text-align: center;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
97
src/components/SettingDrawer/settingConfig.js
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import message from 'ant-design-vue/es/message'
|
||||||
|
|
||||||
|
let lessNodesAppended
|
||||||
|
const colorList = [
|
||||||
|
{
|
||||||
|
key: '薄暮',
|
||||||
|
color: '#F5222D',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '火山',
|
||||||
|
color: '#FA541C',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '日暮',
|
||||||
|
color: '#FAAD14',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '明青',
|
||||||
|
color: '#13C2C2',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '极光绿',
|
||||||
|
color: '#52C41A',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '拂晓蓝(默认)',
|
||||||
|
color: '#1890FF',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '极客蓝',
|
||||||
|
color: '#2F54EB',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '酱紫',
|
||||||
|
color: '#722ED1',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const updateTheme = primaryColor => {
|
||||||
|
// Don't compile less in production!
|
||||||
|
/* if (process.env.NODE_ENV === 'production') {
|
||||||
|
return;
|
||||||
|
} */
|
||||||
|
// Determine if the component is remounted
|
||||||
|
if (!primaryColor) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const hideMessage = message.loading('正在编译主题!', 3)
|
||||||
|
console.info(`正在编译主题!`)
|
||||||
|
function buildIt() {
|
||||||
|
// 正确的判定less是否已经加载less.modifyVars可用
|
||||||
|
if (!window.less || !window.less.modifyVars) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// less.modifyVars可用
|
||||||
|
window.less
|
||||||
|
.modifyVars({
|
||||||
|
'@primary-color': primaryColor,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
hideMessage()
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
message.error('Failed to update theme')
|
||||||
|
hideMessage()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (!lessNodesAppended) {
|
||||||
|
// insert less.js and color.less
|
||||||
|
const lessStyleNode = document.createElement('link')
|
||||||
|
const lessConfigNode = document.createElement('script')
|
||||||
|
const lessScriptNode = document.createElement('script')
|
||||||
|
lessStyleNode.setAttribute('rel', 'stylesheet/less')
|
||||||
|
lessStyleNode.setAttribute('href', '/color.less')
|
||||||
|
lessConfigNode.innerHTML = `
|
||||||
|
window.less = {
|
||||||
|
async: true,
|
||||||
|
env: 'production',
|
||||||
|
javascriptEnabled: true
|
||||||
|
};
|
||||||
|
`
|
||||||
|
lessScriptNode.src = 'https://gw.alipayobjects.com/os/lib/less.js/3.8.1/less.min.js'
|
||||||
|
lessScriptNode.async = true
|
||||||
|
lessScriptNode.onload = () => {
|
||||||
|
buildIt()
|
||||||
|
lessScriptNode.onload = null
|
||||||
|
}
|
||||||
|
document.body.appendChild(lessStyleNode)
|
||||||
|
document.body.appendChild(lessConfigNode)
|
||||||
|
document.body.appendChild(lessScriptNode)
|
||||||
|
lessNodesAppended = true
|
||||||
|
} else {
|
||||||
|
buildIt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { updateTheme, colorList }
|
||||||
71
src/components/SvgIcon.vue
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="externalIcon"
|
||||||
|
:style="styleExternalIcon"
|
||||||
|
class="svg-external-icon svg-icon"
|
||||||
|
v-on="$listeners"
|
||||||
|
/>
|
||||||
|
<svg v-else :class="svgClass" aria-hidden="true" v-on="$listeners">
|
||||||
|
<use :xlink:href="iconName" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// doc: https://panjiachen.github.io/vue-element-admin-site/feature/component/svg-icon.html#usage
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'SvgIcon',
|
||||||
|
props: {
|
||||||
|
iconClass: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
className: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
externalIcon() {
|
||||||
|
return this.isExternal(this.iconClass)
|
||||||
|
},
|
||||||
|
iconName() {
|
||||||
|
return `#icon-${this.iconClass}`
|
||||||
|
},
|
||||||
|
svgClass() {
|
||||||
|
if (this.className) {
|
||||||
|
return 'svg-icon ' + this.className
|
||||||
|
} else {
|
||||||
|
return 'svg-icon'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
styleExternalIcon() {
|
||||||
|
return {
|
||||||
|
mask: `url(${this.iconClass}) no-repeat 50% 50%`,
|
||||||
|
'-webkit-mask': `url(${this.iconClass}) no-repeat 50% 50%`,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
isExternal(path) {
|
||||||
|
return /^(https?:|mailto:|tel:)/.test(path)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.svg-icon {
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
vertical-align: -0.15em;
|
||||||
|
fill: currentColor;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.svg-external-icon {
|
||||||
|
background-color: currentColor;
|
||||||
|
mask-size: cover !important;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
227
src/components/TopTabs.vue
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<!-- 右键操作菜单 -->
|
||||||
|
<contextmenu
|
||||||
|
:itemList="menuItemList"
|
||||||
|
:visible.sync="menuVisible"
|
||||||
|
:position="menuPosition"
|
||||||
|
@select="onMenuSelect"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<a-tabs
|
||||||
|
v-model="activeKey"
|
||||||
|
type="editable-card"
|
||||||
|
@edit="onEdit"
|
||||||
|
@contextmenu.native="onContextmenu"
|
||||||
|
:hideAdd="true"
|
||||||
|
>
|
||||||
|
<a-tab-pane v-for="pane in panes" :key="pane.key" :closable="pane.closable">
|
||||||
|
<span slot="tab" :paneKey="pane.key">{{ pane.title }}</span>
|
||||||
|
{{ pane.content }}
|
||||||
|
</a-tab-pane>
|
||||||
|
</a-tabs>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Contextmenu from '@/components/menu/Contextmenu'
|
||||||
|
import { mapGetters } from 'vuex'
|
||||||
|
export default {
|
||||||
|
name: 'TopTabs',
|
||||||
|
props: {
|
||||||
|
isShow: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
Contextmenu,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
const panes = [{ title: '首页', key: '/home', closable: false }]
|
||||||
|
const menuItemList = [
|
||||||
|
{ key: '4', icon: 'reload', text: '刷 新' },
|
||||||
|
{ key: '1', icon: 'arrow-left', text: '关闭左侧' },
|
||||||
|
{ key: '2', icon: 'arrow-right', text: '关闭右侧' },
|
||||||
|
{ key: '3', icon: 'close', text: '关闭其它' },
|
||||||
|
]
|
||||||
|
return {
|
||||||
|
activeKey: panes[0].key,
|
||||||
|
panes,
|
||||||
|
contextPaneKey: '',
|
||||||
|
menuVisible: false,
|
||||||
|
menuItemList,
|
||||||
|
menuPosition: {
|
||||||
|
left: '0px',
|
||||||
|
top: '0px',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters({
|
||||||
|
mapMenu: 'user/mapMenu',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
$route: {
|
||||||
|
immediate: true,
|
||||||
|
handler(newRoute) {
|
||||||
|
// const parentResource = this.findParentResource(
|
||||||
|
// newRoute.path,
|
||||||
|
// this.routesResource,
|
||||||
|
// {}
|
||||||
|
// );
|
||||||
|
// const currentResource = this.mapMenu[newRoute.path]
|
||||||
|
// if (currentResource.alwaysShow) {
|
||||||
|
// const pane = this.panes.find(item =>
|
||||||
|
// currentResource.children.some(child => child.path === item.key)
|
||||||
|
// )
|
||||||
|
// if (!pane) {
|
||||||
|
// this.panes.push({
|
||||||
|
// title: currentResource.meta.title,
|
||||||
|
// key: newRoute.path,
|
||||||
|
// })
|
||||||
|
// } else {
|
||||||
|
// pane.key = newRoute.path
|
||||||
|
// }
|
||||||
|
// } else if (!this.panes.find(item => item.key === newRoute.path)) {
|
||||||
|
// this.panes.push({ title: newRoute.meta.title, key: newRoute.path })
|
||||||
|
// }
|
||||||
|
|
||||||
|
if (!this.panes.find(item => item.key === newRoute.path)) {
|
||||||
|
this.panes.push({ title: newRoute.meta.title, key: newRoute.path })
|
||||||
|
}
|
||||||
|
this.activeKey = newRoute.path
|
||||||
|
},
|
||||||
|
},
|
||||||
|
activeKey(newKey) {
|
||||||
|
// if (this.$route.path !== newKey) {
|
||||||
|
this.$router.push({ path: newKey })
|
||||||
|
// }
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
// findParentResource(path, children, parent) {
|
||||||
|
// for (const item of children) {
|
||||||
|
// if (item.path === path) {
|
||||||
|
// return parent;
|
||||||
|
// } else if (item.children) {
|
||||||
|
// const parentResource = this.findParentResource(
|
||||||
|
// path,
|
||||||
|
// item.children,
|
||||||
|
// item
|
||||||
|
// );
|
||||||
|
// if (parentResource) {
|
||||||
|
// return parentResource;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
onEdit(targetKey, action) {
|
||||||
|
this[action](targetKey)
|
||||||
|
},
|
||||||
|
remove(targetKey) {
|
||||||
|
let activeKey = this.activeKey
|
||||||
|
let lastIndex
|
||||||
|
this.panes.forEach((pane, i) => {
|
||||||
|
if (pane.key === targetKey) {
|
||||||
|
lastIndex = i - 1
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const panes = this.panes.filter(pane => pane.key !== targetKey)
|
||||||
|
if (panes.length && activeKey === targetKey) {
|
||||||
|
if (lastIndex >= 0) {
|
||||||
|
activeKey = panes[lastIndex].key
|
||||||
|
} else {
|
||||||
|
activeKey = panes[0].key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.panes = panes
|
||||||
|
this.activeKey = activeKey
|
||||||
|
},
|
||||||
|
onContextmenu(e) {
|
||||||
|
this.contextPaneKey = this.getPageKey(e.target)
|
||||||
|
if (this.contextPaneKey !== null) {
|
||||||
|
e.preventDefault()
|
||||||
|
this.menuPosition.left = e.clientX + 'px'
|
||||||
|
this.menuPosition.top = e.clientY + 'px'
|
||||||
|
this.menuVisible = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getPageKey(target) {
|
||||||
|
const spans = target.parentElement.querySelectorAll('span[paneKey]')
|
||||||
|
if (spans.length === 1) {
|
||||||
|
return spans[0].getAttribute('paneKey')
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onMenuSelect(key) {
|
||||||
|
let contextIndex, activeIndex
|
||||||
|
this.panes.forEach((pane, i) => {
|
||||||
|
if (pane.key === this.contextPaneKey) {
|
||||||
|
contextIndex = i
|
||||||
|
}
|
||||||
|
if (pane.key === this.activeKey) {
|
||||||
|
activeIndex = i
|
||||||
|
}
|
||||||
|
})
|
||||||
|
switch (key) {
|
||||||
|
case '1':
|
||||||
|
this.closeLeft(contextIndex, activeIndex)
|
||||||
|
break
|
||||||
|
case '2':
|
||||||
|
this.closeRight(contextIndex, activeIndex)
|
||||||
|
break
|
||||||
|
case '3':
|
||||||
|
this.closeOthers(contextIndex, activeIndex)
|
||||||
|
break
|
||||||
|
case '4':
|
||||||
|
this.routeReload()
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
},
|
||||||
|
closeLeft(contextIndex, activeIndex) {
|
||||||
|
if (activeIndex < contextIndex && activeIndex !== 0) {
|
||||||
|
this.activeKey = this.panes[contextIndex].key
|
||||||
|
}
|
||||||
|
this.panes.splice(1, contextIndex - 1)
|
||||||
|
},
|
||||||
|
closeRight(contextIndex, activeIndex) {
|
||||||
|
if (activeIndex > contextIndex) {
|
||||||
|
this.activeKey = this.panes[contextIndex].key
|
||||||
|
}
|
||||||
|
this.panes = this.panes.slice(0, contextIndex + 1)
|
||||||
|
},
|
||||||
|
closeOthers(contextIndex, activeIndex) {
|
||||||
|
if (activeIndex !== contextIndex && activeIndex !== 0) {
|
||||||
|
this.activeKey = this.panes[contextIndex].key
|
||||||
|
}
|
||||||
|
if (contextIndex === 0) {
|
||||||
|
this.panes = [this.panes[0]]
|
||||||
|
} else {
|
||||||
|
this.panes = [this.panes[0], this.panes[contextIndex]]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
routeReload() {
|
||||||
|
this.$emit('update:isShow', false)
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$emit('update:isShow', true)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
/deep/ .ant-tabs-bar.ant-tabs-top-bar.ant-tabs-card-bar {
|
||||||
|
margin-bottom: 0;
|
||||||
|
border-bottom: 1px solid #e8e8e8;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
// /deep/ .ant-tabs.ant-tabs-card > .ant-tabs-bar .ant-tabs-tab-active {
|
||||||
|
// border-color: #e8e8e8 !important;
|
||||||
|
// }
|
||||||
|
</style>
|
||||||
163
src/components/UserPassword.vue
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
<template>
|
||||||
|
<a-modal
|
||||||
|
title="修改密码"
|
||||||
|
:width="800"
|
||||||
|
:visible="visible"
|
||||||
|
:confirmLoading="confirmLoading"
|
||||||
|
@ok="handleOk"
|
||||||
|
@cancel="close"
|
||||||
|
cancelText="关闭"
|
||||||
|
>
|
||||||
|
<a-spin :spinning="confirmLoading">
|
||||||
|
<a-form :form="form">
|
||||||
|
<a-form-item :labelCol="labelCol" :wrapperCol="wrapperCol" label="旧密码">
|
||||||
|
<a-input
|
||||||
|
type="password"
|
||||||
|
placeholder="请输入旧密码"
|
||||||
|
v-decorator="['oldPassword', validatorRules.oldPassword]"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item :labelCol="labelCol" :wrapperCol="wrapperCol" label="新密码">
|
||||||
|
<a-input
|
||||||
|
type="password"
|
||||||
|
placeholder="请输入新密码"
|
||||||
|
v-decorator="['password', validatorRules.password]"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item :labelCol="labelCol" :wrapperCol="wrapperCol" label="确认新密码">
|
||||||
|
<a-input
|
||||||
|
type="password"
|
||||||
|
@blur="handleConfirmBlur"
|
||||||
|
placeholder="请确认新密码"
|
||||||
|
v-decorator="['confirmPassword', validatorRules.confirmPassword]"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</a-spin>
|
||||||
|
</a-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'UserPassword',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
visible: false,
|
||||||
|
confirmLoading: false,
|
||||||
|
validatorRules: {
|
||||||
|
oldPassword: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '请输入旧密码!',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
password: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '请输入新密码!',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: /^(?![0-9]+$)(?![a-zA-Z]+$)(?!([^0-9a-zA-Z])+$)\S{6,20}$/,
|
||||||
|
message:
|
||||||
|
'6-20位字符,只能包含大小写字母、数字及标点符号(除空格),大小写字母·数字和标点符号至少包含2种',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
validator: this.validateToNextPassword,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
confirmPassword: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '请确认新密码!',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: /^(?![0-9]+$)(?![a-zA-Z]+$)(?!([^0-9a-zA-Z])+$)\S{6,20}$/,
|
||||||
|
message:
|
||||||
|
'6-20位字符,只能包含大小写字母、数字及标点符号(除空格),大小写字母·数字和标点符号至少包含2种',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
validator: this.compareToFirstPassword,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
confirmDirty: false,
|
||||||
|
labelCol: {
|
||||||
|
sm: { span: 5 },
|
||||||
|
},
|
||||||
|
wrapperCol: {
|
||||||
|
sm: { span: 16 },
|
||||||
|
},
|
||||||
|
form: this.$form.createForm(this),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
show() {
|
||||||
|
this.form.resetFields()
|
||||||
|
this.visible = true
|
||||||
|
},
|
||||||
|
close() {
|
||||||
|
this.visible = false
|
||||||
|
},
|
||||||
|
handleOk() {
|
||||||
|
// 触发表单验证
|
||||||
|
this.form.validateFields((err, values) => {
|
||||||
|
if (!err) {
|
||||||
|
console.log(values)
|
||||||
|
this.confirmLoading = true
|
||||||
|
new Promise(resolve => {
|
||||||
|
setTimeout(
|
||||||
|
() =>
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
message: '修改密码成功',
|
||||||
|
}),
|
||||||
|
3000
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.then(res => {
|
||||||
|
if (res.success) {
|
||||||
|
console.log(res)
|
||||||
|
this.$message.success(res.message)
|
||||||
|
this.close()
|
||||||
|
} else {
|
||||||
|
this.$message.warning(res.message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.confirmLoading = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
validateToNextPassword(rule, value, callback) {
|
||||||
|
const form = this.form
|
||||||
|
if (value && this.confirmDirty) {
|
||||||
|
form.validateFields(['confirm'], { force: true })
|
||||||
|
}
|
||||||
|
callback()
|
||||||
|
},
|
||||||
|
compareToFirstPassword(rule, value, callback) {
|
||||||
|
const form = this.form
|
||||||
|
if (value && value !== form.getFieldValue('password')) {
|
||||||
|
callback('两次输入的密码不一样!')
|
||||||
|
} else {
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleConfirmBlur(e) {
|
||||||
|
const value = e.target.value
|
||||||
|
this.confirmDirty = this.confirmDirty || !!value
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
21
src/components/layouts/BlankLayout.vue
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<keep-alive>
|
||||||
|
<router-view v-if="keepAlive"></router-view>
|
||||||
|
</keep-alive>
|
||||||
|
<router-view v-if="!keepAlive"></router-view>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'BlankLayout',
|
||||||
|
computed: {
|
||||||
|
keepAlive() {
|
||||||
|
return this.$route.meta.keepAlive
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style></style>
|
||||||
161
src/components/layouts/GlobalLayout.vue
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
<template>
|
||||||
|
<a-layout id="components-layout-demo-custom-trigger">
|
||||||
|
<a-layout-sider v-model="collapsed" :trigger="null" collapsible>
|
||||||
|
<div>
|
||||||
|
<div class="logo" :style="logoNavTheme">
|
||||||
|
<img src="@/assets/logo.png" alt="logo" />
|
||||||
|
<transition name="slide-fade">
|
||||||
|
<span v-if="!collapsed"> Logo </span>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
<side-menu :collapsed="collapsed" class="side-menu" />
|
||||||
|
</div>
|
||||||
|
</a-layout-sider>
|
||||||
|
<a-layout>
|
||||||
|
<Header :collapsed.sync="collapsed" />
|
||||||
|
<a-layout-content>
|
||||||
|
<TopTabs :isShow.sync="isShow" v-if="$store.state.app.multiTab" />
|
||||||
|
<div
|
||||||
|
:class="
|
||||||
|
$store.state.app.multiTab
|
||||||
|
? 'router-wrapper-1-height-top-tabs'
|
||||||
|
: 'router-wrapper-1-height-no-top-tabs'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div v-if="isShow">
|
||||||
|
<keep-alive>
|
||||||
|
<router-view v-if="keepAlive"></router-view>
|
||||||
|
</keep-alive>
|
||||||
|
<router-view v-if="!keepAlive"></router-view>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-layout-content>
|
||||||
|
</a-layout>
|
||||||
|
</a-layout>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import SideMenu from '@/components/menu/SideMenu'
|
||||||
|
import Header from '@/components/Header'
|
||||||
|
import TopTabs from '@/components/TopTabs'
|
||||||
|
import { mapState } from 'vuex'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'GlobalLayout',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isShow: true,
|
||||||
|
collapsed: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
SideMenu,
|
||||||
|
Header,
|
||||||
|
TopTabs,
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
primaryColor: state => state.app.primaryColor,
|
||||||
|
navTheme: state => state.app.navTheme,
|
||||||
|
menu: state => state.user.menu,
|
||||||
|
}),
|
||||||
|
logoNavTheme() {
|
||||||
|
if (this.navTheme === 'dark') {
|
||||||
|
return {
|
||||||
|
background: '#002140',
|
||||||
|
color: 'white',
|
||||||
|
borderRight: '1px #002140 solid',
|
||||||
|
borderBottom: '1px #002140 solid',
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
background: this.primaryColor,
|
||||||
|
color: 'white',
|
||||||
|
// borderRight: `1px #f0f2f5 solid`,
|
||||||
|
borderBottom: `1px #f0f2f5 solid`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
keepAlive() {
|
||||||
|
return this.$route.matched[1].meta?.keepAlive
|
||||||
|
},
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
if (this.primaryColor !== '#1890FF') {
|
||||||
|
this.$store.dispatch('app/toggleColor', this.primaryColor)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
@import '../~@/assets/less/common';
|
||||||
|
|
||||||
|
#components-layout-demo-custom-trigger {
|
||||||
|
// height: 32px;
|
||||||
|
// background: rgba(255, 255, 255, 0.2);
|
||||||
|
// margin: 16px;
|
||||||
|
.logo {
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
height: @header-height+1px;
|
||||||
|
line-height: @header-height+1px;
|
||||||
|
padding-left: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 20px;
|
||||||
|
img {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-layout-sider .side-menu {
|
||||||
|
height: calc(100vh - (@header-height + 1px));
|
||||||
|
overflow: auto;
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ant-layout-header {
|
||||||
|
height: @header-height;
|
||||||
|
line-height: @header-height;
|
||||||
|
background: #fff;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.ant-layout-content {
|
||||||
|
margin: @gap 0 @gap @gap;
|
||||||
|
padding: @gap 0 0 @gap;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.router-wrapper-1-height-top-tabs {
|
||||||
|
height: calc(100vh - @header-height - @tab-height - @gap * 3);
|
||||||
|
overflow: auto;
|
||||||
|
padding: @router-wrapper-1-padding-y 16px;
|
||||||
|
}
|
||||||
|
.router-wrapper-1-height-no-top-tabs {
|
||||||
|
height: calc(100vh - @header-height - @gap * 3);
|
||||||
|
overflow: auto;
|
||||||
|
padding: @router-wrapper-1-padding-y 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-fade-enter-active {
|
||||||
|
animation: slide-fade 0.5s;
|
||||||
|
}
|
||||||
|
.slide-fade-leave-active {
|
||||||
|
animation: slide-fade 0.5s reverse;
|
||||||
|
}
|
||||||
|
@keyframes slide-fade {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(20px);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.5;
|
||||||
|
transform: translateX(10px);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
80
src/components/layouts/SideTabsLayout.vue
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
<template>
|
||||||
|
<!-- <div> -->
|
||||||
|
<a-tabs
|
||||||
|
v-model="activeKey"
|
||||||
|
tab-position="left"
|
||||||
|
:forceRender="false"
|
||||||
|
:class="$store.state.app.multiTab ? 'ant-tabs-top-tabs' : 'ant-tabs-no-top-tabs'"
|
||||||
|
>
|
||||||
|
<a-tab-pane v-for="pane in children" :key="pane.path" :tab="pane.meta.title"
|
||||||
|
><div
|
||||||
|
:class="
|
||||||
|
$store.state.app.multiTab ? 'router-wrapper-2-top-tabs' : 'router-wrapper-2-no-top-tabs'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<!-- {{ pane.meta.title }}
|
||||||
|
<div v-if="pane.meta.title === '基本设置'">
|
||||||
|
<a-input></a-input>
|
||||||
|
<p v-for="i in 50" :key="i">基本设置{{ i }}</p>
|
||||||
|
</div>
|
||||||
|
{{ pane.path }} -->
|
||||||
|
<component :is="pane.path"></component>
|
||||||
|
</div>
|
||||||
|
</a-tab-pane>
|
||||||
|
</a-tabs>
|
||||||
|
<!-- </div> -->
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex'
|
||||||
|
export default {
|
||||||
|
name: 'SideTabsLayout',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
activeKey: '',
|
||||||
|
children: [],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters({
|
||||||
|
mapMenu: 'user/mapMenu',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
// const path = this.$route.matched.find(
|
||||||
|
// item => item.components.default.name === this.$options.name
|
||||||
|
// ).path
|
||||||
|
const path = this.$route.path
|
||||||
|
// this.children = this.findChildrenResource(path, this.routesResource) || [];
|
||||||
|
this.children = this.mapMenu[path].children
|
||||||
|
|
||||||
|
// 根据子菜单, 添加组件到components, 并根据tab栏加载对应的组件
|
||||||
|
let components = {}
|
||||||
|
for (const key of this.children) {
|
||||||
|
components[key.path] = () => import(`@/views/${key.component}`)
|
||||||
|
}
|
||||||
|
this.$options.components = Object.assign(this.$options.components, components)
|
||||||
|
|
||||||
|
if (this.children.length > 0) {
|
||||||
|
this.activeKey = this.children[0].path
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// methods: {
|
||||||
|
// findChildrenResource(path, children) {
|
||||||
|
// for (const item of children) {
|
||||||
|
// if (item.path === path) {
|
||||||
|
// return item.children;
|
||||||
|
// } else if (item.children) {
|
||||||
|
// const children = this.findChildrenResource(path, item.children);
|
||||||
|
// if (children) {
|
||||||
|
// return children;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
@import '../~@/assets/less/common';
|
||||||
|
.layout-content-height();
|
||||||
|
</style>
|
||||||
71
src/components/layouts/UserLayout.vue
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
<template>
|
||||||
|
<div id="userLayout">
|
||||||
|
<div class="container">
|
||||||
|
<div class="top">
|
||||||
|
<div class="header">
|
||||||
|
<a href="/">
|
||||||
|
<img src="~@/assets/logo.png" class="logo" alt="logo" />
|
||||||
|
<span class="title">Ant Design</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="desc">Ant Design 是西湖区最具影响力的 Web 设计规范</div>
|
||||||
|
</div>
|
||||||
|
<router-view />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'UserLayout',
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
#userLayout {
|
||||||
|
height: 100%;
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100%;
|
||||||
|
background: #f0f2f5 url(~@/assets/background.svg) no-repeat 50%;
|
||||||
|
background-size: 100%;
|
||||||
|
padding: 110px 0 144px;
|
||||||
|
position: relative;
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.top {
|
||||||
|
text-align: center;
|
||||||
|
.header {
|
||||||
|
height: 44px;
|
||||||
|
line-height: 44px;
|
||||||
|
.logo {
|
||||||
|
height: 44px;
|
||||||
|
vertical-align: top;
|
||||||
|
margin-right: 16px;
|
||||||
|
border-style: none;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
font-size: 33px;
|
||||||
|
color: rgba(0, 0, 0, 0.85);
|
||||||
|
font-family: Avenir, 'Helvetica Neue', Arial, Helvetica, sans-serif;
|
||||||
|
font-weight: 600;
|
||||||
|
position: relative;
|
||||||
|
top: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.desc {
|
||||||
|
font-size: 14px;
|
||||||
|
color: rgba(0, 0, 0, 0.45);
|
||||||
|
margin-top: 12px;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.main {
|
||||||
|
min-width: 260px;
|
||||||
|
width: 368px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
66
src/components/menu/Contextmenu.vue
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
<template>
|
||||||
|
<a-menu
|
||||||
|
:style="{ left: position.left, top: position.top }"
|
||||||
|
class="contextmenu"
|
||||||
|
v-show="visible"
|
||||||
|
@click="handleClick"
|
||||||
|
:selectedKeys="[]"
|
||||||
|
>
|
||||||
|
<a-menu-item :key="item.key" v-for="item in itemList">
|
||||||
|
<a-icon role="menuitemicon" v-if="item.icon" :type="item.icon" />{{ item.text }}
|
||||||
|
</a-menu-item>
|
||||||
|
</a-menu>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'Contextmenu',
|
||||||
|
props: {
|
||||||
|
visible: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
position: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({ left: '0px', top: '0px' }),
|
||||||
|
},
|
||||||
|
itemList: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
window.addEventListener('mousedown', this.closeMenu)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
closeMenu(e) {
|
||||||
|
if (!e.target.closest('.contextmenu')) {
|
||||||
|
this.$emit('update:visible', false)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleClick({ key }) {
|
||||||
|
this.$emit('select', key)
|
||||||
|
this.$emit('update:visible', false)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.contextmenu {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 999;
|
||||||
|
// border: 1px solid #9e9e9e;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 2px 2px 10px #aaaaaa !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contextmenu /deep/ .ant-menu-item:hover {
|
||||||
|
// background: @primary-color;
|
||||||
|
background: #f2f2f2;
|
||||||
|
// color: white;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
130
src/components/menu/SideMenu.js
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
import Menu from 'ant-design-vue/es/menu'
|
||||||
|
import { mapState } from 'vuex'
|
||||||
|
|
||||||
|
const { Item, SubMenu } = Menu
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'SideMenu',
|
||||||
|
props: {
|
||||||
|
collapsed: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
openKeys: [],
|
||||||
|
selectedKeys: [],
|
||||||
|
cachedOpenKeys: [],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.updateMenu()
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
rootSubmenuKeys() {
|
||||||
|
return this.menu.map(item => item.path)
|
||||||
|
},
|
||||||
|
...mapState({
|
||||||
|
menu: state => state.user.menu,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
$route: function() {
|
||||||
|
this.updateMenu()
|
||||||
|
},
|
||||||
|
collapsed: function(newValue) {
|
||||||
|
this.openKeys = newValue ? [] : this.cachedOpenKeys
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onOpenChange(openKeys) {
|
||||||
|
const latestOpenKey = openKeys.find(key => this.openKeys.indexOf(key) === -1)
|
||||||
|
if (this.rootSubmenuKeys.indexOf(latestOpenKey) === -1) {
|
||||||
|
this.openKeys = openKeys
|
||||||
|
} else {
|
||||||
|
this.openKeys = latestOpenKey ? [latestOpenKey] : []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateMenu() {
|
||||||
|
const routes = this.$route.matched.concat()
|
||||||
|
this.selectedKeys = [routes.pop().path]
|
||||||
|
// 如果父路由有redirectDefaultChild参数, 说明必须让父路由高亮, 父路由的上级展开
|
||||||
|
if (routes[routes.length - 1].meta.redirectDefaultChild) {
|
||||||
|
this.selectedKeys = [routes.pop().path]
|
||||||
|
}
|
||||||
|
this.cachedOpenKeys = routes.map(item => item.path)
|
||||||
|
this.openKeys = this.collapsed ? [] : this.cachedOpenKeys
|
||||||
|
// setTimeout(() => {
|
||||||
|
// document
|
||||||
|
// .querySelector(".side-menu .ant-menu-item-selected")
|
||||||
|
// ?.closest(".side-menu>[role='menuitem']")
|
||||||
|
// .scrollIntoView();
|
||||||
|
// }, 1000);
|
||||||
|
},
|
||||||
|
|
||||||
|
// render
|
||||||
|
renderItem(menu) {
|
||||||
|
if (menu.hidden) {
|
||||||
|
return null
|
||||||
|
} else {
|
||||||
|
return menu.children && !menu.alwaysShow && !menu.meta.redirectDefaultChild
|
||||||
|
? this.renderSubMenu(menu)
|
||||||
|
: this.renderMenuItem(menu)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
renderMenuItem(menu) {
|
||||||
|
let menuItem
|
||||||
|
if (menu.meta?.url) {
|
||||||
|
menuItem = (
|
||||||
|
<a href={menu.meta.url} target="_blank">
|
||||||
|
{this.renderIcon(menu.meta.icon)}
|
||||||
|
<span>{menu.meta.title}</span>
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
menuItem = (
|
||||||
|
<RouterLink to={{ path: menu.path }}>
|
||||||
|
{this.renderIcon(menu.meta.icon)}
|
||||||
|
<span>{menu.meta.title}</span>
|
||||||
|
</RouterLink>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return <Item key={menu.path}>{menuItem}</Item>
|
||||||
|
},
|
||||||
|
renderSubMenu(menu) {
|
||||||
|
return (
|
||||||
|
<SubMenu key={menu.path}>
|
||||||
|
<span slot="title">
|
||||||
|
{this.renderIcon(menu.meta.icon)}
|
||||||
|
<span>{menu.meta.title}</span>
|
||||||
|
</span>
|
||||||
|
{menu.children.map(item => this.renderItem(item))}
|
||||||
|
</SubMenu>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
renderIcon(icon) {
|
||||||
|
if (!icon) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return <a-icon type={icon} />
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const menuTree = this.menu.map(item => {
|
||||||
|
return this.renderItem(item)
|
||||||
|
})
|
||||||
|
return (
|
||||||
|
<Menu
|
||||||
|
theme={this.$store.state.app.navTheme}
|
||||||
|
mode="inline"
|
||||||
|
vModel={this.selectedKeys}
|
||||||
|
openKeys={this.openKeys}
|
||||||
|
on={{ openChange: this.onOpenChange }}>
|
||||||
|
{menuTree}
|
||||||
|
</Menu>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
||||||
13
src/config/defaultSettings.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
export default {
|
||||||
|
primaryColor: '#1890FF', // primary color of ant design
|
||||||
|
navTheme: 'dark',
|
||||||
|
multiTab: true, // 默认多页签模式
|
||||||
|
// fixedHeader: false, // sticky header
|
||||||
|
// fixSiderbar: false, // sticky siderbar
|
||||||
|
|
||||||
|
storageOptions: {
|
||||||
|
namespace: 'vuejs__', // key prefix
|
||||||
|
name: 'ls', // name variable Vue.[ls] or this.[$ls],
|
||||||
|
storage: 'local', // storage name session, local, memory
|
||||||
|
},
|
||||||
|
}
|
||||||
48
src/main.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import Vue from 'vue'
|
||||||
|
import App from './App.vue'
|
||||||
|
import router from './router'
|
||||||
|
import store from './store'
|
||||||
|
|
||||||
|
import Storage from 'vue-ls'
|
||||||
|
|
||||||
|
import './utils/lazy/lazyAntd'
|
||||||
|
import './assets/icons'
|
||||||
|
import defaultSettings from '@/config/defaultSettings'
|
||||||
|
import auth from '@/utils/permission'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If you don't want to use mock-server
|
||||||
|
* you want to use MockJs for mock api
|
||||||
|
* you can execute: mockXHR()
|
||||||
|
*
|
||||||
|
* Currently MockJs will be used in the production environment,
|
||||||
|
* please remove it before going online ! ! !
|
||||||
|
*/
|
||||||
|
if (process.env.NODE_ENV === 'production') {
|
||||||
|
const { mockXHR } = require('../mock')
|
||||||
|
mockXHR()
|
||||||
|
}
|
||||||
|
|
||||||
|
Vue.use(Storage, defaultSettings.storageOptions)
|
||||||
|
|
||||||
|
Vue.directive('auth', auth)
|
||||||
|
|
||||||
|
new Vue({
|
||||||
|
router,
|
||||||
|
store,
|
||||||
|
mounted() {
|
||||||
|
// store.commit(
|
||||||
|
// "app/toggleFixedHeader",
|
||||||
|
// Vue.ls.get("fixedHeader", defaultSettings.fixedHeader)
|
||||||
|
// );
|
||||||
|
// store.commit(
|
||||||
|
// "app/toggleFixedSiderbar",
|
||||||
|
// Vue.ls.get("fixedSiderbar", defaultSettings.fixSiderbar)
|
||||||
|
// );
|
||||||
|
store.commit('app/toggleColor', Vue.ls.get('primaryColor', defaultSettings.primaryColor))
|
||||||
|
store.commit('user/setToken', Vue.ls.get('token'))
|
||||||
|
store.commit('app/toggleNavTheme', Vue.ls.get('navTheme', defaultSettings.navTheme))
|
||||||
|
store.commit('app/setMultiTab', Vue.ls.get('multiTab', defaultSettings.multiTab))
|
||||||
|
},
|
||||||
|
render: h => h(App),
|
||||||
|
}).$mount('#app')
|
||||||
52
src/router/generateIndexRouter.js
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
export function generateIndexRouter(data) {
|
||||||
|
let indexRouter = [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'root',
|
||||||
|
component: () =>
|
||||||
|
import(/* webpackChunkName: "Layout" */ '@/components/layouts/GlobalLayout.vue'),
|
||||||
|
redirect: '/home',
|
||||||
|
children: generateChildRouters(data),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '*',
|
||||||
|
redirect: '/404',
|
||||||
|
hidden: true,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
return indexRouter
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成嵌套路由(子路由)
|
||||||
|
function generateChildRouters(data) {
|
||||||
|
const routers = []
|
||||||
|
for (const item of data) {
|
||||||
|
let componentPath = ''
|
||||||
|
if (item.component.indexOf('layouts') >= 0) {
|
||||||
|
componentPath = 'components/' + item.component
|
||||||
|
} else {
|
||||||
|
componentPath = 'views/' + item.component
|
||||||
|
}
|
||||||
|
let menu = {
|
||||||
|
path: item.path,
|
||||||
|
name: item.name,
|
||||||
|
// component: resolve => require(['@/' + componentPath + '.vue'], resolve),
|
||||||
|
component: () => import(`@/${componentPath}.vue`),
|
||||||
|
redirect: item.redirect || null,
|
||||||
|
meta: { ...item.meta },
|
||||||
|
}
|
||||||
|
if (item.alwaysShow) {
|
||||||
|
menu.component = () => import('@/components/layouts/SideTabsLayout.vue')
|
||||||
|
menu.redirect = null
|
||||||
|
}
|
||||||
|
if (item.meta.url) {
|
||||||
|
menu.component = null
|
||||||
|
}
|
||||||
|
if (item.children?.length > 0 && !item.alwaysShow) {
|
||||||
|
// if (item.children?.length > 0) {
|
||||||
|
menu.children = [...generateChildRouters(item.children)]
|
||||||
|
}
|
||||||
|
routers.push(menu)
|
||||||
|
}
|
||||||
|
return routers
|
||||||
|
}
|
||||||
124
src/router/index.js
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
import Vue from 'vue'
|
||||||
|
import VueRouter from 'vue-router'
|
||||||
|
import store from '@/store'
|
||||||
|
import { generateIndexRouter } from './generateIndexRouter'
|
||||||
|
import UserLayout from '@/components/layouts/UserLayout'
|
||||||
|
|
||||||
|
import NProgress from 'nprogress'
|
||||||
|
import 'nprogress/nprogress.css'
|
||||||
|
|
||||||
|
NProgress.configure({ showSpinner: false })
|
||||||
|
|
||||||
|
// hack router push callback
|
||||||
|
const originalPush = VueRouter.prototype.push
|
||||||
|
VueRouter.prototype.push = function push(location, onResolve, onReject) {
|
||||||
|
if (onResolve || onReject) return originalPush.call(this, location, onResolve, onReject)
|
||||||
|
return originalPush.call(this, location).catch(err => err)
|
||||||
|
}
|
||||||
|
|
||||||
|
Vue.use(VueRouter)
|
||||||
|
|
||||||
|
const whiteList = ['/user/login', '/user/register', '/user/register-result', '/user/alteration']
|
||||||
|
const constantRoutes = [
|
||||||
|
{
|
||||||
|
path: '/user',
|
||||||
|
component: UserLayout,
|
||||||
|
redirect: '/user/login',
|
||||||
|
hidden: true,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '/user/login',
|
||||||
|
name: 'login',
|
||||||
|
component: () => import(/* webpackChunkName: "user" */ '@/views/user/Login'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/404',
|
||||||
|
component: () => import(/* webpackChunkName: "404" */ '@/views/exception/404'),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const router = new VueRouter({
|
||||||
|
routes: constantRoutes,
|
||||||
|
// scrollBehavior(to, from, savedPosition) {
|
||||||
|
// if (savedPosition && to.meta.keepAlive) {
|
||||||
|
// return savedPosition
|
||||||
|
// } else {
|
||||||
|
// return {
|
||||||
|
// x: 0,
|
||||||
|
// y: 0,
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
})
|
||||||
|
|
||||||
|
let asyncRoutes = []
|
||||||
|
router.beforeEach((to, from, next) => {
|
||||||
|
NProgress.start()
|
||||||
|
/**
|
||||||
|
* 1.有token
|
||||||
|
* |-- 1.1 登录页 ==> 跳转首页
|
||||||
|
* |-- 1.2 非登录页 ==> 判断是否有权限菜单
|
||||||
|
* |-- 1.2.1 没有权限菜单 ==> 获取权限, 生成动态路由, 获取用户信息, 放行
|
||||||
|
* |-- 1.2.2 有权限菜单 ==> 放行
|
||||||
|
* 2. 没有token
|
||||||
|
* |-- 2.1 在白名单 ==> 放行
|
||||||
|
* |-- 2.2 不在白名单 ==> 跳转登录页
|
||||||
|
*/
|
||||||
|
if (store.state.user.token || Vue.ls.get('token')) {
|
||||||
|
console.log('打印', Vue.ls.get('token'))
|
||||||
|
/* has token */
|
||||||
|
hasToken(to, from, next)
|
||||||
|
} else {
|
||||||
|
if (whiteList.indexOf(to.path) !== -1) {
|
||||||
|
next()
|
||||||
|
} else {
|
||||||
|
next({ path: '/user/login' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function hasToken(to, from, next) {
|
||||||
|
if (to.path === '/user/login') {
|
||||||
|
next({ path: '/home' })
|
||||||
|
} else {
|
||||||
|
if (!store.state.user.menu[0]) {
|
||||||
|
generateAsyncRouters(to, from, next)
|
||||||
|
} else {
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateAsyncRouters(to, from, next) {
|
||||||
|
store
|
||||||
|
.dispatch('user/getPermissionList')
|
||||||
|
.then(menu => {
|
||||||
|
asyncRoutes = generateIndexRouter(menu)
|
||||||
|
router.addRoutes(asyncRoutes)
|
||||||
|
store.dispatch('user/getUserInfo')
|
||||||
|
next({ path: to.path })
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.log(err)
|
||||||
|
Vue.prototype.$error({
|
||||||
|
title: '请求用户信息失败',
|
||||||
|
destroyOnClose: true,
|
||||||
|
onOk: function() {
|
||||||
|
if (from.path === '/user/login') {
|
||||||
|
store.commit('user/setToken', '')
|
||||||
|
window.location.reload()
|
||||||
|
} else {
|
||||||
|
store.dispatch('user/logout')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
router.afterEach(() => {
|
||||||
|
NProgress.done()
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
17
src/store/index.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import Vue from 'vue'
|
||||||
|
import Vuex from 'vuex'
|
||||||
|
|
||||||
|
import user from './modules/user'
|
||||||
|
import app from './modules/app'
|
||||||
|
|
||||||
|
Vue.use(Vuex)
|
||||||
|
|
||||||
|
export default new Vuex.Store({
|
||||||
|
state: {},
|
||||||
|
mutations: {},
|
||||||
|
actions: {},
|
||||||
|
modules: {
|
||||||
|
user: { namespaced: true, ...user },
|
||||||
|
app: { namespaced: true, ...app },
|
||||||
|
},
|
||||||
|
})
|
||||||
48
src/store/modules/app.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import Vue from 'vue'
|
||||||
|
import { updateTheme } from '@/components/SettingDrawer/settingConfig'
|
||||||
|
|
||||||
|
const app = {
|
||||||
|
state: {
|
||||||
|
// fixedHeader: true,
|
||||||
|
// fixSiderbar: true,
|
||||||
|
primaryColor: '',
|
||||||
|
multiTab: true,
|
||||||
|
navTheme: 'dark',
|
||||||
|
},
|
||||||
|
mutations: {
|
||||||
|
// toggleFixedHeader: (state, fixed) => {
|
||||||
|
// Vue.ls.set("fixedHeader", fixed);
|
||||||
|
// state.fixedHeader = fixed;
|
||||||
|
// },
|
||||||
|
// toggleFixedSiderbar: (state, fixed) => {
|
||||||
|
// Vue.ls.set("fixedSiderbar", fixed);
|
||||||
|
// state.fixSiderbar = fixed;
|
||||||
|
// },
|
||||||
|
toggleColor(state, color) {
|
||||||
|
Vue.ls.set('primaryColor', color)
|
||||||
|
state.primaryColor = color
|
||||||
|
},
|
||||||
|
setMultiTab(state, multiTab) {
|
||||||
|
Vue.ls.set('multiTab', multiTab)
|
||||||
|
state.multiTab = multiTab
|
||||||
|
},
|
||||||
|
toggleNavTheme(state, navTheme) {
|
||||||
|
Vue.ls.set('navTheme', navTheme)
|
||||||
|
state.navTheme = navTheme
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
// toggleFixedHeader({ commit }, fixedHeader) {
|
||||||
|
// commit("toggleFixedHeader", fixedHeader);
|
||||||
|
// },
|
||||||
|
// toggleFixedSiderbar({ commit }, fixSiderbar) {
|
||||||
|
// commit("toggleFixedSiderbar", fixSiderbar);
|
||||||
|
// },
|
||||||
|
toggleColor({ commit }, color) {
|
||||||
|
updateTheme(color)
|
||||||
|
commit('toggleColor', color)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default app
|
||||||
119
src/store/modules/user.js
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
import Vue from 'vue'
|
||||||
|
import { getUserInfo, getUserPermission } from '@/api/user'
|
||||||
|
|
||||||
|
// 递归添加每级路由到mapMenu根目录, 并添加父路由
|
||||||
|
function handleMenu(mockMenu = [], mapMenu = {}, parent) {
|
||||||
|
for (const item of mockMenu) {
|
||||||
|
item.parent = parent
|
||||||
|
mapMenu[item.path] = item
|
||||||
|
if (item.children) {
|
||||||
|
handleMenu(item.children, mapMenu, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mapMenu
|
||||||
|
}
|
||||||
|
|
||||||
|
let user = {
|
||||||
|
state: {
|
||||||
|
isAdmin: false,
|
||||||
|
username: '',
|
||||||
|
avatar: '',
|
||||||
|
info: {},
|
||||||
|
token: '',
|
||||||
|
menu: [], // 菜单
|
||||||
|
buttonAuth: [], // 权限: 新增/删除/编辑, 按钮
|
||||||
|
},
|
||||||
|
|
||||||
|
getters: {
|
||||||
|
mapMenu: state => {
|
||||||
|
if (!state.menu[0]) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return handleMenu(state.menu)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
mutations: {
|
||||||
|
setToken(state, value) {
|
||||||
|
state.token = value
|
||||||
|
Vue.ls.set('token', value)
|
||||||
|
},
|
||||||
|
// 保存用户菜单
|
||||||
|
saveMenuList(state, value) {
|
||||||
|
state.menu = value
|
||||||
|
},
|
||||||
|
saveButtonAuth(state, value) {
|
||||||
|
let actionList = value.map(i => i.action)
|
||||||
|
state.buttonAuth = actionList
|
||||||
|
// 把按钮/权限存在section, 退出登录时记得清空
|
||||||
|
sessionStorage.setItem('buttonAuth', JSON.stringify(actionList))
|
||||||
|
},
|
||||||
|
saveIsAdmin(state, value) {
|
||||||
|
state.isAdmin = value
|
||||||
|
if (value) {
|
||||||
|
state.buttonAuth.unshift('admin')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
saveUserName(state, value) {
|
||||||
|
state.username = value
|
||||||
|
},
|
||||||
|
saveAvatar(state, value) {
|
||||||
|
state.avatar = value
|
||||||
|
},
|
||||||
|
saveInfo(state, value) {
|
||||||
|
state.info = value
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
logout({ commit }) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
commit('setToken', '')
|
||||||
|
commit('saveMenuList', [])
|
||||||
|
window.sessionStorage.clear()
|
||||||
|
window.location.reload()
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取用户权限
|
||||||
|
async getPermissionList({ commit }) {
|
||||||
|
const { errcode, data } = await getUserPermission()
|
||||||
|
if (errcode === 0 && data) {
|
||||||
|
const { menu, buttonAuth } = data
|
||||||
|
// 如果二菜单全部是隐藏的, 那么一级菜单也隐藏
|
||||||
|
if (menu && menu.length > 0) {
|
||||||
|
menu.forEach(item => {
|
||||||
|
if (item['children']) {
|
||||||
|
let hasChildrenMenu = item['children'].filter(i => {
|
||||||
|
return !i.hidden || i.hidden == false
|
||||||
|
})
|
||||||
|
if (hasChildrenMenu == null || hasChildrenMenu.length == 0) {
|
||||||
|
item['hidden'] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
commit('saveMenuList', menu)
|
||||||
|
commit('saveButtonAuth', buttonAuth)
|
||||||
|
return new Promise(resolve => {
|
||||||
|
resolve(menu)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async getUserInfo({ commit }) {
|
||||||
|
const { errcode, data } = await getUserInfo()
|
||||||
|
if (errcode === 0 && data) {
|
||||||
|
let { isAdmin, username, avatar, info } = data
|
||||||
|
commit('saveIsAdmin', isAdmin)
|
||||||
|
commit('saveUserName', username)
|
||||||
|
commit('saveAvatar', avatar)
|
||||||
|
commit('saveInfo', info)
|
||||||
|
return new Promise(resolve => resolve())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default user
|
||||||
67
src/utils/lazy/icons.js
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
// 注意: 路径分Outline, Fill, 和 twotone
|
||||||
|
// bell
|
||||||
|
export { default as BellOutline } from '@ant-design/icons/lib/outline/BellOutline'
|
||||||
|
// setting
|
||||||
|
export { default as SettingOutline } from '@ant-design/icons/lib/outline/SettingOutline'
|
||||||
|
// tool
|
||||||
|
export { default as ToolOutline } from '@ant-design/icons/lib/outline/ToolOutline'
|
||||||
|
// cluster
|
||||||
|
export { default as ClusterOutline } from '@ant-design/icons/lib/outline/ClusterOutline'
|
||||||
|
// logout
|
||||||
|
export { default as LogoutOutline } from '@ant-design/icons/lib/outline/LogoutOutline'
|
||||||
|
// reload
|
||||||
|
export { default as ReloadOutline } from '@ant-design/icons/lib/outline/ReloadOutline'
|
||||||
|
// arrow-left
|
||||||
|
export { default as ArrowLeftOutline } from '@ant-design/icons/lib/outline/ArrowLeftOutline'
|
||||||
|
// arrow-right
|
||||||
|
export { default as ArrowRightOutline } from '@ant-design/icons/lib/outline/ArrowRightOutline'
|
||||||
|
// close
|
||||||
|
export { default as CloseOutline } from '@ant-design/icons/lib/outline/CloseOutline'
|
||||||
|
// check
|
||||||
|
export { default as CheckOutline } from '@ant-design/icons/lib/outline/CheckOutline'
|
||||||
|
// user
|
||||||
|
export { default as UserOutline } from '@ant-design/icons/lib/outline/UserOutline'
|
||||||
|
// lock
|
||||||
|
export { default as LockOutline } from '@ant-design/icons/lib/outline/LockOutline'
|
||||||
|
// smile
|
||||||
|
export { default as SmileOutline } from '@ant-design/icons/lib/outline/SmileOutline'
|
||||||
|
// mobile
|
||||||
|
export { default as MobileOutline } from '@ant-design/icons/lib/outline/MobileOutline'
|
||||||
|
// mail
|
||||||
|
export { default as MailOutline } from '@ant-design/icons/lib/outline/MailOutline'
|
||||||
|
// wechat
|
||||||
|
export { default as WechatOutline } from '@ant-design/icons/lib/outline/WechatOutline'
|
||||||
|
// dingding
|
||||||
|
export { default as DingdingOutline } from '@ant-design/icons/lib/outline/DingdingOutline'
|
||||||
|
// menu-fold
|
||||||
|
export { default as MenuFoldOutline } from '@ant-design/icons/lib/outline/MenuFoldOutline'
|
||||||
|
// menu-unfold
|
||||||
|
export { default as MenuUnfoldOutline } from '@ant-design/icons/lib/outline/MenuUnfoldOutline'
|
||||||
|
// info-circle *
|
||||||
|
export { default as InfoCircleFill } from '@ant-design/icons/lib/fill/InfoCircleFill'
|
||||||
|
// check-circle *
|
||||||
|
export { default as CheckCircleOutline } from '@ant-design/icons/lib/outline/CheckCircleOutline'
|
||||||
|
export { default as CheckCircleFill } from '@ant-design/icons/lib/fill/CheckCircleFill'
|
||||||
|
// close-circle *
|
||||||
|
export { default as CloseCircleOutline } from '@ant-design/icons/lib/outline/CloseCircleOutline'
|
||||||
|
export { default as CloseCircleFill } from '@ant-design/icons/lib/fill/CloseCircleFill'
|
||||||
|
// exclamation-circle *
|
||||||
|
export { default as ExclamationCircleFill } from '@ant-design/icons/lib/fill/ExclamationCircleFill'
|
||||||
|
// loading
|
||||||
|
export { default as LoadingOutline } from '@ant-design/icons/lib/outline/LoadingOutline'
|
||||||
|
// question-circle *
|
||||||
|
export { default as QuestionCircleOutline } from '@ant-design/icons/lib/outline/QuestionCircleOutline'
|
||||||
|
export { default as QuestionCircleFill } from '@ant-design/icons/lib/fill/QuestionCircleFill'
|
||||||
|
|
||||||
|
// home
|
||||||
|
export { default as HomeOutline } from '@ant-design/icons/lib/outline/HomeOutline'
|
||||||
|
// github
|
||||||
|
export { default as GithubOutline } from '@ant-design/icons/lib/outline/GithubOutline'
|
||||||
|
// table
|
||||||
|
export { default as TableOutline } from '@ant-design/icons/lib/outline/TableOutline'
|
||||||
|
// profile
|
||||||
|
export { default as ProfileOutline } from '@ant-design/icons/lib/outline/ProfileOutline'
|
||||||
|
// gold
|
||||||
|
export { default as GoldOutline } from '@ant-design/icons/lib/outline/GoldOutline'
|
||||||
|
// dashboard
|
||||||
|
export { default as DashboardOutline } from '@ant-design/icons/lib/outline/DashboardOutline'
|
||||||
71
src/utils/lazy/lazyAntd.js
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import Vue from 'vue'
|
||||||
|
|
||||||
|
// base library
|
||||||
|
import {
|
||||||
|
ConfigProvider,
|
||||||
|
Layout,
|
||||||
|
Input,
|
||||||
|
Button,
|
||||||
|
Switch,
|
||||||
|
Checkbox,
|
||||||
|
Form,
|
||||||
|
Row,
|
||||||
|
Col,
|
||||||
|
Modal,
|
||||||
|
Tabs,
|
||||||
|
Icon,
|
||||||
|
Popover,
|
||||||
|
Dropdown,
|
||||||
|
List,
|
||||||
|
Avatar,
|
||||||
|
Spin,
|
||||||
|
Menu,
|
||||||
|
Drawer,
|
||||||
|
Tooltip,
|
||||||
|
Alert,
|
||||||
|
Tag,
|
||||||
|
Divider,
|
||||||
|
Progress,
|
||||||
|
Result,
|
||||||
|
message,
|
||||||
|
notification,
|
||||||
|
Space,
|
||||||
|
} from 'ant-design-vue'
|
||||||
|
|
||||||
|
Vue.use(ConfigProvider)
|
||||||
|
Vue.use(Layout)
|
||||||
|
Vue.use(Input)
|
||||||
|
Vue.use(Button)
|
||||||
|
Vue.use(Switch)
|
||||||
|
Vue.use(Checkbox)
|
||||||
|
Vue.use(Form)
|
||||||
|
Vue.use(Row)
|
||||||
|
Vue.use(Col)
|
||||||
|
Vue.use(Modal)
|
||||||
|
Vue.use(Tabs)
|
||||||
|
Vue.use(Icon)
|
||||||
|
Vue.use(Popover)
|
||||||
|
Vue.use(Dropdown)
|
||||||
|
Vue.use(List)
|
||||||
|
Vue.use(Avatar)
|
||||||
|
Vue.use(Spin)
|
||||||
|
Vue.use(Menu)
|
||||||
|
Vue.use(Drawer)
|
||||||
|
Vue.use(Tooltip)
|
||||||
|
Vue.use(Alert)
|
||||||
|
Vue.use(Tag)
|
||||||
|
Vue.use(Divider)
|
||||||
|
Vue.use(Progress)
|
||||||
|
Vue.use(Result)
|
||||||
|
Vue.use(Space)
|
||||||
|
|
||||||
|
Vue.prototype.$confirm = Modal.confirm
|
||||||
|
Vue.prototype.$message = message
|
||||||
|
Vue.prototype.$notification = notification
|
||||||
|
Vue.prototype.$info = Modal.info
|
||||||
|
Vue.prototype.$success = Modal.success
|
||||||
|
Vue.prototype.$error = Modal.error
|
||||||
|
Vue.prototype.$warning = Modal.warning
|
||||||
|
|
||||||
|
process.env.NODE_ENV !== 'production' &&
|
||||||
|
console.warn('[jeecg-boot-vue] NOTICE: Antd use lazy-load.')
|
||||||
28
src/utils/permission.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import store from '@/store'
|
||||||
|
|
||||||
|
const disableClickFn = event => {
|
||||||
|
event && event.stopImmediatePropagation()
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
inserted(el, binding) {
|
||||||
|
// if (store.state.user.isAdmin) return
|
||||||
|
const { value } = binding
|
||||||
|
const buttonAuth = store.state.user.buttonAuth
|
||||||
|
if (buttonAuth && buttonAuth instanceof Array && value.length > 0) {
|
||||||
|
const permissionRoles = value
|
||||||
|
const hasPermission = buttonAuth.some(button => {
|
||||||
|
return permissionRoles.includes(button)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!hasPermission) {
|
||||||
|
// el.parentNode && el.parentNode.removeChild(el)
|
||||||
|
el.setAttribute('disabled', 'disabled')
|
||||||
|
el.classList.add('permission-disabled')
|
||||||
|
el.addEventListener('click', disableClickFn, true)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(`need roles! Like v-auth="['add','editor']"`)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
44
src/utils/request.js
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
import store from '@/store'
|
||||||
|
import notification from 'ant-design-vue/es/notification'
|
||||||
|
|
||||||
|
const axiosInstance = axios.create({
|
||||||
|
baseURL: process.env.VUE_APP_API_BASE_URL,
|
||||||
|
timeout: 6000,
|
||||||
|
})
|
||||||
|
|
||||||
|
const errorHandler = err => {
|
||||||
|
notification.error({
|
||||||
|
message: '请求错误',
|
||||||
|
description: err.message,
|
||||||
|
})
|
||||||
|
return { err }
|
||||||
|
}
|
||||||
|
|
||||||
|
axiosInstance.interceptors.request.use(
|
||||||
|
config => {
|
||||||
|
// 头部增加 token
|
||||||
|
if (store.state.user.token) {
|
||||||
|
config.headers.Authorization = 'Bearer ' + store.state.user.token
|
||||||
|
}
|
||||||
|
|
||||||
|
return config
|
||||||
|
},
|
||||||
|
err => {
|
||||||
|
return Promise.reject(err)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
axiosInstance.interceptors.response.use(response => {
|
||||||
|
// 响应没有data属性,说明网络请求失败
|
||||||
|
if (!response.data) {
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
if (response.data.errcode) {
|
||||||
|
// 如果response.data.errcode有值并且不为0的时候返回一个包含错误信息的对象
|
||||||
|
return errorHandler({ message: response.data.errmsg })
|
||||||
|
}
|
||||||
|
return response.data
|
||||||
|
}, errorHandler)
|
||||||
|
|
||||||
|
export default axiosInstance
|
||||||
10
src/views/About.vue
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<template>
|
||||||
|
<div class="about">
|
||||||
|
<h1>This is an about page</h1>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'About',
|
||||||
|
}
|
||||||
|
</script>
|
||||||
18
src/views/Home.vue
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<template>
|
||||||
|
<div style="height: 1000px">
|
||||||
|
<p><svg-icon style="font-size: 18px; color: pink" icon-class="develop" />首页(内容区滚动)</p>
|
||||||
|
<a-input placeholder="Basic usage" style="width: 200px" />
|
||||||
|
<h3 style="margin-top: 20px">按钮权限控制</h3>
|
||||||
|
<a-button v-auth="['admin']">管理员</a-button>
|
||||||
|
<a-button v-auth="['user:add']" style="margin-left: 20px">添加用户</a-button>
|
||||||
|
<a-button v-auth="['user:edit']" style="margin-left: 20px">编辑用户</a-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'Home',
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style></style>
|
||||||
9
src/views/account/Center.vue
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<div>个人中心页</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style></style>
|
||||||
24
src/views/account/settings/BaseSetting.vue
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<a-input></a-input>
|
||||||
|
<p v-for="i in 1000" :key="i">基本设置</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
mounted() {
|
||||||
|
// document.getElementById(this.$route.path).addEventListener('scroll', () => {
|
||||||
|
// console.log('滚动事件')
|
||||||
|
// console.log(document.getElementById(this.$route.path).scrollTop)
|
||||||
|
// })
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
// console.log('销毁基本设置组件')
|
||||||
|
// // console.log(document.getElementById(this.$route.path).scrollTop)
|
||||||
|
// this.$attrs.updateScrollTop(this.$route.path)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style></style>
|
||||||
12
src/views/account/settings/Binding.vue
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<a-input></a-input>
|
||||||
|
<p v-for="i in 1000" :key="i">账号绑定</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style></style>
|
||||||
9
src/views/account/settings/Custom.vue
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<div>个性化设置</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style></style>
|
||||||
9
src/views/account/settings/Notification.vue
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<div>最新消息通知</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style></style>
|
||||||
9
src/views/account/settings/Security.vue
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<div>安全设置</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style></style>
|
||||||
18
src/views/exception/404.vue
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<template>
|
||||||
|
<a-result status="404" title="404" sub-title="Sorry, the page you visited does not exist.">
|
||||||
|
<template #extra>
|
||||||
|
<a-button type="primary" @click="toHome"> Back Home </a-button>
|
||||||
|
</template>
|
||||||
|
</a-result>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'Exception404',
|
||||||
|
methods: {
|
||||||
|
toHome() {
|
||||||
|
this.$router.push({ path: '/' })
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
11
src/views/list/StandardList.vue
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<template>
|
||||||
|
<div>这是标准列表页面</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'StandardList',
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style></style>
|
||||||
11
src/views/list/TableList.vue
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<template>
|
||||||
|
<div>搜索列表三级菜单页面</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'TableList',
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style></style>
|
||||||
14
src/views/modules/online/desform/DesignFormList.vue
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<a-input placeholder="搜索" style="width: 200px" />
|
||||||
|
<a-row v-for="i in 5" :key="i">
|
||||||
|
<router-link :to="{ path: $route.path + '/' + i }">模板{{ i }}</router-link>
|
||||||
|
</a-row>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style></style>
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<div>{{ $route.meta.title }}</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style></style>
|
||||||
13
src/views/oneLevelMenu/index.vue
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<template>
|
||||||
|
<div>一级菜单页面</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
// mounted() {
|
||||||
|
// console.log(this.$router.getRoutes());
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style></style>
|
||||||
9
src/views/profile/Advanced.vue
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<div>高级详情页<a-input placeholder="Basic usage" /></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style></style>
|
||||||
9
src/views/profile/Basic.vue
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<div>基础详情页<a-input placeholder="Basic usage" /></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style></style>
|
||||||
9
src/views/sideTabs/components/Tab1.vue
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<div>tab1<a-input placeholder="Basic usage" /></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style></style>
|
||||||
17
src/views/sideTabs/components/Tab2.vue
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
tab2
|
||||||
|
{{ Array(3000).fill('t2').join(' ') }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
beforeDestroy() {
|
||||||
|
// console.log("tab2即将销毁");
|
||||||
|
// this.$attrs.updateScrollTop("2");
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style></style>
|
||||||
9
src/views/sideTabs/components/Tab3.vue
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<div>tab3<a-input placeholder="Basic usage" /></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style></style>
|
||||||
9
src/views/sideTabs/components/Tab4.vue
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<div>tab4</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style></style>
|
||||||
70
src/views/sideTabs/index.vue
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
<template>
|
||||||
|
<div style="display: flex">
|
||||||
|
<a-tabs
|
||||||
|
v-model="activeKey"
|
||||||
|
tab-position="left"
|
||||||
|
style="flex: none"
|
||||||
|
:class="$store.state.app.multiTab ? 'ant-tabs-top-tabs' : 'ant-tabs-no-top-tabs'"
|
||||||
|
>
|
||||||
|
<!-- @change="onChange" -->
|
||||||
|
<a-tab-pane key="1" tab="Tab1"></a-tab-pane>
|
||||||
|
<a-tab-pane key="2" tab="Tab2"></a-tab-pane>
|
||||||
|
<a-tab-pane key="3" tab="Tab3"></a-tab-pane>
|
||||||
|
<a-tab-pane key="4" tab="Tab4"></a-tab-pane>
|
||||||
|
</a-tabs>
|
||||||
|
<div
|
||||||
|
ref="tabContainer"
|
||||||
|
id="tabContainer"
|
||||||
|
:class="
|
||||||
|
$store.state.app.multiTab ? 'router-wrapper-2-top-tabs' : 'router-wrapper-2-no-top-tabs'
|
||||||
|
"
|
||||||
|
style="backgroun-color: pink; flex: auto"
|
||||||
|
>
|
||||||
|
<component :is="'Tab' + activeKey"></component>
|
||||||
|
<!-- :updateScrollTop="updateScrollTop" -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Tab1 from '@/views/sideTabs/components/Tab1'
|
||||||
|
import Tab2 from '@/views/sideTabs/components/Tab2'
|
||||||
|
import Tab3 from '@/views/sideTabs/components/Tab3'
|
||||||
|
import Tab4 from '@/views/sideTabs/components/Tab4'
|
||||||
|
export default {
|
||||||
|
name: 'SideTab',
|
||||||
|
components: {
|
||||||
|
Tab1,
|
||||||
|
Tab2,
|
||||||
|
Tab3,
|
||||||
|
Tab4,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
activeKey: '1',
|
||||||
|
scrollTop: {
|
||||||
|
Tab1: 0,
|
||||||
|
Tab2: 0,
|
||||||
|
Tab3: 0,
|
||||||
|
Tab4: 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// methods: {
|
||||||
|
// onChange(activeKey) {
|
||||||
|
// console.log("切换到" + activeKey);
|
||||||
|
// this.activeKey = activeKey;
|
||||||
|
// console.log(this.scrollTop["Tab" + activeKey]);
|
||||||
|
// this.$refs.tabContainer.scrollTop = this.scrollTop["Tab" + activeKey];
|
||||||
|
// },
|
||||||
|
// updateScrollTop(activeKey) {
|
||||||
|
// console.log(document.getElementById("tabContainer").scrollTop);
|
||||||
|
// this.scrollTop["Tab" + activeKey] = this.$refs.tabContainer.scrollTop;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
@import '~@/assets/less/common';
|
||||||
|
.layout-content-height();
|
||||||
|
</style>
|
||||||
9
src/views/title/index.vue
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<div>{{ $route.meta.title }}</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style></style>
|
||||||
394
src/views/user/Login.vue
Normal file
@ -0,0 +1,394 @@
|
|||||||
|
<template>
|
||||||
|
<div class="main">
|
||||||
|
<a-form class="user-layout-login" :form="form" @submit="handleSubmit">
|
||||||
|
<a-tabs
|
||||||
|
:activeKey="customActiveKey"
|
||||||
|
:tabBarStyle="{ textAlign: 'center', borderBottom: 'unset' }"
|
||||||
|
@change="handleTabClick"
|
||||||
|
>
|
||||||
|
<a-tab-pane key="tab1" tab="账号密码登录">
|
||||||
|
<a-alert
|
||||||
|
v-if="isLoginError"
|
||||||
|
type="error"
|
||||||
|
showIcon
|
||||||
|
style="margin-bottom: 24px"
|
||||||
|
message="账户或密码错误(admin/ant.design )"
|
||||||
|
/>
|
||||||
|
<a-form-item>
|
||||||
|
<a-input
|
||||||
|
size="large"
|
||||||
|
type="text"
|
||||||
|
placeholder="账户: admin 或者 user"
|
||||||
|
v-decorator="[
|
||||||
|
'username',
|
||||||
|
{
|
||||||
|
rules: [
|
||||||
|
{ required: true, message: '请输入帐户名或邮箱地址' },
|
||||||
|
{ validator: handleUsernameOrEmail },
|
||||||
|
],
|
||||||
|
validateTrigger: 'change',
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<a-icon slot="prefix" type="user" :style="{ color: 'rgba(0,0,0,.25)' }" />
|
||||||
|
</a-input>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item>
|
||||||
|
<a-input-password
|
||||||
|
size="large"
|
||||||
|
placeholder="密码: admin or user"
|
||||||
|
v-decorator="[
|
||||||
|
'password',
|
||||||
|
{
|
||||||
|
rules: [{ required: true, message: '请输入密码' }],
|
||||||
|
validateTrigger: 'blur',
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<a-icon slot="prefix" type="lock" :style="{ color: 'rgba(0,0,0,.25)' }" />
|
||||||
|
</a-input-password>
|
||||||
|
</a-form-item>
|
||||||
|
<a-row :gutter="0">
|
||||||
|
<a-col :span="16">
|
||||||
|
<a-form-item>
|
||||||
|
<a-input
|
||||||
|
v-decorator="[
|
||||||
|
'inputCode',
|
||||||
|
{ rules: [{ required: true, message: '请输入验证码!' }] },
|
||||||
|
]"
|
||||||
|
size="large"
|
||||||
|
type="text"
|
||||||
|
placeholder="请输入验证码"
|
||||||
|
>
|
||||||
|
<a-icon slot="prefix" type="smile" :style="{ color: 'rgba(0,0,0,.25)' }" />
|
||||||
|
</a-input>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="8" style="text-align: right">
|
||||||
|
<img
|
||||||
|
v-if="requestCodeSuccess"
|
||||||
|
style="margin-top: 2px"
|
||||||
|
:src="randCodeImage"
|
||||||
|
@click="handleChangeCheckCode"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
v-else
|
||||||
|
style="margin-top: 2px"
|
||||||
|
src="@/assets/checkcode.png"
|
||||||
|
@click="handleChangeCheckCode"
|
||||||
|
/>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
</a-tab-pane>
|
||||||
|
<a-tab-pane key="tab2" tab="手机号登录">
|
||||||
|
<a-form-item>
|
||||||
|
<a-input
|
||||||
|
size="large"
|
||||||
|
type="text"
|
||||||
|
placeholder="手机号"
|
||||||
|
v-decorator="[
|
||||||
|
'mobile',
|
||||||
|
{
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
pattern: /^1[34578]\d{9}$/,
|
||||||
|
message: '请输入正确的手机号',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
validateTrigger: 'change',
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<a-icon slot="prefix" type="mobile" :style="{ color: 'rgba(0,0,0,.25)' }" />
|
||||||
|
</a-input>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-row :gutter="16">
|
||||||
|
<a-col class="gutter-row" :span="16">
|
||||||
|
<a-form-item>
|
||||||
|
<a-input
|
||||||
|
size="large"
|
||||||
|
type="text"
|
||||||
|
placeholder="验证码"
|
||||||
|
v-decorator="[
|
||||||
|
'captcha',
|
||||||
|
{
|
||||||
|
rules: [{ required: true, message: '请输入验证码' }],
|
||||||
|
validateTrigger: 'blur',
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<a-icon slot="prefix" type="mail" :style="{ color: 'rgba(0,0,0,.25)' }" />
|
||||||
|
</a-input>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col class="gutter-row" :span="8">
|
||||||
|
<a-button
|
||||||
|
class="getCaptcha"
|
||||||
|
tabindex="-1"
|
||||||
|
:disabled="state.smsSendBtn"
|
||||||
|
@click.stop.prevent="getCaptcha"
|
||||||
|
v-text="state.smsSendBtn ? state.time + ' s' : '获取验证码'"
|
||||||
|
></a-button>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
</a-tab-pane>
|
||||||
|
</a-tabs>
|
||||||
|
|
||||||
|
<a-form-item>
|
||||||
|
<a-checkbox v-decorator="['rememberMe', { valuePropName: 'checked' }]">自动登录</a-checkbox>
|
||||||
|
<!-- <router-link
|
||||||
|
:to="{ name: 'recover', params: { user: 'aaa' } }"
|
||||||
|
class="forge-password"
|
||||||
|
style="float: right"
|
||||||
|
>忘记密码</router-link
|
||||||
|
> -->
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item style="margin-top: 24px">
|
||||||
|
<a-button
|
||||||
|
size="large"
|
||||||
|
type="primary"
|
||||||
|
htmlType="submit"
|
||||||
|
class="login-button"
|
||||||
|
:loading="state.loginBtn"
|
||||||
|
:disabled="state.loginBtn"
|
||||||
|
>确定</a-button
|
||||||
|
>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<div class="user-login-other">
|
||||||
|
<span>其他登录方式</span>
|
||||||
|
<a @click="onThirdLogin('github')" title="github"
|
||||||
|
><a-icon class="item-icon" type="github"></a-icon
|
||||||
|
></a>
|
||||||
|
<a @click="onThirdLogin('wechat_enterprise')" title="企业微信"
|
||||||
|
><a-icon class="item-icon" type="wechat"></a-icon
|
||||||
|
></a>
|
||||||
|
<a @click="onThirdLogin('dingtalk')" title="钉钉"
|
||||||
|
><a-icon class="item-icon" type="dingding"></a-icon
|
||||||
|
></a>
|
||||||
|
<!-- <router-link class="register" :to="{ name: 'register' }">注册账户</router-link> -->
|
||||||
|
</div>
|
||||||
|
</a-form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import md5 from 'md5'
|
||||||
|
import { mapActions } from 'vuex'
|
||||||
|
import { login } from '@/api/user'
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
requestCodeSuccess: false,
|
||||||
|
randCodeImage: '',
|
||||||
|
customActiveKey: 'tab1',
|
||||||
|
loginBtn: false,
|
||||||
|
isLoginError: false,
|
||||||
|
form: this.$form.createForm(this),
|
||||||
|
state: {
|
||||||
|
time: 60,
|
||||||
|
loginBtn: false,
|
||||||
|
// login type: 0 email, 1 username, 2 telephone
|
||||||
|
loginType: 0,
|
||||||
|
smsSendBtn: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(['Login', 'Logout']),
|
||||||
|
// handler
|
||||||
|
handleUsernameOrEmail(rule, value, callback) {
|
||||||
|
const { state } = this
|
||||||
|
const regex = /^([a-zA-Z0-9_-])+@([a-zA-Z0-9_-])+((\.[a-zA-Z0-9_-]{2,3}){1,2})$/
|
||||||
|
if (regex.test(value)) {
|
||||||
|
state.loginType = 0
|
||||||
|
} else {
|
||||||
|
state.loginType = 1
|
||||||
|
}
|
||||||
|
callback()
|
||||||
|
},
|
||||||
|
handleTabClick(key) {
|
||||||
|
this.customActiveKey = key
|
||||||
|
// this.form.resetFields()
|
||||||
|
},
|
||||||
|
handleChangeCheckCode() {},
|
||||||
|
onThirdLogin() {},
|
||||||
|
handleSubmit(e) {
|
||||||
|
e.preventDefault()
|
||||||
|
const {
|
||||||
|
form: { validateFields },
|
||||||
|
state,
|
||||||
|
customActiveKey,
|
||||||
|
// Login
|
||||||
|
} = this
|
||||||
|
|
||||||
|
state.loginBtn = true
|
||||||
|
|
||||||
|
const validateFieldsKey =
|
||||||
|
customActiveKey === 'tab1'
|
||||||
|
? ['username', 'password', 'rememberMe']
|
||||||
|
: ['mobile', 'captcha', 'rememberMe']
|
||||||
|
|
||||||
|
validateFields(validateFieldsKey, { force: true }, (err, values) => {
|
||||||
|
if (!err) {
|
||||||
|
console.log('login form', values)
|
||||||
|
let loginParams = { ...values }
|
||||||
|
if (this.customActiveKey === 'tab1') {
|
||||||
|
delete loginParams.username
|
||||||
|
loginParams[!state.loginType ? 'email' : 'username'] = values.username
|
||||||
|
loginParams.password = md5(values.password)
|
||||||
|
}
|
||||||
|
login(loginParams)
|
||||||
|
.then(res => {
|
||||||
|
console.log(res)
|
||||||
|
if (res.errcode === 0) {
|
||||||
|
this.loginSuccess(res.data.token)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => this.requestFailed(err))
|
||||||
|
.finally(() => {
|
||||||
|
state.loginBtn = false
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
setTimeout(() => {
|
||||||
|
state.loginBtn = false
|
||||||
|
}, 600)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取验证码
|
||||||
|
getCaptcha(e) {
|
||||||
|
e.preventDefault()
|
||||||
|
const {
|
||||||
|
form: { validateFields },
|
||||||
|
state,
|
||||||
|
} = this
|
||||||
|
|
||||||
|
validateFields(['mobile'], { force: true }, (err, values) => {
|
||||||
|
if (!err) {
|
||||||
|
console.log(values)
|
||||||
|
state.smsSendBtn = true
|
||||||
|
|
||||||
|
const interval = window.setInterval(() => {
|
||||||
|
if (state.time-- <= 0) {
|
||||||
|
state.time = 60
|
||||||
|
state.smsSendBtn = false
|
||||||
|
window.clearInterval(interval)
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
|
this.$message.loading('验证码发送中..', 3)
|
||||||
|
// const hide = this.$message.loading("验证码发送中..", 0);
|
||||||
|
// getSmsCaptcha({ mobile: values.mobile })
|
||||||
|
// .then(res => {
|
||||||
|
// setTimeout(hide, 2500);
|
||||||
|
// this.$notification["success"]({
|
||||||
|
// message: "提示",
|
||||||
|
// description:
|
||||||
|
// "验证码获取成功,您的验证码为:" + res.result.captcha,
|
||||||
|
// duration: 8
|
||||||
|
// });
|
||||||
|
// })
|
||||||
|
// .catch(err => {
|
||||||
|
// setTimeout(hide, 1);
|
||||||
|
// clearInterval(interval);
|
||||||
|
// state.time = 60;
|
||||||
|
// state.smsSendBtn = false;
|
||||||
|
// this.requestFailed(err);
|
||||||
|
// });
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// 登录成功处理
|
||||||
|
loginSuccess(token) {
|
||||||
|
// check res.homePage define, set $router.push name res.homePage
|
||||||
|
// Why not enter onComplete
|
||||||
|
/*
|
||||||
|
this.$router.push({ name: 'analysis' }, () => {
|
||||||
|
console.log('onComplete')
|
||||||
|
this.$notification.success({
|
||||||
|
message: '欢迎',
|
||||||
|
description: `${timeFix()},欢迎回来`
|
||||||
|
})
|
||||||
|
})
|
||||||
|
*/
|
||||||
|
console.log('登录成功')
|
||||||
|
this.$store.commit('user/setToken', token)
|
||||||
|
this.$router.push({ path: '/' })
|
||||||
|
// 延迟 1 秒显示欢迎信息
|
||||||
|
setTimeout(() => {
|
||||||
|
this.$notification.success({
|
||||||
|
message: '欢迎',
|
||||||
|
description: '欢迎回来',
|
||||||
|
duration: 2,
|
||||||
|
})
|
||||||
|
}, 1000)
|
||||||
|
this.isLoginError = false
|
||||||
|
},
|
||||||
|
|
||||||
|
// 请求失败处理
|
||||||
|
requestFailed(err) {
|
||||||
|
this.isLoginError = true
|
||||||
|
this.$notification.error({
|
||||||
|
message: '错误',
|
||||||
|
description: ((err.response || {}).data || {}).message || '请求出现错误,请稍后再试',
|
||||||
|
duration: 4,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.user-layout-login {
|
||||||
|
label {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.getCaptcha {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.forge-password {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.login-button {
|
||||||
|
padding: 0 15px;
|
||||||
|
font-size: 16px;
|
||||||
|
height: 40px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-login-other {
|
||||||
|
text-align: left;
|
||||||
|
margin-top: 24px;
|
||||||
|
line-height: 22px;
|
||||||
|
|
||||||
|
.item-icon {
|
||||||
|
font-size: 24px;
|
||||||
|
color: rgba(0, 0, 0, 0.2);
|
||||||
|
margin-left: 16px;
|
||||||
|
vertical-align: middle;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.3s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.register {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
87
src/views/user/LoginSimple.vue
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
<template>
|
||||||
|
<a-form
|
||||||
|
id="components-form-demo-normal-login"
|
||||||
|
:form="form"
|
||||||
|
class="login-form"
|
||||||
|
@submit="handleSubmit"
|
||||||
|
>
|
||||||
|
<a-form-item>
|
||||||
|
<a-input
|
||||||
|
v-decorator="[
|
||||||
|
'userName',
|
||||||
|
{
|
||||||
|
rules: [{ required: true, message: 'Please input your username!' }],
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
placeholder="Username"
|
||||||
|
>
|
||||||
|
<a-icon slot="prefix" type="user" style="color: rgba(0, 0, 0, 0.25)" />
|
||||||
|
</a-input>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item>
|
||||||
|
<a-input
|
||||||
|
v-decorator="[
|
||||||
|
'password',
|
||||||
|
{
|
||||||
|
rules: [{ required: true, message: 'Please input your Password!' }],
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
type="password"
|
||||||
|
placeholder="Password"
|
||||||
|
>
|
||||||
|
<a-icon slot="prefix" type="lock" style="color: rgba(0, 0, 0, 0.25)" />
|
||||||
|
</a-input>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item>
|
||||||
|
<a-checkbox
|
||||||
|
v-decorator="[
|
||||||
|
'remember',
|
||||||
|
{
|
||||||
|
valuePropName: 'checked',
|
||||||
|
initialValue: true,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
Remember me
|
||||||
|
</a-checkbox>
|
||||||
|
<a class="login-form-forgot" href=""> Forgot password </a>
|
||||||
|
<a-button type="primary" html-type="submit" class="login-form-button"> Log in </a-button>
|
||||||
|
Or
|
||||||
|
<a href=""> register now! </a>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'Login',
|
||||||
|
beforeCreate() {
|
||||||
|
this.form = this.$form.createForm(this, { name: 'normal_login' })
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleSubmit(e) {
|
||||||
|
e.preventDefault()
|
||||||
|
this.form.validateFields((err, values) => {
|
||||||
|
if (!err) {
|
||||||
|
console.log('Received values of form: ', values)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
#components-form-demo-normal-login.login-form {
|
||||||
|
max-width: 300px;
|
||||||
|
position: fixed;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate3d(-50%, -50%, 0);
|
||||||
|
}
|
||||||
|
#components-form-demo-normal-login .login-form-forgot {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
#components-form-demo-normal-login .login-form-button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
16
test/test.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
var testCase = require("mocha").describe;
|
||||||
|
var pre = require("mocha").before;
|
||||||
|
var assertions = require("mocha").it;
|
||||||
|
var assert = require("chai").assert;
|
||||||
|
|
||||||
|
testCase("Array", function() {
|
||||||
|
pre(function() {
|
||||||
|
// ...
|
||||||
|
});
|
||||||
|
|
||||||
|
testCase("#indexOf()", function() {
|
||||||
|
assertions("should return -1 when not present", function() {
|
||||||
|
assert.equal([1, 2, 3].indexOf(4), -1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
155
vue.config.js
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
'use strict'
|
||||||
|
const path = require('path')
|
||||||
|
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
|
||||||
|
const webpack = require('webpack')
|
||||||
|
const CompressionPlugin = require('compression-webpack-plugin')
|
||||||
|
|
||||||
|
function resolve(dir) {
|
||||||
|
return path.join(__dirname, dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
lintOnSave: process.env.NODE_ENV !== 'production',
|
||||||
|
|
||||||
|
// 如果你不需要生产环境的 source map,可以将其设置为 false 以加速生产环境构建。
|
||||||
|
productionSourceMap: false,
|
||||||
|
|
||||||
|
configureWebpack: config => {
|
||||||
|
// 生产环境取消 console.log
|
||||||
|
if (process.env.NODE_ENV === 'production') {
|
||||||
|
config.optimization.minimizer[0].options.terserOptions.compress.drop_console = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打包文件大小分析
|
||||||
|
if (process.env.NODE_ENV === 'production') {
|
||||||
|
config.plugins.push(new BundleAnalyzerPlugin())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
chainWebpack: config => {
|
||||||
|
// 生产环境,开启js\css压缩
|
||||||
|
if (process.env.NODE_ENV === 'production') {
|
||||||
|
config.plugin('compressionPlugin').use(
|
||||||
|
new CompressionPlugin({
|
||||||
|
test: /\.(js|css|less|html)$/, // 匹配文件名
|
||||||
|
threshold: 10240, // 对超过10k的数据压缩
|
||||||
|
deleteOriginalAssets: false, // 不删除源文件
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
config
|
||||||
|
.plugin('ignore')
|
||||||
|
// 忽略/moment/locale下的所有文件
|
||||||
|
.use(new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/))
|
||||||
|
|
||||||
|
// 按需加载icon
|
||||||
|
config.resolve.alias.set('@ant-design/icons/lib/dist$', resolve('src/utils/lazy/icons.js'))
|
||||||
|
|
||||||
|
// 预加载, 提高首屏速度
|
||||||
|
config.plugin('preload').tap(() => [
|
||||||
|
{
|
||||||
|
rel: 'preload',
|
||||||
|
// to ignore runtime.js
|
||||||
|
// https://github.com/vuejs/vue-cli/blob/dev/packages/@vue/cli-service/lib/config/app.js#L171
|
||||||
|
fileBlacklist: [/\.map$/, /hot-update\.js$/, /runtime\..*\.js$/],
|
||||||
|
include: 'initial',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
config
|
||||||
|
// https://webpack.js.org/configuration/devtool/#development
|
||||||
|
.when(process.env.NODE_ENV === 'development', config =>
|
||||||
|
config.devtool('cheap-module-eval-source-map')
|
||||||
|
)
|
||||||
|
|
||||||
|
// 去掉空闲加载
|
||||||
|
// 空闲加载需要设置 /webpackChunkName: 'chunk-name'/
|
||||||
|
config.plugins.delete('prefetch')
|
||||||
|
|
||||||
|
// set svg-sprite-loader
|
||||||
|
config.module
|
||||||
|
.rule('svg')
|
||||||
|
.exclude.add(resolve('src/assets/icons'))
|
||||||
|
.end()
|
||||||
|
config.module
|
||||||
|
.rule('icons')
|
||||||
|
.test(/\.svg$/)
|
||||||
|
.include.add(resolve('src/assets/icons'))
|
||||||
|
.end()
|
||||||
|
.use('svg-sprite-loader')
|
||||||
|
.loader('svg-sprite-loader')
|
||||||
|
.options({
|
||||||
|
symbolId: 'icon-[name]',
|
||||||
|
})
|
||||||
|
.end()
|
||||||
|
|
||||||
|
config.when(process.env.NODE_ENV !== 'development', config => {
|
||||||
|
config.optimization.splitChunks({
|
||||||
|
chunks: 'all',
|
||||||
|
cacheGroups: {
|
||||||
|
libs: {
|
||||||
|
name: 'chunk-libs',
|
||||||
|
test: /[\\/]node_modules[\\/]/,
|
||||||
|
priority: 10,
|
||||||
|
chunks: 'initial', // 只打包初始时依赖的第三方
|
||||||
|
},
|
||||||
|
antdesignvueUI: {
|
||||||
|
name: 'chunk-antdesignvueUI', // 单独将 antdesignvueUI 拆包
|
||||||
|
priority: 20, // 权重要大于 libs 和 app 不然会被打包进 libs 或者 app
|
||||||
|
test: /[\\/]node_modules[\\/]ant-design-vue[\\/]es(.*)/, // in order to adapt to cnpm
|
||||||
|
},
|
||||||
|
commons: {
|
||||||
|
name: 'chunk-commons',
|
||||||
|
test: resolve('src/components'), // 可自定义拓展你的规则
|
||||||
|
minChunks: 3, // 最小共用次数
|
||||||
|
priority: 5,
|
||||||
|
reuseExistingChunk: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
publicPath: process.env.NODE_ENV === 'production' ? './' : '/',
|
||||||
|
|
||||||
|
// 代理转发
|
||||||
|
devServer: {
|
||||||
|
port: 3005,
|
||||||
|
// open: true, // 自动打开浏览器
|
||||||
|
overlay: {
|
||||||
|
// 编译页面显示错误
|
||||||
|
warnings: false,
|
||||||
|
errors: true,
|
||||||
|
},
|
||||||
|
// proxy: {
|
||||||
|
// ['^' + process.env.VUE_APP_API_BASE_URL]: {
|
||||||
|
// target: 'http://www.baidu.com',
|
||||||
|
// changeOrigin: true,
|
||||||
|
// pathRewrite: {
|
||||||
|
// ['^' + process.env.VUE_APP_API_BASE_URL]:''
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
before: require('./mock/mock-server.js'),
|
||||||
|
},
|
||||||
|
|
||||||
|
// 样式
|
||||||
|
css: {
|
||||||
|
loaderOptions: {
|
||||||
|
less: {
|
||||||
|
// lessOptions: {
|
||||||
|
// If you are using less-loader@5 please spread the lessOptions to options directly
|
||||||
|
modifyVars: {
|
||||||
|
/* less 变量覆盖,用于自定义 ant design 主题 */
|
||||||
|
'primary-color': '#1890FF',
|
||||||
|
'link-color': '#1890FF',
|
||||||
|
'border-radius-base': '2px',
|
||||||
|
},
|
||||||
|
javascriptEnabled: true,
|
||||||
|
// },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
extract: { ignoreOrder: true }, // 忽略mini-css-extract-plugin因为css引入顺序不同警告
|
||||||
|
},
|
||||||
|
}
|
||||||