Compare commits

...

No commits in common. 'main' and 'dev' have entirely different histories.
main ... dev

  1. 14
      .editorconfig
  2. 7
      .env.development
  3. 8
      .env.production
  4. 8
      .env.staging
  5. 2
      .env.test
  6. 4
      .eslintignore
  7. 198
      .eslintrc.js
  8. 22
      .gitignore
  9. 5
      .travis.yml
  10. 3
      .vscode/extensions.json
  11. 21
      LICENSE
  12. 111
      README-zh.md
  13. 100
      README.md
  14. 14
      babel.config.js
  15. 35
      build/index.js
  16. 13
      index.html
  17. 24
      jest.config.js
  18. 9
      jsconfig.json
  19. 57
      mock/index.js
  20. 81
      mock/mock-server.js
  21. 29
      mock/table.js
  22. 84
      mock/user.js
  23. 25
      mock/utils.js
  24. 1971
      package-lock.json
  25. 73
      package.json
  26. 8
      postcss.config.js
  27. BIN
      public/favicon.ico
  28. 17
      public/index.html
  29. 1
      public/vite.svg
  30. 17
      src/App.vue
  31. 9
      src/api/table.js
  32. 24
      src/api/user.js
  33. 201
      src/api/userPermissions.js
  34. BIN
      src/assets/404_images/404.png
  35. BIN
      src/assets/404_images/404_cloud.png
  36. BIN
      src/assets/images/deepChart.png
  37. BIN
      src/assets/images/login.png
  38. 1
      src/assets/vue.svg
  39. 71
      src/components/Breadcrumb/index.vue
  40. 44
      src/components/Hamburger/index.vue
  41. 62
      src/components/SvgIcon/index.vue
  42. 9
      src/icons/index.js
  43. 1
      src/icons/svg/dashboard.svg
  44. 1
      src/icons/svg/example.svg
  45. 1
      src/icons/svg/eye-open.svg
  46. 1
      src/icons/svg/eye.svg
  47. 1
      src/icons/svg/form.svg
  48. 1
      src/icons/svg/link.svg
  49. 1
      src/icons/svg/nested.svg
  50. 1
      src/icons/svg/password.svg
  51. 1
      src/icons/svg/table.svg
  52. 1
      src/icons/svg/tree.svg
  53. 1
      src/icons/svg/user.svg
  54. 22
      src/icons/svgo.yml
  55. 417
      src/layout/Layout.vue
  56. 40
      src/layout/components/AppMain.vue
  57. 133
      src/layout/components/Navbar.vue
  58. 26
      src/layout/components/Sidebar/FixiOSBug.js
  59. 41
      src/layout/components/Sidebar/Item.vue
  60. 43
      src/layout/components/Sidebar/Link.vue
  61. 82
      src/layout/components/Sidebar/Logo.vue
  62. 95
      src/layout/components/Sidebar/SidebarItem.vue
  63. 56
      src/layout/components/Sidebar/index.vue
  64. 3
      src/layout/components/index.js
  65. 93
      src/layout/index.vue
  66. 45
      src/layout/mixin/ResizeHandler.js
  67. 61
      src/main.js
  68. 64
      src/permission.js
  69. 157
      src/router/index.js
  70. 16
      src/settings.js
  71. 8
      src/store/getters.js
  72. 19
      src/store/index.js
  73. 48
      src/store/modules/app.js
  74. 32
      src/store/modules/settings.js
  75. 97
      src/store/modules/user.js
  76. 19
      src/store/permission.js
  77. 79
      src/style.css
  78. 49
      src/styles/element-ui.scss
  79. 65
      src/styles/index.scss
  80. 28
      src/styles/mixin.scss
  81. 226
      src/styles/sidebar.scss
  82. 48
      src/styles/transition.scss
  83. 25
      src/styles/variables.scss
  84. 15
      src/utils/auth.js
  85. 10
      src/utils/get-page-title.js
  86. 117
      src/utils/index.js
  87. 84
      src/utils/myAxios.js
  88. 86
      src/utils/request.js
  89. 20
      src/utils/validate.js
  90. 228
      src/views/404.vue
  91. 402
      src/views/Login.vue
  92. 126
      src/views/UserPermissions/Export.vue
  93. 223
      src/views/UserPermissions/LogDeepExplore.vue
  94. 209
      src/views/UserPermissions/LogDeepMate.vue
  95. 223
      src/views/UserPermissions/LogMarket.vue
  96. 811
      src/views/UserPermissions/Market.vue
  97. 1567
      src/views/UserPermissions/Module.vue
  98. 802
      src/views/UserPermissions/invitedLook.vue
  99. 5
      tests/unit/.eslintrc.js
  100. 98
      tests/unit/components/Breadcrumb.spec.js

14
.editorconfig

@ -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

7
.env.development

@ -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"

8
.env.production

@ -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"

8
.env.staging

@ -1,8 +0,0 @@
NODE_ENV = production
# just a flag
ENV = 'staging'
# base api
VUE_APP_BASE_API = '/stage-api'

2
.env.test

@ -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"

4
.eslintignore

