更新1.1.0

This commit is contained in:
ktianc 2022-11-16 15:41:24 +08:00
parent 49d9ca7652
commit e5c4358905
240 changed files with 7831 additions and 1680 deletions

View File

@ -18,12 +18,33 @@
Kinit 是一套全部开源的快速开发平台,毫无保留给个人及企业免费使用。
- 🧑‍🤝‍🧑前端采用 [vue-element-plus-admin](https://gitee.com/kailong110120130/vue-element-plus-admin) 、[Vue3](https://cn.vuejs.org/guide/introduction.html)、[Element Plus](https://element-plus.gitee.io/zh-CN/guide/design.html)、[TypeScript](https://www.tslang.cn/),等主流技术开发。
- 👭后端采用 Python 语言高性能 [FastAPI](https://fastapi.tiangolo.com/zh/) 框架以及强大的 Mysql 数据库
- 👭后端采用 Python 语言以及现代、快速(高性能 [FastAPI](https://fastapi.tiangolo.com/zh/) 框架。
- 👫权限认证使用[(哈希)密码和 JWT Bearer 令牌的 OAuth2](https://fastapi.tiangolo.com/zh/tutorial/security/oauth2-jwt/),支持多终端认证系统。
- 👬支持加载动态权限菜单,多方式轻松权限控制。
- 💏特别鸣谢:[django-vue-admin](https://gitee.com/liqianglog/django-vue-admin) 、 [vue-element-plus-admin](https://gitee.com/kailong110120130/vue-element-plus-admin)。
- 开箱即用的中后台解决方案,可以用来作为新项目的启动模版,也可用于学习参考。并且时刻关注着最新技术动向,尽可能的第一时间更新。
## 💏特别鸣谢
[ELADMIN](https://eladmin.vip/demo):项目基于 Spring Boot 2.1.0 、 Jpa、 Spring Security、redis、Vue的前后端分离的后台管理系统。
[django-vue-admin](https://gitee.com/liqianglog/django-vue-admin)基于RBAC模型的权限控制的一整套基础开发平台前后端分离后端采用 django+django-rest-framework前端采用 vue+ElementUI。
[vue-element-plus-admin](https://gitee.com/kailong110120130/vue-element-plus-admin)一套基于vue3、element-plus、typescript4、vite3的后台集成方案
[Ant Design Pro](https://preview.pro.ant.design/dashboard/analysis):开箱即用的中台前端/设计解决方案
[Gin-Vue-Admin](https://demo.gin-vue-admin.com)基于vite+vue3+gin搭建的开发基础平台支持TS,JS混用集成jwt鉴权权限管理动态路由显隐可控组件分页封装多点登录拦截资源权限上传下载代码生成器表单生成器等开发必备功能。
[Vben Admin (vvbin.cn)](https://vvbin.cn/next)Vue Vben Admin 是一个免费开源的中后台模版。使用了最新的`vue3`,`vite2`,`TypeScript`等主流技术开发,开箱即用的中后台前端解决方案,也可用于学习参考。
[中华人民共和国行政区划 (github.com)](https://github.com/modood/Administrative-divisions-of-China):省级(省份)、 地级(城市)、 县级(区县)、 乡级(乡镇街道)、 村级(村委会居委会) ,中国省市区镇村二级三级四级五级联动地址数据。
[Vue Admin Plus](https://vue-admin-beautiful.com/admin-plus/#/index)vue-admin-better是github开源admin中最优秀的集成框架之一它是国内首个基于vue3.0的开源admin项目同时支持电脑手机平板默认分支使用vue3.x+antdv开发master分支使用的是vue2.x+element开发。
[小诺开源技术 (xiaonuo.vip)](https://www.xiaonuo.vip/):国内首个国密前后端分离快速开发平台
[my-web:](https://gitee.com/newgateway/my-web)MyWeb 是一个企业级中后台前端/设计解决方案的的项目工程模板,它可以帮助你快速搭建企业级中后台产品原型
## 在线体验
👩👧👦演示地址http://kinit.ktianc.top/
@ -49,11 +70,7 @@ github地址https://github.com/vvandk/kinit 👩‍👦‍👦
- [x] 📚字典管理:对系统中经常使用的一些较为固定的数据进行维护。
- [ ] 📁附件管理:对平台上所有文件、图片等进行统一管理。
- [ ] 🗓️登录日志:用户登录日志记录和查询。
- [ ] 🗓️操作日志:系统正常操作日志记录和查询;系统异常信息日志记录和查询。
- [ ] 📁附件管理对平台上所有文件、图片等进行统一管理对接阿里云OSS。
- [x] 🔒登录认证:目前支持用户使用手机号+密码方式登录。
@ -61,6 +78,32 @@ github地址https://github.com/vvandk/kinit 👩‍👦‍👦
说明:用户在第一次登录时,必须修改当前用户密码。
- [x] 系统配置:对本系统环境信息进行动态配置
网站标题LOGO描述ICO备案号底部内容百度统计代码等等
- [ ] 数据分析:根据用户的登录用户地址分析出哪个地区的人最多
- [x] 🗓️登录日志:用户登录日志记录和查询。
- [x] 🗓️操作日志:系统用户每次操作功能时的详细记录。
- [ ] **🗓️异常日志:获取并展示接口异常日志**
- [x] 🧾接口文档:提供自动生成的交互式 API 文档,与 ReDoc 文档
- [x] 导入导出:灵活支持数据导入导出功能
- [x] 手机验证码登录功能’
## TODO
- [ ] 考虑支持多机部署方案,如果接口使用多机,那么用户是否支持统一认证
- [ ] **自动化编排服务使用docker-compose部署项目**
- [ ] **数据库备份:自动备份数据库**
- [ ] **接入数据大屏**
- [ ] **可视化低代码表单接入低代码表单https://vform666.com/vform3.html?from=element_plus**
## 前序准备
- [FastAPI](https://fastapi.tiangolo.com/zh/) - 熟悉后台接口 Web 框架
@ -73,6 +116,18 @@ github地址https://github.com/vvandk/kinit 👩‍👦‍👦
- [Element-Plus](https://gitee.com/link?target=https%3A%2F%2Felement-plus.org%2F) - element-plus 基本使用
- [Mock.js](https://gitee.com/link?target=https%3A%2F%2Fgithub.com%2Fnuysoft%2FMock) - mockjs 基本语法
### 依赖包
#### 前端
- [vue3-json-viewer](https://gitee.com/isfive/vue3-json-viewer)简单易用的json内容展示组件,适配vue3和vite。
- [vue3-slide-verify](https://github.com/monoplasty/vue3-slide-verify):滑块验证码插件 vue3 + typescript
- [SortableJS/vue.draggable.next](https://github.com/SortableJS/vue.draggable.next)Vue 组件 Vue.js 3.0 允许拖放和与视图模型数组同步。
#### 后端
- [iP查询接口文档](https://user.ip138.com/ip/doc)IP查询第三方服务有1000次的免费次数
## 安装和使用
获取代码

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 865 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 61 KiB

View File

@ -1,11 +0,0 @@
# 环境
NODE_ENV=development
# 接口前缀
VITE_API_BASEPATH=base
# 打包路径
VITE_BASE_PATH=/
# 标题
VITE_APP_TITLE=Kinit

View File

@ -1,11 +1,8 @@
# 环境
NODE_ENV=production
NODE_ENV=development
# 接口前缀
VITE_API_BASEPATH=dev
# 打包路径
VITE_BASE_PATH=/dist-dev/
# 访问基础路径
VITE_BASE_PATH=/
# 是否删除debugger
VITE_DROP_DEBUGGER=false
@ -16,8 +13,8 @@ VITE_DROP_CONSOLE=false
# 是否sourcemap
VITE_SOURCEMAP=true
# 输出路径
# 打包输出路径
VITE_OUT_DIR=dist-dev
# 标题
VITE_APP_TITLE=Kinit
VITE_APP_TITLE=后台系统-开发

View File

@ -1,23 +0,0 @@
# 环境
NODE_ENV=production
# 接口前缀
VITE_API_BASEPATH=pro
# 打包路径
VITE_BASE_PATH=/kinit/
# 是否删除debugger
VITE_DROP_DEBUGGER=true
# 是否删除console.log
VITE_DROP_CONSOLE=true
# 是否sourcemap
VITE_SOURCEMAP=false
# 输出路径
VITE_OUT_DIR=dist-pro
# 标题
VITE_APP_TITLE=Kinit

View File

@ -1,10 +1,7 @@
# 环境
NODE_ENV=production
# 接口前缀
VITE_API_BASEPATH=pro
# 打包路径
# 访问基础路径
VITE_BASE_PATH=/
# 是否删除debugger
@ -16,8 +13,8 @@ VITE_DROP_CONSOLE=true
# 是否sourcemap
VITE_SOURCEMAP=false
# 输出路径
# 打包输出路径
VITE_OUT_DIR=dist-pro
# 标题
VITE_APP_TITLE=Kinit
VITE_APP_TITLE=后台系统

View File

@ -1,23 +0,0 @@
# 环境
NODE_ENV=production
# 接口前缀
VITE_API_BASEPATH=test
# 打包路径
VITE_BASE_PATH=/dist-test/
# 是否删除debugger
VITE_DROP_DEBUGGER=false
# 是否删除console.log
VITE_DROP_CONSOLE=false
# 是否sourcemap
VITE_SOURCEMAP=true
# 输出路径
VITE_OUT_DIR=dist-test
# 标题
VITE_APP_TITLE=Kinit

View File

@ -2,6 +2,86 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
## [1.8.3](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.8.2...v1.8.3) (2022-10-28)
### Performance Improvements
* 优化描述组件 ([73ecc98](https://github.com/kailong321200875/vue-element-plus-admin/commit/73ecc98671d430013920246d98ce9ab1752e56eb))
## [1.8.2](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.8.1...v1.8.2) (2022-10-18)
### Bug Fixes
* Correct spelling of words(aciton →action) ([eb405b2](https://github.com/kailong321200875/vue-element-plus-admin/commit/eb405b2a9041ca0ad4455db79bf617ec910dc485))
* Correct spelling of words(tigger →trigger) ([c2ca2d7](https://github.com/kailong321200875/vue-element-plus-admin/commit/c2ca2d736c92e02380923a6741450844acb41a38))
## [1.8.1](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.8.0...v1.8.1) (2022-10-11)
### Bug Fixes
* 修复cutMenu收起时 ([993af6b](https://github.com/kailong321200875/vue-element-plus-admin/commit/993af6bb6576249e66e0c0ea592ebf851f65ab8c))
### Styling
* cutMenu层级样式 ([32d2408](https://github.com/kailong321200875/vue-element-plus-admin/commit/32d2408588c487cff2cf73e3cc132e5105ff4459))
## [1.8.0](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.7.1...v1.8.0) (2022-10-10)
### Features
* types优化 ([3351155](https://github.com/kailong321200875/vue-element-plus-admin/commit/33511553cd9055b036b2d7491f9c2eda123f8b22))
### Styling
* 优化第四种布局 ([122fa62](https://github.com/kailong321200875/vue-element-plus-admin/commit/122fa62d859413d16175e0d97c7bf13f232dbb3a))
## [1.7.1](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.7.0...v1.7.1) (2022-10-10)
### Bug Fixes
* 修正types提示错误 ([ef3e006](https://github.com/kailong321200875/vue-element-plus-admin/commit/ef3e006859dcd8b93ffb7cffcaeae24cbb330f2a))
## [1.7.0](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.6.6...v1.7.0) (2022-10-09)
### Features
* type抽离 ([8b4fa1a](https://github.com/kailong321200875/vue-element-plus-admin/commit/8b4fa1aa21aa2c1379288315ccd64a6f3375be51))
## [1.6.6](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.6.5...v1.6.6) (2022-10-09)
### Bug Fixes
* table search params ([a62929a](https://github.com/kailong321200875/vue-element-plus-admin/commit/a62929a8dac21028d3dd1cddf98189492c33b093))
## [1.6.5](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.6.4...v1.6.5) (2022-10-08)
### Bug Fixes
* The attribute of option does not work ([d946920](https://github.com/kailong321200875/vue-element-plus-admin/commit/d946920e61ed81beacf9f1f8be7ee1f50505f64d))
### Performance Improvements
* perf store ([d416178](https://github.com/kailong321200875/vue-element-plus-admin/commit/d416178d69ca6100be4b635922b1a22d27629f08))
* token test ([b320e65](https://github.com/kailong321200875/vue-element-plus-admin/commit/b320e658d1a559a6eaebdf374d63649c223c2ecd))
## [1.6.4](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.6.3...v1.6.4) (2022-09-21)
### Bug Fixes
* fix bug ([da39f3b](https://github.com/kailong321200875/vue-element-plus-admin/commit/da39f3bc904ca2d80f432a31709725f9a57deb19))
## [1.6.3](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.6.2...v1.6.3) (2022-08-20)

View File

@ -56,6 +56,12 @@ git clone https://github.com/kailong321200875/vue-element-plus-admin.git
- 安装依赖
```bash
临时修改
pnpm --registry https://registry.npm.taobao.org install any-touch
持久使用
pnpm config set registry https://registry.npm.taobao.org
cd vue-element-plus-admin
pnpm install

View File

@ -1,8 +1,8 @@
<!DOCTYPE html>
<html lang="en">
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<link rel="icon" href="/static/system/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title><%= title %></title>
</head>
@ -126,10 +126,11 @@
</style>
<div class="app-loading">
<div class="app-loading-wrap">
<div class="app-loading-title">
<!-- 设置页面加载时的Logo与标题 -->
<!-- <div class="app-loading-title">
<img src="/logo.png" class="app-loading-logo" alt="Logo" />
<div class="app-loading-title"><%= title %></div>
</div>
</div> -->
<div class="app-loading-item">
<div class="app-loading-outter"></div>
<div class="app-loading-inner"></div>

View File

@ -1,138 +0,0 @@
import { config } from '@/config/axios/config'
import { MockMethod } from 'vite-plugin-mock'
const { result_code } = config
const timeout = 1000
export default [
// 获取统计
{
url: '/api/workplace/total',
method: 'get',
timeout,
response: () => {
return {
code: result_code,
data: {
project: 40,
access: 2340,
todo: 10
}
}
}
},
// 获取项目
{
url: '/api/workplace/project',
method: 'get',
timeout,
response: () => {
return {
code: result_code,
data: [
{
name: 'Github',
icon: 'akar-icons:github-fill',
message: 'workplace.introduction',
personal: 'kinit',
time: new Date()
},
{
name: 'Vue',
icon: 'logos:vue',
message: 'workplace.introduction',
personal: 'kinit',
time: new Date()
},
{
name: 'Angular',
icon: 'logos:angular-icon',
message: 'workplace.introduction',
personal: 'kinit',
time: new Date()
},
{
name: 'React',
icon: 'logos:react',
message: 'workplace.introduction',
personal: 'kinit',
time: new Date()
},
{
name: 'Webpack',
icon: 'logos:webpack',
message: 'workplace.introduction',
personal: 'kinit',
time: new Date()
},
{
name: 'Vite',
icon: 'vscode-icons:file-type-vite',
message: 'workplace.introduction',
personal: 'kinit',
time: new Date()
}
]
}
}
},
// 获取动态
{
url: '/api/workplace/dynamic',
method: 'get',
timeout,
response: () => {
return {
code: result_code,
data: [
{
keys: ['workplace.push', 'Github'],
time: new Date()
},
{
keys: ['workplace.push', 'Github'],
time: new Date()
}
]
}
}
},
// 获取团队信息
{
url: '/api/workplace/team',
method: 'get',
timeout,
response: () => {
return {
code: result_code,
data: [
{
name: 'Github',
icon: 'akar-icons:github-fill'
},
{
name: 'Vue',
icon: 'logos:vue'
},
{
name: 'Angular',
icon: 'logos:angular-icon'
},
{
name: 'React',
icon: 'logos:react'
},
{
name: 'Webpack',
icon: 'logos:webpack'
},
{
name: 'Vite',
icon: 'vscode-icons:file-type-vite'
}
]
}
}
}
] as MockMethod[]

View File

@ -1,20 +1,17 @@
{
"name": "vue-element-plus-admin",
"version": "1.6.3",
"version": "1.8.4",
"description": "一套基于vue3、element-plus、typesScript、vite3的后台集成方案。",
"author": "Archer <502431556@qq.com>",
"private": false,
"scripts": {
"i": "pnpm install",
"dev": "vite --mode base",
"dev": "vite --mode dev",
"ts:check": "vue-tsc --noEmit",
"build:pro": "vite build --mode pro",
"build:gitee": "vite build --mode gitee",
"build:dev": "npm run ts:check && vite build --mode dev",
"build:test": "npm run ts:check && vite build --mode test",
"serve:pro": "vite preview --mode pro",
"serve:dev": "vite preview --mode dev",
"serve:test": "vite preview --mode test",
"npm:check": "npx npm-check-updates",
"clean": "npx rimraf node_modules",
"clean:cache": "npx rimraf node_modules/.cache",
@ -23,84 +20,88 @@
"lint:style": "stylelint --fix \"**/*.{vue,less,postcss,css,scss}\" --cache --cache-location node_modules/.cache/stylelint/",
"lint:lint-staged": "lint-staged -c ./.husky/lintstagedrc.js",
"prepare": "husky install",
"p": "plop"
"p": "plop",
"analysis": "windicss-analysis"
},
"dependencies": {
"@iconify/iconify": "^2.2.1",
"@vueuse/core": "^9.1.1",
"@wangeditor/editor": "^5.1.14",
"@iconify/iconify": "^3.0.0",
"@vueuse/core": "^9.4.0",
"@wangeditor/editor": "^5.1.22",
"@wangeditor/editor-for-vue": "^5.1.10",
"@zxcvbn-ts/core": "^2.0.4",
"@zxcvbn-ts/core": "^2.1.0",
"animate.css": "^4.1.1",
"axios": "^0.27.2",
"echarts": "^5.3.3",
"axios": "^1.1.3",
"clipboard": "^2.0.11",
"echarts": "^5.4.0",
"echarts-wordcloud": "^2.0.0",
"element-plus": "2.2.15",
"element-plus": "2.2.21",
"intro.js": "^6.0.0",
"lodash-es": "^4.17.21",
"mitt": "^3.0.0",
"mockjs": "^1.1.0",
"nprogress": "^0.2.0",
"pinia": "^2.0.21",
"pinia": "^2.0.23",
"pinia-plugin-persist": "^1.0.0",
"qrcode": "^1.5.1",
"qs": "^6.11.0",
"url": "^0.11.0",
"vue": "3.2.37",
"vue": "3.2.41",
"vue-i18n": "9.2.2",
"vue-router": "^4.1.5",
"vue-router": "^4.1.6",
"vue-types": "^4.2.1",
"vue3-json-viewer": "^2.2.2",
"vuedraggable": "4.1.0",
"web-storage-cache": "^1.1.1"
},
"devDependencies": {
"@commitlint/cli": "^17.1.2",
"@commitlint/config-conventional": "^17.1.0",
"@iconify/json": "^2.1.100",
"@intlify/vite-plugin-vue-i18n": "^6.0.1",
"@commitlint/cli": "^17.2.0",
"@commitlint/config-conventional": "^17.2.0",
"@iconify/json": "^2.1.134",
"@intlify/vite-plugin-vue-i18n": "^6.0.3",
"@purge-icons/generated": "^0.9.0",
"@types/intro.js": "^5.1.0",
"@types/lodash-es": "^4.17.6",
"@types/node": "^18.7.13",
"@types/node": "^18.11.9",
"@types/nprogress": "^0.2.0",
"@types/qrcode": "^1.5.0",
"@types/qs": "^6.9.7",
"@typescript-eslint/eslint-plugin": "^5.35.1",
"@typescript-eslint/parser": "^5.35.1",
"@vitejs/plugin-vue": "^3.0.3",
"@vitejs/plugin-vue-jsx": "^2.0.0",
"autoprefixer": "^10.4.8",
"eslint": "^8.23.0",
"@typescript-eslint/eslint-plugin": "^5.42.0",
"@typescript-eslint/parser": "^5.42.0",
"@vitejs/plugin-vue": "^3.2.0",
"@vitejs/plugin-vue-jsx": "^2.1.0",
"autoprefixer": "^10.4.13",
"eslint": "^8.27.0",
"eslint-config-prettier": "^8.5.0",
"eslint-define-config": "^1.6.0",
"eslint-define-config": "^1.11.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-vue": "^9.4.0",
"eslint-plugin-vue": "^9.7.0",
"husky": "^8.0.1",
"less": "^4.1.3",
"lint-staged": "^13.0.3",
"plop": "^3.1.1",
"postcss": "^8.4.16",
"postcss": "^8.4.18",
"postcss-html": "^1.5.0",
"postcss-less": "^6.0.0",
"prettier": "^2.7.1",
"rimraf": "^3.0.2",
"rollup": "^2.78.1",
"stylelint": "^14.11.0",
"rollup": "^3.2.5",
"stylelint": "^14.14.1",
"stylelint-config-html": "^1.1.0",
"stylelint-config-prettier": "^9.0.3",
"stylelint-config-recommended": "^9.0.0",
"stylelint-config-standard": "^28.0.0",
"stylelint-config-standard": "^29.0.0",
"stylelint-order": "^5.0.0",
"typescript": "4.8.2",
"unplugin-vue-macros": "^0.11.0",
"vite": "3.0.9",
"typescript": "4.8.4",
"unplugin-vue-macros": "^0.16.0",
"vite": "3.2.2",
"vite-plugin-eslint": "^1.8.1",
"vite-plugin-html": "^3.2.0",
"vite-plugin-mock": "^2.9.6",
"vite-plugin-purge-icons": "^0.9.0",
"vite-plugin-purge-icons": "^0.9.1",
"vite-plugin-style-import": "2.0.0",
"vite-plugin-svg-icons": "^2.0.1",
"vite-plugin-windicss": "^1.8.7",
"vue-tsc": "^0.40.4",
"vite-plugin-windicss": "^1.8.8",
"vue-tsc": "^1.0.9",
"windicss": "^3.5.6",
"windicss-analysis": "^0.3.5"
},

View File

@ -5,6 +5,7 @@ import { ConfigGlobal } from '@/components/ConfigGlobal'
import { isDark } from '@/utils/is'
import { useDesign } from '@/hooks/web/useDesign'
import { useCache } from '@/hooks/web/useCache'
import { getSystemSettingsClassifysApi } from '@/api/vadmin/system/settings'
const { getPrefixCls } = useDesign()
@ -12,6 +13,36 @@ const prefixCls = getPrefixCls('app')
const appStore = useAppStore()
// mate
const addMeta = (name: string, content: string) => {
const meta = document.createElement('meta')
meta.content = content
meta.name = name
document.getElementsByTagName('head')[0].appendChild(meta)
}
//
const setSystemConfig = async () => {
const res = await getSystemSettingsClassifysApi({ classify: 'web' })
if (res) {
appStore.setTitle(res.data.web_basic.web_title || import.meta.env.VITE_APP_TITLE)
appStore.setLogoImage(res.data.web_basic.web_logo || '/static/system/logo.png')
appStore.setFooterContent(res.data.web_basic.web_copyright || 'Copyright ©2022-present K')
appStore.setIcpNumber(res.data.web_basic.web_icp_number || '')
addMeta(
'description',
res.data.web_basic.web_desc ||
'Kinit 是一套开箱即用的中后台解决方案,可以作为新项目的启动模版。'
)
//
if (res.data.web_baidu.web_baidu) {
eval(res.data.web_baidu.web_baidu)
}
}
}
setSystemConfig()
const currentSize = computed(() => appStore.getCurrentSize)
const greyMode = computed(() => appStore.getGreyMode)
@ -60,4 +91,14 @@ body {
.@{prefix-cls}-grey-mode {
filter: grayscale(100%);
}
ol {
display: block;
list-style-type: decimal;
margin-block-start: 1em;
margin-block-end: 1em;
margin-inline-start: 0px;
margin-inline-end: 0px;
padding-inline-start: 40px;
}
</style>

View File

@ -0,0 +1,23 @@
import request from '@/config/axios'
import type {
AnalysisBannersTypes,
UserAccessSource,
WeeklyUserActivity,
MonthlySales
} from './types'
export const getBannersApi = (): Promise<IResponse<AnalysisBannersTypes[]>> => {
return request.get({ url: '/vadmin/analysis/banners/' })
}
export const getUserAccessSourceApi = (): Promise<IResponse<UserAccessSource[]>> => {
return request.get({ url: '/vadmin/analysis/user/access/source/' })
}
export const getWeeklyUserActivityApi = (): Promise<IResponse<WeeklyUserActivity[]>> => {
return request.get({ url: '/vadmin/analysis/weekly/user/activity/' })
}
export const getMonthlySalesApi = (): Promise<IResponse<MonthlySales[]>> => {
return request.get({ url: '/vadmin/analysis/monthly/sales/' })
}

View File

@ -0,0 +1,20 @@
export type AnalysisBannersTypes = {
id: number
image: string
}
export type UserAccessSource = {
value: number
name: string
}
export type WeeklyUserActivity = {
value: number
name: string
}
export type MonthlySales = {
name: string
estimate: number
actual: number
}

View File

@ -1,22 +1,26 @@
import request from '@/config/axios'
import type { WorkplaceTotal, Project, Dynamic, Team, RadarData } from './types'
import type { WorkplaceTotal, Project, Dynamic, Team, RadarData, Shortcuts } from './types'
export const getCountApi = (): Promise<IResponse<WorkplaceTotal>> => {
return request.get({ url: '/workplace/total' })
return request.get({ url: '/vadmin/workplace/total/' })
}
export const getProjectApi = (): Promise<IResponse<Project>> => {
return request.get({ url: '/workplace/project' })
return request.get({ url: '/vadmin/workplace/project/' })
}
export const getDynamicApi = (): Promise<IResponse<Dynamic[]>> => {
return request.get({ url: '/workplace/dynamic' })
return request.get({ url: '/vadmin/workplace/dynamic/' })
}
export const getTeamApi = (): Promise<IResponse<Team[]>> => {
return request.get({ url: '/workplace/team' })
return request.get({ url: '/vadmin/workplace/team/' })
}
export const getRadarApi = (): Promise<IResponse<RadarData[]>> => {
return request.get({ url: '/workplace/radar' })
return request.get({ url: '/vadmin/workplace/radar/' })
}
export const getShortcutsApi = (): Promise<IResponse<Shortcuts[]>> => {
return request.get({ url: '/vadmin/workplace/shortcuts/' })
}

View File

@ -9,6 +9,7 @@ export type Project = {
icon: string
message: string
personal: string
link: string
time: Date | number | string
}
@ -28,3 +29,8 @@ export type RadarData = {
max: number
name: string
}
export type Shortcuts = {
name: string
link: string
}

View File

@ -8,3 +8,7 @@ export const loginApi = (data: UserLoginType): Promise<IResponse> => {
export const getRoleMenusApi = (): Promise<IResponse<AppCustomRouteRecordRaw[]>> => {
return request.get({ url: '/auth/getMenuList/' })
}
export const postSMSCodeApi = (params: any): Promise<IResponse> => {
return request.post({ url: '/vadmin/system/sms/send/', params })
}

View File

@ -1,6 +1,7 @@
export type UserLoginType = {
telephone: string
password: string
method: string
}
export type UserType = {

View File

@ -31,3 +31,26 @@ export const postCurrentUserUpdateInfo = (data: any): Promise<IResponse> => {
export const getCurrentUserInfo = (): Promise<IResponse> => {
return request.get({ url: `/vadmin/auth/user/current/info/` })
}
export const postExportUserQueryListApi = (params: any, data: any): Promise<IResponse> => {
return request.post({ url: `/vadmin/auth/user/export/query/list/to/excel/`, params, data })
}
export const getImportTemplateApi = (): Promise<IResponse> => {
return request.get({ url: `/vadmin/auth/user/download/import/template/` })
}
export const postImportUserApi = (data: any): Promise<IResponse> => {
return request.post({
url: `/vadmin/auth/import/users/`,
headers: {
'Content-Type': 'multipart/form-data'
},
timeout: 5 * 60 * 1000,
data
})
}
export const postUsersInitPasswordSendSMSApi = (data: any): Promise<IResponse> => {
return request.post({ url: `/vadmin/auth/users/init/password/send/sms/`, data })
}

View File

@ -0,0 +1,5 @@
import request from '@/config/axios'
export const getRecordLoginListApi = (params: any): Promise<IResponse> => {
return request.get({ url: '/vadmin/record/logins/', params })
}

View File

@ -0,0 +1,5 @@
import request from '@/config/axios'
export const getRecordOperationListApi = (params: any): Promise<IResponse> => {
return request.get({ url: '/vadmin/record/operations/', params })
}

View File

@ -0,0 +1,17 @@
import request from '@/config/axios'
export const getSystemSettingsTabsApi = (params: any): Promise<IResponse> => {
return request.get({ url: '/vadmin/system/settings/tabs/', params })
}
export const getSystemSettingsApi = (params: any): Promise<IResponse> => {
return request.get({ url: '/vadmin/system/settings/tabs/values/', params })
}
export const putSystemSettingsApi = (data: any): Promise<IResponse> => {
return request.put({ url: '/vadmin/system/settings/tabs/values/', data })
}
export const getSystemSettingsClassifysApi = (params: any): Promise<IResponse> => {
return request.get({ url: '/vadmin/system/settings/classifys/', params })
}

View File

@ -7,13 +7,14 @@ import { useWindowSize } from '@vueuse/core'
import { useAppStore } from '@/store/modules/app'
import { setCssVar } from '@/utils'
import { useDesign } from '@/hooks/web/useDesign'
import { ElementPlusSize } from '@/types/elementPlus'
const { variables } = useDesign()
const appStore = useAppStore()
const props = defineProps({
size: propTypes.oneOf<ElememtPlusSize[]>(['default', 'small', 'large']).def('default')
size: propTypes.oneOf<ElementPlusSize[]>(['default', 'small', 'large']).def('default')
})
provide('configGlobal', props)

View File

@ -4,7 +4,7 @@ import { PropType, ref } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { useDesign } from '@/hooks/web/useDesign'
import type { RouteLocationNormalizedLoaded } from 'vue-router'
import { contextMenuSchema } from '../../../types/contextMenu'
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('context-menu')

View File

@ -2,8 +2,9 @@
import { ElCollapseTransition, ElDescriptions, ElDescriptionsItem, ElTooltip } from 'element-plus'
import { useDesign } from '@/hooks/web/useDesign'
import { propTypes } from '@/utils/propTypes'
import { ref, unref, PropType, computed, useAttrs } from 'vue'
import { ref, unref, PropType, computed, useAttrs, useSlots } from 'vue'
import { useAppStore } from '@/store/modules/app'
import { DescriptionsSchema } from '@/types/descriptions'
const appStore = useAppStore()
@ -11,6 +12,8 @@ const mobile = computed(() => appStore.getMobile)
const attrs = useAttrs()
const slots = useSlots()
const props = defineProps({
title: propTypes.string.def(''),
message: propTypes.string.def(''),
@ -95,6 +98,9 @@ const toggleClick = () => {
:direction="mobile ? 'vertical' : 'horizontal'"
v-bind="getBindValue"
>
<template v-if="slots['extra']" #extra>
<slot name="extra"></slot>
</template>
<ElDescriptionsItem
v-for="item in schema"
:key="item.field"

View File

@ -72,7 +72,7 @@ const dialogStyle = computed(() => {
</slot>
<Icon
v-if="fullscreen"
class="mr-18px cursor-pointer is-hover mt-2px"
class="mr-18px cursor-pointer is-hover mt-2px z-10"
:icon="isFullscreen ? 'zmdi:fullscreen-exit' : 'zmdi:fullscreen'"
color="var(--el-color-info)"
@click="toggleFull"

View File

@ -116,7 +116,7 @@ defineExpose({
</script>
<template>
<div class="border-1 border-solid border-[var(--tags-view-border-color)]">
<div class="border-1 border-solid border-[var(--tags-view-border-color)] z-3000">
<!-- 工具栏 -->
<Toolbar
:editor="editorRef"

View File

@ -1,7 +1,7 @@
<script setup lang="ts">
import { useAppStore } from '@/store/modules/app'
import { computed } from 'vue'
import { useDesign } from '@/hooks/web/useDesign'
import { computed } from 'vue'
const { getPrefixCls } = useDesign()
@ -9,7 +9,7 @@ const prefixCls = getPrefixCls('footer')
const appStore = useAppStore()
const title = computed(() => appStore.getTitle)
const footerContent = computed(() => appStore.getFooterContent)
</script>
<template>
@ -17,6 +17,6 @@ const title = computed(() => appStore.getTitle)
:class="prefixCls"
class="text-center text-[var(--el-text-color-placeholder)] bg-[var(--app-content-bg-color)] h-[var(--app-footer-height)] leading-[var(--app-footer-height)] dark:bg-[var(--el-bg-color)]"
>
Copyright ©2021-present {{ title }}
{{ footerContent }}
</div>
</template>

View File

@ -1,5 +1,6 @@
import Form from './src/Form.vue'
import { ElForm } from 'element-plus'
import { FormSchema, FormSetPropsType } from '@/types/form'
export interface FormExpose {
setValues: (data: Recordable) => void

View File

@ -20,6 +20,7 @@ import { findIndex } from '@/utils'
import { set } from 'lodash-es'
import { FormProps } from './types'
import { Icon } from '@/components/Icon'
import { FormSchema, FormSetPropsType } from '@/types/form'
const { getPrefixCls } = useDesign()
@ -244,6 +245,11 @@ export default defineComponent({
vModel={formModel.value[item.field]}
{...(autoSetPlaceholder && setTextPlaceholder(item))}
{...setComponentProps(item)}
style={
item?.component === 'Input'
? { width: '100%', ...item.componentProps?.style }
: { ...item.componentProps?.style }
}
{...(notRenderOptions.includes(item?.component as string) &&
item?.componentProps?.options
? { options: item?.componentProps?.options || [] }

View File

@ -22,6 +22,8 @@ import {
} from 'element-plus'
import { InputPassword } from '@/components/InputPassword'
import { Editor } from '@/components/Editor'
import { Text } from '@/components/Text'
import { ComponentName } from '@/types/components'
const componentMap: Recordable<Component, ComponentName> = {
Radio: ElRadioGroup,
@ -46,7 +48,8 @@ const componentMap: Recordable<Component, ComponentName> = {
InputPassword: InputPassword,
Editor: Editor,
TreeSelect: ElTreeSelect,
Tree: ElTree
Tree: ElTree,
Text: Text
}
export { componentMap }

View File

@ -1,3 +1,4 @@
import { FormSchema } from '@/types/form'
import { ElCheckbox, ElCheckboxButton } from 'element-plus'
import { defineComponent } from 'vue'
@ -11,6 +12,12 @@ export const useRenderCheckbox = () => {
>
return item?.componentProps?.options?.map((option) => {
return <Com label={option[labelAlias || 'value']}>{option[valueAlias || 'label']}</Com>
// const { value, ...other } = option
// return (
// <Com label={option[labelAlias || 'value']} {...other}>
// {option[valueAlias || 'label']}
// </Com>
// )
})
}

View File

@ -1,3 +1,4 @@
import { FormSchema } from '@/types/form'
import { ElRadio, ElRadioButton } from 'element-plus'
import { defineComponent } from 'vue'
@ -11,6 +12,12 @@ export const useRenderRadio = () => {
>
return item?.componentProps?.options?.map((option) => {
return <Com label={option[labelAlias || 'value']}>{option[valueAlias || 'label']}</Com>
// const { value, ...other } = option
// return (
// <Com label={option[labelAlias || 'value']} {...other}>
// {option[valueAlias || 'label']}
// </Com>
// )
})
}

View File

@ -1,6 +1,8 @@
import { ElOption, ElOptionGroup } from 'element-plus'
import { getSlot } from '@/utils/tsxHelper'
import { Slots } from 'vue'
import { FormSchema } from '@/types/form'
import { ComponentOptions } from '@/types/components'
export const useRenderSelect = (slots: Slots) => {
// 渲染 select options
@ -29,11 +31,14 @@ export const useRenderSelect = (slots: Slots) => {
// 如果有别名,就取别名
const labelAlias = item?.componentProps?.optionsAlias?.labelField
const valueAlias = item?.componentProps?.optionsAlias?.valueField
const { label, value, ...other } = option
return (
<ElOption
label={option[labelAlias || 'label']}
value={option[valueAlias || 'value']}
disabled={option.disabled}
label={labelAlias ? option[labelAlias] : label}
value={valueAlias ? option[valueAlias] : value}
{...other}
>
{{
default: () =>

View File

@ -2,6 +2,8 @@ import { useI18n } from '@/hooks/web/useI18n'
import type { Slots } from 'vue'
import { getSlot } from '@/utils/tsxHelper'
import { PlaceholderMoel } from './types'
import { FormSchema } from '@/types/form'
import { ColProps } from '@/types/components'
const { t } = useI18n()

View File

@ -1,3 +1,5 @@
import { FormSchema } from '@/types/form'
export interface PlaceholderMoel {
placeholder?: string
startPlaceholder?: string

View File

@ -1,8 +1,9 @@
<script setup lang="ts">
import { PropType } from 'vue'
import { Highlight } from '@//components/Highlight'
import { Highlight } from '@/components/Highlight'
import { useDesign } from '@/hooks/web/useDesign'
import { propTypes } from '@/utils/propTypes'
import { TipSchema } from '@/types/infoTip'
const { getPrefixCls } = useDesign()

View File

@ -17,6 +17,8 @@ const layout = computed(() => appStore.getLayout)
const collapse = computed(() => appStore.getCollapse)
const logoImage = computed(() => appStore.getLogoImage)
onMounted(() => {
if (unref(collapse)) show.value = false
})
@ -65,7 +67,7 @@ watch(
to="/"
>
<img
src="@/assets/imgs/logo.png"
:src="logoImage"
class="w-[calc(var(--logo-height)-10px)] h-[calc(var(--logo-height)-10px)]"
/>
<div

View File

@ -3,11 +3,11 @@ import { computed, defineComponent, unref, PropType } from 'vue'
import { ElMenu, ElScrollbar } from 'element-plus'
import { useAppStore } from '@/store/modules/app'
import { usePermissionStore } from '@/store/modules/permission'
import type { LayoutType } from '@/config/app'
import { useRenderMenuItem } from './components/useRenderMenuItem'
import { useRouter } from 'vue-router'
import { isUrl } from '@/utils/is'
import { useDesign } from '@/hooks/web/useDesign'
import { LayoutType } from '@/types/layout'
const { getPrefixCls } = useDesign()

View File

@ -6,6 +6,7 @@ import { cloneDeep } from 'lodash-es'
import { propTypes } from '@/utils/propTypes'
import { useDesign } from '@/hooks/web/useDesign'
import { isString } from '@/utils/is'
import { QrcodeLogo } from '@/types/qrcode'
const props = defineProps({
// img canvas,imglogo

View File

@ -0,0 +1,3 @@
import RightToolbar from './src/RightToolbar.vue'
export { RightToolbar }

View File

@ -0,0 +1,112 @@
<script setup lang="ts">
import {
ElButton,
ElRow,
ElCol,
ElDropdownItem,
ElDropdownMenu,
ElDropdown,
ElPopover,
ElCheckbox,
ElScrollbar
} from 'element-plus'
import { useIcon } from '@/hooks/web/useIcon'
import { ref, PropType } from 'vue'
import { propTypes } from '@/utils/propTypes'
import draggable from 'vuedraggable'
const emit = defineEmits(['getList', 'update:tableSize'])
const props = defineProps({
tableSize: propTypes.string.def(''),
columns: {
type: Array as PropType<any[]>,
default: () => []
}
})
const handleClickRefresh = () => {
emit('getList')
}
const refresh = useIcon({ icon: 'ic:outline-refresh' })
const spacing = useIcon({ icon: 'mdi:format-paragraph-spacing' })
const settings = useIcon({ icon: 'bytesize:settings' })
const handleCommand = (command: string) => {
emit('update:tableSize', command)
}
const checkAll = ref(false)
const columns = ref(props.columns)
const isIndeterminate = ref(true) // True
const handleCheckAllChange = (val: boolean) => {
columns.value.forEach((item) => {
if (item.disabled !== true) {
item.show = val
}
})
isIndeterminate.value = false
}
const handleCheckChange = () => {
checkAll.value = columns.value.every((item) => item.show)
if (checkAll.value) {
isIndeterminate.value = false
} else {
isIndeterminate.value = columns.value.some((item) => item.show)
}
}
handleCheckChange()
</script>
<template>
<ElRow :gutter="10">
<ElCol :span="1.5">
<ElButton link :icon="refresh" @click="handleClickRefresh">刷新数据</ElButton>
</ElCol>
<ElCol :span="1.5" class="pt-4px">
<ElDropdown trigger="click" @command="handleCommand">
<ElButton link :icon="spacing">密度调整</ElButton>
<template #dropdown>
<ElDropdownMenu>
<ElDropdownItem command="default">默认</ElDropdownItem>
<ElDropdownItem command="large">宽松</ElDropdownItem>
<ElDropdownItem command="small">紧凑</ElDropdownItem>
</ElDropdownMenu>
</template>
</ElDropdown>
</ElCol>
<ElCol :span="1.5">
<ElPopover placement="bottom" :width="200" trigger="click">
<div style="border-bottom: 1px solid #d4d7de">
<el-checkbox
v-model="checkAll"
:indeterminate="isIndeterminate"
@change="handleCheckAllChange"
>全选</el-checkbox
>
</div>
<ElScrollbar max-height="400px">
<draggable :list="columns" item-key="field">
<template #item="{ element }">
<div>
<span class="cursor-move mr-10px">
<Icon icon="akar-icons:drag-vertical" />
</span>
<ElCheckbox
v-model="element.show"
:disabled="element.disabled === true"
@change="handleCheckChange"
>{{ element.label }}</ElCheckbox
>
</div>
</template>
</draggable>
</ElScrollbar>
<template #reference>
<ElButton link :icon="settings">字段设置</ElButton>
</template>
</ElPopover>
</ElCol>
</ElRow>
</template>

View File

@ -8,6 +8,7 @@ import { useForm } from '@/hooks/web/useForm'
import { findIndex } from '@/utils'
import { cloneDeep } from 'lodash-es'
import { set } from 'lodash-es'
import { FormSchema } from '@/types/form'
const { t } = useI18n()

View File

@ -208,7 +208,7 @@ const clear = () => {
<Icon icon="ant-design:setting-outlined" color="#fff" />
</div>
<ElDrawer v-model="drawer" direction="rtl" size="350px">
<ElDrawer v-model="drawer" direction="rtl" size="350px" :z-index="4000">
<template #header>
<span class="text-16px font-700">{{ t('setting.projectSetting') }}</span>
</template>

View File

@ -115,6 +115,13 @@ const dynamicRouterChange = (show: boolean) => {
appStore.setDynamicRouter(show)
}
//
const fixedMenu = ref(appStore.getFixedMenu)
const fixedMenuChange = (show: boolean) => {
appStore.setFixedMenu(show)
}
const layout = computed(() => appStore.getLayout)
watch(
@ -198,5 +205,10 @@ watch(
<span class="text-14px">{{ t('setting.dynamicRouter') }}</span>
<ElSwitch v-model="dynamicRouter" @change="dynamicRouterChange" />
</div>
<div class="flex justify-between items-center">
<span class="text-14px">{{ t('setting.fixedMenu') }}</span>
<ElSwitch v-model="fixedMenu" @change="fixedMenuChange" />
</div>
</div>
</template>

View File

@ -5,6 +5,7 @@ import { useAppStore } from '@/store/modules/app'
import { useI18n } from '@/hooks/web/useI18n'
import { propTypes } from '@/utils/propTypes'
import { useDesign } from '@/hooks/web/useDesign'
import { ElementPlusSize } from '@/types/elementPlus'
const { getPrefixCls } = useDesign()
@ -20,7 +21,7 @@ const appStore = useAppStore()
const sizeMap = computed(() => appStore.sizeMap)
const setCurrentSize = (size: ElememtPlusSize) => {
const setCurrentSize = (size: ElementPlusSize) => {
appStore.setCurrentSize(size)
}
</script>

View File

@ -1,7 +1,7 @@
<script lang="tsx">
import { usePermissionStore } from '@/store/modules/permission'
import { useAppStore } from '@/store/modules/app'
import { computed, unref, defineComponent, watch, ref } from 'vue'
import { computed, unref, defineComponent, watch, ref, onMounted } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { ElScrollbar } from 'element-plus'
import { Icon } from '@/components/Icon'
@ -28,6 +28,8 @@ export default defineComponent({
const collapse = computed(() => appStore.getCollapse)
const fixedMenu = computed(() => appStore.getFixedMenu)
const permissionStore = usePermissionStore()
const routers = computed(() => permissionStore.getRouters)
@ -38,6 +40,27 @@ export default defineComponent({
appStore.setCollapse(!unref(collapse))
}
onMounted(() => {
if (unref(fixedMenu)) {
const path = `/${unref(currentRoute).path.split('/')[1]}`
const children = unref(tabRouters).find(
(v) =>
(v.meta?.alwaysShow || (v?.children?.length && v?.children?.length > 1)) &&
v.path === path
)?.children
tabActive.value = path
if (children) {
permissionStore.setMenuTabRouters(
cloneDeep(children).map((v) => {
v.path = pathResolve(unref(tabActive), v.path)
return v
})
)
}
}
})
watch(
() => routers.value,
(routers: AppRouteRecordRaw[]) => {
@ -66,7 +89,7 @@ export default defineComponent({
)
//
const showMenu = ref(false)
const showMenu = ref(unref(fixedMenu) ? true : false)
// tab
const tabActive = ref('')
@ -77,9 +100,13 @@ export default defineComponent({
window.open(item.path)
return
}
const newPath = item.children ? item.path : item.path.split('/')[0]
const oldPath = unref(tabActive)
tabActive.value = item.children ? item.path : item.path.split('/')[0]
if (item.children) {
showMenu.value = !unref(showMenu)
if (newPath === oldPath || !unref(showMenu)) {
showMenu.value = unref(fixedMenu) ? true : !unref(showMenu)
}
if (unref(showMenu)) {
permissionStore.setMenuTabRouters(
cloneDeep(item.children).map((v) => {
@ -96,7 +123,7 @@ export default defineComponent({
}
//
const isActice = (currentPath: string) => {
const isActive = (currentPath: string) => {
const { path } = unref(currentRoute)
if (tabPathMap[currentPath].includes(path)) {
return true
@ -105,7 +132,7 @@ export default defineComponent({
}
const mouseleave = () => {
if (!unref(showMenu)) return
if (!unref(showMenu) || unref(fixedMenu)) return
showMenu.value = false
}
@ -114,7 +141,7 @@ export default defineComponent({
id={`${variables.namespace}-menu`}
class={[
prefixCls,
'relative bg-[var(--left-menu-bg-color)] top-1px z-999',
'relative bg-[var(--left-menu-bg-color)] top-1px z-3000',
{
'w-[var(--tab-menu-max-width)]': !unref(collapse),
'w-[var(--tab-menu-min-width)]': unref(collapse)
@ -140,7 +167,7 @@ export default defineComponent({
`${prefixCls}__item`,
'text-center text-12px relative py-12px cursor-pointer',
{
'is-active': isActice(v.path)
'is-active': isActive(v.path)
}
]}
onClick={() => {
@ -174,8 +201,8 @@ export default defineComponent({
{
'!left-[var(--tab-menu-min-width)]': unref(collapse),
'!left-[var(--tab-menu-max-width)]': !unref(collapse),
'!w-[calc(var(--left-menu-max-width)+1px)]': unref(showMenu),
'!w-0': !unref(showMenu)
'!w-[calc(var(--left-menu-max-width)+1px)]': unref(showMenu) || unref(fixedMenu),
'!w-0': !unref(showMenu) && !unref(fixedMenu)
}
]}
style="transition: width var(--transition-time-02), left var(--transition-time-02);"

View File

@ -28,9 +28,9 @@ export const filterMenusPath = (
let data: Nullable<AppRouteRecordRaw> = null
const meta = (v.meta ?? {}) as RouteMeta
if (!meta.hidden || meta.canTo) {
const allParentPaht = getAllParentPath<AppRouteRecordRaw>(allRoutes, v.path)
const allParentPath = getAllParentPath<AppRouteRecordRaw>(allRoutes, v.path)
const fullPath = isUrl(v.path) ? v.path : allParentPaht.join('/')
const fullPath = isUrl(v.path) ? v.path : allParentPath.join('/')
data = cloneDeep(v)
data.path = fullPath
@ -42,8 +42,8 @@ export const filterMenusPath = (
res.push(data)
}
if (allParentPaht.length && Reflect.has(tabPathMap, allParentPaht[0])) {
tabPathMap[allParentPaht[0]].push(fullPath)
if (allParentPath.length && Reflect.has(tabPathMap, allParentPath[0])) {
tabPathMap[allParentPath[0]].push(fullPath)
}
}
}

View File

@ -1,5 +1,6 @@
import Table from './src/Table.vue'
import { ElTable } from 'element-plus'
import { TableSetPropsType } from '@/types/table'
export interface TableExpose {
setProps: (props: Recordable) => void

View File

@ -6,6 +6,7 @@ import { setIndex } from './helper'
import { getSlot } from '@/utils/tsxHelper'
import type { TableProps } from './types'
import { set } from 'lodash-es'
import { TableColumn, TableSlotDefault, Pagination, TableSetPropsType } from '../../../types/table'
export default defineComponent({
name: 'Table',
@ -23,6 +24,8 @@ export default defineComponent({
},
//
expand: propTypes.bool.def(false),
// table
stripe: propTypes.bool.def(true),
//
pagination: {
type: Object as PropType<Pagination>,
@ -49,6 +52,7 @@ export default defineComponent({
},
emits: ['update:limit', 'update:page', 'register'],
setup(props, { attrs, slots, emit, expose }) {
console.log('attrs', attrs)
const elTableRef = ref<ComponentRef<typeof ElTable>>()
//
@ -230,6 +234,7 @@ export default defineComponent({
} else {
const props = { ...v }
if (props.children) delete props.children
if (props.show === false) return
return (
<ElTableColumn
showOverflowTooltip={showOverflowTooltip}

View File

@ -1,6 +1,8 @@
import { Pagination, TableColumn } from '@/types/table'
export type TableProps = {
limit?: number
page?: number
pageSize?: number
currentPage?: number
// 是否多选
selection?: boolean
// 是否所有的超出隐藏优先级低于schema中的showOverflowTooltip,

View File

@ -0,0 +1,3 @@
import Text from './src/Text.vue'
export { Text }

View File

@ -0,0 +1,14 @@
<script setup lang="ts">
import { propTypes } from '@/utils/propTypes'
import { ref } from 'vue'
const props = defineProps({
modelValue: propTypes.string.def('')
})
const value = ref(props.modelValue)
</script>
<template>
<span v-html="value"></span>
</template>

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import { ElDropdown, ElDropdownMenu, ElDropdownItem, ElMessageBox } from 'element-plus'
import { ElDropdown, ElDropdownMenu, ElDropdownItem, ElMessageBox, ElButton } from 'element-plus'
import { useI18n } from '@/hooks/web/useI18n'
import { useRouter } from 'vue-router'
import { useDesign } from '@/hooks/web/useDesign'
@ -21,17 +21,13 @@ const loginOut = () => {
cancelButtonText: t('common.cancel'),
type: 'warning'
})
.then(async () => {
.then(() => {
authStore.logout()
replace('/login')
})
.catch(() => {})
}
const toDocument = () => {
window.open('https://element-plus-admin-doc.cn/')
}
const toHome = () => {
push('/system/home')
}
@ -54,13 +50,10 @@ const user = authStore.getUser
<template #dropdown>
<ElDropdownMenu>
<ElDropdownItem>
<div @click="toHome">个人主页</div>
</ElDropdownItem>
<ElDropdownItem>
<div @click="toDocument">前端项目文档</div>
<ElButton @click="toHome" link>个人主页</ElButton>
</ElDropdownItem>
<ElDropdownItem divided>
<div @click="loginOut">{{ t('common.loginOut') }}</div>
<ElButton @click="loginOut" link>退出系统</ElButton>
</ElDropdownItem>
</ElDropdownMenu>
</template>

View File

@ -1,108 +0,0 @@
import { useCache } from '@/hooks/web/useCache'
const { wsCache } = useCache()
export type LayoutType = 'classic' | 'topLeft' | 'top' | 'cutMenu'
export type ThemeTypes = {
elColorPrimary?: string
leftMenuBorderColor?: string
leftMenuBgColor?: string
leftMenuBgLightColor?: string
leftMenuBgActiveColor?: string
leftMenuCollapseBgActiveColor?: string
leftMenuTextColor?: string
leftMenuTextActiveColor?: string
logoTitleTextColor?: string
logoBorderColor?: string
topHeaderBgColor?: string
topHeaderTextColor?: string
topHeaderHoverColor?: string
topToolBorderColor?: string
}
export interface AppState {
breadcrumb: boolean
breadcrumbIcon: boolean
collapse: boolean
uniqueOpened: boolean
hamburger: boolean
screenfull: boolean
size: boolean
locale: boolean
tagsView: boolean
tagsViewIcon: boolean
logo: boolean
fixedHeader: boolean
greyMode: boolean
dynamicRouter: boolean
pageLoading: boolean
layout: LayoutType
title: string
userInfo: string
token: string
isDark: boolean
currentSize: ElememtPlusSize
sizeMap: ElememtPlusSize[]
mobile: boolean
footer: boolean
theme: ThemeTypes
}
export const appModules: AppState = {
userInfo: 'UserInfo', // 登录信息保存的存储字段名称-建议每个项目换一个字段,避免与其他项目冲突,方便后面直接使用这个名称来获取对应的登录用户信息
token: 'Token', // 存储Token字段
sizeMap: ['default', 'large', 'small'],
mobile: false, // 是否是移动端
title: import.meta.env.VITE_APP_TITLE, // 标题
pageLoading: true, // 路由跳转loading
breadcrumb: true, // 面包屑
breadcrumbIcon: true, // 面包屑图标
collapse: false, // 折叠菜单
uniqueOpened: true, // 是否只保持一个子菜单的展开
hamburger: true, // 折叠图标
screenfull: true, // 全屏图标
size: true, // 尺寸图标
locale: true, // 多语言图标
tagsView: true, // 标签页
tagsViewIcon: true, // 是否显示标签图标
logo: true, // logo
fixedHeader: true, // 固定toolheader
footer: true, // 显示页脚
greyMode: false, // 是否开始灰色模式,用于特殊悼念日
dynamicRouter: wsCache.get('dynamicRouter') || true, // 是否动态路由
layout: wsCache.get('layout') || 'classic', // layout布局
isDark: wsCache.get('isDark') || false, // 是否是暗黑模式
currentSize: wsCache.get('default') || 'default', // 组件尺寸
theme: wsCache.get('theme') || {
// 主题色
elColorPrimary: '#409eff',
// 左侧菜单边框颜色
leftMenuBorderColor: 'inherit',
// 左侧菜单背景颜色
leftMenuBgColor: '#001529',
// 左侧菜单浅色背景颜色
leftMenuBgLightColor: '#0f2438',
// 左侧菜单选中背景颜色
leftMenuBgActiveColor: 'var(--el-color-primary)',
// 左侧菜单收起选中背景颜色
leftMenuCollapseBgActiveColor: 'var(--el-color-primary)',
// 左侧菜单字体颜色
leftMenuTextColor: '#bfcbd9',
// 左侧菜单选中字体颜色
leftMenuTextActiveColor: '#fff',
// logo字体颜色
logoTitleTextColor: '#fff',
// logo边框颜色
logoBorderColor: 'inherit',
// 头部背景颜色
topHeaderBgColor: '#fff',
// 头部字体颜色
topHeaderTextColor: 'inherit',
// 头部悬停颜色
topHeaderHoverColor: '#f6f6f6',
// 头部边框颜色
topToolBorderColor: '#eee'
}
}

View File

@ -1,31 +1,8 @@
const config: {
base_url: {
base: string
dev: string
pro: string
test: string
}
result_code: number | string
default_headers: AxiosHeaders
request_timeout: number
} = {
/**
* api请求基础路径
*/
base_url: {
// 开发环境接口前缀
base: '/api',
// 打包开发环境接口前缀
dev: '/api',
// 打包生产环境接口前缀
pro: '/api',
// 打包测试环境接口前缀
test: '/api'
},
/**
*
*/

View File

@ -8,17 +8,15 @@ import { config } from './config'
import { ElMessage } from 'element-plus'
const { result_code, base_url } = config
const { result_code, request_timeout } = config
const appStore = useAppStore()
const { wsCache } = useCache()
export const PATH_URL = base_url[import.meta.env.VITE_API_BASEPATH]
// 创建axios实例
const service: AxiosInstance = axios.create({
baseURL: PATH_URL, // api 的 base_url
timeout: config.request_timeout, // 请求超时时间
baseURL: '/api', // api 的 base_url
timeout: request_timeout, // 请求超时时间
headers: {} // 请求头信息
})

View File

@ -1,32 +0,0 @@
import { useCache } from '@/hooks/web/useCache'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import en from 'element-plus/es/locale/lang/en'
const { wsCache } = useCache()
export const elLocaleMap = {
'zh-CN': zhCn,
en: en
}
export interface LocaleState {
currentLocale: LocaleDropdownType
localeMap: LocaleDropdownType[]
}
export const localeModules: LocaleState = {
currentLocale: {
lang: wsCache.get('lang') || 'zh-CN',
elLocale: elLocaleMap[wsCache.get('lang') || 'zh-CN']
},
// 多语言
localeMap: [
{
lang: 'zh-CN',
name: '简体中文'
},
{
lang: 'en',
name: 'English'
}
]
}

View File

@ -1,3 +1,4 @@
import { ConfigGlobalTypes } from '@/types/configGlobal'
import { inject } from 'vue'
export const useConfigGlobal = () => {

View File

@ -4,6 +4,9 @@ import { findIndex } from '@/utils'
import { useDictStoreWithOut } from '@/store/modules/dict'
import { useI18n } from '@/hooks/web/useI18n'
import type { AxiosPromise } from 'axios'
import { FormSchema } from '@/types/form'
import { TableColumn } from '@/types/table'
import { DescriptionsSchema } from '@/types/descriptions'
export type CrudSchema = Omit<TableColumn, 'children'> & {
search?: CrudSearchParams

View File

@ -2,6 +2,7 @@ import type { Form, FormExpose } from '@/components/Form'
import type { ElForm } from 'element-plus'
import { ref, unref, nextTick } from 'vue'
import type { FormProps } from '@/components/Form/src/types'
import { FormSchema, FormSetPropsType } from '@/types/form'
export const useForm = (props?: FormProps) => {
// From实例

View File

@ -1,6 +1,7 @@
import { h } from 'vue'
import type { VNode } from 'vue'
import { Icon } from '@/components/Icon'
import { IconTypes } from '@/types/icon'
export const useIcon = (props: IconTypes): VNode => {
return h(Icon, props)

View File

@ -4,6 +4,8 @@ import { ref, reactive, watch, computed, unref, nextTick } from 'vue'
import { get } from 'lodash-es'
import type { TableProps } from '@/components/Table/src/types'
import { useI18n } from '@/hooks/web/useI18n'
import { TableSetPropsType } from '@/types/table'
import { columns } from 'element-plus/es/components/table-v2/src/common'
const { t } = useI18n()
@ -15,6 +17,7 @@ interface TableResponse<T = any> {
interface UseTableConfig<T = any> {
getListApi: (option: any) => Promise<IResponse<TableResponse<T>>>
delListApi?: (option: any) => Promise<IResponse>
exportQueryListApi?: (params: any, data: any) => Promise<IResponse>
// 返回数据格式配置
response: {
data: string
@ -136,6 +139,7 @@ export const useTable = <T = any>(config?: UseTableConfig<T>) => {
},
// 与Search组件结合
setSearchParams: (data: Recordable) => {
tableObject.page = 1
tableObject.params = Object.assign(tableObject.params, {
limit: tableObject.limit,
page: tableObject.page,
@ -168,6 +172,31 @@ export const useTable = <T = any>(config?: UseTableConfig<T>) => {
} else {
await delData(ids)
}
},
// 导出筛选列表
exportQueryList: async () => {
if (config?.exportQueryListApi) {
const header = config?.props?.columns
?.filter((item) => item.show === true)
.map((item) => {
return { field: item.field, label: item.label }
})
tableObject.loading = true
const res = await config?.exportQueryListApi(unref(paramsObj), header).finally(() => {
tableObject.loading = false
})
if (res) {
const a = document.createElement('a')
a.style.display = 'none'
a.href = res.data.url
a.target = '_blank'
a.download = res.data.filename
const event = new MouseEvent('click')
a.dispatchEvent(event)
}
} else {
ElMessage.error('删除失败,请配置删除接口!')
}
}
}

View File

@ -48,25 +48,25 @@ export default defineComponent({
{layout.value !== 'top' ? (
<div class="h-full flex items-center">
{hamburger.value && layout.value !== 'cutMenu' ? (
<Collapse class="hover-tigger" color="var(--top-header-text-color)"></Collapse>
<Collapse class="hover-trigger" color="var(--top-header-text-color)"></Collapse>
) : undefined}
{breadcrumb.value ? <Breadcrumb class="<md:hidden"></Breadcrumb> : undefined}
</div>
) : undefined}
<div class="h-full flex items-center">
{screenfull.value ? (
<Screenfull class="hover-tigger" color="var(--top-header-text-color)"></Screenfull>
<Screenfull class="hover-trigger" color="var(--top-header-text-color)"></Screenfull>
) : undefined}
{size.value ? (
<SizeDropdown class="hover-tigger" color="var(--top-header-text-color)"></SizeDropdown>
<SizeDropdown class="hover-trigger" color="var(--top-header-text-color)"></SizeDropdown>
) : undefined}
{locale.value ? (
<LocaleDropdown
class="hover-tigger"
class="hover-trigger"
color="var(--top-header-text-color)"
></LocaleDropdown>
) : undefined}
<UserInfo class="hover-tigger"></UserInfo>
<UserInfo class="hover-trigger"></UserInfo>
</div>
</div>
)

View File

@ -32,6 +32,9 @@ const fixedHeader = computed(() => appStore.getFixedHeader)
// 是否是移动端
const mobile = computed(() => appStore.getMobile)
// 固定菜单
const fixedMenu = computed(() => appStore.getFixedMenu)
export const useRenderLayout = () => {
const renderClassic = () => {
return (
@ -107,7 +110,7 @@ export const useRenderLayout = () => {
return (
<>
<div class="flex items-center bg-[var(--top-header-bg-color)] border-bottom-1 border-solid border-[var(--top-tool-border-color)] dark:border-[var(--el-border-color)]">
{logo.value ? <Logo class="hover-tigger !pr-15px"></Logo> : undefined}
{logo.value ? <Logo class="hover-trigger !pr-15px"></Logo> : undefined}
<ToolHeader class="flex-1"></ToolHeader>
</div>
@ -164,7 +167,7 @@ export const useRenderLayout = () => {
return (
<>
<div class="flex items-center justify-between bg-[var(--top-header-bg-color)] border-bottom-1 border-solid border-[var(--top-tool-border-color)] dark:border-[var(--el-border-color)]">
{logo.value ? <Logo class="hover-tigger"></Logo> : undefined}
{logo.value ? <Logo class="hover-trigger"></Logo> : undefined}
<Menu class="flex-1 px-10px h-[var(--top-tool-height)]"></Menu>
<ToolHeader></ToolHeader>
</div>
@ -201,7 +204,7 @@ export const useRenderLayout = () => {
return (
<>
<div class="flex items-center bg-[var(--top-header-bg-color)] border-bottom-1 border-solid border-[var(--top-tool-border-color)] dark:border-[var(--el-border-color)]">
{logo.value ? <Logo class="hover-tigger !pr-15px"></Logo> : undefined}
{logo.value ? <Logo class="hover-trigger !pr-15px"></Logo> : undefined}
<ToolHeader class="flex-1"></ToolHeader>
</div>
@ -213,9 +216,13 @@ export const useRenderLayout = () => {
'h-[100%]',
{
'w-[calc(100%-var(--tab-menu-min-width))] left-[var(--tab-menu-min-width)]':
collapse.value,
collapse.value && !fixedMenu.value,
'w-[calc(100%-var(--tab-menu-max-width))] left-[var(--tab-menu-max-width)]':
!collapse.value
!collapse.value && !fixedMenu.value,
'w-[calc(100%-var(--tab-menu-min-width)-var(--left-menu-max-width))] ml-[var(--left-menu-max-width)]':
collapse.value && fixedMenu.value,
'w-[calc(100%-var(--tab-menu-max-width)-var(--left-menu-max-width))] ml-[var(--left-menu-max-width)]':
!collapse.value && fixedMenu.value
}
]}
style="transition: all var(--transition-time-02);"
@ -239,7 +246,13 @@ export const useRenderLayout = () => {
'w-[calc(100%-var(--tab-menu-min-width))] left-[var(--tab-menu-min-width)] mt-[var(--logo-height)]':
collapse.value && fixedHeader.value,
'w-[calc(100%-var(--tab-menu-max-width))] left-[var(--tab-menu-max-width)] mt-[var(--logo-height)]':
!collapse.value && fixedHeader.value
!collapse.value && fixedHeader.value,
'!fixed top-0 left-[var(--tab-menu-min-width)+var(--left-menu-max-width)] z-10':
fixedHeader.value && fixedMenu.value,
'w-[calc(100%-var(--tab-menu-min-width)-var(--left-menu-max-width))] left-[var(--tab-menu-min-width)+var(--left-menu-max-width)] mt-[var(--logo-height)]':
collapse.value && fixedHeader.value && fixedMenu.value,
'w-[calc(100%-var(--tab-menu-max-width)-var(--left-menu-max-width))] left-[var(--tab-menu-max-width)+var(--left-menu-max-width)] mt-[var(--logo-height)]':
!collapse.value && fixedHeader.value && fixedMenu.value
}
]}
style="transition: width var(--transition-time-02), left var(--transition-time-02);"

View File

@ -76,7 +76,8 @@ export default {
uniqueOpened: 'Unique opened',
tagsViewIcon: 'Tags view icon',
dynamicRouter: 'Dynamic router',
reExperienced: 'Please exit the login experience again'
reExperienced: 'Please exit the login experience again',
fixedMenu: 'fixed menu'
},
size: {
default: 'Default',

View File

@ -76,7 +76,8 @@ export default {
uniqueOpened: '菜单手风琴',
tagsViewIcon: '标签页图标',
dynamicRouter: '动态路由',
reExperienced: '请重新退出登录体验'
reExperienced: '请重新退出登录体验',
fixedMenu: '固定菜单'
},
size: {
default: '默认',
@ -186,7 +187,7 @@ export default {
happyDay: '祝你开心每一天!',
toady: '今日晴',
project: '项目数',
access: '项目访问',
access: '系统登录',
toDo: '待办',
introduction: '一个正经的简介',
more: '更多',

View File

@ -28,6 +28,8 @@ router.beforeEach(async (to, from, next) => {
if (wsCache.get(appStore.getUserInfo)) {
if (to.path === '/login') {
next({ path: '/' })
} else if (to.path === '/reset/password') {
next()
} else {
if (!authStore.getIsUser) {
await authStore.getUserInfo()
@ -37,16 +39,6 @@ router.beforeEach(async (to, from, next) => {
return
}
// 取消一次性获取所有字典,改为按需获取
// if (!dictStore.getIsSetDict) {
// 获取所有字典
// const res = await getDictApi()
// if (res) {
// dictStore.setDictObj(res.data)
// dictStore.setIsSetDict(true)
// }
// }
// 开发者可根据实际情况进行修改
const res = await getRoleMenusApi()
const { wsCache } = useCache()

View File

@ -1,4 +1,4 @@
import { createRouter, createWebHistory } from 'vue-router'
import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router'
import type { RouteRecordRaw } from 'vue-router'
import type { App } from 'vue'
import { Layout } from '@/utils/routerHelper'
@ -13,7 +13,9 @@ export const constantRouterMap: AppRouteRecordRaw[] = [
redirect: '/dashboard',
name: 'Root',
meta: {
hidden: true
hidden: true,
title: '首页',
noTagsView: true
}
},
{
@ -29,7 +31,7 @@ export const constantRouterMap: AppRouteRecordRaw[] = [
{
path: '/reset/password',
component: () => import('@/views/Reset/Reset.vue'),
name: 'Reset',
name: 'ResetPassword',
meta: {
hidden: true,
title: '重置密码',
@ -48,80 +50,20 @@ export const constantRouterMap: AppRouteRecordRaw[] = [
}
]
export const asyncRouterMap: AppRouteRecordRaw[] = [
// {
// path: '/dashboard',
// component: Layout,
// redirect: '/dashboard/analysis',
// name: 'Dashboard',
// meta: {
// title: t('router.dashboard'),
// icon: 'ant-design:dashboard-filled',
// alwaysShow: true
// },
// children: [
// {
// path: 'analysis',
// component: () => import('@/views/Dashboard/Analysis.vue'),
// name: 'Analysis',
// meta: {
// title: t('router.analysis'),
// noCache: true,
// affix: true
// }
// },
// {
// path: 'workplace',
// component: () => import('@/views/Dashboard/Workplace.vue'),
// name: 'Workplace',
// meta: {
// title: t('router.workplace'),
// noCache: true
// }
// }
// ]
// }
// {
// path: '/authorization',
// component: Layout,
// redirect: '/authorization/user',
// name: 'Authorization',
// meta: {
// title: t('router.authorization'),
// icon: 'eos-icons:role-binding',
// alwaysShow: true
// },
// children: [
// {
// path: 'user',
// component: () => import('@/views/Authorization/User.vue'),
// name: 'User',
// meta: {
// title: t('router.user')
// }
// },
// {
// path: 'role',
// component: () => import('@/views/Authorization/Role.vue'),
// name: 'Role',
// meta: {
// title: t('router.role')
// }
// }
// ]
// }
]
export const asyncRouterMap: AppRouteRecordRaw[] = []
const router = createRouter({
history: createWebHistory(), // HTML5 模式https://router.vuejs.org/zh/guide/essentials/history-mode.html#hash-%E6%A8%A1%E5%BC%8F
// history: createWebHashHistory(), // Hash 模式https://router.vuejs.org/zh/guide/essentials/history-mode.html#hash-%E6%A8%A1%E5%BC%8F
strict: true,
routes: constantRouterMap as RouteRecordRaw[],
scrollBehavior: () => ({ left: 0, top: 0 })
})
export const resetRouter = (): void => {
const resetWhiteNameList = ['Redirect', 'Login', 'NoFind', 'Root', 'Reset']
const resetWhiteNameList = ['Login', 'NoFind', 'Root', 'ResetPassword']
router.getRoutes().forEach((route) => {
// 切记 name 不能重复
const { name } = route
if (name && !resetWhiteNameList.includes(name as string)) {
router.hasRoute(name) && router.removeRoute(name)

View File

@ -1,19 +1,110 @@
import { defineStore } from 'pinia'
import { store } from '../index'
import { useCache } from '@/hooks/web/useCache'
import { appModules } from '@/config/app'
import type { AppState, LayoutType, ThemeTypes } from '@/config/app'
import { setCssVar, humpToUnderline } from '@/utils'
import { ElMessage } from 'element-plus'
import { ElementPlusSize } from '@/types/elementPlus'
import { useCache } from '@/hooks/web/useCache'
import { LayoutType } from '@/types/layout'
import { ThemeTypes } from '@/types/theme'
const { wsCache } = useCache()
export const useAppStore = defineStore({
id: 'app',
state: (): AppState => appModules,
persist: {
// 开启持久化存储
enabled: true
interface AppState {
breadcrumb: boolean
breadcrumbIcon: boolean
collapse: boolean
uniqueOpened: boolean
hamburger: boolean
screenfull: boolean
size: boolean
locale: boolean
tagsView: boolean
tagsViewIcon: boolean
logo: boolean
fixedHeader: boolean
greyMode: boolean
dynamicRouter: boolean
pageLoading: boolean
layout: LayoutType
title: string
userInfo: string
isDark: boolean
currentSize: ElementPlusSize
sizeMap: ElementPlusSize[]
mobile: boolean
footer: boolean
theme: ThemeTypes
fixedMenu: boolean
logoImage: string
footerContent: string
icpNumber: string
token: string
}
export const useAppStore = defineStore('app', {
state: (): AppState => {
return {
userInfo: 'userInfo', // 登录信息存储字段-建议每个项目换一个字段,避免与其他项目冲突
token: 'Token', // 存储Token字段
sizeMap: ['default', 'large', 'small'],
mobile: false, // 是否是移动端
title: import.meta.env.VITE_APP_TITLE, // 标题
pageLoading: false, // 路由跳转loading
breadcrumb: true, // 面包屑
breadcrumbIcon: true, // 面包屑图标
collapse: false, // 折叠菜单
uniqueOpened: true, // 是否只保持一个子菜单的展开
hamburger: true, // 折叠图标
screenfull: true, // 全屏图标
size: true, // 尺寸图标
locale: true, // 多语言图标
tagsView: true, // 标签页
tagsViewIcon: true, // 是否显示标签图标
logo: true, // 是否开启logo显示
logoImage: '', // logo图片
fixedHeader: true, // 固定toolheader
footer: true, // 显示页脚
footerContent: '', // 页脚内容
icpNumber: '', // 备案号
greyMode: false, // 是否开始灰色模式,用于特殊悼念日
dynamicRouter: wsCache.get('dynamicRouter') || true, // 是否动态路由
fixedMenu: wsCache.get('fixedMenu') || true, // 是否固定菜单
layout: wsCache.get('layout') || 'classic', // layout布局
isDark: wsCache.get('isDark') || false, // 是否是暗黑模式
currentSize: wsCache.get('default') || 'default', // 组件尺寸
theme: wsCache.get('theme') || {
// 主题色
elColorPrimary: '#409eff',
// 左侧菜单边框颜色
leftMenuBorderColor: 'inherit',
// 左侧菜单背景颜色
leftMenuBgColor: '#001529',
// 左侧菜单浅色背景颜色
leftMenuBgLightColor: '#0f2438',
// 左侧菜单选中背景颜色
leftMenuBgActiveColor: 'var(--el-color-primary)',
// 左侧菜单收起选中背景颜色
leftMenuCollapseBgActiveColor: 'var(--el-color-primary)',
// 左侧菜单字体颜色
leftMenuTextColor: '#bfcbd9',
// 左侧菜单选中字体颜色
leftMenuTextActiveColor: '#fff',
// logo字体颜色
logoTitleTextColor: '#fff',
// logo边框颜色
logoBorderColor: 'inherit',
// 头部背景颜色
topHeaderBgColor: '#fff',
// 头部字体颜色
topHeaderTextColor: 'inherit',
// 头部悬停颜色
topHeaderHoverColor: '#f6f6f6',
// 头部边框颜色
topToolBorderColor: '#eee'
}
}
},
getters: {
getBreadcrumb(): boolean {
@ -49,6 +140,9 @@ export const useAppStore = defineStore({
getLogo(): boolean {
return this.logo
},
getLogoImage(): string {
return this.logoImage
},
getFixedHeader(): boolean {
return this.fixedHeader
},
@ -58,6 +152,9 @@ export const useAppStore = defineStore({
getDynamicRouter(): boolean {
return this.dynamicRouter
},
getFixedMenu(): boolean {
return this.fixedMenu
},
getPageLoading(): boolean {
return this.pageLoading
},
@ -76,10 +173,10 @@ export const useAppStore = defineStore({
getIsDark(): boolean {
return this.isDark
},
getCurrentSize(): ElememtPlusSize {
getCurrentSize(): ElementPlusSize {
return this.currentSize
},
getSizeMap(): ElememtPlusSize[] {
getSizeMap(): ElementPlusSize[] {
return this.sizeMap
},
getMobile(): boolean {
@ -90,6 +187,12 @@ export const useAppStore = defineStore({
},
getFooter(): boolean {
return this.footer
},
getFooterContent(): string {
return this.footerContent
},
getIcpNumber(): string {
return this.icpNumber
}
},
actions: {
@ -126,6 +229,9 @@ export const useAppStore = defineStore({
setLogo(logo: boolean) {
this.logo = logo
},
setLogoImage(logoImage: string) {
this.logoImage = logoImage
},
setFixedHeader(fixedHeader: boolean) {
this.fixedHeader = fixedHeader
},
@ -136,6 +242,10 @@ export const useAppStore = defineStore({
wsCache.set('dynamicRouter', dynamicRouter)
this.dynamicRouter = dynamicRouter
},
setFixedMenu(fixedMenu: boolean) {
wsCache.set('fixedMenu', fixedMenu)
this.fixedMenu = fixedMenu
},
setPageLoading(pageLoading: boolean) {
this.pageLoading = pageLoading
},
@ -161,7 +271,7 @@ export const useAppStore = defineStore({
}
wsCache.set('isDark', this.isDark)
},
setCurrentSize(currentSize: ElememtPlusSize) {
setCurrentSize(currentSize: ElementPlusSize) {
this.currentSize = currentSize
wsCache.set('currentSize', this.currentSize)
},
@ -179,6 +289,12 @@ export const useAppStore = defineStore({
},
setFooter(footer: boolean) {
this.footer = footer
},
setFooterContent(footerContent: string) {
this.footerContent = footerContent
},
setIcpNumber(icpNumber: string) {
this.icpNumber = icpNumber
}
}
})

View File

@ -28,8 +28,7 @@ export interface AuthState {
isUser: boolean
}
export const useAuthStore = defineStore({
id: 'auth',
export const useAuthStore = defineStore('auth', {
state: (): AuthState => {
return {
user: {},
@ -64,8 +63,8 @@ export const useAuthStore = defineStore({
wsCache.clear()
this.user = {}
this.isUser = false
resetRouter()
tagsViewStore.delAllViews()
resetRouter()
},
updateUser(data: UserState) {
this.user.gender = data.gender

View File

@ -6,8 +6,7 @@ export interface DictState {
dictObj: Recordable
}
export const useDictStore = defineStore({
id: 'dict',
export const useDictStore = defineStore('dict', {
state: (): DictState => ({
dictObj: {}
}),

View File

@ -1,17 +1,40 @@
import { defineStore } from 'pinia'
import { store } from '../index'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import en from 'element-plus/es/locale/lang/en'
import { useCache } from '@/hooks/web/useCache'
import { localeModules, elLocaleMap } from '@/config/locale'
import type { LocaleState } from '@/config/locale'
import { LocaleDropdownType } from '@/types/localeDropdown'
const { wsCache } = useCache()
export const useLocaleStore = defineStore({
id: 'locales',
state: (): LocaleState => localeModules,
persist: {
// 开启持久化存储
enabled: true
const elLocaleMap = {
'zh-CN': zhCn,
en: en
}
interface LocaleState {
currentLocale: LocaleDropdownType
localeMap: LocaleDropdownType[]
}
export const useLocaleStore = defineStore('locales', {
state: (): LocaleState => {
return {
currentLocale: {
lang: wsCache.get('lang') || 'zh-CN',
elLocale: elLocaleMap[wsCache.get('lang') || 'zh-CN']
},
// 多语言
localeMap: [
{
lang: 'zh-CN',
name: '简体中文'
},
{
lang: 'en',
name: 'English'
}
]
}
},
getters: {
getCurrentLocale(): LocaleDropdownType {

View File

@ -11,8 +11,7 @@ export interface PermissionState {
menuTabRouters: AppRouteRecordRaw[]
}
export const usePermissionStore = defineStore({
id: 'permission',
export const usePermissionStore = defineStore('permission', {
state: (): PermissionState => ({
routers: [],
addRouters: [],

View File

@ -10,8 +10,7 @@ export interface TagsViewState {
cachedViews: Set<string>
}
export const useTagsViewStore = defineStore({
id: 'tagsView',
export const useTagsViewStore = defineStore('tagsView', {
state: (): TagsViewState => ({
visitedViews: [],
cachedViews: new Set()

54
kinit-admin/src/types/components.d.ts vendored Normal file
View File

@ -0,0 +1,54 @@
export type ComponentName =
| 'Radio'
| 'RadioButton'
| 'Checkbox'
| 'CheckboxButton'
| 'Input'
| 'Autocomplete'
| 'InputNumber'
| 'Select'
| 'Cascader'
| 'Switch'
| 'Slider'
| 'TimePicker'
| 'DatePicker'
| 'Rate'
| 'ColorPicker'
| 'Transfer'
| 'Divider'
| 'TimeSelect'
| 'SelectV2'
| 'InputPassword'
| 'TreeSelect'
| 'Tree'
| 'Text'
export type ColProps = {
span?: number
xs?: number
sm?: number
md?: number
lg?: number
xl?: number
tag?: string
}
export type ComponentOptions = {
label?: string
value?: FormValueType
disabled?: boolean
key?: string | number
children?: ComponentOptions[]
options?: ComponentOptions[]
} & Recordable
export type ComponentOptionsAlias = {
labelField?: string
valueField?: string
}
export type ComponentProps = {
optionsAlias?: ComponentOptionsAlias
options?: ComponentOptions[]
optionsSlot?: boolean
} & Recordable

View File

@ -0,0 +1,4 @@
import { ElementPlusSize } from './elementPlus'
export interface ConfigGlobalTypes {
size?: ElementPlusSize
}

View File

@ -0,0 +1,7 @@
export type contextMenuSchema = {
disabled?: boolean
divided?: boolean
icon?: string
label: string
command?: (item: contextMenuSchema) => void
}

View File

@ -1,4 +1,4 @@
declare interface DescriptionsSchema {
export interface DescriptionsSchema {
span?: number // 占多少分
field: string // 字段名
label?: string // label名

View File

@ -0,0 +1,3 @@
export type ElementPlusSize = 'default' | 'small' | 'large'
export type ElementPlusInfoType = 'success' | 'info' | 'warning' | 'danger'

47
kinit-admin/src/types/form.d.ts vendored Normal file
View File

@ -0,0 +1,47 @@
import type { CSSProperties } from 'vue'
import { ColProps, ComponentProps, ComponentName } from '@/types/components'
import { FormValueType, FormValueType } from '@/types/form'
import type { AxiosPromise } from 'axios'
export type FormSetPropsType = {
field: string
path: string
value: any
}
export type FormValueType = string | number | string[] | number[] | boolean | undefined | null
export type FormItemProps = {
labelWidth?: string | number
required?: boolean
rules?: Recordable
error?: string
showMessage?: boolean
inlineMessage?: boolean
style?: CSSProperties
}
export type FormSchema = {
// 唯一值
field: string
// 标题
label?: string
// 提示
labelMessage?: string
// col组件属性
colProps?: ColProps
// 表单组件属性slots对应的是表单组件的插槽规则${field}-xxx具体可以查看element-plus文档
componentProps?: { slots?: Recordable } & ComponentProps
// formItem组件属性
formItemProps?: FormItemProps
// 渲染的组件
component?: ComponentName
// 初始值
value?: FormValueType
// 是否隐藏隐藏后就不会在formModel中显示了不会获取到该字段的值
hidden?: boolean
// 是否显示隐藏后还会在formModel中显示可以获取到该字段的值
ifshow?: (values: Recordable) => boolean
// 远程加载下拉项
api?: <T = any>() => AxiosPromise<T>
}

View File

@ -1,4 +1,4 @@
declare interface IconTypes {
export interface IconTypes {
size?: number
color?: string
icon: string

View File

@ -1,4 +1,4 @@
declare interface TipSchema {
export interface TipSchema {
label: string
keys?: string[]
}

1
kinit-admin/src/types/layout.d.ts vendored Normal file
View File

@ -0,0 +1 @@
export type LayoutType = 'classic' | 'topLeft' | 'top' | 'cutMenu'

View File

@ -1,9 +1,9 @@
declare interface Language {
export interface Language {
el: Recordable
name: string
}
declare interface LocaleDropdownType {
export interface LocaleDropdownType {
lang: LocaleType
name?: string
elLocale?: Language

View File

@ -1,4 +1,4 @@
declare interface QrcodeLogo {
export interface QrcodeLogo {
src?: string
logoSize?: number
bgColor?: string

View File

@ -1,16 +1,16 @@
declare type TableColumn = {
export type TableColumn = {
field: string
label?: string
children?: TableColumn[]
} & Recordable
declare type TableSlotDefault = {
export type TableSlotDefault = {
row: Recordable
column: TableColumn
$index: number
} & Recordable
declare interface Pagination {
export interface Pagination {
small?: boolean
background?: boolean
pageSize?: number
@ -29,7 +29,7 @@ declare interface Pagination {
hideOnSinglePage?: boolean
}
declare interface TableSetPropsType {
export interface TableSetPropsType {
field: string
path: string
value: any

16
kinit-admin/src/types/theme.d.ts vendored Normal file
View File

@ -0,0 +1,16 @@
export type ThemeTypes = {
elColorPrimary?: string
leftMenuBorderColor?: string
leftMenuBgColor?: string
leftMenuBgLightColor?: string
leftMenuBgActiveColor?: string
leftMenuCollapseBgActiveColor?: string
leftMenuTextColor?: string
leftMenuTextActiveColor?: string
logoTitleTextColor?: string
logoBorderColor?: string
topHeaderBgColor?: string
topHeaderTextColor?: string
topHeaderHoverColor?: string
topToolBorderColor?: string
}

View File

@ -0,0 +1,53 @@
<script setup lang="ts">
import { ElCarousel, ElCarouselItem, ElImage, ElCard } from 'element-plus'
import Charts from './components/Charts.vue'
import { getBannersApi } from '@/api/dashboard/analysis'
import { ref } from 'vue'
import { AnalysisBannersTypes } from '@/api/dashboard/analysis/types'
const banners = ref([] as AnalysisBannersTypes[])
const loading = ref(false)
const getBanners = async () => {
loading.value = true
const res = await getBannersApi()
if (res) {
banners.value = res.data
}
loading.value = false
}
getBanners()
</script>
<template>
<div class="mb-20px" style="display: flex; justify-content: center; min-width: 1680px">
<ElCard shadow="never" class="w-1680px">
<!-- 添加 v-if 解决显示动态数据时默认会显示一个空白元素 -->
<ElCarousel v-loading="loading" v-if="banners.length > 0" height="500px" :interval="5000">
<ElCarouselItem v-for="item in banners" :key="item.id">
<ElImage :src="item.image" />
</ElCarouselItem>
</ElCarousel>
</ElCard>
</div>
<Charts />
</template>
<style scoped>
.el-carousel__item h3 {
display: flex;
color: #475669;
opacity: 0.75;
line-height: 300px;
margin: 0;
}
.el-carousel__item:nth-child(2n) {
background-color: #99a9bf;
}
.el-carousel__item:nth-child(2n + 1) {
background-color: #d3dce6;
}
</style>

View File

@ -6,8 +6,20 @@ import { ref, reactive } from 'vue'
import { CountTo } from '@/components/CountTo'
import { formatTime } from '@/utils'
import { Highlight } from '@/components/Highlight'
import { getCountApi, getProjectApi, getDynamicApi, getTeamApi } from '@/api/dashboard/workplace'
import type { WorkplaceTotal, Project, Dynamic, Team } from '@/api/dashboard/workplace/types'
import {
getCountApi,
getProjectApi,
getDynamicApi,
getTeamApi,
getShortcutsApi
} from '@/api/dashboard/workplace'
import type {
WorkplaceTotal,
Project,
Dynamic,
Team,
Shortcuts
} from '@/api/dashboard/workplace/types'
import { useCache } from '@/hooks/web/useCache'
import { useAppStoreWithOut } from '@/store/modules/app'
@ -15,6 +27,10 @@ const { wsCache } = useCache()
const loading = ref(true)
const toLink = (link: string) => {
window.open(link)
}
//
let totalSate = reactive<WorkplaceTotal>({
project: 0,
@ -39,6 +55,18 @@ const getProject = async () => {
}
}
let shortcuts = reactive<Shortcuts[]>([])
//
const getShortcuts = async () => {
const res = await getShortcutsApi().catch(() => {})
if (res) {
shortcuts = Object.assign(shortcuts, res.data)
}
}
getShortcuts()
//
let dynamics = reactive<Dynamic[]>([])
@ -154,6 +182,7 @@ const user = wsCache.get(appStore.getUserInfo)
:xs="24"
>
<ElCard shadow="hover">
<div class="cursor-pointer" @click="toLink(item.link)">
<div class="flex items-center">
<Icon :icon="item.icon" :size="25" class="mr-10px" />
<span class="text-16px">{{ item.name }}</span>
@ -163,6 +192,7 @@ const user = wsCache.get(appStore.getUserInfo)
<span>{{ item.personal }}</span>
<span>{{ formatTime(item.time, 'yyyy-MM-dd') }}</span>
</div>
</div>
</ElCard>
</ElCol>
</ElRow>
@ -206,9 +236,18 @@ const user = wsCache.get(appStore.getUserInfo)
<span>{{ t('workplace.shortcutOperation') }}</span>
</template>
<ElSkeleton :loading="loading" animated>
<ElCol :xl="12" :lg="12" :md="12" :sm="24" :xs="24" class="mb-10px">
<ElLink type="default" :underline="false">
{{ t('workplace.operation') }}
<ElCol
v-for="(item, index) in shortcuts"
:key="`card-${index}`"
:xl="12"
:lg="12"
:md="12"
:sm="12"
:xs="12"
class="mb-10px"
>
<ElLink type="primary" :href="item.link" target="_blank" :underline="false">
{{ item.name }}
</ElLink>
</ElCol>
</ElSkeleton>

View File

@ -0,0 +1,125 @@
<script setup lang="ts">
import { ElRow, ElCol, ElCard, ElSkeleton } from 'element-plus'
import { Echart } from '@/components/Echart'
import { pieOptions, barOptions, lineOptions } from './echarts-data'
import { ref, reactive } from 'vue'
import {
getUserAccessSourceApi,
getWeeklyUserActivityApi,
getMonthlySalesApi
} from '@/api/dashboard/analysis'
import { set } from 'lodash-es'
import { EChartsOption } from 'echarts'
import { useI18n } from '@/hooks/web/useI18n'
const { t } = useI18n()
const loading = ref(true)
const pieOptionsData = reactive<EChartsOption>(pieOptions) as EChartsOption
//
const getUserAccessSource = async () => {
const res = await getUserAccessSourceApi().catch(() => {})
if (res) {
set(
pieOptionsData,
'legend.data',
res.data.map((v) => t(v.name))
)
pieOptionsData!.series![0].data = res.data.map((v) => {
return {
name: t(v.name),
value: v.value
}
})
}
}
const barOptionsData = reactive<EChartsOption>(barOptions) as EChartsOption
//
const getWeeklyUserActivity = async () => {
const res = await getWeeklyUserActivityApi().catch(() => {})
if (res) {
set(
barOptionsData,
'xAxis.data',
res.data.map((v) => t(v.name))
)
set(barOptionsData, 'series', [
{
name: t('analysis.activeQuantity'),
data: res.data.map((v) => v.value),
type: 'bar'
}
])
}
}
const lineOptionsData = reactive<EChartsOption>(lineOptions) as EChartsOption
//
const getMonthlySales = async () => {
const res = await getMonthlySalesApi().catch(() => {})
if (res) {
set(
lineOptionsData,
'xAxis.data',
res.data.map((v) => t(v.name))
)
set(lineOptionsData, 'series', [
{
name: t('analysis.estimate'),
smooth: true,
type: 'line',
data: res.data.map((v) => v.estimate),
animationDuration: 2800,
animationEasing: 'cubicInOut'
},
{
name: t('analysis.actual'),
smooth: true,
type: 'line',
itemStyle: {},
data: res.data.map((v) => v.actual),
animationDuration: 2800,
animationEasing: 'quadraticOut'
}
])
}
}
const getAllApi = async () => {
await Promise.all([getUserAccessSource(), getWeeklyUserActivity(), getMonthlySales()])
loading.value = false
}
getAllApi()
</script>
<template>
<ElRow :gutter="20" justify="space-between">
<ElCol :xl="10" :lg="10" :md="24" :sm="24" :xs="24">
<ElCard shadow="hover" class="mb-20px">
<ElSkeleton :loading="loading" animated>
<Echart :options="pieOptionsData" :height="300" />
</ElSkeleton>
</ElCard>
</ElCol>
<ElCol :xl="14" :lg="14" :md="24" :sm="24" :xs="24">
<ElCard shadow="hover" class="mb-20px">
<ElSkeleton :loading="loading" animated>
<Echart :options="barOptionsData" :height="300" />
</ElSkeleton>
</ElCard>
</ElCol>
<ElCol :span="24">
<ElCard shadow="hover" class="mb-20px">
<ElSkeleton :loading="loading" animated :rows="4">
<Echart :options="lineOptionsData" :height="350" />
</ElSkeleton>
</ElCard>
</ElCol>
</ElRow>
</template>

View File

@ -0,0 +1,310 @@
import { EChartsOption } from 'echarts'
import { EChartsOption as EChartsWordOption } from 'echarts-wordcloud'
import { useI18n } from '@/hooks/web/useI18n'
const { t } = useI18n()
export const lineOptions: EChartsOption = {
title: {
text: t('analysis.monthlySales'),
left: 'center'
},
xAxis: {
data: [
t('analysis.january'),
t('analysis.february'),
t('analysis.march'),
t('analysis.april'),
t('analysis.may'),
t('analysis.june'),
t('analysis.july'),
t('analysis.august'),
t('analysis.september'),
t('analysis.october'),
t('analysis.november'),
t('analysis.december')
],
boundaryGap: false,
axisTick: {
show: false
}
},
grid: {
left: 20,
right: 20,
bottom: 20,
top: 80,
containLabel: true
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross'
},
padding: [5, 10]
},
yAxis: {
axisTick: {
show: false
}
},
legend: {
data: [t('analysis.estimate'), t('analysis.actual')],
top: 50
},
series: [
{
name: t('analysis.estimate'),
smooth: true,
type: 'line',
data: [100, 120, 161, 134, 105, 160, 165, 114, 163, 185, 118, 123],
animationDuration: 2800,
animationEasing: 'cubicInOut'
},
{
name: t('analysis.actual'),
smooth: true,
type: 'line',
itemStyle: {},
data: [120, 82, 91, 154, 162, 140, 145, 250, 134, 56, 99, 123],
animationDuration: 2800,
animationEasing: 'quadraticOut'
}
]
}
export const pieOptions: EChartsOption = {
title: {
text: t('analysis.userAccessSource'),
left: 'center'
},
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b} : {c} ({d}%)'
},
legend: {
orient: 'vertical',
left: 'left',
data: [
t('analysis.directAccess'),
t('analysis.mailMarketing'),
t('analysis.allianceAdvertising'),
t('analysis.videoAdvertising'),
t('analysis.searchEngines')
]
},
series: [
{
name: t('analysis.userAccessSource'),
type: 'pie',
radius: '55%',
center: ['50%', '60%'],
data: [
{ value: 335, name: t('analysis.directAccess') },
{ value: 310, name: t('analysis.mailMarketing') },
{ value: 234, name: t('analysis.allianceAdvertising') },
{ value: 135, name: t('analysis.videoAdvertising') },
{ value: 1548, name: t('analysis.searchEngines') }
]
}
]
}
export const barOptions: EChartsOption = {
title: {
text: t('analysis.weeklyUserActivity'),
left: 'center'
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
grid: {
left: 50,
right: 20,
bottom: 20
},
xAxis: {
type: 'category',
data: [
t('analysis.monday'),
t('analysis.tuesday'),
t('analysis.wednesday'),
t('analysis.thursday'),
t('analysis.friday'),
t('analysis.saturday'),
t('analysis.sunday')
],
axisTick: {
alignWithLabel: true
}
},
yAxis: {
type: 'value'
},
series: [
{
name: t('analysis.activeQuantity'),
data: [13253, 34235, 26321, 12340, 24643, 1322, 1324],
type: 'bar'
}
]
}
export const radarOption: EChartsOption = {
legend: {
data: [t('workplace.personal'), t('workplace.team')]
},
radar: {
// shape: 'circle',
indicator: [
{ name: t('workplace.quote'), max: 65 },
{ name: t('workplace.contribution'), max: 160 },
{ name: t('workplace.hot'), max: 300 },
{ name: t('workplace.yield'), max: 130 },
{ name: t('workplace.follow'), max: 100 }
]
},
series: [
{
name: `xxx${t('workplace.index')}`,
type: 'radar',
data: [
{
value: [42, 30, 20, 35, 80],
name: t('workplace.personal')
},
{
value: [50, 140, 290, 100, 90],
name: t('workplace.team')
}
]
}
]
}
export const wordOptions: EChartsWordOption = {
series: [
{
type: 'wordCloud',
gridSize: 2,
sizeRange: [12, 50],
rotationRange: [-90, 90],
shape: 'pentagon',
width: 600,
height: 400,
drawOutOfBound: true,
textStyle: {
color: function () {
return (
'rgb(' +
[
Math.round(Math.random() * 160),
Math.round(Math.random() * 160),
Math.round(Math.random() * 160)
].join(',') +
')'
)
}
},
emphasis: {
textStyle: {
shadowBlur: 10,
shadowColor: '#333'
}
},
data: [
{
name: 'Sam S Club',
value: 10000,
textStyle: {
color: 'black'
},
emphasis: {
textStyle: {
color: 'red'
}
}
},
{
name: 'Macys',
value: 6181
},
{
name: 'Amy Schumer',
value: 4386
},
{
name: 'Jurassic World',
value: 4055
},
{
name: 'Charter Communications',
value: 2467
},
{
name: 'Chick Fil A',
value: 2244
},
{
name: 'Planet Fitness',
value: 1898
},
{
name: 'Pitch Perfect',
value: 1484
},
{
name: 'Express',
value: 1112
},
{
name: 'Home',
value: 965
},
{
name: 'Johnny Depp',
value: 847
},
{
name: 'Lena Dunham',
value: 582
},
{
name: 'Lewis Hamilton',
value: 555
},
{
name: 'KXAN',
value: 550
},
{
name: 'Mary Ellen Mark',
value: 462
},
{
name: 'Farrah Abraham',
value: 366
},
{
name: 'Rita Ora',
value: 360
},
{
name: 'Serena Williams',
value: 282
},
{
name: 'NCAA baseball tournament',
value: 273
},
{
name: 'Point Break',
value: 265
}
]
}
]
}

View File

@ -0,0 +1,310 @@
import { EChartsOption } from 'echarts'
import { EChartsOption as EChartsWordOption } from 'echarts-wordcloud'
import { useI18n } from '@/hooks/web/useI18n'
const { t } = useI18n()
export const lineOptions: EChartsOption = {
title: {
text: t('analysis.monthlySales'),
left: 'center'
},
xAxis: {
data: [
t('analysis.january'),
t('analysis.february'),
t('analysis.march'),
t('analysis.april'),
t('analysis.may'),
t('analysis.june'),
t('analysis.july'),
t('analysis.august'),
t('analysis.september'),
t('analysis.october'),
t('analysis.november'),
t('analysis.december')
],
boundaryGap: false,
axisTick: {
show: false
}
},
grid: {
left: 20,
right: 20,
bottom: 20,
top: 80,
containLabel: true
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross'
},
padding: [5, 10]
},
yAxis: {
axisTick: {
show: false
}
},
legend: {
data: [t('analysis.estimate'), t('analysis.actual')],
top: 50
},
series: [
{
name: t('analysis.estimate'),
smooth: true,
type: 'line',
data: [100, 120, 161, 134, 105, 160, 165, 114, 163, 185, 118, 123],
animationDuration: 2800,
animationEasing: 'cubicInOut'
},
{
name: t('analysis.actual'),
smooth: true,
type: 'line',
itemStyle: {},
data: [120, 82, 91, 154, 162, 140, 145, 250, 134, 56, 99, 123],
animationDuration: 2800,
animationEasing: 'quadraticOut'
}
]
}
export const pieOptions: EChartsOption = {
title: {
text: t('analysis.userAccessSource'),
left: 'center'
},
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b} : {c} ({d}%)'
},
legend: {
orient: 'vertical',
left: 'left',
data: [
t('analysis.directAccess'),
t('analysis.mailMarketing'),
t('analysis.allianceAdvertising'),
t('analysis.videoAdvertising'),
t('analysis.searchEngines')
]
},
series: [
{
name: t('analysis.userAccessSource'),
type: 'pie',
radius: '55%',
center: ['50%', '60%'],
data: [
{ value: 335, name: t('analysis.directAccess') },
{ value: 310, name: t('analysis.mailMarketing') },
{ value: 234, name: t('analysis.allianceAdvertising') },
{ value: 135, name: t('analysis.videoAdvertising') },
{ value: 1548, name: t('analysis.searchEngines') }
]
}
]
}
export const barOptions: EChartsOption = {
title: {
text: t('analysis.weeklyUserActivity'),
left: 'center'
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
grid: {
left: 50,
right: 20,
bottom: 20
},
xAxis: {
type: 'category',
data: [
t('analysis.monday'),
t('analysis.tuesday'),
t('analysis.wednesday'),
t('analysis.thursday'),
t('analysis.friday'),
t('analysis.saturday'),
t('analysis.sunday')
],
axisTick: {
alignWithLabel: true
}
},
yAxis: {
type: 'value'
},
series: [
{
name: t('analysis.activeQuantity'),
data: [13253, 34235, 26321, 12340, 24643, 1322, 1324],
type: 'bar'
}
]
}
export const radarOption: EChartsOption = {
legend: {
data: [t('workplace.personal'), t('workplace.team')]
},
radar: {
// shape: 'circle',
indicator: [
{ name: t('workplace.quote'), max: 65 },
{ name: t('workplace.contribution'), max: 160 },
{ name: t('workplace.hot'), max: 300 },
{ name: t('workplace.yield'), max: 130 },
{ name: t('workplace.follow'), max: 100 }
]
},
series: [
{
name: `xxx${t('workplace.index')}`,
type: 'radar',
data: [
{
value: [42, 30, 20, 35, 80],
name: t('workplace.personal')
},
{
value: [50, 140, 290, 100, 90],
name: t('workplace.team')
}
]
}
]
}
export const wordOptions: EChartsWordOption = {
series: [
{
type: 'wordCloud',
gridSize: 2,
sizeRange: [12, 50],
rotationRange: [-90, 90],
shape: 'pentagon',
width: 600,
height: 400,
drawOutOfBound: true,
textStyle: {
color: function () {
return (
'rgb(' +
[
Math.round(Math.random() * 160),
Math.round(Math.random() * 160),
Math.round(Math.random() * 160)
].join(',') +
')'
)
}
},
emphasis: {
textStyle: {
shadowBlur: 10,
shadowColor: '#333'
}
},
data: [
{
name: 'Sam S Club',
value: 10000,
textStyle: {
color: 'black'
},
emphasis: {
textStyle: {
color: 'red'
}
}
},
{
name: 'Macys',
value: 6181
},
{
name: 'Amy Schumer',
value: 4386
},
{
name: 'Jurassic World',
value: 4055
},
{
name: 'Charter Communications',
value: 2467
},
{
name: 'Chick Fil A',
value: 2244
},
{
name: 'Planet Fitness',
value: 1898
},
{
name: 'Pitch Perfect',
value: 1484
},
{
name: 'Express',
value: 1112
},
{
name: 'Home',
value: 965
},
{
name: 'Johnny Depp',
value: 847
},
{
name: 'Lena Dunham',
value: 582
},
{
name: 'Lewis Hamilton',
value: 555
},
{
name: 'KXAN',
value: 550
},
{
name: 'Mary Ellen Mark',
value: 462
},
{
name: 'Farrah Abraham',
value: 366
},
{
name: 'Rita Ora',
value: 360
},
{
name: 'Serena Williams',
value: 282
},
{
name: 'NCAA baseball tournament',
value: 273
},
{
name: 'Point Break',
value: 265
}
]
}
]
}

View File

@ -81,6 +81,9 @@ const loading = ref(false)
//
const save = async () => {
if (authStore.getUser.id === 1) {
return ElMessage.warning('编辑账号为演示账号,无权限操作!')
}
const formRef = unref(elFormRef)
await formRef?.validate(async (isValid) => {
if (isValid) {

View File

@ -75,6 +75,9 @@ const loading = ref(false)
//
const save = async () => {
if (authStore.getUser.id === 1) {
return ElMessage.warning('编辑账号为演示账号,无权限操作!')
}
const formRef = unref(elFormRef)
await formRef?.validate(async (isValid) => {
if (isValid) {

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