Compare commits

...

No commits in common. 'main' and 'milestone-20260207-AI复盘二期' have entirely different histories.

  1. 14
      .editorconfig
  2. 8
      .env.development
  3. 9
      .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. 1997
      package-lock.json
  25. 74
      package.json
  26. 8
      postcss.config.js
  27. BIN
      public/favicon.ico
  28. 17
      public/index.html
  29. 1
      public/vite.svg
  30. 15
      src/App.vue
  31. 128
      src/api/eventManagement.js
  32. 414
      src/api/platformData.js
  33. 9
      src/api/table.js
  34. 24
      src/api/user.js
  35. 201
      src/api/userPermissions.js
  36. BIN
      src/assets/404_images/404.png
  37. BIN
      src/assets/404_images/404_cloud.png
  38. BIN
      src/assets/images/deepChart.png
  39. BIN
      src/assets/images/login.png
  40. 1
      src/assets/vue.svg
  41. 71
      src/components/Breadcrumb/index.vue
  42. 44
      src/components/Hamburger/index.vue
  43. 62
      src/components/SvgIcon/index.vue
  44. 9
      src/icons/index.js
  45. 1
      src/icons/svg/dashboard.svg
  46. 1
      src/icons/svg/example.svg
  47. 1
      src/icons/svg/eye-open.svg
  48. 1
      src/icons/svg/eye.svg
  49. 1
      src/icons/svg/form.svg
  50. 1
      src/icons/svg/link.svg
  51. 1
      src/icons/svg/nested.svg
  52. 1
      src/icons/svg/password.svg
  53. 1
      src/icons/svg/table.svg
  54. 1
      src/icons/svg/tree.svg
  55. 1
      src/icons/svg/user.svg
  56. 22
      src/icons/svgo.yml
  57. 472
      src/layout/Layout.vue
  58. 40
      src/layout/components/AppMain.vue
  59. 133
      src/layout/components/Navbar.vue
  60. 26
      src/layout/components/Sidebar/FixiOSBug.js
  61. 41
      src/layout/components/Sidebar/Item.vue
  62. 43
      src/layout/components/Sidebar/Link.vue
  63. 82
      src/layout/components/Sidebar/Logo.vue
  64. 95
      src/layout/components/Sidebar/SidebarItem.vue
  65. 56
      src/layout/components/Sidebar/index.vue
  66. 3
      src/layout/components/index.js
  67. 93
      src/layout/index.vue
  68. 45
      src/layout/mixin/ResizeHandler.js
  69. 61
      src/main.js
  70. 64
      src/permission.js
  71. 192
      src/router/index.js
  72. 16
      src/settings.js
  73. 8
      src/store/getters.js
  74. 19
      src/store/index.js
  75. 48
      src/store/modules/app.js
  76. 32
      src/store/modules/settings.js
  77. 97
      src/store/modules/user.js
  78. 19
      src/store/permission.js
  79. 79
      src/style.css
  80. 49
      src/styles/element-ui.scss
  81. 65
      src/styles/index.scss
  82. 28
      src/styles/mixin.scss
  83. 226
      src/styles/sidebar.scss
  84. 48
      src/styles/transition.scss
  85. 25
      src/styles/variables.scss
  86. 15
      src/utils/auth.js
  87. 10
      src/utils/get-page-title.js
  88. 117
      src/utils/index.js
  89. 89
      src/utils/myAxios.js
  90. 86
      src/utils/request.js
  91. 20
      src/utils/validate.js
  92. 228
      src/views/404.vue
  93. 744
      src/views/EventManagement/ContentConfiguration.vue
  94. 524
      src/views/EventManagement/ExchangeRecords.vue
  95. 385
      src/views/EventManagement/LearningPageConfig.vue
  96. 427
      src/views/EventManagement/WinningRecords.vue
  97. 392
      src/views/Login.vue
  98. 622
      src/views/PlatformData/UserActivityStats.vue
  99. 1324
      src/views/PlatformData/UserLoginStats.vue
  100. 1007
      src/views/PlatformData/UserOverview.vue

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

8
.env.development

@ -1,5 +1,3 @@
# 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"
VITE_API_BASE_URLXXCG=https://api.homilychart.com/

9
.env.production

@ -1,6 +1,3 @@
# 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"
VITE_API_BASE_URLXXCG=https://api.homilychart.com/

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* npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.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 # Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea .idea
.vscode
.DS_Store
*.suo *.suo
*.ntvs* *.ntvs*
*.njsproj *.njsproj
*.sln *.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
}

1997
package-lock.json
File diff suppressed because it is too large
View File

74
package.json

