Compare commits
merge into: qimaohong:main
qimaohong:dev
qimaohong:main
qimaohong:master
qimaohong:milestone-20251117-DeepChart后台一期
qimaohong:milestone-20260103-DeepChart后台二期——市场权限管理
qimaohong:milestone-20260107-用户画像记录
qimaohong:songjie/feature-20260107173846-用户行为统计
pull from: qimaohong:dev
qimaohong:dev
qimaohong:main
qimaohong:master
qimaohong:milestone-20251117-DeepChart后台一期
qimaohong:milestone-20260103-DeepChart后台二期——市场权限管理
qimaohong:milestone-20260107-用户画像记录
qimaohong:songjie/feature-20260107173846-用户行为统计
No commits in common. 'main' and 'dev' have entirely different histories.
108 changed files with 7106 additions and 3732 deletions
-
14.editorconfig
-
7.env.development
-
8.env.production
-
8.env.staging
-
2.env.test
-
4.eslintignore
-
198.eslintrc.js
-
22.gitignore
-
5.travis.yml
-
3.vscode/extensions.json
-
21LICENSE
-
111README-zh.md
-
100README.md
-
14babel.config.js
-
35build/index.js
-
13index.html
-
24jest.config.js
-
9jsconfig.json
-
57mock/index.js
-
81mock/mock-server.js
-
29mock/table.js
-
84mock/user.js
-
25mock/utils.js
-
1971package-lock.json
-
73package.json
-
8postcss.config.js
-
BINpublic/favicon.ico
-
17public/index.html
-
1public/vite.svg
-
17src/App.vue
-
9src/api/table.js
-
24src/api/user.js
-
201src/api/userPermissions.js
-
BINsrc/assets/404_images/404.png
-
BINsrc/assets/404_images/404_cloud.png
-
BINsrc/assets/images/deepChart.png
-
BINsrc/assets/images/login.png
-
1src/assets/vue.svg
-
71src/components/Breadcrumb/index.vue
-
44src/components/Hamburger/index.vue
-
62src/components/SvgIcon/index.vue
-
9src/icons/index.js
-
1src/icons/svg/dashboard.svg
-
1src/icons/svg/example.svg
-
1src/icons/svg/eye-open.svg
-
1src/icons/svg/eye.svg
-
1src/icons/svg/form.svg
-
1src/icons/svg/link.svg
-
1src/icons/svg/nested.svg
-
1src/icons/svg/password.svg
-
1src/icons/svg/table.svg
-
1src/icons/svg/tree.svg
-
1src/icons/svg/user.svg
-
22src/icons/svgo.yml
-
417src/layout/Layout.vue
-
40src/layout/components/AppMain.vue
-
133src/layout/components/Navbar.vue
-
26src/layout/components/Sidebar/FixiOSBug.js
-
41src/layout/components/Sidebar/Item.vue
-
43src/layout/components/Sidebar/Link.vue
-
82src/layout/components/Sidebar/Logo.vue
-
95src/layout/components/Sidebar/SidebarItem.vue
-
56src/layout/components/Sidebar/index.vue
-
3src/layout/components/index.js
-
93src/layout/index.vue
-
45src/layout/mixin/ResizeHandler.js
-
61src/main.js
-
64src/permission.js
-
157src/router/index.js
-
16src/settings.js
-
8src/store/getters.js
-
19src/store/index.js
-
48src/store/modules/app.js
-
32src/store/modules/settings.js
-
97src/store/modules/user.js
-
19src/store/permission.js
-
79src/style.css
-
49src/styles/element-ui.scss
-
65src/styles/index.scss
-
28src/styles/mixin.scss
-
226src/styles/sidebar.scss
-
48src/styles/transition.scss
-
25src/styles/variables.scss
-
15src/utils/auth.js
-
10src/utils/get-page-title.js
-
117src/utils/index.js
-
84src/utils/myAxios.js
-
86src/utils/request.js
-
20src/utils/validate.js
-
228src/views/404.vue
-
402src/views/Login.vue
-
126src/views/UserPermissions/Export.vue
-
223src/views/UserPermissions/LogDeepExplore.vue
-
209src/views/UserPermissions/LogDeepMate.vue
-
223src/views/UserPermissions/LogMarket.vue
-
811src/views/UserPermissions/Market.vue
-
1567src/views/UserPermissions/Module.vue
-
802src/views/UserPermissions/invitedLook.vue
-
5tests/unit/.eslintrc.js
-
98tests/unit/components/Breadcrumb.spec.js
@ -1,14 +0,0 @@ |
|||
# http://editorconfig.org |
|||
root = true |
|||
|
|||
[*] |
|||
charset = utf-8 |
|||
indent_style = space |
|||
indent_size = 2 |
|||
end_of_line = lf |
|||
insert_final_newline = true |
|||
trim_trailing_whitespace = true |
|||
|
|||
[*.md] |
|||
insert_final_newline = false |
|||
trim_trailing_whitespace = false |
|||
@ -1,5 +1,2 @@ |
|||
# just a flag |
|||
ENV = 'development' |
|||
|
|||
# base api |
|||
VUE_APP_BASE_API = '/dev-api' |
|||
VITE_API_BASE_URL = "https://dbqb.legu168.cn/adminApi" |
|||
VITE_API_BASE_URL_LINK = "https://dbqb.legu168.cn/hljw/api/haiwai/user/login_jwcode" |
|||
@ -1,6 +1,2 @@ |
|||
# just a flag |
|||
ENV = 'production' |
|||
|
|||
# base api |
|||
VUE_APP_BASE_API = '/prod-api' |
|||
|
|||
VITE_API_BASE_URL = "https://dcapi.homilychart.com/prod/deepchart" |
|||
VITE_API_BASE_URL_LINK = "https://api.homilychart.com/hljw/api/haiwai/user/login_jwcode" |
|||
@ -1,8 +0,0 @@ |
|||
NODE_ENV = production |
|||
|
|||
# just a flag |
|||
ENV = 'staging' |
|||
|
|||
# base api |
|||
VUE_APP_BASE_API = '/stage-api' |
|||
|
|||
@ -0,0 +1,2 @@ |
|||
VITE_API_BASE_URL = "https://dbqb.legu168.cn/adminApi" |
|||
VITE_API_BASE_URL_LINK = "https://dbqb.legu168.cn/hljw/api/haiwai/user/login_jwcode" |
|||
@ -1,4 +0,0 @@ |
|||
build/*.js |
|||
src/assets |
|||
public |
|||
dist |
|||
@ -1,198 +0,0 @@ |
|||
module.exports = { |
|||
root: true, |
|||
parserOptions: { |
|||
parser: 'babel-eslint', |
|||
sourceType: 'module' |
|||
}, |
|||
env: { |
|||
browser: true, |
|||
node: true, |
|||
es6: true, |
|||
}, |
|||
extends: ['plugin:vue/recommended', 'eslint:recommended'], |
|||
|
|||
// add your custom rules here
|
|||
//it is base on https://github.com/vuejs/eslint-config-vue
|
|||
rules: { |
|||
"vue/max-attributes-per-line": [2, { |
|||
"singleline": 10, |
|||
"multiline": { |
|||
"max": 1, |
|||
"allowFirstLine": false |
|||
} |
|||
}], |
|||
"vue/singleline-html-element-content-newline": "off", |
|||
"vue/multiline-html-element-content-newline":"off", |
|||
"vue/name-property-casing": ["error", "PascalCase"], |
|||
"vue/no-v-html": "off", |
|||
'accessor-pairs': 2, |
|||
'arrow-spacing': [2, { |
|||
'before': true, |
|||
'after': true |
|||
}], |
|||
'block-spacing': [2, 'always'], |
|||
'brace-style': [2, '1tbs', { |
|||
'allowSingleLine': true |
|||
}], |
|||
'camelcase': [0, { |
|||
'properties': 'always' |
|||
}], |
|||
'comma-dangle': [2, 'never'], |
|||
'comma-spacing': [2, { |
|||
'before': false, |
|||
'after': true |
|||
}], |
|||
'comma-style': [2, 'last'], |
|||
'constructor-super': 2, |
|||
'curly': [2, 'multi-line'], |
|||
'dot-location': [2, 'property'], |
|||
'eol-last': 2, |
|||
'eqeqeq': ["error", "always", {"null": "ignore"}], |
|||
'generator-star-spacing': [2, { |
|||
'before': true, |
|||
'after': true |
|||
}], |
|||
'handle-callback-err': [2, '^(err|error)$'], |
|||
'indent': [2, 2, { |
|||
'SwitchCase': 1 |
|||
}], |
|||
'jsx-quotes': [2, 'prefer-single'], |
|||
'key-spacing': [2, { |
|||
'beforeColon': false, |
|||
'afterColon': true |
|||
}], |
|||
'keyword-spacing': [2, { |
|||
'before': true, |
|||
'after': true |
|||
}], |
|||
'new-cap': [2, { |
|||
'newIsCap': true, |
|||
'capIsNew': false |
|||
}], |
|||
'new-parens': 2, |
|||
'no-array-constructor': 2, |
|||
'no-caller': 2, |
|||
'no-console': 'off', |
|||
'no-class-assign': 2, |
|||
'no-cond-assign': 2, |
|||
'no-const-assign': 2, |
|||
'no-control-regex': 0, |
|||
'no-delete-var': 2, |
|||
'no-dupe-args': 2, |
|||
'no-dupe-class-members': 2, |
|||
'no-dupe-keys': 2, |
|||
'no-duplicate-case': 2, |
|||
'no-empty-character-class': 2, |
|||
'no-empty-pattern': 2, |
|||
'no-eval': 2, |
|||
'no-ex-assign': 2, |
|||
'no-extend-native': 2, |
|||
'no-extra-bind': 2, |
|||
'no-extra-boolean-cast': 2, |
|||
'no-extra-parens': [2, 'functions'], |
|||
'no-fallthrough': 2, |
|||
'no-floating-decimal': 2, |
|||
'no-func-assign': 2, |
|||
'no-implied-eval': 2, |
|||
'no-inner-declarations': [2, 'functions'], |
|||
'no-invalid-regexp': 2, |
|||
'no-irregular-whitespace': 2, |
|||
'no-iterator': 2, |
|||
'no-label-var': 2, |
|||
'no-labels': [2, { |
|||
'allowLoop': false, |
|||
'allowSwitch': false |
|||
}], |
|||
'no-lone-blocks': 2, |
|||
'no-mixed-spaces-and-tabs': 2, |
|||
'no-multi-spaces': 2, |
|||
'no-multi-str': 2, |
|||
'no-multiple-empty-lines': [2, { |
|||
'max': 1 |
|||
}], |
|||
'no-native-reassign': 2, |
|||
'no-negated-in-lhs': 2, |
|||
'no-new-object': 2, |
|||
'no-new-require': 2, |
|||
'no-new-symbol': 2, |
|||
'no-new-wrappers': 2, |
|||
'no-obj-calls': 2, |
|||
'no-octal': 2, |
|||
'no-octal-escape': 2, |
|||
'no-path-concat': 2, |
|||
'no-proto': 2, |
|||
'no-redeclare': 2, |
|||
'no-regex-spaces': 2, |
|||
'no-return-assign': [2, 'except-parens'], |
|||
'no-self-assign': 2, |
|||
'no-self-compare': 2, |
|||
'no-sequences': 2, |
|||
'no-shadow-restricted-names': 2, |
|||
'no-spaced-func': 2, |
|||
'no-sparse-arrays': 2, |
|||
'no-this-before-super': 2, |
|||
'no-throw-literal': 2, |
|||
'no-trailing-spaces': 2, |
|||
'no-undef': 2, |
|||
'no-undef-init': 2, |
|||
'no-unexpected-multiline': 2, |
|||
'no-unmodified-loop-condition': 2, |
|||
'no-unneeded-ternary': [2, { |
|||
'defaultAssignment': false |
|||
}], |
|||
'no-unreachable': 2, |
|||
'no-unsafe-finally': 2, |
|||
'no-unused-vars': [2, { |
|||
'vars': 'all', |
|||
'args': 'none' |
|||
}], |
|||
'no-useless-call': 2, |
|||
'no-useless-computed-key': 2, |
|||
'no-useless-constructor': 2, |
|||
'no-useless-escape': 0, |
|||
'no-whitespace-before-property': 2, |
|||
'no-with': 2, |
|||
'one-var': [2, { |
|||
'initialized': 'never' |
|||
}], |
|||
'operator-linebreak': [2, 'after', { |
|||
'overrides': { |
|||
'?': 'before', |
|||
':': 'before' |
|||
} |
|||
}], |
|||
'padded-blocks': [2, 'never'], |
|||
'quotes': [2, 'single', { |
|||
'avoidEscape': true, |
|||
'allowTemplateLiterals': true |
|||
}], |
|||
'semi': [2, 'never'], |
|||
'semi-spacing': [2, { |
|||
'before': false, |
|||
'after': true |
|||
}], |
|||
'space-before-blocks': [2, 'always'], |
|||
'space-before-function-paren': [2, 'never'], |
|||
'space-in-parens': [2, 'never'], |
|||
'space-infix-ops': 2, |
|||
'space-unary-ops': [2, { |
|||
'words': true, |
|||
'nonwords': false |
|||
}], |
|||
'spaced-comment': [2, 'always', { |
|||
'markers': ['global', 'globals', 'eslint', 'eslint-disable', '*package', '!', ','] |
|||
}], |
|||
'template-curly-spacing': [2, 'never'], |
|||
'use-isnan': 2, |
|||
'valid-typeof': 2, |
|||
'wrap-iife': [2, 'any'], |
|||
'yield-star-spacing': [2, 'both'], |
|||
'yoda': [2, 'never'], |
|||
'prefer-const': 2, |
|||
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, |
|||
'object-curly-spacing': [2, 'always', { |
|||
objectsInObjects: false |
|||
}], |
|||
'array-bracket-spacing': [2, 'never'] |
|||
} |
|||
} |
|||
@ -1,16 +1,26 @@ |
|||
.DS_Store |
|||
node_modules/ |
|||
dist/ |
|||
# Logs |
|||
logs |
|||
*.log |
|||
npm-debug.log* |
|||
yarn-debug.log* |
|||
yarn-error.log* |
|||
package-lock.json |
|||
tests/**/coverage/ |
|||
pnpm-debug.log* |
|||
lerna-debug.log* |
|||
|
|||
node_modules |
|||
dist |
|||
dist_development |
|||
dist_production |
|||
dist-ssr |
|||
*.local |
|||
|
|||
# Editor directories and files |
|||
.vscode/* |
|||
!.vscode/extensions.json |
|||
.idea |
|||
.vscode |
|||
.DS_Store |
|||
*.suo |
|||
*.ntvs* |
|||
*.njsproj |
|||
*.sln |
|||
*.sw? |
|||
@ -1,5 +0,0 @@ |
|||
language: node_js |
|||
node_js: 10 |
|||
script: npm run test |
|||
notifications: |
|||
email: false |
|||
@ -0,0 +1,3 @@ |
|||
{ |
|||
"recommendations": ["Vue.volar"] |
|||
} |
|||
@ -1,21 +0,0 @@ |
|||
MIT License |
|||
|
|||
Copyright (c) 2017-present PanJiaChen |
|||
|
|||
Permission is hereby granted, free of charge, to any person obtaining a copy |
|||
of this software and associated documentation files (the "Software"), to deal |
|||
in the Software without restriction, including without limitation the rights |
|||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
|||
copies of the Software, and to permit persons to whom the Software is |
|||
furnished to do so, subject to the following conditions: |
|||
|
|||
The above copyright notice and this permission notice shall be included in all |
|||
copies or substantial portions of the Software. |
|||
|
|||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
|||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
|||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
|||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
|||
SOFTWARE. |
|||
@ -1,111 +0,0 @@ |
|||
# vue-admin-template |
|||
|
|||
> 这是一个极简的 vue admin 管理后台。它只包含了 Element UI & axios & iconfont & permission control & lint,这些搭建后台必要的东西。 |
|||
|
|||
[线上地址](http://panjiachen.github.io/vue-admin-template) |
|||
|
|||
[国内访问](https://panjiachen.gitee.io/vue-admin-template) |
|||
|
|||
目前版本为 `v4.0+` 基于 `vue-cli` 进行构建,若你想使用旧版本,可以切换分支到[tag/3.11.0](https://github.com/PanJiaChen/vue-admin-template/tree/tag/3.11.0),它不依赖 `vue-cli`。 |
|||
|
|||
<p align="center"> |
|||
<b>SPONSORED BY</b> |
|||
</p> |
|||
<p align="center"> |
|||
<a href="https://finclip.com?from=vue_element" title="FinClip" target="_blank"> |
|||
<img height="200px" src="https://gitee.com/panjiachen/gitee-cdn/raw/master/vue%E8%B5%9E%E5%8A%A9.png" title="FinClip"> |
|||
</a> |
|||
</p> |
|||
|
|||
## Extra |
|||
|
|||
如果你想要根据用户角色来动态生成侧边栏和 router,你可以使用该分支[permission-control](https://github.com/PanJiaChen/vue-admin-template/tree/permission-control) |
|||
|
|||
## 相关项目 |
|||
|
|||
- [vue-element-admin](https://github.com/PanJiaChen/vue-element-admin) |
|||
|
|||
- [electron-vue-admin](https://github.com/PanJiaChen/electron-vue-admin) |
|||
|
|||
- [vue-typescript-admin-template](https://github.com/Armour/vue-typescript-admin-template) |
|||
|
|||
- [awesome-project](https://github.com/PanJiaChen/vue-element-admin/issues/2312) |
|||
|
|||
写了一个系列的教程配套文章,如何从零构建后一个完整的后台项目: |
|||
|
|||
- [手摸手,带你用 vue 撸后台 系列一(基础篇)](https://juejin.im/post/59097cd7a22b9d0065fb61d2) |
|||
- [手摸手,带你用 vue 撸后台 系列二(登录权限篇)](https://juejin.im/post/591aa14f570c35006961acac) |
|||
- [手摸手,带你用 vue 撸后台 系列三 (实战篇)](https://juejin.im/post/593121aa0ce4630057f70d35) |
|||
- [手摸手,带你用 vue 撸后台 系列四(vueAdmin 一个极简的后台基础模板,专门针对本项目的文章,算作是一篇文档)](https://juejin.im/post/595b4d776fb9a06bbe7dba56) |
|||
- [手摸手,带你封装一个 vue component](https://segmentfault.com/a/1190000009090836) |
|||
|
|||
## Build Setup |
|||
|
|||
```bash |
|||
# 克隆项目 |
|||
git clone https://github.com/PanJiaChen/vue-admin-template.git |
|||
|
|||
# 进入项目目录 |
|||
cd vue-admin-template |
|||
|
|||
# 安装依赖 |
|||
npm install |
|||
|
|||
# 建议不要直接使用 cnpm 安装以来,会有各种诡异的 bug。可以通过如下操作解决 npm 下载速度慢的问题 |
|||
npm install --registry=https://registry.npm.taobao.org |
|||
|
|||
# 启动服务 |
|||
npm run dev |
|||
``` |
|||
|
|||
浏览器访问 [http://localhost:9528](http://localhost:9528) |
|||
|
|||
## 发布 |
|||
|
|||
```bash |
|||
# 构建测试环境 |
|||
npm run build:stage |
|||
|
|||
# 构建生产环境 |
|||
npm run build:prod |
|||
``` |
|||
|
|||
## 其它 |
|||
|
|||
```bash |
|||
# 预览发布环境效果 |
|||
npm run preview |
|||
|
|||
# 预览发布环境效果 + 静态资源分析 |
|||
npm run preview -- --report |
|||
|
|||
# 代码格式检查 |
|||
npm run lint |
|||
|
|||
# 代码格式检查并自动修复 |
|||
npm run lint -- --fix |
|||
``` |
|||
|
|||
更多信息请参考 [使用文档](https://panjiachen.github.io/vue-element-admin-site/zh/) |
|||
|
|||
## 购买贴纸 |
|||
|
|||
你也可以通过 购买[官方授权的贴纸](https://smallsticker.com/product/vue-element-admin) 的方式来支持 vue-element-admin - 每售出一张贴纸,我们将获得 2 元的捐赠。 |
|||
|
|||
## Demo |
|||
|
|||
 |
|||
|
|||
## Browsers support |
|||
|
|||
Modern browsers and Internet Explorer 10+. |
|||
|
|||
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="IE / Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>IE / Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari | |
|||
| --------- | --------- | --------- | --------- | |
|||
| IE10, IE11, Edge| last 2 versions| last 2 versions| last 2 versions |
|||
|
|||
## License |
|||
|
|||
[MIT](https://github.com/PanJiaChen/vue-admin-template/blob/master/LICENSE) license. |
|||
|
|||
Copyright (c) 2017-present PanJiaChen |
|||
@ -1,99 +1,5 @@ |
|||
# vue-admin-template |
|||
# Vue 3 + Vite |
|||
|
|||
English | [简体中文](./README-zh.md) |
|||
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more. |
|||
|
|||
> A minimal vue admin template with Element UI & axios & iconfont & permission control & lint |
|||
|
|||
**Live demo:** http://panjiachen.github.io/vue-admin-template |
|||
|
|||
|
|||
**The current version is `v4.0+` build on `vue-cli`. If you want to use the old version , you can switch branch to [tag/3.11.0](https://github.com/PanJiaChen/vue-admin-template/tree/tag/3.11.0), it does not rely on `vue-cli`** |
|||
|
|||
<p align="center"> |
|||
<b>SPONSORED BY</b> |
|||
</p> |
|||
<p align="center"> |
|||
<a href="https://finclip.com?from=vue_element" title="FinClip" target="_blank"> |
|||
<img height="200px" src="https://gitee.com/panjiachen/gitee-cdn/raw/master/vue%E8%B5%9E%E5%8A%A9.png" title="FinClip"> |
|||
</a> |
|||
</p> |
|||
|
|||
## Build Setup |
|||
|
|||
```bash |
|||
# clone the project |
|||
git clone https://github.com/PanJiaChen/vue-admin-template.git |
|||
|
|||
# enter the project directory |
|||
cd vue-admin-template |
|||
|
|||
# install dependency |
|||
npm install |
|||
|
|||
# develop |
|||
npm run dev |
|||
``` |
|||
|
|||
This will automatically open http://localhost:9528 |
|||
|
|||
## Build |
|||
|
|||
```bash |
|||
# build for test environment |
|||
npm run build:stage |
|||
|
|||
# build for production environment |
|||
npm run build:prod |
|||
``` |
|||
|
|||
## Advanced |
|||
|
|||
```bash |
|||
# preview the release environment effect |
|||
npm run preview |
|||
|
|||
# preview the release environment effect + static resource analysis |
|||
npm run preview -- --report |
|||
|
|||
# code format check |
|||
npm run lint |
|||
|
|||
# code format check and auto fix |
|||
npm run lint -- --fix |
|||
``` |
|||
|
|||
Refer to [Documentation](https://panjiachen.github.io/vue-element-admin-site/guide/essentials/deploy.html) for more information |
|||
|
|||
## Demo |
|||
|
|||
 |
|||
|
|||
## Extra |
|||
|
|||
If you want router permission && generate menu by user roles , you can use this branch [permission-control](https://github.com/PanJiaChen/vue-admin-template/tree/permission-control) |
|||
|
|||
For `typescript` version, you can use [vue-typescript-admin-template](https://github.com/Armour/vue-typescript-admin-template) (Credits: [@Armour](https://github.com/Armour)) |
|||
|
|||
## Related Project |
|||
|
|||
- [vue-element-admin](https://github.com/PanJiaChen/vue-element-admin) |
|||
|
|||
- [electron-vue-admin](https://github.com/PanJiaChen/electron-vue-admin) |
|||
|
|||
- [vue-typescript-admin-template](https://github.com/Armour/vue-typescript-admin-template) |
|||
|
|||
- [awesome-project](https://github.com/PanJiaChen/vue-element-admin/issues/2312) |
|||
|
|||
## Browsers support |
|||
|
|||
Modern browsers and Internet Explorer 10+. |
|||
|
|||
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="IE / Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>IE / Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari | |
|||
| --------- | --------- | --------- | --------- | |
|||
| IE10, IE11, Edge| last 2 versions| last 2 versions| last 2 versions |
|||
|
|||
## License |
|||
|
|||
[MIT](https://github.com/PanJiaChen/vue-admin-template/blob/master/LICENSE) license. |
|||
|
|||
Copyright (c) 2017-present PanJiaChen |
|||
Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support). |
|||
@ -1,14 +0,0 @@ |
|||
module.exports = { |
|||
presets: [ |
|||
// https://github.com/vuejs/vue-cli/tree/master/packages/@vue/babel-preset-app
|
|||
'@vue/cli-plugin-babel/preset' |
|||
], |
|||
'env': { |
|||
'development': { |
|||
// babel-plugin-dynamic-import-node plugin only does one thing by converting all import() to require().
|
|||
// This plugin can significantly increase the speed of hot updates, when you have a large number of pages.
|
|||
// https://panjiachen.github.io/vue-element-admin-site/guide/advanced/lazy-loading.html
|
|||
'plugins': ['dynamic-import-node'] |
|||
} |
|||
} |
|||
} |
|||
@ -1,35 +0,0 @@ |
|||
const { run } = require('runjs') |
|||
const chalk = require('chalk') |
|||
const config = require('../vue.config.js') |
|||
const rawArgv = process.argv.slice(2) |
|||
const args = rawArgv.join(' ') |
|||
|
|||
if (process.env.npm_config_preview || rawArgv.includes('--preview')) { |
|||
const report = rawArgv.includes('--report') |
|||
|
|||
run(`vue-cli-service build ${args}`) |
|||
|
|||
const port = 9526 |
|||
const publicPath = config.publicPath |
|||
|
|||
var connect = require('connect') |
|||
var serveStatic = require('serve-static') |
|||
const app = connect() |
|||
|
|||
app.use( |
|||
publicPath, |
|||
serveStatic('./dist', { |
|||
index: ['index.html', '/'] |
|||
}) |
|||
) |
|||
|
|||
app.listen(port, function () { |
|||
console.log(chalk.green(`> Preview at http://localhost:${port}${publicPath}`)) |
|||
if (report) { |
|||
console.log(chalk.green(`> Report at http://localhost:${port}${publicPath}report.html`)) |
|||
} |
|||
|
|||
}) |
|||
} else { |
|||
run(`vue-cli-service build ${args}`) |
|||
} |
|||
@ -0,0 +1,13 @@ |
|||
<!doctype html> |
|||
<html lang="en"> |
|||
<head> |
|||
<meta charset="UTF-8" /> |
|||
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> |
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
|||
<title>deepchart后台管理系统</title> |
|||
</head> |
|||
<body> |
|||
<div id="app"></div> |
|||
<script type="module" src="/src/main.js"></script> |
|||
</body> |
|||
</html> |
|||
@ -1,24 +0,0 @@ |
|||
module.exports = { |
|||
moduleFileExtensions: ['js', 'jsx', 'json', 'vue'], |
|||
transform: { |
|||
'^.+\\.vue$': 'vue-jest', |
|||
'.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': |
|||
'jest-transform-stub', |
|||
'^.+\\.jsx?$': 'babel-jest' |
|||
}, |
|||
moduleNameMapper: { |
|||
'^@/(.*)$': '<rootDir>/src/$1' |
|||
}, |
|||
snapshotSerializers: ['jest-serializer-vue'], |
|||
testMatch: [ |
|||
'**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)' |
|||
], |
|||
collectCoverageFrom: ['src/utils/**/*.{js,vue}', '!src/utils/auth.js', '!src/utils/request.js', 'src/components/**/*.{js,vue}'], |
|||
coverageDirectory: '<rootDir>/tests/unit/coverage', |
|||
// 'collectCoverage': true,
|
|||
'coverageReporters': [ |
|||
'lcov', |
|||
'text-summary' |
|||
], |
|||
testURL: 'http://localhost/' |
|||
} |
|||
@ -1,9 +0,0 @@ |
|||
{ |
|||
"compilerOptions": { |
|||
"baseUrl": "./", |
|||
"paths": { |
|||
"@/*": ["src/*"] |
|||
} |
|||
}, |
|||
"exclude": ["node_modules", "dist"] |
|||
} |
|||
@ -1,57 +0,0 @@ |
|||
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 |
|||
} |
|||
|
|||
@ -1,81 +0,0 @@ |
|||
const chokidar = require('chokidar') |
|||
const bodyParser = require('body-parser') |
|||
const chalk = require('chalk') |
|||
const path = require('path') |
|||
const Mock = require('mockjs') |
|||
|
|||
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) { |
|||
app[mock.type](mock.url, mock.response) |
|||
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(`${process.env.VUE_APP_BASE_API}${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
|
|||
app.use(bodyParser.json()) |
|||
app.use(bodyParser.urlencoded({ |
|||
extended: true |
|||
})) |
|||
|
|||
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)) |
|||
} |
|||
} |
|||
}) |
|||
} |
|||
@ -1,29 +0,0 @@ |
|||
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: '/vue-admin-template/table/list', |
|||
type: 'get', |
|||
response: config => { |
|||
const items = data.items |
|||
return { |
|||
code: 200, |
|||
data: { |
|||
total: items.length, |
|||
items: items |
|||
} |
|||
} |
|||
} |
|||
} |
|||
] |
|||
@ -1,84 +0,0 @@ |
|||
|
|||
const tokens = { |
|||
admin: { |
|||
token: 'admin-token' |
|||
}, |
|||
editor: { |
|||
token: 'editor-token' |
|||
} |
|||
} |
|||
|
|||
const users = { |
|||
'admin-token': { |
|||
roles: ['admin'], |
|||
introduction: 'I am a super administrator', |
|||
avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif', |
|||
name: 'Super Admin' |
|||
}, |
|||
'editor-token': { |
|||
roles: ['editor'], |
|||
introduction: 'I am an editor', |
|||
avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif', |
|||
name: 'Normal Editor' |
|||
} |
|||
} |
|||
|
|||
module.exports = [ |
|||
// user login
|
|||
{ |
|||
url: '/vue-admin-template/user/login', |
|||
type: 'post', |
|||
response: config => { |
|||
const { username } = config.body |
|||
const token = tokens[username] |
|||
|
|||
// mock error
|
|||
if (!token) { |
|||
return { |
|||
code: 60204, |
|||
message: 'Account and password are incorrect.' |
|||
} |
|||
} |
|||
|
|||
return { |
|||
code: 200, |
|||
data: token |
|||
} |
|||
} |
|||
}, |
|||
|
|||
// get user info
|
|||
{ |
|||
url: '/vue-admin-template/user/info\.*', |
|||
type: 'get', |
|||
response: config => { |
|||
const { token } = config.query |
|||
const info = users[token] |
|||
|
|||
// mock error
|
|||
if (!info) { |
|||
return { |
|||
code: 50008, |
|||
message: 'Login failed, unable to get user details.' |
|||
} |
|||
} |
|||
|
|||
return { |
|||
code: 200, |
|||
data: info |
|||
} |
|||
} |
|||
}, |
|||
|
|||
// user logout
|
|||
{ |
|||
url: '/vue-admin-template/user/logout', |
|||
type: 'post', |
|||
response: _ => { |
|||
return { |
|||
code: 200, |
|||
data: 'success' |
|||
} |
|||
} |
|||
} |
|||
] |
|||
@ -1,25 +0,0 @@ |
|||
/** |
|||
* @param {string} url |
|||
* @returns {Object} |
|||
*/ |
|||
function param2Obj(url) { |
|||
const search = decodeURIComponent(url.split('?')[1]).replace(/\+/g, ' ') |
|||
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 |
|||
} |
|||
1971
package-lock.json
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -1,62 +1,25 @@ |
|||
{ |
|||
"name": "vue-admin-template", |
|||
"version": "4.4.0", |
|||
"description": "A vue admin template with Element UI & axios & iconfont & permission control & lint", |
|||
"author": "Pan <panfree23@gmail.com>", |
|||
"name": "deepchartadmin", |
|||
"private": true, |
|||
"version": "0.0.0", |
|||
"type": "module", |
|||
"scripts": { |
|||
"dev": "vue-cli-service serve", |
|||
"build:prod": "vue-cli-service build", |
|||
"build:stage": "vue-cli-service build --mode staging", |
|||
"preview": "node build/index.js --preview", |
|||
"svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml", |
|||
"lint": "eslint --ext .js,.vue src", |
|||
"test:unit": "jest --clearCache && vue-cli-service test:unit", |
|||
"test:ci": "npm run lint && npm run test:unit" |
|||
"dev": "vite --host", |
|||
"build": "vite build", |
|||
"build:dev": "vite build --mode development", |
|||
"preview": "vite preview" |
|||
}, |
|||
"dependencies": { |
|||
"axios": "0.18.1", |
|||
"core-js": "3.6.5", |
|||
"element-ui": "2.13.2", |
|||
"js-cookie": "2.2.0", |
|||
"normalize.css": "7.0.0", |
|||
"nprogress": "0.2.0", |
|||
"path-to-regexp": "2.4.0", |
|||
"vue": "2.6.10", |
|||
"vue-router": "3.0.6", |
|||
"vuex": "3.1.0" |
|||
"@element-plus/icons-vue": "^2.3.2", |
|||
"axios": "^1.13.2", |
|||
"element-plus": "^2.11.8", |
|||
"normalize.css": "^8.0.1", |
|||
"pinia": "^3.0.4", |
|||
"vue": "^3.5.24", |
|||
"vue-router": "^4.6.3" |
|||
}, |
|||
"devDependencies": { |
|||
"@vue/cli-plugin-babel": "4.4.4", |
|||
"@vue/cli-plugin-eslint": "4.4.4", |
|||
"@vue/cli-plugin-unit-jest": "4.4.4", |
|||
"@vue/cli-service": "4.4.4", |
|||
"@vue/test-utils": "1.0.0-beta.29", |
|||
"autoprefixer": "9.5.1", |
|||
"babel-eslint": "10.1.0", |
|||
"babel-jest": "23.6.0", |
|||
"babel-plugin-dynamic-import-node": "2.3.3", |
|||
"chalk": "2.4.2", |
|||
"connect": "3.6.6", |
|||
"eslint": "6.7.2", |
|||
"eslint-plugin-vue": "6.2.2", |
|||
"html-webpack-plugin": "3.2.0", |
|||
"mockjs": "1.0.1-beta3", |
|||
"runjs": "4.3.2", |
|||
"sass": "1.26.8", |
|||
"sass-loader": "8.0.2", |
|||
"script-ext-html-webpack-plugin": "2.1.3", |
|||
"serve-static": "1.13.2", |
|||
"svg-sprite-loader": "4.1.3", |
|||
"svgo": "1.2.2", |
|||
"vue-template-compiler": "2.6.10" |
|||
}, |
|||
"browserslist": [ |
|||
"> 1%", |
|||
"last 2 versions" |
|||
], |
|||
"engines": { |
|||
"node": ">=8.9", |
|||
"npm": ">= 3.0.0" |
|||
}, |
|||
"license": "MIT" |
|||
"@vitejs/plugin-vue": "^6.0.1", |
|||
"vite": "^7.2.2" |
|||
} |
|||
} |
|||
@ -1,8 +0,0 @@ |
|||
// https://github.com/michael-ciniawsky/postcss-load-config
|
|||
|
|||
module.exports = { |
|||
'plugins': { |
|||
// to edit target browsers: use "browserslist" field in package.json
|
|||
'autoprefixer': {} |
|||
} |
|||
} |
|||
@ -1,17 +0,0 @@ |
|||
<!DOCTYPE html> |
|||
<html> |
|||
<head> |
|||
<meta charset="utf-8"> |
|||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> |
|||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"> |
|||
<link rel="icon" href="<%= BASE_URL %>favicon.ico"> |
|||
<title><%= webpackConfig.name %></title> |
|||
</head> |
|||
<body> |
|||
<noscript> |
|||
<strong>We're sorry but <%= webpackConfig.name %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong> |
|||
</noscript> |
|||
<div id="app"></div> |
|||
<!-- built files will be auto injected --> |
|||
</body> |
|||
</html> |
|||
@ -0,0 +1 @@ |
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg> |
|||
@ -1,11 +1,14 @@ |
|||
<template> |
|||
<div id="app"> |
|||
<router-view /> |
|||
</div> |
|||
<router-view /> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
name: 'App' |
|||
} |
|||
<script setup> |
|||
</script> |
|||
|
|||
<style scoped> |
|||
* { |
|||
margin: 0; |
|||
padding: 0; |
|||
box-sizing: border-box; |
|||
} |
|||
</style> |
|||
@ -1,9 +0,0 @@ |
|||
import request from '@/utils/request' |
|||
|
|||
export function getList(params) { |
|||
return request({ |
|||
url: '/vue-admin-template/table/list', |
|||
method: 'get', |
|||
params |
|||
}) |
|||
} |
|||
@ -1,24 +0,0 @@ |
|||
import request from '@/utils/request' |
|||
|
|||
export function login(data) { |
|||
return request({ |
|||
url: '/vue-admin-template/user/login', |
|||
method: 'post', |
|||
data |
|||
}) |
|||
} |
|||
|
|||
export function getInfo(token) { |
|||
return request({ |
|||
url: '/vue-admin-template/user/info', |
|||
method: 'get', |
|||
params: { token } |
|||
}) |
|||
} |
|||
|
|||
export function logout() { |
|||
return request({ |
|||
url: '/vue-admin-template/user/logout', |
|||
method: 'post' |
|||
}) |
|||
} |
|||
@ -0,0 +1,201 @@ |
|||
import request from '../utils/myAxios'; |
|||
import axios from 'axios' |
|||
var base_url = import.meta.env.VITE_API_BASE_URL |
|||
var base_url_link = import.meta.env.VITE_API_BASE_URL_LINK |
|||
|
|||
// 表单形式请求
|
|||
const requestFrom = axios.create({ |
|||
timeout: 10000, |
|||
headers: { |
|||
'Content-Type': 'application/x-www-form-urlencoded' |
|||
} |
|||
}) |
|||
|
|||
// 登录
|
|||
export function loginApi(params) { |
|||
return requestFrom({ |
|||
url: base_url_link, |
|||
method: "post", |
|||
data: new URLSearchParams(params), |
|||
}); |
|||
} |
|||
|
|||
// 获取地区列表
|
|||
export function marketListApi(params) { |
|||
return request({ |
|||
url: base_url + "/admin/region/list", |
|||
method: "post", |
|||
data: params, |
|||
}); |
|||
} |
|||
|
|||
// 获取导出列表
|
|||
export function exportListApi(params) { |
|||
return request({ |
|||
url: base_url + "/admin/export/status/list", |
|||
method: "post", |
|||
data: params, |
|||
}); |
|||
} |
|||
|
|||
// 操作日志
|
|||
export function logMListApi(params) { |
|||
return request({ |
|||
url: base_url + "/admin/operation/record/list", |
|||
method: "post", |
|||
data: params, |
|||
}); |
|||
} |
|||
|
|||
// 行情期限--获取用户列表
|
|||
export function userMListApi(params) { |
|||
return request({ |
|||
url: base_url + "/admin/market/list", |
|||
method: "post", |
|||
data: params, |
|||
}); |
|||
} |
|||
|
|||
// 行情期限--创建导出
|
|||
export function exportMarketApi(params) { |
|||
return request({ |
|||
url: base_url + "/admin/market/export/create", |
|||
method: "post", |
|||
data: params, |
|||
}); |
|||
} |
|||
|
|||
// 行情期限--开通/编辑
|
|||
export function exitMApi(params) { |
|||
return request({ |
|||
url: base_url + "/admin/market/permission/exit", |
|||
method: "post", |
|||
data: params, |
|||
}); |
|||
} |
|||
|
|||
// deepMate--获取用户列表
|
|||
export function userDMListApi(params) { |
|||
return request({ |
|||
url: base_url + "/admin/deepMate/list", |
|||
method: "post", |
|||
data: params, |
|||
}); |
|||
} |
|||
|
|||
// deepMate--创建导出
|
|||
export function exportDeepMateApi(params) { |
|||
return request({ |
|||
url: base_url + "/admin/deepMate/export/create", |
|||
method: "post", |
|||
data: params, |
|||
}); |
|||
} |
|||
|
|||
// deepMate--开通/编辑
|
|||
export function exitDMApi(params) { |
|||
return request({ |
|||
url: base_url + "/admin/deepMate/permission/exit", |
|||
method: "post", |
|||
data: params, |
|||
}); |
|||
} |
|||
|
|||
// 深度探索--获取用户列表
|
|||
export function userDEListApi(params) { |
|||
return request({ |
|||
url: base_url + "/admin/deepExploration/list", |
|||
method: "post", |
|||
data: params, |
|||
}); |
|||
} |
|||
|
|||
// 深度探索--创建导出
|
|||
export function exportDeepExploreApi(params) { |
|||
return request({ |
|||
url: base_url + "/admin/deepExploration/export/create", |
|||
method: "post", |
|||
data: params, |
|||
}); |
|||
} |
|||
|
|||
// 深度探索-开通/编辑
|
|||
export function exitDEApi(params) { |
|||
return request({ |
|||
url: base_url + "/admin/deepExploration/permission/exit", |
|||
method: "post", |
|||
data: params, |
|||
}); |
|||
} |
|||
|
|||
// 深度探索-权限详情
|
|||
export function detailDEApi(params) { |
|||
return request({ |
|||
url: base_url + "/admin/deepExploration/detail", |
|||
method: "post", |
|||
data: params, |
|||
}); |
|||
} |
|||
|
|||
// 深度探索-指标列表
|
|||
export function indicatorListApi(params) { |
|||
return request({ |
|||
url: base_url + "/admin/indicator/list", |
|||
method: "post", |
|||
data: params, |
|||
}); |
|||
} |
|||
|
|||
// 设置-获取刷新时间与安卓最新安装包
|
|||
export function getEnvApi(params) { |
|||
return request({ |
|||
url: base_url + "/admin/env/get", |
|||
method: "post", |
|||
data: params, |
|||
}); |
|||
} |
|||
|
|||
// 设置-修改刷新时间与安卓最新安装包
|
|||
export function setEnvApi(params) { |
|||
return request({ |
|||
url: base_url + "/admin/env/set", |
|||
method: "post", |
|||
data: params, |
|||
}); |
|||
} |
|||
|
|||
// 登录-获取权限接口
|
|||
export function getPermissionApi(params) { |
|||
return request({ |
|||
url: base_url + "/admin/auth/info", |
|||
method: "post", |
|||
data: params, |
|||
}); |
|||
} |
|||
|
|||
// 用户邀请-获取用户邀请列表接口
|
|||
export function getInvitedListApi(params) { |
|||
return request({ |
|||
url: base_url + "/admin/invite/list", |
|||
method: "post", |
|||
data: params, |
|||
}); |
|||
} |
|||
|
|||
// 用户邀请--创建导出
|
|||
export function exportInvitedApi(params) { |
|||
return request({ |
|||
url: base_url + "/admin/invite/export/create", |
|||
method: "post", |
|||
data: params, |
|||
}); |
|||
} |
|||
|
|||
// 用户邀请--获取来源下拉框
|
|||
export function getOriginListApi(params) { |
|||
return request({ |
|||
url: base_url + "/admin/invite/user/origin/detail", |
|||
method: "post", |
|||
data: params, |
|||
}); |
|||
} |
|||
|
Before Width: 1014 | Height: 556 | Size: 96 KiB |
|
Before Width: 152 | Height: 138 | Size: 4.7 KiB |
|
After Width: 48 | Height: 48 | Size: 1.9 KiB |
|
After Width: 1920 | Height: 1080 | Size: 422 KiB |
@ -0,0 +1 @@ |
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg> |
|||
@ -1,71 +0,0 @@ |
|||
<template> |
|||
<el-breadcrumb class="app-breadcrumb" separator="/"> |
|||
<transition-group name="breadcrumb"> |
|||
<el-breadcrumb-item v-for="(item,index) in levelList" :key="item.path"> |
|||
<span v-if="item.redirect==='noRedirect'||index==levelList.length-1" class="no-redirect">{{ item.meta.title }}</span> |
|||
<a v-else @click.prevent="handleLink(item)">{{ item.meta.title }}</a> |
|||
</el-breadcrumb-item> |
|||
</transition-group> |
|||
</el-breadcrumb> |
|||
</template> |
|||
|
|||
<script> |
|||
import pathToRegexp from 'path-to-regexp' |
|||
|
|||
export default { |
|||
data() { |
|||
return { |
|||
levelList: null |
|||
} |
|||
}, |
|||
watch: { |
|||
$route() { |
|||
this.getBreadcrumb() |
|||
} |
|||
}, |
|||
created() { |
|||
this.getBreadcrumb() |
|||
}, |
|||
methods: { |
|||
getBreadcrumb() { |
|||
const matched = this.$route.matched.filter(item => item.meta && item.meta.title) |
|||
|
|||
this.levelList = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false) |
|||
}, |
|||
isDashboard(route) { |
|||
const name = route && route.name |
|||
if (!name) { |
|||
return false |
|||
} |
|||
return name.trim().toLocaleLowerCase() === 'Dashboard'.toLocaleLowerCase() |
|||
}, |
|||
pathCompile(path) { |
|||
const { params } = this.$route |
|||
var toPath = pathToRegexp.compile(path) |
|||
return toPath(params) |
|||
}, |
|||
handleLink(item) { |
|||
const { redirect, path } = item |
|||
if (redirect) { |
|||
this.$router.push(redirect) |
|||
return |
|||
} |
|||
this.$router.push(this.pathCompile(path)) |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
.app-breadcrumb.el-breadcrumb { |
|||
display: inline-block; |
|||
font-size: 14px; |
|||
line-height: 50px; |
|||
margin-left: 8px; |
|||
|
|||
.no-redirect { |
|||
color: #97a8be; |
|||
cursor: text; |
|||
} |
|||
} |
|||
</style> |
|||
@ -1,44 +0,0 @@ |
|||
<template> |
|||
<div style="padding: 0 15px;" @click="toggleClick"> |
|||
<svg |
|||
:class="{'is-active':isActive}" |
|||
class="hamburger" |
|||
viewBox="0 0 1024 1024" |
|||
xmlns="http://www.w3.org/2000/svg" |
|||
width="64" |
|||
height="64" |
|||
> |
|||
<path d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM142.4 642.1L298.7 519a8.84 8.84 0 0 0 0-13.9L142.4 381.9c-5.8-4.6-14.4-.5-14.4 6.9v246.3a8.9 8.9 0 0 0 14.4 7z" /> |
|||
</svg> |
|||
</div> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
name: 'Hamburger', |
|||
props: { |
|||
isActive: { |
|||
type: Boolean, |
|||
default: false |
|||
} |
|||
}, |
|||
methods: { |
|||
toggleClick() { |
|||
this.$emit('toggleClick') |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.hamburger { |
|||
display: inline-block; |
|||
vertical-align: middle; |
|||
width: 20px; |
|||
height: 20px; |
|||
} |
|||
|
|||
.hamburger.is-active { |
|||
transform: rotate(180deg); |
|||
} |
|||
</style> |
|||
@ -1,62 +0,0 @@ |
|||
<template> |
|||
<div v-if="isExternal" :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 |
|||
import { isExternal } from '@/utils/validate' |
|||
|
|||
export default { |
|||
name: 'SvgIcon', |
|||
props: { |
|||
iconClass: { |
|||
type: String, |
|||
required: true |
|||
}, |
|||
className: { |
|||
type: String, |
|||
default: '' |
|||
} |
|||
}, |
|||
computed: { |
|||
isExternal() { |
|||
return 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%` |
|||
} |
|||
} |
|||
} |
|||
} |
|||
</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> |
|||
@ -1,9 +0,0 @@ |
|||
import Vue from 'vue' |
|||
import SvgIcon from '@/components/SvgIcon'// svg component
|
|||
|
|||
// register globally
|
|||
Vue.component('svg-icon', SvgIcon) |
|||
|
|||
const req = require.context('./svg', false, /\.svg$/) |
|||
const requireAll = requireContext => requireContext.keys().map(requireContext) |
|||
requireAll(req) |
|||
@ -1 +0,0 @@ |
|||
<svg width="128" height="100" xmlns="http://www.w3.org/2000/svg"><path d="M27.429 63.638c0-2.508-.893-4.65-2.679-6.424-1.786-1.775-3.94-2.662-6.464-2.662-2.524 0-4.679.887-6.465 2.662-1.785 1.774-2.678 3.916-2.678 6.424 0 2.508.893 4.65 2.678 6.424 1.786 1.775 3.94 2.662 6.465 2.662 2.524 0 4.678-.887 6.464-2.662 1.786-1.775 2.679-3.916 2.679-6.424zm13.714-31.801c0-2.508-.893-4.65-2.679-6.424-1.785-1.775-3.94-2.662-6.464-2.662-2.524 0-4.679.887-6.464 2.662-1.786 1.774-2.679 3.916-2.679 6.424 0 2.508.893 4.65 2.679 6.424 1.785 1.774 3.94 2.662 6.464 2.662 2.524 0 4.679-.888 6.464-2.662 1.786-1.775 2.679-3.916 2.679-6.424zM71.714 65.98l7.215-27.116c.285-1.23.107-2.378-.536-3.443-.643-1.064-1.56-1.762-2.75-2.094-1.19-.33-2.333-.177-3.429.462-1.095.639-1.81 1.573-2.143 2.804l-7.214 27.116c-2.857.237-5.405 1.266-7.643 3.088-2.238 1.822-3.738 4.152-4.5 6.992-.952 3.644-.476 7.098 1.429 10.364 1.905 3.265 4.69 5.37 8.357 6.317 3.667.947 7.143.474 10.429-1.42 3.285-1.892 5.404-4.66 6.357-8.305.762-2.84.619-5.607-.429-8.305-1.047-2.697-2.762-4.85-5.143-6.46zm47.143-2.342c0-2.508-.893-4.65-2.678-6.424-1.786-1.775-3.94-2.662-6.465-2.662-2.524 0-4.678.887-6.464 2.662-1.786 1.774-2.679 3.916-2.679 6.424 0 2.508.893 4.65 2.679 6.424 1.786 1.775 3.94 2.662 6.464 2.662 2.524 0 4.679-.887 6.465-2.662 1.785-1.775 2.678-3.916 2.678-6.424zm-45.714-45.43c0-2.509-.893-4.65-2.679-6.425C68.68 10.01 66.524 9.122 64 9.122c-2.524 0-4.679.887-6.464 2.661-1.786 1.775-2.679 3.916-2.679 6.425 0 2.508.893 4.65 2.679 6.424 1.785 1.774 3.94 2.662 6.464 2.662 2.524 0 4.679-.888 6.464-2.662 1.786-1.775 2.679-3.916 2.679-6.424zm32 13.629c0-2.508-.893-4.65-2.679-6.424-1.785-1.775-3.94-2.662-6.464-2.662-2.524 0-4.679.887-6.464 2.662-1.786 1.774-2.679 3.916-2.679 6.424 0 2.508.893 4.65 2.679 6.424 1.785 1.774 3.94 2.662 6.464 2.662 2.524 0 4.679-.888 6.464-2.662 1.786-1.775 2.679-3.916 2.679-6.424zM128 63.638c0 12.351-3.357 23.78-10.071 34.286-.905 1.372-2.19 2.058-3.858 2.058H13.93c-1.667 0-2.953-.686-3.858-2.058C3.357 87.465 0 76.037 0 63.638c0-8.613 1.69-16.847 5.071-24.703C8.452 31.08 13 24.312 18.714 18.634c5.715-5.68 12.524-10.199 20.429-13.559C47.048 1.715 55.333.035 64 .035c8.667 0 16.952 1.68 24.857 5.04 7.905 3.36 14.714 7.88 20.429 13.559 5.714 5.678 10.262 12.446 13.643 20.301 3.38 7.856 5.071 16.09 5.071 24.703z"/></svg> |
|||
@ -1 +0,0 @@ |
|||
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M96.258 57.462h31.421C124.794 27.323 100.426 2.956 70.287.07v31.422a32.856 32.856 0 0 1 25.971 25.97zm-38.796-25.97V.07C27.323 2.956 2.956 27.323.07 57.462h31.422a32.856 32.856 0 0 1 25.97-25.97zm12.825 64.766v31.421c30.46-2.885 54.507-27.253 57.713-57.712H96.579c-2.886 13.466-13.146 23.726-26.292 26.291zM31.492 70.287H.07c2.886 30.46 27.253 54.507 57.713 57.713V96.579c-13.466-2.886-23.726-13.146-26.291-26.292z"/></svg> |
|||
@ -1 +0,0 @@ |
|||
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="128" height="128"><defs><style/></defs><path d="M512 128q69.675 0 135.51 21.163t115.498 54.997 93.483 74.837 73.685 82.006 51.67 74.837 32.17 54.827L1024 512q-2.347 4.992-6.315 13.483T998.87 560.17t-31.658 51.669-44.331 59.99-56.832 64.34-69.504 60.16-82.347 51.5-94.848 34.687T512 896q-69.675 0-135.51-21.163t-115.498-54.826-93.483-74.326-73.685-81.493-51.67-74.496-32.17-54.997L0 513.707q2.347-4.992 6.315-13.483t18.816-34.816 31.658-51.84 44.331-60.33 56.832-64.683 69.504-60.331 82.347-51.84 94.848-34.816T512 128.085zm0 85.333q-46.677 0-91.648 12.331t-81.152 31.83-70.656 47.146-59.648 54.485-48.853 57.686-37.675 52.821-26.325 43.99q12.33 21.674 26.325 43.52t37.675 52.351 48.853 57.003 59.648 53.845T339.2 767.02t81.152 31.488T512 810.667t91.648-12.331 81.152-31.659 70.656-46.848 59.648-54.186 48.853-57.344 37.675-52.651T927.957 512q-12.33-21.675-26.325-43.648t-37.675-52.65-48.853-57.345-59.648-54.186-70.656-46.848-81.152-31.659T512 213.334zm0 128q70.656 0 120.661 50.006T682.667 512 632.66 632.661 512 682.667 391.339 632.66 341.333 512t50.006-120.661T512 341.333zm0 85.334q-35.328 0-60.33 25.002T426.666 512t25.002 60.33T512 597.334t60.33-25.002T597.334 512t-25.002-60.33T512 426.666z"/></svg> |
|||
@ -1 +0,0 @@ |
|||
<svg width="128" height="64" xmlns="http://www.w3.org/2000/svg"><path d="M127.072 7.994c1.37-2.208.914-5.152-.914-6.87-2.056-1.717-4.797-1.226-6.396.982-.229.245-25.586 32.382-55.74 32.382-29.24 0-55.74-32.382-55.968-32.627-1.6-1.963-4.57-2.208-6.397-.49C-.17 3.086-.399 6.275 1.2 8.238c.457.736 5.94 7.36 14.62 14.72L4.17 35.96c-1.828 1.963-1.6 5.152.228 6.87.457.98 1.6 1.471 2.742 1.471s2.284-.49 3.198-1.472l12.564-13.983c5.94 4.416 13.021 8.587 20.788 11.53l-4.797 17.418c-.685 2.699.686 5.397 3.198 6.133h1.37c2.057 0 3.884-1.472 4.341-3.68L52.6 42.83c3.655.736 7.538 1.227 11.422 1.227 3.883 0 7.767-.49 11.422-1.227l4.797 17.173c.457 2.208 2.513 3.68 4.34 3.68.457 0 .914 0 1.143-.246 2.513-.736 3.883-3.434 3.198-6.133l-4.797-17.172c7.767-2.944 14.848-7.114 20.788-11.53l12.336 13.738c.913.981 2.056 1.472 3.198 1.472s2.284-.49 3.198-1.472c1.828-1.963 1.828-4.906.228-6.87l-11.65-13.001c9.366-7.36 14.849-14.474 14.849-14.474z"/></svg> |
|||
@ -1 +0,0 @@ |
|||
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M84.068 23.784c-1.02 0-1.877-.32-2.572-.96a8.588 8.588 0 0 1-1.738-2.237 11.524 11.524 0 0 1-1.042-2.621c-.232-.895-.348-1.641-.348-2.238V0h.278c.834 0 1.622.085 2.363.256.742.17 1.645.575 2.711 1.214 1.066.64 2.363 1.535 3.892 2.686 1.53 1.15 3.453 2.664 5.77 4.54 2.502 2.045 4.494 3.771 5.977 5.178 1.483 1.406 2.618 2.6 3.406 3.58.787.98 1.274 1.812 1.46 2.494.185.682.277 1.278.277 1.79v2.046H84.068zM127.3 84.01c.278.682.464 1.535.556 2.558.093 1.023-.37 2.003-1.39 2.94-.463.427-.88.832-1.25 1.215-.372.384-.696.704-.974.96a6.69 6.69 0 0 1-.973.767l-11.816-10.741a44.331 44.331 0 0 0 1.877-1.535 31.028 31.028 0 0 1 1.737-1.406c1.112-.938 2.317-1.343 3.615-1.215 1.297.128 2.363.405 3.197.83.927.427 1.923 1.173 2.989 2.239 1.065 1.065 1.876 2.195 2.432 3.388zM78.23 95.902c2.038 0 3.752-.511 5.143-1.534l-26.969 25.83H18.037c-1.761 0-3.684-.47-5.77-1.407a24.549 24.549 0 0 1-5.838-3.709 21.373 21.373 0 0 1-4.518-5.306c-1.204-2.003-1.807-4.07-1.807-6.202V16.495c0-1.79.44-3.665 1.32-5.626A18.41 18.41 0 0 1 5.04 5.562a21.798 21.798 0 0 1 5.213-3.964C12.198.533 14.237 0 16.37 0h53.24v15.984c0 1.62.278 3.367.834 5.242a16.704 16.704 0 0 0 2.572 5.179c1.159 1.577 2.665 2.898 4.518 3.964 1.853 1.066 4.078 1.598 6.673 1.598h20.295v42.325L85.458 92.45c1.02-1.364 1.529-2.856 1.529-4.476 0-2.216-.857-4.113-2.572-5.69-1.714-1.577-3.776-2.366-6.186-2.366H26.1c-2.409 0-4.448.789-6.116 2.366-1.668 1.577-2.502 3.474-2.502 5.69 0 2.217.834 4.092 2.502 5.626 1.668 1.535 3.707 2.302 6.117 2.302h52.13zM26.1 47.951c-2.41 0-4.449.789-6.117 2.366-1.668 1.577-2.502 3.473-2.502 5.69 0 2.216.834 4.092 2.502 5.626 1.668 1.534 3.707 2.302 6.117 2.302h52.13c2.409 0 4.47-.768 6.185-2.302 1.715-1.534 2.572-3.41 2.572-5.626 0-2.217-.857-4.113-2.572-5.69-1.714-1.577-3.776-2.366-6.186-2.366H26.1zm52.407 64.063l1.807-1.663 3.476-3.196a479.75 479.75 0 0 0 4.587-4.284 500.757 500.757 0 0 1 5.004-4.667c3.985-3.666 8.48-7.758 13.485-12.276l11.677 10.741-13.485 12.404-5.004 4.603-4.587 4.22a179.46 179.46 0 0 0-3.267 3.068c-.88.853-1.367 1.322-1.46 1.407-.463.341-.973.703-1.529 1.087-.556.383-1.112.703-1.668.959-.556.256-1.413.575-2.572.959a83.5 83.5 0 0 1-3.545 1.087 72.2 72.2 0 0 1-3.475.895c-1.112.256-1.946.426-2.502.511-1.112.17-1.854.043-2.224-.383-.371-.426-.464-1.151-.278-2.174.092-.511.278-1.279.556-2.302.278-1.023.602-2.067.973-3.132l1.042-3.005c.325-.938.58-1.577.765-1.918a10.157 10.157 0 0 1 2.224-2.941z"/></svg> |
|||
@ -1 +0,0 @@ |
|||
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M115.625 127.937H.063V12.375h57.781v12.374H12.438v90.813h90.813V70.156h12.374z"/><path d="M116.426 2.821l8.753 8.753-56.734 56.734-8.753-8.745z"/><path d="M127.893 37.982h-12.375V12.375H88.706V0h39.187z"/></svg> |
|||
@ -1 +0,0 @@ |
|||
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M.002 9.2c0 5.044 3.58 9.133 7.998 9.133 4.417 0 7.997-4.089 7.997-9.133 0-5.043-3.58-9.132-7.997-9.132S.002 4.157.002 9.2zM31.997.066h95.981V18.33H31.997V.066zm0 45.669c0 5.044 3.58 9.132 7.998 9.132 4.417 0 7.997-4.088 7.997-9.132 0-3.263-1.524-6.278-3.998-7.91-2.475-1.63-5.524-1.63-7.998 0-2.475 1.632-4 4.647-4 7.91zM63.992 36.6h63.986v18.265H63.992V36.6zm-31.995 82.2c0 5.043 3.58 9.132 7.998 9.132 4.417 0 7.997-4.089 7.997-9.132 0-5.044-3.58-9.133-7.997-9.133s-7.998 4.089-7.998 9.133zm31.995-9.131h63.986v18.265H63.992V109.67zm0-27.404c0 5.044 3.58 9.133 7.998 9.133 4.417 0 7.997-4.089 7.997-9.133 0-3.263-1.524-6.277-3.998-7.909-2.475-1.631-5.524-1.631-7.998 0-2.475 1.632-4 4.646-4 7.91zm31.995-9.13h31.991V91.4H95.987V73.135z"/></svg> |
|||
@ -1 +0,0 @@ |
|||
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M108.8 44.322H89.6v-5.36c0-9.04-3.308-24.163-25.6-24.163-23.145 0-25.6 16.881-25.6 24.162v5.361H19.2v-5.36C19.2 15.281 36.798 0 64 0c27.202 0 44.8 15.281 44.8 38.961v5.361zm-32 39.356c0-5.44-5.763-9.832-12.8-9.832-7.037 0-12.8 4.392-12.8 9.832 0 3.682 2.567 6.808 6.407 8.477v11.205c0 2.718 2.875 4.962 6.4 4.962 3.524 0 6.4-2.244 6.4-4.962V92.155c3.833-1.669 6.393-4.795 6.393-8.477zM128 64v49.201c0 8.158-8.645 14.799-19.2 14.799H19.2C8.651 128 0 121.359 0 113.201V64c0-8.153 8.645-14.799 19.2-14.799h89.6c10.555 0 19.2 6.646 19.2 14.799z"/></svg> |
|||
@ -1 +0,0 @@ |
|||
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M.006.064h127.988v31.104H.006V.064zm0 38.016h38.396v41.472H.006V38.08zm0 48.384h38.396v41.472H.006V86.464zM44.802 38.08h38.396v41.472H44.802V38.08zm0 48.384h38.396v41.472H44.802V86.464zM89.598 38.08h38.396v41.472H89.598zm0 48.384h38.396v41.472H89.598z"/><path d="M.006.064h127.988v31.104H.006V.064zm0 38.016h38.396v41.472H.006V38.08zm0 48.384h38.396v41.472H.006V86.464zM44.802 38.08h38.396v41.472H44.802V38.08zm0 48.384h38.396v41.472H44.802V86.464zM89.598 38.08h38.396v41.472H89.598zm0 48.384h38.396v41.472H89.598z"/></svg> |
|||
@ -1 +0,0 @@ |
|||
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M126.713 90.023c.858.985 1.287 2.134 1.287 3.447v29.553c0 1.423-.429 2.6-1.287 3.53-.858.93-1.907 1.395-3.146 1.395H97.824c-1.145 0-2.146-.465-3.004-1.395-.858-.93-1.287-2.107-1.287-3.53V93.47c0-.875.19-1.696.572-2.462.382-.766.906-1.368 1.573-1.806a3.84 3.84 0 0 1 2.146-.657h9.725V69.007a3.84 3.84 0 0 0-.43-1.806 3.569 3.569 0 0 0-1.143-1.313 2.714 2.714 0 0 0-1.573-.492h-36.47v23.149h9.725c1.144 0 2.145.492 3.004 1.478.858.985 1.287 2.134 1.287 3.447v29.553c0 .876-.191 1.696-.573 2.463-.38.766-.905 1.368-1.573 1.806a3.84 3.84 0 0 1-2.145.656H51.915a3.84 3.84 0 0 1-2.145-.656c-.668-.438-1.216-1.04-1.645-1.806a4.96 4.96 0 0 1-.644-2.463V93.47c0-1.313.43-2.462 1.288-3.447.858-.986 1.907-1.478 3.146-1.478h9.582v-23.15h-37.9c-.953 0-1.74.356-2.359 1.068-.62.711-.93 1.56-.93 2.544v19.538h9.726c1.239 0 2.264.492 3.074 1.478.81.985 1.216 2.134 1.216 3.447v29.553c0 1.423-.405 2.6-1.216 3.53-.81.93-1.835 1.395-3.074 1.395H4.29c-.476 0-.93-.082-1.358-.246a4.1 4.1 0 0 1-1.144-.657 4.658 4.658 0 0 1-.93-1.067 5.186 5.186 0 0 1-.643-1.395 5.566 5.566 0 0 1-.215-1.56V93.47c0-.437.048-.875.143-1.313a3.95 3.95 0 0 1 .429-1.15c.19-.328.429-.656.715-.984.286-.329.572-.602.858-.821.286-.22.62-.383 1.001-.493.382-.11.763-.164 1.144-.164h9.726V61.619c0-.985.31-1.833.93-2.544.619-.712 1.358-1.068 2.216-1.068h44.335V39.62h-9.582c-1.24 0-2.288-.492-3.146-1.477a5.09 5.09 0 0 1-1.287-3.448V5.14c0-1.423.429-2.627 1.287-3.612.858-.985 1.907-1.477 3.146-1.477h25.743c.763 0 1.478.246 2.145.739a5.17 5.17 0 0 1 1.573 1.888c.382.766.573 1.587.573 2.462v29.553c0 1.313-.43 2.463-1.287 3.448-.859.985-1.86 1.477-3.004 1.477h-9.725v18.389h42.762c.954 0 1.74.355 2.36 1.067.62.711.93 1.56.93 2.545v26.925h9.582c1.239 0 2.288.492 3.146 1.478z"/></svg> |
|||
@ -1 +0,0 @@ |
|||
<svg width="130" height="130" xmlns="http://www.w3.org/2000/svg"><path d="M63.444 64.996c20.633 0 37.359-14.308 37.359-31.953 0-17.649-16.726-31.952-37.359-31.952-20.631 0-37.36 14.303-37.358 31.952 0 17.645 16.727 31.953 37.359 31.953zM80.57 75.65H49.434c-26.652 0-48.26 18.477-48.26 41.27v2.664c0 9.316 21.608 9.325 48.26 9.325H80.57c26.649 0 48.256-.344 48.256-9.325v-2.663c0-22.794-21.605-41.271-48.256-41.271z" stroke="#979797"/></svg> |
|||
@ -1,22 +0,0 @@ |
|||
# 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' |
|||
@ -0,0 +1,417 @@ |
|||
<!-- @format --> |
|||
|
|||
<template> |
|||
<el-container style="min-height: 100vh"> |
|||
<el-aside class="sidebar" v-if="!$route.meta.hiddenSidebar"> |
|||
<!-- 侧边栏头部 --> |
|||
<div class="sidebar-header"> |
|||
<img src="../assets/images/deepChart.png" class="sidebar-logo" /> |
|||
<span class="sidebar-title">DeepChart</span> |
|||
</div> |
|||
|
|||
<!-- 侧边栏菜单 --> |
|||
<el-menu class="sidebar-menu" background-color="transparent" router :default-active="lastActivePath" :unique-opened="true"> |
|||
<el-sub-menu v-for="parentRoute in filteredSidebarRoutes" :key="parentRoute.name" :index="`/${parentRoute.path}`"> |
|||
<!-- 父目录 --> |
|||
<template #title> |
|||
<el-icon> |
|||
<component :is="parentRoute.meta.icon" /> |
|||
</el-icon> |
|||
<span class="sidebar-parent-text">{{ parentRoute.meta.title }}</span> |
|||
</template> |
|||
|
|||
<!-- 子目录 --> |
|||
<el-menu-item v-for="childRoute in parentRoute.filteredChildren" :key="childRoute.name" :index="`/${parentRoute.path}/${childRoute.path}`" :to="`/${parentRoute.path}/${childRoute.path}`" class="sidebar-child-container"> |
|||
<el-icon class="sidebar-child-icon" /> |
|||
<span class="sidebar-child-text">{{ childRoute.meta.title }}</span> |
|||
</el-menu-item> |
|||
</el-sub-menu> |
|||
</el-menu> |
|||
|
|||
<div class="sidebar-logout" @click="handleLogout"> |
|||
<el-icon style="bottom: -3px"><SwitchButton /></el-icon> |
|||
退出登录 |
|||
</div> |
|||
<div class="sidebar-set" @click="handleSet" v-if="permission == '2'"> |
|||
<el-icon style="bottom: -3px"><Setting /></el-icon> |
|||
设置 |
|||
</div> |
|||
</el-aside> |
|||
|
|||
<!-- 首页刷新时间弹窗 --> |
|||
<el-dialog v-model="setValue" title="设置" width="500"> |
|||
<div class="refresh-time-container"> |
|||
<el-button type="danger">首页刷新时间</el-button> |
|||
<el-form-item label="刷新时间" style="margin-top: 20px; margin-bottom: 40px"> |
|||
<el-input-number v-model="refreshTime" :step="1" placeholder="请输入刷新时间" style="width: 200px"> |
|||
<template #suffix> |
|||
<span>s</span> |
|||
</template> |
|||
</el-input-number> |
|||
</el-form-item> |
|||
<el-button type="danger">Android下载链接配置</el-button> |
|||
<el-form-item label="编辑链接" style="margin-top: 20px; margin-bottom: 40px"> |
|||
<el-input v-model="Androidurl" style="width: 300px" placeholder="请输入最新Android下载链接" /> |
|||
</el-form-item> |
|||
</div> |
|||
<template #footer> |
|||
<div class="dialog-footer"> |
|||
<el-button @click="setValue = false">取消</el-button> |
|||
<el-button type="danger" @click="setEnv">确认修改</el-button> |
|||
</div> |
|||
</template> |
|||
</el-dialog> |
|||
|
|||
<!-- 主页面 --> |
|||
<el-main class="main-content"> |
|||
<router-view /> |
|||
</el-main> |
|||
</el-container> |
|||
</template> |
|||
|
|||
<script setup> |
|||
import { computed, ref, watch, onMounted } from "vue"; |
|||
import { useRouter, useRoute } from "vue-router"; |
|||
import { ElMessage } from "element-plus"; |
|||
import { getEnvApi, setEnvApi } from "../api/userPermissions"; |
|||
import { usePermissionStore } from "../store/permission"; |
|||
const permissionStore = usePermissionStore(); |
|||
|
|||
const router = useRouter(); |
|||
const route = useRoute(); |
|||
|
|||
// token |
|||
const token = localStorage.getItem("token"); |
|||
//permission |
|||
const permission = ref("-1"); |
|||
// 递归收集父路由 |
|||
const collectParentRoutes = (routes) => { |
|||
let parentRoutes = []; |
|||
routes.forEach((route) => { |
|||
if (route.meta?.isParentNav === true && route.meta?.showSidebar === true) { |
|||
parentRoutes.push(route); |
|||
} |
|||
if (route.children && route.children.length > 0) { |
|||
parentRoutes = [...parentRoutes, ...collectParentRoutes(route.children)]; |
|||
} |
|||
}); |
|||
return parentRoutes; |
|||
}; |
|||
|
|||
const getPermission = async () => { |
|||
const result = await getPermissionApi({ token: token }); |
|||
console.log("result", result); |
|||
permission.value = result.permission_level; |
|||
}; |
|||
|
|||
// 过滤侧边栏路由 |
|||
const filteredSidebarRoutes = computed(() => { |
|||
const allParentRoutes = collectParentRoutes(router.options.routes); |
|||
|
|||
return allParentRoutes |
|||
.map((parentRoute) => { |
|||
const filteredChildren = |
|||
parentRoute.children?.filter((childRoute) => { |
|||
// 必须标记为显示侧边栏 |
|||
if (childRoute.meta?.showSidebar !== true) return false; |
|||
|
|||
|
|||
// 权限判断 |
|||
if (permission.value == "2") { |
|||
// 权限为2可以看到全部 |
|||
return true; |
|||
} else if (permission.value == "1") { |
|||
// 权限为1只可以看到查看被邀请用户 |
|||
return childRoute.name === "invitedLook"; |
|||
} |
|||
|
|||
|
|||
// 默认情况(如未登录或无权限字段),根据实际需求处理,这里暂时隐藏 |
|||
return false; |
|||
}) || []; |
|||
return { ...parentRoute, filteredChildren }; |
|||
}) |
|||
.filter((parentRoute) => parentRoute.filteredChildren.length > 0); |
|||
}); |
|||
|
|||
// 计算所有侧边栏有效菜单集合 |
|||
const validMenuIndexes = computed(() => { |
|||
const indexes = []; |
|||
filteredSidebarRoutes.value.forEach((parentRoute) => { |
|||
// 收集父菜单 |
|||
indexes.push(`/${parentRoute.path}`); |
|||
// 收集子菜单 |
|||
parentRoute.filteredChildren.forEach((childRoute) => { |
|||
indexes.push(`/${parentRoute.path}/${childRoute.path}`); |
|||
}); |
|||
}); |
|||
return indexes; |
|||
}); |
|||
|
|||
// 存储最后一次选中的「有效侧边栏路径」 |
|||
const lastActivePath = ref(""); |
|||
|
|||
// 初始化+监听路由变化,更新最后有效路径 |
|||
watch( |
|||
() => route.path, // 监听当前路由路径变化 |
|||
(newPath) => { |
|||
// 如果新路由是侧边栏有效路径,更新,否则不跟新 |
|||
if (validMenuIndexes.value.includes(newPath)) { |
|||
lastActivePath.value = newPath; |
|||
} |
|||
}, |
|||
{ immediate: true } // 初始化时立即执行一次 |
|||
); |
|||
|
|||
// 退出登录 |
|||
const handleLogout = () => { |
|||
try { |
|||
localStorage.removeItem("token"); |
|||
router.push("/login"); |
|||
ElMessage.success("退出登录成功"); |
|||
} catch (error) { |
|||
ElMessage.error("退出登录失败,请重试"); |
|||
} |
|||
}; |
|||
|
|||
// 设置弹窗开关 |
|||
const setValue = ref(false); |
|||
|
|||
// 首页刷新时间 |
|||
const refreshTime = ref(""); |
|||
|
|||
// 安卓最新安装包 |
|||
const Androidurl = ref(""); |
|||
|
|||
// 设置按钮 |
|||
const handleSet = async () => { |
|||
const data1 = await getEnvApi({ |
|||
token: token, |
|||
key: "SYNC_INTERVAL", |
|||
}); |
|||
|
|||
const data2 = await getEnvApi({ |
|||
token: token, |
|||
key: "DOWNLOAD_URL", |
|||
}); |
|||
|
|||
refreshTime.value = data1; |
|||
Androidurl.value = data2; |
|||
setValue.value = true; |
|||
}; |
|||
|
|||
// 确认修改按钮 |
|||
const setEnv = async () => { |
|||
try { |
|||
await setEnvApi({ |
|||
token: token, |
|||
key: "SYNC_INTERVAL", |
|||
value: refreshTime.value, |
|||
}); |
|||
|
|||
await setEnvApi({ |
|||
token: token, |
|||
key: "DOWNLOAD_URL", |
|||
value: Androidurl.value, |
|||
}); |
|||
|
|||
ElMessage.success("修改成功"); |
|||
setValue.value = false; |
|||
} catch (error) { |
|||
ElMessage.error("修改失败"); |
|||
setValue.value = false; |
|||
} |
|||
}; |
|||
|
|||
onMounted(async () => { |
|||
permission.value = await permissionStore.getPermission(); |
|||
}); |
|||
</script> |
|||
|
|||
<style scoped> |
|||
/* 侧边栏核心样式 */ |
|||
.sidebar { |
|||
position: fixed; |
|||
left: 24px; |
|||
top: 10px; |
|||
width: 336px; |
|||
height: calc(100vh - 20px); |
|||
flex-shrink: 0; |
|||
border-radius: 8px; |
|||
background: #fee6e6; |
|||
box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.25); |
|||
overflow: hidden; |
|||
position: relative; |
|||
} |
|||
|
|||
/* 侧边栏头部样式 */ |
|||
.sidebar-header { |
|||
position: absolute; |
|||
left: 45px; |
|||
top: 59px; |
|||
display: flex; |
|||
align-items: center; |
|||
} |
|||
|
|||
.sidebar-logo { |
|||
height: 40px; |
|||
width: auto; |
|||
object-fit: contain; |
|||
} |
|||
|
|||
.sidebar-title { |
|||
margin-left: 10px; |
|||
color: #000000; |
|||
font-family: "PingFang SC", sans-serif; |
|||
font-size: 32px; |
|||
font-style: normal; |
|||
font-weight: 700; |
|||
line-height: 33.1px; |
|||
white-space: nowrap; |
|||
} |
|||
|
|||
/* 侧边栏菜单容器 */ |
|||
.sidebar-menu { |
|||
margin-left: 20px; |
|||
margin-top: 169px; |
|||
width: calc(100% - 20px); |
|||
border-right: none; |
|||
} |
|||
|
|||
/* 主内容区样式 */ |
|||
.main-content { |
|||
padding: 20px; |
|||
background-color: #fee6e6; |
|||
height: calc(100vh - 20px); |
|||
margin-left: 50px; |
|||
margin-right: 24px; |
|||
margin-top: 10px; |
|||
box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.25); |
|||
} |
|||
|
|||
/* 父目录文字样式 */ |
|||
.sidebar-parent-text { |
|||
height: 33.1px; |
|||
flex: 1 0 0; |
|||
overflow: hidden; |
|||
color: #1f0303; |
|||
text-overflow: ellipsis; |
|||
white-space: nowrap; |
|||
font-family: "PingFang SC", sans-serif; |
|||
font-size: 21.06px; |
|||
font-style: normal; |
|||
font-weight: 700; |
|||
line-height: 33.1px; |
|||
display: inline-flex; |
|||
align-items: center; |
|||
} |
|||
|
|||
/* 子目录核心样式 */ |
|||
.sidebar-child-container { |
|||
width: 300px !important; |
|||
height: 60px !important; |
|||
margin-left: 2px !important; |
|||
background: #fee6e6 !important; |
|||
position: relative; |
|||
padding: 0 !important; |
|||
border-radius: 6.02px !important; |
|||
box-sizing: border-box !important; |
|||
} |
|||
|
|||
/* 子目录图标样式 */ |
|||
.sidebar-child-icon { |
|||
position: absolute; |
|||
left: 48px; |
|||
top: 50%; |
|||
transform: translateY(-50%); |
|||
font-size: 18px !important; |
|||
color: inherit; |
|||
} |
|||
|
|||
/* 子目录文字样式(未选中) */ |
|||
.sidebar-child-text { |
|||
position: absolute; |
|||
left: 58px; |
|||
top: 20px; |
|||
height: 33.1px; |
|||
overflow: hidden; |
|||
color: #03071f; |
|||
text-overflow: ellipsis; |
|||
white-space: nowrap; |
|||
font-family: "PingFang SC", sans-serif; |
|||
font-size: 18px; |
|||
font-style: normal; |
|||
font-weight: 400; |
|||
line-height: 22px; |
|||
} |
|||
|
|||
/* 子目录选中态样式 */ |
|||
.sidebar-child-container.is-active { |
|||
background: #ffffff !important; /* 选中后容器变为白色 */ |
|||
} |
|||
|
|||
/* 选中态文字样式 */ |
|||
.sidebar-child-container.is-active .sidebar-child-text { |
|||
color: #ff0000 !important; /* 选中后文字红色 */ |
|||
font-weight: 500; |
|||
} |
|||
|
|||
/* 覆盖Element Plus默认样式 */ |
|||
.el-sub-menu__title { |
|||
height: 33.1px !important; |
|||
line-height: 33.1px !important; |
|||
padding: 0 !important; |
|||
} |
|||
.el-menu-item { |
|||
border: none !important; |
|||
} |
|||
.el-menu--vertical .el-menu-item { |
|||
width: 300px !important; |
|||
} |
|||
|
|||
/* 退出登录 */ |
|||
.sidebar-logout { |
|||
position: absolute; |
|||
bottom: 30px; |
|||
left: 10%; |
|||
|
|||
color: #1f0303; |
|||
font-family: "PingFang SC", sans-serif; |
|||
font-size: 21.06px; |
|||
font-style: normal; |
|||
font-weight: 700; |
|||
line-height: 33.1px; |
|||
|
|||
cursor: pointer; |
|||
user-select: none; |
|||
transition: color 0.2s ease; |
|||
} |
|||
|
|||
/* 退出登录hover效果 */ |
|||
.sidebar-logout:hover { |
|||
color: #ff0000; |
|||
} |
|||
|
|||
/* 设置 */ |
|||
.sidebar-set { |
|||
position: absolute; |
|||
bottom: 30px; |
|||
left: 60%; |
|||
|
|||
color: #1f0303; |
|||
font-family: "PingFang SC", sans-serif; |
|||
font-size: 21.06px; |
|||
font-style: normal; |
|||
font-weight: 700; |
|||
line-height: 33.1px; |
|||
|
|||
cursor: pointer; |
|||
user-select: none; |
|||
transition: color 0.2s ease; |
|||
} |
|||
|
|||
/* 设置hover效果 */ |
|||
.sidebar-set:hover { |
|||
color: #ff0000; |
|||
} |
|||
</style> |
|||
@ -1,40 +0,0 @@ |
|||
<template> |
|||
<section class="app-main"> |
|||
<transition name="fade-transform" mode="out-in"> |
|||
<router-view :key="key" /> |
|||
</transition> |
|||
</section> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
name: 'AppMain', |
|||
computed: { |
|||
key() { |
|||
return this.$route.path |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.app-main { |
|||
/*50 = navbar */ |
|||
min-height: calc(100vh - 50px); |
|||
width: 100%; |
|||
position: relative; |
|||
overflow: hidden; |
|||
} |
|||
.fixed-header+.app-main { |
|||
padding-top: 50px; |
|||
} |
|||
</style> |
|||
|
|||
<style lang="scss"> |
|||
// fix css style bug in open el-dialog |
|||
.el-popup-parent--hidden { |
|||
.fixed-header { |
|||
padding-right: 15px; |
|||
} |
|||
} |
|||
</style> |
|||
@ -1,133 +0,0 @@ |
|||
<template> |
|||
<div class="navbar"> |
|||
<hamburger :is-active="sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" /> |
|||
|
|||
<breadcrumb class="breadcrumb-container" /> |
|||
|
|||
<div class="right-menu"> |
|||
<el-dropdown class="avatar-container" trigger="click"> |
|||
<div class="avatar-wrapper"> |
|||
<img :src="avatar+'?imageView2/1/w/80/h/80'" class="user-avatar"> |
|||
<i class="el-icon-caret-bottom" /> |
|||
</div> |
|||
<el-dropdown-menu slot="dropdown" class="user-dropdown"> |
|||
<!-- <router-link to="/"> |
|||
<el-dropdown-item> |
|||
Home |
|||
</el-dropdown-item> |
|||
</router-link> --> |
|||
<el-dropdown-item @click.native="logout"> |
|||
<span style="display:block;">退出登录</span> |
|||
</el-dropdown-item> |
|||
</el-dropdown-menu> |
|||
</el-dropdown> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script> |
|||
import { mapGetters } from 'vuex' |
|||
import Breadcrumb from '@/components/Breadcrumb' |
|||
import Hamburger from '@/components/Hamburger' |
|||
|
|||
export default { |
|||
components: { |
|||
Breadcrumb, |
|||
Hamburger |
|||
}, |
|||
computed: { |
|||
...mapGetters([ |
|||
'sidebar', |
|||
'avatar' |
|||
]) |
|||
}, |
|||
methods: { |
|||
toggleSideBar() { |
|||
this.$store.dispatch('app/toggleSideBar') |
|||
}, |
|||
async logout() { |
|||
await this.$store.dispatch('user/logout') |
|||
this.$router.push(`/login?redirect=${this.$route.fullPath}`) |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
.navbar { |
|||
height: 50px; |
|||
overflow: hidden; |
|||
position: relative; |
|||
background: #fff; |
|||
box-shadow: 0 1px 4px rgba(0,21,41,.08); |
|||
|
|||
.hamburger-container { |
|||
line-height: 46px; |
|||
height: 100%; |
|||
float: left; |
|||
cursor: pointer; |
|||
transition: background .3s; |
|||
-webkit-tap-highlight-color:transparent; |
|||
|
|||
&:hover { |
|||
background: rgba(0, 0, 0, .025) |
|||
} |
|||
} |
|||
|
|||
.breadcrumb-container { |
|||
float: left; |
|||
} |
|||
|
|||
.right-menu { |
|||
float: right; |
|||
height: 100%; |
|||
line-height: 50px; |
|||
|
|||
&:focus { |
|||
outline: none; |
|||
} |
|||
|
|||
.right-menu-item { |
|||
display: inline-block; |
|||
padding: 0 8px; |
|||
height: 100%; |
|||
font-size: 18px; |
|||
color: #5a5e66; |
|||
vertical-align: text-bottom; |
|||
|
|||
&.hover-effect { |
|||
cursor: pointer; |
|||
transition: background .3s; |
|||
|
|||
&:hover { |
|||
background: rgba(0, 0, 0, .025) |
|||
} |
|||
} |
|||
} |
|||
|
|||
.avatar-container { |
|||
margin-right: 30px; |
|||
|
|||
.avatar-wrapper { |
|||
margin-top: 5px; |
|||
position: relative; |
|||
|
|||
.user-avatar { |
|||
cursor: pointer; |
|||
width: 40px; |
|||
height: 40px; |
|||
border-radius: 10px; |
|||
} |
|||
|
|||
.el-icon-caret-bottom { |
|||
cursor: pointer; |
|||
position: absolute; |
|||
right: -20px; |
|||
top: 25px; |
|||
font-size: 12px; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
</style> |
|||
@ -1,26 +0,0 @@ |
|||
export default { |
|||
computed: { |
|||
device() { |
|||
return this.$store.state.app.device |
|||
} |
|||
}, |
|||
mounted() { |
|||
// In order to fix the click on menu on the ios device will trigger the mouseleave bug
|
|||
// https://github.com/PanJiaChen/vue-element-admin/issues/1135
|
|||
this.fixBugIniOS() |
|||
}, |
|||
methods: { |
|||
fixBugIniOS() { |
|||
const $subMenu = this.$refs.subMenu |
|||
if ($subMenu) { |
|||
const handleMouseleave = $subMenu.handleMouseleave |
|||
$subMenu.handleMouseleave = (e) => { |
|||
if (this.device === 'mobile') { |
|||
return |
|||
} |
|||
handleMouseleave(e) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,41 +0,0 @@ |
|||
<script> |
|||
export default { |
|||
name: 'MenuItem', |
|||
functional: true, |
|||
props: { |
|||
icon: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
title: { |
|||
type: String, |
|||
default: '' |
|||
} |
|||
}, |
|||
render(h, context) { |
|||
const { icon, title } = context.props |
|||
const vnodes = [] |
|||
|
|||
if (icon) { |
|||
if (icon.includes('el-icon')) { |
|||
vnodes.push(<i class={[icon, 'sub-el-icon']} />) |
|||
} else { |
|||
vnodes.push(<svg-icon icon-class={icon}/>) |
|||
} |
|||
} |
|||
|
|||
if (title) { |
|||
vnodes.push(<span slot='title'>{(title)}</span>) |
|||
} |
|||
return vnodes |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.sub-el-icon { |
|||
color: currentColor; |
|||
width: 1em; |
|||
height: 1em; |
|||
} |
|||
</style> |
|||
@ -1,43 +0,0 @@ |
|||
<template> |
|||
<component :is="type" v-bind="linkProps(to)"> |
|||
<slot /> |
|||
</component> |
|||
</template> |
|||
|
|||
<script> |
|||
import { isExternal } from '@/utils/validate' |
|||
|
|||
export default { |
|||
props: { |
|||
to: { |
|||
type: String, |
|||
required: true |
|||
} |
|||
}, |
|||
computed: { |
|||
isExternal() { |
|||
return isExternal(this.to) |
|||
}, |
|||
type() { |
|||
if (this.isExternal) { |
|||
return 'a' |
|||
} |
|||
return 'router-link' |
|||
} |
|||
}, |
|||
methods: { |
|||
linkProps(to) { |
|||
if (this.isExternal) { |
|||
return { |
|||
href: to, |
|||
target: '_blank', |
|||
rel: 'noopener' |
|||
} |
|||
} |
|||
return { |
|||
to: to |
|||
} |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
@ -1,82 +0,0 @@ |
|||
<template> |
|||
<div class="sidebar-logo-container" :class="{'collapse':collapse}"> |
|||
<transition name="sidebarLogoFade"> |
|||
<router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/"> |
|||
<img v-if="logo" :src="logo" class="sidebar-logo"> |
|||
<h1 v-else class="sidebar-title">{{ title }} </h1> |
|||
</router-link> |
|||
<router-link v-else key="expand" class="sidebar-logo-link" to="/"> |
|||
<img v-if="logo" :src="logo" class="sidebar-logo"> |
|||
<h1 class="sidebar-title">{{ title }} </h1> |
|||
</router-link> |
|||
</transition> |
|||
</div> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
name: 'SidebarLogo', |
|||
props: { |
|||
collapse: { |
|||
type: Boolean, |
|||
required: true |
|||
} |
|||
}, |
|||
data() { |
|||
return { |
|||
title: 'Vue Admin Template', |
|||
logo: 'https://wpimg.wallstcn.com/69a1c46c-eb1c-4b46-8bd4-e9e686ef5251.png' |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
.sidebarLogoFade-enter-active { |
|||
transition: opacity 1.5s; |
|||
} |
|||
|
|||
.sidebarLogoFade-enter, |
|||
.sidebarLogoFade-leave-to { |
|||
opacity: 0; |
|||
} |
|||
|
|||
.sidebar-logo-container { |
|||
position: relative; |
|||
width: 100%; |
|||
height: 50px; |
|||
line-height: 50px; |
|||
background: #2b2f3a; |
|||
text-align: center; |
|||
overflow: hidden; |
|||
|
|||
& .sidebar-logo-link { |
|||
height: 100%; |
|||
width: 100%; |
|||
|
|||
& .sidebar-logo { |
|||
width: 32px; |
|||
height: 32px; |
|||
vertical-align: middle; |
|||
margin-right: 12px; |
|||
} |
|||
|
|||
& .sidebar-title { |
|||
display: inline-block; |
|||
margin: 0; |
|||
color: #fff; |
|||
font-weight: 600; |
|||
line-height: 50px; |
|||
font-size: 14px; |
|||
font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif; |
|||
vertical-align: middle; |
|||
} |
|||
} |
|||
|
|||
&.collapse { |
|||
.sidebar-logo { |
|||
margin-right: 0px; |
|||
} |
|||
} |
|||
} |
|||
</style> |
|||
@ -1,95 +0,0 @@ |
|||
<template> |
|||
<div v-if="!item.hidden"> |
|||
<template v-if="hasOneShowingChild(item.children,item) && (!onlyOneChild.children||onlyOneChild.noShowingChildren)&&!item.alwaysShow"> |
|||
<app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path)"> |
|||
<el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{'submenu-title-noDropdown':!isNest}"> |
|||
<item :icon="onlyOneChild.meta.icon||(item.meta&&item.meta.icon)" :title="onlyOneChild.meta.title" /> |
|||
</el-menu-item> |
|||
</app-link> |
|||
</template> |
|||
|
|||
<el-submenu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body> |
|||
<template slot="title"> |
|||
<item v-if="item.meta" :icon="item.meta && item.meta.icon" :title="item.meta.title" /> |
|||
</template> |
|||
<sidebar-item |
|||
v-for="child in item.children" |
|||
:key="child.path" |
|||
:is-nest="true" |
|||
:item="child" |
|||
:base-path="resolvePath(child.path)" |
|||
class="nest-menu" |
|||
/> |
|||
</el-submenu> |
|||
</div> |
|||
</template> |
|||
|
|||
<script> |
|||
import path from 'path' |
|||
import { isExternal } from '@/utils/validate' |
|||
import Item from './Item' |
|||
import AppLink from './Link' |
|||
import FixiOSBug from './FixiOSBug' |
|||
|
|||
export default { |
|||
name: 'SidebarItem', |
|||
components: { Item, AppLink }, |
|||
mixins: [FixiOSBug], |
|||
props: { |
|||
// route object |
|||
item: { |
|||
type: Object, |
|||
required: true |
|||
}, |
|||
isNest: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
basePath: { |
|||
type: String, |
|||
default: '' |
|||
} |
|||
}, |
|||
data() { |
|||
// To fix https://github.com/PanJiaChen/vue-admin-template/issues/237 |
|||
// TODO: refactor with render function |
|||
this.onlyOneChild = null |
|||
return {} |
|||
}, |
|||
methods: { |
|||
hasOneShowingChild(children = [], parent) { |
|||
const showingChildren = children.filter(item => { |
|||
if (item.hidden) { |
|||
return false |
|||
} else { |
|||
// Temp set(will be used if only has one showing child) |
|||
this.onlyOneChild = item |
|||
return true |
|||
} |
|||
}) |
|||
|
|||
// When there is only one child router, the child router is displayed by default |
|||
if (showingChildren.length === 1) { |
|||
return true |
|||
} |
|||
|
|||
// Show parent if there are no child router to display |
|||
if (showingChildren.length === 0) { |
|||
this.onlyOneChild = { ... parent, path: '', noShowingChildren: true } |
|||
return true |
|||
} |
|||
|
|||
return false |
|||
}, |
|||
resolvePath(routePath) { |
|||
if (isExternal(routePath)) { |
|||
return routePath |
|||
} |
|||
if (isExternal(this.basePath)) { |
|||
return this.basePath |
|||
} |
|||
return path.resolve(this.basePath, routePath) |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
@ -1,56 +0,0 @@ |
|||
<template> |
|||
<div :class="{'has-logo':showLogo}"> |
|||
<logo v-if="showLogo" :collapse="isCollapse" /> |
|||
<el-scrollbar wrap-class="scrollbar-wrapper"> |
|||
<el-menu |
|||
:default-active="activeMenu" |
|||
:collapse="isCollapse" |
|||
:background-color="variables.menuBg" |
|||
:text-color="variables.menuText" |
|||
:unique-opened="false" |
|||
:active-text-color="variables.menuActiveText" |
|||
:collapse-transition="false" |
|||
mode="vertical" |
|||
> |
|||
<sidebar-item v-for="route in routes" :key="route.path" :item="route" :base-path="route.path" /> |
|||
</el-menu> |
|||
</el-scrollbar> |
|||
</div> |
|||
</template> |
|||
|
|||
<script> |
|||
import { mapGetters } from 'vuex' |
|||
import Logo from './Logo' |
|||
import SidebarItem from './SidebarItem' |
|||
import variables from '@/styles/variables.scss' |
|||
|
|||
export default { |
|||
components: { SidebarItem, Logo }, |
|||
computed: { |
|||
...mapGetters([ |
|||
'sidebar' |
|||
]), |
|||
routes() { |
|||
return this.$router.options.routes |
|||
}, |
|||
activeMenu() { |
|||
const route = this.$route |
|||
const { meta, path } = route |
|||
// if set path, the sidebar will highlight the path you set |
|||
if (meta.activeMenu) { |
|||
return meta.activeMenu |
|||
} |
|||
return path |
|||
}, |
|||
showLogo() { |
|||
return this.$store.state.settings.sidebarLogo |
|||
}, |
|||
variables() { |
|||
return variables |
|||
}, |
|||
isCollapse() { |
|||
return !this.sidebar.opened |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
@ -1,3 +0,0 @@ |
|||
export { default as Navbar } from './Navbar' |
|||
export { default as Sidebar } from './Sidebar' |
|||
export { default as AppMain } from './AppMain' |
|||
@ -1,93 +0,0 @@ |
|||
<template> |
|||
<div :class="classObj" class="app-wrapper"> |
|||
<div v-if="device==='mobile'&&sidebar.opened" class="drawer-bg" @click="handleClickOutside" /> |
|||
<sidebar class="sidebar-container" /> |
|||
<div class="main-container"> |
|||
<div :class="{'fixed-header':fixedHeader}"> |
|||
<navbar /> |
|||
</div> |
|||
<app-main /> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script> |
|||
import { Navbar, Sidebar, AppMain } from './components' |
|||
import ResizeMixin from './mixin/ResizeHandler' |
|||
|
|||
export default { |
|||
name: 'Layout', |
|||
components: { |
|||
Navbar, |
|||
Sidebar, |
|||
AppMain |
|||
}, |
|||
mixins: [ResizeMixin], |
|||
computed: { |
|||
sidebar() { |
|||
return this.$store.state.app.sidebar |
|||
}, |
|||
device() { |
|||
return this.$store.state.app.device |
|||
}, |
|||
fixedHeader() { |
|||
return this.$store.state.settings.fixedHeader |
|||
}, |
|||
classObj() { |
|||
return { |
|||
hideSidebar: !this.sidebar.opened, |
|||
openSidebar: this.sidebar.opened, |
|||
withoutAnimation: this.sidebar.withoutAnimation, |
|||
mobile: this.device === 'mobile' |
|||
} |
|||
} |
|||
}, |
|||
methods: { |
|||
handleClickOutside() { |
|||
this.$store.dispatch('app/closeSideBar', { withoutAnimation: false }) |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
@import "~@/styles/mixin.scss"; |
|||
@import "~@/styles/variables.scss"; |
|||
|
|||
.app-wrapper { |
|||
@include clearfix; |
|||
position: relative; |
|||
height: 100%; |
|||
width: 100%; |
|||
&.mobile.openSidebar{ |
|||
position: fixed; |
|||
top: 0; |
|||
} |
|||
} |
|||
.drawer-bg { |
|||
background: #000; |
|||
opacity: 0.3; |
|||
width: 100%; |
|||
top: 0; |
|||
height: 100%; |
|||
position: absolute; |
|||
z-index: 999; |
|||
} |
|||
|
|||
.fixed-header { |
|||
position: fixed; |
|||
top: 0; |
|||
right: 0; |
|||
z-index: 9; |
|||
width: calc(100% - #{$sideBarWidth}); |
|||
transition: width 0.28s; |
|||
} |
|||
|
|||
.hideSidebar .fixed-header { |
|||
width: calc(100% - 54px) |
|||
} |
|||
|
|||
.mobile .fixed-header { |
|||
width: 100%; |
|||
} |
|||
</style> |
|||
@ -1,45 +0,0 @@ |
|||
import store from '@/store' |
|||
|
|||
const { body } = document |
|||
const WIDTH = 992 // refer to Bootstrap's responsive design
|
|||
|
|||
export default { |
|||
watch: { |
|||
$route(route) { |
|||
if (this.device === 'mobile' && this.sidebar.opened) { |
|||
store.dispatch('app/closeSideBar', { withoutAnimation: false }) |
|||
} |
|||
} |
|||
}, |
|||
beforeMount() { |
|||
window.addEventListener('resize', this.$_resizeHandler) |
|||
}, |
|||
beforeDestroy() { |
|||
window.removeEventListener('resize', this.$_resizeHandler) |
|||
}, |
|||
mounted() { |
|||
const isMobile = this.$_isMobile() |
|||
if (isMobile) { |
|||
store.dispatch('app/toggleDevice', 'mobile') |
|||
store.dispatch('app/closeSideBar', { withoutAnimation: true }) |
|||
} |
|||
}, |
|||
methods: { |
|||
// use $_ for mixins properties
|
|||
// https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential
|
|||
$_isMobile() { |
|||
const rect = body.getBoundingClientRect() |
|||
return rect.width - 1 < WIDTH |
|||
}, |
|||
$_resizeHandler() { |
|||
if (!document.hidden) { |
|||
const isMobile = this.$_isMobile() |
|||
store.dispatch('app/toggleDevice', isMobile ? 'mobile' : 'desktop') |
|||
|
|||
if (isMobile) { |
|||
store.dispatch('app/closeSideBar', { withoutAnimation: true }) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,43 +1,26 @@ |
|||
import Vue from 'vue' |
|||
|
|||
import 'normalize.css/normalize.css' // A modern alternative to CSS resets
|
|||
|
|||
import ElementUI from 'element-ui' |
|||
import 'element-ui/lib/theme-chalk/index.css' |
|||
import locale from 'element-ui/lib/locale/lang/en' // lang i18n
|
|||
|
|||
import '@/styles/index.scss' // global css
|
|||
|
|||
import App from './App' |
|||
import store from './store' |
|||
import { createApp } from 'vue' |
|||
import { createPinia } from 'pinia' |
|||
import App from './App.vue' |
|||
import router from './router' |
|||
|
|||
import '@/icons' // icon
|
|||
import '@/permission' // permission control
|
|||
|
|||
/** |
|||
* 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() |
|||
// 导入 Element Plus 样式和组件
|
|||
import ElementPlus from 'element-plus' |
|||
// 导入 Element Plus 图标
|
|||
import * as ElementPlusIconsVue from '@element-plus/icons-vue' |
|||
// 全局中文
|
|||
import zhCn from 'element-plus/dist/locale/zh-cn.mjs' |
|||
import 'element-plus/dist/index.css' |
|||
import 'normalize.css'; |
|||
|
|||
const app = createApp(App) |
|||
|
|||
for (const [key, component] of Object.entries(ElementPlusIconsVue)) { |
|||
app.component(key, component) |
|||
} |
|||
|
|||
// set ElementUI lang to EN
|
|||
Vue.use(ElementUI, { locale }) |
|||
// 如果想要中文版 element-ui,按如下方式声明
|
|||
// Vue.use(ElementUI)
|
|||
|
|||
Vue.config.productionTip = false |
|||
|
|||
new Vue({ |
|||
el: '#app', |
|||
router, |
|||
store, |
|||
render: h => h(App) |
|||
app.use(ElementPlus, { |
|||
locale: zhCn, |
|||
}) |
|||
app.use(createPinia()) |
|||
app.use(router) |
|||
|
|||
app.mount('#app') |
|||
@ -1,64 +0,0 @@ |
|||
import router from './router' |
|||
import store from './store' |
|||
import { Message } from 'element-ui' |
|||
import NProgress from 'nprogress' // progress bar
|
|||
import 'nprogress/nprogress.css' // progress bar style
|
|||
import { getToken } from '@/utils/auth' // get token from cookie
|
|||
import getPageTitle from '@/utils/get-page-title' |
|||
|
|||
NProgress.configure({ showSpinner: false }) // NProgress Configuration
|
|||
|
|||
const whiteList = ['/login'] // no redirect whitelist
|
|||
|
|||
router.beforeEach(async(to, from, next) => { |
|||
// start progress bar
|
|||
NProgress.start() |
|||
|
|||
// set page title
|
|||
document.title = getPageTitle(to.meta.title) |
|||
|
|||
// determine whether the user has logged in
|
|||
const hasToken = getToken() |
|||
|
|||
if (hasToken) { |
|||
if (to.path === '/login') { |
|||
// if is logged in, redirect to the home page
|
|||
next({ path: '/' }) |
|||
NProgress.done() |
|||
} else { |
|||
const hasGetUserInfo = store.getters.name |
|||
if (hasGetUserInfo) { |
|||
next() |
|||
} else { |
|||
try { |
|||
// get user info
|
|||
await store.dispatch('user/getInfo') |
|||
|
|||
next() |
|||
} catch (error) { |
|||
// remove token and go to login page to re-login
|
|||
await store.dispatch('user/resetToken') |
|||
Message.error(error || 'Has Error') |
|||
next(`/login?redirect=${to.path}`) |
|||
NProgress.done() |
|||
} |
|||
} |
|||
} |
|||
} else { |
|||
/* has no token*/ |
|||
|
|||
if (whiteList.indexOf(to.path) !== -1) { |
|||
// in the free login whitelist, go directly
|
|||
next() |
|||
} else { |
|||
// other pages that do not have permission to access are redirected to the login page.
|
|||
next(`/login?redirect=${to.path}`) |
|||
NProgress.done() |
|||
} |
|||
} |
|||
}) |
|||
|
|||
router.afterEach(() => { |
|||
// finish progress bar
|
|||
NProgress.done() |
|||
}) |
|||
@ -1,82 +1,111 @@ |
|||
import Vue from 'vue' |
|||
import Router from 'vue-router' |
|||
import { createRouter, createWebHistory } from 'vue-router' |
|||
import Layout from '../layout/Layout.vue' |
|||
|
|||
Vue.use(Router) |
|||
|
|||
import Layout from '@/layout' |
|||
|
|||
export const constantRoutes = [ |
|||
{ |
|||
path: '/login', |
|||
component: () => import('@/views/Login'), |
|||
hidden: true |
|||
const routes = [ |
|||
{ |
|||
path: '/', |
|||
redirect: '/userPermissions/market' |
|||
}, |
|||
|
|||
{ |
|||
path: '/404', |
|||
component: () => import('@/views/404'), |
|||
hidden: true |
|||
}, |
|||
|
|||
{ |
|||
path: '/', |
|||
component: Layout, |
|||
redirect: '/userPermissions/market' |
|||
path: '/login', |
|||
name: 'login', |
|||
component: () => import('../views/Login.vue'), |
|||
meta: { |
|||
title: '登录', |
|||
hiddenSidebar: true, |
|||
requireAuth: false // 登录页不需要登录
|
|||
} |
|||
}, |
|||
|
|||
{ |
|||
path: '/userPermissions', |
|||
path: '', |
|||
component: Layout, |
|||
meta: { |
|||
title: '用户权限管理', |
|||
icon: 'user' |
|||
requireAuth: true // Layout下所有子路由都需要登录
|
|||
}, |
|||
alwaysShow: true, |
|||
children: [ |
|||
{ |
|||
path: 'market', |
|||
name: 'market', |
|||
component: () => import('@/views/UserPermissions/Market.vue'), |
|||
meta: { |
|||
title: '行情期限' |
|||
} |
|||
}, |
|||
{ |
|||
path: 'module', |
|||
name: 'module', |
|||
component: () => import('@/views/UserPermissions/Module.vue'), |
|||
meta: { |
|||
title: '模块期限' |
|||
} |
|||
}, |
|||
// DeepMate操作日志
|
|||
{ |
|||
path: 'LogDeepMate', |
|||
name: 'LogDeepMate', |
|||
component: () => import('@/views/UserPermissions/LogDeepMate.vue') |
|||
}, |
|||
// 深度探索操作日志
|
|||
{ |
|||
path: 'LogDeepExplore', |
|||
name: 'LogDeepExplore', |
|||
component: () => import('@/views/UserPermissions/LogDeepExplore.vue') |
|||
path: 'userPermissions', |
|||
name: 'userPermissions', |
|||
meta: { title: '用户权限管理', icon: "UserFilled", showSidebar: true, isParentNav: true }, |
|||
children: [ |
|||
{ |
|||
path: 'market', |
|||
name: 'market', |
|||
component: () => import('../views/UserPermissions/Market.vue'), |
|||
meta: { title: '行情期限', showSidebar: true } |
|||
}, |
|||
{ |
|||
path: 'module', |
|||
name: 'module', |
|||
component: () => import('../views/UserPermissions/Module.vue'), |
|||
meta: { title: '模块期限', showSidebar: true } |
|||
}, |
|||
{ |
|||
path: 'invitedLook', |
|||
name: 'invitedLook', |
|||
component: () => import('../views/UserPermissions/invitedLook.vue'), |
|||
meta: { title: '查看被邀请用户', showSidebar: true } |
|||
}, |
|||
// 深度探索--操作日志
|
|||
{ |
|||
path: 'logDeepexplore', |
|||
name: 'logDeepexplore', |
|||
component: () => import('../views/UserPermissions/LogDeepExplore.vue'), |
|||
meta: { hidden: true } |
|||
}, |
|||
// DeepMate--操作日志
|
|||
{ |
|||
path: 'logDeepMate', |
|||
name: 'logDeepMate', |
|||
component: () => import('../views/UserPermissions/LogDeepMate.vue'), |
|||
meta: { hidden: true } |
|||
}, |
|||
// 行情期限--操作日志
|
|||
{ |
|||
path: 'logMarket', |
|||
name: 'logMarket', |
|||
component: () => import('../views/UserPermissions/LogMarket.vue'), |
|||
meta: { hidden: true } |
|||
}, |
|||
// 导出列表页面
|
|||
{ |
|||
path: 'export', |
|||
name: 'export', |
|||
component: () => import('../views/UserPermissions/Export.vue'), |
|||
meta: { hidden: true } |
|||
} |
|||
] |
|||
} |
|||
] |
|||
}, |
|||
|
|||
{ path: '*', redirect: '/404', hidden: true } |
|||
} |
|||
] |
|||
|
|||
const createRouter = () => new Router({ |
|||
scrollBehavior: () => ({ y: 0 }), |
|||
routes: constantRoutes |
|||
const router = createRouter({ |
|||
history: createWebHistory(import.meta.env.BASE_URL), |
|||
routes |
|||
}) |
|||
|
|||
const router = createRouter() |
|||
// 全局前置守卫
|
|||
router.beforeEach((to, from, next) => { |
|||
// 登录状态判断
|
|||
const token = localStorage.getItem('token') |
|||
|
|||
export function resetRouter() { |
|||
const newRouter = createRouter() |
|||
router.matcher = newRouter.matcher |
|||
} |
|||
// 权限拦截逻辑
|
|||
if (to.meta.requireAuth) { |
|||
// 目标页面需要登录:已登录→放行,未登录→跳登录页
|
|||
if (token) { |
|||
next() // 已登录,放行
|
|||
} else { |
|||
next({ path: '/login' }) // 跳登录页
|
|||
} |
|||
} else { |
|||
// 目标页面不需要登录:已登录→跳首页,未登录→放行
|
|||
if (token && to.path === '/login') { |
|||
next('/') // 自动跳首页
|
|||
} else { |
|||
next() // 未登录,放行到登录页
|
|||
} |
|||
} |
|||
}) |
|||
|
|||
export default router |
|||
export default router |
|||
@ -1,16 +0,0 @@ |
|||
module.exports = { |
|||
|
|||
title: 'Vue Admin Template', |
|||
|
|||
/** |
|||
* @type {boolean} true | false |
|||
* @description Whether fix the header |
|||
*/ |
|||
fixedHeader: false, |
|||
|
|||
/** |
|||
* @type {boolean} true | false |
|||
* @description Whether show the logo in sidebar |
|||
*/ |
|||
sidebarLogo: false |
|||
} |
|||
@ -1,8 +0,0 @@ |
|||
const getters = { |
|||
sidebar: state => state.app.sidebar, |
|||
device: state => state.app.device, |
|||
token: state => state.user.token, |
|||
avatar: state => state.user.avatar, |
|||
name: state => state.user.name |
|||
} |
|||
export default getters |
|||
@ -1,19 +0,0 @@ |
|||
import Vue from 'vue' |
|||
import Vuex from 'vuex' |
|||
import getters from './getters' |
|||
import app from './modules/app' |
|||
import settings from './modules/settings' |
|||
import user from './modules/user' |
|||
|
|||
Vue.use(Vuex) |
|||
|
|||
const store = new Vuex.Store({ |
|||
modules: { |
|||
app, |
|||
settings, |
|||
user |
|||
}, |
|||
getters |
|||
}) |
|||
|
|||
export default store |
|||
@ -1,48 +0,0 @@ |
|||
import Cookies from 'js-cookie' |
|||
|
|||
const state = { |
|||
sidebar: { |
|||
opened: Cookies.get('sidebarStatus') ? !!+Cookies.get('sidebarStatus') : true, |
|||
withoutAnimation: false |
|||
}, |
|||
device: 'desktop' |
|||
} |
|||
|
|||
const mutations = { |
|||
TOGGLE_SIDEBAR: state => { |
|||
state.sidebar.opened = !state.sidebar.opened |
|||
state.sidebar.withoutAnimation = false |
|||
if (state.sidebar.opened) { |
|||
Cookies.set('sidebarStatus', 1) |
|||
} else { |
|||
Cookies.set('sidebarStatus', 0) |
|||
} |
|||
}, |
|||
CLOSE_SIDEBAR: (state, withoutAnimation) => { |
|||
Cookies.set('sidebarStatus', 0) |
|||
state.sidebar.opened = false |
|||
state.sidebar.withoutAnimation = withoutAnimation |
|||
}, |
|||
TOGGLE_DEVICE: (state, device) => { |
|||
state.device = device |
|||
} |
|||
} |
|||
|
|||
const actions = { |
|||
toggleSideBar({ commit }) { |
|||
commit('TOGGLE_SIDEBAR') |
|||
}, |
|||
closeSideBar({ commit }, { withoutAnimation }) { |
|||
commit('CLOSE_SIDEBAR', withoutAnimation) |
|||
}, |
|||
toggleDevice({ commit }, device) { |
|||
commit('TOGGLE_DEVICE', device) |
|||
} |
|||
} |
|||
|
|||
export default { |
|||
namespaced: true, |
|||
state, |
|||
mutations, |
|||
actions |
|||
} |
|||
@ -1,32 +0,0 @@ |
|||
import defaultSettings from '@/settings' |
|||
|
|||
const { showSettings, fixedHeader, sidebarLogo } = defaultSettings |
|||
|
|||
const state = { |
|||
showSettings: showSettings, |
|||
fixedHeader: fixedHeader, |
|||
sidebarLogo: sidebarLogo |
|||
} |
|||
|
|||
const mutations = { |
|||
CHANGE_SETTING: (state, { key, value }) => { |
|||
// eslint-disable-next-line no-prototype-builtins
|
|||
if (state.hasOwnProperty(key)) { |
|||
state[key] = value |
|||
} |
|||
} |
|||
} |
|||
|
|||
const actions = { |
|||
changeSetting({ commit }, data) { |
|||
commit('CHANGE_SETTING', data) |
|||
} |
|||
} |
|||
|
|||
export default { |
|||
namespaced: true, |
|||
state, |
|||
mutations, |
|||
actions |
|||
} |
|||
|
|||
@ -1,97 +0,0 @@ |
|||
import { login, logout, getInfo } from '@/api/user' |
|||
import { getToken, setToken, removeToken } from '@/utils/auth' |
|||
import { resetRouter } from '@/router' |
|||
|
|||
const getDefaultState = () => { |
|||
return { |
|||
token: getToken(), |
|||
name: '', |
|||
avatar: '' |
|||
} |
|||
} |
|||
|
|||
const state = getDefaultState() |
|||
|
|||
const mutations = { |
|||
RESET_STATE: (state) => { |
|||
Object.assign(state, getDefaultState()) |
|||
}, |
|||
SET_TOKEN: (state, token) => { |
|||
state.token = token |
|||
}, |
|||
SET_NAME: (state, name) => { |
|||
state.name = name |
|||
}, |
|||
SET_AVATAR: (state, avatar) => { |
|||
state.avatar = avatar |
|||
} |
|||
} |
|||
|
|||
const actions = { |
|||
// user login
|
|||
login({ commit }, userInfo) { |
|||
const { username, password } = userInfo |
|||
return new Promise((resolve, reject) => { |
|||
login({ username: username.trim(), password: password }).then(response => { |
|||
const { data } = response |
|||
commit('SET_TOKEN', data.token) |
|||
setToken(data.token) |
|||
resolve() |
|||
}).catch(error => { |
|||
reject(error) |
|||
}) |
|||
}) |
|||
}, |
|||
|
|||
// get user info
|
|||
getInfo({ commit, state }) { |
|||
return new Promise((resolve, reject) => { |
|||
getInfo(state.token).then(response => { |
|||
const { data } = response |
|||
|
|||
if (!data) { |
|||
return reject('Verification failed, please Login again.') |
|||
} |
|||
|
|||
const { name, avatar } = data |
|||
|
|||
commit('SET_NAME', name) |
|||
commit('SET_AVATAR', avatar) |
|||
resolve(data) |
|||
}).catch(error => { |
|||
reject(error) |
|||
}) |
|||
}) |
|||
}, |
|||
|
|||
// user logout
|
|||
logout({ commit, state }) { |
|||
return new Promise((resolve, reject) => { |
|||
logout(state.token).then(() => { |
|||
removeToken() // must remove token first
|
|||
resetRouter() |
|||
commit('RESET_STATE') |
|||
resolve() |
|||
}).catch(error => { |
|||
reject(error) |
|||
}) |
|||
}) |
|||
}, |
|||
|
|||
// remove token
|
|||
resetToken({ commit }) { |
|||
return new Promise(resolve => { |
|||
removeToken() // must remove token first
|
|||
commit('RESET_STATE') |
|||
resolve() |
|||
}) |
|||
} |
|||
} |
|||
|
|||
export default { |
|||
namespaced: true, |
|||
state, |
|||
mutations, |
|||
actions |
|||
} |
|||
|
|||
@ -0,0 +1,19 @@ |
|||
/** @format */ |
|||
|
|||
import { defineStore } from "pinia"; |
|||
import { ref } from "vue"; |
|||
import { getPermissionApi } from "../api/userPermissions"; |
|||
export const usePermissionStore = defineStore("permission", () => { |
|||
//permission
|
|||
const permission = ref("-1"); |
|||
|
|||
async function getPermission() { |
|||
// token
|
|||
const token = localStorage.getItem("token"); |
|||
const result = await getPermissionApi({ token: token }); |
|||
permission.value = result.permission_level; |
|||
return permission.value; |
|||
} |
|||
|
|||
return { permission, getPermission }; |
|||
}); |
|||
@ -0,0 +1,79 @@ |
|||
:root { |
|||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; |
|||
line-height: 1.5; |
|||
font-weight: 400; |
|||
|
|||
color-scheme: light dark; |
|||
color: rgba(255, 255, 255, 0.87); |
|||
background-color: #242424; |
|||
|
|||
font-synthesis: none; |
|||
text-rendering: optimizeLegibility; |
|||
-webkit-font-smoothing: antialiased; |
|||
-moz-osx-font-smoothing: grayscale; |
|||
} |
|||
|
|||
a { |
|||
font-weight: 500; |
|||
color: #646cff; |
|||
text-decoration: inherit; |
|||
} |
|||
a:hover { |
|||
color: #535bf2; |
|||
} |
|||
|
|||
body { |
|||
margin: 0; |
|||
display: flex; |
|||
place-items: center; |
|||
min-width: 320px; |
|||
min-height: 100vh; |
|||
} |
|||
|
|||
h1 { |
|||
font-size: 3.2em; |
|||
line-height: 1.1; |
|||
} |
|||
|
|||
button { |
|||
border-radius: 8px; |
|||
border: 1px solid transparent; |
|||
padding: 0.6em 1.2em; |
|||
font-size: 1em; |
|||
font-weight: 500; |
|||
font-family: inherit; |
|||
background-color: #1a1a1a; |
|||
cursor: pointer; |
|||
transition: border-color 0.25s; |
|||
} |
|||
button:hover { |
|||
border-color: #646cff; |
|||
} |
|||
button:focus, |
|||
button:focus-visible { |
|||
outline: 4px auto -webkit-focus-ring-color; |
|||
} |
|||
|
|||
.card { |
|||
padding: 2em; |
|||
} |
|||
|
|||
#app { |
|||
max-width: 1280px; |
|||
margin: 0 auto; |
|||
padding: 2rem; |
|||
text-align: center; |
|||
} |
|||
|
|||
@media (prefers-color-scheme: light) { |
|||
:root { |
|||
color: #213547; |
|||
background-color: #ffffff; |
|||
} |
|||
a:hover { |
|||
color: #747bff; |
|||
} |
|||
button { |
|||
background-color: #f9f9f9; |
|||
} |
|||
} |
|||
@ -1,49 +0,0 @@ |
|||
// cover some element-ui styles |
|||
|
|||
.el-breadcrumb__inner, |
|||
.el-breadcrumb__inner a { |
|||
font-weight: 400 !important; |
|||
} |
|||
|
|||
.el-upload { |
|||
input[type="file"] { |
|||
display: none !important; |
|||
} |
|||
} |
|||
|
|||
.el-upload__input { |
|||
display: none; |
|||
} |
|||
|
|||
|
|||
// to fixed https://github.com/ElemeFE/element/issues/2461 |
|||
.el-dialog { |
|||
transform: none; |
|||
left: 0; |
|||
position: relative; |
|||
margin: 0 auto; |
|||
} |
|||
|
|||
// refine element ui upload |
|||
.upload-container { |
|||
.el-upload { |
|||
width: 100%; |
|||
|
|||
.el-upload-dragger { |
|||
width: 100%; |
|||
height: 200px; |
|||
} |
|||
} |
|||
} |
|||
|
|||
// dropdown |
|||
.el-dropdown-menu { |
|||
a { |
|||
display: block |
|||
} |
|||
} |
|||
|
|||
// to fix el-date-picker css style |
|||
.el-range-separator { |
|||
box-sizing: content-box; |
|||
} |
|||
@ -1,65 +0,0 @@ |
|||
@import './variables.scss'; |
|||
@import './mixin.scss'; |
|||
@import './transition.scss'; |
|||
@import './element-ui.scss'; |
|||
@import './sidebar.scss'; |
|||
|
|||
body { |
|||
height: 100%; |
|||
-moz-osx-font-smoothing: grayscale; |
|||
-webkit-font-smoothing: antialiased; |
|||
text-rendering: optimizeLegibility; |
|||
font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif; |
|||
} |
|||
|
|||
label { |
|||
font-weight: 700; |
|||
} |
|||
|
|||
html { |
|||
height: 100%; |
|||
box-sizing: border-box; |
|||
} |
|||
|
|||
#app { |
|||
height: 100%; |
|||
} |
|||
|
|||
*, |
|||
*:before, |
|||
*:after { |
|||
box-sizing: inherit; |
|||
} |
|||
|
|||
a:focus, |
|||
a:active { |
|||
outline: none; |
|||
} |
|||
|
|||
a, |
|||
a:focus, |
|||
a:hover { |
|||
cursor: pointer; |
|||
color: inherit; |
|||
text-decoration: none; |
|||
} |
|||
|
|||
div:focus { |
|||
outline: none; |
|||
} |
|||
|
|||
.clearfix { |
|||
&:after { |
|||
visibility: hidden; |
|||
display: block; |
|||
font-size: 0; |
|||
content: " "; |
|||
clear: both; |
|||
height: 0; |
|||
} |
|||
} |
|||
|
|||
// main-container global css |
|||
.app-container { |
|||
padding: 20px; |
|||
} |
|||
@ -1,28 +0,0 @@ |
|||
@mixin clearfix { |
|||
&:after { |
|||
content: ""; |
|||
display: table; |
|||
clear: both; |
|||
} |
|||
} |
|||
|
|||
@mixin scrollBar { |
|||
&::-webkit-scrollbar-track-piece { |
|||
background: #d3dce6; |
|||
} |
|||
|
|||
&::-webkit-scrollbar { |
|||
width: 6px; |
|||
} |
|||
|
|||
&::-webkit-scrollbar-thumb { |
|||
background: #99a9bf; |
|||
border-radius: 20px; |
|||
} |
|||
} |
|||
|
|||
@mixin relative { |
|||
position: relative; |
|||
width: 100%; |
|||
height: 100%; |
|||
} |
|||
@ -1,226 +0,0 @@ |
|||
#app { |
|||
|
|||
.main-container { |
|||
min-height: 100%; |
|||
transition: margin-left .28s; |
|||
margin-left: $sideBarWidth; |
|||
position: relative; |
|||
} |
|||
|
|||
.sidebar-container { |
|||
transition: width 0.28s; |
|||
width: $sideBarWidth !important; |
|||
background-color: $menuBg; |
|||
height: 100%; |
|||
position: fixed; |
|||
font-size: 0px; |
|||
top: 0; |
|||
bottom: 0; |
|||
left: 0; |
|||
z-index: 1001; |
|||
overflow: hidden; |
|||
|
|||
// reset element-ui css |
|||
.horizontal-collapse-transition { |
|||
transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out; |
|||
} |
|||
|
|||
.scrollbar-wrapper { |
|||
overflow-x: hidden !important; |
|||
} |
|||
|
|||
.el-scrollbar__bar.is-vertical { |
|||
right: 0px; |
|||
} |
|||
|
|||
.el-scrollbar { |
|||
height: 100%; |
|||
} |
|||
|
|||
&.has-logo { |
|||
.el-scrollbar { |
|||
height: calc(100% - 50px); |
|||
} |
|||
} |
|||
|
|||
.is-horizontal { |
|||
display: none; |
|||
} |
|||
|
|||
a { |
|||
display: inline-block; |
|||
width: 100%; |
|||
overflow: hidden; |
|||
} |
|||
|
|||
.svg-icon { |
|||
margin-right: 16px; |
|||
} |
|||
|
|||
.sub-el-icon { |
|||
margin-right: 12px; |
|||
margin-left: -2px; |
|||
} |
|||
|
|||
.el-menu { |
|||
border: none; |
|||
height: 100%; |
|||
width: 100% !important; |
|||
} |
|||
|
|||
// menu hover |
|||
.submenu-title-noDropdown, |
|||
.el-submenu__title { |
|||
&:hover { |
|||
background-color: $menuHover !important; |
|||
} |
|||
} |
|||
|
|||
.is-active>.el-submenu__title { |
|||
color: $subMenuActiveText !important; |
|||
} |
|||
|
|||
& .nest-menu .el-submenu>.el-submenu__title, |
|||
& .el-submenu .el-menu-item { |
|||
min-width: $sideBarWidth !important; |
|||
background-color: $subMenuBg !important; |
|||
|
|||
&:hover { |
|||
background-color: $subMenuHover !important; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.hideSidebar { |
|||
.sidebar-container { |
|||
width: 54px !important; |
|||
} |
|||
|
|||
.main-container { |
|||
margin-left: 54px; |
|||
} |
|||
|
|||
.submenu-title-noDropdown { |
|||
padding: 0 !important; |
|||
position: relative; |
|||
|
|||
.el-tooltip { |
|||
padding: 0 !important; |
|||
|
|||
.svg-icon { |
|||
margin-left: 20px; |
|||
} |
|||
|
|||
.sub-el-icon { |
|||
margin-left: 19px; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.el-submenu { |
|||
overflow: hidden; |
|||
|
|||
&>.el-submenu__title { |
|||
padding: 0 !important; |
|||
|
|||
.svg-icon { |
|||
margin-left: 20px; |
|||
} |
|||
|
|||
.sub-el-icon { |
|||
margin-left: 19px; |
|||
} |
|||
|
|||
.el-submenu__icon-arrow { |
|||
display: none; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.el-menu--collapse { |
|||
.el-submenu { |
|||
&>.el-submenu__title { |
|||
&>span { |
|||
height: 0; |
|||
width: 0; |
|||
overflow: hidden; |
|||
visibility: hidden; |
|||
display: inline-block; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
.el-menu--collapse .el-menu .el-submenu { |
|||
min-width: $sideBarWidth !important; |
|||
} |
|||
|
|||
// mobile responsive |
|||
.mobile { |
|||
.main-container { |
|||
margin-left: 0px; |
|||
} |
|||
|
|||
.sidebar-container { |
|||
transition: transform .28s; |
|||
width: $sideBarWidth !important; |
|||
} |
|||
|
|||
&.hideSidebar { |
|||
.sidebar-container { |
|||
pointer-events: none; |
|||
transition-duration: 0.3s; |
|||
transform: translate3d(-$sideBarWidth, 0, 0); |
|||
} |
|||
} |
|||
} |
|||
|
|||
.withoutAnimation { |
|||
|
|||
.main-container, |
|||
.sidebar-container { |
|||
transition: none; |
|||
} |
|||
} |
|||
} |
|||
|
|||
// when menu collapsed |
|||
.el-menu--vertical { |
|||
&>.el-menu { |
|||
.svg-icon { |
|||
margin-right: 16px; |
|||
} |
|||
.sub-el-icon { |
|||
margin-right: 12px; |
|||
margin-left: -2px; |
|||
} |
|||
} |
|||
|
|||
.nest-menu .el-submenu>.el-submenu__title, |
|||
.el-menu-item { |
|||
&:hover { |
|||
// you can use $subMenuHover |
|||
background-color: $menuHover !important; |
|||
} |
|||
} |
|||
|
|||
// the scroll bar appears when the subMenu is too long |
|||
>.el-menu--popup { |
|||
max-height: 100vh; |
|||
overflow-y: auto; |
|||
|
|||
&::-webkit-scrollbar-track-piece { |
|||
background: #d3dce6; |
|||
} |
|||
|
|||
&::-webkit-scrollbar { |
|||
width: 6px; |
|||
} |
|||
|
|||
&::-webkit-scrollbar-thumb { |
|||
background: #99a9bf; |
|||
border-radius: 20px; |
|||
} |
|||
} |
|||
} |
|||
@ -1,48 +0,0 @@ |
|||
// global transition css |
|||
|
|||
/* fade */ |
|||
.fade-enter-active, |
|||
.fade-leave-active { |
|||
transition: opacity 0.28s; |
|||
} |
|||
|
|||
.fade-enter, |
|||
.fade-leave-active { |
|||
opacity: 0; |
|||
} |
|||
|
|||
/* fade-transform */ |
|||
.fade-transform-leave-active, |
|||
.fade-transform-enter-active { |
|||
transition: all .5s; |
|||
} |
|||
|
|||
.fade-transform-enter { |
|||
opacity: 0; |
|||
transform: translateX(-30px); |
|||
} |
|||
|
|||
.fade-transform-leave-to { |
|||
opacity: 0; |
|||
transform: translateX(30px); |
|||
} |
|||
|
|||
/* breadcrumb transition */ |
|||
.breadcrumb-enter-active, |
|||
.breadcrumb-leave-active { |
|||
transition: all .5s; |
|||
} |
|||
|
|||
.breadcrumb-enter, |
|||
.breadcrumb-leave-active { |
|||
opacity: 0; |
|||
transform: translateX(20px); |
|||
} |
|||
|
|||
.breadcrumb-move { |
|||
transition: all .5s; |
|||
} |
|||
|
|||
.breadcrumb-leave-active { |
|||
position: absolute; |
|||
} |
|||
@ -1,25 +0,0 @@ |
|||
// sidebar |
|||
$menuText:#bfcbd9; |
|||
$menuActiveText:#409EFF; |
|||
$subMenuActiveText:#f4f4f5; //https://github.com/ElemeFE/element/issues/12951 |
|||
|
|||
$menuBg:#304156; |
|||
$menuHover:#263445; |
|||
|
|||
$subMenuBg:#1f2d3d; |
|||
$subMenuHover:#001528; |
|||
|
|||
$sideBarWidth: 210px; |
|||
|
|||
// the :export directive is the magic sauce for webpack |
|||
// https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass |
|||
:export { |
|||
menuText: $menuText; |
|||
menuActiveText: $menuActiveText; |
|||
subMenuActiveText: $subMenuActiveText; |
|||
menuBg: $menuBg; |
|||
menuHover: $menuHover; |
|||
subMenuBg: $subMenuBg; |
|||
subMenuHover: $subMenuHover; |
|||
sideBarWidth: $sideBarWidth; |
|||
} |
|||
@ -1,15 +0,0 @@ |
|||
import Cookies from 'js-cookie' |
|||
|
|||
const TokenKey = 'vue_admin_template_token' |
|||
|
|||
export function getToken() { |
|||
return Cookies.get(TokenKey) |
|||
} |
|||
|
|||
export function setToken(token) { |
|||
return Cookies.set(TokenKey, token) |
|||
} |
|||
|
|||
export function removeToken() { |
|||
return Cookies.remove(TokenKey) |
|||
} |
|||
@ -1,10 +0,0 @@ |
|||
import defaultSettings from '@/settings' |
|||
|
|||
const title = defaultSettings.title || 'Vue Admin Template' |
|||
|
|||
export default function getPageTitle(pageTitle) { |
|||
if (pageTitle) { |
|||
return `${pageTitle} - ${title}` |
|||
} |
|||
return `${title}` |
|||
} |
|||
@ -1,117 +0,0 @@ |
|||
/** |
|||
* Created by PanJiaChen on 16/11/18. |
|||
*/ |
|||
|
|||
/** |
|||
* Parse the time to string |
|||
* @param {(Object|string|number)} time |
|||
* @param {string} cFormat |
|||
* @returns {string | null} |
|||
*/ |
|||
export function parseTime(time, cFormat) { |
|||
if (arguments.length === 0 || !time) { |
|||
return null |
|||
} |
|||
const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}' |
|||
let date |
|||
if (typeof time === 'object') { |
|||
date = time |
|||
} else { |
|||
if ((typeof time === 'string')) { |
|||
if ((/^[0-9]+$/.test(time))) { |
|||
// support "1548221490638"
|
|||
time = parseInt(time) |
|||
} else { |
|||
// support safari
|
|||
// https://stackoverflow.com/questions/4310953/invalid-date-in-safari
|
|||
time = time.replace(new RegExp(/-/gm), '/') |
|||
} |
|||
} |
|||
|
|||
if ((typeof time === 'number') && (time.toString().length === 10)) { |
|||
time = time * 1000 |
|||
} |
|||
date = new Date(time) |
|||
} |
|||
const formatObj = { |
|||
y: date.getFullYear(), |
|||
m: date.getMonth() + 1, |
|||
d: date.getDate(), |
|||
h: date.getHours(), |
|||
i: date.getMinutes(), |
|||
s: date.getSeconds(), |
|||
a: date.getDay() |
|||
} |
|||
const time_str = format.replace(/{([ymdhisa])+}/g, (result, key) => { |
|||
const value = formatObj[key] |
|||
// Note: getDay() returns 0 on Sunday
|
|||
if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value ] } |
|||
return value.toString().padStart(2, '0') |
|||
}) |
|||
return time_str |
|||
} |
|||
|
|||
/** |
|||
* @param {number} time |
|||
* @param {string} option |
|||
* @returns {string} |
|||
*/ |
|||
export function formatTime(time, option) { |
|||
if (('' + time).length === 10) { |
|||
time = parseInt(time) * 1000 |
|||
} else { |
|||
time = +time |
|||
} |
|||
const d = new Date(time) |
|||
const now = Date.now() |
|||
|
|||
const diff = (now - d) / 1000 |
|||
|
|||
if (diff < 30) { |
|||
return '刚刚' |
|||
} else if (diff < 3600) { |
|||
// less 1 hour
|
|||
return Math.ceil(diff / 60) + '分钟前' |
|||
} else if (diff < 3600 * 24) { |
|||
return Math.ceil(diff / 3600) + '小时前' |
|||
} else if (diff < 3600 * 24 * 2) { |
|||
return '1天前' |
|||
} |
|||
if (option) { |
|||
return parseTime(time, option) |
|||
} else { |
|||
return ( |
|||
d.getMonth() + |
|||
1 + |
|||
'月' + |
|||
d.getDate() + |
|||
'日' + |
|||
d.getHours() + |
|||
'时' + |
|||
d.getMinutes() + |
|||
'分' |
|||
) |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* @param {string} url |
|||
* @returns {Object} |
|||
*/ |
|||
export function param2Obj(url) { |
|||
const search = decodeURIComponent(url.split('?')[1]).replace(/\+/g, ' ') |
|||
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 |
|||
} |
|||
@ -0,0 +1,84 @@ |
|||
// src/utils/myAxios.js
|
|||
import axios from 'axios' |
|||
import { ElMessage } from 'element-plus' |
|||
import router from '../router/index'; |
|||
|
|||
// 创建 axios 实例
|
|||
const myAxios = axios.create({ |
|||
baseURL: import.meta.env.VITE_API_BASE_URL, // 从环境变量获取基础地址(推荐)
|
|||
timeout: 10000, // 超时时间(10秒)
|
|||
headers: { |
|||
'Content-Type': 'application/json;charset=utf-8' // 默认请求头
|
|||
} |
|||
}) |
|||
|
|||
// 请求拦截器:添加 token、处理请求前逻辑
|
|||
myAxios.interceptors.request.use( |
|||
(config) => { |
|||
const isLoginApi = config.url === '/login'; |
|||
// 示例:从本地存储获取 token 并添加到请求头
|
|||
if (!isLoginApi) { |
|||
const token = localStorage.getItem('token'); |
|||
if (token) { |
|||
config.headers.Authorization = `Bearer ${token}`; |
|||
} else { |
|||
router.push('/login'); |
|||
} |
|||
} |
|||
return config |
|||
}, |
|||
(error) => { |
|||
// 请求错误时的处理(如参数错误)
|
|||
ElMessage.error('请求参数错误') |
|||
return Promise.reject(error) |
|||
} |
|||
) |
|||
|
|||
// 响应拦截器:处理响应数据、统一错误提示
|
|||
myAxios.interceptors.response.use( |
|||
(response) => { |
|||
const res = response.data |
|||
|
|||
// 假设后端接口规范:成功时 code=200,其他为错误
|
|||
if (res.code !== 200) { |
|||
if( res.code == 401){ |
|||
localStorage.removeItem('token'); |
|||
router.push('/login') |
|||
} |
|||
// 错误提示(根据后端消息提示,或自定义)
|
|||
ElMessage.error(res.msg || '操作失败') |
|||
return Promise.reject(new Error(res.msg || 'Error')) |
|||
} else { |
|||
// 只返回数据部分(过滤响应体中的其他字段)
|
|||
return res.data |
|||
} |
|||
}, |
|||
(error) => { |
|||
// 网络错误或服务器错误处理
|
|||
let message = '网络异常,请稍后重试' |
|||
if (error.response) { |
|||
// 根据 HTTP 状态码自定义提示
|
|||
switch (error.response.status) { |
|||
case 401: |
|||
message = '未授权,请重新登录' |
|||
// router.push('/login')
|
|||
break |
|||
case 403: |
|||
message = '没有权限访问' |
|||
break |
|||
case 404: |
|||
message = '接口不存在' |
|||
break |
|||
case 500: |
|||
message = '服务器内部错误' |
|||
break |
|||
default: |
|||
message = `请求错误(${error.response.status})` |
|||
} |
|||
} |
|||
ElMessage.error(message) |
|||
return Promise.reject(error) |
|||
} |
|||
) |
|||
|
|||
export default myAxios |
|||
@ -1,86 +0,0 @@ |
|||
import axios from 'axios' |
|||
import { MessageBox, Message } from 'element-ui' |
|||
import store from '@/store' |
|||
import { getToken } from '@/utils/auth' |
|||
|
|||
// create an axios instance
|
|||
const service = axios.create({ |
|||
baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url
|
|||
// withCredentials: true, // send cookies when cross-domain requests
|
|||
timeout: 5000 // request timeout
|
|||
}) |
|||
|
|||
// request interceptor
|
|||
service.interceptors.request.use( |
|||
config => { |
|||
// do something before request is sent
|
|||
|
|||
const token = store.getters.token |
|||
if (token) { |
|||
// let each request carry token
|
|||
// ['X-Token'] is a custom headers key
|
|||
// please modify it according to the actual situation
|
|||
config.headers['token'] = token |
|||
} |
|||
return config |
|||
}, |
|||
error => { |
|||
// do something with request error
|
|||
console.log(error) // for debug
|
|||
return Promise.reject(error) |
|||
} |
|||
) |
|||
|
|||
// response interceptor
|
|||
service.interceptors.response.use( |
|||
/** |
|||
* If you want to get http information such as headers or status |
|||
* Please return response => response |
|||
*/ |
|||
|
|||
/** |
|||
* Determine the request status by custom code |
|||
* Here is just an example |
|||
* You can also judge the status by HTTP Status Code |
|||
*/ |
|||
response => { |
|||
const res = response.data |
|||
|
|||
// if the custom code is not 200, it is judged as an error.
|
|||
if (res.code !== 200) { |
|||
Message({ |
|||
message: res.message || 'Error', |
|||
type: 'error', |
|||
duration: 5 * 1000 |
|||
}) |
|||
|
|||
// 50008: Illegal token; 50012: Other clients logged in; 50014: Token expired;
|
|||
if (res.code === 50008 || res.code === 50012 || res.code === 50014) { |
|||
// to re-login
|
|||
MessageBox.confirm('You have been logged out, you can cancel to stay on this page, or log in again', 'Confirm logout', { |
|||
confirmButtonText: 'Re-Login', |
|||
cancelButtonText: 'Cancel', |
|||
type: 'warning' |
|||
}).then(() => { |
|||
store.dispatch('user/resetToken').then(() => { |
|||
location.reload() |
|||
}) |
|||
}) |
|||
} |
|||
return Promise.reject(new Error(res.message || 'Error')) |
|||
} else { |
|||
return res |
|||
} |
|||
}, |
|||
error => { |
|||
console.log('err' + error) // for debug
|
|||
Message({ |
|||
message: error.message, |
|||
type: 'error', |
|||
duration: 5 * 1000 |
|||
}) |
|||
return Promise.reject(error) |
|||
} |
|||
) |
|||
|
|||
export default service |
|||
@ -1,20 +0,0 @@ |
|||
/** |
|||
* Created by PanJiaChen on 16/11/18. |
|||
*/ |
|||
|
|||
/** |
|||
* @param {string} path |
|||
* @returns {Boolean} |
|||
*/ |
|||
export function isExternal(path) { |
|||
return /^(https?:|mailto:|tel:)/.test(path) |
|||
} |
|||
|
|||
/** |
|||
* @param {string} str |
|||
* @returns {Boolean} |
|||
*/ |
|||
export function validUsername(str) { |
|||
const valid_map = ['admin', 'editor'] |
|||
return valid_map.indexOf(str.trim()) >= 0 |
|||
} |
|||
@ -1,228 +0,0 @@ |
|||
<template> |
|||
<div class="wscn-http404-container"> |
|||
<div class="wscn-http404"> |
|||
<div class="pic-404"> |
|||
<img class="pic-404__parent" src="@/assets/404_images/404.png" alt="404"> |
|||
<img class="pic-404__child left" src="@/assets/404_images/404_cloud.png" alt="404"> |
|||
<img class="pic-404__child mid" src="@/assets/404_images/404_cloud.png" alt="404"> |
|||
<img class="pic-404__child right" src="@/assets/404_images/404_cloud.png" alt="404"> |
|||
</div> |
|||
<div class="bullshit"> |
|||
<div class="bullshit__oops">OOPS!</div> |
|||
<div class="bullshit__info">All rights reserved |
|||
<a style="color:#20a0ff" href="https://wallstreetcn.com" target="_blank">wallstreetcn</a> |
|||
</div> |
|||
<div class="bullshit__headline">{{ message }}</div> |
|||
<div class="bullshit__info">Please check that the URL you entered is correct, or click the button below to return to the homepage.</div> |
|||
<a href="" class="bullshit__return-home">Back to home</a> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script> |
|||
|
|||
export default { |
|||
name: 'Page404', |
|||
computed: { |
|||
message() { |
|||
return 'The webmaster said that you can not enter this page...' |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
.wscn-http404-container{ |
|||
transform: translate(-50%,-50%); |
|||
position: absolute; |
|||
top: 40%; |
|||
left: 50%; |
|||
} |
|||
.wscn-http404 { |
|||
position: relative; |
|||
width: 1200px; |
|||
padding: 0 50px; |
|||
overflow: hidden; |
|||
.pic-404 { |
|||
position: relative; |
|||
float: left; |
|||
width: 600px; |
|||
overflow: hidden; |
|||
&__parent { |
|||
width: 100%; |
|||
} |
|||
&__child { |
|||
position: absolute; |
|||
&.left { |
|||
width: 80px; |
|||
top: 17px; |
|||
left: 220px; |
|||
opacity: 0; |
|||
animation-name: cloudLeft; |
|||
animation-duration: 2s; |
|||
animation-timing-function: linear; |
|||
animation-fill-mode: forwards; |
|||
animation-delay: 1s; |
|||
} |
|||
&.mid { |
|||
width: 46px; |
|||
top: 10px; |
|||
left: 420px; |
|||
opacity: 0; |
|||
animation-name: cloudMid; |
|||
animation-duration: 2s; |
|||
animation-timing-function: linear; |
|||
animation-fill-mode: forwards; |
|||
animation-delay: 1.2s; |
|||
} |
|||
&.right { |
|||
width: 62px; |
|||
top: 100px; |
|||
left: 500px; |
|||
opacity: 0; |
|||
animation-name: cloudRight; |
|||
animation-duration: 2s; |
|||
animation-timing-function: linear; |
|||
animation-fill-mode: forwards; |
|||
animation-delay: 1s; |
|||
} |
|||
@keyframes cloudLeft { |
|||
0% { |
|||
top: 17px; |
|||
left: 220px; |
|||
opacity: 0; |
|||
} |
|||
20% { |
|||
top: 33px; |
|||
left: 188px; |
|||
opacity: 1; |
|||
} |
|||
80% { |
|||
top: 81px; |
|||
left: 92px; |
|||
opacity: 1; |
|||
} |
|||
100% { |
|||
top: 97px; |
|||
left: 60px; |
|||
opacity: 0; |
|||
} |
|||
} |
|||
@keyframes cloudMid { |
|||
0% { |
|||
top: 10px; |
|||
left: 420px; |
|||
opacity: 0; |
|||
} |
|||
20% { |
|||
top: 40px; |
|||
left: 360px; |
|||
opacity: 1; |
|||
} |
|||
70% { |
|||
top: 130px; |
|||
left: 180px; |
|||
opacity: 1; |
|||
} |
|||
100% { |
|||
top: 160px; |
|||
left: 120px; |
|||
opacity: 0; |
|||
} |
|||
} |
|||
@keyframes cloudRight { |
|||
0% { |
|||
top: 100px; |
|||
left: 500px; |
|||
opacity: 0; |
|||
} |
|||
20% { |
|||
top: 120px; |
|||
left: 460px; |
|||
opacity: 1; |
|||
} |
|||
80% { |
|||
top: 180px; |
|||
left: 340px; |
|||
opacity: 1; |
|||
} |
|||
100% { |
|||
top: 200px; |
|||
left: 300px; |
|||
opacity: 0; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
.bullshit { |
|||
position: relative; |
|||
float: left; |
|||
width: 300px; |
|||
padding: 30px 0; |
|||
overflow: hidden; |
|||
&__oops { |
|||
font-size: 32px; |
|||
font-weight: bold; |
|||
line-height: 40px; |
|||
color: #1482f0; |
|||
opacity: 0; |
|||
margin-bottom: 20px; |
|||
animation-name: slideUp; |
|||
animation-duration: 0.5s; |
|||
animation-fill-mode: forwards; |
|||
} |
|||
&__headline { |
|||
font-size: 20px; |
|||
line-height: 24px; |
|||
color: #222; |
|||
font-weight: bold; |
|||
opacity: 0; |
|||
margin-bottom: 10px; |
|||
animation-name: slideUp; |
|||
animation-duration: 0.5s; |
|||
animation-delay: 0.1s; |
|||
animation-fill-mode: forwards; |
|||
} |
|||
&__info { |
|||
font-size: 13px; |
|||
line-height: 21px; |
|||
color: grey; |
|||
opacity: 0; |
|||
margin-bottom: 30px; |
|||
animation-name: slideUp; |
|||
animation-duration: 0.5s; |
|||
animation-delay: 0.2s; |
|||
animation-fill-mode: forwards; |
|||
} |
|||
&__return-home { |
|||
display: block; |
|||
float: left; |
|||
width: 110px; |
|||
height: 36px; |
|||
background: #1482f0; |
|||
border-radius: 100px; |
|||
text-align: center; |
|||
color: #ffffff; |
|||
opacity: 0; |
|||
font-size: 14px; |
|||
line-height: 36px; |
|||
cursor: pointer; |
|||
animation-name: slideUp; |
|||
animation-duration: 0.5s; |
|||
animation-delay: 0.3s; |
|||
animation-fill-mode: forwards; |
|||
} |
|||
@keyframes slideUp { |
|||
0% { |
|||
transform: translateY(60px); |
|||
opacity: 0; |
|||
} |
|||
100% { |
|||
transform: translateY(0); |
|||
opacity: 1; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
</style> |
|||
@ -1,232 +1,222 @@ |
|||
<template> |
|||
<div class="login-container"> |
|||
<el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form" auto-complete="on" label-position="left"> |
|||
|
|||
<div class="title-container"> |
|||
<h3 class="title">欢迎登录系统</h3> |
|||
</div> |
|||
|
|||
<el-form-item prop="username"> |
|||
<span class="svg-container"> |
|||
<svg-icon icon-class="user" /> |
|||
</span> |
|||
<el-input |
|||
ref="username" |
|||
v-model="loginForm.username" |
|||
placeholder="Username" |
|||
name="username" |
|||
type="text" |
|||
tabindex="1" |
|||
auto-complete="on" |
|||
/> |
|||
</el-form-item> |
|||
|
|||
<el-form-item prop="password"> |
|||
<span class="svg-container"> |
|||
<svg-icon icon-class="password" /> |
|||
</span> |
|||
<el-input |
|||
:key="passwordType" |
|||
ref="password" |
|||
v-model="loginForm.password" |
|||
:type="passwordType" |
|||
placeholder="Password" |
|||
name="password" |
|||
tabindex="2" |
|||
auto-complete="on" |
|||
@keyup.enter.native="handleLogin" |
|||
/> |
|||
<span class="show-pwd" @click="showPwd"> |
|||
<svg-icon :icon-class="passwordType === 'password' ? 'eye' : 'eye-open'" /> |
|||
</span> |
|||
</el-form-item> |
|||
|
|||
<el-button :loading="loading" type="primary" style="width:100%;margin-bottom:30px;" @click.native.prevent="handleLogin">登录</el-button> |
|||
|
|||
</el-form> |
|||
<!-- 背景容器 --> |
|||
<div class="login-bg"> |
|||
<!-- 登录表单 --> |
|||
<div class="login-form"> |
|||
<h3>欢迎登录系统</h3> |
|||
<el-form :model="loginForm" label-width="0" class="form-item"> |
|||
<!-- 账号输入框 --> |
|||
<el-form-item> |
|||
<el-input v-model="loginForm.username" prefix-icon="User" placeholder="请输入登录账号" clearable @input="clearErrorMsg"/> |
|||
</el-form-item> |
|||
<!-- 密码输入框 --> |
|||
<el-form-item> |
|||
<el-input v-model="loginForm.password" prefix-icon="Lock" type="password" placeholder="请输入登录密码" clearable @input="clearErrorMsg"/> |
|||
</el-form-item> |
|||
<!-- 错误提示 --> |
|||
<div v-show="errorMsg" class="error-tip fixed-height">{{ errorMsg }}</div> |
|||
<div v-show="!errorMsg" class="error-placeholder fixed-height"></div> |
|||
<!-- 记住密码复选框 --> |
|||
<el-form-item class="remember"> |
|||
<el-checkbox v-model="loginForm.remember">记住密码</el-checkbox> |
|||
</el-form-item> |
|||
<!-- 登录按钮 --> |
|||
<el-form-item> |
|||
<el-button type="primary" style="width: 100%;" @click="handleLogin"> |
|||
登 录 |
|||
</el-button> |
|||
</el-form-item> |
|||
</el-form> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script> |
|||
import { validUsername } from '@/utils/validate' |
|||
<script setup> |
|||
import { ref, onMounted } from 'vue'; |
|||
import { useRouter } from 'vue-router'; |
|||
import { loginApi } from '../api/userPermissions' |
|||
|
|||
const router = useRouter(); |
|||
|
|||
// 表单数据 |
|||
const loginForm = ref({ |
|||
username: '', |
|||
password: '', |
|||
remember: false |
|||
}); |
|||
|
|||
// 错误提示 |
|||
const errorMsg = ref(''); |
|||
|
|||
// 页面加载时,自动填充“记住的账号密码” |
|||
onMounted(() => { |
|||
const storedLogin = localStorage.getItem('deepchart_login'); |
|||
if (storedLogin) { |
|||
const { username, password, remember } = JSON.parse(storedLogin); |
|||
loginForm.value.username = username; |
|||
loginForm.value.password = password; |
|||
loginForm.value.remember = remember; |
|||
} |
|||
}); |
|||
|
|||
// 输入时清空错误提示 |
|||
const clearErrorMsg = () => { |
|||
errorMsg.value = ''; |
|||
}; |
|||
|
|||
// 登录逻辑:包含表单校验、消息提醒、记住密码存储 |
|||
const handleLogin = async() => { |
|||
// 先清空之前的错误提示 |
|||
errorMsg.value = ''; |
|||
|
|||
// 校验必填项 |
|||
if (!loginForm.value.username && !loginForm.value.password) { |
|||
errorMsg.value = '请输入账号和密码后再登录'; |
|||
return; |
|||
} |
|||
if (!loginForm.value.username) { |
|||
errorMsg.value = '账号为必填项,请输入账号'; |
|||
return; |
|||
} |
|||
if (!loginForm.value.password) { |
|||
errorMsg.value = '密码为必填项,请输入密码'; |
|||
return; |
|||
} |
|||
|
|||
export default { |
|||
name: 'Login', |
|||
data() { |
|||
const validateUsername = (rule, value, callback) => { |
|||
if (!validUsername(value)) { |
|||
callback(new Error('Please enter the correct user name')) |
|||
} else { |
|||
callback() |
|||
} |
|||
} |
|||
const validatePassword = (rule, value, callback) => { |
|||
if (value.length < 6) { |
|||
callback(new Error('The password can not be less than 6 digits')) |
|||
} else { |
|||
callback() |
|||
} |
|||
} |
|||
return { |
|||
loginForm: { |
|||
username: 'admin', |
|||
password: '111111' |
|||
}, |
|||
loginRules: { |
|||
username: [{ required: true, trigger: 'blur', validator: validateUsername }], |
|||
password: [{ required: true, trigger: 'blur', validator: validatePassword }] |
|||
}, |
|||
loading: false, |
|||
passwordType: 'password', |
|||
redirect: undefined |
|||
} |
|||
}, |
|||
watch: { |
|||
$route: { |
|||
handler: function(route) { |
|||
this.redirect = route.query && route.query.redirect |
|||
}, |
|||
immediate: true |
|||
} |
|||
}, |
|||
methods: { |
|||
showPwd() { |
|||
if (this.passwordType === 'password') { |
|||
this.passwordType = '' |
|||
// 登录 |
|||
try { |
|||
const res = await loginApi({ |
|||
username: Number(loginForm.value.username), |
|||
password: loginForm.value.password, |
|||
app_from: 'en' |
|||
}); |
|||
const data = res.data; |
|||
|
|||
if (data && data.data.token) { |
|||
localStorage.setItem('token', data.data.token); |
|||
// “记住密码”存储 |
|||
if (loginForm.value.remember) { |
|||
localStorage.setItem('deepchart_login', JSON.stringify({ |
|||
username: loginForm.value.username, |
|||
password: loginForm.value.password, |
|||
remember: true |
|||
})); |
|||
} else { |
|||
this.passwordType = 'password' |
|||
localStorage.removeItem('deepchart_login'); |
|||
} |
|||
this.$nextTick(() => { |
|||
this.$refs.password.focus() |
|||
}) |
|||
}, |
|||
handleLogin() { |
|||
this.$refs.loginForm.validate(valid => { |
|||
if (valid) { |
|||
this.loading = true |
|||
this.$store.dispatch('user/login', this.loginForm).then(() => { |
|||
this.$router.push({ path: this.redirect || '/' }) |
|||
this.loading = false |
|||
}).catch(() => { |
|||
this.loading = false |
|||
}) |
|||
} else { |
|||
console.log('error submit!!') |
|||
return false |
|||
} |
|||
}) |
|||
|
|||
// 跳转到首页 |
|||
router.push('/'); |
|||
} else { |
|||
errorMsg.value = '账号或密码有误,请重新输入'; |
|||
} |
|||
} catch (error) { |
|||
errorMsg.value = '登录失败,请稍后重试'; |
|||
console.error('登录请求异常:', error); |
|||
} |
|||
} |
|||
}; |
|||
</script> |
|||
|
|||
<style lang="scss"> |
|||
/* 修复input 背景不协调 和光标变色 */ |
|||
/* Detail see https://github.com/PanJiaChen/vue-element-admin/pull/927 */ |
|||
<style scoped> |
|||
/* 背景图全屏样式 */ |
|||
.login-bg { |
|||
background-image: url('../assets/images/login.png'); |
|||
background-size: cover; |
|||
background-position: center; |
|||
height: calc(100vh - 16px); |
|||
display: flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
} |
|||
|
|||
$bg:#283443; |
|||
$light_gray:#fff; |
|||
$cursor: #fff; |
|||
/* 表单样式 */ |
|||
.login-form { |
|||
margin-left: 650px; |
|||
width: 400px; |
|||
padding: 40px 30px; |
|||
} |
|||
|
|||
@supports (-webkit-mask: none) and (not (cater-color: $cursor)) { |
|||
.login-container .el-input input { |
|||
color: $cursor; |
|||
} |
|||
/* 标题样式 */ |
|||
.login-form h3 { |
|||
text-align: center; |
|||
margin-bottom: 50px; |
|||
color: #2d3748; |
|||
font-size: 24px; |
|||
font-weight: 600; |
|||
} |
|||
|
|||
/* reset element-ui css */ |
|||
.login-container { |
|||
.el-input { |
|||
display: inline-block; |
|||
height: 47px; |
|||
width: 85%; |
|||
|
|||
input { |
|||
background: transparent; |
|||
border: 0px; |
|||
-webkit-appearance: none; |
|||
border-radius: 0px; |
|||
padding: 12px 5px 12px 15px; |
|||
color: $light_gray; |
|||
height: 47px; |
|||
caret-color: $cursor; |
|||
|
|||
&:-webkit-autofill { |
|||
box-shadow: 0 0 0px 1000px $bg inset !important; |
|||
-webkit-text-fill-color: $cursor !important; |
|||
} |
|||
} |
|||
} |
|||
.form-item { |
|||
width: 100%; |
|||
} |
|||
|
|||
.el-form-item { |
|||
border: 1px solid rgba(255, 255, 255, 0.1); |
|||
background: rgba(0, 0, 0, 0.1); |
|||
border-radius: 5px; |
|||
color: #454545; |
|||
} |
|||
/* 输入框区域 */ |
|||
.el-form-item { |
|||
margin-bottom: 30px; |
|||
} |
|||
</style> |
|||
|
|||
<style lang="scss" scoped> |
|||
$bg:#2d3a4b; |
|||
$dark_gray:#889aa4; |
|||
$light_gray:#eee; |
|||
.el-input { |
|||
border-radius: 4px; |
|||
border: 1px solid #e5e7eb; |
|||
height: 48px; |
|||
} |
|||
|
|||
.login-container { |
|||
min-height: 100%; |
|||
width: 100%; |
|||
background-color: $bg; |
|||
overflow: hidden; |
|||
|
|||
.login-form { |
|||
position: relative; |
|||
width: 520px; |
|||
max-width: 100%; |
|||
padding: 160px 35px 0; |
|||
margin: 0 auto; |
|||
overflow: hidden; |
|||
} |
|||
.el-input__inner { |
|||
padding: 0 16px; |
|||
font-size: 16px; |
|||
} |
|||
|
|||
.tips { |
|||
font-size: 14px; |
|||
color: #fff; |
|||
margin-bottom: 10px; |
|||
.el-input__placeholder { |
|||
font-size: 16px !important; |
|||
} |
|||
|
|||
span { |
|||
&:first-of-type { |
|||
margin-right: 16px; |
|||
} |
|||
} |
|||
} |
|||
/* 记住密码 */ |
|||
.remember { |
|||
text-align: left; |
|||
margin-bottom: 32px; |
|||
font-size: 15px; |
|||
color: #4b5563; |
|||
} |
|||
|
|||
.svg-container { |
|||
padding: 6px 5px 6px 15px; |
|||
color: $dark_gray; |
|||
vertical-align: middle; |
|||
width: 30px; |
|||
display: inline-block; |
|||
} |
|||
.el-checkbox__label { |
|||
font-size: 15px !important; |
|||
margin-left: 8px !important; |
|||
} |
|||
|
|||
.title-container { |
|||
position: relative; |
|||
/* 登录按钮 */ |
|||
.el-button--primary { |
|||
height: 52px; |
|||
font-size: 18px; |
|||
border-radius: 4px; |
|||
background-color: #1890ff; |
|||
border: none; |
|||
} |
|||
|
|||
.title { |
|||
font-size: 26px; |
|||
color: $light_gray; |
|||
margin: 0px auto 40px auto; |
|||
text-align: center; |
|||
font-weight: bold; |
|||
} |
|||
} |
|||
.el-button--primary:hover { |
|||
background-color: #096dd9; |
|||
} |
|||
|
|||
.show-pwd { |
|||
position: absolute; |
|||
right: 10px; |
|||
top: 7px; |
|||
font-size: 16px; |
|||
color: $dark_gray; |
|||
cursor: pointer; |
|||
user-select: none; |
|||
} |
|||
/* 图标大小适配 */ |
|||
.el-input__prefix { |
|||
font-size: 18px !important; |
|||
} |
|||
|
|||
/* 错误提示 */ |
|||
.error-tip { |
|||
color: #ff4d4f; |
|||
font-size: 14px; |
|||
font-weight: 700; |
|||
text-align: left; |
|||
line-height: 1.5; |
|||
overflow: hidden; |
|||
} |
|||
</style> |
|||
|
|||
.error-placeholder { |
|||
visibility: hidden; |
|||
} |
|||
|
|||
.fixed-height { |
|||
height: 21px; |
|||
margin-bottom: 6px; |
|||
} |
|||
|
|||
</style> |
|||
@ -0,0 +1,126 @@ |
|||
<template> |
|||
<div class="export-list-page"> |
|||
<!-- 页面标题 --> |
|||
<h3 class="page-title">导出文件列表</h3> |
|||
|
|||
<!-- 表格 --> |
|||
<el-table |
|||
:data="exportList" |
|||
stripe |
|||
:header-cell-style="{ 'background-color':'#f8f9fa'}" |
|||
style="width: 100%; margin-bottom: 16px;" |
|||
> |
|||
<el-table-column prop="file_name" label="文件名" align="center" header-align="center"/> |
|||
<el-table-column prop="type" label="导出类型" align="center" header-align="center"/> |
|||
<el-table-column label="状态" align="center" header-align="center" width="100"> |
|||
<template #default="scope"> |
|||
<el-tag :type="statusTypeMap[scope.row.status].type" size="small"> |
|||
{{ statusTypeMap[scope.row.status].label }} |
|||
</el-tag> |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column prop="created_at" label="创建时间" align="center" header-align="center"/> |
|||
<el-table-column label="操作" align="center" header-align="center" width="120"> |
|||
<template #default="scope"> |
|||
<el-button |
|||
type="primary" |
|||
size="mini" |
|||
:disabled="scope.row.status !== 2" |
|||
@click="handleDownload(scope.row)" |
|||
> |
|||
下载 |
|||
</el-button> |
|||
</template> |
|||
</el-table-column> |
|||
</el-table> |
|||
|
|||
<!-- 统计信息 + 关闭按钮 --> |
|||
<div class="page-footer"> |
|||
<span class="total-count">共 {{ totalCount }} 条导出记录</span> |
|||
<el-button type="text" @click="handleClose">关闭</el-button> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup> |
|||
import { ref, onMounted } from 'vue' |
|||
import { useRouter } from 'vue-router'; |
|||
import { exportListApi } from '../../api/userPermissions' |
|||
|
|||
// token |
|||
const token = localStorage.getItem('token') |
|||
|
|||
// 路由实例 |
|||
const router = useRouter(); |
|||
|
|||
// 导出列表数据 |
|||
const exportList = ref([]); |
|||
const totalCount = ref(0) |
|||
|
|||
// 状态映射配置 |
|||
const statusTypeMap = { |
|||
0: { type: 'danger', label: '执行失败' }, |
|||
1: { type: 'primary', label: '执行中' }, |
|||
2: { type: 'success', label: '执行成功' } |
|||
}; |
|||
|
|||
// 组件挂载 |
|||
onMounted(() => { |
|||
exportListAll(); |
|||
}); |
|||
|
|||
// 获取导出列表 |
|||
const exportListAll = async () => { |
|||
try { |
|||
const data = await exportListApi({token: token}); |
|||
exportList.value = data.list; |
|||
totalCount.value = data.total; |
|||
} catch (error) { |
|||
exportList.value = []; |
|||
} |
|||
}; |
|||
|
|||
// 下载按钮 |
|||
const handleDownload = (exportdata) => { |
|||
if (!exportdata.download_url) return; |
|||
const link = document.createElement('a'); |
|||
link.href = exportdata.download_url; |
|||
link.style.display = 'none'; |
|||
document.body.appendChild(link); |
|||
link.click(); |
|||
document.body.removeChild(link); |
|||
}; |
|||
|
|||
// 关闭按钮 |
|||
const handleClose = () => { |
|||
router.go(-1); |
|||
}; |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.export-list-page { |
|||
padding: 20px; |
|||
background: #fff; |
|||
border-radius: 8px; |
|||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.08); |
|||
} |
|||
|
|||
.page-title { |
|||
margin: 0 0 20px 0; |
|||
font-size: 16px; |
|||
color: #141414; |
|||
font-weight: 600; |
|||
} |
|||
|
|||
.page-footer { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
color: #666; |
|||
font-size: 14px; |
|||
} |
|||
|
|||
.total-count { |
|||
color: #888; |
|||
} |
|||
</style> |
|||
@ -1,13 +1,224 @@ |
|||
<template> |
|||
<div>深度探索日志</div> |
|||
<div class="page-container"> |
|||
<!-- 头部 --> |
|||
<div class="button-group"> |
|||
<el-button type="danger" @click="goback">返回上一页</el-button> |
|||
</div> |
|||
|
|||
<!-- 数据表格 --> |
|||
<el-table |
|||
:data="tableData" |
|||
style="width: 100%; margin-top: 20px;" |
|||
header-cell-class-name="table-header" |
|||
@sort-change="handleSortChange" |
|||
:default-sort="{ prop: null, order: null }" |
|||
class="table-rounded" |
|||
:loading="tableLoading" |
|||
> |
|||
<el-table-column prop="id" label="序号" align="center" header-align="center" width="80"> |
|||
<template #default="scope"> |
|||
{{ (currentPage - 1) * pageSize + scope.$index + 1 }} |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column prop="dccode" label="账号" align="center" header-align="center" width="120"/> |
|||
<el-table-column prop="name" label="姓名" align="center" header-align="center" width="150"/> |
|||
<el-table-column prop="module_name" label="模块名称" align="center" header-align="center" width="100"/> |
|||
<el-table-column prop="created_at" label="操作时间" align="center" header-align="center" sortable="custom" width="200"/> |
|||
<el-table-column prop="expire_time" label="到期时间" align="center" header-align="center" sortable="custom" width="200"/> |
|||
<el-table-column prop="remark" label="备注" align="center" header-align="center"> |
|||
<template #default="scope"> |
|||
<el-tooltip :content="scope.row.remark" placement="top"> |
|||
<span class="note-ellipsis"> |
|||
{{ scope.row.remark }} |
|||
</span> |
|||
</el-tooltip> |
|||
</template> |
|||
</el-table-column> |
|||
</el-table> |
|||
|
|||
<!-- 分页组件 --> |
|||
<div class="demo-pagination-block"> |
|||
<el-pagination |
|||
@size-change="handleSizeChange" |
|||
@current-change="handleCurrentChange" |
|||
:current-page="currentPage" |
|||
:page-sizes="[10, 20, 50, 100]" |
|||
:page-size="pageSize" |
|||
layout="total, sizes, prev, pager, next, jumper" |
|||
:total="datatotal" |
|||
/> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
<script setup> |
|||
import { ref, onMounted } from 'vue'; |
|||
import { logMListApi } from '../../api/userPermissions' |
|||
import { useRoute, useRouter } from 'vue-router'; |
|||
|
|||
} |
|||
// token |
|||
const token = localStorage.getItem('token') |
|||
|
|||
//获取路由实例 |
|||
const route = useRoute(); |
|||
const router = useRouter(); |
|||
|
|||
// 全局账号 |
|||
const targetId = ref(''); |
|||
|
|||
// 排序参数 |
|||
const sortProp = ref(null); |
|||
const sortOrder = ref(null); |
|||
|
|||
// 表格数据 |
|||
const tableData = ref([]); |
|||
const tableLoading = ref(false); |
|||
const datatotal = ref(0); |
|||
|
|||
// 分页参数 |
|||
const currentPage = ref(1); |
|||
const pageSize = ref(10); |
|||
|
|||
// 获取表格数据 |
|||
const logTableData = async (dccode) => { |
|||
try { |
|||
tableLoading.value = true; |
|||
const requestParams = { |
|||
token: token, |
|||
dccode: dccode, |
|||
type: 3, |
|||
sort_field: sortProp.value, |
|||
sort_order: sortOrder.value, |
|||
page: currentPage.value, |
|||
page_size: pageSize.value |
|||
}; |
|||
const data = await logMListApi(requestParams); |
|||
tableData.value = data.list |
|||
datatotal.value = data.total |
|||
} catch (error) { |
|||
console.error('获取表格数据失败:', error); |
|||
tableData.value = []; |
|||
datatotal.value = 0; |
|||
} finally { |
|||
tableLoading.value = false; |
|||
} |
|||
}; |
|||
|
|||
// 组件挂载时 |
|||
onMounted(() => { |
|||
targetId.value = route.query.dccode; |
|||
logTableData(targetId.value) |
|||
}); |
|||
|
|||
// 返回按钮 |
|||
const goback = () => { |
|||
router.push({ |
|||
path: "/userPermissions/module", |
|||
query: { taber: "DeepExplore" } |
|||
}); |
|||
}; |
|||
|
|||
// 分页方法 |
|||
const handleSizeChange = (val) => { |
|||
pageSize.value = val; |
|||
logTableData(targetId.value); |
|||
console.log(`每页 ${val} 条`); |
|||
}; |
|||
|
|||
const handleCurrentChange = (val) => { |
|||
currentPage.value = val; |
|||
logTableData(targetId.value); |
|||
console.log(`当前页: ${val}`); |
|||
}; |
|||
|
|||
// 排序事件 |
|||
const handleSortChange = (sort) => { |
|||
const { prop, order } = sort; |
|||
|
|||
if (!['created_at', 'expire_time'].includes(prop)) return; |
|||
|
|||
// 覆盖排序状态(实现二选一) |
|||
sortProp.value = prop; // 保存当前排序字段 |
|||
sortOrder.value = order; // 保存当前排序方式 |
|||
|
|||
logTableData(targetId.value); |
|||
}; |
|||
</script> |
|||
|
|||
<style> |
|||
<style scoped> |
|||
/* 父容器 */ |
|||
.page-container { |
|||
position: relative; |
|||
min-height: 600px; |
|||
} |
|||
|
|||
/* 按钮组 */ |
|||
.button-group { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 10px; |
|||
margin-bottom: 20px; |
|||
} |
|||
|
|||
</style> |
|||
/* 按钮样式 */ |
|||
.button-group .el-button { |
|||
padding: 6px 10px !important; |
|||
font-size: 14px !important; |
|||
height: 36px !important; |
|||
} |
|||
|
|||
/* 表格样式 */ |
|||
.table-rounded { |
|||
border-radius: 12px !important; |
|||
overflow: hidden !important; |
|||
border: 1px solid #e4e7ed !important; |
|||
min-height: 800px; |
|||
} |
|||
|
|||
.table-header { |
|||
text-align: center !important; |
|||
font-weight: 800 !important; |
|||
font-size: 15px !important; |
|||
color: #333 !important; |
|||
background-color: #f8f9fa !important; |
|||
} |
|||
|
|||
.el-table__cell { |
|||
border-right: none !important; |
|||
border-bottom: 1px solid #e4e7ed !important; |
|||
} |
|||
|
|||
.el-table__header th.el-table__cell { |
|||
border-right: none !important; |
|||
border-bottom: 1px solid #e4e7ed !important; |
|||
} |
|||
|
|||
.el-table__row:hover .el-table__cell { |
|||
background-color: #fafafa !important; |
|||
} |
|||
|
|||
/* 备注 */ |
|||
.note-ellipsis { |
|||
display: inline-block; |
|||
width: 100%; |
|||
white-space: nowrap; |
|||
overflow: hidden; |
|||
text-overflow: ellipsis; |
|||
padding: 0 4px; |
|||
} |
|||
|
|||
/* 分页组件 */ |
|||
.demo-pagination-block { |
|||
display: flex; |
|||
width: 100%; |
|||
height: 44px; |
|||
padding: 0 16px; |
|||
align-items: center; |
|||
gap: 16px; |
|||
margin-top: 10px; |
|||
border-radius: 0 0 3px 3px; |
|||
border-top: 1px solid #EAEAEA; |
|||
background: #FEFBFB; |
|||
box-sizing: border-box; |
|||
} |
|||
</style> |
|||
@ -1,13 +1,210 @@ |
|||
<template> |
|||
<div>deepMate日志</div> |
|||
<div class="page-container"> |
|||
<!-- 头部 --> |
|||
<div class="button-group"> |
|||
<el-button type="danger" @click="goback">返回上一页</el-button> |
|||
</div> |
|||
|
|||
<!-- 数据表格 --> |
|||
<el-table |
|||
:data="tableData" |
|||
style="width: 100%; margin-top: 20px;" |
|||
header-cell-class-name="table-header" |
|||
@sort-change="handleSortChange" |
|||
:default-sort="{ prop: null, order: null }" |
|||
class="table-rounded" |
|||
:loading="tableLoading" |
|||
> |
|||
<el-table-column prop="id" label="序号" align="center" header-align="center" width="80"> |
|||
<template #default="scope"> |
|||
{{ (currentPage - 1) * pageSize + scope.$index + 1 }} |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column prop="dccode" label="账号" align="center" header-align="center"/> |
|||
<el-table-column prop="name" label="姓名" align="center" header-align="center"/> |
|||
<el-table-column prop="module_name" label="模块名称" align="center" header-align="center"/> |
|||
<el-table-column prop="token_change_num" label="token数量" align="center" header-align="center"/> |
|||
<el-table-column prop="created_at" label="操作时间" align="center" header-align="center" sortable="custom"/> |
|||
</el-table> |
|||
|
|||
<!-- 分页组件 --> |
|||
<div class="demo-pagination-block"> |
|||
<el-pagination |
|||
@size-change="handleSizeChange" |
|||
@current-change="handleCurrentChange" |
|||
:current-page="currentPage" |
|||
:page-sizes="[10, 20, 50, 100]" |
|||
:page-size="pageSize" |
|||
layout="total, sizes, prev, pager, next, jumper" |
|||
:total="datatotal" |
|||
/> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
<script setup> |
|||
import { ref, onMounted } from 'vue'; |
|||
import { logMListApi } from '../../api/userPermissions' |
|||
import { useRoute, useRouter } from 'vue-router'; |
|||
|
|||
} |
|||
// token |
|||
const token = localStorage.getItem('token') |
|||
|
|||
// 组件挂载时 |
|||
onMounted(() => { |
|||
targetId.value = route.query.dccode; |
|||
logTableData(targetId.value) |
|||
}); |
|||
|
|||
// 获取路由实例 |
|||
const route = useRoute(); |
|||
const router = useRouter(); |
|||
|
|||
// 全局账号 |
|||
const targetId = ref(''); |
|||
|
|||
// 排序参数 |
|||
const sortProp = ref(null); |
|||
const sortOrder = ref(null); |
|||
|
|||
// 表格数据 |
|||
const tableData = ref([]); |
|||
const tableLoading = ref(false); |
|||
const datatotal = ref(0); |
|||
|
|||
// 分页参数 |
|||
const currentPage = ref(1); |
|||
const pageSize = ref(10); |
|||
|
|||
// 获取表格数据 |
|||
const logTableData = async (dccode) => { |
|||
try { |
|||
tableLoading.value = true; |
|||
const requestParams = { |
|||
token: token, |
|||
dccode: dccode, |
|||
type: 2, |
|||
sort_field: sortProp.value, |
|||
sort_order: sortOrder.value, |
|||
page: currentPage.value, |
|||
page_size: pageSize.value |
|||
}; |
|||
const data = await logMListApi(requestParams); |
|||
tableData.value = data.list |
|||
datatotal.value = data.total |
|||
} catch (error) { |
|||
console.error('获取表格数据失败:', error); |
|||
tableData.value = []; |
|||
datatotal.value = 0; |
|||
} finally { |
|||
tableLoading.value = false; |
|||
} |
|||
}; |
|||
|
|||
// 返回按钮 |
|||
const goback = () => { |
|||
router.push({ |
|||
path: "/userPermissions/module", |
|||
query: { taber: "DeepMate" } |
|||
}); |
|||
}; |
|||
|
|||
// 分页方法 |
|||
const handleSizeChange = (val) => { |
|||
pageSize.value = val; |
|||
logTableData(targetId.value); |
|||
console.log(`每页 ${val} 条`); |
|||
}; |
|||
|
|||
const handleCurrentChange = (val) => { |
|||
currentPage.value = val; |
|||
logTableData(targetId.value); |
|||
console.log(`当前页: ${val}`); |
|||
}; |
|||
|
|||
// 排序事件 |
|||
const handleSortChange = (sort) => { |
|||
const { prop, order } = sort; |
|||
sortProp.value = prop; |
|||
sortOrder.value = order; |
|||
logTableData(targetId.value); |
|||
}; |
|||
</script> |
|||
|
|||
<style> |
|||
<style scoped> |
|||
/* 父容器 */ |
|||
.page-container { |
|||
position: relative; |
|||
min-height: 600px; |
|||
} |
|||
|
|||
/* 按钮组 */ |
|||
.button-group { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 10px; |
|||
margin-bottom: 20px; |
|||
} |
|||
|
|||
/* 按钮样式 */ |
|||
.button-group .el-button { |
|||
padding: 6px 10px !important; |
|||
font-size: 14px !important; |
|||
height: 36px !important; |
|||
} |
|||
|
|||
/* 表格样式 */ |
|||
.table-rounded { |
|||
border-radius: 12px !important; |
|||
overflow: hidden !important; |
|||
border: 1px solid #e4e7ed !important; |
|||
min-height: 800px; |
|||
} |
|||
|
|||
.table-header { |
|||
text-align: center !important; |
|||
font-weight: 800 !important; |
|||
font-size: 15px !important; |
|||
color: #333 !important; |
|||
background-color: #f8f9fa !important; |
|||
} |
|||
|
|||
.el-table__cell { |
|||
border-right: none !important; |
|||
border-bottom: 1px solid #e4e7ed !important; |
|||
} |
|||
|
|||
.el-table__header th.el-table__cell { |
|||
border-right: none !important; |
|||
border-bottom: 1px solid #e4e7ed !important; |
|||
} |
|||
|
|||
.el-table__row:hover .el-table__cell { |
|||
background-color: #fafafa !important; |
|||
} |
|||
|
|||
/* 备注 */ |
|||
.note-ellipsis { |
|||
display: inline-block; |
|||
width: 100%; |
|||
white-space: nowrap; |
|||
overflow: hidden; |
|||
text-overflow: ellipsis; |
|||
padding: 0 4px; |
|||
} |
|||
|
|||
</style> |
|||
/* 分页组件 */ |
|||
.demo-pagination-block { |
|||
display: flex; |
|||
width: 100%; |
|||
height: 44px; |
|||
padding: 0 16px; |
|||
align-items: center; |
|||
gap: 16px; |
|||
margin-top: 10px; |
|||
border-radius: 0 0 3px 3px; |
|||
border-top: 1px solid #EAEAEA; |
|||
background: #FEFBFB; |
|||
box-sizing: border-box; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,223 @@ |
|||
<template> |
|||
<div class="page-container"> |
|||
<!-- 头部 --> |
|||
<div class="button-group"> |
|||
<el-button type="danger" @click="goback">返回上一页</el-button> |
|||
</div> |
|||
|
|||
<!-- 数据表格 --> |
|||
<el-table |
|||
:data="tableData" |
|||
style="width: 100%; margin-top: 20px;" |
|||
header-cell-class-name="table-header" |
|||
@sort-change="handleSortChange" |
|||
:default-sort="{ prop: null, order: null }" |
|||
class="table-rounded" |
|||
:loading="tableLoading" |
|||
> |
|||
<el-table-column prop="id" label="序号" align="center" header-align="center" width="80"> |
|||
<template #default="scope"> |
|||
{{ (currentPage - 1) * pageSize + scope.$index + 1 }} |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column prop="dccode" label="账号" align="center" header-align="center" width="120"/> |
|||
<el-table-column prop="name" label="姓名" align="center" header-align="center" width="150"/> |
|||
<el-table-column prop="module_name" label="模块名称" align="center" header-align="center" width="90"/> |
|||
<el-table-column prop="created_at" label="操作时间" align="center" header-align="center" sortable="custom" width="200"/> |
|||
<el-table-column prop="expire_time" label="到期时间" align="center" header-align="center" sortable="custom" width="200"/> |
|||
<el-table-column prop="remark" label="备注" align="center" header-align="center"> |
|||
<template #default="scope"> |
|||
<el-tooltip :content="scope.row.remark" placement="top"> |
|||
<span class="note-ellipsis"> |
|||
{{ scope.row.remark }} |
|||
</span> |
|||
</el-tooltip> |
|||
</template> |
|||
</el-table-column> |
|||
</el-table> |
|||
|
|||
<!-- 分页组件 --> |
|||
<div class="demo-pagination-block"> |
|||
<el-pagination |
|||
@size-change="handleSizeChange" |
|||
@current-change="handleCurrentChange" |
|||
:current-page="currentPage" |
|||
:page-sizes="[10, 20, 50, 100]" |
|||
:page-size="pageSize" |
|||
layout="total, sizes, prev, pager, next, jumper" |
|||
:total="datatotal" |
|||
/> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup> |
|||
import { ref, onMounted } from 'vue'; |
|||
import { logMListApi } from '../../api/userPermissions' |
|||
import { useRoute, useRouter } from 'vue-router'; |
|||
|
|||
// token |
|||
const token = localStorage.getItem('token') |
|||
|
|||
//获取路由实例 |
|||
const route = useRoute(); |
|||
const router = useRouter(); |
|||
|
|||
// 全局账号 |
|||
const targetId = ref(''); |
|||
|
|||
// 排序参数 |
|||
const sortProp = ref(null); |
|||
const sortOrder = ref(null); |
|||
|
|||
// 表格数据 |
|||
const tableData = ref([]); |
|||
const tableLoading = ref(false); |
|||
const datatotal = ref(0); |
|||
|
|||
// 分页参数 |
|||
const currentPage = ref(1); |
|||
const pageSize = ref(10); |
|||
|
|||
// 获取表格数据 |
|||
const logTableData = async (dccode) => { |
|||
try { |
|||
tableLoading.value = true; |
|||
const requestParams = { |
|||
token: token, |
|||
dccode: dccode, |
|||
type: 1, |
|||
sort_field: sortProp.value, |
|||
sort_order: sortOrder.value, |
|||
page: currentPage.value, |
|||
page_size: pageSize.value |
|||
}; |
|||
const data = await logMListApi(requestParams); |
|||
tableData.value = data.list |
|||
datatotal.value = data.total |
|||
} catch (error) { |
|||
console.error('获取表格数据失败:', error); |
|||
tableData.value = []; |
|||
datatotal.value = 0; |
|||
} finally { |
|||
tableLoading.value = false; |
|||
} |
|||
}; |
|||
|
|||
// 组件挂载时 |
|||
onMounted(() => { |
|||
targetId.value = route.query.dccode; |
|||
logTableData(targetId.value) |
|||
}); |
|||
|
|||
// 返回按钮 |
|||
const goback = () => { |
|||
router.push({ |
|||
path: "/userPermissions/market" |
|||
}); |
|||
}; |
|||
|
|||
// 分页方法 |
|||
const handleSizeChange = (val) => { |
|||
pageSize.value = val; |
|||
logTableData(targetId.value); |
|||
console.log(`每页 ${val} 条`); |
|||
}; |
|||
|
|||
const handleCurrentChange = (val) => { |
|||
currentPage.value = val; |
|||
logTableData(targetId.value); |
|||
console.log(`当前页: ${val}`); |
|||
}; |
|||
|
|||
// 排序事件 |
|||
const handleSortChange = (sort) => { |
|||
const { prop, order } = sort; |
|||
|
|||
if (!['created_at', 'expire_time'].includes(prop)) return; |
|||
|
|||
// 覆盖排序状态(实现二选一) |
|||
sortProp.value = prop; // 保存当前排序字段 |
|||
sortOrder.value = order; // 保存当前排序方式 |
|||
|
|||
logTableData(targetId.value); |
|||
}; |
|||
</script> |
|||
|
|||
<style scoped> |
|||
/* 父容器 */ |
|||
.page-container { |
|||
position: relative; |
|||
min-height: 600px; |
|||
} |
|||
|
|||
/* 按钮组 */ |
|||
.button-group { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 10px; |
|||
margin-bottom: 20px; |
|||
} |
|||
|
|||
/* 按钮样式 */ |
|||
.button-group .el-button { |
|||
padding: 6px 10px !important; |
|||
font-size: 14px !important; |
|||
height: 36px !important; |
|||
} |
|||
|
|||
/* 表格样式 */ |
|||
.table-rounded { |
|||
border-radius: 12px !important; |
|||
overflow: hidden !important; |
|||
border: 1px solid #e4e7ed !important; |
|||
min-height: 800px; |
|||
} |
|||
|
|||
.table-header { |
|||
text-align: center !important; |
|||
font-weight: 800 !important; |
|||
font-size: 15px !important; |
|||
color: #333 !important; |
|||
background-color: #f8f9fa !important; |
|||
} |
|||
|
|||
.el-table__cell { |
|||
border-right: none !important; |
|||
border-bottom: 1px solid #e4e7ed !important; |
|||
} |
|||
|
|||
.el-table__header th.el-table__cell { |
|||
border-right: none !important; |
|||
border-bottom: 1px solid #e4e7ed !important; |
|||
} |
|||
|
|||
.el-table__row:hover .el-table__cell { |
|||
background-color: #fafafa !important; |
|||
} |
|||
|
|||
/* 备注 */ |
|||
.note-ellipsis { |
|||
display: inline-block; |
|||
width: 100%; |
|||
white-space: nowrap; |
|||
overflow: hidden; |
|||
text-overflow: ellipsis; |
|||
padding: 0 4px; |
|||
} |
|||
|
|||
/* 分页组件 */ |
|||
.demo-pagination-block { |
|||
display: flex; |
|||
width: 100%; |
|||
height: 44px; |
|||
padding: 0 16px; |
|||
align-items: center; |
|||
gap: 16px; |
|||
margin-top: 10px; |
|||
border-radius: 0 0 3px 3px; |
|||
border-top: 1px solid #EAEAEA; |
|||
background: #FEFBFB; |
|||
box-sizing: border-box; |
|||
} |
|||
</style> |
|||
@ -1,13 +1,814 @@ |
|||
<template> |
|||
<div>1111</div> |
|||
<div class="page-container"> |
|||
<!-- 搜索区域 --> |
|||
<div class="search-container"> |
|||
<div class="search-form"> |
|||
<div class="search-group"> |
|||
|
|||
<div class="search-group1"> |
|||
<div class="search-item"> |
|||
<span class="form-label">账号</span> |
|||
<el-input |
|||
v-model="searchForm.dccode" |
|||
placeholder="请输入账号" |
|||
clearable |
|||
style="height: 36px; width: 140px;" |
|||
/> |
|||
</div> |
|||
<div class="search-item"> |
|||
<span class="form-label">姓名</span> |
|||
<el-input |
|||
v-model="searchForm.dcname" |
|||
placeholder="请输入姓名" |
|||
clearable |
|||
style="height: 36px; width: 140px;" |
|||
/> |
|||
</div> |
|||
<div class="search-item"> |
|||
<span class="form-label">来源</span> |
|||
<el-select v-model="searchForm.origin_id" placeholder="请选择来源" clearable filterable style="height: 36px; width: 160px" :loading="isOriginLoading"> |
|||
<el-option v-for="origin in originList" :key="origin.origin_id" :label="origin.origin_name" :value="origin.origin_id" /> |
|||
</el-select> |
|||
</div> |
|||
<div class="search-item"> |
|||
<span class="form-label">地区</span> |
|||
<el-select |
|||
v-model="searchForm.market" |
|||
placeholder="请选择地区" |
|||
clearable |
|||
filterable |
|||
style="height: 36px; width: 160px;" |
|||
:loading="isRegionLoading" |
|||
> |
|||
<el-option |
|||
v-for="region in regionList" |
|||
:key="region.ID" |
|||
:label="region.Name" |
|||
:value="region.ID" |
|||
/> |
|||
</el-select> |
|||
</div> |
|||
</div> |
|||
<div class="search-group2"> |
|||
<div class="search-item"> |
|||
<span class="form-label">注册方式</span> |
|||
<el-input |
|||
v-model="searchForm.register_type" |
|||
placeholder="请输入手机号/邮箱" |
|||
clearable |
|||
style="height: 36px; width: 220px;" |
|||
/> |
|||
</div> |
|||
<div class="search-item"> |
|||
<span class="form-label">注册时间</span> |
|||
<el-date-picker v-model="searchDate" type="datetimerange" :shortcuts="shortcuts" range-separator="至" start-placeholder="开始时间" end-placeholder="结束时间" value-format="YYYY-MM-DD HH:mm:ss" /> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="button-group"> |
|||
<el-button type="primary" @click="search">搜索</el-button> |
|||
<el-button type="danger" @click="enableAccess">开通权限</el-button> |
|||
<el-button type="success" @click="exportExcel">导出Excel列表</el-button> |
|||
<el-button color="#626aef" @click="exportList">查看导出列表</el-button> |
|||
<el-button type="primary" @click="resetBn">重置</el-button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- 数据 --> |
|||
<el-table |
|||
:data="tableData" |
|||
style="width: 100%; margin-top: 20px;" |
|||
header-cell-class-name="table-header" |
|||
@sort-change="handleSortChange" |
|||
:default-sort="{ prop: null, order: null }" |
|||
class="table-rounded" |
|||
:loading="tableLoading" |
|||
> |
|||
<el-table-column prop="id" label="序号" align="center" header-align="center" width="80"> |
|||
<template #default="scope"> |
|||
{{ (currentPage - 1) * pageSize + scope.$index + 1 }} |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column prop="jwcode" label="账号" align="center" header-align="center" width="120"/> |
|||
<el-table-column prop="username" label="姓名" align="center" header-align="center" width="150"/> |
|||
<el-table-column prop="origin_name" label="来源" align="center" header-align="center" width="200"/> |
|||
<el-table-column prop="country" label="地区" align="center" header-align="center" width="150"/> |
|||
<el-table-column prop="dialingCode" label="手机区号" align="center" header-align="center" width="80"/> |
|||
<el-table-column prop="mobiles" label="手机号" align="center" header-align="center" width="200"/> |
|||
<!-- <el-table-column prop="emails" label="邮箱" align="center" header-align="center" width="200"/> --> |
|||
<el-table-column prop="emails" label="邮箱" align="center" header-align="center" width="150"> |
|||
<template #default="scope"> |
|||
<el-tooltip |
|||
effect="dark" |
|||
:content="scope.row.emails" |
|||
placement="top" |
|||
> |
|||
<span class="ellipsis-text">{{ scope.row.emails }}</span> |
|||
</el-tooltip> |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column prop="regtime" label="注册时间" align="center" header-align="center" sortable="custom" width="200"/> |
|||
<el-table-column prop="expire_time" label="到期时间" align="center" header-align="center" sortable="custom" width="200"/> |
|||
<el-table-column label="操作" width="180" align="center" header-align="center" fixed="right"> |
|||
<template #default="scope"> |
|||
<el-button type="text" @click="handleEdit(scope.row)">编辑</el-button> |
|||
<el-button type="text" @click="handleLog(scope.row.jwcode)">操作日志</el-button> |
|||
</template> |
|||
</el-table-column> |
|||
</el-table> |
|||
|
|||
<!-- 分页组件 --> |
|||
<div class="demo-pagination-block"> |
|||
<el-pagination |
|||
@size-change="handleSizeChange" |
|||
@current-change="handleCurrentChange" |
|||
:current-page="currentPage" |
|||
:page-sizes="[10, 20, 50, 100]" |
|||
:page-size="pageSize" |
|||
layout="total, sizes, prev, pager, next, jumper" |
|||
:total= "datatotal" |
|||
/> |
|||
</div> |
|||
|
|||
<el-dialog v-model="dialogVisible" :title="addOrUpdata === 1 ? '添加权限' : '设置'" width="500px" :before-close="cancel"> |
|||
<!-- 设置用户 --> |
|||
<div class="form-item" v-if="addOrUpdata === 1"> |
|||
<label class="form-label">设置用户</label> |
|||
<el-input type="textarea" v-model="hlidsInput" rows=10 |
|||
placeholder="请输入HLid... |
|||
示例: |
|||
90048004 |
|||
90048005 |
|||
90048006" |
|||
/> |
|||
<div class="tip">支持批量输入,每次最多1000个(手动/Excel粘贴均可)</div> |
|||
</div> |
|||
|
|||
<!-- 编辑回显 --> |
|||
<div class="info-container" v-if="addOrUpdata === 0"> |
|||
<span class="info-item">Homily ID:{{ hlidsInput }}</span> |
|||
<span class="info-item">当前到期时间:{{ deadline.split(' ')[0] }}</span> |
|||
</div> |
|||
|
|||
<!-- 设置权限时间 --> |
|||
<div class="form-item"> |
|||
<label class="form-label">设置权限时间</label> |
|||
<el-radio-group v-model="timeType" class="radio-group"> |
|||
<el-radio label="expire" class="radio-item"> |
|||
<span>到期时间</span> |
|||
<el-date-picker |
|||
v-model="expireTime" |
|||
type="date" |
|||
placeholder="请选择到期日期" |
|||
:disabled-date="disabledDate" |
|||
style="width: 220px; margin-left: 8px;" |
|||
/> |
|||
</el-radio> |
|||
<el-radio label="delay" class="radio-item"> |
|||
<span>延期时间</span> |
|||
<el-input |
|||
v-model.number="delayValue" |
|||
type="number" |
|||
style="width: 60px; margin: 0 8px;" |
|||
placeholder="1" |
|||
@input="handleDelayInput" |
|||
/> |
|||
<el-select v-model="delayUnit" placeholder="请选择" style="width: 150px;"> |
|||
<el-option label="年" value="year"></el-option> |
|||
<el-option label="月" value="month"></el-option> |
|||
<el-option label="周" value="week"></el-option> |
|||
<el-option label="日" value="day"></el-option> |
|||
</el-select> |
|||
</el-radio> |
|||
</el-radio-group> |
|||
</div> |
|||
|
|||
<!-- 备注--> |
|||
<div class="form-item inline-form-item"> |
|||
<label class="form-label">备注</label> |
|||
<el-input |
|||
type="textarea" |
|||
v-model="remark" |
|||
rows=3 |
|||
placeholder="请输入备注..." |
|||
maxlength="150" |
|||
show-word-limit |
|||
/> |
|||
</div> |
|||
|
|||
<!-- 操作人 --> |
|||
<div class="form-item inline-form-item"> |
|||
<label class="form-label">操作人</label> |
|||
<el-input v-model="operator" placeholder="请填写操作人"/> |
|||
</div> |
|||
|
|||
<!-- 按钮区域 --> |
|||
<div class="dialog-footer"> |
|||
<el-button type="default" plain @click="cancel">取消</el-button> |
|||
<el-button type="primary" @click="submitForm" style="background-color: #ff0000; border-color: #ff0000;">提交</el-button> |
|||
</div> |
|||
</el-dialog> |
|||
</div> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
<script setup> |
|||
import { ref, reactive, onMounted } from 'vue'; |
|||
import { ElMessage } from 'element-plus'; |
|||
import { marketListApi, userMListApi, getOriginListApi,exportMarketApi, exitMApi } from '../../api/userPermissions' |
|||
import router from '../../router'; |
|||
|
|||
} |
|||
// token |
|||
const token = localStorage.getItem('token') |
|||
|
|||
// 搜索表单 |
|||
const searchForm = reactive({ |
|||
dccode: '', |
|||
dcname: '', |
|||
origin_id: "", |
|||
market: '', |
|||
register_type: '' |
|||
}); |
|||
const searchDate = ref([]); |
|||
|
|||
|
|||
// 排序参数 |
|||
const sortProp = ref(null); |
|||
const sortOrder = ref(null); |
|||
|
|||
// 表格数据 |
|||
const tableData = ref([]); |
|||
const tableLoading = ref(false); |
|||
const datatotal = ref(0) |
|||
|
|||
// 分页参数 |
|||
const currentPage = ref(1); |
|||
const pageSize = ref(10); |
|||
|
|||
// 地区下拉框 |
|||
const regionList = ref([]); |
|||
const isRegionLoading = ref(false); |
|||
|
|||
|
|||
// 来源下拉框 |
|||
const originList = ref([]); |
|||
const isOriginLoading = ref(false); |
|||
|
|||
// 弹框显隐控制 |
|||
const dialogVisible = ref(false); |
|||
|
|||
// 开通权限表单 |
|||
const hlidsInput = ref(''); |
|||
const timeType = ref(''); |
|||
const expireTime = ref(''); |
|||
const delayValue = ref(''); |
|||
const delayUnit = ref(''); |
|||
const remark = ref(''); |
|||
const operator = ref(''); |
|||
|
|||
// 判断开通还是编辑 |
|||
const addOrUpdata = ref(0); |
|||
|
|||
// 编辑回显内容 |
|||
const deadline = ref(''); |
|||
|
|||
// 禁用当前日期之前的日期(当天0点之前的时间) |
|||
const disabledDate = (time) => { |
|||
return time.getTime() < new Date().getTime() - 8.64e7; |
|||
}; |
|||
|
|||
// 格式化日期 |
|||
const formatDate = (date) => { |
|||
if (!date) return ''; |
|||
const year = date.getFullYear(); |
|||
const month = String(date.getMonth() + 1).padStart(2, '0'); |
|||
const day = String(date.getDate()).padStart(2, '0'); |
|||
return `${year}/${month}/${day}`; |
|||
}; |
|||
|
|||
// 校验HLid |
|||
const checkHlids = () => { |
|||
// 非空 |
|||
if (!hlidsInput.value.trim()) { |
|||
ElMessage.error('请输入HLid'); |
|||
return false; |
|||
} |
|||
// 处理输入:去空、去重,转数组 |
|||
const hlidList = hlidsInput.value.split('\n') |
|||
.map(item => item.trim()) |
|||
.filter(item => item) |
|||
.filter((item, index, self) => self.indexOf(item) === index); // 去重 |
|||
// 数量校验(最多1000个) |
|||
if (hlidList.length > 1000) { |
|||
ElMessage.error('HLid数量不能超过1000个'); |
|||
return false; |
|||
} |
|||
// 格式校验(8位数字) |
|||
const hlidReg = /^\d{8}$/; |
|||
for (const hlid of hlidList) { |
|||
if (!hlidReg.test(hlid)) { |
|||
ElMessage.error(`有HLid格式错误:${hlid},请重新输入`); |
|||
return false; |
|||
} |
|||
} |
|||
return true; |
|||
}; |
|||
|
|||
// 校验时间 |
|||
const checkTime = () => { |
|||
if (timeType.value === 'expire') { |
|||
// 到期时间 |
|||
if (!expireTime.value) { |
|||
ElMessage.error('请选择到期时间'); |
|||
return false; |
|||
} |
|||
} else if (timeType.value === 'delay') { |
|||
// 延期时间 |
|||
if (!delayValue.value || !delayUnit.value) { |
|||
ElMessage.error('延期时间必须填写完整(数值+单位)'); |
|||
return false; |
|||
} |
|||
const delayNum = Number(delayValue.value); |
|||
if (isNaN(delayNum) || delayNum < 0) { |
|||
ElMessage.error('延期时间不能为负数,请输入有效正数'); |
|||
return false; |
|||
} |
|||
if (delayNum === 0) { |
|||
ElMessage.error('延期时间不能为0,请输入有效正数'); |
|||
return false; |
|||
} |
|||
} else { |
|||
ElMessage.error('请设置权限时间'); |
|||
return false; |
|||
} |
|||
return true; |
|||
}; |
|||
|
|||
// 禁止为小数 |
|||
const handleDelayInput = () => { |
|||
if (delayValue.value !== null && delayValue.value !== undefined) { |
|||
delayValue.value = Math.floor(delayValue.value); |
|||
} |
|||
}; |
|||
|
|||
// 校验备注 |
|||
const checkRemark = () => { |
|||
if (!remark.value.trim()) { |
|||
ElMessage.error('请输入备注'); |
|||
return false; |
|||
} |
|||
return true; |
|||
}; |
|||
|
|||
// 防抖 |
|||
const Noshake = ref(false) |
|||
|
|||
// 提交表单 |
|||
const submitForm = async () => { |
|||
// 防抖 |
|||
if (Noshake.value) return; |
|||
Noshake.value = true; |
|||
|
|||
// 表单校验 |
|||
if (!checkHlids() || !checkTime() || !checkRemark()) { |
|||
Noshake.value = false; |
|||
return; |
|||
} |
|||
|
|||
try { |
|||
// 组装后端要求的参数格式 |
|||
const requestParams = { |
|||
token: token, |
|||
// HLid |
|||
hlids: hlidsInput.value.split('\n') |
|||
.map(item => item.trim()) |
|||
.filter(item => item) |
|||
.join('\n'), |
|||
// 备注 |
|||
remark: remark.value.trim(), |
|||
// 操作人 |
|||
...(operator.value.trim() && { operator: operator.value.trim() }), |
|||
// 时间参数 |
|||
...(timeType.value === 'expire' |
|||
? { expire_time: formatDate(expireTime.value) } |
|||
: { |
|||
[delayUnit.value]: Number(delayValue.value) |
|||
}) |
|||
}; |
|||
|
|||
console.log('传给后端的参数:', requestParams); |
|||
|
|||
// 调用后端接口 |
|||
const res = await exitMApi(requestParams); |
|||
ElMessage.success('成功添加用户权限'); |
|||
|
|||
// 重置表单并关闭弹框 |
|||
resetForm(); |
|||
dialogVisible.value = false; |
|||
addOrUpdata.value = 0; |
|||
|
|||
// 重新获取表格 |
|||
fetchTableData(); |
|||
} catch (error) { |
|||
ElMessage.error('添加权限失败,请重试'); |
|||
} finally { |
|||
Noshake.value = false; |
|||
} |
|||
}; |
|||
|
|||
// 取消表单 |
|||
const cancel = () => { |
|||
resetForm(); |
|||
dialogVisible.value = false; |
|||
addOrUpdata.value = 0; |
|||
}; |
|||
|
|||
// 重置表单数据 |
|||
const resetForm = () => { |
|||
hlidsInput.value = ''; |
|||
timeType.value = ''; |
|||
expireTime.value = ''; |
|||
delayValue.value = ''; |
|||
delayUnit.value = ''; |
|||
remark.value = ''; |
|||
operator.value = ''; |
|||
}; |
|||
|
|||
// 获取来源下拉框 |
|||
const fetchOriginList = async () => { |
|||
try { |
|||
isOriginLoading.value = true; |
|||
const data = await getOriginListApi({ |
|||
token: token, |
|||
app_form: "en", |
|||
}); |
|||
originList.value = data.list; |
|||
} catch (error) { |
|||
console.error("获取来源下拉框失败:", error); |
|||
originList.value = []; |
|||
} finally { |
|||
isOriginLoading.value = false; |
|||
} |
|||
}; |
|||
|
|||
// 获取地区列表 |
|||
const fetchRegionList = async () => { |
|||
try { |
|||
isRegionLoading.value = true; |
|||
const data = await marketListApi({ |
|||
token: token, |
|||
app_form: "en" |
|||
}); |
|||
regionList.value = data.list; |
|||
} catch (error) { |
|||
console.error('获取地区列表失败:', error); |
|||
regionList.value = []; |
|||
} finally { |
|||
isRegionLoading.value = false; |
|||
} |
|||
}; |
|||
|
|||
// 获取表格数据 |
|||
const fetchTableData = async () => { |
|||
try { |
|||
tableLoading.value = true; |
|||
const requestParams = { |
|||
token: token, |
|||
dccode: searchForm.dccode, |
|||
dcname: searchForm.dcname, |
|||
origin_id: searchForm.origin_id, |
|||
market: searchForm.market, |
|||
register_type: searchForm.register_type, |
|||
start_time: searchDate.value && searchDate.value[0] ? searchDate.value[0] : "", |
|||
end_time: searchDate.value && searchDate.value[1] ? searchDate.value[1] : "", |
|||
sort_field: sortProp.value, |
|||
sort_order: sortOrder.value, |
|||
page: currentPage.value, |
|||
page_size: pageSize.value |
|||
}; |
|||
const data = await userMListApi(requestParams); |
|||
tableData.value = data.list |
|||
datatotal.value = data.total |
|||
} catch (error) { |
|||
console.error('获取表格数据失败:', error); |
|||
tableData.value = []; |
|||
datatotal.value = 0 |
|||
} finally { |
|||
tableLoading.value = false; |
|||
} |
|||
}; |
|||
|
|||
// 组件挂载时:获取地区列表 + 初始化表格数据 |
|||
onMounted(() => { |
|||
fetchOriginList() |
|||
fetchRegionList(); |
|||
fetchTableData(); |
|||
}); |
|||
|
|||
// 搜索按钮 |
|||
const search = () => { |
|||
currentPage.value = 1; |
|||
fetchTableData(); |
|||
}; |
|||
|
|||
// 开通权限按钮 |
|||
const enableAccess = () => { |
|||
dialogVisible.value = true; |
|||
addOrUpdata.value = 1; |
|||
}; |
|||
|
|||
// 导出Excel列表按钮 |
|||
const exportExcel = async () => { |
|||
const requestParams = { |
|||
token: token, |
|||
dccode: searchForm.dccode, |
|||
dcname: searchForm.dcname, |
|||
origin_id: searchForm.origin_id, |
|||
market: searchForm.market, |
|||
register_type: searchForm.register_type, |
|||
start_time: searchDate.value && searchDate.value[0] ? searchDate.value[0] : "", |
|||
end_time: searchDate.value && searchDate.value[1] ? searchDate.value[1] : "", |
|||
sort_field: sortProp.value, |
|||
sort_order: sortOrder.value |
|||
}; |
|||
const data = await exportMarketApi(requestParams); |
|||
if (data != '') { |
|||
ElMessage.success('已导出'); |
|||
} |
|||
}; |
|||
|
|||
// 查看导出列表按钮 |
|||
const exportList = () => { |
|||
router.push({ |
|||
path: "/userPermissions/export" |
|||
}); |
|||
}; |
|||
|
|||
// 重置按钮 |
|||
const resetBn = () => { |
|||
searchForm.dccode = ''; |
|||
searchForm.dcname = ''; |
|||
searchForm.origin_id = ''; |
|||
searchForm.market = ''; |
|||
searchForm.register_type = ''; |
|||
searchDate.value = []; |
|||
sortProp.value = null; |
|||
sortOrder.value = null; |
|||
currentPage.value = 1; |
|||
pageSize.value = 10; |
|||
fetchTableData(); |
|||
}; |
|||
|
|||
// 编辑 |
|||
const handleEdit = (row) => { |
|||
dialogVisible.value = true; |
|||
hlidsInput.value = row.jwcode; |
|||
deadline.value = row.expire_time; |
|||
}; |
|||
|
|||
// 操作日志 |
|||
const handleLog = (jwcode) => { |
|||
router.push({ |
|||
path: "/userPermissions/logMarket", |
|||
query: { dccode: jwcode } |
|||
}); |
|||
}; |
|||
|
|||
// 分页方法 |
|||
const handleSizeChange = (val) => { |
|||
pageSize.value = val; |
|||
fetchTableData(); |
|||
console.log(`每页 ${val} 条`); |
|||
}; |
|||
|
|||
const handleCurrentChange = (val) => { |
|||
currentPage.value = val; |
|||
fetchTableData(); |
|||
console.log(`当前页: ${val}`); |
|||
}; |
|||
|
|||
// 排序事件 |
|||
const handleSortChange = (sort) => { |
|||
const { prop, order } = sort; |
|||
|
|||
// 仅允许注册时间和到期时间排序 |
|||
if (!['regtime', 'expire_time'].includes(prop)) return; |
|||
|
|||
// 覆盖排序状态(实现二选一) |
|||
sortProp.value = prop; // 保存当前排序字段 |
|||
sortOrder.value = order; // 保存当前排序方式 |
|||
|
|||
fetchTableData(); |
|||
// console.log(`排序字段:${sortProp.value},排序方式:${sortOrder.value}`); |
|||
}; |
|||
</script> |
|||
|
|||
<style> |
|||
<style scoped> |
|||
/* 父容器 */ |
|||
.page-container { |
|||
position: relative; |
|||
min-height: 600px; |
|||
} |
|||
|
|||
/* 搜索区域 */ |
|||
.search-container { |
|||
display: flex; |
|||
height: auto; |
|||
flex-direction: column; |
|||
justify-content: center; |
|||
align-items: flex-start; |
|||
gap: 12px; |
|||
align-self: stretch; |
|||
border-radius: 8px; |
|||
background: #FEFAF9; |
|||
box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.25); |
|||
padding: 15px; |
|||
margin-bottom: 20px; |
|||
} |
|||
|
|||
/* 搜索表单 */ |
|||
.search-form { |
|||
display: flex; |
|||
align-items: center; |
|||
width: 100%; |
|||
gap: 15px; |
|||
flex-wrap: wrap; |
|||
row-gap: 8px; |
|||
} |
|||
|
|||
/* 搜索组 */ |
|||
.search-group { |
|||
} |
|||
|
|||
.search-group1{ |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 15px; |
|||
} |
|||
|
|||
.search-group2{ |
|||
display: flex; |
|||
margin-top: 15px; |
|||
align-items: center; |
|||
gap: 15px; |
|||
} |
|||
|
|||
|
|||
|
|||
/* 单个搜索项 */ |
|||
.search-item { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 6px; |
|||
} |
|||
|
|||
/* 搜索标签文字 */ |
|||
.form-label { |
|||
font-weight: 800 !important; |
|||
font-size: 15px; |
|||
text-align: left; |
|||
color: #333; |
|||
margin-top: 13px; |
|||
font-family: "SimHei", "Heiti SC", "Microsoft YaHei", sans-serif !important; |
|||
} |
|||
|
|||
/* 按钮组 */ |
|||
.button-group { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 0px !important; |
|||
margin-left: auto; |
|||
} |
|||
|
|||
/* 按钮样式 */ |
|||
.button-group .el-button { |
|||
padding: 6px 10px !important; |
|||
font-size: 14px !important; |
|||
height: 36px !important; |
|||
} |
|||
|
|||
/* 表格样式 */ |
|||
.table-rounded { |
|||
border-radius: 12px !important; |
|||
overflow: hidden !important; |
|||
border: 1px solid #e4e7ed !important; |
|||
height: 700px; |
|||
} |
|||
|
|||
.table-header { |
|||
text-align: center !important; |
|||
font-weight: 800 !important; |
|||
font-size: 15px !important; |
|||
color: #333 !important; |
|||
background-color: #f8f9fa !important; |
|||
} |
|||
|
|||
.el-table__cell { |
|||
border-right: none !important; |
|||
border-bottom: 1px solid #e4e7ed !important; |
|||
} |
|||
|
|||
.el-table__header th.el-table__cell { |
|||
border-right: none !important; |
|||
border-bottom: 1px solid #e4e7ed !important; |
|||
} |
|||
|
|||
.el-table__row:hover .el-table__cell { |
|||
background-color: #fafafa !important; |
|||
} |
|||
|
|||
.ellipsis-text { |
|||
display: inline-block; |
|||
width: 100%; |
|||
white-space: nowrap; |
|||
overflow: hidden; |
|||
text-overflow: ellipsis; |
|||
vertical-align: middle; |
|||
} |
|||
|
|||
/* 分页组件样式 */ |
|||
.demo-pagination-block { |
|||
display: flex; |
|||
width: 100%; |
|||
height: 44px; |
|||
padding: 0 16px; |
|||
align-items: center; |
|||
gap: 16px; |
|||
position: absolute; |
|||
margin-top: 10px; |
|||
border-radius: 0 0 3px 3px; |
|||
border-top: 1px solid #EAEAEA; |
|||
background: #FEFBFB; |
|||
box-sizing: border-box; |
|||
} |
|||
|
|||
/* 添加/修改样式 */ |
|||
.form-item { |
|||
margin-bottom: 24px; |
|||
padding-left: 20px; |
|||
padding-right: 20px; |
|||
text-align: left; |
|||
} |
|||
|
|||
.form-label { |
|||
display: block; |
|||
margin-bottom: 8px; |
|||
font-weight: 500; |
|||
} |
|||
|
|||
.radio-group { |
|||
display: flex; |
|||
flex-direction: column; |
|||
gap: 12px; |
|||
align-items: flex-start; |
|||
} |
|||
|
|||
.radio-item { |
|||
display: flex; |
|||
align-items: center; |
|||
} |
|||
|
|||
.radio-item span { |
|||
margin-right: 8px; |
|||
} |
|||
|
|||
.tip { |
|||
font-size: 12px; |
|||
color: #909399; |
|||
margin-top: 4px; |
|||
} |
|||
|
|||
.dialog-footer { |
|||
display: flex; |
|||
justify-content: flex-end; |
|||
gap: 16px; |
|||
margin-top: 20px; |
|||
} |
|||
|
|||
.inline-form-item { |
|||
display: flex; |
|||
align-items: flex-start; |
|||
gap: 12px; |
|||
} |
|||
|
|||
.inline-form-item .form-label { |
|||
display: inline-block; |
|||
margin-bottom: 0; |
|||
width: 60px; |
|||
text-align: left; |
|||
padding-right: 12px; |
|||
} |
|||
|
|||
.info-container { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 40px; |
|||
padding: 0 20px 18px; |
|||
color: #333; |
|||
font-size: 16px; |
|||
} |
|||
|
|||
.info-item { |
|||
white-space: nowrap; |
|||
} |
|||
</style> |
|||
1567
src/views/UserPermissions/Module.vue
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,802 @@ |
|||
<!-- @format --> |
|||
|
|||
<template> |
|||
<div> |
|||
<div class="page-container"> |
|||
<!-- 搜索区域 --> |
|||
<div class="search-container" @keyup.enter="search"> |
|||
<div class="search-form"> |
|||
<div class="search-group"> |
|||
<div class="search-group1"> |
|||
<div class="search-item"> |
|||
<span class="form-label">账号</span> |
|||
<el-input v-model="searchForm.invitee" placeholder="请输入账号" clearable style="height: 36px; width: 140px" /> |
|||
</div> |
|||
<div class="search-item"> |
|||
<span class="form-label">姓名</span> |
|||
<el-input v-model="searchForm.username" placeholder="请输入姓名" clearable style="height: 36px; width: 180px" /> |
|||
</div> |
|||
<div class="search-item"> |
|||
<span class="form-label">手机号</span> |
|||
<el-input v-model="searchForm.phone" placeholder="请输入手机号" clearable style="height: 36px; width: 200px" /> |
|||
</div> |
|||
<div class="search-item"> |
|||
<span class="form-label">邮箱</span> |
|||
<el-input v-model="searchForm.email" placeholder="请输入邮箱" clearable style="height: 36px; width: 200px" /> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="search-group2"> |
|||
<div class="search-item"> |
|||
<span class="form-label">归属</span> |
|||
<el-input v-model="searchForm.inviter" placeholder="请输入归属账号" :disabled="permission != '2'" clearable style="height: 36px; width: 140px" /> |
|||
</div> |
|||
<div class="search-item"> |
|||
<span class="form-label">来源</span> |
|||
<el-select v-model="searchForm.origin_id" placeholder="请选择来源" clearable filterable style="height: 36px; width: 160px" :loading="isOriginLoading"> |
|||
<el-option v-for="origin in originList" :key="origin.origin_id" :label="origin.origin_name" :value="origin.origin_id" /> |
|||
</el-select> |
|||
</div> |
|||
<div class="search-item"> |
|||
<span class="form-label">客户类型</span> |
|||
<el-select v-model="searchForm.user_role" placeholder="请选择客户类型" clearable style="height: 36px; width: 160px"> |
|||
<el-option label="非网" value="2" /> |
|||
<el-option label="会员" value="1" /> |
|||
</el-select> |
|||
</div> |
|||
<div class="search-item"> |
|||
<span class="form-label">地区</span> |
|||
<el-select v-model="searchForm.country" placeholder="请选择地区" clearable filterable style="height: 36px; width: 160px" :loading="isRegionLoading"> |
|||
<el-option v-for="region in regionList" :key="region.ID" :label="region.Name" :value="region.ID" /> |
|||
</el-select> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="search-group3"> |
|||
<div class="search-item"> |
|||
<span class="form-label">注册时间</span> |
|||
<el-date-picker v-model="searchDate" type="datetimerange" :shortcuts="shortcuts" range-separator="至" start-placeholder="开始时间" end-placeholder="结束时间" value-format="YYYY-MM-DD HH:mm:ss" /> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="button-group"> |
|||
<el-button type="primary" @click="search">搜索</el-button> |
|||
<el-button type="success" @click="exportExcel">导出Excel列表</el-button> |
|||
<el-button color="#626aef" @click="exportList">查看导出列表</el-button> |
|||
<el-button type="primary" @click="resetBn">重置</el-button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- 数据 --> |
|||
<el-table :data="tableData" style="width: 100%; margin-top: 20px" header-cell-class-name="table-header" class="table-rounded" :loading="tableLoading"> |
|||
<el-table-column prop="id" label="序号" align="center" header-align="center" width="80"> |
|||
<template #default="scope"> |
|||
{{ (currentPage - 1) * pageSizeDM + scope.$index + 1 }} |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column prop="invitee" label="账号" align="center" header-align="center" /> |
|||
<el-table-column prop="username" label="姓名" align="center" header-align="center" /> |
|||
<el-table-column prop="inviter" label="归属" align="center" header-align="center" /> |
|||
<el-table-column prop="origin_name" label="来源" align="center" header-align="center" /> |
|||
<el-table-column prop="email" label="邮箱" align="center" header-align="center" width="200"> |
|||
<template #default="scope"> |
|||
<el-tooltip effect="dark" :content="scope.row.email" placement="top"> |
|||
<span class="ellipsis-text">{{ scope.row.email }}</span> |
|||
</el-tooltip> |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column prop="dialing_code" label="手机区号" align="center" header-align="center" /> |
|||
<el-table-column prop="phone" label="手机号" align="center" header-align="center" /> |
|||
<el-table-column prop="country" label="地区" align="center" header-align="center" /> |
|||
<el-table-column prop="regtime" label="注册时间" align="center" header-align="center" width="250" /> |
|||
</el-table> |
|||
|
|||
<!-- 分页组件 --> |
|||
<div class="demo-pagination-block"> |
|||
<el-pagination |
|||
@size-change="handleSizeChangeDM" |
|||
@current-change="handleCurrentChangeDM" |
|||
:current-page="currentPage" |
|||
:page-sizes="[10, 20, 50, 100]" |
|||
:page-size="pageSizeDM" |
|||
layout="total, sizes, prev, pager, next, jumper" |
|||
:total="datatotalDM" |
|||
/> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup> |
|||
import { ref, reactive, onMounted, computed } from "vue"; |
|||
import { ElMessage } from "element-plus"; |
|||
import { getInvitedListApi, exportInvitedApi, marketListApi, getOriginListApi } from "../../api/userPermissions"; |
|||
import router from "../../router"; |
|||
import { useRoute } from "vue-router"; |
|||
import { usePermissionStore } from "../../store/permission"; |
|||
const permissionStore = usePermissionStore(); |
|||
|
|||
// token |
|||
const token = localStorage.getItem("token"); |
|||
//permission |
|||
const permission = ref("-1"); |
|||
|
|||
// 获取路由实例 |
|||
const route = useRoute(); |
|||
|
|||
// 组件挂载时:获取地区列表 + 初始化表格数据 |
|||
onMounted(async () => { |
|||
permission.value = permissionStore.permission; |
|||
fetchOriginList(); |
|||
fetchRegionList(); |
|||
getTableData(); |
|||
}); |
|||
|
|||
const shortcuts = [ |
|||
{ |
|||
text: "最近7天", |
|||
value: () => { |
|||
const end = new Date(); |
|||
const start = new Date(); |
|||
start.setDate(start.getDate() - 7); |
|||
return [start, end]; |
|||
}, |
|||
}, |
|||
{ |
|||
text: "最近1个月", |
|||
value: () => { |
|||
const end = new Date(); |
|||
const start = new Date(); |
|||
start.setMonth(start.getMonth() - 1); |
|||
return [start, end]; |
|||
}, |
|||
}, |
|||
{ |
|||
text: "最近3个月", |
|||
value: () => { |
|||
const end = new Date(); |
|||
const start = new Date(); |
|||
start.setMonth(start.getMonth() - 3); |
|||
return [start, end]; |
|||
}, |
|||
}, |
|||
]; |
|||
|
|||
// 地区下拉框 |
|||
const regionList = ref([]); |
|||
const isRegionLoading = ref(false); |
|||
|
|||
// 获取地区列表 |
|||
const fetchRegionList = async () => { |
|||
try { |
|||
isRegionLoading.value = true; |
|||
const data = await marketListApi({ |
|||
token: token, |
|||
app_form: "en", |
|||
}); |
|||
regionList.value = data.list; |
|||
} catch (error) { |
|||
console.error("获取地区列表失败:", error); |
|||
regionList.value = []; |
|||
} finally { |
|||
isRegionLoading.value = false; |
|||
} |
|||
}; |
|||
|
|||
// 来源下拉框 |
|||
const originList = ref([]); |
|||
const isOriginLoading = ref(false); |
|||
|
|||
// 获取来源下拉框 |
|||
const fetchOriginList = async () => { |
|||
try { |
|||
isOriginLoading.value = true; |
|||
const data = await getOriginListApi({ |
|||
token: token, |
|||
app_form: "en", |
|||
}); |
|||
originList.value = data.list; |
|||
} catch (error) { |
|||
console.error("获取来源下拉框失败:", error); |
|||
originList.value = []; |
|||
} finally { |
|||
isOriginLoading.value = false; |
|||
} |
|||
}; |
|||
|
|||
// 搜索表单 |
|||
const searchForm = reactive({ |
|||
invitee: "", |
|||
username: "", |
|||
phone: "", |
|||
email: "", |
|||
inviter: "", |
|||
origin_id: "", |
|||
user_role: "", |
|||
country: "", |
|||
}); |
|||
|
|||
const searchDate = ref([]); |
|||
|
|||
// 排序参数 |
|||
const sortOrder = ref(null); |
|||
|
|||
// 表格数据 |
|||
const tableData = ref([]); |
|||
const tableLoading = ref(false); |
|||
const datatotalDM = ref(0); |
|||
|
|||
// 分页参数 |
|||
const currentPage = ref(1); |
|||
const pageSizeDM = ref(10); |
|||
|
|||
// 获取表格数据 |
|||
const getTableData = async () => { |
|||
try { |
|||
tableLoading.value = true; |
|||
const requestParams = { |
|||
token: token, |
|||
invitee: searchForm.invitee, |
|||
username: searchForm.username, |
|||
phone: searchForm.phone, |
|||
email: searchForm.email, |
|||
inviter: searchForm.inviter, |
|||
origin_id: searchForm.origin_id, |
|||
user_role: searchForm.user_role, |
|||
country: searchForm.country, |
|||
start_time: searchDate.value && searchDate.value[0] ? searchDate.value[0] : "", |
|||
end_time: searchDate.value && searchDate.value[1] ? searchDate.value[1] : "", |
|||
page: currentPage.value, |
|||
page_size: pageSizeDM.value, |
|||
}; |
|||
const data = await getInvitedListApi(requestParams); |
|||
tableData.value = data.list; |
|||
datatotalDM.value = data.total; |
|||
} catch (error) { |
|||
console.error("获取表格数据失败:", error); |
|||
tableData.value = []; |
|||
datatotalDM.value = 0; |
|||
} finally { |
|||
tableLoading.value = false; |
|||
} |
|||
}; |
|||
|
|||
// 分页方法 |
|||
const handleSizeChangeDM = (val) => { |
|||
pageSizeDM.value = val; |
|||
getTableData(); |
|||
console.log(`每页 ${val} 条`); |
|||
}; |
|||
|
|||
const handleCurrentChangeDM = (val) => { |
|||
currentPage.value = val; |
|||
getTableData(); |
|||
console.log(`当前页: ${val}`); |
|||
}; |
|||
|
|||
// 搜索按钮 |
|||
const search = () => { |
|||
currentPage.value = 1; |
|||
getTableData(); |
|||
}; |
|||
|
|||
// 重置按钮 |
|||
const resetBn = () => { |
|||
searchForm.invitee = ""; |
|||
searchForm.username = ""; |
|||
searchForm.phone = ""; |
|||
searchForm.email = ""; |
|||
searchForm.inviter = ""; |
|||
searchForm.origin_id = ""; |
|||
searchForm.user_role = ""; |
|||
searchForm.country = ""; |
|||
searchDate.value = []; |
|||
|
|||
currentPage.value = 1; |
|||
pageSizeDM.value = 10; |
|||
getTableData(); |
|||
}; |
|||
|
|||
// 导出Excel列表按钮 |
|||
const exportExcel = async () => { |
|||
const requestParams = { |
|||
token: token, |
|||
invitee: searchForm.invitee, |
|||
username: searchForm.username, |
|||
phone: searchForm.phone, |
|||
email: searchForm.email, |
|||
inviter: searchForm.inviter, |
|||
origin_id: searchForm.origin_id, |
|||
user_role: searchForm.user_role, |
|||
start_time: searchDate.value && searchDate.value[0] ? searchDate.value[0] : "", |
|||
end_time: searchDate.value && searchDate.value[1] ? searchDate.value[1] : "", |
|||
country: searchForm.country, |
|||
}; |
|||
const data = await exportInvitedApi(requestParams); |
|||
if (data != "") { |
|||
ElMessage.success("已导出"); |
|||
} |
|||
}; |
|||
|
|||
// 查看导出列表按钮 |
|||
const exportList = () => { |
|||
router.push({ |
|||
path: "/userPermissions/export", |
|||
}); |
|||
}; |
|||
|
|||
// 格式化日期 |
|||
const formatDate = (date) => { |
|||
if (!date) return ""; |
|||
const year = date.getFullYear(); |
|||
const month = String(date.getMonth() + 1).padStart(2, "0"); |
|||
const day = String(date.getDate()).padStart(2, "0"); |
|||
return `${year}/${month}/${day}`; |
|||
}; |
|||
|
|||
// 校验指标 |
|||
const checkmodel = () => { |
|||
if (indicator_id.value.length === 0) { |
|||
ElMessage.error("请至少选择一个指标"); |
|||
return false; |
|||
} |
|||
return true; |
|||
}; |
|||
|
|||
// 校验时间 |
|||
const checkTime = () => { |
|||
if (timeType.value === "expire") { |
|||
// 到期时间 |
|||
if (!expireTime.value) { |
|||
ElMessage.error("请选择到期时间"); |
|||
return false; |
|||
} |
|||
} else if (timeType.value === "delay") { |
|||
// 延期时间 |
|||
if (!delayValue.value || !delayUnit.value) { |
|||
ElMessage.error("延期时间必须填写完整(数值+单位)"); |
|||
return false; |
|||
} |
|||
const delayNum = Number(delayValue.value); |
|||
if (isNaN(delayNum) || delayNum < 0) { |
|||
ElMessage.error("延期时间不能为负数,请输入有效正数"); |
|||
return false; |
|||
} |
|||
if (delayNum === 0) { |
|||
ElMessage.error("延期时间不能为0,请输入有效正数"); |
|||
return false; |
|||
} |
|||
} else { |
|||
ElMessage.error("请设置权限时间"); |
|||
return false; |
|||
} |
|||
return true; |
|||
}; |
|||
|
|||
// 校验备注 |
|||
const checkRemark = () => { |
|||
if (!remark.value.trim()) { |
|||
ElMessage.error("请输入备注"); |
|||
return false; |
|||
} |
|||
return true; |
|||
}; |
|||
</script> |
|||
|
|||
<style scoped> |
|||
/* 父容器 */ |
|||
.page-container { |
|||
position: relative; |
|||
min-height: 600px; |
|||
} |
|||
|
|||
/* 搜索区域 */ |
|||
.search-container { |
|||
display: flex; |
|||
height: 170px; |
|||
flex-direction: column; |
|||
justify-content: center; |
|||
align-items: flex-start; |
|||
gap: 10px; |
|||
align-self: stretch; |
|||
border-radius: 8px; |
|||
background: #fefaf9; |
|||
box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.25); |
|||
padding: 0 15px; |
|||
margin-bottom: 20px; |
|||
} |
|||
|
|||
/* 搜索表单 */ |
|||
.search-form { |
|||
display: flex; |
|||
align-items: center; |
|||
width: 100%; |
|||
gap: 15px; |
|||
flex-wrap: nowrap; |
|||
} |
|||
|
|||
.search-group { |
|||
} |
|||
|
|||
.search-group1 { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 15px; |
|||
} |
|||
|
|||
.search-group2 { |
|||
display: flex; |
|||
margin-top: 15px; |
|||
align-items: center; |
|||
gap: 15px; |
|||
} |
|||
|
|||
.search-group3 { |
|||
display: flex; |
|||
margin-top: 15px; |
|||
align-items: center; |
|||
gap: 15px; |
|||
} |
|||
|
|||
/* 单个搜索项 */ |
|||
.search-item { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 6px; |
|||
} |
|||
|
|||
/* 搜索标签文字 */ |
|||
.form-label { |
|||
font-weight: 800 !important; |
|||
font-size: 15px; |
|||
text-align: left; |
|||
color: #333; |
|||
margin-top: 13px; |
|||
font-family: "SimHei", "Heiti SC", "Microsoft YaHei", sans-serif !important; |
|||
} |
|||
|
|||
/* 按钮组 */ |
|||
.button-group { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 0px !important; |
|||
margin-left: auto; |
|||
} |
|||
|
|||
/* 按钮样式 */ |
|||
.button-group .el-button { |
|||
padding: 6px 10px !important; |
|||
font-size: 14px !important; |
|||
height: 36px !important; |
|||
} |
|||
|
|||
/* 表格样式 */ |
|||
.table-rounded { |
|||
border-radius: 12px !important; |
|||
overflow: hidden !important; |
|||
border: 1px solid #e4e7ed !important; |
|||
height: 650px; |
|||
} |
|||
|
|||
.table-roundedDE { |
|||
border-radius: 12px !important; |
|||
overflow: hidden !important; |
|||
border: 1px solid #e4e7ed !important; |
|||
min-height: 650px; |
|||
} |
|||
|
|||
.table-header { |
|||
text-align: center !important; |
|||
font-weight: 800 !important; |
|||
font-size: 15px !important; |
|||
color: #333 !important; |
|||
background-color: #f8f9fa !important; |
|||
} |
|||
|
|||
.el-table__cell { |
|||
border-right: none !important; |
|||
border-bottom: 1px solid #e4e7ed !important; |
|||
} |
|||
|
|||
.el-table__header th.el-table__cell { |
|||
border-right: none !important; |
|||
border-bottom: 1px solid #e4e7ed !important; |
|||
} |
|||
|
|||
.el-table__row:hover .el-table__cell { |
|||
background-color: #fafafa !important; |
|||
} |
|||
|
|||
/* 分页组件样式 */ |
|||
.demo-pagination-block { |
|||
display: flex; |
|||
width: 100%; |
|||
height: 44px; |
|||
padding: 0 16px; |
|||
align-items: center; |
|||
gap: 16px; |
|||
position: absolute; |
|||
margin-top: 10px; |
|||
border-radius: 0 0 3px 3px; |
|||
border-top: 1px solid #eaeaea; |
|||
background: #fefbfb; |
|||
box-sizing: border-box; |
|||
} |
|||
|
|||
/* 添加/修改样式 */ |
|||
.form-item { |
|||
margin-bottom: 24px; |
|||
padding-left: 20px; |
|||
padding-right: 20px; |
|||
text-align: left; |
|||
} |
|||
|
|||
.form-label { |
|||
display: block; |
|||
margin-bottom: 8px; |
|||
font-weight: 500; |
|||
} |
|||
|
|||
.radio-group { |
|||
display: flex; |
|||
flex-direction: column; |
|||
gap: 12px; |
|||
align-items: flex-start; |
|||
} |
|||
|
|||
.radio-item { |
|||
display: flex; |
|||
align-items: center; |
|||
} |
|||
|
|||
.radio-item span { |
|||
margin-right: 8px; |
|||
} |
|||
|
|||
.tip { |
|||
font-size: 12px; |
|||
color: #909399; |
|||
margin-top: 4px; |
|||
} |
|||
|
|||
.dialog-footer { |
|||
display: flex; |
|||
justify-content: flex-end; |
|||
gap: 16px; |
|||
margin-top: 20px; |
|||
} |
|||
|
|||
.inline-form-item { |
|||
display: flex; |
|||
align-items: flex-start; |
|||
gap: 12px; |
|||
} |
|||
|
|||
.inline-form-item .form-label { |
|||
display: inline-block; |
|||
margin-bottom: 0; |
|||
width: 60px; |
|||
text-align: left; |
|||
padding-right: 12px; |
|||
} |
|||
|
|||
.info-container { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 40px; |
|||
padding: 0 20px 18px; |
|||
color: #333; |
|||
font-size: 16px; |
|||
} |
|||
|
|||
.info-item { |
|||
white-space: nowrap; |
|||
} |
|||
|
|||
/* Tab容器 */ |
|||
.tab-group { |
|||
display: flex; |
|||
gap: 12px; |
|||
justify-content: flex-start; |
|||
margin: 4px 0; |
|||
padding-left: 2px; |
|||
} |
|||
|
|||
/* Tab按钮 */ |
|||
.tab-btn { |
|||
width: 120px; |
|||
padding: 6px 0; |
|||
text-align: center; |
|||
border: 1px solid #ff0000; |
|||
border-radius: 4px; |
|||
background-color: #ffffff; |
|||
color: #ff0000; |
|||
font-size: 14px; |
|||
cursor: pointer; |
|||
transition: all 0.2s; |
|||
outline: none; |
|||
} |
|||
|
|||
/* Tab选中样式 */ |
|||
.tab-btn.active { |
|||
background-color: #ff0000; |
|||
color: #ffffff; |
|||
} |
|||
|
|||
/* hover效果优化 */ |
|||
.tab-btn:not(.active):hover { |
|||
background-color: #fff5f5; |
|||
} |
|||
|
|||
/* 搜索区域(深度探索) */ |
|||
.search-containerDE { |
|||
display: flex; |
|||
height: auto; |
|||
flex-direction: column; |
|||
justify-content: center; |
|||
align-items: flex-start; |
|||
gap: 12px; |
|||
align-self: stretch; |
|||
border-radius: 8px; |
|||
background: #fefaf9; |
|||
box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.25); |
|||
padding: 15px; |
|||
margin-bottom: 20px; |
|||
} |
|||
|
|||
/* 搜索表单(深度探索) */ |
|||
.search-formDE { |
|||
display: flex; |
|||
align-items: center; |
|||
width: 100%; |
|||
gap: 15px; |
|||
flex-wrap: wrap; |
|||
row-gap: 8px; |
|||
} |
|||
|
|||
/* 单个搜索项(深度探索) */ |
|||
.search-itemDE { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 6px; |
|||
} |
|||
|
|||
/* 搜索标签文字(深度探索) */ |
|||
.form-labelDE { |
|||
font-weight: 800 !important; |
|||
font-size: 15px; |
|||
text-align: left; |
|||
color: #333; |
|||
margin-top: 0; |
|||
font-family: "SimHei", "Heiti SC", "Microsoft YaHei", sans-serif !important; |
|||
} |
|||
|
|||
/* 按钮组(深度探索) */ |
|||
.button-groupDE { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 10px !important; |
|||
} |
|||
|
|||
/* 按钮样式(深度探索) */ |
|||
.button-groupDE .el-button { |
|||
padding: 6px 10px !important; |
|||
font-size: 14px !important; |
|||
height: 36px !important; |
|||
} |
|||
|
|||
/* 文本溢出省略样式(深度探索) */ |
|||
.ellipsis-text { |
|||
display: inline-block; |
|||
width: 100%; |
|||
white-space: nowrap; |
|||
overflow: hidden; |
|||
text-overflow: ellipsis; |
|||
vertical-align: middle; |
|||
} |
|||
|
|||
/* 开通/编辑指标复选(深度探索) */ |
|||
.indicator-checkbox-group { |
|||
display: flex; |
|||
flex-wrap: wrap; |
|||
gap: 8px; |
|||
margin-top: 8px; |
|||
width: 100%; |
|||
} |
|||
|
|||
.indicator-checkbox-item { |
|||
flex: 0 0 calc(20% - 5px); |
|||
box-sizing: border-box; |
|||
padding: 4px 0; |
|||
} |
|||
|
|||
:deep(.el-checkbox__input.is-checked .el-checkbox__inner) { |
|||
background-color: #ff0000 !important; |
|||
border-color: #ff0000 !important; |
|||
} |
|||
|
|||
:deep(.el-checkbox__input.is-checked .el-checkbox__inner::after) { |
|||
border-color: #fff !important; |
|||
} |
|||
|
|||
:deep(.el-checkbox__input:hover .el-checkbox__inner) { |
|||
border-color: #ff0000 !important; |
|||
} |
|||
|
|||
:deep(.el-checkbox__input:focus .el-checkbox__inner) { |
|||
box-shadow: 0 0 0 2px rgba(255, 0, 0, 0.2) !important; |
|||
} |
|||
|
|||
:deep(.el-checkbox__label) { |
|||
color: #333 !important; |
|||
font-size: 14px !important; |
|||
} |
|||
|
|||
/* 权限详情(深度探索) */ |
|||
.permission-detail-dialog { |
|||
--el-dialog-padding-primary: 15px; |
|||
} |
|||
|
|||
.detail-container { |
|||
background-color: #fff1f0; |
|||
border-radius: 6px; |
|||
padding: 15px; |
|||
} |
|||
|
|||
.hlid-item { |
|||
margin-bottom: 25px; |
|||
} |
|||
|
|||
.label { |
|||
color: #666; |
|||
font-weight: 500; |
|||
} |
|||
|
|||
.value { |
|||
color: #333; |
|||
} |
|||
|
|||
.expire-section { |
|||
margin-top: 10px; |
|||
} |
|||
|
|||
.section-title { |
|||
font-size: 16px; |
|||
color: #333; |
|||
margin-bottom: 10px; |
|||
font-weight: bold; |
|||
} |
|||
|
|||
.indicator-list { |
|||
display: flex; |
|||
flex-direction: column; |
|||
gap: 8px; |
|||
} |
|||
|
|||
.indicator-item { |
|||
padding: 6px 0; |
|||
display: flex; |
|||
align-items: center; |
|||
} |
|||
|
|||
.indicator-name { |
|||
color: #666; |
|||
font-size: 14px; |
|||
} |
|||
|
|||
.expire-time { |
|||
color: #333; |
|||
font-size: 14px; |
|||
margin-left: 8px; |
|||
} |
|||
|
|||
.expired-tag { |
|||
background-color: #ffccd5; |
|||
color: #f56c6c; |
|||
font-size: 12px; |
|||
padding: 2px 8px; |
|||
border-radius: 4px; |
|||
margin-left: 8px; |
|||
} |
|||
</style> |
|||
@ -1,5 +0,0 @@ |
|||
module.exports = { |
|||
env: { |
|||
jest: true |
|||
} |
|||
} |
|||
@ -1,98 +0,0 @@ |
|||
import { mount, createLocalVue } from '@vue/test-utils' |
|||
import VueRouter from 'vue-router' |
|||
import ElementUI from 'element-ui' |
|||
import Breadcrumb from '@/components/Breadcrumb/index.vue' |
|||
|
|||
const localVue = createLocalVue() |
|||
localVue.use(VueRouter) |
|||
localVue.use(ElementUI) |
|||
|
|||
const routes = [ |
|||
{ |
|||
path: '/', |
|||
name: 'home', |
|||
children: [{ |
|||
path: 'dashboard', |
|||
name: 'dashboard' |
|||
}] |
|||
}, |
|||
{ |
|||
path: '/menu', |
|||
name: 'menu', |
|||
children: [{ |
|||
path: 'menu1', |
|||
name: 'menu1', |
|||
meta: { title: 'menu1' }, |
|||
children: [{ |
|||
path: 'menu1-1', |
|||
name: 'menu1-1', |
|||
meta: { title: 'menu1-1' } |
|||
}, |
|||
{ |
|||
path: 'menu1-2', |
|||
name: 'menu1-2', |
|||
redirect: 'noredirect', |
|||
meta: { title: 'menu1-2' }, |
|||
children: [{ |
|||
path: 'menu1-2-1', |
|||
name: 'menu1-2-1', |
|||
meta: { title: 'menu1-2-1' } |
|||
}, |
|||
{ |
|||
path: 'menu1-2-2', |
|||
name: 'menu1-2-2' |
|||
}] |
|||
}] |
|||
}] |
|||
}] |
|||
|
|||
const router = new VueRouter({ |
|||
routes |
|||
}) |
|||
|
|||
describe('Breadcrumb.vue', () => { |
|||
const wrapper = mount(Breadcrumb, { |
|||
localVue, |
|||
router |
|||
}) |
|||
it('dashboard', () => { |
|||
router.push('/dashboard') |
|||
const len = wrapper.findAll('.el-breadcrumb__inner').length |
|||
expect(len).toBe(1) |
|||
}) |
|||
it('normal route', () => { |
|||
router.push('/menu/menu1') |
|||
const len = wrapper.findAll('.el-breadcrumb__inner').length |
|||
expect(len).toBe(2) |
|||
}) |
|||
it('nested route', () => { |
|||
router.push('/menu/menu1/menu1-2/menu1-2-1') |
|||
const len = wrapper.findAll('.el-breadcrumb__inner').length |
|||
expect(len).toBe(4) |
|||
}) |
|||
it('no meta.title', () => { |
|||
router.push('/menu/menu1/menu1-2/menu1-2-2') |
|||
const len = wrapper.findAll('.el-breadcrumb__inner').length |
|||
expect(len).toBe(3) |
|||
}) |
|||
// it('click link', () => {
|
|||
// router.push('/menu/menu1/menu1-2/menu1-2-2')
|
|||
// const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner')
|
|||
// const second = breadcrumbArray.at(1)
|
|||
// console.log(breadcrumbArray)
|
|||
// const href = second.find('a').attributes().href
|
|||
// expect(href).toBe('#/menu/menu1')
|
|||
// })
|
|||
// it('noRedirect', () => {
|
|||
// router.push('/menu/menu1/menu1-2/menu1-2-1')
|
|||
// const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner')
|
|||
// const redirectBreadcrumb = breadcrumbArray.at(2)
|
|||
// expect(redirectBreadcrumb.contains('a')).toBe(false)
|
|||
// })
|
|||
it('last breadcrumb', () => { |
|||
router.push('/menu/menu1/menu1-2/menu1-2-1') |
|||
const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner') |
|||
const redirectBreadcrumb = breadcrumbArray.at(3) |
|||
expect(redirectBreadcrumb.contains('a')).toBe(false) |
|||
}) |
|||
}) |
|||
Some files were not shown because too many files changed in this diff
Write
Preview
Loading…
Cancel
Save
Reference in new issue