@ -1,4 +0,0 @@
build/*.js
src/assets
public
dist

198
.eslintrc.js

@ -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']
}
}

22
.gitignore

@ -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?

5
.travis.yml

@ -1,5 +0,0 @@
language: node_js
node_js: 10
script: npm run test
notifications:
email: false

3
.vscode/extensions.json

@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}

21
LICENSE

@ -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.

111
README-zh.md

@ -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
![demo](https://github.com/PanJiaChen/PanJiaChen.github.io/blob/master/images/demo.gif)
## 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

100
README.md

@ -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
![demo](https://github.com/PanJiaChen/PanJiaChen.github.io/blob/master/images/demo.gif)
## 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).

14
babel.config.js

@ -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']
}
}
}

35
build/index.js

@ -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}`)
}

13
index.html

@ -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>

24
jest.config.js

@ -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/'
}

9
jsconfig.json

@ -1,9 +0,0 @@
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@/*": ["src/*"]
}
},
"exclude": ["node_modules", "dist"]
}

57
mock/index.js

@ -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
}

81
mock/mock-server.js

@ -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))
}
}
})
}

29
mock/table.js

@ -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
}
}
}
}
]

84
mock/user.js

@ -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'
}
}
}
]

25
mock/utils.js

@ -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

73
package.json

@ -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"
}
}

8
postcss.config.js

@ -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': {}
}
}

BIN
public/favicon.ico

17
public/index.html

@ -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>

1
public/vite.svg

@ -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>

17
src/App.vue

@ -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>

9
src/api/table.js

@ -1,9 +0,0 @@
import request from '@/utils/request'
export function getList(params) {
return request({
url: '/vue-admin-template/table/list',
method: 'get',
params
})
}

24
src/api/user.js

@ -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'
})
}

201
src/api/userPermissions.js

@ -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,
});
}

BIN
src/assets/404_images/404.png

Before

Width: 1014  |  Height: 556  |  Size: 96 KiB

BIN
src/assets/404_images/404_cloud.png

Before

Width: 152  |  Height: 138  |  Size: 4.7 KiB

BIN
src/assets/images/deepChart.png

After

Width: 48  |  Height: 48  |  Size: 1.9 KiB

BIN
src/assets/images/login.png

After

Width: 1920  |  Height: 1080  |  Size: 422 KiB

1
src/assets/vue.svg

@ -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>

71
src/components/Breadcrumb/index.vue

@ -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>

44
src/components/Hamburger/index.vue

@ -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>

62
src/components/SvgIcon/index.vue

@ -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>

9
src/icons/index.js

@ -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
src/icons/svg/dashboard.svg

@ -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
src/icons/svg/example.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
src/icons/svg/eye-open.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
src/icons/svg/eye.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
src/icons/svg/form.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
src/icons/svg/link.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
src/icons/svg/nested.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
src/icons/svg/password.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
src/icons/svg/table.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
src/icons/svg/tree.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
src/icons/svg/user.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>

22
src/icons/svgo.yml

@ -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'

417
src/layout/Layout.vue

@ -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>

40
src/layout/components/AppMain.vue

@ -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>

133
src/layout/components/Navbar.vue

@ -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>

26
src/layout/components/Sidebar/FixiOSBug.js

@ -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)
}
}
}
}
}

41
src/layout/components/Sidebar/Item.vue

@ -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>

43
src/layout/components/Sidebar/Link.vue

@ -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>

82
src/layout/components/Sidebar/Logo.vue

@ -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>

95
src/layout/components/Sidebar/SidebarItem.vue

@ -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>

56
src/layout/components/Sidebar/index.vue

@ -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>

3
src/layout/components/index.js

@ -1,3 +0,0 @@
export { default as Navbar } from './Navbar'
export { default as Sidebar } from './Sidebar'
export { default as AppMain } from './AppMain'

93
src/layout/index.vue

@ -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>

45
src/layout/mixin/ResizeHandler.js

@ -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 })
}
}
}
}
}

61
src/main.js

@ -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')

64
src/permission.js

@ -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()
})

157
src/router/index.js

@ -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

16
src/settings.js

@ -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
}

8
src/store/getters.js

@ -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

19
src/store/index.js

@ -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

48
src/store/modules/app.js

@ -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
}

32
src/store/modules/settings.js

@ -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
}

97
src/store/modules/user.js

@ -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
}

19
src/store/permission.js

@ -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 };
});

79
src/style.css

@ -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;
}
}

49
src/styles/element-ui.scss

@ -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;
}

65
src/styles/index.scss

@ -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;
}

28
src/styles/mixin.scss

@ -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%;
}

226
src/styles/sidebar.scss

@ -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;
}
}
}

48
src/styles/transition.scss

@ -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;
}

25
src/styles/variables.scss

@ -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;
}

15
src/utils/auth.js

@ -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)
}

10
src/utils/get-page-title.js

@ -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}`
}

117
src/utils/index.js

@ -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
}

84
src/utils/myAxios.js

@ -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

86
src/utils/request.js

@ -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

20
src/utils/validate.js

@ -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
}

228
src/views/404.vue

@ -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>

402
src/views/Login.vue

@ -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>

126
src/views/UserPermissions/Export.vue

@ -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>

223
src/views/UserPermissions/LogDeepExplore.vue

@ -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>

209
src/views/UserPermissions/LogDeepMate.vue

@ -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>

223
src/views/UserPermissions/LogMarket.vue

@ -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>

811
src/views/UserPermissions/Market.vue

@ -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

802
src/views/UserPermissions/invitedLook.vue

@ -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>

5
tests/unit/.eslintrc.js

@ -1,5 +0,0 @@
module.exports = {
env: {
jest: true
}
}

98
tests/unit/components/Breadcrumb.spec.js

@ -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

Loading…
Cancel
Save