@ -1,62 +1,26 @@
{ {
"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": { "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": { "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",
"echarts": "^6.0.0",
"element-plus": "^2.11.8",
"normalize.css": "^8.0.1",
"pinia": "^3.0.4",
"vue": "^3.5.24",
"vue-router": "^4.6.3"
}, },
"devDependencies": { "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>

15
src/App.vue

@ -1,11 +1,14 @@
<template> <template>
<div id="app">
<router-view /> <router-view />
</div>
</template> </template>
<script>
export default {
name: 'App'
}
<script setup>
</script> </script>
<style scoped>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
</style>

128
src/api/eventManagement.js

@ -0,0 +1,128 @@
import request from '../utils/myAxios';
var base_url = import.meta.env.VITE_API_BASE_URL
// 历史记录--获取用户抽奖记录
export function userLuckyDrawListApi(params) {
return request({
url: base_url + "/admin/luckyDraw/list",
method: "post",
data: params,
});
}
// 历史记录--导出用户抽奖记录
export function exportUserLuckyDrawListApi(params) {
return request({
url: base_url + "/admin/luckyDraw/export/create",
method: "post",
data: params,
});
}
// 内容配置--获取配置列表
export function getContentListApi(params) {
return request({
url: base_url + "/admin/luckyDraw/getContentList",
method: "post",
data: params,
});
}
// 内容配置--添加奖品
export function addDrawConfigApi(params) {
return request({
url: base_url + "/admin/luckyDraw/addDrawConfig",
method: "post",
data: params,
});
}
// 内容配置--删除奖品
export function deleteDrawApi(params) {
return request({
url: base_url + "/admin/luckyDraw/deleteDraw",
method: "post",
data: params,
});
}
// 内容配置--修改奖品状态
export function changeStatusApi(params) {
return request({
url: base_url + "/admin/luckyDraw/changeStatus",
method: "post",
data: params,
});
}
// 内容配置--获取模板名称
export function getTemplateApi(params) {
return request({
url: base_url + "/admin/indicator/list",
method: "post",
data: params,
});
}
// Token兑换记录--获取token兑换记录
export function getTokenExchangeRecordApi(params) {
return request({
url: base_url + "/admin/luckyDraw/tokenExchangeRecord",
method: "post",
data: params,
});
}
// Token兑换记录--导出token兑换记录
export function exportTokenExchangeRecordApi(params) {
return request({
url: base_url + "/admin/luckyDraw/tokenExport/create",
method: "post",
data: params,
});
}
// Token兑换记录--获取token兑换方式下拉框
export function getDropDownListApi(params) {
return request({
url: base_url + "/admin/luckyDraw/token/dropDownList",
method: "post",
data: params,
});
}
// 获取学习页配置列表--已接
export function getLearningPageListApi(params) {
return request({
url: base_url + "/admin/studyArticle/list",
method: "post",
data: params,
});
}
// 添加关联文章--已接
export function addLearningPageApi(params) {
return request({
url: base_url + "/admin/studyArticle/save",
method: "post",
data: params,
});
}
// 删除学习页配置--已接
export function deleteLearningPageApi(params) {
return request({
url: base_url + "/admin/studyArticle/delete",
method: "post",
data: params,
});
}
// 修改关联状态--已接
export function changeLearningPageStatusApi(params) {
return request({
url: base_url + "/admin/studyArticle/status",
method: "post",
data: params,
});
}

414
src/api/platformData.js

@ -0,0 +1,414 @@
import request from '../utils/myAxios'
var base_url = import.meta.env.VITE_API_BASE_URL
var base_url_link = import.meta.env.VITE_API_BASE_URL_LINK
// 获取用户数据概览列表
export function getUserOverviewList() {
const formData = new FormData();
formData.append('token', localStorage.getItem('token'));
return request({
url: base_url + '/admin/user/overview/list',
method: 'post',
headers: {
'token': localStorage.getItem('token'),
'client': 'ios',
'version': '1',
'Content-Type': 'multipart/form-data'
},
data: formData
})
}
// 获取用户数据明细列表
export function getUserFullReportList(params) {
const formData = new FormData();
formData.append('token', localStorage.getItem('token'));
if (params) {
if (params.start_time) formData.append('start_time', params.start_time);
if (params.end_time) formData.append('end_time', params.end_time);
}
return request({
url: base_url + '/admin/user/fullReport/list',
method: 'post',
headers: {
'token': localStorage.getItem('token'),
'client': 'ios',
'version': '1',
'Content-Type': 'multipart/form-data'
},
data: formData
})
}
// 导出用户数据明细 PDF
export function exportUserFullReport(params) {
const formData = new FormData();
formData.append('token', localStorage.getItem('token'));
if (params) {
if (params.start_time) formData.append('start_time', params.start_time);
if (params.end_time) formData.append('end_time', params.end_time);
}
return request({
url: base_url + '/admin/user/fullReport/list/exportPDF',
method: 'post',
headers: {
'token': localStorage.getItem('token'),
'client': 'ios',
'version': '1',
'Content-Type': 'multipart/form-data'
},
data: formData,
responseType: 'blob' // Important for file download
})
}
// 获取用户登录数据列表
export function getUserLoginList(params) {
const formData = new FormData();
formData.append('token', localStorage.getItem('token'));
if (params) {
if (params.region) formData.append('region', params.region);
if (params.start_time) formData.append('start_time', params.start_time);
if (params.end_time) formData.append('end_time', params.end_time);
}
return request({
url: base_url + '/admin/user/login/list',
method: 'post',
headers: {
'token': localStorage.getItem('token'),
'client': 'ios',
'version': '1',
'Content-Type': 'multipart/form-data'
},
data: formData
})
}
// 获取今日登录数据(渠道分布)
export function getUserLoginChannel(params) {
const formData = new FormData();
formData.append('token', localStorage.getItem('token'));
if (params) {
if (params.region) formData.append('region', params.region);
if (params.start_time) formData.append('start_time', params.start_time);
if (params.end_time) formData.append('end_time', params.end_time);
}
return request({
url: base_url + '/admin/user/login/channel',
method: 'post',
headers: {
'token': localStorage.getItem('token'),
'client': 'ios',
'version': '1',
'Content-Type': 'multipart/form-data'
},
data: formData
})
}
// 获取会员登录数据(渠道分布)
export function getUserLoginChannelMember(params) {
const formData = new FormData();
formData.append('token', localStorage.getItem('token'));
if (params) {
if (params.region) formData.append('region', params.region);
if (params.start_time) formData.append('start_time', params.start_time);
if (params.end_time) formData.append('end_time', params.end_time);
}
return request({
url: base_url + '/admin/user/login/channelMember',
method: 'post',
headers: {
'token': localStorage.getItem('token'),
'client': 'ios',
'version': '1',
'Content-Type': 'multipart/form-data'
},
data: formData
})
}
// 获取非网登录数据(渠道分布)
export function getUserLoginChannelNoMember(params) {
const formData = new FormData();
formData.append('token', localStorage.getItem('token'));
if (params) {
if (params.region) formData.append('region', params.region);
if (params.start_time) formData.append('start_time', params.start_time);
if (params.end_time) formData.append('end_time', params.end_time);
}
return request({
url: base_url + '/admin/user/active/channelNonMember',
method: 'post',
headers: {
'token': localStorage.getItem('token'),
'client': 'ios',
'version': '1',
'Content-Type': 'multipart/form-data'
},
data: formData
})
}
// 获取用户登录趋势数据
export function getUserLoginTrend(params) {
const formData = new FormData();
formData.append('token', localStorage.getItem('token'));
if (params) {
if (params.region) formData.append('region', params.region);
if (params.start_time) formData.append('start_time', params.start_time);
if (params.end_time) formData.append('end_time', params.end_time);
}
return request({
url: base_url + '/admin/user/login/trend',
method: 'post',
headers: {
'token': localStorage.getItem('token'),
'client': 'ios',
'version': '1',
'Content-Type': 'multipart/form-data'
},
data: formData
})
}
// 导出用户登录数据 PDF
export function exportUserLoginPDF(params) {
const formData = new FormData();
formData.append('token', localStorage.getItem('token'));
if (params) {
if (params.region) formData.append('region', params.region);
if (params.start_time) formData.append('start_time', params.start_time);
if (params.end_time) formData.append('end_time', params.end_time);
}
return request({
url: base_url + '/admin/user/login/export/pdf',
method: 'post',
headers: {
'token': localStorage.getItem('token'),
'client': 'ios',
'version': '1',
'Content-Type': 'multipart/form-data'
},
data: formData,
responseType: 'blob' // Important for file download
})
}
// 获取各地区登录活跃数据
export function getRegionActiveData(params) {
const formData = new FormData();
formData.append('token', localStorage.getItem('token'));
if (params) {
if (params.start_time) formData.append('start_time', params.start_time);
if (params.end_time) formData.append('end_time', params.end_time);
// identity: 0:全部 1:会员 2:非网 (可不传,默认全部)
if (params.identity !== undefined) formData.append('identity', params.identity);
}
return request({
url: base_url + '/admin/user/login/statistics/regionActiveData',
method: 'post',
headers: {
'token': localStorage.getItem('token'),
'client': 'ios',
'version': '1',
'Content-Type': 'multipart/form-data'
},
data: formData
})
}
// 获取各地区登录活跃柱状图数据
export function getRegionActiveDataHistogram(params) {
const formData = new FormData();
formData.append('token', localStorage.getItem('token'));
if (params) {
if (params.start_time) formData.append('start_time', params.start_time);
if (params.end_time) formData.append('end_time', params.end_time);
}
return request({
url: base_url + '/admin/user/login/statistics/regionActiveDataHistogram',
method: 'post',
headers: {
'token': localStorage.getItem('token'),
'client': 'ios',
'version': '1',
'Content-Type': 'multipart/form-data'
},
data: formData
})
}
// 获取各地区用户分布(饼图)
export function getRegionUserDistribution(params) {
const formData = new FormData();
formData.append('token', localStorage.getItem('token'));
if (params) {
if (params.start_time) formData.append('start_time', params.start_time);
if (params.end_time) formData.append('end_time', params.end_time);
// identity: 0:全部 1:会员 2:非网 (可不传,默认全部)
if (params.identity !== undefined) formData.append('identity', params.identity);
}
return request({
url: base_url + '/admin/user/login/statistics/regionUserDistribution',
method: 'post',
headers: {
'token': localStorage.getItem('token'),
'client': 'ios',
'version': '1',
'Content-Type': 'multipart/form-data'
},
data: formData
})
}
// 导出各地区登录活跃数据
export function exportRegionActiveData(params) {
const formData = new FormData();
formData.append('token', localStorage.getItem('token'));
if (params) {
if (params.start_time) formData.append('start_time', params.start_time);
if (params.end_time) formData.append('end_time', params.end_time);
}
return request({
url: base_url + '/admin/user/login/statistics/export',
method: 'post',
headers: {
'token': localStorage.getItem('token'),
'client': 'ios',
'version': '1',
'Content-Type': 'multipart/form-data'
},
data: formData,
responseType: 'blob' // 设置响应类型为二进制流
})
}
// 获取DeepChart用户活跃度趋势
export function getUserDeepChartTrend(params) {
const formData = new FormData();
formData.append('token', localStorage.getItem('token'));
if (params) {
if (params.startTime) formData.append('startTime', params.startTime);
if (params.endTime) formData.append('endTime', params.endTime);
if (params.mode) formData.append('mode', params.mode);
}
return request({
url: base_url + '/admin/user/deepchart/trend',
method: 'post',
headers: {
'token': localStorage.getItem('token'),
'client': 'ios',
'version': '1',
'Content-Type': 'multipart/form-data'
},
data: formData
})
}
// 获取DeepChart活跃用户明细
export function getDeepChartActiveUserList(params) {
const formData = new FormData();
formData.append('token', localStorage.getItem('token'));
if (params) {
if (params.page) formData.append('page', params.page);
if (params.page_size) formData.append('page_size', params.page_size);
if (params.jwcode) formData.append('jwcode', params.jwcode);
if (params.region && params.region !== 'all') formData.append('region', params.region);
if (params.startTime) formData.append('startTime', params.startTime);
if (params.endTime) formData.append('endTime', params.endTime);
if (params.isExport) formData.append('isExport', params.isExport);
}
return request({
url: base_url + '/admin/user/active/list',
method: 'post',
headers: {
'token': localStorage.getItem('token'),
'client': 'ios',
'version': '1',
'Content-Type': 'multipart/form-data'
},
data: formData
})
}
// 获取地区列表
export function getRegionsList() {
const formData = new FormData();
formData.append('token', localStorage.getItem('token'));
return request({
url: base_url + '/admin/regions/list',
method: 'post',
headers: {
'token': localStorage.getItem('token'),
'client': 'ios',
'version': '1',
'Content-Type': 'multipart/form-data'
},
data: formData
})
}
// 导出DeepChart活跃用户明细
export function exportDeepChartActiveUserList(params) {
const formData = new FormData();
formData.append('token', localStorage.getItem('token'));
if (params) {
if (params.jwcode) formData.append('jwcode', params.jwcode);
if (params.region && params.region !== 'all') formData.append('region', params.region);
if (params.startTime) formData.append('startTime', params.startTime);
if (params.endTime) formData.append('endTime', params.endTime);
}
return request({
url: base_url + '/admin/user/active/list/export',
method: 'post',
headers: {
'token': localStorage.getItem('token'),
'client': 'ios',
'version': '1',
'Content-Type': 'multipart/form-data'
},
data: formData
})
}
// 获取导出列表
export function getExportStatusList(params) {
const formData = new FormData();
formData.append('token', localStorage.getItem('token'));
if (params) {
if (params.page) formData.append('page', params.page);
if (params.page_size) formData.append('page_size', params.page_size);
}
return request({
url: base_url + '/admin/export/status/list',
method: 'post',
headers: {
'token': localStorage.getItem('token'),
'client': 'ios',
'version': '1',
'Content-Type': 'multipart/form-data'
},
data: formData
})
}

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'

472
src/layout/Layout.vue

@ -0,0 +1,472 @@
<!-- @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">
<!-- 第一层循环遍历父路由 (: 用户权限管理活动管理) -->
<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>
<!-- 第二层循环遍历子路由 -->
<template v-for="childRoute in parentRoute.filteredChildren" :key="childRoute.name">
<!-- 如果有孙子菜单新年幸运签渲染为子目录 el-sub-menu -->
<el-sub-menu v-if="childRoute.children && childRoute.children.length > 0" :index="`/${parentRoute.path}/${childRoute.path}`">
<template #title>
<el-icon class="sidebar-child-icon" />
<span class="sidebar-child-text">{{ childRoute.meta.title }}</span>
</template>
<!-- 第三层循环遍历孙子路由 (: 历史记录内容配置) -->
<el-menu-item v-for="grandChild in childRoute.children" :key="grandChild.name" :index="`/${parentRoute.path}/${childRoute.path}/${grandChild.path}`" class="sidebar-grandchild-container">
<span class="sidebar-grandchild-text">{{ grandChild.meta.title }}</span>
</el-menu-item>
</el-sub-menu>
<!-- 如果没有子菜单行情期限渲染为点击项 el-menu-item -->
<el-menu-item v-else :index="`/${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>
</template>
</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) => {
// children []
const rawChildren = parentRoute.children || [];
const filteredChildren = rawChildren
.map((childRoute) => {
// childRoute
const newChild = { ...childRoute };
if (newChild.children && newChild.children.length > 0) {
newChild.children = newChild.children.filter(
(grandChild) => grandChild.meta?.showSidebar === true
);
}
return newChild;
})
.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}`);
//
if (childRoute.children && childRoute.children.length > 0) {
childRoute.children.forEach((grandChild) => {
// ()
indexes.push(`/${parentRoute.path}/${childRoute.path}/${grandChild.path}`);
});
}
});
});
return indexes;
});
//
const lastActivePath = ref("");
// +
watch(
[() => route.path, validMenuIndexes],
([newPath, newIndexes]) => {
//
if (newIndexes.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;
height: 70%;
overflow-y: auto;
overflow-x: hidden;
}
/* 主内容区样式 */
.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-grandchild-text {
position: absolute;
left: 78px;
top: 15px;
height: 33px;
overflow: hidden;
color: #03071f;
text-overflow: ellipsis;
white-space: nowrap;
font-family: "PingFang SC", sans-serif;
font-size: 16px;
font-style: normal;
font-weight: 400;
line-height: 22px;
}
/* 子目录选中态样式 */
.sidebar-child-container.is-active,.sidebar-grandchild-container.is-active {
background: #ffffff !important; /* 选中后容器变为白色 */
}
/* 选中态文字样式 */
.sidebar-child-container.is-active .sidebar-child-text,.sidebar-grandchild-container.is-active .sidebar-grandchild-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 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()
})

192
src/router/index.js

@ -1,82 +1,174 @@
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
import Layout from '@/layout'
export const constantRoutes = [
{
path: '/login',
component: () => import('@/views/Login'),
hidden: true
},
{
path: '/404',
component: () => import('@/views/404'),
hidden: true
},
import { createRouter, createWebHistory } from 'vue-router'
import Layout from '../layout/Layout.vue'
const routes = [
{ {
path: '/', path: '/',
component: Layout,
redirect: '/userPermissions/market' redirect: '/userPermissions/market'
}, },
{ {
path: '/userPermissions',
path: '/login',
name: 'login',
component: () => import('../views/Login.vue'),
meta: {
title: '登录',
hiddenSidebar: true,
requireAuth: false // 登录页不需要登录
}
},
{
path: '',
component: Layout, component: Layout,
meta: { meta: {
title: '用户权限管理',
icon: 'user'
requireAuth: true // Layout下所有子路由都需要登录
}, },
alwaysShow: true,
children: [
{
path: 'userPermissions',
name: 'userPermissions',
meta: { title: '用户权限管理', icon: "UserFilled", showSidebar: true, isParentNav: true },
children: [ children: [
{ {
path: 'market', path: 'market',
name: 'market', name: 'market',
component: () => import('@/views/UserPermissions/Market.vue'),
meta: {
title: '行情期限'
}
component: () => import('../views/UserPermissions/Market.vue'),
meta: { title: '行情期限', showSidebar: true }
}, },
{ {
path: 'module', path: 'module',
name: 'module', name: 'module',
component: () => import('@/views/UserPermissions/Module.vue'),
meta: {
title: '模块期限'
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: 'platformData',
name: 'platformData',
meta: { title: '平台数据管理', icon: "TrendCharts", showSidebar: true, isParentNav: true },
children: [
{
path: 'overview',
name: 'overview',
component: () => import('../views/PlatformData/UserOverview.vue'),
meta: { title: '用户数据概览', showSidebar: true }
}, },
// DeepMate操作日志
{ {
path: 'LogDeepMate',
name: 'LogDeepMate',
component: () => import('@/views/UserPermissions/LogDeepMate.vue')
path: 'loginStats',
name: 'loginStats',
component: () => import('../views/PlatformData/UserLoginStats.vue'),
meta: { title: '用户登录统计', showSidebar: true }
}, },
// 深度探索操作日志
{ {
path: 'LogDeepExplore',
name: 'LogDeepExplore',
component: () => import('@/views/UserPermissions/LogDeepExplore.vue')
path: 'activityStats',
name: 'activityStats',
component: () => import('../views/PlatformData/UserActivityStats.vue'),
meta: { title: '用户活跃度统计', showSidebar: true }
} }
] ]
}, },
{ path: '*', redirect: '/404', hidden: true }
{
path: 'EventManagement',
name: 'EventManagement',
meta: { title: '活动管理', icon: "Management", showSidebar: true, isParentNav: true },
children: [
{
path: 'NewYearAct',
name: 'NewYearAct',
meta: { title: '新年幸运签', showSidebar: true },
children: [
{
path: 'winningRecords',
name: 'winningRecords',
component: () => import('../views/EventManagement/WinningRecords.vue'),
meta: { title: '历史记录', showSidebar: true }
},
{
path: 'contentConfiguration',
name: 'contentConfiguration',
component: () => import('../views/EventManagement/ContentConfiguration.vue'),
meta: { title: '内容配置', showSidebar: true }
},
{
path: 'exchangeRecords',
name: 'exchangeRecords',
component: () => import('../views/EventManagement/ExchangeRecords.vue'),
meta: { title: 'Token兑换记录', showSidebar: true }
},
{
path: 'learningPageConfig',
name: 'learningPageConfig',
component: () => import('../views/EventManagement/LearningPageConfig.vue'),
meta: { title: '学习页配置', showSidebar: 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
}

89
src/utils/myAxios.js

@ -0,0 +1,89 @@
// 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) => {
// 如果响应类型是 blob,直接返回整个 response.data
if (response.config.responseType === 'blob' || response.headers['content-type']?.includes('application/pdf')) {
return response.data;
}
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>

744
src/views/EventManagement/ContentConfiguration.vue

@ -0,0 +1,744 @@
<template>
<div class="page-container">
<div class="search-container">
<el-button type="danger" @click="add">添加</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="prize_type"
label="类型"
align="center"
header-align="center"
>
<template #default="scope">
{{ prizeTypeOptions.find(item => item.value === scope.row.prize_type)?.label }}
</template>
</el-table-column>
<el-table-column
prop="prize_name"
label="物品名称"
align="center"
header-align="center"
/>
<el-table-column
prop="stick_type"
label="福签"
align="center"
header-align="center"
>
<template #default="scope">
{{ typeOptions.find(item => item.value === scope.row.stick_type)?.label }}
</template>
</el-table-column>
<el-table-column
prop="probability"
label="概率"
align="center"
header-align="center"
>
<template #default="scope"> {{ scope.row.probability }}% </template>
</el-table-column>
<el-table-column label="状态" prop="status" align="center" header-align="center">
<template #default="scope">
<el-switch
v-model="scope.row.status"
:active-value="1"
:inactive-value="0"
inline-prompt
style="
--el-switch-on-color: #13ce66;
--el-switch-off-color: #ff4949;
"
active-text="ON"
inactive-text="OFF"
:before-change="() => beforeChangeState(scope.row)"
>
</el-switch>
</template>
</el-table-column>
<el-table-column
prop="time"
label="时间"
align="center"
header-align="center"
/>
<el-table-column label="操作" align="center" header-align="center">
<template #default="scope">
<el-button type="text" @click="deleteDraw(scope.row)">删除</el-button>
<el-button type="text" @click="handleEdit(scope.row)">编辑</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="dialogFormVisible" width="500" :show-close="false">
<el-form
:model="form"
style="width: 400px; margin: 0 auto"
:rules="rules"
ref="formRef"
>
<el-form-item label="类型" prop="prize_type">
<el-select v-model="form.prize_type" placeholder="请选择类型" clearable>
<el-option
v-for="item in prizeTypeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item :label="nameConfig.label" :prop="nameConfig.prop">
<el-select
v-if="form.prize_type === 5"
v-model="form.templateName"
placeholder="请选择模板"
clearable
:loading="isRegionLoading"
>
<el-option
v-for="item in templateList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
<el-input
v-else
v-model="form[nameConfig.prop]"
:type="nameConfig.type"
autocomplete="off"
:placeholder="nameConfig.placeholder"
clearable
/>
</el-form-item>
<el-form-item v-if="form.prize_type === 5" label="期限" prop="term_value">
<el-input
v-model.number="form.term_value"
placeholder="请输入模板期限"
clearable
>
<template #append>
<el-select
v-model="form.time_unit"
placeholder="单位"
style="width: 60px"
>
<el-option label="日" :value=3 />
<el-option label="月" :value=2 />
<el-option label="年" :value=1 />
</el-select>
</template>
</el-input>
</el-form-item>
<el-form-item label="概率" prop="probability">
<el-input
v-model.number="form.probability"
type="number"
autocomplete="off"
placeholder="请输入概率"
clearable
>
<template #append><span style="width: 20px;">%</span></template>
</el-input>
<div class="tip">(小于等于100%)</div>
</el-form-item>
<el-form-item label="福签" prop="stick_type">
<el-select
v-model="form.stick_type"
placeholder="请选择类型"
clearable
>
<el-option
v-for="item in typeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="图片" prop="img">
<el-upload
ref="uploadRef"
v-model:file-list="fileList"
class="avatar-uploader"
:action="uploadUrl"
:limit="1"
list-type="picture-card"
:on-success="handleSuccess"
:before-upload="beforeUpload"
:on-remove="handleRemove"
:on-exceed="handleExceed"
>
<el-icon><Plus /></el-icon>
</el-upload>
<div class="tip">
大小180*180像素支持PNGJPG格式图片需小于100K
</div>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="dialogFormVisible = false">取消</el-button>
<el-button type="danger" @click="submitForm(formRef)">
提交
</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, computed, watch, nextTick } from "vue";
import { ElMessage, genFileId, ElMessageBox } from "element-plus";
import router from "../../router";
import {
getContentListApi,
addDrawConfigApi,
deleteDrawApi,
changeStatusApi,
getTemplateApi,
} from "../../api/eventManagement";
const uploadUrl = import.meta.env.VITE_API_BASE_URLXXCG + "hljw/api/aws/upload";
const uploadRef = ref();
const fileList = ref([]);
const formRef = ref();
const isEdit = ref(false); //
const form = reactive({
id: undefined,
stick_type: "",
prize_type: "",
item_name: "",
num: null,
probability: null,
img: "",
templateName: "",
term_value: null,
time_unit: 3,
});
const isRegionLoading = ref(false);
const templateList = ref([]);
const typeOptions = ref([
{ label: "好运签", value: 1 },
{ label: "福气签", value: 2 },
{ label: "富贵签", value: 3 },
{ label: "财神签", value: 4 },
{ label: "上上签", value: 5 },
{ label: "锦鲤签", value: 6 },
]);
const prizeTypeOptions = ref([
{ label: "金币", value: 2 },
{ label: "金豆", value: 3 },
{ label: "Token", value: 1 },
{ label: "实物", value: 4 },
{ label: "模板", value: 5 },
]);
const nameConfig = computed(() => {
switch (form.prize_type) {
case 1: // Token
return { label: "数量", placeholder: "请输入Token数量", prop: "num", type: "number" };
case 2: //
return { label: "数量", placeholder: "请输入金币数量", prop: "num", type: "number" };
case 3: //
return { label: "数量", placeholder: "请输入金豆数量", prop: "num", type: "number" };
case 5: //
return { label: "名称", placeholder: "请输入模板名称", prop: "templateName", type: "text" };
default: //
return { label: "名称", placeholder: "请输入物品名称", prop: "item_name", type: "text" };
}
});
const handleSuccess = (response, uploadFile) => {
form.img = response.data.url;
};
const beforeUpload = (rawFile) => {
if (!rawFile.type.startsWith("image/")) {
ElMessage.error("请上传图片文件!");
return false;
} else if (rawFile.size / 1024 > 100) {
ElMessage.error("图片大小必须小于100K!");
return false;
}
return true;
};
const handleRemove = (file, fileList) => {
form.img = "";
};
const handleExceed = (files) => {
// 1.
uploadRef.value.clearFiles();
const file = files[0];
// 2. uid key
file.uid = genFileId();
// 3.
uploadRef.value.handleStart(file);
// 4.
uploadRef.value.submit();
};
// token
const token = localStorage.getItem("token");
const dialogFormVisible = ref(false);
const rules = computed(() => {
const baseRules = {
stick_type: [{ required: true, message: "请选择类型", trigger: "change" }],
prize_type: [{ required: true, message: "请选择类型", trigger: "change" }],
probability: [
{ required: true, message: "请输入概率", trigger: "blur" },
//
{ validator: validateNum, trigger: "blur" },
],
img: [{ required: true, message: "请上传图片", trigger: "change" }], // change
};
if ([1, 2, 3].includes(form.prize_type)) {
return {
...baseRules,
num: [
{ required: true, message: "请输入数量", trigger: "blur" },
//
{ validator: validateNum, trigger: "blur" },
],
};
} else if (form.prize_type === 5) {
return {
...baseRules,
templateName: [
{ required: true, message: "请选择模板", trigger: "change" },
],
term_value: [
{ required: true, message: "请输入期限", trigger: "blur" },
//
{ validator: validateTerm, trigger: "blur" },
],
};
} else {
return {
...baseRules,
item_name: [
{ required: true, message: "请输入物品名称", trigger: "blur" },
],
};
}
});
watch(
() => form.prize_type,
(newVal) => {
if (isEdit.value || !newVal) {
return;
}
//
form.item_name = "";
form.templateName = "";
form.num = null;
form.term_value = null;
form.time_unit = 3;
//
if (formRef.value) {
formRef.value.clearValidate(["item_name", "templateName", "num"]);
}
}
);
const validateNum = (rule, value, callback) => {
// required
if (value === "" || value === null || value === undefined) {
callback();
return;
}
//
if (Number(value) < 0) {
callback(new Error("不能为负数"));
} else {
callback();
}
};
const validateTerm = (rule, value, callback) => {
// required
if (value === "" || value === null || value === undefined) {
callback();
return;
}
//
if (!Number.isInteger(Number(value)) || Number(value) <= 0) {
return callback(new Error("必须为正整数"));
} else {
callback();
}
};
//
const tableData = ref([]);
const tableLoading = ref(false);
const datatotal = ref(0);
//
const currentPage = ref(1);
const pageSize = ref(10);
const beforeChangeState = (row) => {
return new Promise(async (resolve, reject) => {
try {
const targetStatus = row.status === 1 ? 0 : 1;
await changeStatusApi({
token: token,
id: row.id,
status: targetStatus,
});
ElMessage.success("状态更新成功");
resolve(true);
// fetchTableData();
} catch (error) {
// reject()switch
reject(new Error("状态更新失败"));
}
});
};
//
const resetForm = () => {
form.id = undefined;
form.stick_type = "";
form.prize_type = "";
form.item_name = "";
form.templateName = "";
form.term_value = null;
form.time_unit = 3;
form.num = null;
form.probability = null;
form.img = "";
fileList.value = [];
};
//
const add = () => {
isEdit.value = false;
resetForm();
dialogFormVisible.value = true;
//
/* nextTick dialogFormVisible = true <el-dialog>
但由于弹窗可能有动画或者内部组件是懒加载的 nextTick 执行的那一瞬间<el-form> 可能还没有真正渲染到 DOM
此时 formRef.value undefined所以 clearValidate() 其实根本没有执行成功*/
// nextTick(() => {
// formRef.value?.clearValidate();
// });
//setTimeout DOM formRef
setTimeout(() => {
if (formRef.value) {
formRef.value.clearValidate();
}
}, 0);
};
const submitForm = async () => {
try {
await formRef.value.validate();
const requestParams = {
token: token,
stick_type: form.stick_type,
prize_type: form.prize_type,
probability: form.probability,
img: form.img,
};
if (form.id) {
requestParams.id = form.id;
}
if ([1, 2, 3].includes(form.prize_type)) {
requestParams.num = Number(form.num);
} else if (form.prize_type === 5) {
requestParams.indicator_id = form.templateName;
requestParams.num = Number(form.term_value);
requestParams.time_unit = form.time_unit;
const selectedItem = templateList.value.find(item => item.id === form.templateName);
requestParams.item_name = selectedItem ? selectedItem.name : '';
} else {
requestParams.item_name = form.item_name;
}
const data = await addDrawConfigApi(requestParams);
ElMessage.success(form.id ? "修改成功" : "添加成功");
dialogFormVisible.value = false;
fetchTableData();
} catch (error) {}
};
const deleteDraw = async (row) => {
try {
await ElMessageBox.confirm("确定要删除吗?", "确认删除", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
confirmButtonClass: "custom-confirm-btn",
});
const requestParams = {
token: token,
id: row.id,
};
const data = await deleteDrawApi(requestParams);
ElMessage.success("删除成功");
fetchTableData();
} catch (error) {
if (error === "cancel") {
//
ElMessage.info("已取消删除");
} else {
//
ElMessage.error("删除失败");
}
}
};
const handleEdit = (row) => {
isEdit.value = true;
resetForm();
form.id = row.id;
form.stick_type = row.stick_type;
form.prize_type = row.prize_type; // watchnextTick
form.probability = row.probability;
form.img = row.img;
// ( Upload )
if (row.img) {
fileList.value = [{ name: 'img', url: row.img }];
}
//
if ([1, 2, 3].includes(row.prize_type)) {
form.num = row.num;
} else if (row.prize_type === 5) {
// ID
form.templateName = row.indicator_id;
form.term_value = row.num;
form.time_unit = row.time_unit;
} else {
//
form.item_name = row.item_name;
}
dialogFormVisible.value = true;
// 6. DOM
nextTick(() => {
isEdit.value = false; //
if (formRef.value) {
formRef.value.clearValidate(); //
}
});
};
//
const fetchTemplateList = async () => {
try {
isRegionLoading.value = true;
const data = await getTemplateApi({
token: token,
app_form: "en",
});
templateList.value = data.list;
} catch (error) {
console.error("获取模板列表失败:", error);
templateList.value = [];
} finally {
isRegionLoading.value = false;
}
};
//
const fetchTableData = async () => {
try {
tableLoading.value = true;
const requestParams = {
token: token,
page: currentPage.value,
page_size: pageSize.value,
};
const data = await getContentListApi(requestParams);
tableData.value = data.list;
datatotal.value = data.total;
} catch (error) {
console.error("获取表格数据失败:", error);
tableData.value = [];
datatotal.value = 0;
} finally {
tableLoading.value = false;
}
};
// +
onMounted(() => {
fetchTemplateList();
fetchTableData();
});
//
const handleSizeChange = (val) => {
pageSize.value = val;
fetchTableData();
console.log(`每页 ${val}`);
};
const handleCurrentChange = (val) => {
currentPage.value = val;
fetchTableData();
console.log(`当前页: ${val}`);
};
</script>
<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;
}
/* 表格样式 */
.table-rounded {
border-radius: 12px !important;
overflow: hidden !important;
border: 1px solid #e4e7ed !important;
height: 750px;
}
.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;
}
.tip {
font-size: 12px;
color: #8c939d;
}
.avatar-uploader .avatar {
width: 120px;
height: 120px;
display: block;
}
</style>
<style>
.custom-confirm-btn {
background: #e13d52;
border-color: #e13d52;
color: white !important;
border-radius: 6px !important;
padding: 8px 16px !important;
}
.custom-confirm-btn:hover {
background: #d88b95;
border-color: #d88b95;
}
.avatar-uploader .el-upload {
border: 1px dashed var(--el-border-color);
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
transition: var(--el-transition-duration-fast);
}
.avatar-uploader .el-upload:hover {
border-color: var(--el-color-primary);
}
.el-icon.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 120px;
height: 120px;
text-align: center;
}
</style>

524
src/views/EventManagement/ExchangeRecords.vue

@ -0,0 +1,524 @@
<!-- @format -->
<template>
<div>
<div class="page-container">
<!-- 搜索区域 -->
<div class="search-container" @keyup.enter="search">
<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.name"
placeholder="请输入姓名"
clearable
style="height: 36px; width: 140px"
/>
</div>
<div class="search-item">
<span class="form-label">归属</span>
<el-input
v-model="searchForm.inviter"
placeholder="请输入归属账号"
clearable
style="height: 36px; width: 140px"
/>
</div>
</div>
<div class="search-group2">
<div class="search-item">
<span class="form-label">兑换方式</span>
<el-select
v-model="searchForm.type"
placeholder="请选择兑换方式"
clearable
style="height: 36px; width: 160px"
>
<el-option
v-for="item in exchangeTypeList"
:key="item.gold_id"
:label="item.gold_type"
:value="item.gold_id"
/>
</el-select>
</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"
style="height: 36px; width: 280px"
/>
</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>
<!-- 数据 -->
<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="dccode"
label="账号"
align="center"
header-align="center"
/>
<el-table-column
prop="name"
label="姓名"
align="center"
header-align="center"
/>
<el-table-column
prop="inviter"
label="归属"
align="center"
header-align="center"
/>
<el-table-column
prop="type"
label="兑换方式"
align="center"
header-align="center"
/>
<el-table-column
prop="token_num"
label="兑换数量"
align="center"
header-align="center"
>
<template #default="scope"> {{ scope.row.token_num }}Token </template>
</el-table-column>
<el-table-column
prop="created_at"
label="兑换时间"
align="center"
header-align="center"
/>
</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="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 {
getTokenExchangeRecordApi,
exportTokenExchangeRecordApi,
getDropDownListApi,
} from "../../api/eventManagement";
import router from "../../router";
import { useRoute } from "vue-router";
import { usePermissionStore } from "../../store/permission";
const permissionStore = usePermissionStore();
//
const exchangeTypeList = ref([]);
// token
const token = localStorage.getItem("token");
//permission
const permission = ref("-1");
//
const route = useRoute();
// +
onMounted(async () => {
permission.value = permissionStore.permission;
fetchExchangeTypeList();
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 searchForm = reactive({
dccode: "",
name: "",
inviter: "",
type: "",
});
const searchDate = ref([]);
//
const tableData = ref([]);
const tableLoading = ref(false);
const datatotalDM = ref(0);
//
const currentPage = ref(1);
const pageSizeDM = ref(10);
//
const fetchExchangeTypeList = async () => {
try {
const data = await getDropDownListApi({
token: token
});
exchangeTypeList.value = data;
} catch (error) {
console.error("获取兑换方式列表失败:", error);
exchangeTypeList.value = [];
}
};
//
const getTableData = async () => {
try {
tableLoading.value = true;
const requestParams = {
token: token,
dccode: searchForm.dccode,
name: searchForm.name,
inviter: searchForm.inviter,
type: Number(searchForm.type),
start_at:
searchDate.value && searchDate.value[0] ? searchDate.value[0] : "",
end_at:
searchDate.value && searchDate.value[1] ? searchDate.value[1] : "",
page: currentPage.value,
page_size: pageSizeDM.value,
};
const data = await getTokenExchangeRecordApi(requestParams);
tableData.value = data.list;
datatotalDM.value = data.total;
} catch (error) {
console.error("获取表格数据失败:", error);
tableData.value = [];
datatotalDM.value = 0;
} finally {
tableLoading.value = false;
}
};
//
const handleSizeChange = (val) => {
pageSizeDM.value = val;
getTableData();
console.log(`每页 ${val}`);
};
const handleCurrentChange = (val) => {
currentPage.value = val;
getTableData();
console.log(`当前页: ${val}`);
};
//
const search = () => {
currentPage.value = 1;
getTableData();
};
//
const resetBn = () => {
searchForm.dccode = "";
searchForm.name = "";
searchForm.inviter = "";
searchForm.type = "";
searchDate.value = [];
currentPage.value = 1;
pageSizeDM.value = 10;
getTableData();
};
// Excel
const exportExcel = async () => {
const requestParams = {
token: token,
dccode: searchForm.dccode,
name: searchForm.name,
inviter: searchForm.inviter,
type: Number(searchForm.type),
start_at:
searchDate.value && searchDate.value[0] ? searchDate.value[0] : "",
end_at:
searchDate.value && searchDate.value[1] ? searchDate.value[1] : "",
};
const data = await exportTokenExchangeRecordApi(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;
};
</script>
<style scoped>
/* 父容器 */
.page-container {
position: relative;
min-height: 700px;
}
/* 搜索区域 */
.search-container {
display: flex;
height: auto;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
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-group {
display: flex;
flex-direction: column;
flex: 1;
}
.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: 13px 0;
font-family: "SimHei", "Heiti SC", "Microsoft YaHei", sans-serif !important;
display: block;
}
/* 按钮组 */
.button-group {
display: flex;
align-items: center;
gap: 0px !important;
margin-left: auto;
flex-shrink: 0;
}
/* 按钮样式 */
.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-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;
}
/* 文本溢出省略样式(深度探索) */
.ellipsis-text {
display: inline-block;
width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: middle;
}
: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;
}
.label {
color: #666;
font-weight: 500;
}
.value {
color: #333;
}
</style>

385
src/views/EventManagement/LearningPageConfig.vue

@ -0,0 +1,385 @@
<template>
<div class="page-container">
<div class="search-container">
<el-button type="danger" @click="addArticle">添加关联文章</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="article_id"
label="文章id"
align="center"
header-align="center"
/>
<el-table-column
prop="article_title"
label="关联文章"
align="center"
header-align="center"
/>
<el-table-column label="状态" prop="status" align="center" header-align="center">
<template #default="scope">
<el-switch
v-model="scope.row.status"
:active-value="1"
:inactive-value="0"
inline-prompt
style="
--el-switch-on-color: #13ce66;
--el-switch-off-color: #ff4949;
"
active-text="ON"
inactive-text="OFF"
:disabled="activeRecordId && activeRecordId !== scope.row.id"
:before-change="() => beforeChangeState(scope.row)"
>
</el-switch>
</template>
</el-table-column>
<el-table-column label="操作" align="center" header-align="center">
<template #default="scope">
<el-button type="text" @click="deleteArticle(scope.row)">删除</el-button>
<el-button type="text" @click="editArticle(scope.row)">编辑</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="dialogFormVisible" width="500" :show-close="false" :title="isEditMode ? '更改关联文章' : '添加关联文章'">
<el-form
:model="form"
style="width: 400px; margin: 0 auto"
:rules="rules"
ref="formRef"
>
<el-form-item label="文章id" prop="article_id">
<el-input
v-model="form.article_id"
type="number"
autocomplete="off"
placeholder="请输入文章id"
clearable
/>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="dialogFormVisible = false">取消</el-button>
<el-button type="danger" @click="submitForm(formRef)">
提交
</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, computed, watch } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import router from "../../router";
import {
getLearningPageListApi,
addLearningPageApi,
deleteLearningPageApi,
changeLearningPageStatusApi,
} from "../../api/eventManagement";
const formRef = ref();
const form = reactive({
id: null,
article_id: null,
});
const validateNum = (rule, value, callback) => {
if (value === "" || value === null || value === undefined) {
callback();
return;
}
if (Number(value) < 0) {
callback(new Error("不能为负数"));
} else {
callback();
}
};
const rules = {
article_id: [
{ required: true, message: "请输入文章id", trigger: "blur" },
{ validator: validateNum, trigger: "blur" },
],
};
// token
const token = localStorage.getItem("token");
const dialogFormVisible = ref(false);
const isEditMode = ref(false);
//
const tableData = ref([]);
const tableLoading = ref(false);
const datatotal = ref(0);
const activeRecordId = ref(null);
//
const currentPage = ref(1);
const pageSize = ref(10);
const beforeChangeState = (row) => {
return new Promise(async (resolve, reject) => {
try {
const targetStatus = row.status === 1 ? 0 : 1;
// on
if (targetStatus === 1 && activeRecordId.value && activeRecordId.value !== row.id) {
ElMessage.error("已有状态为ON的记录,无法修改其他记录的状态");
reject(new Error("已有状态为ON的记录"));
return;
}
await changeLearningPageStatusApi({
token: token,
id: row.id,
status: targetStatus,
});
ElMessage.success("状态更新成功");
resolve(true);
fetchTableData();
} catch (error) {
reject(new Error("状态更新失败"));
}
});
};
//
const resetForm = () => {
form.id = null;
form.article_id = null;
};
//
const addArticle = () => {
resetForm();
isEditMode.value = false;
dialogFormVisible.value = true;
};
//
const editArticle = (row) => {
form.id = row.id;
form.article_id = row.article_id;
isEditMode.value = true;
dialogFormVisible.value = true;
};
const submitForm = async () => {
try {
await formRef.value.validate();
const requestParams = {
token: token,
article_id: form.article_id,
};
if (isEditMode.value) {
requestParams.id = form.id;
await addLearningPageApi(requestParams);
ElMessage.success("编辑成功");
} else {
await addLearningPageApi(requestParams);
ElMessage.success("添加成功");
}
dialogFormVisible.value = false;
fetchTableData();
} catch (error) {}
};
const deleteArticle = async (row) => {
try {
await ElMessageBox.confirm("确定要删除吗?", "确认删除", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
confirmButtonClass: "custom-confirm-btn",
});
const requestParams = {
token: token,
id: row.id,
};
const data = await deleteLearningPageApi(requestParams);
ElMessage.success("删除成功");
fetchTableData();
} catch (error) {
if (error === "cancel") {
ElMessage.info("已取消删除");
} else {
ElMessage.error("删除失败");
}
}
};
//
const fetchTableData = async () => {
try {
tableLoading.value = true;
const requestParams = {
token: token,
page: currentPage.value,
page_size: pageSize.value,
};
const data = await getLearningPageListApi(requestParams);
tableData.value = data.list;
datatotal.value = data.total;
// on
const activeRecord = data.list.find(item => item.status === 1);
activeRecordId.value = activeRecord ? activeRecord.id : null;
} catch (error) {
console.error("获取表格数据失败:", error);
tableData.value = [];
datatotal.value = 0;
activeRecordId.value = null;
} finally {
tableLoading.value = false;
}
};
// +
onMounted(() => {
fetchTableData();
});
//
const handleSizeChange = (val) => {
pageSize.value = val;
fetchTableData();
console.log(`每页 ${val}`);
};
const handleCurrentChange = (val) => {
currentPage.value = val;
fetchTableData();
console.log(`当前页: ${val}`);
};
const handleSortChange = (val) => {
console.log("排序改变:", val);
};
</script>
<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;
}
/* 表格样式 */
.table-rounded {
border-radius: 12px !important;
overflow: hidden !important;
border: 1px solid #e4e7ed !important;
height: 750px;
}
.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;
}
.tip {
font-size: 12px;
color: #8c939d;
}
</style>
<style>
.custom-confirm-btn {
background: #e13d52;
border-color: #e13d52;
color: white !important;
border-radius: 6px !important;
padding: 8px 16px !important;
}
.custom-confirm-btn:hover {
background: #d88b95;
border-color: #d88b95;
}
</style>

427
src/views/EventManagement/WinningRecords.vue

@ -0,0 +1,427 @@
<template>
<div class="page-container">
<!-- 搜索区域 -->
<div class="search-container">
<div class="search-group">
<span class="form-label">账号</span>
<el-input
v-model="searchForm.dccode"
placeholder="请输入账号"
clearable
style="height: 36px; width: 140px"
/>
<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>
<span class="form-label">物品类型</span>
<el-select
v-model="searchForm.prize_type"
placeholder="请选择类型"
style="height: 36px; width: 160px"
clearable
>
<el-option
v-for="item in typeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</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>
<!-- 数据 -->
<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="market"
label="地区"
align="center"
header-align="center"
/>
<el-table-column
prop="prize_name"
label="物品名称"
align="center"
header-align="center"
/>
<el-table-column
prop="stick_type"
label="福签"
align="center"
header-align="center"
/>
<el-table-column
prop="num"
label="数量"
align="center"
header-align="center"
/>
<el-table-column
prop="created_at"
label="时间"
align="center"
header-align="center"
/>
</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, reactive, onMounted } from "vue";
import { ElMessage } from "element-plus";
import { marketListApi } from "../../api/userPermissions";
import { userLuckyDrawListApi, exportUserLuckyDrawListApi } from "../../api/eventManagement";
import router from "../../router";
// token
const token = localStorage.getItem("token");
const typeOptions = ref([
{ label: "金币", value: 2 },
{ label: "金豆", value: 3 },
{ label: "Token", value: 1 },
{ label: "实物", value: 4 },
{ label: "模板", value: 5 },
]);
//
const searchForm = reactive({
dccode: "",
country: "",
prize_type: "",
});
//
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);
// 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 resetForm = () => {
hlidsInput.value = "";
timeType.value = "";
expireTime.value = "";
delayValue.value = "";
delayUnit.value = "";
remark.value = "";
operator.value = "";
};
//
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,
country: searchForm.country,
prize_type: searchForm.prize_type,
page: currentPage.value,
page_size: pageSize.value,
};
const data = await userLuckyDrawListApi(requestParams);
tableData.value = data.list;
datatotal.value = data.total;
} catch (error) {
console.error("获取表格数据失败:", error);
tableData.value = [];
datatotal.value = 0;
} finally {
tableLoading.value = false;
}
};
// +
onMounted(() => {
fetchRegionList();
fetchTableData();
});
//
const search = () => {
currentPage.value = 1;
fetchTableData();
};
// Excel
const exportExcel = async () => {
const requestParams = {
token: token,
dccode: searchForm.dccode,
country: searchForm.country,
prize_type: searchForm.prize_type,
};
const data = await exportUserLuckyDrawListApi(requestParams);
console.log(data);
if (data != "") {
ElMessage.success("已导出");
}
};
//
const exportList = () => {
router.push({
path: '/userPermissions/export'
});
};
//
const resetBn = () => {
searchForm.dccode = "";
searchForm.country = "";
searchForm.prize_type = "";
currentPage.value = 1;
pageSize.value = 10;
fetchTableData();
};
//
const handleSizeChange = (val) => {
pageSize.value = val;
fetchTableData();
console.log(`每页 ${val}`);
};
const handleCurrentChange = (val) => {
currentPage.value = val;
fetchTableData();
console.log(`当前页: ${val}`);
};
</script>
<style scoped>
/* 父容器 */
.page-container {
position: relative;
min-height: 600px;
}
/* 搜索区域 */
.search-container {
display: flex;
height: auto;
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;
flex-wrap: wrap;
}
.search-group {
display: flex;
align-items: center;
gap: 15px;
}
/* 搜索标签文字 */
.form-label {
font-weight: 800 !important;
font-size: 15px;
color: #333;
font-family: "SimHei", "Heiti SC", "Microsoft YaHei", sans-serif !important;
font-weight: 500;
}
/* 按钮组 */
.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: 750px;
}
.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;
}
</style>

392
src/views/Login.vue

@ -1,232 +1,222 @@
<template> <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"
/>
<!-- 背景容器 -->
<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-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-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-item>
<el-button :loading="loading" type="primary" style="width:100%;margin-bottom:30px;" @click.native.prevent="handleLogin">登录</el-button>
</el-form> </el-form>
</div> </div>
</div>
</template> </template>
<script>
import { validUsername } from '@/utils/validate'
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'))
<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;
}
//
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 { } 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
localStorage.removeItem('deepchart_login');
} }
},
methods: {
showPwd() {
if (this.passwordType === 'password') {
this.passwordType = ''
} else {
this.passwordType = 'password'
}
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
})
//
router.push('/');
} else { } else {
console.log('error submit!!')
return false
}
})
errorMsg.value = '账号或密码有误,请重新输入';
} }
} catch (error) {
errorMsg.value = '登录失败,请稍后重试';
console.error('登录请求异常:', error);
} }
}
};
</script> </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;
.el-input__inner {
padding: 0 16px;
font-size: 16px;
}
.login-form {
position: relative;
width: 520px;
max-width: 100%;
padding: 160px 35px 0;
margin: 0 auto;
overflow: hidden;
}
.el-input__placeholder {
font-size: 16px !important;
}
.tips {
font-size: 14px;
color: #fff;
margin-bottom: 10px;
/* 记住密码 */
.remember {
text-align: left;
margin-bottom: 32px;
font-size: 15px;
color: #4b5563;
}
span {
&:first-of-type {
margin-right: 16px;
}
}
}
.el-checkbox__label {
font-size: 15px !important;
margin-left: 8px !important;
}
.svg-container {
padding: 6px 5px 6px 15px;
color: $dark_gray;
vertical-align: middle;
width: 30px;
display: inline-block;
}
/* 登录按钮 */
.el-button--primary {
height: 52px;
font-size: 18px;
border-radius: 4px;
background-color: #1890ff;
border: none;
}
.title-container {
position: relative;
.el-button--primary:hover {
background-color: #096dd9;
}
.title {
font-size: 26px;
color: $light_gray;
margin: 0px auto 40px auto;
text-align: center;
font-weight: bold;
}
}
/* 图标大小适配 */
.el-input__prefix {
font-size: 18px !important;
}
.show-pwd {
position: absolute;
right: 10px;
top: 7px;
font-size: 16px;
color: $dark_gray;
cursor: pointer;
user-select: none;
}
/* 错误提示 */
.error-tip {
color: #ff4d4f;
font-size: 14px;
font-weight: 700;
text-align: left;
line-height: 1.5;
overflow: hidden;
}
.error-placeholder {
visibility: hidden;
} }
.fixed-height {
height: 21px;
margin-bottom: 6px;
}
</style> </style>

622
src/views/PlatformData/UserActivityStats.vue

@ -0,0 +1,622 @@
<template>
<div class="user-activity-stats-container">
<!-- 用户活跃度趋势图 -->
<div class="content-card">
<div class="card-header">
<el-icon class="header-icon"><TrendCharts /></el-icon>
<span class="header-title">用户活跃度趋势图</span>
</div>
<div class="card-filter-row">
<div class="filter-left">
<span class="filter-label">时间段查询</span>
<el-date-picker
v-model="chartDateRange"
type="daterange"
range-separator="至"
start-placeholder="开始时间"
end-placeholder="结束时间"
size="default"
@change="handleChartDateChange"
/>
</div>
<div class="filter-right">
<el-button-group>
<el-button :type="chartMode === 'day' ? 'danger' : 'default'" @click="handleModeChange('day')">每日</el-button>
<!-- <el-button :type="chartMode === 'week' ? 'danger' : 'default'" @click="handleModeChange('week')">近七日</el-button> -->
<el-button :type="chartMode === 'month' ? 'danger' : 'default'" @click="handleModeChange('month')">近三十日</el-button>
</el-button-group>
</div>
</div>
<div class="chart-container" ref="chartRef"></div>
</div>
<!-- DeepChart活跃用户明细 -->
<div class="content-card">
<div class="card-header">
<el-icon class="header-icon"><DataLine /></el-icon>
<span class="header-title">DeepChart活跃用户明细</span>
</div>
<div class="table-filter-row">
<el-input v-model="tableAccount" placeholder="请输入账号" style="width: 150px" />
<!-- <el-select v-model="tableRegion" placeholder="请选择所属地区" style="width: 150px">
<el-option label="全部" value="all" />
<el-option
v-for="item in regionOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select> -->
<el-date-picker
v-model="tableDateRange"
type="daterange"
range-separator="至"
start-placeholder="开始时间"
end-placeholder="结束时间"
size="default"
style="width: 240px"
/>
<el-button type="primary" class="search-btn-small" @click="handleTableSearch">搜索</el-button>
<el-button type="primary" class="reset-btn-small" @click="handleTableReset">重置</el-button>
<el-button type="danger" class="export-btn-small" @click="handleExport">数据导出</el-button>
<el-button type="danger" class="export-list-btn" @click="handleShowExportList">查看导出列表</el-button>
</div>
<el-table :data="tableData" style="width: 100%" :header-cell-style="headerCellStyle">
<el-table-column prop="index" label="序号" width="80" align="center" />
<el-table-column prop="account" label="账号" min-width="120" />
<el-table-column prop="name" label="姓名" min-width="120" />
<el-table-column prop="region" label="地区" width="100" />
<el-table-column prop="loginCount" label="登录次数" width="100" align="center" />
<!-- <el-table-column prop="totalDuration" label="总停留时长" min-width="150" align="center" />
<el-table-column prop="avgDuration" label="平均停留时长" min-width="150" align="center" /> -->
<el-table-column prop="deepMate" label="DeepMate" min-width="100" align="center" />
<el-table-column prop="deepExplore" label="深度探索" min-width="100" align="center" />
<el-table-column prop="marketInfo" label="行情" min-width="100" align="center" />
<el-table-column prop="updateTime" label="更新日期" min-width="150" align="center" />
</el-table>
<div class="pagination-container">
<div class="total-count">{{ total }}</div>
<el-pagination
background
layout="sizes, prev, pager, next, jumper"
:total="total"
:page-sizes="[10, 20, 50, 100]"
v-model:current-page="currentPage"
v-model:page-size="pageSize"
@current-change="handlePageChange"
@size-change="handleSizeChange"
/>
</div>
</div>
<!-- 导出列表弹窗 -->
<el-dialog
v-model="exportDialogVisible"
title="导出记录"
width="800px"
:close-on-click-modal="false"
>
<el-table :data="exportList" style="width: 100%" border v-loading="exportListLoading">
<el-table-column prop="index" label="序号" width="80" align="center" />
<el-table-column prop="type" label="导出类型" width="150" align="center" />
<el-table-column prop="created_at" label="导出时间" width="180" align="center" />
<el-table-column prop="status" label="执行状态" width="100" align="center">
<template #default="scope">
<el-tag v-if="scope.row.status === 2" type="success">成功</el-tag>
<el-tag v-else-if="scope.row.status === 1" type="warning">执行中</el-tag>
<el-tag v-else type="danger">失败</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" min-width="120" align="center">
<template #default="scope">
<el-button
v-if="scope.row.status === 2 && scope.row.download_url"
type="primary"
link
@click="handleDownload(scope.row.download_url)"
>
下载
</el-button>
<span v-else>-</span>
</template>
</el-table-column>
</el-table>
<div class="pagination-container" style="margin-top: 15px;">
<el-pagination
background
layout="prev, pager, next"
:total="exportTotal"
:page-size="exportPageSize"
v-model:current-page="exportCurrentPage"
@current-change="handleExportPageChange"
/>
</div>
</el-dialog>
<!-- 悬浮刷新时间 -->
<div class="refresh-time" v-if="lastUpdateTime">
数据刷新时间{{ lastUpdateTime }}
</div>
</div>
</template>
<script setup>
import { ref, onMounted, nextTick, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import * as echarts from 'echarts';
import { ElMessage } from 'element-plus';
import { getUserDeepChartTrend, getDeepChartActiveUserList, getRegionsList, exportDeepChartActiveUserList, getExportStatusList } from '../../api/platformData';
const route = useRoute();
const router = useRouter();
//
const chartDateRange = ref('');
const chartMode = ref(route.query.mode || 'day'); // day, week, month
const chartRef = ref(null);
let chartInstance = null;
const lastUpdateTime = ref('');
// URL
const initQueryParams = () => {
const { mode, startTime, endTime } = route.query;
if (mode) {
chartMode.value = mode;
}
if (startTime && endTime) {
chartDateRange.value = [new Date(startTime), new Date(endTime)];
}
};
initQueryParams();
const updateUrlParams = () => {
const query = { ...route.query };
query.mode = chartMode.value;
if (chartDateRange.value && chartDateRange.value.length === 2) {
query.startTime = formatDate(chartDateRange.value[0]);
query.endTime = formatDate(chartDateRange.value[1]);
} else {
delete query.startTime;
delete query.endTime;
}
router.replace({ query });
};
const handleChartDateChange = () => {
updateUrlParams();
fetchChartData();
};
const handleModeChange = (mode) => {
chartMode.value = mode;
updateUrlParams();
fetchChartData();
};
//
const formatDate = (date) => {
if (!date) return '';
const d = new Date(date);
const pad = (n) => n < 10 ? '0' + n : n;
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
};
const formatDateTime = (date) => {
if (!date) return '';
const d = new Date(date);
const pad = (n) => n < 10 ? '0' + n : n;
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
};
const fetchChartData = async () => {
const params = {
mode: chartMode.value
};
if (chartDateRange.value && chartDateRange.value.length === 2) {
params.startTime = formatDate(chartDateRange.value[0]);
params.endTime = formatDate(chartDateRange.value[1]);
}
try {
const res = await getUserDeepChartTrend(params);
console.log("获取DeepChart趋势数据响应:", res);
const data = res.data || res; //
if (Array.isArray(data)) {
updateChart(data);
}
lastUpdateTime.value = formatDateTime(new Date());
} catch (e) {
console.error('获取DeepChart趋势数据失败:', e);
}
};
const updateChart = (data) => {
if (!chartRef.value) return;
if (!chartInstance) {
chartInstance = echarts.init(chartRef.value);
}
const dates = data.map(item => item.time_point);
const activeUsers = data.map(item => item.active_users);
const useCounts = data.map(item => item.use_count);
const option = {
tooltip: {
trigger: 'axis',
axisPointer: { type: 'cross' }
},
legend: {
data: ['DeepChart活跃人数', 'DeepChart使用次数'],
top: 'top',
left: 'center',
itemWidth: 35,
itemHeight: 18,
textStyle: {
fontSize: 14
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
boundaryGap: false,
data: dates
},
yAxis: {
type: 'value',
min: 0,
axisLabel: { formatter: '{value}' }
},
series: [
{
name: 'DeepChart活跃人数',
type: 'line',
data: activeUsers,
itemStyle: { color: '#e74c3c' },
symbol: 'circle',
symbolSize: 8,
lineStyle: { width: 3 }
},
{
name: 'DeepChart使用次数',
type: 'line',
data: useCounts,
itemStyle: { color: '#2ecc71' },
symbol: 'circle',
symbolSize: 8,
lineStyle: { width: 3 }
}
]
};
chartInstance.setOption(option);
};
const tableAccount = ref('');
const tableRegion = ref('');
const tableDateRange = ref('');
const currentPage = ref(1);
const pageSize = ref(10);
const total = ref(0);
const regionOptions = ref([]);
//
const exportDialogVisible = ref(false);
const exportList = ref([]);
const exportListLoading = ref(false);
const exportCurrentPage = ref(1);
const exportPageSize = ref(10);
const exportTotal = ref(0);
const handleShowExportList = () => {
exportDialogVisible.value = true;
exportCurrentPage.value = 1;
fetchExportList();
};
const handleExportPageChange = (page) => {
exportCurrentPage.value = page;
fetchExportList();
};
const fetchExportList = async () => {
exportListLoading.value = true;
try {
const params = {
page: exportCurrentPage.value,
page_size: exportPageSize.value
};
const res = await getExportStatusList(params);
console.log("获取导出列表响应:", res);
const data = res.data || res;
if (data && data.list) {
exportList.value = data.list.map((item, index) => ({
...item,
index: (exportCurrentPage.value - 1) * exportPageSize.value + index + 1
}));
exportTotal.value = data.total || 0;
} else {
exportList.value = [];
exportTotal.value = 0;
}
} catch (e) {
console.error('获取导出列表失败:', e);
ElMessage.error('获取导出列表失败');
} finally {
exportListLoading.value = false;
}
};
const handleDownload = (url) => {
window.open(url.trim(), '_blank');
};
//
const fetchRegionOptions = async () => {
try {
const res = await getRegionsList();
console.log("获取地区列表响应:", res);
const data = res.data || res;
if (data && data.list) {
regionOptions.value = data.list.map(region => ({ label: region, value: region }));
}
} catch (e) {
console.error('获取地区列表失败:', e);
}
};
//
const tableData = ref([]);
const handleTableSearch = () => {
currentPage.value = 1;
fetchTableData();
};
const handleTableReset = () => {
tableAccount.value = '';
tableRegion.value = '';
tableDateRange.value = '';
currentPage.value = 1;
fetchTableData();
};
const handlePageChange = (page) => {
currentPage.value = page;
fetchTableData();
};
const handleSizeChange = (size) => {
pageSize.value = size;
currentPage.value = 1;
fetchTableData();
};
const handleExport = async () => {
const params = {};
if (tableAccount.value) params.jwcode = tableAccount.value;
if (tableRegion.value && tableRegion.value !== 'all') params.region = tableRegion.value;
if (tableDateRange.value && tableDateRange.value.length === 2) {
params.startTime = formatDate(tableDateRange.value[0]);
params.endTime = formatDate(tableDateRange.value[1]);
}
try {
const res = await exportDeepChartActiveUserList(params);
console.log("导出DeepChart活跃用户明细响应:", res);
// ID ()
// res datares.code
const isSuccess = (typeof res === 'number' && res > 0) ||
(res && res.code === 200) ||
(res && typeof res.data === 'number' && res.data > 0);
if (isSuccess) {
ElMessage.success('导出任务已添加到队列,请稍后在“查看导出列表”中查看结果。');
} else {
ElMessage.error((res && res.msg) || '导出请求失败');
}
} catch (e) {
console.error('导出DeepChart活跃用户明细失败:', e);
ElMessage.error('导出请求发生错误');
}
};
const fetchTableData = async () => {
const params = {
page: currentPage.value,
page_size: pageSize.value
};
if (tableAccount.value) params.jwcode = tableAccount.value;
if (tableRegion.value && tableRegion.value !== 'all') params.region = tableRegion.value;
if (tableDateRange.value && tableDateRange.value.length === 2) {
params.startTime = formatDate(tableDateRange.value[0]);
params.endTime = formatDate(tableDateRange.value[1]);
}
try {
const res = await getDeepChartActiveUserList(params);
console.log("获取DeepChart活跃用户明细响应:", res);
const data = res.data || res; //
if (data && data.list) {
tableData.value = data.list.map((item, index) => ({
index: (currentPage.value - 1) * pageSize.value + index + 1,
account: item.jwcode,
name: item.username,
region: item.region,
loginCount: item.login_count,
totalDuration: formatDuration(item.stay_time),
avgDuration: formatDuration(item.avg_stay_time),
deepMate: item.deepmate_count,
deepExplore: item.dive_seek_count,
marketInfo: item.market_info_count, //
updateTime: item.update_time // 使 update_time
}));
total.value = data.total || 0;
} else {
tableData.value = [];
total.value = 0;
}
lastUpdateTime.value = formatDateTime(new Date());
} catch (e) {
console.error('获取DeepChart活跃用户明细失败:', e);
}
};
// ( -> )
const formatDuration = (seconds) => {
if (!seconds) return '0秒';
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = seconds % 60;
let str = '';
if (h > 0) str += `${h}小时`;
if (m > 0) str += `${m}分钟`;
if (s > 0) str += `${s}`;
return str || '0秒';
};
const headerCellStyle = {
background: '#f5f7fa',
color: '#333',
fontWeight: 'bold'
};
const initChart = () => {
if (chartRef.value) {
//
if (!chartInstance) {
chartInstance = echarts.init(chartRef.value);
}
//
fetchChartData();
}
};
onMounted(() => {
nextTick(() => {
initChart();
fetchTableData();
fetchRegionOptions();
});
});
</script>
<style scoped>
.user-activity-stats-container {
padding: 20px;
background-color: #fee6e6;
min-height: calc(100vh - 40px);
}
/* Content Cards */
.content-card {
background: #fff;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
border: 1px solid #f0f0f0;
}
.card-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 20px;
}
.header-icon {
color: #409eff;
font-size: 20px;
}
.header-title {
font-size: 18px;
font-weight: bold;
color: #333;
}
/* Chart Section */
.card-filter-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
background: #fafafa;
padding: 10px;
border-radius: 4px;
}
.filter-left {
display: flex;
align-items: center;
gap: 10px;
}
.filter-label {
font-size: 14px;
font-weight: bold;
}
.chart-container {
width: 100%;
height: 400px;
}
/* Table Section */
.table-filter-row {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 20px;
}
.search-btn-small { background-color: #409eff; width: 70px; }
.reset-btn-small { background-color: #409eff; border-color: #409eff; width: 70px; }
.export-btn-small { background-color: #ff7875; border-color: #ff7875; width: 90px; }
.export-list-btn { background-color: #ff7875; border-color: #ff7875; width: 110px; }
/* Pagination */
.pagination-container {
display: flex;
justify-content: center;
align-items: center;
margin-top: 20px;
}
.total-count {
font-size: 14px;
color: #606266;
margin-right: 20px;
}
.refresh-time {
position: fixed;
bottom: 20px;
right: 20px;
background: rgba(0, 0, 0, 0.6);
color: #fff;
padding: 8px 15px;
border-radius: 20px;
font-size: 12px;
z-index: 2000;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
</style>

1324
src/views/PlatformData/UserLoginStats.vue
File diff suppressed because it is too large
View File

1007
src/views/PlatformData/UserOverview.vue
File diff suppressed because it is too large
View File

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save