mirror of
https://github.com/soybeanjs/soybean-admin.git
synced 2025-10-14 13:53:41 +08:00
Compare commits
47 Commits
thin-v0.10
...
v0.10.4
Author | SHA1 | Date | |
---|---|---|---|
|
ead48f4502 | ||
|
305d95672a | ||
|
8a792c7d63 | ||
|
93ed5ad085 | ||
|
41f23386b2 | ||
|
c91644b829 | ||
|
073fd16bd7 | ||
|
f92ee770e0 | ||
|
1e6d52357e | ||
|
751ded44f3 | ||
|
8567f3e34e | ||
|
83f2514403 | ||
|
ad6ac7222c | ||
|
3ae1952624 | ||
|
3db549af40 | ||
|
94179ae552 | ||
|
7f35e87ed8 | ||
|
00da0009ef | ||
|
cffc30afa3 | ||
|
24cf1d9284 | ||
|
9296e6987d | ||
|
809fa85706 | ||
|
b3ae7605d3 | ||
|
864ec4737d | ||
|
854d0bcf20 | ||
|
458e387b68 | ||
|
56c770c49d | ||
|
946447394d | ||
|
44ba3273cb | ||
|
0f7b9d5e2b | ||
|
8a3f66db7b | ||
|
0eaa327d47 | ||
|
08e0cf5ad5 | ||
|
d7aea9d11c | ||
|
135ce77288 | ||
|
19141a73d2 | ||
|
9d1051b0bd | ||
|
c46a5920e5 | ||
|
43ac23f113 | ||
|
13f6cd8ef4 | ||
|
0e6d289128 | ||
|
bba68bff29 | ||
|
6e0cce4d49 | ||
|
d3ebe95076 | ||
|
cbda4a38a3 | ||
|
3318041b92 | ||
|
af53ec7625 |
10
.env
10
.env
@@ -10,11 +10,11 @@ VITE_APP_DESC=SoybeanAdmin是一个中后台管理系统模版
|
|||||||
VITE_AUTH_ROUTE_MODE=static
|
VITE_AUTH_ROUTE_MODE=static
|
||||||
|
|
||||||
# 路由首页(根路由重定向), 用于static模式的权限路由,dynamic模式取决于后端返回的路由首页
|
# 路由首页(根路由重定向), 用于static模式的权限路由,dynamic模式取决于后端返回的路由首页
|
||||||
VITE_ROUTE_HOME_PATH=/multi-menu/first/second
|
VITE_ROUTE_HOME_PATH=/dashboard/analysis
|
||||||
|
|
||||||
# iconify图标作为组件的前缀
|
# iconify图标作为组件的前缀
|
||||||
VITE_ICON_PREFFIX=icon
|
VITE_ICON_PREFIX=icon
|
||||||
|
|
||||||
# 本地SVG图标作为组件的前缀, 请注意一定要包含 VITE_ICON_PREFFIX
|
# 本地SVG图标作为组件的前缀, 请注意一定要包含 VITE_ICON_PREFIX
|
||||||
# 格式 {VITE_ICON_PREFFIX}-{本地图标集合名称}
|
# 格式 {VITE_ICON_PREFIX}-{本地图标集合名称}
|
||||||
VITE_ICON_LOCAL_PREFFIX=icon-local
|
VITE_ICON_LOCAL_PREFIX=icon-local
|
||||||
|
@@ -1 +1,10 @@
|
|||||||
|
VITE_VISUALIZER=N
|
||||||
|
|
||||||
|
VITE_COMPRESS=N
|
||||||
|
|
||||||
|
# gzip | brotliCompress | deflate | deflateRaw
|
||||||
|
VITE_COMPRESS_TYPE=gzip
|
||||||
|
|
||||||
|
VITE_PWA=N
|
||||||
|
|
||||||
VITE_PROD_MOCK=Y
|
VITE_PROD_MOCK=Y
|
||||||
|
@@ -1,4 +1,3 @@
|
|||||||
!.env-config.ts
|
!.env-config.ts
|
||||||
components.d.ts
|
|
||||||
router-page.d.ts
|
router-page.d.ts
|
||||||
*.svg
|
|
||||||
|
@@ -10,7 +10,8 @@ module.exports = {
|
|||||||
{
|
{
|
||||||
files: ['*.vue'],
|
files: ['*.vue'],
|
||||||
rules: {
|
rules: {
|
||||||
'no-undef': 'off' // use tsc to check the ts code of the vue
|
'no-undef': 'off', // use tsc to check the ts code of the vue
|
||||||
|
'vue/no-setup-props-destructure': 'off' // wait to fix this rule
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -34,3 +34,4 @@ stats.html
|
|||||||
/src/typings/components.d.ts
|
/src/typings/components.d.ts
|
||||||
package-lock.json
|
package-lock.json
|
||||||
yarn.lock
|
yarn.lock
|
||||||
|
pnpm-lock.yaml
|
||||||
|
14
.vscode/extensions.json
vendored
14
.vscode/extensions.json
vendored
@@ -1,27 +1,19 @@
|
|||||||
{
|
{
|
||||||
"recommendations": [
|
"recommendations": [
|
||||||
"afzalsayed96.icones",
|
|
||||||
"antfu.iconify",
|
|
||||||
"antfu.unocss",
|
"antfu.unocss",
|
||||||
"christian-kohler.path-intellisense",
|
|
||||||
"dbaeumer.vscode-eslint",
|
"dbaeumer.vscode-eslint",
|
||||||
"eamodio.gitlens",
|
|
||||||
"editorconfig.editorconfig",
|
"editorconfig.editorconfig",
|
||||||
"esbenp.prettier-vscode",
|
"esbenp.prettier-vscode",
|
||||||
"formulahendry.auto-close-tag",
|
|
||||||
"formulahendry.auto-complete-tag",
|
"formulahendry.auto-complete-tag",
|
||||||
|
"formulahendry.auto-close-tag",
|
||||||
"formulahendry.auto-rename-tag",
|
"formulahendry.auto-rename-tag",
|
||||||
"kisstkondoros.vscode-gutter-preview",
|
"kisstkondoros.vscode-gutter-preview",
|
||||||
"lokalise.i18n-ally",
|
"lokalise.i18n-ally",
|
||||||
"mariusalchimavicius.json-to-ts",
|
"mariusalchimavicius.json-to-ts",
|
||||||
"mhutchie.git-graph",
|
"mhutchie.git-graph",
|
||||||
"mikestead.dotenv",
|
|
||||||
"naumovs.color-highlight",
|
|
||||||
"pkief.material-icon-theme",
|
|
||||||
"sdras.vue-vscode-snippets",
|
"sdras.vue-vscode-snippets",
|
||||||
|
"streetsidesoftware.code-spell-checker",
|
||||||
"vue.volar",
|
"vue.volar",
|
||||||
"vue.vscode-typescript-vue-plugin",
|
"vue.vscode-typescript-vue-plugin"
|
||||||
"whtouche.vscode-js-console-utils",
|
|
||||||
"zhuangtongfa.material-theme"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
123
.vscode/settings.json
vendored
123
.vscode/settings.json
vendored
@@ -1,75 +1,76 @@
|
|||||||
{
|
{
|
||||||
|
"cSpell.ignorePaths": [
|
||||||
|
"package.json",
|
||||||
|
"package-lock.json",
|
||||||
|
"yarn.lock",
|
||||||
|
"pnpm-lock.yaml",
|
||||||
|
"node_modules",
|
||||||
|
"vscode-extension",
|
||||||
|
".git/objects",
|
||||||
|
".vscode",
|
||||||
|
".vscode-insiders",
|
||||||
|
"CHANGELOG.md",
|
||||||
|
"dist",
|
||||||
|
"public",
|
||||||
|
"styles"
|
||||||
|
],
|
||||||
|
"cSpell.words": [
|
||||||
|
"AMAP",
|
||||||
|
"antdesign",
|
||||||
|
"antv",
|
||||||
|
"apacheecharts",
|
||||||
|
"areaspline",
|
||||||
|
"bmapgl",
|
||||||
|
"colord",
|
||||||
|
"echarts",
|
||||||
|
"gitee",
|
||||||
|
"gridicons",
|
||||||
|
"iconify",
|
||||||
|
"jsapi",
|
||||||
|
"naiveui",
|
||||||
|
"Popconfirm",
|
||||||
|
"Posva",
|
||||||
|
"Shenzhen",
|
||||||
|
"Sider",
|
||||||
|
"tauri",
|
||||||
|
"unocss",
|
||||||
|
"unplugin",
|
||||||
|
"vditor",
|
||||||
|
"VERCEL",
|
||||||
|
"Vite",
|
||||||
|
"vitejs",
|
||||||
|
"vuedraggable",
|
||||||
|
"vueuse",
|
||||||
|
"wangeditor",
|
||||||
|
"wechat",
|
||||||
|
"xgplayer",
|
||||||
|
"yanbowe",
|
||||||
|
"ភាសាខ្មែរ"
|
||||||
|
],
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll.eslint": true
|
"source.fixAll.eslint": true
|
||||||
},
|
},
|
||||||
"editor.fontLigatures": true,
|
"editor.fontLigatures": true,
|
||||||
"editor.formatOnSave": false,
|
"editor.formatOnSave": false,
|
||||||
"editor.guides.bracketPairs": "active",
|
|
||||||
"editor.quickSuggestions": {
|
"editor.quickSuggestions": {
|
||||||
"strings": true
|
"strings": true
|
||||||
},
|
},
|
||||||
"editor.tabSize": 2,
|
"editor.tabSize": 2,
|
||||||
"eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact", "vue", "json"],
|
"eslint.validate": ["json"],
|
||||||
"files.associations": {
|
"files.associations": {
|
||||||
"*.env.*": "dotenv"
|
"*.env.*": "dotenv",
|
||||||
|
"*.svg": "html"
|
||||||
},
|
},
|
||||||
"files.eol": "\n",
|
"files.eol": "\n",
|
||||||
"git.enableSmartCommit": true,
|
"i18n-ally.displayLanguage": "zh-CN",
|
||||||
"gutterpreview.paths": {
|
"i18n-ally.enabledParsers": ["ts"],
|
||||||
"@": "/src",
|
"i18n-ally.enabledFrameworks": ["vue"],
|
||||||
"~@": "/src"
|
"i18n-ally.editor.preferEditor": true,
|
||||||
},
|
"i18n-ally.keystyle": "nested",
|
||||||
"material-icon-theme.activeIconPack": "angular",
|
"i18n-ally.localesPaths": ["src/locales/lang"],
|
||||||
"material-icon-theme.files.associations": {},
|
"material-icon-theme.activeIconPack": "vue",
|
||||||
"material-icon-theme.folders.associations": {
|
"[html][css][less][scss][sass][markdown][yaml][yml][jsonc]": {
|
||||||
"src-tauri": "src",
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
"enum": "typescript",
|
"editor.formatOnSave": true
|
||||||
"enums": "typescript",
|
}
|
||||||
"store": "context",
|
|
||||||
"stores": "context",
|
|
||||||
"composable": "hook",
|
|
||||||
"composables": "hook",
|
|
||||||
"directive": "tools",
|
|
||||||
"directives": "tools",
|
|
||||||
"business": "core",
|
|
||||||
"request": "api",
|
|
||||||
"adapter": "middleware"
|
|
||||||
},
|
|
||||||
"path-intellisense.mappings": {
|
|
||||||
"@": "${workspaceFolder}/src",
|
|
||||||
"~@": "${workspaceFolder}/src"
|
|
||||||
},
|
|
||||||
"terminal.integrated.fontSize": 14,
|
|
||||||
"terminal.integrated.fontWeight": 500,
|
|
||||||
"terminal.integrated.tabs.enabled": true,
|
|
||||||
"workbench.iconTheme": "material-icon-theme",
|
|
||||||
"workbench.colorTheme": "One Dark Pro",
|
|
||||||
"[html]": {
|
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
|
||||||
},
|
|
||||||
"[json]": {
|
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
|
||||||
},
|
|
||||||
"[jsonc]": {
|
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
|
||||||
},
|
|
||||||
"[javascript]": {
|
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
|
||||||
},
|
|
||||||
"[javascriptreact]": {
|
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
|
||||||
},
|
|
||||||
"[markdown]": {
|
|
||||||
"editor.defaultFormatter": "yzhang.markdown-all-in-one"
|
|
||||||
},
|
|
||||||
"[typescript]": {
|
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
|
||||||
},
|
|
||||||
"[typescriptreact]": {
|
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
|
||||||
},
|
|
||||||
"[vue]": {
|
|
||||||
"editor.defaultFormatter": "Vue.volar"
|
|
||||||
},
|
|
||||||
"i18n-ally.localesPaths": ["src/locales", "src/locales/lang"]
|
|
||||||
}
|
}
|
||||||
|
1275
CHANGELOG.md
Normal file
1275
CHANGELOG.md
Normal file
File diff suppressed because it is too large
Load Diff
16
Makefile
Normal file
16
Makefile
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
ImageTag ?=v0.9.6
|
||||||
|
SoybeanAdminImg ?= soybeanjs/soybean-admin:$(ImageTag)
|
||||||
|
|
||||||
|
VERSION=$(shell git rev-parse --short HEAD)
|
||||||
|
|
||||||
|
soybean-admin: soybean-admin-build soybean-admin-push
|
||||||
|
|
||||||
|
soybean-admin-build:
|
||||||
|
docker build --build-arg version=$(VERSION) -t ${SoybeanAdminImg} -f docker/Dockerfile .
|
||||||
|
|
||||||
|
soybean-admin-push:
|
||||||
|
docker push ${SoybeanAdminImg}
|
||||||
|
|
||||||
|
# run tauri app:
|
||||||
|
run:
|
||||||
|
pnpm tauri dev
|
15
README.md
15
README.md
@@ -1,5 +1,5 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="https://soybeanjs-1300612522.cos.ap-guangzhou.myqcloud.com/uPic/soybean.svg" style="width: 160px;"/>
|
<img src="./public/favicon.svg" style="width: 160px;"/>
|
||||||
<h1>Soybean Admin</h1>
|
<h1>Soybean Admin</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -34,17 +34,17 @@
|
|||||||
|
|
||||||
## 在线预览
|
## 在线预览
|
||||||
|
|
||||||
- [Soybean Admin 预览地址](https://soybean.pro/)
|
- [Soybean Admin 预览地址](https://admin.soybeanjs.cn/)
|
||||||
|
|
||||||
## 文档
|
## 文档
|
||||||
|
|
||||||
- [项目文档预览地址](https://docs.soybean.pro)
|
- [项目文档预览地址](https://admin-docs.soybeanjs.cn/)
|
||||||
|
|
||||||
## 代码仓库
|
## 代码仓库
|
||||||
|
|
||||||
| 仓库 | github 地址 | gitee 镜像 | 预览 |
|
| 仓库 | GitHub 地址 | gitee 镜像 | 预览 |
|
||||||
| -------------- | ----------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | --------------------------------------------------------- |
|
| -------------- | ----------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | --------------------------------------------------------- |
|
||||||
| soybean-admin | [github](https://github.com/honghuangdc/soybean-admin) | [gitee](https://gitee.com/honghuangdc/soybean-admin) | [预览](https://soybean.pro/) |
|
| soybean-admin | [GitHub](https://github.com/honghuangdc/soybean-admin) | [gitee](https://gitee.com/honghuangdc/soybean-admin) | [预览](https://admin.soybeanjs.cn/) |
|
||||||
| tauri 版 | [tauri 版](https://github.com/honghuangdc/soybean-admin/tree/tauri) | [tauri 版](https://gitee.com/honghuangdc/soybean-admin/tree/tauri) | |
|
| tauri 版 | [tauri 版](https://github.com/honghuangdc/soybean-admin/tree/tauri) | [tauri 版](https://gitee.com/honghuangdc/soybean-admin/tree/tauri) | |
|
||||||
| 精简版 | [精简版](https://github.com/honghuangdc/soybean-admin/tree/thin) | [精简版](https://gitee.com/honghuangdc/soybean-admin/tree/thin) | |
|
| 精简版 | [精简版](https://github.com/honghuangdc/soybean-admin/tree/thin) | [精简版](https://gitee.com/honghuangdc/soybean-admin/tree/thin) | |
|
||||||
| 集成 fast-crud | [集成 fast-crud](https://github.com/honghuangdc/soybean-admin/tree/fast-crud) | [集成 fast-crud](https://gitee.com/honghuangdc/soybean-admin/tree/fast-crud) | [预览](http://fast-crud.docmirror.cn/soybean/#/crud/demo) |
|
| 集成 fast-crud | [集成 fast-crud](https://github.com/honghuangdc/soybean-admin/tree/fast-crud) | [集成 fast-crud](https://gitee.com/honghuangdc/soybean-admin/tree/fast-crud) | [预览](http://fast-crud.docmirror.cn/soybean/#/crud/demo) |
|
||||||
@@ -123,7 +123,8 @@ pnpm build
|
|||||||
- Docker 部署 Soybean
|
- Docker 部署 Soybean
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run --name soybean -p 80:80 -d soybeanjs/soybean-admin:v0.9.6
|
docker build -t soybean-admin-image -f docker/Dockerfile .
|
||||||
|
docker run -d -p 80:80 soybean-admin-image
|
||||||
```
|
```
|
||||||
|
|
||||||
- 访问 SoybeanAdmin
|
- 访问 SoybeanAdmin
|
||||||
@@ -138,7 +139,7 @@ docker run --name soybean -p 80:80 -d soybeanjs/soybean-admin:v0.9.6
|
|||||||
|
|
||||||
项目已经内置 Angular 提交规范,直接执行 commit 命令即可生成符合 Angular 提交规范的 commit。
|
项目已经内置 Angular 提交规范,直接执行 commit 命令即可生成符合 Angular 提交规范的 commit。
|
||||||
|
|
||||||
项目已用 simple-git-hooks 代替了 husky, 旧版本用了 husky,执行 pnpm soy init-git-hooks 进行初始化配置
|
项目已用 simple-git-hooks 代替了 husky, 旧版本用了 husky,执行 pnpm soy init-simple-git-hooks 进行初始化配置
|
||||||
|
|
||||||
## 浏览器支持
|
## 浏览器支持
|
||||||
|
|
||||||
|
8
build/config/define.ts
Normal file
8
build/config/define.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
/** 项目构建时间 */
|
||||||
|
const PROJECT_BUILD_TIME = JSON.stringify(dayjs().format('YYYY-MM-DD HH:mm:ss'));
|
||||||
|
|
||||||
|
export const viteDefine = {
|
||||||
|
PROJECT_BUILD_TIME
|
||||||
|
};
|
@@ -1 +1,2 @@
|
|||||||
|
export * from './define';
|
||||||
export * from './proxy';
|
export * from './proxy';
|
||||||
|
6
build/plugins/compress.ts
Normal file
6
build/plugins/compress.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import ViteCompression from 'vite-plugin-compression';
|
||||||
|
|
||||||
|
export default (viteEnv: ImportMetaEnv) => {
|
||||||
|
const { VITE_COMPRESS_TYPE = 'gzip' } = viteEnv;
|
||||||
|
return ViteCompression({ algorithm: VITE_COMPRESS_TYPE });
|
||||||
|
};
|
@@ -2,10 +2,15 @@ import type { PluginOption } from 'vite';
|
|||||||
import vue from '@vitejs/plugin-vue';
|
import vue from '@vitejs/plugin-vue';
|
||||||
import vueJsx from '@vitejs/plugin-vue-jsx';
|
import vueJsx from '@vitejs/plugin-vue-jsx';
|
||||||
import unocss from '@unocss/vite';
|
import unocss from '@unocss/vite';
|
||||||
|
import progress from 'vite-plugin-progress';
|
||||||
import VueDevtools from 'vite-plugin-vue-devtools';
|
import VueDevtools from 'vite-plugin-vue-devtools';
|
||||||
import pageRoute from '@soybeanjs/vite-plugin-vue-page-route';
|
import pageRoute from '@soybeanjs/vite-plugin-vue-page-route';
|
||||||
|
import { webUpdateNotice } from '@plugin-web-update-notification/vite';
|
||||||
import unplugin from './unplugin';
|
import unplugin from './unplugin';
|
||||||
import mock from './mock';
|
import mock from './mock';
|
||||||
|
import visualizer from './visualizer';
|
||||||
|
import compress from './compress';
|
||||||
|
import pwa from './pwa';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* vite插件
|
* vite插件
|
||||||
@@ -22,9 +27,27 @@ export function setupVitePlugins(viteEnv: ImportMetaEnv): (PluginOption | Plugin
|
|||||||
VueDevtools(),
|
VueDevtools(),
|
||||||
...unplugin(viteEnv),
|
...unplugin(viteEnv),
|
||||||
unocss(),
|
unocss(),
|
||||||
mock(viteEnv)
|
mock(viteEnv),
|
||||||
|
progress(),
|
||||||
|
webUpdateNotice({
|
||||||
|
notificationProps: {
|
||||||
|
title: '👋 有新版本了',
|
||||||
|
description: '点击刷新页面获取最新版本',
|
||||||
|
buttonText: '刷新',
|
||||||
|
dismissButtonText: '忽略'
|
||||||
|
}
|
||||||
|
})
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (viteEnv.VITE_VISUALIZER === 'Y') {
|
||||||
|
plugins.push(visualizer as PluginOption);
|
||||||
|
}
|
||||||
|
if (viteEnv.VITE_COMPRESS === 'Y') {
|
||||||
|
plugins.push(compress(viteEnv));
|
||||||
|
}
|
||||||
|
if (viteEnv.VITE_PWA === 'Y' || viteEnv.VITE_VERCEL === 'Y') {
|
||||||
|
plugins.push(pwa());
|
||||||
|
}
|
||||||
if (viteEnv.VITE_SOYBEAN_ROUTE_PLUGIN === 'Y') {
|
if (viteEnv.VITE_SOYBEAN_ROUTE_PLUGIN === 'Y') {
|
||||||
plugins.push(pageRoute());
|
plugins.push(pageRoute());
|
||||||
}
|
}
|
||||||
|
31
build/plugins/pwa.ts
Normal file
31
build/plugins/pwa.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { VitePWA } from 'vite-plugin-pwa';
|
||||||
|
|
||||||
|
export default function setupVitePwa() {
|
||||||
|
return VitePWA({
|
||||||
|
registerType: 'autoUpdate',
|
||||||
|
includeAssets: ['favicon.ico'],
|
||||||
|
manifest: {
|
||||||
|
name: 'SoybeanAdmin',
|
||||||
|
short_name: 'SoybeanAdmin',
|
||||||
|
theme_color: '#fff',
|
||||||
|
icons: [
|
||||||
|
{
|
||||||
|
src: '/logo.png',
|
||||||
|
sizes: '192x192',
|
||||||
|
type: 'image/png'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: '/logo.png',
|
||||||
|
sizes: '512x512',
|
||||||
|
type: 'image/png'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: '/logo.png',
|
||||||
|
sizes: '512x512',
|
||||||
|
type: 'image/png',
|
||||||
|
purpose: 'any maskable'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
@@ -7,13 +7,13 @@ import { createSvgIconsPlugin } from 'vite-plugin-svg-icons';
|
|||||||
import { getSrcPath } from '../utils';
|
import { getSrcPath } from '../utils';
|
||||||
|
|
||||||
export default function unplugin(viteEnv: ImportMetaEnv) {
|
export default function unplugin(viteEnv: ImportMetaEnv) {
|
||||||
const { VITE_ICON_PREFFIX, VITE_ICON_LOCAL_PREFFIX } = viteEnv;
|
const { VITE_ICON_PREFIX, VITE_ICON_LOCAL_PREFIX } = viteEnv;
|
||||||
|
|
||||||
const srcPath = getSrcPath();
|
const srcPath = getSrcPath();
|
||||||
const localIconPath = `${srcPath}/assets/svg-icon`;
|
const localIconPath = `${srcPath}/assets/svg-icon`;
|
||||||
|
|
||||||
/** 本地svg图标集合名称 */
|
/** 本地svg图标集合名称 */
|
||||||
const collectionName = VITE_ICON_LOCAL_PREFFIX.replace(`${VITE_ICON_PREFFIX}-`, '');
|
const collectionName = VITE_ICON_LOCAL_PREFIX.replace(`${VITE_ICON_PREFIX}-`, '');
|
||||||
|
|
||||||
return [
|
return [
|
||||||
Icons({
|
Icons({
|
||||||
@@ -31,12 +31,12 @@ export default function unplugin(viteEnv: ImportMetaEnv) {
|
|||||||
types: [{ from: 'vue-router', names: ['RouterLink', 'RouterView'] }],
|
types: [{ from: 'vue-router', names: ['RouterLink', 'RouterView'] }],
|
||||||
resolvers: [
|
resolvers: [
|
||||||
NaiveUiResolver(),
|
NaiveUiResolver(),
|
||||||
IconsResolver({ customCollections: [collectionName], componentPrefix: VITE_ICON_PREFFIX })
|
IconsResolver({ customCollections: [collectionName], componentPrefix: VITE_ICON_PREFIX })
|
||||||
]
|
]
|
||||||
}),
|
}),
|
||||||
createSvgIconsPlugin({
|
createSvgIconsPlugin({
|
||||||
iconDirs: [localIconPath],
|
iconDirs: [localIconPath],
|
||||||
symbolId: `${VITE_ICON_LOCAL_PREFFIX}-[dir]-[name]`,
|
symbolId: `${VITE_ICON_LOCAL_PREFIX}-[dir]-[name]`,
|
||||||
inject: 'body-last',
|
inject: 'body-last',
|
||||||
customDomId: '__SVG_ICON_LOCAL__'
|
customDomId: '__SVG_ICON_LOCAL__'
|
||||||
})
|
})
|
||||||
|
7
build/plugins/visualizer.ts
Normal file
7
build/plugins/visualizer.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { visualizer } from 'rollup-plugin-visualizer';
|
||||||
|
|
||||||
|
export default visualizer({
|
||||||
|
gzipSize: true,
|
||||||
|
brotliSize: true,
|
||||||
|
open: true
|
||||||
|
});
|
32
docker/.dockerignore
Normal file
32
docker/.dockerignore
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
node_modules
|
||||||
|
.DS_Store
|
||||||
|
dist
|
||||||
|
.npmrc
|
||||||
|
.cache
|
||||||
|
|
||||||
|
tests/server/static
|
||||||
|
tests/server/static/upload
|
||||||
|
|
||||||
|
.local
|
||||||
|
# local env files
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Log files
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.idea
|
||||||
|
# .vscode
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
yarn.lock
|
||||||
|
pnpm-lock.yaml
|
||||||
|
/vite-profile.cpuprofile
|
24
docker/Dockerfile
Normal file
24
docker/Dockerfile
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
FROM node:16.17.0 as builder
|
||||||
|
|
||||||
|
ENV WORKDIR=/soybean-admin
|
||||||
|
|
||||||
|
WORKDIR $WORKDIR
|
||||||
|
|
||||||
|
COPY ./ $WORKDIR/
|
||||||
|
|
||||||
|
ARG version
|
||||||
|
ENV COMMITID=$version
|
||||||
|
|
||||||
|
RUN npm i -g pnpm
|
||||||
|
|
||||||
|
RUN pnpm install
|
||||||
|
RUN pnpm build
|
||||||
|
|
||||||
|
FROM nginx:alpine as prod
|
||||||
|
|
||||||
|
RUN mkdir /soybean
|
||||||
|
|
||||||
|
COPY --from=builder /soybean-admin/dist /soybean-admin
|
||||||
|
COPY --from=builder /soybean-admin/docker/nginx.conf /etc/nginx/nginx.conf
|
||||||
|
|
||||||
|
EXPOSE 80
|
54
docker/nginx.conf
Normal file
54
docker/nginx.conf
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
user nginx;
|
||||||
|
worker_processes 1;
|
||||||
|
error_log /var/log/nginx/error.log warn;
|
||||||
|
pid /var/run/nginx.pid;
|
||||||
|
|
||||||
|
events {
|
||||||
|
worker_connections 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||||
|
'$status $body_bytes_sent "$http_referer" '
|
||||||
|
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||||
|
access_log /var/log/nginx/access.log main;
|
||||||
|
sendfile on;
|
||||||
|
keepalive_timeout 65;
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
# 不缓存html,防止程序更新后缓存继续生效
|
||||||
|
if ($request_filename ~* .*\.(?:htm|html)$) {
|
||||||
|
add_header Cache-Control "private, no-store, no-cache, must-revalidate, proxy-revalidate";
|
||||||
|
access_log on;
|
||||||
|
}
|
||||||
|
root /soybean-admin/;
|
||||||
|
index index.html index.htm;
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# location /soybean/soybean-webserver/v1 {
|
||||||
|
# proxy_set_header Host $host;
|
||||||
|
# proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
# proxy_set_header REMOTE-HOST $remote_addr;
|
||||||
|
|
||||||
|
# # 后台接口地址
|
||||||
|
# proxy_pass http://192.168.1.99:30597/v1;
|
||||||
|
# proxy_redirect default;
|
||||||
|
# add_header Access-Control-Allow-Origin *;
|
||||||
|
# add_header Access-Control-Allow-Headers X-Requested-With;
|
||||||
|
# add_header Access-Control-Allow-Methods GET,POST,OPTIONS;
|
||||||
|
# }
|
||||||
|
|
||||||
|
error_page 500 502 503 504 /50x.html;
|
||||||
|
location = /50x.html {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,7 +1,12 @@
|
|||||||
|
<!-- prettier-ignore -->
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="zh-cmn-Hans">
|
<html lang="zh-cmn-Hans">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
|
<meta http-equiv="Expires" content="0" />
|
||||||
|
<meta http-equiv="Pragma" content="no-cache" />
|
||||||
|
<meta http-equiv="Cache-control" content="no-cache" />
|
||||||
|
<meta http-equiv="Cache" content="no-cache" />
|
||||||
<link rel="icon" href="/favicon.svg" />
|
<link rel="icon" href="/favicon.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>%VITE_APP_NAME%</title>
|
<title>%VITE_APP_NAME%</title>
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import auth from './auth';
|
import auth from './auth';
|
||||||
import route from './route';
|
import route from './route';
|
||||||
|
import management from './management';
|
||||||
|
|
||||||
export default [...auth, ...route];
|
export default [...auth, ...route, ...management];
|
||||||
|
33
mock/api/management.ts
Normal file
33
mock/api/management.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { mock } from 'mockjs';
|
||||||
|
import type { MockMethod } from 'vite-plugin-mock';
|
||||||
|
|
||||||
|
const apis: MockMethod[] = [
|
||||||
|
{
|
||||||
|
url: '/mock/getAllUserList',
|
||||||
|
method: 'post',
|
||||||
|
response: (): Service.MockServiceResult<ApiUserManagement.User[]> => {
|
||||||
|
const data = mock({
|
||||||
|
'list|1000': [
|
||||||
|
{
|
||||||
|
id: '@id',
|
||||||
|
userName: '@cname',
|
||||||
|
'age|18-56': 56,
|
||||||
|
'gender|1': ['0', '1', null],
|
||||||
|
phone:
|
||||||
|
/^[1](([3][0-9])|([4][01456789])|([5][012356789])|([6][2567])|([7][0-8])|([8][0-9])|([9][012356789]))[0-9]{8}$/,
|
||||||
|
'email|1': ['@email("qq.com")', null],
|
||||||
|
'userStatus|1': ['1', '2', '3', '4', null]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: 'ok',
|
||||||
|
data: data.list
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export default apis;
|
@@ -8,7 +8,7 @@ const apis: MockMethod[] = [
|
|||||||
response: (options: Service.MockOption): Service.MockServiceResult => {
|
response: (options: Service.MockOption): Service.MockServiceResult => {
|
||||||
const { userId = undefined } = options.body;
|
const { userId = undefined } = options.body;
|
||||||
|
|
||||||
const routeHomeName: AuthRoute.LastDegreeRouteKey = 'multi-menu_first_second';
|
const routeHomeName: AuthRoute.LastDegreeRouteKey = 'dashboard_analysis';
|
||||||
|
|
||||||
const role = userModel.find(item => item.userId === userId)?.userRole || 'user';
|
const role = userModel.find(item => item.userId === userId)?.userRole || 'user';
|
||||||
|
|
||||||
|
1204
mock/model/route.ts
1204
mock/model/route.ts
File diff suppressed because it is too large
Load Diff
93
package.json
93
package.json
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "soybean-admin",
|
"name": "soybean-admin",
|
||||||
"version": "0.10.3",
|
"version": "0.10.4",
|
||||||
"description": "A fresh and elegant admin template, based on Vue3、Vite3、TypeScript、NaiveUI and UnoCSS. 一个基于Vue3、Vite3、TypeScript、NaiveUI and UnoCSS的清新优雅的中后台模版。",
|
"description": "A fresh and elegant admin template, based on Vue3、Vite3、TypeScript、NaiveUI and UnoCSS. 一个基于Vue3、Vite3、TypeScript、NaiveUI and UnoCSS的清新优雅的中后台模版。",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Soybean",
|
"name": "Soybean",
|
||||||
"email": "honghuangdc@gmail.com",
|
"email": "soybeanjs@outlook.com",
|
||||||
"url": "https://github.com/honghuangdc"
|
"url": "https://github.com/soybeanjs"
|
||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"homepage": "https://github.com/honghuangdc/soybean-admin",
|
"homepage": "https://github.com/honghuangdc/soybean-admin",
|
||||||
@@ -49,60 +49,87 @@
|
|||||||
"commit": "soy git-commit",
|
"commit": "soy git-commit",
|
||||||
"cleanup": "soy cleanup",
|
"cleanup": "soy cleanup",
|
||||||
"update-pkg": "soy ncu",
|
"update-pkg": "soy ncu",
|
||||||
|
"release": "soy release",
|
||||||
"tsx": "tsx",
|
"tsx": "tsx",
|
||||||
"logo": "tsx ./scripts/logo.ts"
|
"logo": "tsx ./scripts/logo.ts",
|
||||||
|
"prepare": "soy init-simple-git-hooks"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@antv/data-set": "0.11.8",
|
||||||
|
"@antv/g2": "4.2.10",
|
||||||
"@better-scroll/core": "2.5.1",
|
"@better-scroll/core": "2.5.1",
|
||||||
"@soybeanjs/vue-materials": "0.2.0",
|
"@soybeanjs/vue-materials": "0.2.0",
|
||||||
"@vueuse/core": "10.1.2",
|
"@vueuse/core": "10.4.1",
|
||||||
"axios": "1.4.0",
|
"axios": "1.5.0",
|
||||||
"clipboard": "2.0.11",
|
"clipboard": "2.0.11",
|
||||||
"colord": "2.9.3",
|
"colord": "2.9.3",
|
||||||
"crypto-js": "4.1.1",
|
"crypto-js": "4.1.1",
|
||||||
"dayjs": "1.11.8",
|
"dayjs": "1.11.10",
|
||||||
|
"echarts": "5.4.3",
|
||||||
"form-data": "4.0.0",
|
"form-data": "4.0.0",
|
||||||
"lodash-es": "4.17.21",
|
"lodash-es": "4.17.21",
|
||||||
"naive-ui": "2.34.4",
|
"naive-ui": "2.34.4",
|
||||||
"pinia": "2.1.4",
|
"pinia": "2.1.6",
|
||||||
|
"print-js": "1.6.0",
|
||||||
"qs": "6.11.2",
|
"qs": "6.11.2",
|
||||||
"ua-parser-js": "1.0.35",
|
"socket.io-client": "4.7.2",
|
||||||
|
"swiper": "10.2.0",
|
||||||
|
"ua-parser-js": "1.0.36",
|
||||||
|
"vditor": "3.9.5",
|
||||||
"vue": "3.3.4",
|
"vue": "3.3.4",
|
||||||
"vue-i18n": "9.2.2",
|
"vue-i18n": "9.4.1",
|
||||||
"vue-router": "4.2.2"
|
"vue-router": "4.2.4",
|
||||||
|
"vuedraggable": "4.1.0",
|
||||||
|
"wangeditor": "4.7.15",
|
||||||
|
"xgplayer": "3.0.9"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@iconify/json": "2.2.78",
|
"@amap/amap-jsapi-types": "0.0.13",
|
||||||
|
"@iconify/json": "2.2.118",
|
||||||
"@iconify/vue": "4.1.1",
|
"@iconify/vue": "4.1.1",
|
||||||
"@soybeanjs/cli": "0.6.2",
|
"@plugin-web-update-notification/vite": "^1.6.5",
|
||||||
"@soybeanjs/vite-plugin-vue-page-route": "0.0.5",
|
"@soybeanjs/cli": "0.7.1",
|
||||||
"@types/crypto-js": "4.1.1",
|
"@soybeanjs/vite-plugin-vue-page-route": "0.0.10",
|
||||||
"@types/node": "20.3.1",
|
"@types/bmapgl": "0.0.7",
|
||||||
"@types/qs": "6.9.7",
|
"@types/crypto-js": "4.1.2",
|
||||||
"@types/ua-parser-js": "0.7.36",
|
"@types/node": "20.6.3",
|
||||||
"@unocss/preset-uno": "0.53.1",
|
"@types/qs": "6.9.8",
|
||||||
"@unocss/transformer-directives": "0.53.1",
|
"@types/ua-parser-js": "0.7.37",
|
||||||
"@unocss/vite": "0.53.1",
|
"@unocss/preset-uno": "0.56.0",
|
||||||
"@vitejs/plugin-vue": "4.2.3",
|
"@unocss/transformer-directives": "0.56.0",
|
||||||
"@vitejs/plugin-vue-jsx": "3.0.1",
|
"@unocss/vite": "0.56.0",
|
||||||
|
"@vitejs/plugin-vue": "4.3.4",
|
||||||
|
"@vitejs/plugin-vue-jsx": "3.0.2",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"eslint": "8.42.0",
|
"eslint": "8.49.0",
|
||||||
"eslint-config-soybeanjs": "0.4.9",
|
"eslint-config-soybeanjs": "0.5.6",
|
||||||
"mockjs": "1.1.0",
|
"mockjs": "1.1.0",
|
||||||
"sass": "1.63.4",
|
"rollup-plugin-visualizer": "5.9.2",
|
||||||
"tsx": "3.12.7",
|
"sass": "1.67.0",
|
||||||
"typescript": "5.1.3",
|
"simple-git-hooks": "2.9.0",
|
||||||
"unplugin-icons": "0.16.3",
|
"tsx": "3.12.10",
|
||||||
"unplugin-vue-components": "0.25.1",
|
"typescript": "5.2.2",
|
||||||
"vite": "4.3.9",
|
"unplugin-icons": "0.17.0",
|
||||||
|
"unplugin-vue-components": "0.25.2",
|
||||||
|
"vite": "4.4.9",
|
||||||
|
"vite-plugin-compression": "0.5.1",
|
||||||
"vite-plugin-mock": "2.9.8",
|
"vite-plugin-mock": "2.9.8",
|
||||||
|
"vite-plugin-progress": "0.0.7",
|
||||||
|
"vite-plugin-pwa": "0.16.5",
|
||||||
"vite-plugin-svg-icons": "2.0.1",
|
"vite-plugin-svg-icons": "2.0.1",
|
||||||
"vite-plugin-vue-devtools": "0.2.0",
|
"vite-plugin-vue-devtools": "1.0.0-rc.4",
|
||||||
"vue-tsc": "1.6.5"
|
"vue-tsc": "1.8.13"
|
||||||
},
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"patchedDependencies": {
|
"patchedDependencies": {
|
||||||
"mockjs@1.1.0": "patches/mockjs@1.1.0.patch"
|
"mockjs@1.1.0": "patches/mockjs@1.1.0.patch"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"simple-git-hooks": {
|
||||||
|
"commit-msg": "pnpm soy git-commit-verify",
|
||||||
|
"pre-commit": "pnpm typecheck && pnpm soy lint-staged"
|
||||||
|
},
|
||||||
|
"soybean": {
|
||||||
|
"useSoybeanToken": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
6798
pnpm-lock.yaml
generated
6798
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1 +1 @@
|
|||||||
<svg viewBox="0 0 160 160" xmlns="http://www.w3.org/2000/svg"><path d="M81.28 55.9c-.1-11.67-2.93-22.55-9.37-32.38-1-1.5-2.14-2.86-2.5-4.71a8.1 8.1 0 014-8.61 7.89 7.89 0 019.3 1.23 35.999 35.999 0 015.9 8.83 75.18 75.18 0 018.44 28.58 83.211 83.211 0 01-5.23 36.74 102.983 102.983 0 01-3 7.28 1.2 1.2 0 000 1.41c9.58 13.3 21.76 23 37.85 27.24a54.37 54.37 0 0019.68 1.57 7.72 7.72 0 018.36 6.9 7.903 7.903 0 01-6.7 9 64.744 64.744 0 01-23-1.33 77.68 77.68 0 01-36.93-19.88 93.628 93.628 0 01-11.91-13.71 2.18 2.18 0 00-2.3-1.06 72.744 72.744 0 00-27.38 7.55c-11.6 6-20.67 14.58-26.4 26.45a10.134 10.134 0 01-3.7 4.7 8 8 0 01-9.19-.7 7.86 7.86 0 01-2.36-9.28 60.324 60.324 0 018.72-14.52c12.2-15.43 28.21-24.59 47.32-28.57A85.085 85.085 0 0173.07 87c.524.015 1-.307 1.18-.8a76.06 76.06 0 006.53-22.3c.351-2.652.518-5.325.5-8z" fill="#1890ff"/><path d="M136.26 108.34a44.742 44.742 0 01-11.13-2.87 46.108 46.108 0 01-19.66-13.76 8 8 0 015.72-13.22 7.93 7.93 0 016.54 2.93 33.27 33.27 0 0018.87 10.75c1.546.155 3.058.553 4.48 1.18a8.08 8.08 0 013.84 9.21c-.92 3.52-4.13 5.81-8.66 5.78zm-80.6-75.02a7.61 7.61 0 016.64 5 49.139 49.139 0 013.64 17 46.33 46.33 0 01-2.46 17.28c-2 5.77-8.24 7.79-12.89 4.15a8.1 8.1 0 01-2.39-9 31.679 31.679 0 001.68-12.36 35.77 35.77 0 00-2.43-11c-2.1-5.45 1.75-11.07 8.21-11.07zm22.26 93.25a8 8 0 01-6.68 7.86 32.88 32.88 0 00-19.7 12.19 8.13 8.13 0 01-11.21 1.62 8 8 0 01-1.41-11.58A51.043 51.043 0 0154 123.81a45.842 45.842 0 0114-5.1c5.35-1.04 9.91 2.56 9.92 7.86z" fill="#1890ff"/></svg>
|
<svg viewBox="0 0 160 160" xmlns="http://www.w3.org/2000/svg"><path d="M81.28 55.9c-.1-11.67-2.93-22.55-9.37-32.38-1-1.5-2.14-2.86-2.5-4.71a8.1 8.1 0 014-8.61 7.89 7.89 0 019.3 1.23 35.999 35.999 0 015.9 8.83 75.18 75.18 0 018.44 28.58 83.211 83.211 0 01-5.23 36.74 102.983 102.983 0 01-3 7.28 1.2 1.2 0 000 1.41c9.58 13.3 21.76 23 37.85 27.24a54.37 54.37 0 0019.68 1.57 7.72 7.72 0 018.36 6.9 7.903 7.903 0 01-6.7 9 64.744 64.744 0 01-23-1.33 77.68 77.68 0 01-36.93-19.88 93.628 93.628 0 01-11.91-13.71 2.18 2.18 0 00-2.3-1.06 72.744 72.744 0 00-27.38 7.55c-11.6 6-20.67 14.58-26.4 26.45a10.134 10.134 0 01-3.7 4.7 8 8 0 01-9.19-.7 7.86 7.86 0 01-2.36-9.28 60.324 60.324 0 018.72-14.52c12.2-15.43 28.21-24.59 47.32-28.57A85.085 85.085 0 0173.07 87c.524.015 1-.307 1.18-.8a76.06 76.06 0 006.53-22.3c.351-2.652.518-5.325.5-8z" fill="#646cff"/><path d="M136.26 108.34a44.742 44.742 0 01-11.13-2.87 46.108 46.108 0 01-19.66-13.76 8 8 0 015.72-13.22 7.93 7.93 0 016.54 2.93 33.27 33.27 0 0018.87 10.75c1.546.155 3.058.553 4.48 1.18a8.08 8.08 0 013.84 9.21c-.92 3.52-4.13 5.81-8.66 5.78zm-80.6-75.02a7.61 7.61 0 016.64 5 49.139 49.139 0 013.64 17 46.33 46.33 0 01-2.46 17.28c-2 5.77-8.24 7.79-12.89 4.15a8.1 8.1 0 01-2.39-9 31.679 31.679 0 001.68-12.36 35.77 35.77 0 00-2.43-11c-2.1-5.45 1.75-11.07 8.21-11.07zm22.26 93.25a8 8 0 01-6.68 7.86 32.88 32.88 0 00-19.7 12.19 8.13 8.13 0 01-11.21 1.62 8 8 0 01-1.41-11.58A51.043 51.043 0 0154 123.81a45.842 45.842 0 0114-5.1c5.35-1.04 9.91 2.56 9.92 7.86z" fill="#646cff"/></svg>
|
||||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
@@ -4,25 +4,23 @@
|
|||||||
<div class="w-56px h-56px my-36px">
|
<div class="w-56px h-56px my-36px">
|
||||||
<div class="relative h-full animate-spin">
|
<div class="relative h-full animate-spin">
|
||||||
<div
|
<div
|
||||||
v-for="(item, index) in lodingClasses"
|
v-for="(item, index) in loadingClasses"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="absolute w-16px h-16px bg-primary rounded-8px animate-pulse"
|
class="absolute w-16px h-16px bg-primary rounded-8px animate-pulse"
|
||||||
:class="item"
|
:class="item"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h2 class="text-28px font-500 text-#646464">{{ title }}</h2>
|
<h2 class="text-28px font-500 text-#646464">{{ $t('system.title') }}</h2>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useAppInfo } from '@/composables';
|
import { sessionStg, getRgbOfColor } from '@/utils';
|
||||||
import { localStg, getRgbOfColor } from '@/utils';
|
import { $t } from '@/locales';
|
||||||
import themeSettings from '@/settings/theme.json';
|
import themeSettings from '@/settings/theme.json';
|
||||||
|
|
||||||
const { title } = useAppInfo();
|
const loadingClasses = [
|
||||||
|
|
||||||
const lodingClasses = [
|
|
||||||
'left-0 top-0',
|
'left-0 top-0',
|
||||||
'left-0 bottom-0 animate-delay-500',
|
'left-0 bottom-0 animate-delay-500',
|
||||||
'right-0 top-0 animate-delay-1000',
|
'right-0 top-0 animate-delay-1000',
|
||||||
@@ -31,7 +29,7 @@ const lodingClasses = [
|
|||||||
|
|
||||||
function addThemeColorCssVars() {
|
function addThemeColorCssVars() {
|
||||||
const defaultColor = themeSettings.themeColor;
|
const defaultColor = themeSettings.themeColor;
|
||||||
const themeColor = localStg.get('themeColor') || defaultColor;
|
const themeColor = sessionStg.get('themeColor') || defaultColor;
|
||||||
|
|
||||||
const { r, g, b } = getRgbOfColor(themeColor);
|
const { r, g, b } = getRgbOfColor(themeColor);
|
||||||
|
|
||||||
|
@@ -13,6 +13,8 @@ defineOptions({ name: 'DarkModeSwitch' });
|
|||||||
interface Props {
|
interface Props {
|
||||||
/** 暗黑模式 */
|
/** 暗黑模式 */
|
||||||
dark?: boolean;
|
dark?: boolean;
|
||||||
|
/** 自定义暗黑模式动画过渡 */
|
||||||
|
customizeTransition?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
@@ -34,32 +36,35 @@ const darkMode = computed({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleSwitch(event: MouseEvent) {
|
async function handleSwitch(event: MouseEvent) {
|
||||||
const x = event.clientX;
|
const x = event.clientX;
|
||||||
const y = event.clientY;
|
const y = event.clientY;
|
||||||
const endRadius = Math.hypot(Math.max(x, innerWidth - x), Math.max(y, innerHeight - y));
|
|
||||||
// @ts-expect-error: Transition API
|
if (!props.customizeTransition || !document.startViewTransition) {
|
||||||
if (!document.startViewTransition) {
|
|
||||||
darkMode.value = !darkMode.value;
|
darkMode.value = !darkMode.value;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// @ts-expect-error: Transition API
|
|
||||||
|
const endRadius = Math.hypot(Math.max(x, innerWidth - x), Math.max(y, innerHeight - y));
|
||||||
|
|
||||||
const transition = document.startViewTransition(() => {
|
const transition = document.startViewTransition(() => {
|
||||||
darkMode.value = !darkMode.value;
|
darkMode.value = !darkMode.value;
|
||||||
});
|
});
|
||||||
transition.ready.then(() => {
|
|
||||||
const clipPath = [`circle(0px at ${x}px ${y}px)`, `circle(${endRadius}px at ${x}px ${y}px)`];
|
await transition.ready;
|
||||||
document.documentElement.animate(
|
|
||||||
{
|
const clipPath = [`circle(0px at ${x}px ${y}px)`, `circle(${endRadius}px at ${x}px ${y}px)`];
|
||||||
clipPath: darkMode.value ? clipPath : [...clipPath].reverse()
|
|
||||||
},
|
document.documentElement.animate(
|
||||||
{
|
{
|
||||||
duration: 300,
|
clipPath: darkMode.value ? clipPath : [...clipPath].reverse()
|
||||||
easing: 'ease-in',
|
},
|
||||||
pseudoElement: darkMode.value ? '::view-transition-new(root)' : '::view-transition-old(root)'
|
{
|
||||||
}
|
duration: 300,
|
||||||
);
|
easing: 'ease-in',
|
||||||
});
|
pseudoElement: darkMode.value ? '::view-transition-new(root)' : '::view-transition-old(root)'
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
116
src/components/custom/count-to.vue
Normal file
116
src/components/custom/count-to.vue
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
<template>
|
||||||
|
<span>{{ value }}</span>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, onMounted, ref, watch, watchEffect } from 'vue';
|
||||||
|
import { TransitionPresets, useTransition } from '@vueuse/core';
|
||||||
|
import { isNumber } from '@/utils';
|
||||||
|
|
||||||
|
defineOptions({ name: 'CountTo' });
|
||||||
|
|
||||||
|
type TransitionKey = keyof typeof TransitionPresets;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
/** 初始值 */
|
||||||
|
startValue?: number;
|
||||||
|
/** 结束值 */
|
||||||
|
endValue?: number;
|
||||||
|
/** 动画时长 */
|
||||||
|
duration?: number;
|
||||||
|
/** 自动动画 */
|
||||||
|
autoplay?: boolean;
|
||||||
|
/** 进制 */
|
||||||
|
decimals?: number;
|
||||||
|
/** 前缀 */
|
||||||
|
prefix?: string;
|
||||||
|
/** 后缀 */
|
||||||
|
suffix?: string;
|
||||||
|
/** 分割符号 */
|
||||||
|
separator?: string;
|
||||||
|
/** 小数点 */
|
||||||
|
decimal?: string;
|
||||||
|
/** 使用缓冲动画函数 */
|
||||||
|
useEasing?: boolean;
|
||||||
|
/** 缓冲动画函数类型 */
|
||||||
|
transition?: TransitionKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
startValue: 0,
|
||||||
|
endValue: 2021,
|
||||||
|
duration: 1500,
|
||||||
|
autoplay: true,
|
||||||
|
decimals: 0,
|
||||||
|
prefix: '',
|
||||||
|
suffix: '',
|
||||||
|
separator: ',',
|
||||||
|
decimal: '.',
|
||||||
|
useEasing: true,
|
||||||
|
transition: 'linear'
|
||||||
|
});
|
||||||
|
|
||||||
|
interface Emits {
|
||||||
|
(e: 'on-started'): void;
|
||||||
|
(e: 'on-finished'): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits<Emits>();
|
||||||
|
|
||||||
|
const source = ref(props.startValue);
|
||||||
|
let outputValue = useTransition(source);
|
||||||
|
const value = computed(() => formatNumber(outputValue.value));
|
||||||
|
const disabled = ref(false);
|
||||||
|
|
||||||
|
function run() {
|
||||||
|
outputValue = useTransition(source, {
|
||||||
|
disabled,
|
||||||
|
duration: props.duration,
|
||||||
|
onStarted: () => emit('on-started'),
|
||||||
|
onFinished: () => emit('on-finished'),
|
||||||
|
...(props.useEasing ? { transition: TransitionPresets[props.transition] } : {})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function start() {
|
||||||
|
run();
|
||||||
|
source.value = props.endValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatNumber(num: number | string) {
|
||||||
|
if (num !== 0 && !num) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const { decimals, decimal, separator, suffix, prefix } = props;
|
||||||
|
let number = Number(num).toFixed(decimals);
|
||||||
|
number = String(number);
|
||||||
|
|
||||||
|
const x = number.split('.');
|
||||||
|
let x1 = x[0];
|
||||||
|
const x2 = x.length > 1 ? decimal + x[1] : '';
|
||||||
|
const rgx = /(\d+)(\d{3})/;
|
||||||
|
if (separator && !isNumber(separator)) {
|
||||||
|
while (rgx.test(x1)) {
|
||||||
|
x1 = x1.replace(rgx, `$1${separator}$2`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return prefix + x1 + x2 + suffix;
|
||||||
|
}
|
||||||
|
|
||||||
|
watch([() => props.startValue, () => props.endValue], () => {
|
||||||
|
if (props.autoplay) {
|
||||||
|
start();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
source.value = props.startValue;
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (props.autoplay) {
|
||||||
|
start();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
17
src/components/custom/github-link.vue
Normal file
17
src/components/custom/github-link.vue
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<template>
|
||||||
|
<web-site-link label="github地址:" :link="link" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import WebSiteLink from './web-site-link.vue';
|
||||||
|
|
||||||
|
defineOptions({ name: 'GithubLink' });
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
/** github链接 */
|
||||||
|
link: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<Props>();
|
||||||
|
</script>
|
||||||
|
<style scoped></style>
|
77
src/components/custom/icon-select.vue
Normal file
77
src/components/custom/icon-select.vue
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
<template>
|
||||||
|
<n-popover placement="bottom-end" trigger="click">
|
||||||
|
<template #trigger>
|
||||||
|
<n-input v-model:value="modelValue" readonly placeholder="点击选择图标">
|
||||||
|
<template #suffix>
|
||||||
|
<svg-icon :icon="selectedIcon" class="text-30px p-5px" />
|
||||||
|
</template>
|
||||||
|
</n-input>
|
||||||
|
</template>
|
||||||
|
<template #header>
|
||||||
|
<n-input v-model:value="searchValue" placeholder="搜索图标"></n-input>
|
||||||
|
</template>
|
||||||
|
<div v-if="iconsList.length > 0" class="grid grid-cols-9 h-auto overflow-auto">
|
||||||
|
<span v-for="iconItem in iconsList" :key="iconItem" @click="handleChange(iconItem)">
|
||||||
|
<svg-icon
|
||||||
|
:icon="iconItem"
|
||||||
|
class="border-1px border-#d9d9d9 text-30px m-2px p-5px cursor-pointer"
|
||||||
|
:class="{ 'border-primary': modelValue === iconItem }"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<n-empty v-else class="w-306px" description="你什么也找不到" />
|
||||||
|
</n-popover>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
defineOptions({ name: 'IconSelect' });
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
/** 选中的图标 */
|
||||||
|
value: string;
|
||||||
|
/** 图标列表 */
|
||||||
|
icons: string[];
|
||||||
|
/** 未选中图标 */
|
||||||
|
emptyIcon?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
emptyIcon: 'mdi:apps'
|
||||||
|
});
|
||||||
|
|
||||||
|
interface Emits {
|
||||||
|
(e: 'update:value', val: string): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits<Emits>();
|
||||||
|
|
||||||
|
const modelValue = computed({
|
||||||
|
get() {
|
||||||
|
return props.value;
|
||||||
|
},
|
||||||
|
set(val: string) {
|
||||||
|
emit('update:value', val);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedIcon = computed(() => modelValue.value || props.emptyIcon);
|
||||||
|
|
||||||
|
const searchValue = ref('');
|
||||||
|
|
||||||
|
const iconsList = computed(() => props.icons.filter(v => v.includes(searchValue.value)));
|
||||||
|
|
||||||
|
function handleChange(iconItem: string) {
|
||||||
|
modelValue.value = iconItem;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
:deep(.n-input-wrapper) {
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
:deep(.n-input__suffix) {
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
}
|
||||||
|
</style>
|
42
src/components/custom/image-verify.vue
Normal file
42
src/components/custom/image-verify.vue
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<canvas ref="domRef" width="152" height="40" class="cursor-pointer" @click="getImgCode"></canvas>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { watch } from 'vue';
|
||||||
|
import { useImageVerify } from '@/hooks';
|
||||||
|
|
||||||
|
defineOptions({ name: 'ImageVerify' });
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
code?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
code: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
interface Emits {
|
||||||
|
(e: 'update:code', code: string): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits<Emits>();
|
||||||
|
|
||||||
|
const { domRef, imgCode, setImgCode, getImgCode } = useImageVerify();
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.code,
|
||||||
|
newValue => {
|
||||||
|
setImgCode(newValue);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
watch(imgCode, newValue => {
|
||||||
|
emit('update:code', newValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
defineExpose({ getImgCode });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
@@ -37,13 +37,13 @@ const bindAttrs = computed<{ class: string; style: string }>(() => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const symbolId = computed(() => {
|
const symbolId = computed(() => {
|
||||||
const { VITE_ICON_LOCAL_PREFFIX: preffix } = import.meta.env;
|
const { VITE_ICON_LOCAL_PREFIX: prefix } = import.meta.env;
|
||||||
|
|
||||||
const defaultLocalIcon = 'no-icon';
|
const defaultLocalIcon = 'no-icon';
|
||||||
|
|
||||||
const icon = props.localIcon || defaultLocalIcon;
|
const icon = props.localIcon || defaultLocalIcon;
|
||||||
|
|
||||||
return `#${preffix}-${icon}`;
|
return `#${prefix}-${icon}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
/** 渲染本地icon */
|
/** 渲染本地icon */
|
||||||
|
23
src/components/custom/web-site-link.vue
Normal file
23
src/components/custom/web-site-link.vue
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<template>
|
||||||
|
<p>
|
||||||
|
<span>{{ label }}</span>
|
||||||
|
<a class="text-blue-500" :href="link" target="_blank">
|
||||||
|
{{ link }}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
defineOptions({ name: 'WebSiteLink' });
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
/** 网址名称 */
|
||||||
|
label: string;
|
||||||
|
/** 网址链接 */
|
||||||
|
link: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<Props>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
174
src/composables/echarts.ts
Normal file
174
src/composables/echarts.ts
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
import { nextTick, effectScope, onScopeDispose, ref, watch } from 'vue';
|
||||||
|
import type { ComputedRef, Ref } from 'vue';
|
||||||
|
import * as echarts from 'echarts/core';
|
||||||
|
import { BarChart, GaugeChart, LineChart, PictorialBarChart, PieChart, RadarChart, ScatterChart } from 'echarts/charts';
|
||||||
|
import type {
|
||||||
|
BarSeriesOption,
|
||||||
|
GaugeSeriesOption,
|
||||||
|
LineSeriesOption,
|
||||||
|
PictorialBarSeriesOption,
|
||||||
|
PieSeriesOption,
|
||||||
|
RadarSeriesOption,
|
||||||
|
ScatterSeriesOption
|
||||||
|
} from 'echarts/charts';
|
||||||
|
import {
|
||||||
|
DatasetComponent,
|
||||||
|
GridComponent,
|
||||||
|
LegendComponent,
|
||||||
|
TitleComponent,
|
||||||
|
ToolboxComponent,
|
||||||
|
TooltipComponent,
|
||||||
|
TransformComponent
|
||||||
|
} from 'echarts/components';
|
||||||
|
import type {
|
||||||
|
DatasetComponentOption,
|
||||||
|
GridComponentOption,
|
||||||
|
LegendComponentOption,
|
||||||
|
TitleComponentOption,
|
||||||
|
ToolboxComponentOption,
|
||||||
|
TooltipComponentOption
|
||||||
|
} from 'echarts/components';
|
||||||
|
import { LabelLayout, UniversalTransition } from 'echarts/features';
|
||||||
|
import { CanvasRenderer } from 'echarts/renderers';
|
||||||
|
import { useElementSize } from '@vueuse/core';
|
||||||
|
import { useThemeStore } from '@/store';
|
||||||
|
|
||||||
|
export type ECOption = echarts.ComposeOption<
|
||||||
|
| BarSeriesOption
|
||||||
|
| LineSeriesOption
|
||||||
|
| PieSeriesOption
|
||||||
|
| ScatterSeriesOption
|
||||||
|
| PictorialBarSeriesOption
|
||||||
|
| RadarSeriesOption
|
||||||
|
| GaugeSeriesOption
|
||||||
|
| TitleComponentOption
|
||||||
|
| LegendComponentOption
|
||||||
|
| TooltipComponentOption
|
||||||
|
| GridComponentOption
|
||||||
|
| ToolboxComponentOption
|
||||||
|
| DatasetComponentOption
|
||||||
|
>;
|
||||||
|
|
||||||
|
echarts.use([
|
||||||
|
TitleComponent,
|
||||||
|
LegendComponent,
|
||||||
|
TooltipComponent,
|
||||||
|
GridComponent,
|
||||||
|
DatasetComponent,
|
||||||
|
TransformComponent,
|
||||||
|
ToolboxComponent,
|
||||||
|
BarChart,
|
||||||
|
LineChart,
|
||||||
|
PieChart,
|
||||||
|
ScatterChart,
|
||||||
|
PictorialBarChart,
|
||||||
|
RadarChart,
|
||||||
|
GaugeChart,
|
||||||
|
LabelLayout,
|
||||||
|
UniversalTransition,
|
||||||
|
CanvasRenderer
|
||||||
|
]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Echarts hooks函数
|
||||||
|
* @param options - 图表配置
|
||||||
|
* @param renderFun - 图表渲染函数(例如:图表监听函数)
|
||||||
|
* @description 按需引入图表组件,没注册的组件需要先引入
|
||||||
|
*/
|
||||||
|
export function useEcharts(
|
||||||
|
options: Ref<ECOption> | ComputedRef<ECOption>,
|
||||||
|
renderFun?: (chartInstance: echarts.ECharts) => void
|
||||||
|
) {
|
||||||
|
const theme = useThemeStore();
|
||||||
|
|
||||||
|
const domRef = ref<HTMLElement>();
|
||||||
|
|
||||||
|
const initialSize = { width: 0, height: 0 };
|
||||||
|
const { width, height } = useElementSize(domRef, initialSize);
|
||||||
|
|
||||||
|
let chart: echarts.ECharts | null = null;
|
||||||
|
|
||||||
|
function canRender() {
|
||||||
|
return initialSize.width > 0 && initialSize.height > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRendered() {
|
||||||
|
return Boolean(domRef.value && chart);
|
||||||
|
}
|
||||||
|
|
||||||
|
function update(updateOptions: ECOption) {
|
||||||
|
if (isRendered()) {
|
||||||
|
chart?.clear();
|
||||||
|
chart!.setOption({ ...updateOptions, backgroundColor: 'transparent' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function render() {
|
||||||
|
if (domRef.value) {
|
||||||
|
const chartTheme = theme.darkMode ? 'dark' : 'light';
|
||||||
|
await nextTick();
|
||||||
|
chart = echarts.init(domRef.value, chartTheme);
|
||||||
|
if (renderFun) {
|
||||||
|
renderFun(chart);
|
||||||
|
}
|
||||||
|
update(options.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resize() {
|
||||||
|
chart?.resize();
|
||||||
|
}
|
||||||
|
|
||||||
|
function destroy() {
|
||||||
|
chart?.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTheme() {
|
||||||
|
destroy();
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
|
||||||
|
const scope = effectScope();
|
||||||
|
|
||||||
|
scope.run(() => {
|
||||||
|
watch([width, height], ([newWidth, newHeight]) => {
|
||||||
|
initialSize.width = newWidth;
|
||||||
|
initialSize.height = newHeight;
|
||||||
|
if (newWidth === 0 && newHeight === 0) {
|
||||||
|
// 节点被删除 将chart置为空
|
||||||
|
chart = null;
|
||||||
|
}
|
||||||
|
if (canRender()) {
|
||||||
|
if (!isRendered()) {
|
||||||
|
render();
|
||||||
|
} else {
|
||||||
|
resize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
options,
|
||||||
|
newValue => {
|
||||||
|
update(newValue);
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => theme.darkMode,
|
||||||
|
() => {
|
||||||
|
updateTheme();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
onScopeDispose(() => {
|
||||||
|
destroy();
|
||||||
|
scope.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
domRef
|
||||||
|
};
|
||||||
|
}
|
@@ -48,7 +48,7 @@ export const useIconRender = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!icon && !localIcon) {
|
if (!icon && !localIcon) {
|
||||||
throw Error('没有传递图标名称,请确保给icon或localIcon传递有效值!');
|
window.console.warn('没有传递图标名称,请确保给icon或localIcon传递有效值!');
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => h(SvgIcon, { icon, localIcon, style });
|
return () => h(SvgIcon, { icon, localIcon, style });
|
||||||
|
@@ -2,4 +2,6 @@ export * from './system';
|
|||||||
export * from './router';
|
export * from './router';
|
||||||
export * from './layout';
|
export * from './layout';
|
||||||
export * from './events';
|
export * from './events';
|
||||||
|
export * from './echarts';
|
||||||
export * from './icon';
|
export * from './icon';
|
||||||
|
export * from './websocket';
|
||||||
|
@@ -2,26 +2,6 @@ import UAParser from 'ua-parser-js';
|
|||||||
import { useAuthStore } from '@/store';
|
import { useAuthStore } from '@/store';
|
||||||
import { isArray, isString } from '@/utils';
|
import { isArray, isString } from '@/utils';
|
||||||
|
|
||||||
interface AppInfo {
|
|
||||||
/** 项目名称 */
|
|
||||||
name: string;
|
|
||||||
/** 项目标题 */
|
|
||||||
title: string;
|
|
||||||
/** 项目描述 */
|
|
||||||
desc: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 项目信息 */
|
|
||||||
export function useAppInfo(): AppInfo {
|
|
||||||
const { VITE_APP_NAME: name, VITE_APP_TITLE: title, VITE_APP_DESC: desc } = import.meta.env;
|
|
||||||
|
|
||||||
return {
|
|
||||||
name,
|
|
||||||
title,
|
|
||||||
desc
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 获取设备信息 */
|
/** 获取设备信息 */
|
||||||
export function useDeviceInfo() {
|
export function useDeviceInfo() {
|
||||||
const parser = new UAParser();
|
const parser = new UAParser();
|
||||||
|
50
src/composables/websocket.ts
Normal file
50
src/composables/websocket.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { io } from 'socket.io-client';
|
||||||
|
import type { Socket } from 'socket.io-client';
|
||||||
|
import { useAppStore } from '../store';
|
||||||
|
|
||||||
|
type ListenEvents = {
|
||||||
|
update: (id: string, data: { name: string; age: number }) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type EmitEvents = {
|
||||||
|
update: (id: string, data: { name: string; age: number }) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useWebsocket() {
|
||||||
|
const app = useAppStore();
|
||||||
|
|
||||||
|
const socket: Socket<ListenEvents, EmitEvents> = (app.socket || io('ws://localhost:8080')) as Socket<
|
||||||
|
ListenEvents,
|
||||||
|
EmitEvents
|
||||||
|
>;
|
||||||
|
|
||||||
|
if (!app.socket) {
|
||||||
|
app.setSocket(socket);
|
||||||
|
}
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
window.console.log('[socket.io] connecting...');
|
||||||
|
|
||||||
|
socket.on('connect', () => {
|
||||||
|
window.console.log('[socket.io] connected.');
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('disconnect', () => {
|
||||||
|
window.console.log('[socket.io] disconnected.');
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('update', (id, data) => {
|
||||||
|
window.console.log('[socket.io] update', id, data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleUpdate(id: string, data: { name: string; age: number }) {
|
||||||
|
socket.emit('update', id, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
init();
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleUpdate
|
||||||
|
};
|
||||||
|
}
|
@@ -1,2 +1,3 @@
|
|||||||
export * from './service';
|
export * from './service';
|
||||||
export * from './regexp';
|
export * from './regexp';
|
||||||
|
export * from './map-sdk';
|
||||||
|
8
src/config/map-sdk.ts
Normal file
8
src/config/map-sdk.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/** 百度地图sdk地址 */
|
||||||
|
export const BAIDU_MAP_SDK_URL = `https://api.map.baidu.com/getscript?v=3.0&ak=KSezYymXPth1DIGILRX3oYN9PxbOQQmU&services=&t=20210201100830&s=1`;
|
||||||
|
|
||||||
|
/** 高德地图sdk地址 */
|
||||||
|
export const AMAP_SDK_URL = 'https://webapi.amap.com/maps?v=2.0&key=e7bd02bd504062087e6563daf4d6721d';
|
||||||
|
|
||||||
|
/** 腾讯地图sdk地址 */
|
||||||
|
export const TENCENT_MAP_SDK_URL = 'https://map.qq.com/api/gljs?v=1.exp&key=A6DBZ-KXPLW-JKSRY-ONZF4-CPHY3-K6BL7';
|
@@ -1,16 +1,33 @@
|
|||||||
|
import { $t } from '@/locales';
|
||||||
import { transformObjectToOption } from './_shared';
|
import { transformObjectToOption } from './_shared';
|
||||||
|
|
||||||
export const loginModuleLabels: Record<UnionKey.LoginModule, string> = {
|
export const loginModuleLabels: Record<UnionKey.LoginModule, string> = {
|
||||||
'pwd-login': '账密登录',
|
'pwd-login': $t('page.login.pwdLogin.title'),
|
||||||
'code-login': '手机验证码登录',
|
'code-login': $t('page.login.codeLogin.title'),
|
||||||
register: '注册',
|
register: $t('page.login.register.title'),
|
||||||
'reset-pwd': '重置密码',
|
'reset-pwd': $t('page.login.resetPwd.title'),
|
||||||
'bind-wechat': '微信绑定'
|
'bind-wechat': $t('page.login.bindWeChat.title')
|
||||||
};
|
};
|
||||||
|
|
||||||
export const userRoleLabels: Record<Auth.RoleType, string> = {
|
export const userRoleLabels: Record<Auth.RoleType, string> = {
|
||||||
super: '超级管理员',
|
super: $t('page.login.pwdLogin.superAdmin'),
|
||||||
admin: '管理员',
|
admin: $t('page.login.pwdLogin.admin'),
|
||||||
user: '普通用户'
|
user: $t('page.login.pwdLogin.user')
|
||||||
};
|
};
|
||||||
export const userRoleOptions = transformObjectToOption(userRoleLabels);
|
export const userRoleOptions = transformObjectToOption(userRoleLabels);
|
||||||
|
|
||||||
|
/** 用户性别 */
|
||||||
|
export const genderLabels: Record<UserManagement.GenderKey, string> = {
|
||||||
|
0: '女',
|
||||||
|
1: '男'
|
||||||
|
};
|
||||||
|
export const genderOptions = transformObjectToOption(genderLabels);
|
||||||
|
|
||||||
|
/** 用户状态 */
|
||||||
|
export const userStatusLabels: Record<UserManagement.UserStatusKey, string> = {
|
||||||
|
1: '启用',
|
||||||
|
2: '禁用',
|
||||||
|
3: '冻结',
|
||||||
|
4: '软删除'
|
||||||
|
};
|
||||||
|
export const userStatusOptions = transformObjectToOption(userStatusLabels);
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import useCountDown from './use-count-down';
|
import useCountDown from './use-count-down';
|
||||||
import useSmsCode from './use-sms-code';
|
import useSmsCode from './use-sms-code';
|
||||||
|
import useImageVerify from './use-image-verify';
|
||||||
|
|
||||||
export { useCountDown, useSmsCode };
|
export { useCountDown, useSmsCode, useImageVerify };
|
||||||
|
180
src/hooks/business/use-hook-table.ts
Normal file
180
src/hooks/business/use-hook-table.ts
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
import { ref, reactive } from 'vue';
|
||||||
|
import type { Ref } from 'vue';
|
||||||
|
import type { PaginationProps, DataTableBaseColumn, DataTableSelectionColumn, DataTableExpandColumn } from 'naive-ui';
|
||||||
|
import type { TableColumnGroup } from 'naive-ui/es/data-table/src/interface';
|
||||||
|
import { useLoadingEmpty } from '../common';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 接口请求函数
|
||||||
|
*/
|
||||||
|
type ApiFn<T = any, R = any> = (args: T) => Promise<Service.RequestResult<R>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 接口请求函数的参数
|
||||||
|
*/
|
||||||
|
type GetApiFnParameters<T extends ApiFn, R = any> = T extends (args: infer P) => Promise<Service.RequestResult<R>>
|
||||||
|
? P
|
||||||
|
: never;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 接口请求函数的返回值
|
||||||
|
*/
|
||||||
|
type GetApiFnReturnType<T extends ApiFn, P = any> = T extends (args: P) => Promise<Service.RequestResult<infer R>>
|
||||||
|
? R
|
||||||
|
: never;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表格接口请求后转换后的数据
|
||||||
|
*/
|
||||||
|
type Transformer<TableData, Response> = (response: Response) => {
|
||||||
|
data: TableData[];
|
||||||
|
pageNum: number;
|
||||||
|
pageSize: number;
|
||||||
|
total: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 列表接口参数更新
|
||||||
|
*/
|
||||||
|
type ApiParamsUpdater<P, R> = (params: P) => R;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页参数
|
||||||
|
*/
|
||||||
|
type PagePropsOfPagination = Pick<PaginationProps, 'page' | 'pageSize'>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自定义的列 key
|
||||||
|
*/
|
||||||
|
type CustomColumnKey<K = never> = K | 'action';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表格的列
|
||||||
|
*/
|
||||||
|
type HookTableColumn<T = Record<string, unknown>> =
|
||||||
|
| (Omit<TableColumnGroup<T>, 'key'> & { key: CustomColumnKey<keyof T> })
|
||||||
|
| (Omit<DataTableBaseColumn<T>, 'key'> & { key: CustomColumnKey<keyof T> })
|
||||||
|
| DataTableSelectionColumn<T>
|
||||||
|
| DataTableExpandColumn<T>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表格配置
|
||||||
|
*/
|
||||||
|
type HookTableConfig<TableData, Fn extends ApiFn> = {
|
||||||
|
/**
|
||||||
|
* 列表接口参数
|
||||||
|
*/
|
||||||
|
apiParams: GetApiFnParameters<Fn>;
|
||||||
|
/**
|
||||||
|
* 列表接口返回数据转换
|
||||||
|
*/
|
||||||
|
transformer: Transformer<TableData, GetApiFnReturnType<Fn>>;
|
||||||
|
/**
|
||||||
|
* 列表列
|
||||||
|
*/
|
||||||
|
columns: () => HookTableColumn<TableData>[];
|
||||||
|
/**
|
||||||
|
* 列表接口参数更新
|
||||||
|
* @description 用于更新分页参数, 如果列表接口的参数不包含同名分页参数属性 `page` 和 `pageSize`, 需要通过此函数更新
|
||||||
|
* @default p => p
|
||||||
|
*/
|
||||||
|
apiParamsUpdater?: ApiParamsUpdater<GetApiFnParameters<Fn> & Partial<PagePropsOfPagination>, GetApiFnParameters<Fn>>;
|
||||||
|
/**
|
||||||
|
* 列表分页参数
|
||||||
|
*/
|
||||||
|
pagination?: PaginationProps;
|
||||||
|
/**
|
||||||
|
* 是否立即请求
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
immediate?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通用表格 hook
|
||||||
|
* @param apiFn 接口请求函数
|
||||||
|
* @param config 表格配置
|
||||||
|
*/
|
||||||
|
export default function useHookTable<TableData, Fn extends ApiFn>(apiFn: Fn, config: HookTableConfig<TableData, Fn>) {
|
||||||
|
const { loading, startLoading, endLoading, empty, setEmpty } = useLoadingEmpty();
|
||||||
|
|
||||||
|
const { apiParams, transformer, apiParamsUpdater = p => p, immediate = true } = config;
|
||||||
|
|
||||||
|
const data: Ref<TableData[]> = ref([]);
|
||||||
|
|
||||||
|
function updateData(update: TableData[]) {
|
||||||
|
data.value = update;
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns = ref(config.columns()) as Ref<HookTableColumn<TableData>[]>;
|
||||||
|
|
||||||
|
const requestParams = ref(apiParams) as Ref<HookTableConfig<TableData, Fn>['apiParams']>;
|
||||||
|
|
||||||
|
function updateRequestParamsByPagination(p: PagePropsOfPagination) {
|
||||||
|
requestParams.value = apiParamsUpdater({ ...requestParams.value, ...p });
|
||||||
|
}
|
||||||
|
|
||||||
|
const pagination = reactive({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
showSizePicker: true,
|
||||||
|
pageSizes: [10, 15, 20, 25, 30],
|
||||||
|
onChange: (page: number) => {
|
||||||
|
pagination.page = page;
|
||||||
|
|
||||||
|
updateRequestParamsByPagination({ page });
|
||||||
|
getData();
|
||||||
|
},
|
||||||
|
onUpdatePageSize: (pageSize: number) => {
|
||||||
|
pagination.pageSize = pageSize;
|
||||||
|
pagination.page = 1;
|
||||||
|
|
||||||
|
updateRequestParamsByPagination({ pageSize });
|
||||||
|
getData();
|
||||||
|
},
|
||||||
|
...config.pagination
|
||||||
|
}) as PaginationProps;
|
||||||
|
|
||||||
|
function updatePagination(update: Partial<PaginationProps>) {
|
||||||
|
Object.assign(pagination, update);
|
||||||
|
|
||||||
|
updateRequestParamsByPagination({ page: pagination.page, pageSize: pagination.pageSize });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getData() {
|
||||||
|
startLoading();
|
||||||
|
|
||||||
|
const { data: apiData, error } = await apiFn(requestParams.value);
|
||||||
|
|
||||||
|
if (!error && data) {
|
||||||
|
const { data: tableData, pageNum, pageSize, total } = transformer(apiData);
|
||||||
|
|
||||||
|
updateData(tableData);
|
||||||
|
|
||||||
|
setEmpty(tableData.length === 0);
|
||||||
|
|
||||||
|
updatePagination({ page: pageNum, pageSize, itemCount: total });
|
||||||
|
}
|
||||||
|
|
||||||
|
endLoading();
|
||||||
|
}
|
||||||
|
|
||||||
|
function reloadColumns() {
|
||||||
|
columns.value = config.columns();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (immediate) {
|
||||||
|
getData();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
columns,
|
||||||
|
loading,
|
||||||
|
empty,
|
||||||
|
pagination,
|
||||||
|
getData,
|
||||||
|
updatePagination,
|
||||||
|
reloadColumns
|
||||||
|
};
|
||||||
|
}
|
87
src/hooks/business/use-image-verify.ts
Normal file
87
src/hooks/business/use-image-verify.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绘制图形验证码
|
||||||
|
* @param width - 图形宽度
|
||||||
|
* @param height - 图形高度
|
||||||
|
*/
|
||||||
|
export default function useImageVerify(width = 152, height = 40) {
|
||||||
|
const domRef = ref<HTMLCanvasElement>();
|
||||||
|
const imgCode = ref('');
|
||||||
|
|
||||||
|
function setImgCode(code: string) {
|
||||||
|
imgCode.value = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getImgCode() {
|
||||||
|
if (!domRef.value) return;
|
||||||
|
imgCode.value = draw(domRef.value, width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getImgCode();
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
domRef,
|
||||||
|
imgCode,
|
||||||
|
setImgCode,
|
||||||
|
getImgCode
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function randomNum(min: number, max: number) {
|
||||||
|
const num = Math.floor(Math.random() * (max - min) + min);
|
||||||
|
return num;
|
||||||
|
}
|
||||||
|
|
||||||
|
function randomColor(min: number, max: number) {
|
||||||
|
const r = randomNum(min, max);
|
||||||
|
const g = randomNum(min, max);
|
||||||
|
const b = randomNum(min, max);
|
||||||
|
return `rgb(${r},${g},${b})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function draw(dom: HTMLCanvasElement, width: number, height: number) {
|
||||||
|
let imgCode = '';
|
||||||
|
|
||||||
|
const NUMBER_STRING = '0123456789';
|
||||||
|
|
||||||
|
const ctx = dom.getContext('2d');
|
||||||
|
if (!ctx) return imgCode;
|
||||||
|
|
||||||
|
ctx.fillStyle = randomColor(180, 230);
|
||||||
|
ctx.fillRect(0, 0, width, height);
|
||||||
|
|
||||||
|
for (let i = 0; i < 4; i += 1) {
|
||||||
|
const text = NUMBER_STRING[randomNum(0, NUMBER_STRING.length)];
|
||||||
|
imgCode += text;
|
||||||
|
const fontSize = randomNum(18, 41);
|
||||||
|
const deg = randomNum(-30, 30);
|
||||||
|
ctx.font = `${fontSize}px Simhei`;
|
||||||
|
ctx.textBaseline = 'top';
|
||||||
|
ctx.fillStyle = randomColor(80, 150);
|
||||||
|
ctx.save();
|
||||||
|
ctx.translate(30 * i + 23, 15);
|
||||||
|
ctx.rotate((deg * Math.PI) / 180);
|
||||||
|
ctx.fillText(text, -15 + 5, -15);
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
for (let i = 0; i < 5; i += 1) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(randomNum(0, width), randomNum(0, height));
|
||||||
|
ctx.lineTo(randomNum(0, width), randomNum(0, height));
|
||||||
|
ctx.strokeStyle = randomColor(180, 230);
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
for (let i = 0; i < 41; i += 1) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(randomNum(0, width), randomNum(0, height), 1, 0, 2 * Math.PI);
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fillStyle = randomColor(150, 200);
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
|
||||||
|
return imgCode;
|
||||||
|
}
|
23
src/layouts/common/global-header/components/github-site.vue
Normal file
23
src/layouts/common/global-header/components/github-site.vue
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<template>
|
||||||
|
<hover-container
|
||||||
|
tooltip-content="github"
|
||||||
|
class="w-40px h-full"
|
||||||
|
:inverted="theme.header.inverted"
|
||||||
|
@click="handleClickLink"
|
||||||
|
>
|
||||||
|
<icon-mdi-github class="text-20px" />
|
||||||
|
</hover-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { useThemeStore } from '@/store';
|
||||||
|
|
||||||
|
defineOptions({ name: 'GithubSite' });
|
||||||
|
|
||||||
|
const theme = useThemeStore();
|
||||||
|
function handleClickLink() {
|
||||||
|
window.open('https://github.com/honghuangdc/soybean-admin', '_blank');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
@@ -35,7 +35,7 @@ import { routePath } from '@/router';
|
|||||||
import { useRouteStore, useThemeStore } from '@/store';
|
import { useRouteStore, useThemeStore } from '@/store';
|
||||||
import { useRouterPush } from '@/composables';
|
import { useRouterPush } from '@/composables';
|
||||||
import { getBreadcrumbByRouteKey } from '@/utils';
|
import { getBreadcrumbByRouteKey } from '@/utils';
|
||||||
import { t } from '@/locales';
|
import { $t } from '@/locales';
|
||||||
|
|
||||||
defineOptions({ name: 'GlobalBreadcrumb' });
|
defineOptions({ name: 'GlobalBreadcrumb' });
|
||||||
|
|
||||||
@@ -48,8 +48,8 @@ const breadcrumbs = computed(() =>
|
|||||||
getBreadcrumbByRouteKey(route.name as string, routeStore.menus as App.GlobalMenuOption[], routePath('root')).map(
|
getBreadcrumbByRouteKey(route.name as string, routeStore.menus as App.GlobalMenuOption[], routePath('root')).map(
|
||||||
item => ({
|
item => ({
|
||||||
...item,
|
...item,
|
||||||
label: item.i18nTitle ? t(item.i18nTitle) : item.label,
|
label: item.i18nTitle ? $t(item.i18nTitle) : item.label,
|
||||||
options: item.options?.map(oItem => ({ ...oItem, label: oItem.i18nTitle ? t(oItem.i18nTitle) : oItem.label }))
|
options: item.options?.map(oItem => ({ ...oItem, label: oItem.i18nTitle ? $t(oItem.i18nTitle) : oItem.label }))
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
@@ -1,10 +1,23 @@
|
|||||||
import MenuCollapse from './menu-collapse.vue';
|
import MenuCollapse from './menu-collapse.vue';
|
||||||
import GlobalBreadcrumb from './global-breadcrumb.vue';
|
import GlobalBreadcrumb from './global-breadcrumb.vue';
|
||||||
import HeaderMenu from './header-menu.vue';
|
import HeaderMenu from './header-menu.vue';
|
||||||
|
import GithubSite from './github-site.vue';
|
||||||
import FullScreen from './full-screen.vue';
|
import FullScreen from './full-screen.vue';
|
||||||
import ThemeMode from './theme-mode.vue';
|
import ThemeMode from './theme-mode.vue';
|
||||||
import UserAvatar from './user-avatar.vue';
|
import UserAvatar from './user-avatar.vue';
|
||||||
|
import SystemMessage from './system-message.vue';
|
||||||
import SettingButton from './setting-button.vue';
|
import SettingButton from './setting-button.vue';
|
||||||
import ToggleLang from './toggle-lang.vue';
|
import ToggleLang from './toggle-lang.vue';
|
||||||
|
|
||||||
export { MenuCollapse, GlobalBreadcrumb, HeaderMenu, FullScreen, ThemeMode, UserAvatar, SettingButton, ToggleLang };
|
export {
|
||||||
|
MenuCollapse,
|
||||||
|
GlobalBreadcrumb,
|
||||||
|
HeaderMenu,
|
||||||
|
GithubSite,
|
||||||
|
FullScreen,
|
||||||
|
ThemeMode,
|
||||||
|
UserAvatar,
|
||||||
|
SystemMessage,
|
||||||
|
SettingButton,
|
||||||
|
ToggleLang
|
||||||
|
};
|
||||||
|
57
src/layouts/common/global-header/components/message-list.vue
Normal file
57
src/layouts/common/global-header/components/message-list.vue
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
<template>
|
||||||
|
<n-scrollbar class="max-h-360px">
|
||||||
|
<n-list>
|
||||||
|
<n-list-item
|
||||||
|
v-for="(item, index) in list"
|
||||||
|
:key="item.id"
|
||||||
|
class="hover:bg-#f6f6f6 dark:hover:bg-dark cursor-pointer"
|
||||||
|
@click="handleRead(index)"
|
||||||
|
>
|
||||||
|
<n-thing class="px-15px" :class="{ 'opacity-30': item.isRead }">
|
||||||
|
<template #avatar>
|
||||||
|
<n-avatar v-if="item.avatar" :src="item.avatar" />
|
||||||
|
<svg-icon v-else class="text-34px text-primary" :icon="item.icon" :local-icon="item.svgIcon" />
|
||||||
|
</template>
|
||||||
|
<template #header>
|
||||||
|
<n-ellipsis :line-clamp="1">
|
||||||
|
{{ item.title }}
|
||||||
|
<template #tooltip>
|
||||||
|
{{ item.title }}
|
||||||
|
</template>
|
||||||
|
</n-ellipsis>
|
||||||
|
</template>
|
||||||
|
<template v-if="item.tagTitle" #header-extra>
|
||||||
|
<n-tag v-bind="item.tagProps" size="small">{{ item.tagTitle }}</n-tag>
|
||||||
|
</template>
|
||||||
|
<template #description>
|
||||||
|
<n-ellipsis v-if="item.description" :line-clamp="2">
|
||||||
|
{{ item.description }}
|
||||||
|
</n-ellipsis>
|
||||||
|
<p>{{ item.date }}</p>
|
||||||
|
</template>
|
||||||
|
</n-thing>
|
||||||
|
</n-list-item>
|
||||||
|
</n-list>
|
||||||
|
</n-scrollbar>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
defineOptions({ name: 'MessageList' });
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
list?: App.MessageList[];
|
||||||
|
}
|
||||||
|
|
||||||
|
withDefaults(defineProps<Props>(), {
|
||||||
|
list: () => []
|
||||||
|
});
|
||||||
|
|
||||||
|
interface Emits {
|
||||||
|
(e: 'read', val: number): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits<Emits>();
|
||||||
|
|
||||||
|
function handleRead(index: number) {
|
||||||
|
emit('read', index);
|
||||||
|
}
|
||||||
|
</script>
|
217
src/layouts/common/global-header/components/system-message.vue
Normal file
217
src/layouts/common/global-header/components/system-message.vue
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
<template>
|
||||||
|
<n-popover class="!p-0" trigger="click" placement="bottom">
|
||||||
|
<template #trigger>
|
||||||
|
<hover-container tooltip-content="消息通知" :inverted="theme.header.inverted" class="relative w-40px h-full">
|
||||||
|
<icon-clarity:notification-line class="text-18px" />
|
||||||
|
<n-badge
|
||||||
|
:value="count"
|
||||||
|
:max="99"
|
||||||
|
:class="[count < 10 ? '-right-2px' : '-right-10px']"
|
||||||
|
class="absolute top-10px"
|
||||||
|
/>
|
||||||
|
</hover-container>
|
||||||
|
</template>
|
||||||
|
<n-tabs
|
||||||
|
v-model:value="currentTab"
|
||||||
|
:class="[isMobile ? 'w-276px' : 'w-360px']"
|
||||||
|
type="line"
|
||||||
|
justify-content="space-evenly"
|
||||||
|
>
|
||||||
|
<n-tab-pane v-for="(item, index) in tabData" :key="item.key" :name="index">
|
||||||
|
<template #tab>
|
||||||
|
<div class="flex-x-center items-center" :class="[isMobile ? 'w-92px' : 'w-120px']">
|
||||||
|
<span class="mr-5px">{{ item.name }}</span>
|
||||||
|
<n-badge
|
||||||
|
v-bind="item.badgeProps"
|
||||||
|
:value="item.list.filter(message => !message.isRead).length"
|
||||||
|
:max="99"
|
||||||
|
show-zero
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<loading-empty-wrapper
|
||||||
|
class="h-360px"
|
||||||
|
:loading="loading"
|
||||||
|
:empty="item.list.length === 0"
|
||||||
|
placeholder-class="bg-$n-color transition-background-color duration-300 ease-in-out"
|
||||||
|
>
|
||||||
|
<message-list :list="item.list" @read="handleRead" />
|
||||||
|
</loading-empty-wrapper>
|
||||||
|
</n-tab-pane>
|
||||||
|
</n-tabs>
|
||||||
|
<div v-if="showAction" class="flex border-t border-$n-divider-color cursor-pointer">
|
||||||
|
<div class="flex-1 text-center py-10px" @click="handleClear">清空</div>
|
||||||
|
<div class="flex-1 text-center py-10px border-l border-$n-divider-color" @click="handleAllRead">全部已读</div>
|
||||||
|
<div class="flex-1 text-center py-10px border-l border-$n-divider-color" @click="handleLoadMore">查看更多</div>
|
||||||
|
</div>
|
||||||
|
</n-popover>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { useThemeStore } from '@/store';
|
||||||
|
import { useBasicLayout } from '@/composables';
|
||||||
|
import { useBoolean } from '@/hooks';
|
||||||
|
import MessageList from './message-list.vue';
|
||||||
|
|
||||||
|
defineOptions({ name: 'SystemMessage' });
|
||||||
|
|
||||||
|
const theme = useThemeStore();
|
||||||
|
const { isMobile } = useBasicLayout();
|
||||||
|
const { bool: loading, setBool: setLoading } = useBoolean();
|
||||||
|
|
||||||
|
const currentTab = ref(0);
|
||||||
|
|
||||||
|
const tabData = ref<App.MessageTab[]>([
|
||||||
|
{
|
||||||
|
key: 1,
|
||||||
|
name: '通知',
|
||||||
|
badgeProps: { type: 'warning' },
|
||||||
|
list: [
|
||||||
|
{ id: 1, icon: 'ri:message-3-line', title: '你收到了5条新消息', date: '2022-06-17' },
|
||||||
|
{ id: 4, icon: 'ri:message-3-line', title: 'Soybean Admin 1.0.0 版本正在筹备中', date: '2022-06-17' },
|
||||||
|
{ id: 2, icon: 'ri:message-3-line', title: 'Soybean Admin 0.9.6 版本发布了', date: '2022-06-16' },
|
||||||
|
{ id: 3, icon: 'ri:message-3-line', title: 'Soybean Admin 0.9.5 版本发布了', date: '2022-06-07' },
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
icon: 'ri:message-3-line',
|
||||||
|
title: '测试超长标题测试超长标题测试超长标题测试超长标题测试超长标题测试超长标题测试超长标题测试超长标题',
|
||||||
|
date: '2022-06-17'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 2,
|
||||||
|
name: '消息',
|
||||||
|
badgeProps: { type: 'error' },
|
||||||
|
list: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: '项目动态',
|
||||||
|
svgIcon: 'avatar',
|
||||||
|
description: 'Soybean 刚才把工作台页面随便写了一些,凑合能看了!',
|
||||||
|
date: '2021-11-07 22:45:32'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: '项目动态',
|
||||||
|
svgIcon: 'avatar',
|
||||||
|
description: 'Soybean 正在忙于为soybean-admin写项目说明文档!',
|
||||||
|
date: '2021-11-03 20:33:31'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: '项目动态',
|
||||||
|
svgIcon: 'avatar',
|
||||||
|
description: 'Soybean 准备为soybean-admin 1.0的发布做充分的准备工作!',
|
||||||
|
date: '2021-10-31 22:43:12'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
title: '项目动态',
|
||||||
|
svgIcon: 'avatar',
|
||||||
|
description: '@yanbowe 向soybean-admin提交了一个bug,多标签栏不会自适应。',
|
||||||
|
date: '2021-10-27 10:24:54'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
title: '项目动态',
|
||||||
|
svgIcon: 'avatar',
|
||||||
|
description: 'Soybean 在2021年5月28日创建了开源项目soybean-admin!',
|
||||||
|
date: '2021-05-28 22:22:22'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 3,
|
||||||
|
name: '待办',
|
||||||
|
badgeProps: { type: 'info' },
|
||||||
|
list: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
icon: 'ri:calendar-todo-line',
|
||||||
|
title: '缓存主题配置',
|
||||||
|
description: '任务正在计划中',
|
||||||
|
date: '2022-06-17',
|
||||||
|
tagTitle: '未开始',
|
||||||
|
tagProps: { type: 'default' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
icon: 'ri:calendar-todo-line',
|
||||||
|
title: '添加锁屏组件、全局Iframe组件',
|
||||||
|
description: '任务正在计划中',
|
||||||
|
date: '2022-06-17',
|
||||||
|
tagTitle: '未开始',
|
||||||
|
tagProps: { type: 'default' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
icon: 'ri:calendar-todo-line',
|
||||||
|
title: '示例页面完善',
|
||||||
|
description: '任务正在计划中',
|
||||||
|
date: '2022-06-17',
|
||||||
|
tagTitle: '未开始',
|
||||||
|
tagProps: { type: 'default' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
icon: 'ri:calendar-todo-line',
|
||||||
|
title: '表单、表格示例',
|
||||||
|
description: '任务正在计划中',
|
||||||
|
date: '2022-06-17',
|
||||||
|
tagTitle: '未开始',
|
||||||
|
tagProps: { type: 'default' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
icon: 'ri:calendar-todo-line',
|
||||||
|
title: '性能优化(优化递归函数)',
|
||||||
|
description: '任务正在计划中',
|
||||||
|
date: '2022-06-17',
|
||||||
|
tagTitle: '未开始',
|
||||||
|
tagProps: { type: 'default' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
icon: 'ri:calendar-todo-line',
|
||||||
|
title: '精简版(新分支thin)',
|
||||||
|
description: '任务正在计划中',
|
||||||
|
date: '2022-06-17',
|
||||||
|
tagTitle: '未开始',
|
||||||
|
tagProps: { type: 'default' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const count = computed(() => {
|
||||||
|
return tabData.value.reduce((acc, cur) => {
|
||||||
|
return acc + cur.list.filter(item => !item.isRead).length;
|
||||||
|
}, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
const showAction = computed(() => tabData.value[currentTab.value].list.length > 0);
|
||||||
|
|
||||||
|
function handleRead(index: number) {
|
||||||
|
tabData.value[currentTab.value].list[index].isRead = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAllRead() {
|
||||||
|
tabData.value[currentTab.value].list.forEach(item => Object.assign(item, { isRead: true }));
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClear() {
|
||||||
|
tabData.value[currentTab.value].list = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleLoadMore() {
|
||||||
|
const { list } = tabData.value[currentTab.value];
|
||||||
|
setLoading(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
list.push(...tabData.value[currentTab.value].list);
|
||||||
|
setLoading(false);
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style scoped></style>
|
@@ -1,6 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<hover-container class="w-40px" :inverted="theme.header.inverted" tooltip-content="主题模式">
|
<hover-container class="w-40px" :inverted="theme.header.inverted" tooltip-content="主题模式">
|
||||||
<dark-mode-switch :dark="theme.darkMode" class="wh-full" @update:dark="theme.setDarkMode" />
|
<dark-mode-switch
|
||||||
|
:dark="theme.darkMode"
|
||||||
|
:customize-transition="theme.isCustomizeDarkModeTransition"
|
||||||
|
class="wh-full"
|
||||||
|
@update:dark="theme.setDarkMode"
|
||||||
|
/>
|
||||||
</hover-container>
|
</hover-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@@ -15,7 +15,7 @@ import { localStg } from '@/utils';
|
|||||||
const theme = useThemeStore();
|
const theme = useThemeStore();
|
||||||
const { locale } = useI18n();
|
const { locale } = useI18n();
|
||||||
|
|
||||||
const language = ref<I18nType.langType>(localStg.get('lang') || 'zh-CN');
|
const language = ref<I18nType.LangType>(localStg.get('lang') || 'zh-CN');
|
||||||
const options = [
|
const options = [
|
||||||
{
|
{
|
||||||
label: '中文',
|
label: '中文',
|
||||||
@@ -31,9 +31,9 @@ const options = [
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
const handleSelect = (key: string) => {
|
const handleSelect = (key: string) => {
|
||||||
language.value = key as I18nType.langType;
|
language.value = key as I18nType.LangType;
|
||||||
locale.value = key;
|
locale.value = key;
|
||||||
localStg.set('lang', key as I18nType.langType);
|
localStg.set('lang', key as I18nType.LangType);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
@@ -7,9 +7,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<header-menu v-else />
|
<header-menu v-else />
|
||||||
<div class="flex justify-end h-full">
|
<div class="flex justify-end h-full">
|
||||||
|
<global-search />
|
||||||
|
<github-site />
|
||||||
<full-screen />
|
<full-screen />
|
||||||
<theme-mode />
|
<theme-mode />
|
||||||
<toggle-lang />
|
<toggle-lang />
|
||||||
|
<system-message />
|
||||||
<setting-button v-if="showButton" />
|
<setting-button v-if="showButton" />
|
||||||
<user-avatar />
|
<user-avatar />
|
||||||
</div>
|
</div>
|
||||||
@@ -20,12 +23,15 @@
|
|||||||
import { useThemeStore } from '@/store';
|
import { useThemeStore } from '@/store';
|
||||||
import { useBasicLayout } from '@/composables';
|
import { useBasicLayout } from '@/composables';
|
||||||
import GlobalLogo from '../global-logo/index.vue';
|
import GlobalLogo from '../global-logo/index.vue';
|
||||||
|
import GlobalSearch from '../global-search/index.vue';
|
||||||
import {
|
import {
|
||||||
FullScreen,
|
FullScreen,
|
||||||
|
GithubSite,
|
||||||
GlobalBreadcrumb,
|
GlobalBreadcrumb,
|
||||||
HeaderMenu,
|
HeaderMenu,
|
||||||
MenuCollapse,
|
MenuCollapse,
|
||||||
SettingButton,
|
SettingButton,
|
||||||
|
SystemMessage,
|
||||||
ThemeMode,
|
ThemeMode,
|
||||||
UserAvatar,
|
UserAvatar,
|
||||||
ToggleLang
|
ToggleLang
|
||||||
|
@@ -2,14 +2,14 @@
|
|||||||
<router-link :to="routeHomePath" class="flex-center w-full nowrap-hidden">
|
<router-link :to="routeHomePath" class="flex-center w-full nowrap-hidden">
|
||||||
<system-logo class="text-32px text-primary" />
|
<system-logo class="text-32px text-primary" />
|
||||||
<h2 v-show="showTitle" class="pl-8px text-16px font-bold text-primary transition duration-300 ease-in-out">
|
<h2 v-show="showTitle" class="pl-8px text-16px font-bold text-primary transition duration-300 ease-in-out">
|
||||||
{{ t('message.system.title') }}
|
{{ $t('system.title') }}
|
||||||
</h2>
|
</h2>
|
||||||
</router-link>
|
</router-link>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { routePath } from '@/router';
|
import { routePath } from '@/router';
|
||||||
import { t } from '@/locales';
|
import { $t } from '@/locales';
|
||||||
|
|
||||||
defineOptions({ name: 'GlobalLogo' });
|
defineOptions({ name: 'GlobalLogo' });
|
||||||
|
|
||||||
|
3
src/layouts/common/global-search/components/index.ts
Normal file
3
src/layouts/common/global-search/components/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import SearchModal from './search-modal.vue';
|
||||||
|
|
||||||
|
export { SearchModal };
|
@@ -0,0 +1,30 @@
|
|||||||
|
<template>
|
||||||
|
<div class="px-24px h-44px flex-y-center">
|
||||||
|
<span class="mr-14px flex-y-center">
|
||||||
|
<icon-mdi-keyboard-return class="icon text-20px p-2px mr-6px" />
|
||||||
|
<span>确认</span>
|
||||||
|
</span>
|
||||||
|
<span class="mr-14px flex-y-center">
|
||||||
|
<icon-mdi-arrow-up-thin class="icon text-20px p-2px mr-5px" />
|
||||||
|
<icon-mdi-arrow-down-thin class="icon text-20px p-2px mr-6px" />
|
||||||
|
<span>切换</span>
|
||||||
|
</span>
|
||||||
|
<span class="flex-y-center">
|
||||||
|
<icon-mdi-keyboard-esc class="icon text-20px p-2px mr-6px" />
|
||||||
|
<span>关闭</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
defineOptions({ name: 'SearchFooter' });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.icon {
|
||||||
|
box-shadow:
|
||||||
|
inset 0 -2px #cdcde6,
|
||||||
|
inset 0 0 1px 1px #fff,
|
||||||
|
0 1px 2px 1px #1e235a66;
|
||||||
|
}
|
||||||
|
</style>
|
147
src/layouts/common/global-search/components/search-modal.vue
Normal file
147
src/layouts/common/global-search/components/search-modal.vue
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
<template>
|
||||||
|
<n-modal
|
||||||
|
v-model:show="show"
|
||||||
|
:segmented="{ footer: 'soft' }"
|
||||||
|
:closable="false"
|
||||||
|
preset="card"
|
||||||
|
footer-style="padding: 0; margin: 0"
|
||||||
|
class="fixed left-0 right-0"
|
||||||
|
:class="[isMobile ? 'wh-full top-0px rounded-0' : 'w-630px top-50px']"
|
||||||
|
@after-leave="handleClose"
|
||||||
|
>
|
||||||
|
<n-input-group>
|
||||||
|
<n-input ref="inputRef" v-model:value="keyword" clearable placeholder="请输入关键词搜索" @input="handleSearch">
|
||||||
|
<template #prefix>
|
||||||
|
<icon-uil-search class="text-15px text-#c2c2c2" />
|
||||||
|
</template>
|
||||||
|
</n-input>
|
||||||
|
<n-button v-if="isMobile" type="primary" ghost @click="handleClose">取消</n-button>
|
||||||
|
</n-input-group>
|
||||||
|
|
||||||
|
<div class="mt-20px">
|
||||||
|
<n-empty v-if="resultOptions.length === 0" description="暂无搜索结果" />
|
||||||
|
<search-result v-else v-model:value="activePath" :options="resultOptions" @enter="handleEnter" />
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<search-footer v-if="!isMobile" />
|
||||||
|
</template>
|
||||||
|
</n-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, nextTick, ref, shallowRef, watch } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { onKeyStroke, useDebounceFn } from '@vueuse/core';
|
||||||
|
import { useRouteStore } from '@/store';
|
||||||
|
import { useBasicLayout } from '@/composables';
|
||||||
|
import SearchResult from './search-result.vue';
|
||||||
|
import SearchFooter from './search-footer.vue';
|
||||||
|
|
||||||
|
defineOptions({ name: 'SearchModal' });
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
/** 弹窗显隐 */
|
||||||
|
value: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
interface Emits {
|
||||||
|
(e: 'update:value', val: boolean): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits<Emits>();
|
||||||
|
|
||||||
|
const { isMobile } = useBasicLayout();
|
||||||
|
const router = useRouter();
|
||||||
|
const routeStore = useRouteStore();
|
||||||
|
|
||||||
|
const keyword = ref('');
|
||||||
|
const activePath = ref('');
|
||||||
|
const resultOptions = shallowRef<AuthRoute.Route[]>([]);
|
||||||
|
const inputRef = ref<HTMLInputElement>();
|
||||||
|
|
||||||
|
const handleSearch = useDebounceFn(search, 300);
|
||||||
|
|
||||||
|
const show = computed({
|
||||||
|
get() {
|
||||||
|
return props.value;
|
||||||
|
},
|
||||||
|
set(val: boolean) {
|
||||||
|
emit('update:value', val);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(show, async val => {
|
||||||
|
if (val) {
|
||||||
|
/** 自动聚焦 */
|
||||||
|
await nextTick();
|
||||||
|
inputRef.value?.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 查询 */
|
||||||
|
function search() {
|
||||||
|
resultOptions.value = routeStore.searchMenus.filter(
|
||||||
|
menu => keyword.value && menu.meta?.title.toLocaleLowerCase().includes(keyword.value.toLocaleLowerCase().trim())
|
||||||
|
);
|
||||||
|
if (resultOptions.value?.length > 0) {
|
||||||
|
activePath.value = resultOptions.value[0].path;
|
||||||
|
} else {
|
||||||
|
activePath.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClose() {
|
||||||
|
show.value = false;
|
||||||
|
/** 延时处理防止用户看到某些操作 */
|
||||||
|
setTimeout(() => {
|
||||||
|
resultOptions.value = [];
|
||||||
|
keyword.value = '';
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** key up */
|
||||||
|
function handleUp() {
|
||||||
|
const { length } = resultOptions.value;
|
||||||
|
if (length === 0) return;
|
||||||
|
const index = resultOptions.value.findIndex(item => item.path === activePath.value);
|
||||||
|
if (index === 0) {
|
||||||
|
activePath.value = resultOptions.value[length - 1].path;
|
||||||
|
} else {
|
||||||
|
activePath.value = resultOptions.value[index - 1].path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** key down */
|
||||||
|
function handleDown() {
|
||||||
|
const { length } = resultOptions.value;
|
||||||
|
if (length === 0) return;
|
||||||
|
const index = resultOptions.value.findIndex(item => item.path === activePath.value);
|
||||||
|
if (index + 1 === length) {
|
||||||
|
activePath.value = resultOptions.value[0].path;
|
||||||
|
} else {
|
||||||
|
activePath.value = resultOptions.value[index + 1].path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** key enter */
|
||||||
|
function handleEnter() {
|
||||||
|
const { length } = resultOptions.value;
|
||||||
|
if (length === 0 || activePath.value === '') return;
|
||||||
|
const routeItem = resultOptions.value.find(item => item.path === activePath.value);
|
||||||
|
if (routeItem?.meta?.href) {
|
||||||
|
window.open(activePath.value, '__blank');
|
||||||
|
} else {
|
||||||
|
router.push(activePath.value);
|
||||||
|
handleClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onKeyStroke('Escape', handleClose);
|
||||||
|
onKeyStroke('Enter', handleEnter);
|
||||||
|
onKeyStroke('ArrowUp', handleUp);
|
||||||
|
onKeyStroke('ArrowDown', handleDown);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
@@ -0,0 +1,64 @@
|
|||||||
|
<template>
|
||||||
|
<n-scrollbar>
|
||||||
|
<div class="pb-12px">
|
||||||
|
<template v-for="item in options" :key="item.path">
|
||||||
|
<div
|
||||||
|
class="bg-#e5e7eb dark:bg-dark h-56px mt-8px px-14px rounded-4px cursor-pointer flex-y-center justify-between"
|
||||||
|
:style="{
|
||||||
|
background: item.path === active ? theme.themeColor : '',
|
||||||
|
color: item.path === active ? '#fff' : ''
|
||||||
|
}"
|
||||||
|
@click="handleTo"
|
||||||
|
@mouseenter="handleMouse(item)"
|
||||||
|
>
|
||||||
|
<svg-icon :icon="item.meta.icon" :local-icon="item.meta.localIcon" />
|
||||||
|
<span class="flex-1 ml-5px">{{ item.meta?.title }}</span>
|
||||||
|
<icon-ant-design-enter-outlined class="icon text-20px p-2px mr-3px" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</n-scrollbar>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { useThemeStore } from '@/store';
|
||||||
|
|
||||||
|
defineOptions({ name: 'SearchResult' });
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
value: string;
|
||||||
|
options: AuthRoute.Route[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
interface Emits {
|
||||||
|
(e: 'update:value', val: string): void;
|
||||||
|
(e: 'enter'): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits<Emits>();
|
||||||
|
|
||||||
|
const theme = useThemeStore();
|
||||||
|
|
||||||
|
const active = computed({
|
||||||
|
get() {
|
||||||
|
return props.value;
|
||||||
|
},
|
||||||
|
set(val: string) {
|
||||||
|
emit('update:value', val);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 鼠标移入 */
|
||||||
|
async function handleMouse(item: AuthRoute.Route) {
|
||||||
|
active.value = item.path;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTo() {
|
||||||
|
emit('enter');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
30
src/layouts/common/global-search/index.vue
Normal file
30
src/layouts/common/global-search/index.vue
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<hover-container
|
||||||
|
class="w-40px h-full"
|
||||||
|
tooltip-content="搜索"
|
||||||
|
:inverted="theme.header.inverted"
|
||||||
|
@click="handleSearch"
|
||||||
|
>
|
||||||
|
<icon-uil-search class="text-20px" />
|
||||||
|
</hover-container>
|
||||||
|
<search-modal v-model:value="show" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { useThemeStore } from '@/store';
|
||||||
|
import { useBoolean } from '@/hooks';
|
||||||
|
import { SearchModal } from './components';
|
||||||
|
|
||||||
|
defineOptions({ name: 'GlobalSearch' });
|
||||||
|
|
||||||
|
const { bool: show, toggle } = useBoolean();
|
||||||
|
const theme = useThemeStore();
|
||||||
|
|
||||||
|
function handleSearch() {
|
||||||
|
toggle();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
@@ -9,7 +9,7 @@
|
|||||||
:style="{ width: showDrawer ? theme.sider.mixChildMenuWidth + 'px' : '0px' }"
|
:style="{ width: showDrawer ? theme.sider.mixChildMenuWidth + 'px' : '0px' }"
|
||||||
>
|
>
|
||||||
<header class="header-height flex-y-center justify-between" :style="{ height: theme.header.height + 'px' }">
|
<header class="header-height flex-y-center justify-between" :style="{ height: theme.header.height + 'px' }">
|
||||||
<h2 class="text-primary pl-8px text-16px font-bold">{{ title }}</h2>
|
<h2 class="text-primary pl-8px text-16px font-bold">{{ $t('system.title') }}</h2>
|
||||||
<div class="px-8px text-16px text-gray-600 cursor-pointer" @click="app.toggleMixSiderFixed">
|
<div class="px-8px text-16px text-gray-600 cursor-pointer" @click="app.toggleMixSiderFixed">
|
||||||
<icon-mdi-pin-off v-if="app.mixSiderFixed" />
|
<icon-mdi-pin-off v-if="app.mixSiderFixed" />
|
||||||
<icon-mdi-pin v-else />
|
<icon-mdi-pin v-else />
|
||||||
@@ -35,8 +35,9 @@ import { computed, ref, watch } from 'vue';
|
|||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
import type { MenuOption } from 'naive-ui';
|
import type { MenuOption } from 'naive-ui';
|
||||||
import { useAppStore, useThemeStore } from '@/store';
|
import { useAppStore, useThemeStore } from '@/store';
|
||||||
import { useAppInfo, useRouterPush } from '@/composables';
|
import { useRouterPush } from '@/composables';
|
||||||
import { getActiveKeyPathsOfMenus } from '@/utils';
|
import { getActiveKeyPathsOfMenus } from '@/utils';
|
||||||
|
import { $t } from '@/locales';
|
||||||
|
|
||||||
defineOptions({ name: 'MixMenuDrawer' });
|
defineOptions({ name: 'MixMenuDrawer' });
|
||||||
|
|
||||||
@@ -53,7 +54,6 @@ const route = useRoute();
|
|||||||
const app = useAppStore();
|
const app = useAppStore();
|
||||||
const theme = useThemeStore();
|
const theme = useThemeStore();
|
||||||
const { routerPush } = useRouterPush();
|
const { routerPush } = useRouterPush();
|
||||||
const { title } = useAppInfo();
|
|
||||||
|
|
||||||
const showDrawer = computed(() => (props.visible && props.menus.length) || app.mixSiderFixed);
|
const showDrawer = computed(() => (props.visible && props.menus.length) || app.mixSiderFixed);
|
||||||
|
|
||||||
|
@@ -28,7 +28,7 @@ import { useRouterPush } from '@/composables';
|
|||||||
import { useBoolean } from '@/hooks';
|
import { useBoolean } from '@/hooks';
|
||||||
import { translateMenuLabel } from '@/utils';
|
import { translateMenuLabel } from '@/utils';
|
||||||
import { GlobalLogo } from '@/layouts/common';
|
import { GlobalLogo } from '@/layouts/common';
|
||||||
import { t } from '@/locales';
|
import { $t } from '@/locales';
|
||||||
import { MixMenuCollapse, MixMenuDetail, MixMenuDrawer } from './components';
|
import { MixMenuCollapse, MixMenuDetail, MixMenuDrawer } from './components';
|
||||||
|
|
||||||
defineOptions({ name: 'VerticalMixSider' });
|
defineOptions({ name: 'VerticalMixSider' });
|
||||||
@@ -53,7 +53,7 @@ const firstDegreeMenus = computed(() =>
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
routeName,
|
routeName,
|
||||||
label: i18nTitle ? t(i18nTitle) : label,
|
label: i18nTitle ? $t(i18nTitle) : label,
|
||||||
icon,
|
icon,
|
||||||
hasChildren
|
hasChildren
|
||||||
};
|
};
|
||||||
|
@@ -6,26 +6,21 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
import { useAppStore, useRouteStore } from '@/store';
|
import { useRouteStore } from '@/store';
|
||||||
import { useLoading } from '@/hooks';
|
import { useLoading } from '@/hooks';
|
||||||
|
|
||||||
defineOptions({ name: 'ReloadButton' });
|
defineOptions({ name: 'ReloadButton' });
|
||||||
|
|
||||||
const app = useAppStore();
|
const { reCacheRoute } = useRouteStore();
|
||||||
const routeStore = useRouteStore();
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const { loading, startLoading, endLoading } = useLoading();
|
const { loading, startLoading, endLoading } = useLoading();
|
||||||
|
|
||||||
function handleRefresh() {
|
async function handleRefresh() {
|
||||||
const isCached = routeStore.cacheRoutes.includes(String(route.name));
|
|
||||||
if (isCached) {
|
|
||||||
routeStore.removeCacheRoute(route.name as AuthRoute.AllRouteKey);
|
|
||||||
}
|
|
||||||
startLoading();
|
startLoading();
|
||||||
app.reloadPage();
|
|
||||||
|
await reCacheRoute(route.name as AuthRoute.AllRouteKey);
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (isCached) {
|
|
||||||
routeStore.addCacheRoute(route.name as AuthRoute.AllRouteKey);
|
|
||||||
}
|
|
||||||
endLoading();
|
endLoading();
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
|
@@ -19,7 +19,7 @@
|
|||||||
class="inline-block align-text-bottom text-16px"
|
class="inline-block align-text-bottom text-16px"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
{{ item.meta.i18nTitle ? t(item.meta.i18nTitle) : item.meta.title }}
|
{{ item.meta.i18nTitle ? $t(item.meta.i18nTitle) : item.meta.title }}
|
||||||
</PageTab>
|
</PageTab>
|
||||||
</div>
|
</div>
|
||||||
<context-menu
|
<context-menu
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
import { computed, nextTick, reactive, ref, watch } from 'vue';
|
import { computed, nextTick, reactive, ref, watch } from 'vue';
|
||||||
import { PageTab } from '@soybeanjs/vue-materials';
|
import { PageTab } from '@soybeanjs/vue-materials';
|
||||||
import { useTabStore, useThemeStore } from '@/store';
|
import { useTabStore, useThemeStore } from '@/store';
|
||||||
import { t } from '@/locales';
|
import { $t } from '@/locales';
|
||||||
import { ContextMenu } from './components';
|
import { ContextMenu } from './components';
|
||||||
|
|
||||||
defineOptions({ name: 'TabDetail' });
|
defineOptions({ name: 'TabDetail' });
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<n-divider title-placement="center">主题模式</n-divider>
|
<n-divider title-placement="center">{{ $t('layout.settingDrawer.themeModeTitle') }}</n-divider>
|
||||||
<n-space vertical size="large">
|
<n-space vertical size="large">
|
||||||
<setting-menu label="深色主题">
|
<setting-menu :label="$t('layout.settingDrawer.darkMode')">
|
||||||
<n-switch :value="theme.darkMode" @update:value="theme.setDarkMode">
|
<n-switch :value="theme.darkMode" @update:value="theme.setDarkMode">
|
||||||
<template #checked>
|
<template #checked>
|
||||||
<icon-mdi-white-balance-sunny class="text-14px text-white" />
|
<icon-mdi-white-balance-sunny class="text-14px text-white" />
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</n-switch>
|
</n-switch>
|
||||||
</setting-menu>
|
</setting-menu>
|
||||||
<setting-menu label="跟随系统">
|
<setting-menu :label="$t('layout.settingDrawer.followSystemTheme')">
|
||||||
<n-switch :value="theme.followSystemTheme" @update:value="theme.setFollowSystemTheme">
|
<n-switch :value="theme.followSystemTheme" @update:value="theme.setFollowSystemTheme">
|
||||||
<template #checked>
|
<template #checked>
|
||||||
<icon-ic-baseline-do-not-disturb class="text-14px text-white" />
|
<icon-ic-baseline-do-not-disturb class="text-14px text-white" />
|
||||||
@@ -21,13 +21,23 @@
|
|||||||
</template>
|
</template>
|
||||||
</n-switch>
|
</n-switch>
|
||||||
</setting-menu>
|
</setting-menu>
|
||||||
<setting-menu label="侧边栏深色">
|
<setting-menu :label="$t('layout.settingDrawer.isCustomizeDarkModeTransition')">
|
||||||
|
<n-switch :value="theme.isCustomizeDarkModeTransition" @update:value="theme.setIsCustomizeDarkModeTransition">
|
||||||
|
<template #checked>
|
||||||
|
<icon-ic-baseline-do-not-disturb class="text-14px text-white" />
|
||||||
|
</template>
|
||||||
|
<template #unchecked>
|
||||||
|
<icon-ic-round-hdr-auto class="text-14px text-white" />
|
||||||
|
</template>
|
||||||
|
</n-switch>
|
||||||
|
</setting-menu>
|
||||||
|
<setting-menu :label="$t('layout.settingDrawer.sider.inverted')">
|
||||||
<n-switch :value="theme.sider.inverted" @update:value="theme.setSiderInverted" />
|
<n-switch :value="theme.sider.inverted" @update:value="theme.setSiderInverted" />
|
||||||
</setting-menu>
|
</setting-menu>
|
||||||
<setting-menu label="头部深色">
|
<setting-menu :label="$t('layout.settingDrawer.header.inverted')">
|
||||||
<n-switch :value="theme.header.inverted" @update:value="theme.setHeaderInverted" />
|
<n-switch :value="theme.header.inverted" @update:value="theme.setHeaderInverted" />
|
||||||
</setting-menu>
|
</setting-menu>
|
||||||
<setting-menu label="底部深色">
|
<setting-menu :label="$t('layout.settingDrawer.footer.inverted')">
|
||||||
<n-switch :value="theme.footer.inverted" @update:value="theme.setFooterInverted" />
|
<n-switch :value="theme.footer.inverted" @update:value="theme.setFooterInverted" />
|
||||||
</setting-menu>
|
</setting-menu>
|
||||||
</n-space>
|
</n-space>
|
||||||
@@ -35,6 +45,7 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { useThemeStore } from '@/store';
|
import { useThemeStore } from '@/store';
|
||||||
|
import { $t } from '@/locales';
|
||||||
import SettingMenu from '../setting-menu/index.vue';
|
import SettingMenu from '../setting-menu/index.vue';
|
||||||
|
|
||||||
defineOptions({ name: 'DarkMode' });
|
defineOptions({ name: 'DarkMode' });
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<n-divider title-placement="center">布局模式</n-divider>
|
<n-divider title-placement="center">{{ $t('layout.settingDrawer.layoutModelTitle') }}</n-divider>
|
||||||
<n-space justify="space-around" :wrap="true" :size="24" class="px-12px">
|
<n-space justify="space-around" :wrap="true" :size="24" class="px-12px">
|
||||||
<layout-card
|
<layout-card
|
||||||
v-for="item in theme.layout.modeList"
|
v-for="item in theme.layout.modeList"
|
||||||
@@ -43,6 +43,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useThemeStore } from '@/store';
|
import { useThemeStore } from '@/store';
|
||||||
|
import { $t } from '@/locales';
|
||||||
import { LayoutCard } from './components';
|
import { LayoutCard } from './components';
|
||||||
|
|
||||||
defineOptions({ name: 'LayoutMode' });
|
defineOptions({ name: 'LayoutMode' });
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<n-divider title-placement="center">界面功能</n-divider>
|
<n-divider title-placement="center">{{ $t('layout.settingDrawer.pageFunctionsTitle') }}</n-divider>
|
||||||
<n-space vertical size="large">
|
<n-space vertical size="large">
|
||||||
<setting-menu label="滚动模式">
|
<setting-menu :label="$t('layout.settingDrawer.scrollMode')">
|
||||||
<n-select
|
<n-select
|
||||||
class="w-120px"
|
class="w-120px"
|
||||||
size="small"
|
size="small"
|
||||||
@@ -10,10 +10,10 @@
|
|||||||
@update:value="theme.setScrollMode"
|
@update:value="theme.setScrollMode"
|
||||||
/>
|
/>
|
||||||
</setting-menu>
|
</setting-menu>
|
||||||
<setting-menu label="固定头部和多页签">
|
<setting-menu :label="$t('layout.settingDrawer.fixedHeaderAndTab')">
|
||||||
<n-switch :value="theme.fixedHeaderAndTab" @update:value="theme.setIsFixedHeaderAndTab" />
|
<n-switch :value="theme.fixedHeaderAndTab" @update:value="theme.setIsFixedHeaderAndTab" />
|
||||||
</setting-menu>
|
</setting-menu>
|
||||||
<setting-menu label="顶部菜单位置">
|
<setting-menu :label="$t('layout.settingDrawer.menu.horizontalPosition')">
|
||||||
<n-select
|
<n-select
|
||||||
class="w-120px"
|
class="w-120px"
|
||||||
size="small"
|
size="small"
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
@update:value="theme.setHorizontalMenuPosition"
|
@update:value="theme.setHorizontalMenuPosition"
|
||||||
/>
|
/>
|
||||||
</setting-menu>
|
</setting-menu>
|
||||||
<setting-menu label="头部高度">
|
<setting-menu :label="$t('layout.settingDrawer.header.height')">
|
||||||
<n-input-number
|
<n-input-number
|
||||||
class="w-120px"
|
class="w-120px"
|
||||||
size="small"
|
size="small"
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
@update:value="theme.setHeaderHeight"
|
@update:value="theme.setHeaderHeight"
|
||||||
/>
|
/>
|
||||||
</setting-menu>
|
</setting-menu>
|
||||||
<setting-menu label="多页签高度">
|
<setting-menu :label="$t('layout.settingDrawer.tab.height')">
|
||||||
<n-input-number
|
<n-input-number
|
||||||
class="w-120px"
|
class="w-120px"
|
||||||
size="small"
|
size="small"
|
||||||
@@ -40,10 +40,10 @@
|
|||||||
@update:value="theme.setTabHeight"
|
@update:value="theme.setTabHeight"
|
||||||
/>
|
/>
|
||||||
</setting-menu>
|
</setting-menu>
|
||||||
<setting-menu label="多页签缓存">
|
<setting-menu :label="$t('layout.settingDrawer.tab.isCache')">
|
||||||
<n-switch :value="theme.tab.isCache" @update:value="theme.setTabIsCache" />
|
<n-switch :value="theme.tab.isCache" @update:value="theme.setTabIsCache" />
|
||||||
</setting-menu>
|
</setting-menu>
|
||||||
<setting-menu label="侧边栏展开宽度">
|
<setting-menu :label="$t('layout.settingDrawer.sider.width')">
|
||||||
<n-input-number
|
<n-input-number
|
||||||
class="w-120px"
|
class="w-120px"
|
||||||
size="small"
|
size="small"
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
@update:value="theme.setSiderWidth"
|
@update:value="theme.setSiderWidth"
|
||||||
/>
|
/>
|
||||||
</setting-menu>
|
</setting-menu>
|
||||||
<setting-menu label="左侧混合侧边栏展开宽度">
|
<setting-menu :label="$t('layout.settingDrawer.sider.mixWidth')">
|
||||||
<n-input-number
|
<n-input-number
|
||||||
class="w-120px"
|
class="w-120px"
|
||||||
size="small"
|
size="small"
|
||||||
@@ -61,13 +61,13 @@
|
|||||||
@update:value="theme.setMixSiderWidth"
|
@update:value="theme.setMixSiderWidth"
|
||||||
/>
|
/>
|
||||||
</setting-menu>
|
</setting-menu>
|
||||||
<setting-menu label="显示底部">
|
<setting-menu :label="$t('layout.settingDrawer.footer.visible')">
|
||||||
<n-switch :value="theme.footer.visible" @update:value="theme.setFooterVisible" />
|
<n-switch :value="theme.footer.visible" @update:value="theme.setFooterVisible" />
|
||||||
</setting-menu>
|
</setting-menu>
|
||||||
<setting-menu label="固定底部">
|
<setting-menu :label="$t('layout.settingDrawer.footer.fixed')">
|
||||||
<n-switch :value="theme.footer.fixed" @update:value="theme.setFooterIsFixed" />
|
<n-switch :value="theme.footer.fixed" @update:value="theme.setFooterIsFixed" />
|
||||||
</setting-menu>
|
</setting-menu>
|
||||||
<setting-menu label="底部居右">
|
<setting-menu :label="$t('layout.settingDrawer.footer.right')">
|
||||||
<n-switch :value="theme.footer.right" @update:value="theme.setFooterIsRight" />
|
<n-switch :value="theme.footer.right" @update:value="theme.setFooterIsRight" />
|
||||||
</setting-menu>
|
</setting-menu>
|
||||||
</n-space>
|
</n-space>
|
||||||
@@ -75,6 +75,7 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { useThemeStore } from '@/store';
|
import { useThemeStore } from '@/store';
|
||||||
|
import { $t } from '@/locales';
|
||||||
import SettingMenu from '../setting-menu/index.vue';
|
import SettingMenu from '../setting-menu/index.vue';
|
||||||
|
|
||||||
defineOptions({ name: 'PageFunc' });
|
defineOptions({ name: 'PageFunc' });
|
||||||
|
@@ -1,16 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<n-divider title-placement="center">界面显示</n-divider>
|
<n-divider title-placement="center">{{ $t('layout.settingDrawer.pageViewTitle') }}</n-divider>
|
||||||
<n-space vertical size="large">
|
<n-space vertical size="large">
|
||||||
<setting-menu label="面包屑">
|
<setting-menu :label="$t('layout.settingDrawer.header.crumb.visible')">
|
||||||
<n-switch :value="theme.header.crumb.visible" @update:value="theme.setHeaderCrumbVisible" />
|
<n-switch :value="theme.header.crumb.visible" @update:value="theme.setHeaderCrumbVisible" />
|
||||||
</setting-menu>
|
</setting-menu>
|
||||||
<setting-menu label="面包屑图标">
|
<setting-menu :label="$t('layout.settingDrawer.header.crumb.icon')">
|
||||||
<n-switch :value="theme.header.crumb.showIcon" @update:value="theme.setHeaderCrumbIconVisible" />
|
<n-switch :value="theme.header.crumb.showIcon" @update:value="theme.setHeaderCrumbIconVisible" />
|
||||||
</setting-menu>
|
</setting-menu>
|
||||||
<setting-menu label="多页签">
|
<setting-menu :label="$t('layout.settingDrawer.tab.visible')">
|
||||||
<n-switch :value="theme.tab.visible" @update:value="theme.setTabVisible" />
|
<n-switch :value="theme.tab.visible" @update:value="theme.setTabVisible" />
|
||||||
</setting-menu>
|
</setting-menu>
|
||||||
<setting-menu label="多页签风格">
|
<setting-menu :label="$t('layout.settingDrawer.tab.modeList.mode')">
|
||||||
<n-select
|
<n-select
|
||||||
class="w-120px"
|
class="w-120px"
|
||||||
size="small"
|
size="small"
|
||||||
@@ -19,10 +19,10 @@
|
|||||||
@update:value="theme.setTabMode"
|
@update:value="theme.setTabMode"
|
||||||
/>
|
/>
|
||||||
</setting-menu>
|
</setting-menu>
|
||||||
<setting-menu label="页面切换动画">
|
<setting-menu :label="$t('layout.settingDrawer.page.animate')">
|
||||||
<n-switch :value="theme.page.animate" @update:value="theme.setPageIsAnimate" />
|
<n-switch :value="theme.page.animate" @update:value="theme.setPageIsAnimate" />
|
||||||
</setting-menu>
|
</setting-menu>
|
||||||
<setting-menu label="页面切换动画类型">
|
<setting-menu :label="$t('layout.settingDrawer.page.animateMode')">
|
||||||
<n-select
|
<n-select
|
||||||
class="w-120px"
|
class="w-120px"
|
||||||
size="small"
|
size="small"
|
||||||
@@ -36,6 +36,7 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { useThemeStore } from '@/store';
|
import { useThemeStore } from '@/store';
|
||||||
|
import { $t } from '@/locales';
|
||||||
import SettingMenu from '../setting-menu/index.vue';
|
import SettingMenu from '../setting-menu/index.vue';
|
||||||
|
|
||||||
defineOptions({ name: 'PageView' });
|
defineOptions({ name: 'PageView' });
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<n-divider title-placement="center">系统主题</n-divider>
|
<n-divider title-placement="center">{{ $t('layout.settingDrawer.systemThemeTitle') }}</n-divider>
|
||||||
<n-grid :cols="8" :x-gap="8" :y-gap="12">
|
<n-grid :cols="8" :x-gap="8" :y-gap="12">
|
||||||
<n-grid-item v-for="color in theme.themeColorList" :key="color" class="flex-x-center">
|
<n-grid-item v-for="color in theme.themeColorList" :key="color" class="flex-x-center">
|
||||||
<color-checkbox :color="color" :checked="color === theme.themeColor" @click="theme.setThemeColor(color)" />
|
<color-checkbox :color="color" :checked="color === theme.themeColor" @click="theme.setThemeColor(color)" />
|
||||||
@@ -7,7 +7,9 @@
|
|||||||
</n-grid>
|
</n-grid>
|
||||||
<n-space :vertical="true" class="pt-12px">
|
<n-space :vertical="true" class="pt-12px">
|
||||||
<n-color-picker :value="theme.themeColor" :show-alpha="false" @update-value="theme.setThemeColor" />
|
<n-color-picker :value="theme.themeColor" :show-alpha="false" @update-value="theme.setThemeColor" />
|
||||||
<n-button :block="true" :type="otherColorBtnType" @click="openModal">更多颜色</n-button>
|
<n-button :block="true" :type="otherColorBtnType" @click="openModal">
|
||||||
|
{{ $t('layout.settingDrawer.systemTheme.moreColors') }}
|
||||||
|
</n-button>
|
||||||
</n-space>
|
</n-space>
|
||||||
<color-modal :visible="visible" @close="closeModal" />
|
<color-modal :visible="visible" @close="closeModal" />
|
||||||
</template>
|
</template>
|
||||||
@@ -17,6 +19,7 @@ import { computed } from 'vue';
|
|||||||
import { isInTraditionColors } from '@/settings';
|
import { isInTraditionColors } from '@/settings';
|
||||||
import { useThemeStore } from '@/store';
|
import { useThemeStore } from '@/store';
|
||||||
import { useBoolean } from '@/hooks';
|
import { useBoolean } from '@/hooks';
|
||||||
|
import { $t } from '@/locales';
|
||||||
import { ColorCheckbox, ColorModal } from './components';
|
import { ColorCheckbox, ColorModal } from './components';
|
||||||
|
|
||||||
defineOptions({ name: 'ThemeColorSelect' });
|
defineOptions({ name: 'ThemeColorSelect' });
|
||||||
|
@@ -1,11 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<n-divider title-placement="center">主题配置</n-divider>
|
<n-divider title-placement="center">{{ $t('layout.settingDrawer.themeConfiguration.title') }}</n-divider>
|
||||||
<textarea id="themeConfigCopyTarget" v-model="dataClipboardText" class="absolute opacity-0" />
|
<textarea id="themeConfigCopyTarget" v-model="dataClipboardText" class="absolute opacity-0" />
|
||||||
<n-space vertical>
|
<n-space vertical>
|
||||||
<div ref="copyRef" data-clipboard-target="#themeConfigCopyTarget">
|
<div ref="copyRef" data-clipboard-target="#themeConfigCopyTarget">
|
||||||
<n-button type="primary" :block="true">拷贝当前配置</n-button>
|
<n-button type="primary" :block="true">{{ $t('layout.settingDrawer.themeConfiguration.copy') }}</n-button>
|
||||||
</div>
|
</div>
|
||||||
<n-button type="warning" :block="true" @click="handleResetConfig">重置当前配置</n-button>
|
<n-button type="warning" :block="true" @click="handleResetConfig">
|
||||||
|
{{ $t('layout.settingDrawer.themeConfiguration.reset') }}
|
||||||
|
</n-button>
|
||||||
</n-space>
|
</n-space>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -13,6 +15,7 @@
|
|||||||
import { onMounted, onUnmounted, ref, watch } from 'vue';
|
import { onMounted, onUnmounted, ref, watch } from 'vue';
|
||||||
import Clipboard from 'clipboard';
|
import Clipboard from 'clipboard';
|
||||||
import { useThemeStore } from '@/store';
|
import { useThemeStore } from '@/store';
|
||||||
|
import { $t } from '@/locales';
|
||||||
|
|
||||||
defineOptions({ name: 'ThemeConfig' });
|
defineOptions({ name: 'ThemeConfig' });
|
||||||
|
|
||||||
@@ -28,7 +31,7 @@ function getClipboardText() {
|
|||||||
|
|
||||||
function handleResetConfig() {
|
function handleResetConfig() {
|
||||||
theme.resetThemeStore();
|
theme.resetThemeStore();
|
||||||
window.$message?.success('已重置配置,请重新拷贝!');
|
window.$message?.success($t('layout.settingDrawer.themeConfiguration.resetSuccess'));
|
||||||
}
|
}
|
||||||
|
|
||||||
function clipboardEventListener() {
|
function clipboardEventListener() {
|
||||||
@@ -36,9 +39,9 @@ function clipboardEventListener() {
|
|||||||
const copy = new Clipboard(copyRef.value);
|
const copy = new Clipboard(copyRef.value);
|
||||||
copy.on('success', () => {
|
copy.on('success', () => {
|
||||||
window.$dialog?.success({
|
window.$dialog?.success({
|
||||||
title: '操作成功',
|
title: $t('layout.settingDrawer.themeConfiguration.operateSuccess'),
|
||||||
content: '复制成功,请替换 src/settings/theme.json的内容!',
|
content: $t('layout.settingDrawer.themeConfiguration.copySuccess'),
|
||||||
positiveText: '确定'
|
positiveText: $t('layout.settingDrawer.themeConfiguration.confirmCopy')
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<n-drawer :show="app.settingDrawerVisible" display-directive="show" :width="330" @mask-click="app.closeSettingDrawer">
|
<n-drawer :show="app.settingDrawerVisible" display-directive="show" :width="330" @mask-click="app.closeSettingDrawer">
|
||||||
<n-drawer-content title="主题配置" :native-scrollbar="false">
|
<n-drawer-content :title="$t('layout.settingDrawer.title')" :native-scrollbar="false">
|
||||||
<dark-mode />
|
<dark-mode />
|
||||||
<layout-mode />
|
<layout-mode />
|
||||||
<theme-color-select />
|
<theme-color-select />
|
||||||
@@ -14,6 +14,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useAppStore } from '@/store';
|
import { useAppStore } from '@/store';
|
||||||
|
import { $t } from '@/locales';
|
||||||
import { DarkMode, DrawerButton, LayoutMode, PageFunc, PageView, ThemeColorSelect, ThemeConfig } from './components';
|
import { DarkMode, DrawerButton, LayoutMode, PageFunc, PageView, ThemeColorSelect, ThemeConfig } from './components';
|
||||||
|
|
||||||
defineOptions({ name: 'SettingDrawer' });
|
defineOptions({ name: 'SettingDrawer' });
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
import type { App } from 'vue';
|
import type { App } from 'vue';
|
||||||
import { createI18n } from 'vue-i18n';
|
import { createI18n } from 'vue-i18n';
|
||||||
import { localStg } from '@/utils';
|
import type { TranslateOptions } from 'vue-i18n';
|
||||||
import messages from './lang';
|
import { localStg } from '@/utils/storage';
|
||||||
import type { LocaleKey } from './lang';
|
import messages from './locale';
|
||||||
|
|
||||||
const i18n = createI18n({
|
const i18n = createI18n({
|
||||||
locale: localStg.get('lang') || 'zh-CN',
|
locale: localStg.get('lang') || 'zh-CN',
|
||||||
@@ -15,10 +15,20 @@ export function setupI18n(app: App) {
|
|||||||
app.use(i18n);
|
app.use(i18n);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function t(key: string) {
|
interface T {
|
||||||
return i18n.global.t(key);
|
(key: I18nType.I18nKey): string;
|
||||||
|
(key: I18nType.I18nKey, plural: number, options?: TranslateOptions<I18nType.LangType>): string;
|
||||||
|
(key: I18nType.I18nKey, defaultMsg: string, options?: TranslateOptions<I18nType.I18nKey>): string;
|
||||||
|
(key: I18nType.I18nKey, list: unknown[], options?: TranslateOptions<I18nType.I18nKey>): string;
|
||||||
|
(key: I18nType.I18nKey, list: unknown[], plural: number): string;
|
||||||
|
(key: I18nType.I18nKey, list: unknown[], defaultMsg: string): string;
|
||||||
|
(key: I18nType.I18nKey, named: Record<string, unknown>, options?: TranslateOptions<I18nType.LangType>): string;
|
||||||
|
(key: I18nType.I18nKey, named: Record<string, unknown>, plural: number): string;
|
||||||
|
(key: I18nType.I18nKey, named: Record<string, unknown>, defaultMsg: string): string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setLocale(locale: LocaleKey) {
|
export const $t = i18n.global.t as T;
|
||||||
|
|
||||||
|
export function setLocale(locale: I18nType.LangType) {
|
||||||
i18n.global.locale.value = locale;
|
i18n.global.locale.value = locale;
|
||||||
}
|
}
|
||||||
|
@@ -1,83 +1,217 @@
|
|||||||
import type { LocaleMessages } from 'vue-i18n';
|
const locale: I18nType.Schema = {
|
||||||
|
system: {
|
||||||
const locale: LocaleMessages<I18nType.Schema> = {
|
title: 'SoybeanAdmin'
|
||||||
message: {
|
},
|
||||||
system: {
|
common: {
|
||||||
title: 'SoybeanAdmin'
|
add: 'Add',
|
||||||
|
addSuccess: 'Add Success',
|
||||||
|
edit: 'Edit',
|
||||||
|
editSuccess: 'Edit Success',
|
||||||
|
delete: 'Delete',
|
||||||
|
deleteSuccess: 'Delete Success',
|
||||||
|
batchDelete: 'Batch Delete',
|
||||||
|
confirm: 'Confirm',
|
||||||
|
cancel: 'Cancel',
|
||||||
|
pleaseCheckValue: 'Please check the value is valid',
|
||||||
|
action: 'Action'
|
||||||
|
},
|
||||||
|
routes: {
|
||||||
|
dashboard: {
|
||||||
|
_value: 'Dashboard',
|
||||||
|
analysis: 'Analysis',
|
||||||
|
workbench: 'Workbench'
|
||||||
},
|
},
|
||||||
routes: {
|
document: {
|
||||||
dashboard: {
|
_value: 'Document',
|
||||||
_value: 'Dashboard',
|
vue: 'Vue Document',
|
||||||
analysis: 'Analysis',
|
vite: 'Vite Document',
|
||||||
workbench: 'Workbench'
|
naive: 'NaiveUI Document',
|
||||||
|
project: 'Project Document',
|
||||||
|
'project-link': 'Project Document(href)'
|
||||||
|
},
|
||||||
|
component: {
|
||||||
|
_value: 'Component',
|
||||||
|
button: 'Button',
|
||||||
|
card: 'Card',
|
||||||
|
table: 'Table'
|
||||||
|
},
|
||||||
|
plugin: {
|
||||||
|
_value: 'Plugin',
|
||||||
|
charts: {
|
||||||
|
_value: 'Chart',
|
||||||
|
echarts: 'ECharts',
|
||||||
|
antv: 'AntV'
|
||||||
},
|
},
|
||||||
document: {
|
copy: 'Copy',
|
||||||
_value: 'Document',
|
editor: {
|
||||||
vue: 'Vue Document',
|
_value: 'Editor',
|
||||||
vite: 'Vite Document',
|
quill: 'Quill',
|
||||||
naive: 'NaiveUI Document',
|
markdown: 'Markdown'
|
||||||
project: 'Project Document',
|
|
||||||
'project-link': 'Project Document(href)'
|
|
||||||
},
|
},
|
||||||
component: {
|
icon: 'Icon',
|
||||||
_value: 'Component',
|
map: 'Map',
|
||||||
button: 'Button',
|
print: 'Print',
|
||||||
card: 'Card',
|
swiper: 'Swiper',
|
||||||
table: 'Table'
|
video: 'Video'
|
||||||
|
},
|
||||||
|
'auth-demo': {
|
||||||
|
_value: 'Auth Demo',
|
||||||
|
permission: 'Toggle Permission',
|
||||||
|
super: 'Super Auth'
|
||||||
|
},
|
||||||
|
function: {
|
||||||
|
_value: 'Function',
|
||||||
|
tab: 'System Tab'
|
||||||
|
},
|
||||||
|
exception: {
|
||||||
|
_value: 'Exception',
|
||||||
|
403: '403',
|
||||||
|
404: '404',
|
||||||
|
500: '500'
|
||||||
|
},
|
||||||
|
'multi-menu': {
|
||||||
|
_value: 'Multi Degree Menu',
|
||||||
|
first: {
|
||||||
|
_value: 'First Degree',
|
||||||
|
second: 'Second Degree',
|
||||||
|
'second-new': {
|
||||||
|
_value: 'Second Degree With Children',
|
||||||
|
third: 'Third Degree'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
management: {
|
||||||
|
_value: 'System Management',
|
||||||
|
auth: 'Auth',
|
||||||
|
role: 'Role',
|
||||||
|
route: 'Route',
|
||||||
|
user: 'User'
|
||||||
|
},
|
||||||
|
about: 'About'
|
||||||
|
},
|
||||||
|
layout: {
|
||||||
|
settingDrawer: {
|
||||||
|
title: 'Theme configuration',
|
||||||
|
themeModeTitle: 'Theme mode',
|
||||||
|
darkMode: 'Dark mode',
|
||||||
|
layoutModelTitle: 'Layout mode',
|
||||||
|
systemThemeTitle: 'System theme',
|
||||||
|
pageFunctionsTitle: 'Page functions',
|
||||||
|
pageViewTitle: 'Page view',
|
||||||
|
followSystemTheme: 'Follow the system',
|
||||||
|
isCustomizeDarkModeTransition: 'Custom dark theme animation transition',
|
||||||
|
scrollMode: 'scrollMode',
|
||||||
|
scrollModeList: {
|
||||||
|
wrapper: 'Outer layer scroll',
|
||||||
|
content: 'Main body scroll'
|
||||||
},
|
},
|
||||||
plugin: {
|
fixedHeaderAndTab: 'Fixed header and multiple tabs',
|
||||||
_value: 'Plugin',
|
header: {
|
||||||
charts: {
|
inverted: 'darkHead',
|
||||||
_value: 'Chart',
|
height: 'Head Height',
|
||||||
echarts: 'ECharts',
|
crumb: {
|
||||||
antv: 'AntV'
|
visible: 'Crumb',
|
||||||
},
|
icon: 'Crumb icon'
|
||||||
copy: 'Copy',
|
|
||||||
editor: {
|
|
||||||
_value: 'Editor',
|
|
||||||
quill: 'Quill',
|
|
||||||
markdown: 'Markdown'
|
|
||||||
},
|
|
||||||
icon: 'Icon',
|
|
||||||
map: 'Map',
|
|
||||||
print: 'Print',
|
|
||||||
swiper: 'Swiper',
|
|
||||||
video: 'Video'
|
|
||||||
},
|
|
||||||
'auth-demo': {
|
|
||||||
_value: 'Auth Demo',
|
|
||||||
permission: 'Toggle Permission',
|
|
||||||
super: 'Super Auth'
|
|
||||||
},
|
|
||||||
function: {
|
|
||||||
_value: 'Function',
|
|
||||||
tab: 'System Tab'
|
|
||||||
},
|
|
||||||
exception: {
|
|
||||||
_value: 'Exception',
|
|
||||||
403: '403',
|
|
||||||
404: '404',
|
|
||||||
500: '500'
|
|
||||||
},
|
|
||||||
'multi-menu': {
|
|
||||||
_value: 'Multi Degree Menu',
|
|
||||||
first: {
|
|
||||||
_value: 'First Degree',
|
|
||||||
second: 'Second Degree',
|
|
||||||
'second-new': {
|
|
||||||
_value: 'Second Degree With Children',
|
|
||||||
third: 'Third Degree'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
management: {
|
tab: {
|
||||||
_value: 'System Management',
|
visible: 'Multi-page tab',
|
||||||
auth: 'Auth',
|
height: 'Multiple tab height',
|
||||||
role: 'Role',
|
modeList: {
|
||||||
route: 'Route',
|
mode: 'Multi-tab style',
|
||||||
user: 'User'
|
chrome: 'Google style',
|
||||||
|
button: 'Button style'
|
||||||
|
},
|
||||||
|
isCache: 'Multiple tab caching'
|
||||||
},
|
},
|
||||||
about: 'About'
|
sider: {
|
||||||
|
inverted: 'Dark sidebar',
|
||||||
|
width: 'Sidebar expanded width',
|
||||||
|
mixWidth: 'Left hybrid sidebar expanded width'
|
||||||
|
},
|
||||||
|
menu: {
|
||||||
|
horizontalPosition: 'Top menu position',
|
||||||
|
horizontalPositionList: {
|
||||||
|
flexStart: 'Right',
|
||||||
|
center: 'center',
|
||||||
|
flexEnd: 'Left'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
inverted: 'Dark bottom',
|
||||||
|
visible: 'Show bottom',
|
||||||
|
fixed: 'Fixed bottom',
|
||||||
|
right: 'Bottom to the right'
|
||||||
|
},
|
||||||
|
page: {
|
||||||
|
animate: 'switch animation',
|
||||||
|
animateMode: 'switch animation type',
|
||||||
|
animateModeList: {
|
||||||
|
zoomFade: 'Gradual change',
|
||||||
|
zoomOut: 'Flash',
|
||||||
|
fadeSlide: 'Slide',
|
||||||
|
fade: 'Fade away',
|
||||||
|
fadeBottom: 'Bottom fade',
|
||||||
|
fadeScale: 'Resizing fade away'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
systemTheme: {
|
||||||
|
moreColors: 'More colors'
|
||||||
|
},
|
||||||
|
themeConfiguration: {
|
||||||
|
title: 'Theme configuration',
|
||||||
|
copy: 'Copy the current configuration',
|
||||||
|
reset: 'Reset the current configuration',
|
||||||
|
resetSuccess: 'The configuration has been reset, please copy it again!',
|
||||||
|
operateSuccess: 'Successful operation',
|
||||||
|
copySuccess: 'Copy success, please replace the content of src/settings/theme.json!',
|
||||||
|
confirmCopy: 'Confirm'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
page: {
|
||||||
|
login: {
|
||||||
|
common: {
|
||||||
|
userNamePlaceholder: 'Please enter user name',
|
||||||
|
phonePlaceholder: 'Please enter phone number',
|
||||||
|
codePlaceholder: 'Please enter verification code',
|
||||||
|
passwordPlaceholder: 'Please enter password',
|
||||||
|
confirmPasswordPlaceholder: 'Please enter password again',
|
||||||
|
codeLogin: 'Verification code login',
|
||||||
|
confirm: 'Confirm',
|
||||||
|
back: 'Back',
|
||||||
|
validateSuccess: 'Verification passed',
|
||||||
|
loginSuccess: 'Login success',
|
||||||
|
welcomeBack: 'Welcome back, {userName}!'
|
||||||
|
},
|
||||||
|
pwdLogin: {
|
||||||
|
title: 'Password Login',
|
||||||
|
rememberMe: 'Remember me',
|
||||||
|
forgetPassword: 'Forget password?',
|
||||||
|
register: 'Register account',
|
||||||
|
otherAccountLogin: 'Other Account Login',
|
||||||
|
otherLoginMode: 'Other Login Mode',
|
||||||
|
superAdmin: 'Super Administrator',
|
||||||
|
admin: 'Administrator',
|
||||||
|
user: 'Ordinary User'
|
||||||
|
},
|
||||||
|
codeLogin: {
|
||||||
|
title: 'Verification Code Login',
|
||||||
|
getCode: 'Get verification code',
|
||||||
|
imageCodePlaceholder: 'Please enter image verification code'
|
||||||
|
},
|
||||||
|
register: {
|
||||||
|
title: 'Register Account',
|
||||||
|
agreement: 'I have read and agree to',
|
||||||
|
protocol: '《User Agreement》',
|
||||||
|
policy: '《Privacy Policy》'
|
||||||
|
},
|
||||||
|
resetPwd: {
|
||||||
|
title: 'Reset Password'
|
||||||
|
},
|
||||||
|
bindWeChat: {
|
||||||
|
title: 'Bind WeChat'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@@ -1,13 +0,0 @@
|
|||||||
import zhCN from './zh-cn';
|
|
||||||
import en from './en';
|
|
||||||
import kmKH from './km-KH';
|
|
||||||
|
|
||||||
const locales = {
|
|
||||||
'zh-CN': zhCN,
|
|
||||||
en,
|
|
||||||
'km-KH': kmKH
|
|
||||||
};
|
|
||||||
|
|
||||||
export type LocaleKey = keyof typeof locales;
|
|
||||||
|
|
||||||
export default locales;
|
|
@@ -1,83 +1,217 @@
|
|||||||
import type { LocaleMessages } from 'vue-i18n';
|
const locale: I18nType.Schema = {
|
||||||
|
system: {
|
||||||
const locale: LocaleMessages<I18nType.Schema> = {
|
title: 'ប្រព័ន្ធគ្រប់គ្រង'
|
||||||
message: {
|
},
|
||||||
system: {
|
common: {
|
||||||
title: 'ប្រព័ន្ធគ្រប់គ្រង'
|
add: 'បន្ថែម',
|
||||||
|
addSuccess: 'បន្ថែមជោគជ័យ',
|
||||||
|
edit: 'កែប្រែ',
|
||||||
|
editSuccess: 'កែប្រែជោគជ័យ',
|
||||||
|
delete: 'លុប',
|
||||||
|
deleteSuccess: 'លុបជោគជ័យ',
|
||||||
|
batchDelete: 'លុបច្រើន',
|
||||||
|
confirm: 'យល់ព្រម',
|
||||||
|
cancel: 'បោះបង់',
|
||||||
|
pleaseCheckValue: 'សូមពិនិត្យមើលតម្លៃដែលបានបញ្ចូលដើម្បីបញ្ជាក់ថាត្រូវប្រើប្រាស់បាន',
|
||||||
|
action: 'សកម្មភាព'
|
||||||
|
},
|
||||||
|
routes: {
|
||||||
|
dashboard: {
|
||||||
|
_value: 'ផ្ទាំងទិន្នន័យ',
|
||||||
|
analysis: 'ផ្ទាំងវិភាគ',
|
||||||
|
workbench: 'ផ្ទាំងការងារ'
|
||||||
},
|
},
|
||||||
routes: {
|
document: {
|
||||||
dashboard: {
|
_value: 'ឯកសារ',
|
||||||
_value: 'ផ្ទាំងទិន្នន័យ',
|
vue: 'ឯកសារ Vue',
|
||||||
analysis: 'ផ្ទាំងវិភាគ',
|
vite: 'ឯកសារ Vite',
|
||||||
workbench: 'ផ្ទាំងការងារ'
|
naive: 'ឯកសារ NaiveUI',
|
||||||
|
project: 'ឯកសារគម្រោង',
|
||||||
|
'project-link': 'ឯកសារគម្រោង(href)'
|
||||||
|
},
|
||||||
|
component: {
|
||||||
|
_value: 'សមាសភាគ',
|
||||||
|
button: 'ប៊ូតុង',
|
||||||
|
card: 'កាត',
|
||||||
|
table: 'តារាង'
|
||||||
|
},
|
||||||
|
plugin: {
|
||||||
|
_value: 'មុខងារជំនួយ',
|
||||||
|
charts: {
|
||||||
|
_value: 'តារាង Chart',
|
||||||
|
echarts: 'តារាង ECharts',
|
||||||
|
antv: 'AntV'
|
||||||
},
|
},
|
||||||
document: {
|
copy: 'ចម្លង',
|
||||||
_value: 'ឯកសារ',
|
editor: {
|
||||||
vue: 'ឯកសារ Vue',
|
_value: 'កែប្រែ',
|
||||||
vite: 'ឯកសារ Vite',
|
quill: 'Quill',
|
||||||
naive: 'ឯកសារ NaiveUI',
|
markdown: 'Markdown'
|
||||||
project: 'ឯកសារគម្រោង',
|
|
||||||
'project-link': 'ឯកសារគម្រោង(href)'
|
|
||||||
},
|
},
|
||||||
component: {
|
icon: 'អាយខន',
|
||||||
_value: 'សមាសភាគ',
|
map: 'ផែនទី',
|
||||||
button: 'ប៊ូតុង',
|
print: 'បោះពុម្ភ',
|
||||||
card: 'កាត',
|
swiper: 'Swiper',
|
||||||
table: 'តារាង'
|
video: 'វីដេអូ'
|
||||||
|
},
|
||||||
|
'auth-demo': {
|
||||||
|
_value: 'ឌីមូ Auth',
|
||||||
|
permission: 'បិទ/បើកការអនុញ្ញាត',
|
||||||
|
super: 'Super Auth'
|
||||||
|
},
|
||||||
|
function: {
|
||||||
|
_value: 'មុខងារ',
|
||||||
|
tab: 'ថេបប្រព័ន្ធ'
|
||||||
|
},
|
||||||
|
exception: {
|
||||||
|
_value: 'ករណីពិេសស',
|
||||||
|
403: '403',
|
||||||
|
404: '404',
|
||||||
|
500: '500'
|
||||||
|
},
|
||||||
|
'multi-menu': {
|
||||||
|
_value: 'ម៉ឺនុយពហុដឺក្រេ',
|
||||||
|
first: {
|
||||||
|
_value: 'ដឺក្រេទី១',
|
||||||
|
second: 'ដែក្រេទី២',
|
||||||
|
'second-new': {
|
||||||
|
_value: 'ដឺក្រេទី២មានអនុក្រោម',
|
||||||
|
third: 'ដឺក្រេទី៣'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
management: {
|
||||||
|
_value: 'ការគ្រប់គ្រងប្រព័ន្ធ',
|
||||||
|
auth: 'Auth',
|
||||||
|
role: 'សិទ្ធី',
|
||||||
|
route: 'ផ្លូវប្រព័ន្ធ',
|
||||||
|
user: 'អ្នកប្រើប្រាស់'
|
||||||
|
},
|
||||||
|
about: 'អំពីប្រព័ន្ធ'
|
||||||
|
},
|
||||||
|
layout: {
|
||||||
|
settingDrawer: {
|
||||||
|
title: 'ការកំណត់ស្បែក',
|
||||||
|
themeModeTitle: 'ស្បែករបស់របស់អ្នក',
|
||||||
|
darkMode: 'របៀបងារស្បែកងងឹត',
|
||||||
|
layoutModelTitle: 'របៀបប្រើប្រាស់របស់អ្នក',
|
||||||
|
systemThemeTitle: 'ស្បែករបស់ប្រព័ន្ធគ្រប់គ្រង',
|
||||||
|
pageFunctionsTitle: 'មុខងារទំនាក់ទំនងរបស់ទំព័រ',
|
||||||
|
pageViewTitle: 'ទំព័រទស្សន៍ទាយ',
|
||||||
|
followSystemTheme: 'តាមដានស្បែកប្រព័ន្ធគ្រប់គ្រង',
|
||||||
|
isCustomizeDarkModeTransition: 'ប្រើប្រាស់របៀបងារស្បែកងងឹតផ្ទាល់ខ្លួន',
|
||||||
|
scrollMode: 'របៀបរុករក',
|
||||||
|
scrollModeList: {
|
||||||
|
wrapper: 'រុករកជាក់លាក់',
|
||||||
|
content: 'រុករកមានមុខងារ'
|
||||||
},
|
},
|
||||||
plugin: {
|
fixedHeaderAndTab: 'បិទការរុករកជាក់លាក់និងរុករកមានមុខងារ',
|
||||||
_value: 'មុខងារជំនួយ',
|
header: {
|
||||||
charts: {
|
inverted: 'បង្កើតការរុករកជាក់លាក់',
|
||||||
_value: 'តារាង Chart',
|
height: 'កម្ពស់',
|
||||||
echarts: 'តារាង ECharts',
|
crumb: {
|
||||||
antv: 'AntV'
|
visible: 'បង្ហាញរុករកជាក់លាក់',
|
||||||
},
|
icon: 'រុករកជាក់លាក់រូបតំណាង'
|
||||||
copy: 'ចម្លង',
|
|
||||||
editor: {
|
|
||||||
_value: 'កែប្រែ',
|
|
||||||
quill: 'Quill',
|
|
||||||
markdown: 'Markdown'
|
|
||||||
},
|
|
||||||
icon: 'អាយខន',
|
|
||||||
map: 'ផែនទី',
|
|
||||||
print: 'បោះពុម្ភ',
|
|
||||||
swiper: 'Swiper',
|
|
||||||
video: 'វីដេអូ'
|
|
||||||
},
|
|
||||||
'auth-demo': {
|
|
||||||
_value: 'ឌីមូ Auth',
|
|
||||||
permission: 'បិទ/បើកការអនុញ្ញាត',
|
|
||||||
super: 'Super Auth'
|
|
||||||
},
|
|
||||||
function: {
|
|
||||||
_value: 'មុខងារ',
|
|
||||||
tab: 'ថេបប្រព័ន្ធ'
|
|
||||||
},
|
|
||||||
exception: {
|
|
||||||
_value: 'ករណីពិេសស',
|
|
||||||
403: '403',
|
|
||||||
404: '404',
|
|
||||||
500: '500'
|
|
||||||
},
|
|
||||||
'multi-menu': {
|
|
||||||
_value: 'ម៉ឺនុយពហុដឺក្រេ',
|
|
||||||
first: {
|
|
||||||
_value: 'ដឺក្រេទី១',
|
|
||||||
second: 'ដែក្រេទី២',
|
|
||||||
'second-new': {
|
|
||||||
_value: 'ដឺក្រេទី២មានអនុក្រោម',
|
|
||||||
third: 'ដឺក្រេទី៣'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
management: {
|
tab: {
|
||||||
_value: 'ការគ្រប់គ្រងប្រព័ន្ធ',
|
visible: 'បង្ហាញរុករកជាក់លាក់',
|
||||||
auth: 'Auth',
|
height: 'កម្ពស់',
|
||||||
role: 'សិទ្ធី',
|
modeList: {
|
||||||
route: 'ផ្លូវប្រព័ន្ធ',
|
mode: 'របៀប',
|
||||||
|
chrome: 'ក្រុមហ៊ុន',
|
||||||
|
button: 'ប៊ូតុង'
|
||||||
|
},
|
||||||
|
isCache: 'រក្សាទុកការរុករកជាក់លាក់'
|
||||||
|
},
|
||||||
|
sider: {
|
||||||
|
inverted: 'បង្កើតការរុករកជាក់លាក់',
|
||||||
|
width: 'ទទឹង',
|
||||||
|
mixWidth: 'ទទឹងបញ្ចូល'
|
||||||
|
},
|
||||||
|
menu: {
|
||||||
|
horizontalPosition: 'ទីតាំងផ្ដេក',
|
||||||
|
horizontalPositionList: {
|
||||||
|
flexStart: 'ចាប់ផ្ដើមឈុត',
|
||||||
|
center: 'កណ្តាល',
|
||||||
|
flexEnd: 'ចាប់ផ្ដើមចុងក្រោយ'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
inverted: 'បង្កើតការរុករកជាក់លាក់',
|
||||||
|
visible: 'បង្ហាញការរុករកជាក់លាក់',
|
||||||
|
fixed: 'ការរុករកជាក់លាក់',
|
||||||
|
right: 'ត្រឡប់ទៅស្តាំ'
|
||||||
|
},
|
||||||
|
page: {
|
||||||
|
animate: 'ការផ្លាស់ប្តូរ',
|
||||||
|
animateMode: 'របៀបផ្លាស់ប្តូរ',
|
||||||
|
animateModeList: {
|
||||||
|
zoomFade: 'ពង្រីកបង្ហាញនិងលាស់ប្តូរ',
|
||||||
|
zoomOut: 'ពង្រីកបង្ហាញនិងលាស់ប្តូរ',
|
||||||
|
fadeSlide: 'ពង្រីកបង្ហាញនិងលាស់ប្តូរ',
|
||||||
|
fade: 'ពង្រីកបង្ហាញនិងលាស់ប្តូរ',
|
||||||
|
fadeBottom: 'ពង្រីកបង្ហាញនិងលាស់ប្តូរ',
|
||||||
|
fadeScale: 'ពង្រីកបង្ហាញនិងលាស់ប្តូរ'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
systemTheme: {
|
||||||
|
moreColors: 'ពន្លឺច្រើនទៀត'
|
||||||
|
},
|
||||||
|
themeConfiguration: {
|
||||||
|
title: 'ការកំណត់ស្បែក',
|
||||||
|
copy: 'ចម្លង',
|
||||||
|
reset: 'កំណត់ឡើងវិញ',
|
||||||
|
resetSuccess: 'កំណត់ឡើងវិញជោគជ័យ, សូមចម្លងឯកសារស្បែកឡើងវិញ!',
|
||||||
|
operateSuccess: 'សម្រាប់ការប្រើប្រាស់ជោគជ័យ',
|
||||||
|
copySuccess: 'ចម្លងជោគជ័យ, សូមជោគជ័យឯកសារ src/settings/theme.json!',
|
||||||
|
confirmCopy: 'យល់ព្រម'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
page: {
|
||||||
|
login: {
|
||||||
|
common: {
|
||||||
|
userNamePlaceholder: 'ឈ្មោះអ្នកប្រើប្រាស់',
|
||||||
|
phonePlaceholder: 'លេខទូរស័ព្ទ',
|
||||||
|
codePlaceholder: 'លេខកូដ',
|
||||||
|
passwordPlaceholder: 'លេខសម្ងាត់',
|
||||||
|
confirmPasswordPlaceholder: 'បញ្ជាក់លេខសម្ងាត់',
|
||||||
|
codeLogin: 'ចូលតាមលេខកូដ',
|
||||||
|
confirm: 'យល់ព្រម',
|
||||||
|
back: 'ត្រឡប់ក្រោយ',
|
||||||
|
validateSuccess: 'បញ្ជាក់ជោគជ័យ',
|
||||||
|
loginSuccess: 'ចូលជោគជ័យ',
|
||||||
|
welcomeBack: 'សូមស្វាគមន៍ម្តងទៀត, {userName}!'
|
||||||
|
},
|
||||||
|
pwdLogin: {
|
||||||
|
title: 'ចូលគណនី',
|
||||||
|
rememberMe: 'ចងចាំខ្ញុំ',
|
||||||
|
forgetPassword: 'ភ្លេចលេខសម្ងាត់',
|
||||||
|
register: 'ចុះឈ្មោះ',
|
||||||
|
otherAccountLogin: 'ចូលតាមគណនីផ្សេងទៀត',
|
||||||
|
otherLoginMode: 'របៀបចូលគណនីផ្សេងទៀត',
|
||||||
|
superAdmin: 'អ្នកគ្រប់គ្រងសុវត្ថិភាព',
|
||||||
|
admin: 'អ្នកគ្រប់គ្រង',
|
||||||
user: 'អ្នកប្រើប្រាស់'
|
user: 'អ្នកប្រើប្រាស់'
|
||||||
},
|
},
|
||||||
about: 'អំពីប្រព័ន្ធ'
|
codeLogin: {
|
||||||
|
title: 'ចូលតាមលេខកូដ',
|
||||||
|
getCode: 'ទទួលលេខកូដ',
|
||||||
|
imageCodePlaceholder: 'លេខកូដរូបភាព'
|
||||||
|
},
|
||||||
|
register: {
|
||||||
|
title: 'ចុះឈ្មោះ',
|
||||||
|
agreement: 'យល់ព្រមនឹង',
|
||||||
|
protocol: 'សម្រាប់ការប្រើប្រាស់',
|
||||||
|
policy: 'គោលការណ៍ផ្សេងៗ'
|
||||||
|
},
|
||||||
|
resetPwd: {
|
||||||
|
title: 'កំណត់លេខសម្ងាត់ថ្មី'
|
||||||
|
},
|
||||||
|
bindWeChat: {
|
||||||
|
title: 'ភ្ជាប់គណនីរបស់អ្នកជាមួយគណនីរបស់អ្នក'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
219
src/locales/lang/zh-CN.ts
Normal file
219
src/locales/lang/zh-CN.ts
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
const locale: I18nType.Schema = {
|
||||||
|
system: {
|
||||||
|
title: 'Soybean管理系统'
|
||||||
|
},
|
||||||
|
common: {
|
||||||
|
add: '添加',
|
||||||
|
addSuccess: '添加成功',
|
||||||
|
edit: '修改',
|
||||||
|
editSuccess: '修改成功',
|
||||||
|
delete: '删除',
|
||||||
|
deleteSuccess: '删除成功',
|
||||||
|
batchDelete: '批量删除',
|
||||||
|
confirm: '确认',
|
||||||
|
cancel: '取消',
|
||||||
|
pleaseCheckValue: '请检查输入的值是否合法',
|
||||||
|
action: '操作'
|
||||||
|
},
|
||||||
|
routes: {
|
||||||
|
dashboard: {
|
||||||
|
_value: '仪表盘',
|
||||||
|
analysis: '分析页',
|
||||||
|
workbench: '工作台'
|
||||||
|
},
|
||||||
|
document: {
|
||||||
|
_value: '文档',
|
||||||
|
vue: 'Vue文档',
|
||||||
|
vite: 'Vite文档',
|
||||||
|
naive: 'NaiveUI文档',
|
||||||
|
project: '项目文档',
|
||||||
|
'project-link': '项目文档(外链)'
|
||||||
|
},
|
||||||
|
component: {
|
||||||
|
_value: '组件示例',
|
||||||
|
button: '按钮',
|
||||||
|
card: '卡片',
|
||||||
|
table: '表格'
|
||||||
|
},
|
||||||
|
plugin: {
|
||||||
|
_value: '插件示例',
|
||||||
|
charts: {
|
||||||
|
_value: '图表',
|
||||||
|
echarts: 'ECharts',
|
||||||
|
antv: 'AntV'
|
||||||
|
},
|
||||||
|
copy: '剪贴板',
|
||||||
|
editor: {
|
||||||
|
_value: '编辑器',
|
||||||
|
quill: '富文本',
|
||||||
|
markdown: 'Markdown'
|
||||||
|
},
|
||||||
|
icon: '图标',
|
||||||
|
map: '地图',
|
||||||
|
print: '打印',
|
||||||
|
swiper: 'Swiper',
|
||||||
|
video: '视频'
|
||||||
|
},
|
||||||
|
'auth-demo': {
|
||||||
|
_value: '权限示例',
|
||||||
|
permission: '切换权限',
|
||||||
|
super: '超级管理员可见'
|
||||||
|
},
|
||||||
|
function: {
|
||||||
|
_value: '功能',
|
||||||
|
tab: 'Tab页签'
|
||||||
|
},
|
||||||
|
exception: {
|
||||||
|
_value: '异常页',
|
||||||
|
403: '403',
|
||||||
|
404: '404',
|
||||||
|
500: '500'
|
||||||
|
},
|
||||||
|
'multi-menu': {
|
||||||
|
_value: '多级菜单',
|
||||||
|
first: {
|
||||||
|
_value: '一级菜单',
|
||||||
|
second: '二级菜单',
|
||||||
|
'second-new': {
|
||||||
|
_value: '二级菜单(有子菜单)',
|
||||||
|
third: '三级菜单'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
management: {
|
||||||
|
_value: '系统管理',
|
||||||
|
auth: '权限管理',
|
||||||
|
role: '角色管理',
|
||||||
|
route: '路由管理',
|
||||||
|
user: '用户管理'
|
||||||
|
},
|
||||||
|
about: '关于'
|
||||||
|
},
|
||||||
|
layout: {
|
||||||
|
settingDrawer: {
|
||||||
|
title: '主题配置',
|
||||||
|
themeModeTitle: '主题模式',
|
||||||
|
darkMode: '深色主题',
|
||||||
|
layoutModelTitle: '布局模式',
|
||||||
|
systemThemeTitle: '系统主题',
|
||||||
|
pageFunctionsTitle: '界面功能',
|
||||||
|
pageViewTitle: '界面显示',
|
||||||
|
followSystemTheme: '跟随系统',
|
||||||
|
isCustomizeDarkModeTransition: '自定义暗黑主题动画过渡',
|
||||||
|
scrollMode: '滚动模式',
|
||||||
|
scrollModeList: {
|
||||||
|
wrapper: '外层滚动',
|
||||||
|
content: '主体滚动'
|
||||||
|
},
|
||||||
|
fixedHeaderAndTab: '固定头部和多页签',
|
||||||
|
header: {
|
||||||
|
inverted: '头部深色',
|
||||||
|
height: '头部高度',
|
||||||
|
crumb: {
|
||||||
|
visible: '面包屑',
|
||||||
|
icon: '面包屑图标'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tab: {
|
||||||
|
visible: '多页签',
|
||||||
|
height: '多页签高度',
|
||||||
|
modeList: {
|
||||||
|
mode: '多页签风格',
|
||||||
|
chrome: '谷歌风格',
|
||||||
|
button: '按钮风格'
|
||||||
|
},
|
||||||
|
isCache: '多页签缓存'
|
||||||
|
},
|
||||||
|
sider: {
|
||||||
|
inverted: '侧边栏深色',
|
||||||
|
width: '侧边栏展开宽度',
|
||||||
|
mixWidth: '左侧混合侧边栏展开宽度'
|
||||||
|
},
|
||||||
|
menu: {
|
||||||
|
horizontalPosition: '顶部菜单位置',
|
||||||
|
horizontalPositionList: {
|
||||||
|
flexStart: '居左',
|
||||||
|
center: '居中',
|
||||||
|
flexEnd: '居右'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
inverted: '底部深色',
|
||||||
|
visible: '显示底部',
|
||||||
|
fixed: '固定底部',
|
||||||
|
right: '底部居右'
|
||||||
|
},
|
||||||
|
page: {
|
||||||
|
animate: '页面切换动画',
|
||||||
|
animateMode: '页面切换动画类型',
|
||||||
|
animateModeList: {
|
||||||
|
zoomFade: '渐变',
|
||||||
|
zoomOut: '闪现',
|
||||||
|
fadeSlide: '滑动',
|
||||||
|
fade: '消退',
|
||||||
|
fadeBottom: '底部消退',
|
||||||
|
fadeScale: '缩放消退'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
systemTheme: {
|
||||||
|
moreColors: '更多颜色'
|
||||||
|
},
|
||||||
|
themeConfiguration: {
|
||||||
|
title: '主题配置',
|
||||||
|
copy: '拷贝当前配置',
|
||||||
|
reset: '重置当前配置',
|
||||||
|
resetSuccess: '已重置配置,请重新拷贝!',
|
||||||
|
operateSuccess: '操作成功',
|
||||||
|
copySuccess: '复制成功,请替换 src/settings/theme.json的内容!',
|
||||||
|
confirmCopy: '确认'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
page: {
|
||||||
|
login: {
|
||||||
|
common: {
|
||||||
|
userNamePlaceholder: '请输入用户名',
|
||||||
|
phonePlaceholder: '请输入手机号',
|
||||||
|
codePlaceholder: '请输入验证码',
|
||||||
|
passwordPlaceholder: '请输入密码',
|
||||||
|
confirmPasswordPlaceholder: '请再次输入密码',
|
||||||
|
codeLogin: '验证码登录',
|
||||||
|
confirm: '确定',
|
||||||
|
back: '返回',
|
||||||
|
validateSuccess: '验证成功',
|
||||||
|
loginSuccess: '登录成功',
|
||||||
|
welcomeBack: '欢迎回来,{userName}!'
|
||||||
|
},
|
||||||
|
pwdLogin: {
|
||||||
|
title: '密码登录',
|
||||||
|
rememberMe: '记住我',
|
||||||
|
forgetPassword: '忘记密码?',
|
||||||
|
register: '注册账号',
|
||||||
|
otherAccountLogin: '其他账号登录',
|
||||||
|
otherLoginMode: '其他登录方式',
|
||||||
|
superAdmin: '超级管理员',
|
||||||
|
admin: '管理员',
|
||||||
|
user: '普通用户'
|
||||||
|
},
|
||||||
|
codeLogin: {
|
||||||
|
title: '验证码登录',
|
||||||
|
getCode: '获取验证码',
|
||||||
|
imageCodePlaceholder: '请输入图片验证码'
|
||||||
|
},
|
||||||
|
register: {
|
||||||
|
title: '注册账号',
|
||||||
|
agreement: '我已经仔细阅读并接受',
|
||||||
|
protocol: '《用户协议》',
|
||||||
|
policy: '《隐私权政策》'
|
||||||
|
},
|
||||||
|
resetPwd: {
|
||||||
|
title: '重置密码'
|
||||||
|
},
|
||||||
|
bindWeChat: {
|
||||||
|
title: '绑定微信'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default locale;
|
@@ -1,85 +0,0 @@
|
|||||||
import type { LocaleMessages } from 'vue-i18n';
|
|
||||||
|
|
||||||
const locale: LocaleMessages<I18nType.Schema> = {
|
|
||||||
message: {
|
|
||||||
system: {
|
|
||||||
title: 'Soybean管理系统'
|
|
||||||
},
|
|
||||||
routes: {
|
|
||||||
dashboard: {
|
|
||||||
_value: '仪表盘',
|
|
||||||
analysis: '分析页',
|
|
||||||
workbench: '工作台'
|
|
||||||
},
|
|
||||||
document: {
|
|
||||||
_value: '文档',
|
|
||||||
vue: 'Vue文档',
|
|
||||||
vite: 'Vite文档',
|
|
||||||
naive: 'NaiveUI文档',
|
|
||||||
project: '项目文档',
|
|
||||||
'project-link': '项目文档(外链)'
|
|
||||||
},
|
|
||||||
component: {
|
|
||||||
_value: '组件示例',
|
|
||||||
button: '按钮',
|
|
||||||
card: '卡片',
|
|
||||||
table: '表格'
|
|
||||||
},
|
|
||||||
plugin: {
|
|
||||||
_value: '插件示例',
|
|
||||||
charts: {
|
|
||||||
_value: '图表',
|
|
||||||
echarts: 'ECharts',
|
|
||||||
antv: 'AntV'
|
|
||||||
},
|
|
||||||
copy: '剪贴板',
|
|
||||||
editor: {
|
|
||||||
_value: '编辑器',
|
|
||||||
quill: '富文本',
|
|
||||||
markdown: 'Markdown'
|
|
||||||
},
|
|
||||||
icon: '图标',
|
|
||||||
map: '地图',
|
|
||||||
print: '打印',
|
|
||||||
swiper: 'Swiper',
|
|
||||||
video: '视频'
|
|
||||||
},
|
|
||||||
'auth-demo': {
|
|
||||||
_value: '权限示例',
|
|
||||||
permission: '切换权限',
|
|
||||||
super: '超级管理员可见'
|
|
||||||
},
|
|
||||||
function: {
|
|
||||||
_value: '功能',
|
|
||||||
tab: 'Tab页签'
|
|
||||||
},
|
|
||||||
exception: {
|
|
||||||
_value: '异常页',
|
|
||||||
403: '403',
|
|
||||||
404: '404',
|
|
||||||
500: '500'
|
|
||||||
},
|
|
||||||
'multi-menu': {
|
|
||||||
_value: '多级菜单',
|
|
||||||
first: {
|
|
||||||
_value: '一级菜单',
|
|
||||||
second: '二级菜单',
|
|
||||||
'second-new': {
|
|
||||||
_value: '二级菜单(有子菜单)',
|
|
||||||
third: '三级菜单'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
management: {
|
|
||||||
_value: '系统管理',
|
|
||||||
auth: '权限管理',
|
|
||||||
role: '角色管理',
|
|
||||||
route: '路由管理',
|
|
||||||
user: '用户管理'
|
|
||||||
},
|
|
||||||
about: '关于'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default locale;
|
|
11
src/locales/locale.ts
Normal file
11
src/locales/locale.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import zhCN from './lang/zh-CN';
|
||||||
|
import en from './lang/en';
|
||||||
|
import kmKH from './lang/km-KH';
|
||||||
|
|
||||||
|
const locales: Record<I18nType.LangType, I18nType.Schema> = {
|
||||||
|
'zh-CN': zhCN,
|
||||||
|
en,
|
||||||
|
'km-KH': kmKH
|
||||||
|
};
|
||||||
|
|
||||||
|
export default locales;
|
@@ -1,7 +1,12 @@
|
|||||||
import 'uno.css';
|
import 'uno.css';
|
||||||
import '@soybeanjs/vue-materials/dist/style.css';
|
import '@soybeanjs/vue-materials/dist/style.css';
|
||||||
|
import 'swiper/css';
|
||||||
|
import 'swiper/css/navigation';
|
||||||
|
import 'swiper/css/pagination';
|
||||||
import 'virtual:svg-icons-register';
|
import 'virtual:svg-icons-register';
|
||||||
import '../styles/css/global.css';
|
import '../styles/css/global.css';
|
||||||
|
|
||||||
/** import static assets: css, js , font and so on. - [引入静态资源,css、js和字体文件等] */
|
/** import static assets: css, js , font and so on. - [引入静态资源,css、js和字体文件等] */
|
||||||
export default function setupAssets() {}
|
export default function setupAssets() {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import type { Router } from 'vue-router';
|
import type { Router } from 'vue-router';
|
||||||
import { useTitle } from '@vueuse/core';
|
import { useTitle } from '@vueuse/core';
|
||||||
import { t } from '@/locales';
|
import { $t } from '@/locales';
|
||||||
import { createPermissionGuard } from './permission';
|
import { createPermissionGuard } from './permission';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -16,7 +16,7 @@ export function createRouterGuard(router: Router) {
|
|||||||
});
|
});
|
||||||
router.afterEach(to => {
|
router.afterEach(to => {
|
||||||
// 设置document title
|
// 设置document title
|
||||||
useTitle(to.meta.i18nTitle ? t(to.meta.i18nTitle) : to.meta.title);
|
useTitle(to.meta.i18nTitle ? $t(to.meta.i18nTitle) : to.meta.title);
|
||||||
// 结束 loadingBar
|
// 结束 loadingBar
|
||||||
window.$loadingBar?.finish();
|
window.$loadingBar?.finish();
|
||||||
});
|
});
|
||||||
|
17
src/router/modules/about.ts
Normal file
17
src/router/modules/about.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
const about: AuthRoute.Route = {
|
||||||
|
name: 'about',
|
||||||
|
path: '/about',
|
||||||
|
component: 'self',
|
||||||
|
meta: {
|
||||||
|
title: '关于',
|
||||||
|
i18nTitle: 'routes.about',
|
||||||
|
requiresAuth: true,
|
||||||
|
keepAlive: true,
|
||||||
|
singleLayout: 'basic',
|
||||||
|
permissions: ['super', 'admin', 'user'],
|
||||||
|
icon: 'fluent:book-information-24-regular',
|
||||||
|
order: 10
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default about;
|
38
src/router/modules/auth-demo.ts
Normal file
38
src/router/modules/auth-demo.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
const authDemo: AuthRoute.Route = {
|
||||||
|
name: 'auth-demo',
|
||||||
|
path: '/auth-demo',
|
||||||
|
component: 'basic',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'auth-demo_permission',
|
||||||
|
path: '/auth-demo/permission',
|
||||||
|
component: 'self',
|
||||||
|
meta: {
|
||||||
|
title: '权限切换',
|
||||||
|
i18nTitle: 'routes.auth-demo.permission',
|
||||||
|
requiresAuth: true,
|
||||||
|
icon: 'ic:round-construction'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'auth-demo_super',
|
||||||
|
path: '/auth-demo/super',
|
||||||
|
component: 'self',
|
||||||
|
meta: {
|
||||||
|
title: '超级管理员可见',
|
||||||
|
i18nTitle: 'routes.auth-demo.super',
|
||||||
|
requiresAuth: true,
|
||||||
|
permissions: ['super'],
|
||||||
|
icon: 'ic:round-supervisor-account'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
meta: {
|
||||||
|
title: '权限示例',
|
||||||
|
i18nTitle: 'routes.auth-demo._value',
|
||||||
|
icon: 'ic:baseline-security',
|
||||||
|
order: 5
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default authDemo;
|
48
src/router/modules/component.ts
Normal file
48
src/router/modules/component.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
const component: AuthRoute.Route = {
|
||||||
|
name: 'component',
|
||||||
|
path: '/component',
|
||||||
|
component: 'basic',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'component_button',
|
||||||
|
path: '/component/button',
|
||||||
|
component: 'self',
|
||||||
|
meta: {
|
||||||
|
title: '按钮',
|
||||||
|
i18nTitle: 'routes.component.button',
|
||||||
|
requiresAuth: true,
|
||||||
|
icon: 'mdi:button-cursor'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'component_card',
|
||||||
|
path: '/component/card',
|
||||||
|
component: 'self',
|
||||||
|
meta: {
|
||||||
|
title: '卡片',
|
||||||
|
i18nTitle: 'routes.component.card',
|
||||||
|
requiresAuth: true,
|
||||||
|
icon: 'mdi:card-outline'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'component_table',
|
||||||
|
path: '/component/table',
|
||||||
|
component: 'self',
|
||||||
|
meta: {
|
||||||
|
title: '表格',
|
||||||
|
i18nTitle: 'routes.component.table',
|
||||||
|
requiresAuth: true,
|
||||||
|
icon: 'mdi:table-large'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
meta: {
|
||||||
|
title: '组件示例',
|
||||||
|
i18nTitle: 'routes.component._value',
|
||||||
|
icon: 'cib:app-store',
|
||||||
|
order: 3
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default component;
|
37
src/router/modules/dashboard.ts
Normal file
37
src/router/modules/dashboard.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
const dashboard: AuthRoute.Route = {
|
||||||
|
name: 'dashboard',
|
||||||
|
path: '/dashboard',
|
||||||
|
component: 'basic',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'dashboard_analysis',
|
||||||
|
path: '/dashboard/analysis',
|
||||||
|
component: 'self',
|
||||||
|
meta: {
|
||||||
|
title: '分析页',
|
||||||
|
requiresAuth: true,
|
||||||
|
icon: 'icon-park-outline:analysis',
|
||||||
|
i18nTitle: 'routes.dashboard.analysis'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'dashboard_workbench',
|
||||||
|
path: '/dashboard/workbench',
|
||||||
|
component: 'self',
|
||||||
|
meta: {
|
||||||
|
title: '工作台',
|
||||||
|
requiresAuth: true,
|
||||||
|
icon: 'icon-park-outline:workbench',
|
||||||
|
i18nTitle: 'routes.dashboard.workbench'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
meta: {
|
||||||
|
title: '仪表盘',
|
||||||
|
icon: 'mdi:monitor-dashboard',
|
||||||
|
order: 1,
|
||||||
|
i18nTitle: 'routes.dashboard._value'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default dashboard;
|
70
src/router/modules/document.ts
Normal file
70
src/router/modules/document.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
const document: AuthRoute.Route = {
|
||||||
|
name: 'document',
|
||||||
|
path: '/document',
|
||||||
|
component: 'basic',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'document_vue',
|
||||||
|
path: '/document/vue',
|
||||||
|
component: 'self',
|
||||||
|
meta: {
|
||||||
|
title: 'vue文档',
|
||||||
|
i18nTitle: 'routes.document.vue',
|
||||||
|
requiresAuth: true,
|
||||||
|
icon: 'logos:vue'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'document_vite',
|
||||||
|
path: '/document/vite',
|
||||||
|
component: 'self',
|
||||||
|
meta: {
|
||||||
|
title: 'vite文档',
|
||||||
|
i18nTitle: 'routes.document.vite',
|
||||||
|
requiresAuth: true,
|
||||||
|
icon: 'logos:vitejs'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'document_naive',
|
||||||
|
path: '/document/naive',
|
||||||
|
component: 'self',
|
||||||
|
meta: {
|
||||||
|
title: 'naive文档',
|
||||||
|
i18nTitle: 'routes.document.naive',
|
||||||
|
requiresAuth: true,
|
||||||
|
icon: 'logos:naiveui'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'document_project',
|
||||||
|
path: '/document/project',
|
||||||
|
component: 'self',
|
||||||
|
meta: {
|
||||||
|
title: '项目文档',
|
||||||
|
i18nTitle: 'routes.document.project',
|
||||||
|
requiresAuth: true,
|
||||||
|
localIcon: 'logo'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'document_project-link',
|
||||||
|
path: '/document/project-link',
|
||||||
|
meta: {
|
||||||
|
title: '项目文档(外链)',
|
||||||
|
i18nTitle: 'routes.document.project-link',
|
||||||
|
requiresAuth: true,
|
||||||
|
localIcon: 'logo',
|
||||||
|
href: 'https://admin-docs.soybeanjs.cn/'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
meta: {
|
||||||
|
title: '文档',
|
||||||
|
i18nTitle: 'routes.document._value',
|
||||||
|
icon: 'mdi:file-document-multiple-outline',
|
||||||
|
order: 2
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default document;
|
48
src/router/modules/exception.ts
Normal file
48
src/router/modules/exception.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
const exception: AuthRoute.Route = {
|
||||||
|
name: 'exception',
|
||||||
|
path: '/exception',
|
||||||
|
component: 'basic',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'exception_403',
|
||||||
|
path: '/exception/403',
|
||||||
|
component: 'self',
|
||||||
|
meta: {
|
||||||
|
title: '异常页403',
|
||||||
|
i18nTitle: 'routes.exception.403',
|
||||||
|
requiresAuth: true,
|
||||||
|
icon: 'ic:baseline-block'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'exception_404',
|
||||||
|
path: '/exception/404',
|
||||||
|
component: 'self',
|
||||||
|
meta: {
|
||||||
|
title: '异常页404',
|
||||||
|
i18nTitle: 'routes.exception.404',
|
||||||
|
requiresAuth: true,
|
||||||
|
icon: 'ic:baseline-web-asset-off'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'exception_500',
|
||||||
|
path: '/exception/500',
|
||||||
|
component: 'self',
|
||||||
|
meta: {
|
||||||
|
title: '异常页500',
|
||||||
|
i18nTitle: 'routes.exception.500',
|
||||||
|
requiresAuth: true,
|
||||||
|
icon: 'ic:baseline-wifi-off'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
meta: {
|
||||||
|
i18nTitle: 'routes.exception._value',
|
||||||
|
title: '异常页',
|
||||||
|
icon: 'ant-design:exception-outlined',
|
||||||
|
order: 7
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default exception;
|
51
src/router/modules/function.ts
Normal file
51
src/router/modules/function.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
const functionRoute: AuthRoute.Route = {
|
||||||
|
name: 'function',
|
||||||
|
path: '/function',
|
||||||
|
component: 'basic',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'function_tab',
|
||||||
|
path: '/function/tab',
|
||||||
|
component: 'self',
|
||||||
|
meta: {
|
||||||
|
title: 'Tab',
|
||||||
|
i18nTitle: 'routes.function.tab',
|
||||||
|
requiresAuth: true,
|
||||||
|
icon: 'ic:round-tab'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'function_tab-detail',
|
||||||
|
path: '/function/tab-detail',
|
||||||
|
component: 'self',
|
||||||
|
meta: {
|
||||||
|
title: 'Tab Detail',
|
||||||
|
requiresAuth: true,
|
||||||
|
hide: true,
|
||||||
|
activeMenu: 'function_tab',
|
||||||
|
icon: 'ic:round-tab'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'function_tab-multi-detail',
|
||||||
|
path: '/function/tab-multi-detail',
|
||||||
|
component: 'self',
|
||||||
|
meta: {
|
||||||
|
title: 'Tab Multi Detail',
|
||||||
|
requiresAuth: true,
|
||||||
|
hide: true,
|
||||||
|
multiTab: true,
|
||||||
|
activeMenu: 'function_tab',
|
||||||
|
icon: 'ic:round-tab'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
meta: {
|
||||||
|
title: '功能',
|
||||||
|
i18nTitle: 'routes.function._value',
|
||||||
|
icon: 'icon-park-outline:all-application',
|
||||||
|
order: 6
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default functionRoute;
|
63
src/router/modules/management.ts
Normal file
63
src/router/modules/management.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
const management: AuthRoute.Route = {
|
||||||
|
name: 'management',
|
||||||
|
path: '/management',
|
||||||
|
component: 'basic',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'management_auth',
|
||||||
|
path: '/management/auth',
|
||||||
|
component: 'self',
|
||||||
|
meta: {
|
||||||
|
title: '权限管理',
|
||||||
|
i18nTitle: 'routes.management.auth',
|
||||||
|
requiresAuth: true,
|
||||||
|
keepAlive: true,
|
||||||
|
icon: 'ic:baseline-security'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'management_role',
|
||||||
|
path: '/management/role',
|
||||||
|
component: 'self',
|
||||||
|
meta: {
|
||||||
|
title: '角色管理',
|
||||||
|
i18nTitle: 'routes.management.role',
|
||||||
|
requiresAuth: true,
|
||||||
|
keepAlive: true,
|
||||||
|
icon: 'carbon:user-role'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'management_user',
|
||||||
|
path: '/management/user',
|
||||||
|
component: 'self',
|
||||||
|
meta: {
|
||||||
|
title: '用户管理',
|
||||||
|
i18nTitle: 'routes.management.user',
|
||||||
|
requiresAuth: true,
|
||||||
|
keepAlive: true,
|
||||||
|
icon: 'ic:round-manage-accounts'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'management_route',
|
||||||
|
path: '/management/route',
|
||||||
|
component: 'self',
|
||||||
|
meta: {
|
||||||
|
title: '路由管理',
|
||||||
|
i18nTitle: 'routes.management.route',
|
||||||
|
requiresAuth: true,
|
||||||
|
keepAlive: true,
|
||||||
|
icon: 'material-symbols:route'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
meta: {
|
||||||
|
title: '系统管理',
|
||||||
|
i18nTitle: 'routes.management._value',
|
||||||
|
icon: 'carbon:cloud-service-management',
|
||||||
|
order: 9
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default management;
|
@@ -14,7 +14,7 @@ const multiMenu: AuthRoute.Route = {
|
|||||||
component: 'self',
|
component: 'self',
|
||||||
meta: {
|
meta: {
|
||||||
title: '二级菜单',
|
title: '二级菜单',
|
||||||
i18nTitle: 'message.routes.multi-menu.first.second',
|
i18nTitle: 'routes.multi-menu.first.second',
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
icon: 'mdi:menu'
|
icon: 'mdi:menu'
|
||||||
}
|
}
|
||||||
@@ -30,7 +30,7 @@ const multiMenu: AuthRoute.Route = {
|
|||||||
component: 'self',
|
component: 'self',
|
||||||
meta: {
|
meta: {
|
||||||
title: '三级菜单',
|
title: '三级菜单',
|
||||||
i18nTitle: 'message.routes.multi-menu.first.second-new.third',
|
i18nTitle: 'routes.multi-menu.first.second-new.third',
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
icon: 'mdi:menu'
|
icon: 'mdi:menu'
|
||||||
}
|
}
|
||||||
@@ -38,21 +38,21 @@ const multiMenu: AuthRoute.Route = {
|
|||||||
],
|
],
|
||||||
meta: {
|
meta: {
|
||||||
title: '二级菜单(有子菜单)',
|
title: '二级菜单(有子菜单)',
|
||||||
i18nTitle: 'message.routes.multi-menu.first.second-new._value',
|
i18nTitle: 'routes.multi-menu.first.second-new._value',
|
||||||
icon: 'mdi:menu'
|
icon: 'mdi:menu'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
meta: {
|
meta: {
|
||||||
title: '一级菜单',
|
title: '一级菜单',
|
||||||
i18nTitle: 'message.routes.multi-menu.first._value',
|
i18nTitle: 'routes.multi-menu.first._value',
|
||||||
icon: 'mdi:menu'
|
icon: 'mdi:menu'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
meta: {
|
meta: {
|
||||||
title: '多级菜单',
|
title: '多级菜单',
|
||||||
i18nTitle: 'message.routes.multi-menu._value',
|
i18nTitle: 'routes.multi-menu._value',
|
||||||
icon: 'carbon:menu',
|
icon: 'carbon:menu',
|
||||||
order: 8
|
order: 8
|
||||||
}
|
}
|
||||||
|
149
src/router/modules/plugin.ts
Normal file
149
src/router/modules/plugin.ts
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
const plugin: AuthRoute.Route = {
|
||||||
|
name: 'plugin',
|
||||||
|
path: '/plugin',
|
||||||
|
component: 'basic',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'plugin_charts',
|
||||||
|
path: '/plugin/charts',
|
||||||
|
component: 'multi',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'plugin_charts_echarts',
|
||||||
|
path: '/plugin/charts/echarts',
|
||||||
|
component: 'self',
|
||||||
|
meta: {
|
||||||
|
title: 'ECharts',
|
||||||
|
i18nTitle: 'routes.plugin.charts.echarts',
|
||||||
|
requiresAuth: true,
|
||||||
|
icon: 'simple-icons:apacheecharts'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'plugin_charts_antv',
|
||||||
|
path: '/plugin/charts/antv',
|
||||||
|
component: 'self',
|
||||||
|
meta: {
|
||||||
|
title: 'AntV',
|
||||||
|
i18nTitle: 'routes.plugin.charts.antv',
|
||||||
|
requiresAuth: true,
|
||||||
|
icon: 'simple-icons:antdesign'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
meta: {
|
||||||
|
title: '图表',
|
||||||
|
i18nTitle: 'routes.plugin.charts._value',
|
||||||
|
icon: 'mdi:chart-areaspline'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'plugin_map',
|
||||||
|
path: '/plugin/map',
|
||||||
|
component: 'self',
|
||||||
|
meta: {
|
||||||
|
title: '地图',
|
||||||
|
i18nTitle: 'routes.plugin.map',
|
||||||
|
requiresAuth: true,
|
||||||
|
icon: 'mdi:map'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'plugin_video',
|
||||||
|
path: '/plugin/video',
|
||||||
|
component: 'self',
|
||||||
|
meta: {
|
||||||
|
title: '视频',
|
||||||
|
i18nTitle: 'routes.plugin.video',
|
||||||
|
requiresAuth: true,
|
||||||
|
icon: 'mdi:video'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'plugin_editor',
|
||||||
|
path: '/plugin/editor',
|
||||||
|
component: 'multi',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'plugin_editor_quill',
|
||||||
|
path: '/plugin/editor/quill',
|
||||||
|
component: 'self',
|
||||||
|
meta: {
|
||||||
|
title: '富文本编辑器',
|
||||||
|
i18nTitle: 'routes.plugin.editor.quill',
|
||||||
|
requiresAuth: true,
|
||||||
|
icon: 'mdi:file-document-edit-outline'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'plugin_editor_markdown',
|
||||||
|
path: '/plugin/editor/markdown',
|
||||||
|
component: 'self',
|
||||||
|
meta: {
|
||||||
|
title: 'markdown编辑器',
|
||||||
|
i18nTitle: 'routes.plugin.editor.markdown',
|
||||||
|
requiresAuth: true,
|
||||||
|
icon: 'ri:markdown-line'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
meta: {
|
||||||
|
title: '编辑器',
|
||||||
|
i18nTitle: 'routes.plugin.editor._value',
|
||||||
|
icon: 'icon-park-outline:editor'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'plugin_swiper',
|
||||||
|
path: '/plugin/swiper',
|
||||||
|
component: 'self',
|
||||||
|
meta: {
|
||||||
|
title: 'Swiper插件',
|
||||||
|
i18nTitle: 'routes.plugin.swiper',
|
||||||
|
requiresAuth: true,
|
||||||
|
icon: 'simple-icons:swiper'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'plugin_copy',
|
||||||
|
path: '/plugin/copy',
|
||||||
|
component: 'self',
|
||||||
|
meta: {
|
||||||
|
title: '剪贴板',
|
||||||
|
i18nTitle: 'routes.plugin.copy',
|
||||||
|
requiresAuth: true,
|
||||||
|
icon: 'mdi:clipboard-outline'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'plugin_icon',
|
||||||
|
path: '/plugin/icon',
|
||||||
|
component: 'self',
|
||||||
|
meta: {
|
||||||
|
title: '图标',
|
||||||
|
i18nTitle: 'routes.plugin.icon',
|
||||||
|
requiresAuth: true,
|
||||||
|
localIcon: 'custom-icon'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'plugin_print',
|
||||||
|
path: '/plugin/print',
|
||||||
|
component: 'self',
|
||||||
|
meta: {
|
||||||
|
title: '打印',
|
||||||
|
i18nTitle: 'routes.plugin.print',
|
||||||
|
requiresAuth: true,
|
||||||
|
icon: 'mdi:printer'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
meta: {
|
||||||
|
title: '插件示例',
|
||||||
|
i18nTitle: 'routes.plugin._value',
|
||||||
|
icon: 'clarity:plugin-line',
|
||||||
|
order: 4
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default plugin;
|
@@ -1 +1,2 @@
|
|||||||
export * from './auth';
|
export * from './auth';
|
||||||
|
export * from './management';
|
||||||
|
13
src/service/api/management.adapter.ts
Normal file
13
src/service/api/management.adapter.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
export function adapterOfFetchUserList(data: ApiUserManagement.User[] | null): UserManagement.User[] {
|
||||||
|
if (!data) return [];
|
||||||
|
|
||||||
|
return data.map((item, index) => {
|
||||||
|
const user: UserManagement.User = {
|
||||||
|
index: index + 1,
|
||||||
|
key: item.id,
|
||||||
|
...item
|
||||||
|
};
|
||||||
|
|
||||||
|
return user;
|
||||||
|
});
|
||||||
|
}
|
9
src/service/api/management.ts
Normal file
9
src/service/api/management.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { adapter } from '@/utils';
|
||||||
|
import { mockRequest } from '../request';
|
||||||
|
import { adapterOfFetchUserList } from './management.adapter';
|
||||||
|
|
||||||
|
/** 获取用户列表 */
|
||||||
|
export const fetchUserList = async () => {
|
||||||
|
const data = await mockRequest.post<ApiUserManagement.User[] | null>('/getAllUserList');
|
||||||
|
return adapter(adapterOfFetchUserList, data);
|
||||||
|
};
|
@@ -11,6 +11,8 @@ import {
|
|||||||
} from '@/utils';
|
} from '@/utils';
|
||||||
import { handleRefreshToken } from './helpers';
|
import { handleRefreshToken } from './helpers';
|
||||||
|
|
||||||
|
type RefreshRequestQueue = (config: AxiosRequestConfig) => void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 封装axios请求类
|
* 封装axios请求类
|
||||||
* @author Soybean<honghuangdc@gmail.com>
|
* @author Soybean<honghuangdc@gmail.com>
|
||||||
@@ -20,6 +22,10 @@ export default class CustomAxiosInstance {
|
|||||||
|
|
||||||
backendConfig: Service.BackendResultConfig;
|
backendConfig: Service.BackendResultConfig;
|
||||||
|
|
||||||
|
isRefreshing: boolean;
|
||||||
|
|
||||||
|
retryQueues: RefreshRequestQueue[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param axiosConfig - axios配置
|
* @param axiosConfig - axios配置
|
||||||
@@ -37,6 +43,8 @@ export default class CustomAxiosInstance {
|
|||||||
this.backendConfig = backendConfig;
|
this.backendConfig = backendConfig;
|
||||||
this.instance = axios.create(axiosConfig);
|
this.instance = axios.create(axiosConfig);
|
||||||
this.setInterceptor();
|
this.setInterceptor();
|
||||||
|
this.isRefreshing = false;
|
||||||
|
this.retryQueues = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 设置请求拦截器 */
|
/** 设置请求拦截器 */
|
||||||
@@ -60,7 +68,7 @@ export default class CustomAxiosInstance {
|
|||||||
);
|
);
|
||||||
this.instance.interceptors.response.use(
|
this.instance.interceptors.response.use(
|
||||||
(async response => {
|
(async response => {
|
||||||
const { status } = response;
|
const { status, config } = response;
|
||||||
if (status === 200 || status < 300 || status === 304) {
|
if (status === 200 || status < 300 || status === 304) {
|
||||||
const backend = response.data;
|
const backend = response.data;
|
||||||
const { codeKey, dataKey, successCode } = this.backendConfig;
|
const { codeKey, dataKey, successCode } = this.backendConfig;
|
||||||
@@ -71,10 +79,24 @@ export default class CustomAxiosInstance {
|
|||||||
|
|
||||||
// token失效, 刷新token
|
// token失效, 刷新token
|
||||||
if (REFRESH_TOKEN_CODE.includes(backend[codeKey])) {
|
if (REFRESH_TOKEN_CODE.includes(backend[codeKey])) {
|
||||||
const config = await handleRefreshToken(response.config);
|
// 原始请求
|
||||||
if (config) {
|
const originRequest = new Promise(resolve => {
|
||||||
return this.instance.request(config);
|
this.retryQueues.push((refreshConfig: AxiosRequestConfig) => {
|
||||||
|
config.headers.Authorization = refreshConfig.headers?.Authorization;
|
||||||
|
resolve(this.instance.request(config));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!this.isRefreshing) {
|
||||||
|
this.isRefreshing = true;
|
||||||
|
const refreshConfig = await handleRefreshToken(response.config);
|
||||||
|
if (refreshConfig) {
|
||||||
|
this.retryQueues.map(cb => cb(refreshConfig));
|
||||||
|
}
|
||||||
|
this.retryQueues = [];
|
||||||
|
this.isRefreshing = false;
|
||||||
}
|
}
|
||||||
|
return originRequest;
|
||||||
}
|
}
|
||||||
|
|
||||||
const error = handleBackendError(backend, this.backendConfig);
|
const error = handleBackendError(backend, this.backendConfig);
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"darkMode": false,
|
"darkMode": false,
|
||||||
"followSystemTheme": true,
|
"followSystemTheme": true,
|
||||||
|
"isCustomizeDarkModeTransition": false,
|
||||||
"layout": {
|
"layout": {
|
||||||
"minWidth": 900,
|
"minWidth": 900,
|
||||||
"mode": "vertical",
|
"mode": "vertical",
|
||||||
@@ -34,15 +35,15 @@
|
|||||||
"label": "主体滚动"
|
"label": "主体滚动"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"themeColor": "#1890ff",
|
"themeColor": "#646cff",
|
||||||
"themeColorList": [
|
"themeColorList": [
|
||||||
"#1890ff",
|
"#1890ff",
|
||||||
"#409EFF",
|
"#409EFF",
|
||||||
"#2d8cf0",
|
|
||||||
"#007AFF",
|
"#007AFF",
|
||||||
"#5ac8fa",
|
"#5ac8fa",
|
||||||
"#5856D6",
|
"#5856D6",
|
||||||
"#536dfe",
|
"#536dfe",
|
||||||
|
"#646cff",
|
||||||
"#9c27b0",
|
"#9c27b0",
|
||||||
"#AF52DE",
|
"#AF52DE",
|
||||||
"#0096c7",
|
"#0096c7",
|
||||||
|
@@ -10,11 +10,11 @@ import jsonSetting from './theme.json';
|
|||||||
const themeColorList = [
|
const themeColorList = [
|
||||||
'#1890ff',
|
'#1890ff',
|
||||||
'#409EFF',
|
'#409EFF',
|
||||||
'#2d8cf0',
|
|
||||||
'#007AFF',
|
'#007AFF',
|
||||||
'#5ac8fa',
|
'#5ac8fa',
|
||||||
'#5856D6',
|
'#5856D6',
|
||||||
'#536dfe',
|
'#536dfe',
|
||||||
|
'#646cff',
|
||||||
'#9c27b0',
|
'#9c27b0',
|
||||||
'#AF52DE',
|
'#AF52DE',
|
||||||
'#0096c7',
|
'#0096c7',
|
||||||
@@ -37,6 +37,7 @@ const themeColorList = [
|
|||||||
const defaultThemeSetting: Theme.Setting = {
|
const defaultThemeSetting: Theme.Setting = {
|
||||||
darkMode: false,
|
darkMode: false,
|
||||||
followSystemTheme: true,
|
followSystemTheme: true,
|
||||||
|
isCustomizeDarkModeTransition: false,
|
||||||
layout: {
|
layout: {
|
||||||
minWidth: 900,
|
minWidth: 900,
|
||||||
mode: 'vertical',
|
mode: 'vertical',
|
||||||
@@ -44,7 +45,7 @@ const defaultThemeSetting: Theme.Setting = {
|
|||||||
},
|
},
|
||||||
scrollMode: 'content',
|
scrollMode: 'content',
|
||||||
scrollModeList: themeScrollModeOptions,
|
scrollModeList: themeScrollModeOptions,
|
||||||
themeColor: themeColorList[0],
|
themeColor: themeColorList[6],
|
||||||
themeColorList,
|
themeColorList,
|
||||||
otherColor: {
|
otherColor: {
|
||||||
info: '#2080f0',
|
info: '#2080f0',
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import { nextTick } from 'vue';
|
import { nextTick } from 'vue';
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
|
import type { Socket } from 'socket.io-client';
|
||||||
import { LAYOUT_SCROLL_EL_ID } from '@soybeanjs/vue-materials';
|
import { LAYOUT_SCROLL_EL_ID } from '@soybeanjs/vue-materials';
|
||||||
|
|
||||||
interface AppState {
|
interface AppState {
|
||||||
@@ -17,6 +18,8 @@ interface AppState {
|
|||||||
siderCollapse: boolean;
|
siderCollapse: boolean;
|
||||||
/** vertical-mix模式下 侧边栏的固定状态 */
|
/** vertical-mix模式下 侧边栏的固定状态 */
|
||||||
mixSiderFixed: boolean;
|
mixSiderFixed: boolean;
|
||||||
|
/** socket.io 实例 */
|
||||||
|
socket: Socket | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useAppStore = defineStore('app-store', {
|
export const useAppStore = defineStore('app-store', {
|
||||||
@@ -27,7 +30,8 @@ export const useAppStore = defineStore('app-store', {
|
|||||||
reloadFlag: true,
|
reloadFlag: true,
|
||||||
settingDrawerVisible: false,
|
settingDrawerVisible: false,
|
||||||
siderCollapse: false,
|
siderCollapse: false,
|
||||||
mixSiderFixed: false
|
mixSiderFixed: false,
|
||||||
|
socket: null
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
/**
|
/**
|
||||||
@@ -97,6 +101,10 @@ export const useAppStore = defineStore('app-store', {
|
|||||||
/** 设置主体内容全屏 */
|
/** 设置主体内容全屏 */
|
||||||
setContentFull(full: boolean) {
|
setContentFull(full: boolean) {
|
||||||
this.contentFull = full;
|
this.contentFull = full;
|
||||||
|
},
|
||||||
|
/** 设置socket实例 */
|
||||||
|
setSocket<T extends Socket = Socket>(socket: T) {
|
||||||
|
this.socket = socket;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@@ -4,6 +4,7 @@ import { router } from '@/router';
|
|||||||
import { fetchLogin, fetchUserInfo } from '@/service';
|
import { fetchLogin, fetchUserInfo } from '@/service';
|
||||||
import { useRouterPush } from '@/composables';
|
import { useRouterPush } from '@/composables';
|
||||||
import { localStg } from '@/utils';
|
import { localStg } from '@/utils';
|
||||||
|
import { $t } from '@/locales';
|
||||||
import { useTabStore } from '../tab';
|
import { useTabStore } from '../tab';
|
||||||
import { useRouteStore } from '../route';
|
import { useRouteStore } from '../route';
|
||||||
import { getToken, getUserInfo, clearAuthStorage } from './helpers';
|
import { getToken, getUserInfo, clearAuthStorage } from './helpers';
|
||||||
@@ -68,8 +69,8 @@ export const useAuthStore = defineStore('auth-store', {
|
|||||||
// 登录成功弹出欢迎提示
|
// 登录成功弹出欢迎提示
|
||||||
if (route.isInitAuthRoute) {
|
if (route.isInitAuthRoute) {
|
||||||
window.$notification?.success({
|
window.$notification?.success({
|
||||||
title: '登录成功!',
|
title: $t('page.login.common.loginSuccess'),
|
||||||
content: `欢迎回来,${this.userInfo.userName}!`,
|
content: $t('page.login.common.welcomeBack', { userName: this.userInfo.userName }),
|
||||||
duration: 3000
|
duration: 3000
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@@ -14,6 +14,7 @@ import {
|
|||||||
transformRoutePathToRouteName,
|
transformRoutePathToRouteName,
|
||||||
sortRoutes
|
sortRoutes
|
||||||
} from '@/utils';
|
} from '@/utils';
|
||||||
|
import { useAppStore } from '../app';
|
||||||
import { useAuthStore } from '../auth';
|
import { useAuthStore } from '../auth';
|
||||||
import { useTabStore } from '../tab';
|
import { useTabStore } from '../tab';
|
||||||
|
|
||||||
@@ -119,9 +120,10 @@ export const useRouteStore = defineStore('route-store', {
|
|||||||
const { error, data } = await fetchUserRoutes(userId);
|
const { error, data } = await fetchUserRoutes(userId);
|
||||||
|
|
||||||
if (!error) {
|
if (!error) {
|
||||||
|
this.handleAuthRoute(sortRoutes(data.routes));
|
||||||
|
// home相关处理需要在最后,否则会出现找不到主页404的情况
|
||||||
this.routeHomeName = data.home;
|
this.routeHomeName = data.home;
|
||||||
this.handleUpdateRootRedirect(data.home);
|
this.handleUpdateRootRedirect(data.home);
|
||||||
this.handleAuthRoute(sortRoutes(data.routes));
|
|
||||||
|
|
||||||
initHomeTab(data.home, router);
|
initHomeTab(data.home, router);
|
||||||
|
|
||||||
@@ -150,7 +152,6 @@ export const useRouteStore = defineStore('route-store', {
|
|||||||
await this.initStaticRoute();
|
await this.initStaticRoute();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/** 从缓存路由中去除某个路由 */
|
/** 从缓存路由中去除某个路由 */
|
||||||
removeCacheRoute(name: AuthRoute.AllRouteKey) {
|
removeCacheRoute(name: AuthRoute.AllRouteKey) {
|
||||||
const index = this.cacheRoutes.indexOf(name);
|
const index = this.cacheRoutes.indexOf(name);
|
||||||
@@ -158,13 +159,30 @@ export const useRouteStore = defineStore('route-store', {
|
|||||||
this.cacheRoutes.splice(index, 1);
|
this.cacheRoutes.splice(index, 1);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/** 添加某个缓存路由 */
|
/** 添加某个缓存路由 */
|
||||||
addCacheRoute(name: AuthRoute.AllRouteKey) {
|
addCacheRoute(name: AuthRoute.AllRouteKey) {
|
||||||
const index = this.cacheRoutes.indexOf(name);
|
const index = this.cacheRoutes.indexOf(name);
|
||||||
if (index === -1) {
|
if (index === -1) {
|
||||||
this.cacheRoutes.push(name);
|
this.cacheRoutes.push(name);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 重新缓存路由
|
||||||
|
*/
|
||||||
|
async reCacheRoute(name: AuthRoute.AllRouteKey) {
|
||||||
|
const { reloadPage } = useAppStore();
|
||||||
|
|
||||||
|
const isCached = this.cacheRoutes.includes(name);
|
||||||
|
|
||||||
|
if (isCached) {
|
||||||
|
this.removeCacheRoute(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
await reloadPage();
|
||||||
|
|
||||||
|
if (isCached) {
|
||||||
|
this.addCacheRoute(name as AuthRoute.AllRouteKey);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user