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