Compare commits

..

52 Commits

Author SHA1 Message Date
Soybean
08d83ecbea chore(release): 0.9.5 2022-06-07 01:55:57 +08:00
Soybean
4122685803 feat(projects): 支持同一路由根据不同query和hash同时显示不同Tab
ISSUES CLOSED: #64
2022-06-07 01:51:40 +08:00
Soybean
434ab1c560 feat(projects): 动态路由根路由重定向只需取决于后端返回的路由首页
ISSUES CLOSED: \
2022-06-06 22:47:46 +08:00
Soybean
ae99e57c52 docs(projects): update README.md 2022-06-06 18:37:27 +08:00
Soybean
e3c4a6ece6 build(deps): 依赖升级 2022-06-06 12:14:19 +08:00
Soybean
c8717c25b8 build(projects): 配置更改 2022-06-06 12:11:01 +08:00
Soybean
e9656c6e76 docs(projects): update docs 2022-06-04 12:04:44 +08:00
Soybean
fd78791229 build(projects): 配置优化 2022-06-04 11:48:24 +08:00
Soybean
de09f82586 build(projects): 代码优化
ISSUES CLOSED: \
2022-06-02 02:39:26 +08:00
Soybean
c7762490de feat(projects): 补充更多的ECharts示例
ISSUES CLOSED: \
2022-06-01 00:27:28 +08:00
Soybean
4558c24d1c fix(projects): 修复@antv/g2生产环境报错
ISSUES CLOSED: \
2022-05-31 23:02:24 +08:00
Soybean
d9ac7e4de0 refactor(projects): 代码优化
ISSUES CLOSED: \
2022-05-31 00:26:52 +08:00
Soybean
6a5a357f50 build(deps): update deps
ISSUES CLOSED: \
2022-05-30 22:53:44 +08:00
Soybean
44b022aefd feat(projects): 添加antv g2图表示例
ISSUES CLOSED: \
2022-05-30 22:39:44 +08:00
Soybean
0a46ea0844 feat(projects): 添加插件页面:图表
ISSUES CLOSED: \
2022-05-29 23:44:47 +08:00
Soybean
39854a492b feat(projects): 添加百度地图、升级依赖
ISSUES CLOSED: \
2022-05-29 22:39:07 +08:00
Soybean
4c2f535a9b refactor(projects): 代码优化
ISSUES CLOSED: \
2022-05-28 20:26:29 +08:00
Soybean
be45d83766 build(deps): update deps
ISSUES CLOSED: \
2022-05-28 17:06:55 +08:00
Soybean
d28b9039bb refactor(projects): 代码优化
ISSUES CLOSED: \
2022-05-28 13:33:09 +08:00
Soybean
8f6d6ce3cb refactor(styles): 代码格式
ISSUES CLOSED: \
2022-05-28 12:30:17 +08:00
Soybean
07baac7cf8 build(other): update cz config 2022-05-27 09:53:40 +08:00
Soybean
7487ab79b3 chore(deps): update deps
ISSUES CLOSED: \
2022-05-27 00:54:13 +08:00
Soybean
a70e4161be chore(deps): update deps
ISSUES CLOSED: \
2022-05-19 23:51:28 +08:00
Soybean
095c432363 refactor(projects): 代码优化
ISSUES CLOSED: \
2022-05-19 00:15:37 +08:00
Soybean
028096e53f build(deps): update deps
ISSUES CLOSED: \
2022-05-19 00:00:47 +08:00
Soybean
44ab55d594 refactor(projects): 代码优化
ISSUES CLOSED: \
2022-05-18 23:43:41 +08:00
Soybean
4b80a66114 docs(projects): update README.md
ISSUES CLOSED: \
2022-05-18 23:23:04 +08:00
Soybean
cc0bb088ec Merge pull request #91 from yanbowe/main
路由meta新增activeMenu属性
2022-05-17 19:09:05 +08:00
燕博文
3e4f9e2824 fix(route): 当为左侧混合菜单时activeMenu无效情况 2022-05-17 14:06:20 +08:00
燕博文
ebd16a4d1a feat(route): 路由meta新增activeMenu属性 2022-05-17 13:54:48 +08:00
Soybean
84cb07baec docs(projects): update README.md
ISSUES CLOSED: \
2022-05-16 22:17:01 +08:00
Soybean
0811ffa5ae docs(projects): update README.md 2022-05-16 20:09:07 +08:00
Soybean
3f822a7d76 build(deps): update deps
ISSUES CLOSED: \
2022-05-14 00:57:23 +08:00
Soybean
a1c7e10574 refactor(projects): 代码优化 2022-05-11 00:18:36 +08:00
Soybean
50d7ccd82d build(deps): update deps 2022-05-10 23:53:56 +08:00
Soybean
e0233061d3 fix(projects): 修复页面切换时导致的溢出滚动条 2022-05-10 23:22:24 +08:00
Soybean
a0c405dadd build(projects): update config 2022-05-10 22:07:53 +08:00
Soybean
60f912508b fix(projects): 修复权限切换路由数据未更新的问题 2022-05-09 23:53:09 +08:00
Soybean
3590b65e22 refactor(projects): 代码优化 2022-05-09 20:52:43 +08:00
Soybean
92b8406444 build(deps): update deps 2022-05-07 01:01:46 +08:00
Soybean
e7ad08685e feat(projects): 引入echarts替换antvG2plot 2022-05-07 00:58:24 +08:00
Soybean
14c145eef1 refactor(projects): 代码优化 2022-05-05 18:42:29 +08:00
Soybean
518f7eed28 build(deps): update deps 2022-05-05 13:11:16 +08:00
Soybean
21e63998d0 docs(projects): update README.md 2022-05-05 12:50:14 +08:00
Soybean
38ee2a62cd Merge pull request #86 from dxxzst/patch-1
fix(projects): 修复插件不存在的错误提示
2022-05-04 21:14:17 +08:00
Grazing Wind
716528206e fix(projects): 修复插件不存在的错误提示
已经改成小写
2022-05-04 09:35:19 +08:00
Soybean
b81143e55e Merge pull request #84 from tclyjy/main
refactor(layouts): layout/header 反转色样式补充
2022-04-29 20:59:59 +08:00
Eric_Yuan
0243b27505 style(GlobalBreadcrumb): 代码格式fix 2022-04-29 19:19:10 +08:00
Soybean
909c12d3c6 Merge pull request #83 from toolvcn/相思
feat(projects): 添加自动跟随系统主题设置
2022-04-29 17:56:27 +08:00
元家怿
01d0bcbfd0 refactor(layouts): layout/header 反转色样式补充 2022-04-29 15:53:12 +08:00
相思
ba07b695dd feat(projects): 添加自动跟随系统主题设置 2022-04-29 15:02:51 +08:00
Soybean
3d8befa376 docs(projects): update README.md 2022-04-29 02:05:00 +08:00
175 changed files with 4306 additions and 2088 deletions

1
.env
View File

@@ -9,4 +9,5 @@ VITE_APP_DESC=SoybeanAdmin是一个中后台管理系统模版
# 权限路由模式: static dynamic # 权限路由模式: static dynamic
VITE_AUTH_ROUTE_MODE=dynamic VITE_AUTH_ROUTE_MODE=dynamic
# 路由首页(根路由重定向), 用于static模式的权限路由dynamic模式取决于后端返回的路由首页
VITE_ROUTE_HOME_PATH=/dashboard/analysis VITE_ROUTE_HOME_PATH=/dashboard/analysis

View File

@@ -26,9 +26,47 @@ module.exports = {
'@vue/eslint-config-prettier', '@vue/eslint-config-prettier',
'@vue/typescript/recommended' '@vue/typescript/recommended'
], ],
overrides: [
{
files: ['*.vue'],
rules: {
'no-undef': 'off'
}
},
{
files: ['*.html'],
rules: {
'vue/comment-directive': 'off'
}
}
],
settings: {
'import/resolver': {
alias: {
map: [
['~', '.'],
['@', './src']
],
extensions: ['.js', '.jsx', '.mjs', '.ts', '.tsx', '.d.ts']
},
node: {
extensions: ['.js', '.jsx', '.mjs', '.ts', '.tsx', '.d.ts']
}
}
},
rules: { rules: {
'import/extensions': 'off', 'import/extensions': [
'import/no-extraneous-dependencies': 'off', 'warn',
'ignorePackages',
{
js: 'never',
jsx: 'never',
mjs: 'never',
ts: 'never',
tsx: 'never'
}
],
'import/no-extraneous-dependencies': ['error', { devDependencies: true, peerDependencies: true }],
'import/order': [ 'import/order': [
'error', 'error',
{ {
@@ -144,7 +182,7 @@ module.exports = {
pathGroupsExcludedImportTypes: ['vue', 'vue-router', 'vuex', 'pinia', 'naive-ui'] pathGroupsExcludedImportTypes: ['vue', 'vue-router', 'vuex', 'pinia', 'naive-ui']
} }
], ],
'import/no-unresolved': 'off', 'import/no-unresolved': ['error', { ignore: ['uno.css', '~icons/*'] }],
'import/prefer-default-export': 'off', 'import/prefer-default-export': 'off',
'max-classes-per-file': 'off', 'max-classes-per-file': 'off',
'no-param-reassign': [ 'no-param-reassign': [
@@ -154,7 +192,6 @@ module.exports = {
ignorePropertyModificationsFor: ['state', 'acc', 'e'] ignorePropertyModificationsFor: ['state', 'acc', 'e']
} }
], ],
'no-plusplus': 'off',
'no-shadow': 'off', 'no-shadow': 'off',
'no-unused-vars': 'off', 'no-unused-vars': 'off',
'no-use-before-define': 'off', 'no-use-before-define': 'off',
@@ -164,7 +201,6 @@ module.exports = {
ignores: ['index'] ignores: ['index']
} }
], ],
'@typescript-eslint/ban-types': 'off',
'@typescript-eslint/no-empty-interface': [ '@typescript-eslint/no-empty-interface': [
'error', 'error',
{ {
@@ -175,20 +211,6 @@ module.exports = {
'@typescript-eslint/no-non-null-assertion': 'off', '@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-shadow': 'error', '@typescript-eslint/no-shadow': 'error',
'@typescript-eslint/no-unused-vars': ['warn', { ignoreRestSiblings: true, varsIgnorePattern: '^_' }], '@typescript-eslint/no-unused-vars': ['warn', { ignoreRestSiblings: true, varsIgnorePattern: '^_' }],
'@typescript-eslint/no-use-before-define': ['warn', { classes: true, functions: false, typedefs: false }] '@typescript-eslint/no-use-before-define': ['error', { classes: true, functions: false, typedefs: false }]
}, }
overrides: [
{
files: ['*.vue'],
rules: {
'no-undef': 'off'
}
},
{
files: ['*.html'],
rules: {
'vue/comment-directive': 'off'
}
}
]
}; };

2
.gitignore vendored
View File

@@ -28,3 +28,5 @@ stats.html
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?
/src/typings/components.d.ts

1
.npmrc
View File

@@ -1 +1,2 @@
registry=https://registry.npmmirror.com/
shamefully-hoist=true shamefully-hoist=true

View File

@@ -1,36 +1,35 @@
{ {
"recommendations": [ "recommendations": [
"formulahendry.auto-close-tag",
"formulahendry.auto-complete-tag",
"steoates.autoimport",
"formulahendry.auto-rename-tag",
"coenraads.bracket-pair-colorizer-2",
"naumovs.color-highlight",
"pranaygp.vscode-css-peek",
"mikestead.dotenv",
"editorconfig.editorconfig",
"dsznajder.es7-react-js-snippets",
"dbaeumer.vscode-eslint",
"miguelsolorio.fluent-icons",
"mhutchie.git-graph",
"eamodio.gitlens",
"lokalise.i18n-ally",
"afzalsayed96.icones", "afzalsayed96.icones",
"antfu.iconify", "antfu.iconify",
"kisstkondoros.vscode-gutter-preview", "antfu.unocss",
"xabikos.javascriptsnippets",
"whtouche.vscode-js-console-utils",
"ritwickdey.liveserver",
"yzhang.markdown-all-in-one",
"pkief.material-icon-theme",
"zhuangtongfa.material-theme",
"jimdong.naive-ui-snippets",
"christian-kohler.path-intellisense", "christian-kohler.path-intellisense",
"esbenp.prettier-vscode", "coenraads.bracket-pair-colorizer-2",
"johnsoncodehk.volar",
"johnsoncodehk.vscode-typescript-vue-plugin",
"dariofuzinato.vue-peek", "dariofuzinato.vue-peek",
"wscats.vue", "dbaeumer.vscode-eslint",
"antfu.unocss" "dsznajder.es7-react-js-snippets",
"eamodio.gitlens",
"editorconfig.editorconfig",
"esbenp.prettier-vscode",
"formulahendry.auto-complete-tag",
"formulahendry.auto-close-tag",
"formulahendry.auto-rename-tag",
"jimdong.naive-ui-snippets",
"kisstkondoros.vscode-gutter-preview",
"lokalise.i18n-ally",
"mhutchie.git-graph",
"miguelsolorio.fluent-icons",
"mikestead.dotenv",
"naumovs.color-highlight",
"pkief.material-icon-theme",
"pranaygp.vscode-css-peek",
"ritwickdey.liveserver",
"steoates.autoimport",
"vue.volar",
"vue.vscode-typescript-vue-plugin",
"whtouche.vscode-js-console-utils",
"xabikos.javascriptsnippets",
"yzhang.markdown-all-in-one",
"zhuangtongfa.material-theme"
] ]
} }

97
.vscode/settings.json vendored
View File

@@ -1,62 +1,24 @@
{ {
"editor.quickSuggestions": { "editor.bracketPairColorization.enabled": true,
"strings": true
},
"workbench.iconTheme": "material-icon-theme",
"workbench.colorTheme": "One Dark Pro",
"editor.tabSize": 2,
"editor.fontLigatures": true,
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.fixAll.eslint": true "source.fixAll.eslint": true
}, },
"editor.bracketPairColorization.enabled": true, "editor.fontLigatures": true,
"editor.formatOnSave": false,
"editor.guides.bracketPairs": "active", "editor.guides.bracketPairs": "active",
"git.enableSmartCommit": true, "editor.quickSuggestions": {
"path-intellisense.mappings": { "strings": true
"@": "${workspaceFolder}/src",
"~@": "${workspaceFolder}/src",
}, },
"editor.tabSize": 2,
"files.associations": {
"*.env.*": "dotenv"
},
"git.enableSmartCommit": true,
"gutterpreview.paths": { "gutterpreview.paths": {
"@": "/src", "@": "/src",
"~@": "/src" "~@": "/src"
}, },
"terminal.integrated.cursorStyle": "line",
"files.associations": {
"*.env.*": "dotenv"
},
"[jsonc]": {
"editor.defaultFormatter": "vscode.json-language-features"
},
"[json]": {
"editor.defaultFormatter": "vscode.json-language-features"
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"terminal.integrated.fontSize": 14,
"terminal.integrated.fontWeight": 500,
"i18n-ally.displayLanguage": "zh", "i18n-ally.displayLanguage": "zh",
"[html]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[vue]": {
"editor.defaultFormatter": "johnsoncodehk.volar"
},
"terminal.integrated.tabs.enabled": true,
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[markdown]": {
"editor.defaultFormatter": "yzhang.markdown-all-in-one"
},
"vue3snippets.enable-compile-vue-file-on-did-save-code": false,
"editor.formatOnSave": false,
"material-icon-theme.activeIconPack": "angular", "material-icon-theme.activeIconPack": "angular",
"material-icon-theme.files.associations": {}, "material-icon-theme.files.associations": {},
"material-icon-theme.folders.associations": { "material-icon-theme.folders.associations": {
@@ -72,5 +34,42 @@
"request": "api", "request": "api",
"adapter": "middleware" "adapter": "middleware"
}, },
"unocss.root": "src" "path-intellisense.mappings": {
"@": "${workspaceFolder}/src",
"~@": "${workspaceFolder}/src",
},
"terminal.integrated.cursorStyle": "line",
"terminal.integrated.fontSize": 14,
"terminal.integrated.fontWeight": 500,
"terminal.integrated.tabs.enabled": true,
"unocss.root": "src",
"workbench.iconTheme": "material-icon-theme",
"workbench.colorTheme": "One Dark Pro",
"[html]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "vscode.json-language-features"
},
"[jsonc]": {
"editor.defaultFormatter": "vscode.json-language-features"
},
"[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"
}
} }

View File

@@ -2,6 +2,30 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
### [0.9.5](https://github.com/honghuangdc/soybean-admin/compare/v0.9.4...v0.9.5) (2022-06-06)
### Features
* **projects:** 支持同一路由根据不同query和hash同时显示不同Tab ([4122685](https://github.com/honghuangdc/soybean-admin/commit/4122685803f8a0a485682d16cec74e27945adc47)), closes [#64](https://github.com/honghuangdc/soybean-admin/issues/64)
* **projects:** 动态路由根路由重定向只需取决于后端返回的路由首页 ([434ab1c](https://github.com/honghuangdc/soybean-admin/commit/434ab1c560b260f8a19895405eb1d3c3313052d7))
* **projects:** 补充更多的ECharts示例 ([c776249](https://github.com/honghuangdc/soybean-admin/commit/c7762490def77695bedf179ffc63e3e95d15e14d))
* **projects:** 添加百度地图、升级依赖 ([39854a4](https://github.com/honghuangdc/soybean-admin/commit/39854a492b9cce71e0c7ed52af9985cb4abd6a97))
* **projects:** 添加插件页面:图表 ([0a46ea0](https://github.com/honghuangdc/soybean-admin/commit/0a46ea08443f6b879434e925d440cf07e9494fcb))
* **projects:** 添加自动跟随系统主题设置 ([ba07b69](https://github.com/honghuangdc/soybean-admin/commit/ba07b695dd9dc5d3f8ebf57d0f2e69d624994962))
* **projects:** 添加antv g2图表示例 ([44b022a](https://github.com/honghuangdc/soybean-admin/commit/44b022aefd7dbb4c34886814cf04767450dec026))
* **projects:** 引入echarts替换antvG2plot ([e7ad086](https://github.com/honghuangdc/soybean-admin/commit/e7ad08685e8ac52a8906fc94e656192275f9764c))
* **route:** 路由meta新增activeMenu属性 ([ebd16a4](https://github.com/honghuangdc/soybean-admin/commit/ebd16a4d1ab1a95a27838a2d4f20cc1d1e7309ae))
### Bug Fixes
* **projects:** 修复@antv/g2生产环境报错 ([4558c24](https://github.com/honghuangdc/soybean-admin/commit/4558c24d1c1e1faa3326650fc16e6baf384509ac))
* **projects:** 修复插件不存在的错误提示 ([7165282](https://github.com/honghuangdc/soybean-admin/commit/716528206e9f63e873607d0afd59d83f6984e3fe))
* **projects:** 修复权限切换路由数据未更新的问题 ([60f9125](https://github.com/honghuangdc/soybean-admin/commit/60f912508b0e685957fb22ef0ed1f83272847263))
* **projects:** 修复页面切换时导致的溢出滚动条 ([e023306](https://github.com/honghuangdc/soybean-admin/commit/e0233061d3bca236b4c4bb462ce00f7ca186b9fa))
* **route:** 当为左侧混合菜单时activeMenu无效情况 ([3e4f9e2](https://github.com/honghuangdc/soybean-admin/commit/3e4f9e282442073447c5c24c33d65bc6130978ee))
### [0.9.4](https://github.com/honghuangdc/soybean-admin/compare/v0.9.3...v0.9.4) (2022-04-28) ### [0.9.4](https://github.com/honghuangdc/soybean-admin/compare/v0.9.3...v0.9.4) (2022-04-28)

View File

@@ -7,12 +7,12 @@
## 简介 ## 简介
Soybean Admin 是一个基于 Vue3、Vite、TypeScript、Naive UI 的免费中后台模版它使用了最新的前端技术栈内置丰富的主题配置有着极高的代码规范基于mock实现的动态权限路由开箱即用的中后台前端解决方案也可用于学习参考。 Soybean Admin 是一个基于 Vue3、Vite、TypeScript、Naive UI 的免费中后台模版它使用了最新的前端技术栈内置丰富的主题配置有着极高的代码规范基于mock实现的动态权限路由开箱即用的中后台前端解决方案也可用于学习参考。
## 特性 ## 特性
- **最新技术栈**:使用 Vue3/vite2 等前端前沿技术开发, 使用高效率的npm包管理器pnpm - **最新技术栈**:使用 Vue3/vite2 等前端前沿技术开发, 使用高效率的npm包管理器pnpm
- **TypeScript**: 应用程序级 JavaScript 的语言 - **TypeScript**应用程序级 JavaScript 的语言
- **主题**丰富可配置的主题、暗黑模式基于原子css - unocss的动态主题颜色 - **主题**丰富可配置的主题、暗黑模式基于原子css - unocss的动态主题颜色
- **代码规范**:丰富的规范插件及极高的代码规范 - **代码规范**:丰富的规范插件及极高的代码规范
- **权限路由**简易的路由配置、基于mock的动态路由能快速实现后端动态路由 - **权限路由**简易的路由配置、基于mock的动态路由能快速实现后端动态路由
@@ -32,41 +32,60 @@ Soybean Admin 是一个基于 Vue3、Vite、TypeScript、Naive UI 的免费中
- [gitee](https://gitee.com/honghuangdc/soybean-admin) - [gitee](https://gitee.com/honghuangdc/soybean-admin)
## 更新日志
[CHANGELOG](./CHANGELOG.md)
## 后端服务
- [soybean-admin-java(开发中)](https://github.com/honghuangdc/soybean-admin-java)
- [soybean-admin-go(开发中)](https://github.com/honghuangdc/soybean-admin-go)
- [soybean-admin-nestjs(开发中)](https://github.com/honghuangdc/soybean-admin-nestjs)
## 项目示例图 ## 项目示例图
![](https://s2.loli.net/2022/01/24/ovK6Oyqr7gIMu2n.png) ![](https://s2.loli.net/2022/05/16/keOtgFH27r9nqYS.png)
![](https://s2.loli.net/2022/01/24/O8loxYhMySHwGfJ.png) ![](https://s2.loli.net/2022/05/18/bW7mftiQexkvSTG.png)
![](https://s2.loli.net/2022/01/24/HKwpJ7Ab6j8fVvk.png) ![](https://s2.loli.net/2022/05/16/uV5nzjb3gYptAEl.png)
![](https://s2.loli.net/2022/01/24/bqJRSDZHBv3jsif.png) ![](https://s2.loli.net/2022/05/16/rSnNHLdpuvkKxWq.png)
![](https://s2.loli.net/2022/01/24/wXpHeau6UrSTWdF.png) ![](https://s2.loli.net/2022/05/18/Mt6YZqmDxO8v4uR.png)
![](https://s2.loli.net/2022/05/16/ktH5dcG3fuFOoKA.png)
![](https://s2.loli.net/2022/05/16/VPl6Ru1iCAhLcS4.png)
![](https://s2.loli.net/2022/05/16/bRlAKuHW7ZVh9DT.png)
![](https://s2.loli.net/2022/05/16/JvNt61rx5nca42i.png)
![](https://s2.loli.net/2022/05/18/8WJvaz13ibXmsND.png)
![](https://s2.loli.net/2022/02/16/pBwF2gaxXnKZe3D.png)
![](https://s2.loli.net/2022/02/16/pfuxVEPsTJIXw5n.png)
## 开发计划 ## 开发计划
- [x] 添加前端静态路由 - [x] 引入ECharts替换AntV G2Plot
- [x] 集成unocss替换windicss - [x] 图表示例ECharts、AntV G2
- [x] 用户角色切换示例、按钮级别权限指令 - [ ] 多页签支持query、hash等参数同一页面支持多个Tab
- [ ] 引入ECharts替换AntV G2Plot - [ ] 缓存主题配置
- [ ] 最近功能的有关文档更新
- [ ] 性能优化(优化递归函数)
- [ ] 精简版(新分支thin)
- [ ] 表单、表格示例
- [ ] 添加锁屏组件、全局Iframe组件 - [ ] 添加锁屏组件、全局Iframe组件
- [ ] 示例页面完善 - [ ] 示例页面完善
- [ ] 其他UI版本 - [ ] 性能优化(优化递归函数)
- [ ] 精简版(新分支thin)
- [ ] 文档完善
- [ ] 表单、表格示例
- [ ] element-plus版本 - [ ] element-plus版本
- [ ] 其他UI版本
- [ ] soybean-admin cli工具(选择不同UI) - [ ] soybean-admin cli工具(选择不同UI)
- [ ] 前端可视化创建路由页面
- [ ] soybean-admin 后台服务java版: [soybean-admin-java](https://github.com/honghuangdc/soybean-admin-java) - [ ] soybean-admin 后台服务java版: [soybean-admin-java](https://github.com/honghuangdc/soybean-admin-java)
- [ ] soybean-admin 后台服务go版: [soybean-admin-go](https://github.com/honghuangdc/soybean-admin-go) - [ ] soybean-admin 后台服务go版: [soybean-admin-go](https://github.com/honghuangdc/soybean-admin-go)
- [ ] soybean-admin 后台服务nodejs版: [soybean-admin-nestjs](https://github.com/honghuangdc/soybean-admin-nestjs) - [ ] soybean-admin 后台服务nodejs版: [soybean-admin-nestjs](https://github.com/honghuangdc/soybean-admin-nestjs)
- [ ] 前端可视化创建路由页面
## 安装使用 ## 安装使用
@@ -94,11 +113,8 @@ pnpm dev
pnpm build pnpm build
``` ```
::: warning 注意
**本地环境需要安装 pnpm 6.x 、Node.js 14.x 和 Git** **本地环境需要安装 pnpm 6.x 、Node.js 14.x 和 Git**
:::
## 如何贡献 ## 如何贡献
@@ -128,14 +144,19 @@ pnpm i -g commitizen
[@Soybean](https://github.com/honghuangdc) [@Soybean](https://github.com/honghuangdc)
## 捐赠
如果你觉得这个项目对你有帮助可以请Soybean喝杯饮料表示支持Soybean开源的动力离不开各位的支持和鼓励。
![赞助](https://s2.loli.net/2022/01/24/i9cpq7lTCrKUoFf.png)
## 交流 ## 交流
`Soybean Admin` 是完全开源免费的项目在帮助开发者更方便地进行中大型管理系统开发同时也提供微信和QQ交流群使用问题欢迎在群内提问。 `Soybean Admin` 是完全开源免费的项目在帮助开发者更方便地进行中大型管理系统开发同时也提供微信和QQ交流群使用问题欢迎在群内提问。
- 本人微信号honghuangdc,欢迎来技术交流,业务咨询。 - 微信交流群(添加本人微信拉进群),欢迎来技术交流,业务咨询。
- 微信交流群: <div style="text-align:left">
**微信群的人数已经满200个了无法扫码可以添加本人的微信再邀请进入** <img src="https://s2.loli.net/2022/05/16/3YGBgXnVPJdslk8.jpg" style="width:200px" />
</div>
- QQ交流群 `711301266` - QQ交流群 `711301266`

View File

@@ -131,6 +131,47 @@ export const routeModel: Record<Auth.RoleType, AuthRoute.Route[]> = {
path: '/plugin', path: '/plugin',
component: 'basic', component: 'basic',
children: [ children: [
{
name: 'plugin_charts',
path: '/plugin/charts',
component: 'multi',
children: [
{
name: 'plugin_charts_echarts',
path: '/plugin/charts/echarts',
component: 'self',
meta: {
title: 'ECharts',
requiresAuth: true,
icon: 'simple-icons:apacheecharts'
}
},
{
name: 'plugin_charts_d3',
path: '/plugin/charts/d3',
component: 'self',
meta: {
title: 'D3',
requiresAuth: true,
icon: 'simple-icons:d3dotjs'
}
},
{
name: 'plugin_charts_antv',
path: '/plugin/charts/antv',
component: 'self',
meta: {
title: 'AntV',
requiresAuth: true,
icon: 'ant-design:bar-chart-outlined'
}
}
],
meta: {
title: '图表',
icon: 'material-symbols:bar-chart-rounded'
}
},
{ {
name: 'plugin_map', name: 'plugin_map',
path: '/plugin/map', path: '/plugin/map',
@@ -261,6 +302,53 @@ export const routeModel: Record<Auth.RoleType, AuthRoute.Route[]> = {
order: 5 order: 5
} }
}, },
{
name: 'function',
path: '/function',
component: 'basic',
children: [
{
name: 'function_tab',
path: '/function/tab',
component: 'self',
meta: {
title: '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: '功能',
icon: 'ri:function-line',
order: 6
}
},
{ {
name: 'exception', name: 'exception',
path: '/exception', path: '/exception',
@@ -300,7 +388,7 @@ export const routeModel: Record<Auth.RoleType, AuthRoute.Route[]> = {
meta: { meta: {
title: '异常页', title: '异常页',
icon: 'ant-design:exception-outlined', icon: 'ant-design:exception-outlined',
order: 6 order: 7
} }
}, },
{ {
@@ -354,7 +442,7 @@ export const routeModel: Record<Auth.RoleType, AuthRoute.Route[]> = {
meta: { meta: {
title: '多级菜单', title: '多级菜单',
icon: 'carbon:menu', icon: 'carbon:menu',
order: 7 order: 8
} }
}, },
{ {
@@ -366,7 +454,7 @@ export const routeModel: Record<Auth.RoleType, AuthRoute.Route[]> = {
requiresAuth: true, requiresAuth: true,
singleLayout: 'basic', singleLayout: 'basic',
icon: 'fluent:book-information-24-regular', icon: 'fluent:book-information-24-regular',
order: 8 order: 9
} }
} }
], ],
@@ -502,6 +590,47 @@ export const routeModel: Record<Auth.RoleType, AuthRoute.Route[]> = {
path: '/plugin', path: '/plugin',
component: 'basic', component: 'basic',
children: [ children: [
{
name: 'plugin_charts',
path: '/plugin/charts',
component: 'multi',
children: [
{
name: 'plugin_charts_echarts',
path: '/plugin/charts/echarts',
component: 'self',
meta: {
title: 'ECharts',
requiresAuth: true,
icon: 'simple-icons:apacheecharts'
}
},
{
name: 'plugin_charts_d3',
path: '/plugin/charts/d3',
component: 'self',
meta: {
title: 'D3',
requiresAuth: true,
icon: 'simple-icons:d3dotjs'
}
},
{
name: 'plugin_charts_antv',
path: '/plugin/charts/antv',
component: 'self',
meta: {
title: 'AntV',
requiresAuth: true,
icon: 'ant-design:bar-chart-outlined'
}
}
],
meta: {
title: '图表',
icon: 'material-symbols:bar-chart-rounded'
}
},
{ {
name: 'plugin_map', name: 'plugin_map',
path: '/plugin/map', path: '/plugin/map',
@@ -622,6 +751,53 @@ export const routeModel: Record<Auth.RoleType, AuthRoute.Route[]> = {
order: 5 order: 5
} }
}, },
{
name: 'function',
path: '/function',
component: 'basic',
children: [
{
name: 'function_tab',
path: '/function/tab',
component: 'self',
meta: {
title: '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: '功能',
icon: 'ri:function-line',
order: 6
}
},
{ {
name: 'exception', name: 'exception',
path: '/exception', path: '/exception',
@@ -661,7 +837,7 @@ export const routeModel: Record<Auth.RoleType, AuthRoute.Route[]> = {
meta: { meta: {
title: '异常页', title: '异常页',
icon: 'ant-design:exception-outlined', icon: 'ant-design:exception-outlined',
order: 6 order: 7
} }
}, },
{ {
@@ -715,7 +891,7 @@ export const routeModel: Record<Auth.RoleType, AuthRoute.Route[]> = {
meta: { meta: {
title: '多级菜单', title: '多级菜单',
icon: 'carbon:menu', icon: 'carbon:menu',
order: 7 order: 8
} }
}, },
{ {
@@ -727,7 +903,7 @@ export const routeModel: Record<Auth.RoleType, AuthRoute.Route[]> = {
requiresAuth: true, requiresAuth: true,
singleLayout: 'basic', singleLayout: 'basic',
icon: 'fluent:book-information-24-regular', icon: 'fluent:book-information-24-regular',
order: 8 order: 9
} }
} }
], ],

View File

@@ -1,6 +1,7 @@
{ {
"name": "soybean-admin", "name": "soybean-admin",
"version": "0.9.4", "version": "0.9.5",
"license": "MIT",
"scripts": { "scripts": {
"dev": "cross-env VITE_ENV_TYPE=dev vite", "dev": "cross-env VITE_ENV_TYPE=dev vite",
"dev:test": "cross-env VITE_ENV_TYPE=test vite", "dev:test": "cross-env VITE_ENV_TYPE=test vite",
@@ -9,7 +10,7 @@
"build:dev": "npm run typecheck && cross-env VITE_ENV_TYPE=dev vite build", "build:dev": "npm run typecheck && cross-env VITE_ENV_TYPE=dev vite build",
"build:test": "npm run typecheck && cross-env VITE_ENV_TYPE=test vite build", "build:test": "npm run typecheck && cross-env VITE_ENV_TYPE=test vite build",
"build:vercel": "cross-env VITE_HASH_ROUTE=true vite build", "build:vercel": "cross-env VITE_HASH_ROUTE=true vite build",
"preview": "vite preview --port 5050", "preview": "vite preview",
"typecheck": "vue-tsc --noEmit", "typecheck": "vue-tsc --noEmit",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix", "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
"prepare": "husky install", "prepare": "husky install",
@@ -26,44 +27,46 @@
} }
}, },
"dependencies": { "dependencies": {
"@antv/g2plot": "^2.4.16", "@antv/data-set": "^0.11.8",
"@antv/g2": "^4.2.2",
"@better-scroll/core": "^2.4.2", "@better-scroll/core": "^2.4.2",
"@soybeanjs/vue-admin-layout": "^1.0.3", "@soybeanjs/vue-admin-layout": "^1.0.4",
"@soybeanjs/vue-admin-tab": "^1.0.1", "@soybeanjs/vue-admin-tab": "^1.0.2",
"@vueuse/core": "^8.3.1", "@vueuse/core": "^8.6.0",
"axios": "^0.27.2", "axios": "^0.27.2",
"clipboard": "^2.0.10", "clipboard": "^2.0.11",
"colord": "^2.9.2", "colord": "^2.9.2",
"crypto-js": "^4.1.1", "crypto-js": "^4.1.1",
"dayjs": "^1.11.1", "dayjs": "^1.11.3",
"echarts": "^5.3.2",
"form-data": "^4.0.0", "form-data": "^4.0.0",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"naive-ui": "^2.28.2", "naive-ui": "^2.30.0",
"pinia": "^2.0.13", "pinia": "^2.0.14",
"print-js": "^1.6.0", "print-js": "^1.6.0",
"qs": "^6.10.3", "qs": "^6.10.3",
"swiper": "^8.1.4", "swiper": "^8.2.2",
"ua-parser-js": "^1.0.2", "ua-parser-js": "^1.0.2",
"vditor": "^3.8.13", "vditor": "^3.8.15",
"vue": "3.2.33", "vue": "3.2.36",
"vue-router": "^4.0.14", "vue-router": "^4.0.15",
"wangeditor": "^4.7.15", "wangeditor": "^4.7.15",
"xgplayer": "^2.31.6" "xgplayer": "^2.31.6"
}, },
"devDependencies": { "devDependencies": {
"@amap/amap-jsapi-types": "^0.0.8", "@amap/amap-jsapi-types": "^0.0.8",
"@commitlint/cli": "^16.2.4", "@commitlint/cli": "^17.0.2",
"@commitlint/config-conventional": "^16.2.4", "@commitlint/config-conventional": "^17.0.2",
"@iconify/json": "^2.1.33", "@iconify/json": "^2.1.56",
"@iconify/vue": "^3.2.1", "@iconify/vue": "^3.2.1",
"@types/bmapgl": "^0.0.5", "@types/bmapgl": "^0.0.5",
"@types/crypto-js": "^4.1.1", "@types/crypto-js": "^4.1.1",
"@types/node": "^17.0.29", "@types/node": "^17.0.40",
"@types/qs": "^6.9.7", "@types/qs": "^6.9.7",
"@types/ua-parser-js": "^0.7.36", "@types/ua-parser-js": "^0.7.36",
"@typescript-eslint/eslint-plugin": "^5.21.0", "@typescript-eslint/eslint-plugin": "^5.27.0",
"@typescript-eslint/parser": "^5.21.0", "@typescript-eslint/parser": "^5.27.0",
"@vitejs/plugin-vue": "^2.3.1", "@vitejs/plugin-vue": "^2.3.3",
"@vitejs/plugin-vue-jsx": "^1.3.10", "@vitejs/plugin-vue-jsx": "^1.3.10",
"@vue/eslint-config-prettier": "^7.0.0", "@vue/eslint-config-prettier": "^7.0.0",
"@vue/eslint-config-typescript": "^10.0.0", "@vue/eslint-config-typescript": "^10.0.0",
@@ -71,31 +74,32 @@
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"cz-conventional-changelog": "^3.3.0", "cz-conventional-changelog": "^3.3.0",
"cz-customizable": "^6.3.0", "cz-customizable": "^6.3.0",
"eslint": "^8.14.0", "eslint": "^8.17.0",
"eslint-config-airbnb-base": "^15.0.0", "eslint-config-airbnb-base": "^15.0.0",
"eslint-config-prettier": "^8.5.0", "eslint-config-prettier": "^8.5.0",
"eslint-import-resolver-alias": "^1.1.2",
"eslint-plugin-import": "^2.26.0", "eslint-plugin-import": "^2.26.0",
"eslint-plugin-prettier": "^4.0.0", "eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-vue": "^8.7.1", "eslint-plugin-vue": "9.1.0",
"husky": "^7.0.4", "husky": "^8.0.1",
"lint-staged": "^12.4.1", "lint-staged": "^13.0.0",
"mockjs": "^1.1.0", "mockjs": "^1.1.0",
"patch-package": "^6.4.7", "patch-package": "^6.4.7",
"postinstall-postinstall": "^2.1.0", "postinstall-postinstall": "^2.1.0",
"prettier": "^2.6.2", "prettier": "^2.6.2",
"rollup-plugin-visualizer": "^5.6.0", "rollup-plugin-visualizer": "^5.6.0",
"sass": "^1.51.0", "sass": "^1.52.2",
"standard-version": "^9.3.2", "standard-version": "^9.5.0",
"typescript": "^4.6.3", "typescript": "^4.7.3",
"unocss": "^0.32.1", "unocss": "^0.37.4",
"unplugin-icons": "^0.14.1", "unplugin-icons": "^0.14.3",
"unplugin-vue-components": "0.19.3", "unplugin-vue-components": "0.19.6",
"unplugin-vue-define-options": "^0.6.1", "unplugin-vue-define-options": "^0.6.1",
"vite": "^2.9.6", "vite": "^2.9.9",
"vite-plugin-compression": "^0.5.1", "vite-plugin-compression": "^0.5.1",
"vite-plugin-html": "^3.2.0", "vite-plugin-html": "^3.2.0",
"vite-plugin-mock": "^2.9.6", "vite-plugin-mock": "^2.9.6",
"vue-tsc": "^0.34.10", "vue-eslint-parser": "^9.0.2",
"vueuc": "^0.4.32" "vue-tsc": "^0.36.1"
} }
} }

2907
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -20,4 +20,5 @@ const theme = useThemeStore();
subscribeStore(); subscribeStore();
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -91,4 +91,5 @@ onUnmounted(() => {
stopHandle(); stopHandle();
}); });
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -44,4 +44,5 @@ function handleClickPolicy() {
emit('click-policy'); emit('click-policy');
} }
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -15,4 +15,5 @@ withDefaults(defineProps<Props>(), {
inverted: false inverted: false
}); });
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -1,5 +1,5 @@
<template> <template>
<div class="flex-center text-18px hover:text-primary cursor-pointer" @click="handleSwitch"> <div class="flex-center text-18px cursor-pointer" @click="handleSwitch">
<icon-mdi-moon-waning-crescent v-if="darkMode" /> <icon-mdi-moon-waning-crescent v-if="darkMode" />
<icon-mdi-white-balance-sunny v-else /> <icon-mdi-white-balance-sunny v-else />
</div> </div>
@@ -36,4 +36,5 @@ function handleSwitch() {
darkMode.value = !darkMode.value; darkMode.value = !darkMode.value;
} }
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -12,7 +12,6 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { RouterLink } from 'vue-router';
import { routeName } from '@/router'; import { routeName } from '@/router';
type ExceptionType = '403' | '404' | '500'; type ExceptionType = '403' | '404' | '500';
@@ -26,4 +25,5 @@ defineProps<Props>();
const routeHomePath = routeName('root'); const routeHomePath = routeName('root');
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -2,36 +2,44 @@
<div v-if="showTooltip"> <div v-if="showTooltip">
<n-tooltip :placement="placement" trigger="hover"> <n-tooltip :placement="placement" trigger="hover">
<template #trigger> <template #trigger>
<div class="flex-center h-full cursor-pointer hover:bg-[#f6f6f6] dark:hover:bg-[#333]" :class="contentClass"> <div class="flex-center h-full cursor-pointer dark:hover:bg-[#333]" :class="contentClassName">
<slot></slot> <slot></slot>
</div> </div>
</template> </template>
{{ tooltipContent }} {{ tooltipContent }}
</n-tooltip> </n-tooltip>
</div> </div>
<div v-else class="flex-center cursor-pointer hover:bg-[#f6f6f6] dark:hover:bg-[#333]" :class="contentClass"> <div v-else class="flex-center cursor-pointer dark:hover:bg-[#333]" :class="contentClassName">
<slot></slot> <slot></slot>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from 'vue'; import { computed } from 'vue';
import type { FollowerPlacement } from 'vueuc'; import type { PopoverPlacement } from 'naive-ui';
interface Props { interface Props {
/** tooltip显示文本 */ /** tooltip显示文本 */
tooltipContent?: string; tooltipContent?: string;
/** tooltip的位置 */ /** tooltip的位置 */
placement?: FollowerPlacement; placement?: PopoverPlacement;
/** class类 */ /** class类 */
contentClass?: string; contentClass?: string;
/** 反转模式下 */
inverted?: boolean;
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
tooltipContent: '', tooltipContent: '',
placement: 'bottom', placement: 'bottom',
contentClass: '' contentClass: '',
inverted: false
}); });
const showTooltip = computed(() => Boolean(props.tooltipContent)); const showTooltip = computed(() => Boolean(props.tooltipContent));
const contentClassName = computed(
() => `${props.contentClass} ${props.inverted ? 'hover:bg-primary' : 'hover:bg-[#f6f6f6]'}`
);
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -13,4 +13,5 @@ withDefaults(defineProps<Props>(), {
fill: false fill: false
}); });
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -44,4 +44,5 @@ onMounted(() => {
defineExpose({ instance }); defineExpose({ instance });
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -106,3 +106,5 @@ onMounted(() => {
} }
}); });
</script> </script>
<style scoped></style>

View File

@@ -66,6 +66,7 @@ function handleChange(iconItem: string) {
modelValue.value = iconItem; modelValue.value = iconItem;
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
:deep(.n-input-wrapper) { :deep(.n-input-wrapper) {
padding-right: 0; padding-right: 0;

View File

@@ -36,4 +36,5 @@ watch(imgCode, newValue => {
defineExpose({ getImgCode }); defineExpose({ getImgCode });
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -17,4 +17,5 @@ interface Props {
defineProps<Props>(); defineProps<Props>();
</script> </script>
<style scoped></style> <style scoped></style>

163
src/composables/echarts.ts Normal file
View File

@@ -0,0 +1,163 @@
import { ref, watch, nextTick, onUnmounted } from 'vue';
import type { Ref, ComputedRef } from 'vue';
import * as echarts from 'echarts/core';
import { BarChart, LineChart, PieChart, ScatterChart, PictorialBarChart, RadarChart, GaugeChart } from 'echarts/charts';
import type {
BarSeriesOption,
LineSeriesOption,
PieSeriesOption,
ScatterSeriesOption,
PictorialBarSeriesOption,
RadarSeriesOption,
GaugeSeriesOption
} from 'echarts/charts';
import {
TitleComponent,
LegendComponent,
TooltipComponent,
GridComponent,
DatasetComponent,
TransformComponent,
ToolboxComponent
} from 'echarts/components';
import type {
TitleComponentOption,
LegendComponentOption,
TooltipComponentOption,
GridComponentOption,
ToolboxComponentOption,
DatasetComponentOption
} 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 | null>(null);
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!.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 stopSizeWatch = watch([width, height], ([newWidth, newHeight]) => {
initialSize.width = newWidth;
initialSize.height = newHeight;
if (canRender()) {
if (!isRendered()) {
render();
} else {
resize();
}
}
});
const stopOptionWatch = watch(options, newValue => {
update(newValue);
});
const stopDarkModeWatch = watch(
() => theme.darkMode,
() => {
updateTheme();
}
);
onUnmounted(() => {
destroy();
stopSizeWatch();
stopOptionWatch();
stopDarkModeWatch();
});
return {
domRef
};
}

View File

@@ -1,3 +1,4 @@
export * from './system'; export * from './system';
export * from './router'; export * from './router';
export * from './layout'; export * from './layout';
export * from './echarts';

View File

@@ -1,13 +1,13 @@
import { computed } from 'vue'; import { computed } from 'vue';
import { useAppStore, useThemeStore } from '@/store'; import { useAppStore, useThemeStore } from '@/store';
type LayoutMode = 'vertical' | 'horizontal';
type LayoutHeaderProps = Record<EnumType.ThemeLayoutMode, GlobalHeaderProps>; type LayoutHeaderProps = Record<EnumType.ThemeLayoutMode, GlobalHeaderProps>;
export function useBasicLayout() { export function useBasicLayout() {
const app = useAppStore(); const app = useAppStore();
const theme = useThemeStore(); const theme = useThemeStore();
type LayoutMode = 'vertical' | 'horizontal';
const mode = computed(() => { const mode = computed(() => {
const vertical: LayoutMode = 'vertical'; const vertical: LayoutMode = 'vertical';
const horizontal: LayoutMode = 'horizontal'; const horizontal: LayoutMode = 'horizontal';

View File

@@ -3,7 +3,6 @@ export enum EnumUserRole {
super = '超级管理员', super = '超级管理员',
admin = '管理员', admin = '管理员',
user = '普通用户' user = '普通用户'
// custom = '自定义角色'
} }
/** 登录模块 */ /** 登录模块 */

View File

@@ -16,7 +16,7 @@ export enum EnumStorageKey {
/** 用户信息 */ /** 用户信息 */
'user-info' = '__USER_INFO__', 'user-info' = '__USER_INFO__',
/** 多页签路由信息 */ /** 多页签路由信息 */
'tab-routes' = '__TAB_ROUTES__' 'multi-tab-routes' = '__MULTI_TAB_ROUTES__'
} }
/** 数据类型 */ /** 数据类型 */
@@ -31,5 +31,6 @@ export enum EnumDataType {
date = '[object Date]', date = '[object Date]',
regexp = '[object RegExp]', regexp = '[object RegExp]',
set = '[object Set]', set = '[object Set]',
map = '[object Map]' map = '[object Map]',
file = '[object File]'
} }

View File

@@ -52,6 +52,7 @@ function draw(dom: HTMLCanvasElement, width: number, height: number) {
ctx.fillStyle = randomColor(180, 230); ctx.fillStyle = randomColor(180, 230);
ctx.fillRect(0, 0, width, height); ctx.fillRect(0, 0, width, height);
for (let i = 0; i < 4; i += 1) { for (let i = 0; i < 4; i += 1) {
const text = NUMBER_STRING[randomNum(0, NUMBER_STRING.length)]; const text = NUMBER_STRING[randomNum(0, NUMBER_STRING.length)];
imgCode += text; imgCode += text;
@@ -81,5 +82,6 @@ function draw(dom: HTMLCanvasElement, width: number, height: number) {
ctx.fillStyle = randomColor(150, 200); ctx.fillStyle = randomColor(150, 200);
ctx.fill(); ctx.fill();
} }
return imgCode; return imgCode;
} }

View File

@@ -7,6 +7,7 @@ import useCountDown from './useCountDown';
export default function useSmsCode() { export default function useSmsCode() {
const { loading, startLoading, endLoading } = useLoading(); const { loading, startLoading, endLoading } = useLoading();
const { counts, start, isCounting } = useCountDown(60); const { counts, start, isCounting } = useCountDown(60);
const initLabel = '获取验证码'; const initLabel = '获取验证码';
const countingLabel = (second: number) => `${second}秒后重新获取`; const countingLabel = (second: number) => `${second}秒后重新获取`;
const label = computed(() => { const label = computed(() => {
@@ -40,6 +41,7 @@ export default function useSmsCode() {
async function getSmsCode(phone: string) { async function getSmsCode(phone: string) {
const valid = isPhoneValid(phone); const valid = isPhoneValid(phone);
if (!valid || loading.value) return; if (!valid || loading.value) return;
startLoading(); startLoading();
const { data } = await fetchSmsCode(phone); const { data } = await fetchSmsCode(phone);
if (data) { if (data) {

View File

@@ -3,7 +3,5 @@ import useBoolean from './useBoolean';
import useLoading from './useLoading'; import useLoading from './useLoading';
import useLoadingEmpty from './useLoadingEmpty'; import useLoadingEmpty from './useLoadingEmpty';
import useReload from './useReload'; import useReload from './useReload';
import useBodyScroll from './useBodyScroll';
import useModalVisible from './useModalVisible';
export { useContext, useBoolean, useLoading, useLoadingEmpty, useReload, useBodyScroll, useModalVisible }; export { useContext, useBoolean, useLoading, useLoadingEmpty, useReload };

View File

@@ -1,47 +0,0 @@
interface ScrollBodyStyle {
overflow: string;
paddingRight: string;
}
/**
* body标签滚动
* @param duration - 显示滚动条的延迟时间
*/
export default function useBodyScroll(duration = 300) {
const defaultStyle: ScrollBodyStyle = {
overflow: '',
paddingRight: ''
};
function getInitBodyStyle() {
const { overflow, paddingRight } = document.body.style;
Object.assign(defaultStyle, { overflow, paddingRight });
}
function setScrollBodyStyle() {
document.body.style.paddingRight = `${window.innerWidth - document.body.clientWidth}px`;
document.body.style.overflow = 'hidden';
}
function resetScrollBodyStyle() {
document.body.style.overflow = defaultStyle.overflow;
document.body.style.paddingRight = defaultStyle.paddingRight;
}
/**
* 处理body的滚动条
* @param hideScroll - 禁止滚动
*/
function scrollBodyHandler(hideScroll: boolean) {
if (hideScroll) {
setScrollBodyStyle();
} else {
setTimeout(() => {
resetScrollBodyStyle();
}, duration);
}
}
getInitBodyStyle();
return {
scrollBodyHandler
};
}

View File

@@ -1,33 +0,0 @@
import { watch, onUnmounted } from 'vue';
import useBoolean from './useBoolean';
import useBodyScroll from './useBodyScroll';
/**
* 使用弹窗
* @param hideScroll - 关闭html滚动条
*/
export default function useModalVisible(hideScroll = true) {
const { bool: visible, setTrue: openModal, setFalse: closeModal, toggle: toggleModal } = useBoolean();
const { scrollBodyHandler } = useBodyScroll();
function modalVisibleWatcher() {
const stopHandle = watch(visible, async newValue => {
scrollBodyHandler(newValue);
});
onUnmounted(() => {
stopHandle();
});
}
if (hideScroll) {
modalVisibleWatcher();
}
return {
visible,
openModal,
closeModal,
toggleModal
};
}

View File

@@ -13,9 +13,12 @@ export default function useReload() {
async function handleReload(duration = 0) { async function handleReload(duration = 0) {
setFalse(); setFalse();
await nextTick(); await nextTick();
setTimeout(() => {
setTrue(); if (duration > 0) {
}, duration); setTimeout(() => {
setTrue();
}, duration);
}
} }
return { return {

View File

@@ -10,6 +10,7 @@
:sider-width="siderWidth" :sider-width="siderWidth"
:sider-collapsed-width="siderCollapsedWidth" :sider-collapsed-width="siderCollapsedWidth"
:sider-collapse="app.siderCollapse" :sider-collapse="app.siderCollapse"
:add-main-overflow-hidden="addMainOverflowHidden"
:fixed-footer="theme.footer.fixed" :fixed-footer="theme.footer.fixed"
> >
<template #header> <template #header>
@@ -21,7 +22,7 @@
<template #sider> <template #sider>
<global-sider /> <global-sider />
</template> </template>
<global-content /> <global-content @hide-main-overflow="setAddMainOverflowHidden" />
<template #footer> <template #footer>
<global-footer /> <global-footer />
</template> </template>
@@ -33,11 +34,15 @@
import AdminLayout from '@soybeanjs/vue-admin-layout'; import AdminLayout from '@soybeanjs/vue-admin-layout';
import { useAppStore, useThemeStore } from '@/store'; import { useAppStore, useThemeStore } from '@/store';
import { useBasicLayout } from '@/composables'; import { useBasicLayout } from '@/composables';
import { useBoolean } from '@/hooks';
import { SettingDrawer, GlobalHeader, GlobalTab, GlobalSider, GlobalContent, GlobalFooter } from '../common'; import { SettingDrawer, GlobalHeader, GlobalTab, GlobalSider, GlobalContent, GlobalFooter } from '../common';
const app = useAppStore(); const app = useAppStore();
const theme = useThemeStore(); const theme = useThemeStore();
const { mode, headerProps, siderVisible, siderWidth, siderCollapsedWidth } = useBasicLayout(); const { mode, headerProps, siderVisible, siderWidth, siderCollapsedWidth } = useBasicLayout();
const { bool: addMainOverflowHidden, setBool: setAddMainOverflowHidden } = useBoolean();
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -5,4 +5,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { GlobalContent } from '../common'; import { GlobalContent } from '../common';
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -4,9 +4,15 @@
class="h-full bg-[#f6f9f8] dark:bg-[#101014] transition duration-300 ease-in-out" class="h-full bg-[#f6f9f8] dark:bg-[#101014] transition duration-300 ease-in-out"
> >
<router-view v-slot="{ Component, route }"> <router-view v-slot="{ Component, route }">
<transition :name="theme.page.animate ? theme.page.animateMode : undefined" mode="out-in" appear> <transition
:name="theme.pageAnimateMode"
mode="out-in"
:appear="true"
@before-leave="handleBeforeLeave"
@after-enter="handleAfterEnter"
>
<keep-alive :include="routeStore.cacheRoutes"> <keep-alive :include="routeStore.cacheRoutes">
<component :is="Component" v-if="app.reloadFlag" :key="route.path" /> <component :is="Component" v-if="app.reloadFlag" :key="route.fullPath" />
</keep-alive> </keep-alive>
</transition> </transition>
</router-view> </router-view>
@@ -14,7 +20,6 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { RouterView } from 'vue-router';
import { useAppStore, useThemeStore, useRouteStore } from '@/store'; import { useAppStore, useThemeStore, useRouteStore } from '@/store';
interface Props { interface Props {
@@ -22,12 +27,27 @@ interface Props {
showPadding?: boolean; showPadding?: boolean;
} }
interface Emits {
/** 禁止主体溢出 */
(e: 'hide-main-overflow', hidden: boolean): void;
}
withDefaults(defineProps<Props>(), { withDefaults(defineProps<Props>(), {
showPadding: true showPadding: true
}); });
const emit = defineEmits<Emits>();
const app = useAppStore(); const app = useAppStore();
const theme = useThemeStore(); const theme = useThemeStore();
const routeStore = useRouteStore(); const routeStore = useRouteStore();
function handleBeforeLeave() {
emit('hide-main-overflow', true);
}
function handleAfterEnter() {
emit('hide-main-overflow', false);
}
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -5,4 +5,5 @@
</template> </template>
<script setup lang="ts"></script> <script setup lang="ts"></script>
<style scoped></style> <style scoped></style>

View File

@@ -1,5 +1,5 @@
<template> <template>
<hover-container class="w-40px h-full" tooltip-content="全屏" content-class="hover:text-primary" @click="toggle"> <hover-container class="w-40px h-full" tooltip-content="全屏" :inverted="theme.header.inverted" @click="toggle">
<icon-gridicons-fullscreen-exit v-if="isFullscreen" class="text-18px" /> <icon-gridicons-fullscreen-exit v-if="isFullscreen" class="text-18px" />
<icon-gridicons-fullscreen v-else class="text-18px" /> <icon-gridicons-fullscreen v-else class="text-18px" />
</hover-container> </hover-container>
@@ -7,7 +7,10 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useFullscreen } from '@vueuse/core'; import { useFullscreen } from '@vueuse/core';
import { useThemeStore } from '@/store';
const { isFullscreen, toggle } = useFullscreen(); const { isFullscreen, toggle } = useFullscreen();
const theme = useThemeStore();
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -2,7 +2,7 @@
<hover-container <hover-container
tooltip-content="github" tooltip-content="github"
class="w-40px h-full" class="w-40px h-full"
content-class="hover:text-primary" :inverted="theme.header.inverted"
@click="handleClickLink" @click="handleClickLink"
> >
<icon-mdi-github class="text-20px" /> <icon-mdi-github class="text-20px" />
@@ -10,8 +10,12 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { useThemeStore } from '@/store';
const theme = useThemeStore();
function handleClickLink() { function handleClickLink() {
window.open('https://github.com/honghuangdc/soybean-admin', '_blank'); window.open('https://github.com/honghuangdc/soybean-admin', '_blank');
} }
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -17,8 +17,9 @@
:is="breadcrumb.icon" :is="breadcrumb.icon"
v-if="theme.header.crumb.showIcon" v-if="theme.header.crumb.showIcon"
class="inline-block align-text-bottom mr-4px text-16px" class="inline-block align-text-bottom mr-4px text-16px"
:class="{ 'text-#BBBBBB': theme.header.inverted }"
/> />
<span>{{ breadcrumb.label }}</span> <span :class="{ 'text-#BBBBBB': theme.header.inverted }">{{ breadcrumb.label }}</span>
</template> </template>
</n-breadcrumb-item> </n-breadcrumb-item>
</template> </template>
@@ -46,4 +47,5 @@ function dropdownSelect(key: string) {
routerPush({ name: key }); routerPush({ name: key });
} }
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -21,11 +21,12 @@ const theme = useThemeStore();
const { routerPush } = useRouterPush(); const { routerPush } = useRouterPush();
const menus = computed(() => routeStore.menus as GlobalMenuOption[]); const menus = computed(() => routeStore.menus as GlobalMenuOption[]);
const activeKey = computed(() => route.name as string); const activeKey = computed(() => (route.meta?.activeMenu ? route.meta.activeMenu : route.name) as string);
function handleUpdateMenu(_key: string, item: MenuOption) { function handleUpdateMenu(_key: string, item: MenuOption) {
const menuItem = item as GlobalMenuOption; const menuItem = item as GlobalMenuOption;
routerPush(menuItem.routePath); routerPush(menuItem.routePath);
} }
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -1,13 +1,15 @@
<template> <template>
<hover-container class="w-40px h-full" content-class="hover:text-primary" @click="app.toggleSiderCollapse"> <hover-container class="w-40px h-full" :inverted="theme.header.inverted" @click="app.toggleSiderCollapse">
<icon-line-md-menu-unfold-left v-if="app.siderCollapse" class="text-16px" /> <icon-line-md-menu-unfold-left v-if="app.siderCollapse" class="text-16px" />
<icon-line-md-menu-fold-left v-else class="text-16px" /> <icon-line-md-menu-fold-left v-else class="text-16px" />
</hover-container> </hover-container>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { useAppStore } from '@/store'; import { useAppStore, useThemeStore } from '@/store';
const app = useAppStore(); const app = useAppStore();
const theme = useThemeStore();
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -1,5 +1,5 @@
<template> <template>
<hover-container class="w-40px" content-class="hover:text-primary" 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" class="wh-full" @update:dark="theme.setDarkMode" />
</hover-container> </hover-container>
</template> </template>
@@ -9,4 +9,5 @@ import { useThemeStore } from '@/store';
const theme = useThemeStore(); const theme = useThemeStore();
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -1,6 +1,6 @@
<template> <template>
<n-dropdown :options="options" @select="handleDropdown"> <n-dropdown :options="options" @select="handleDropdown">
<hover-container class="px-12px" content-class="hover:text-primary"> <hover-container class="px-12px" :inverted="theme.header.inverted">
<icon-custom-avatar class="text-32px" /> <icon-custom-avatar class="text-32px" />
<span class="pl-8px text-16px font-medium">{{ auth.userInfo.userName }}</span> <span class="pl-8px text-16px font-medium">{{ auth.userInfo.userName }}</span>
</hover-container> </hover-container>
@@ -8,12 +8,13 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { useAuthStore } from '@/store'; import { useAuthStore, useThemeStore } from '@/store';
import { iconifyRender } from '@/utils'; import { iconifyRender } from '@/utils';
type DropdownKey = 'user-center' | 'logout'; type DropdownKey = 'user-center' | 'logout';
const auth = useAuthStore(); const auth = useAuthStore();
const theme = useThemeStore();
const options = [ const options = [
{ {
@@ -47,4 +48,5 @@ function handleDropdown(optionKey: string) {
} }
} }
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -45,6 +45,7 @@ defineProps<Props>();
const theme = useThemeStore(); const theme = useThemeStore();
</script> </script>
<style scoped> <style scoped>
.global-header { .global-header {
box-shadow: 0 1px 2px rgb(0 21 41 / 8%); box-shadow: 0 1px 2px rgb(0 21 41 / 8%);

View File

@@ -8,7 +8,6 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { RouterLink } from 'vue-router';
import { routePath } from '@/router'; import { routePath } from '@/router';
import { useAppInfo } from '@/composables'; import { useAppInfo } from '@/composables';
@@ -22,4 +21,5 @@ defineProps<Props>();
const { title } = useAppInfo(); const { title } = useAppInfo();
const routeHomePath = routePath('root'); const routeHomePath = routePath('root');
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -17,6 +17,7 @@
</template> </template>
<script lang="ts" setup></script> <script lang="ts" setup></script>
<style lang="scss" scoped> <style lang="scss" scoped>
.icon { .icon {
box-shadow: inset 0 -2px #cdcde6, inset 0 0 1px 1px #fff, 0 1px 2px 1px #1e235a66; box-shadow: inset 0 -2px #cdcde6, inset 0 0 1px 1px #fff, 0 1px 2px 1px #1e235a66;

View File

@@ -134,4 +134,5 @@ onKeyStroke('Enter', handleEnter);
onKeyStroke('ArrowUp', handleUp); onKeyStroke('ArrowUp', handleUp);
onKeyStroke('ArrowDown', handleDown); onKeyStroke('ArrowDown', handleDown);
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>

View File

@@ -59,4 +59,5 @@ function handleTo() {
emit('enter'); emit('enter');
} }
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>

View File

@@ -3,7 +3,7 @@
<hover-container <hover-container
class="w-40px h-full" class="w-40px h-full"
tooltip-content="搜索" tooltip-content="搜索"
content-class="hover:text-primary" :inverted="theme.header.inverted"
@click="handleSearch" @click="handleSearch"
> >
<icon-uil-search class="text-20px" /> <icon-uil-search class="text-20px" />
@@ -13,13 +13,16 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { useThemeStore } from '@/store';
import { useBoolean } from '@/hooks'; import { useBoolean } from '@/hooks';
import { SearchModal } from './components'; import { SearchModal } from './components';
const { bool: show, toggle } = useBoolean(); const { bool: show, toggle } = useBoolean();
const theme = useThemeStore();
function handleSearch() { function handleSearch() {
toggle(); toggle();
} }
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>

View File

@@ -10,4 +10,5 @@ import { useAppStore } from '@/store';
const app = useAppStore(); const app = useAppStore();
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -42,4 +42,5 @@ const { bool: isHover, setTrue, setFalse } = useBoolean();
const isActive = computed(() => props.routeName === props.activeRouteName); const isActive = computed(() => props.routeName === props.activeRouteName);
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -53,7 +53,7 @@ const { title } = useAppInfo();
const showDrawer = computed(() => (props.visible && props.menus.length) || app.mixSiderFixed); const showDrawer = computed(() => (props.visible && props.menus.length) || app.mixSiderFixed);
const activeKey = computed(() => route.name as string); const activeKey = computed(() => (route.meta?.activeMenu ? route.meta.activeMenu : route.name) as string);
const expandedKeys = ref<string[]>([]); const expandedKeys = ref<string[]>([]);
function handleUpdateMenu(_key: string, item: MenuOption) { function handleUpdateMenu(_key: string, item: MenuOption) {
@@ -73,6 +73,7 @@ watch(
{ immediate: true } { immediate: true }
); );
</script> </script>
<style scoped> <style scoped>
.drawer-shadow { .drawer-shadow {
box-shadow: 2px 0 8px 0 rgb(29 35 41 / 5%); box-shadow: 2px 0 8px 0 rgb(29 35 41 / 5%);

View File

@@ -58,7 +58,7 @@ const firstDegreeMenus = computed(() =>
function getActiveParentRouteName() { function getActiveParentRouteName() {
firstDegreeMenus.value.some(item => { firstDegreeMenus.value.some(item => {
const routeName = route.name as string; const routeName = (route.meta?.activeMenu ? route.meta.activeMenu : route.name) as string;
const flag = routeName?.includes(item.routeName); const flag = routeName?.includes(item.routeName);
if (flag) { if (flag) {
setActiveParentRouteName(item.routeName); setActiveParentRouteName(item.routeName);
@@ -101,4 +101,5 @@ watch(
{ immediate: true } { immediate: true }
); );
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -29,7 +29,7 @@ const theme = useThemeStore();
const routeStore = useRouteStore(); const routeStore = useRouteStore();
const { routerPush } = useRouterPush(); const { routerPush } = useRouterPush();
const activeKey = computed(() => route.name as string); const activeKey = computed(() => (route.meta?.activeMenu ? route.meta.activeMenu : route.name) as string);
const expandedKeys = ref<string[]>([]); const expandedKeys = ref<string[]>([]);
function handleUpdateMenu(_key: string, item: MenuOption) { function handleUpdateMenu(_key: string, item: MenuOption) {
@@ -49,4 +49,5 @@ watch(
{ immediate: true } { immediate: true }
); );
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -17,4 +17,5 @@ const theme = useThemeStore();
const isHorizontalMix = computed(() => theme.layout.mode === 'horizontal-mix'); const isHorizontalMix = computed(() => theme.layout.mode === 'horizontal-mix');
const showTitle = computed(() => !app.siderCollapse && theme.layout.mode !== 'vertical-mix'); const showTitle = computed(() => !app.siderCollapse && theme.layout.mode !== 'vertical-mix');
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -12,6 +12,7 @@ const theme = useThemeStore();
const isVerticalMix = computed(() => theme.layout.mode === 'vertical-mix'); const isVerticalMix = computed(() => theme.layout.mode === 'vertical-mix');
</script> </script>
<style scoped> <style scoped>
.global-sider { .global-sider {
box-shadow: 2px 0 8px 0 rgb(29 35 41 / 5%); box-shadow: 2px 0 8px 0 rgb(29 35 41 / 5%);

View File

@@ -19,4 +19,5 @@ function handleRefresh() {
}, 1000); }, 1000);
} }
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -69,7 +69,7 @@ const options = computed<Option[]>(() => [
{ {
label: '关闭', label: '关闭',
key: 'close-current', key: 'close-current',
disabled: props.currentPath === tab.homeTab.path, disabled: props.currentPath === tab.homeTab.fullPath,
icon: iconifyRender('ant-design:close-outlined') icon: iconifyRender('ant-design:close-outlined')
}, },
{ {
@@ -131,4 +131,5 @@ function handleDropdown(optionKey: string) {
hide(); hide();
} }
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -3,15 +3,15 @@
<component <component
:is="activeComponent" :is="activeComponent"
v-for="(item, index) in tab.tabs" v-for="(item, index) in tab.tabs"
:key="item.path" :key="item.fullPath"
:is-active="tab.activeTab === item.path" :is-active="tab.activeTab === item.fullPath"
:primary-color="theme.themeColor" :primary-color="theme.themeColor"
:closable="item.path !== tab.homeTab.path" :closable="item.name !== tab.homeTab.name"
:dark-mode="theme.darkMode" :dark-mode="theme.darkMode"
:class="{ '!mr-0': isChromeMode && index === tab.tabs.length - 1, 'mr-10px': !isChromeMode }" :class="{ '!mr-0': isChromeMode && index === tab.tabs.length - 1, 'mr-10px': !isChromeMode }"
@click="tab.handleClickTab(item.path)" @click="tab.handleClickTab(item.fullPath)"
@close="tab.removeTab(item.path)" @close="tab.removeTab(item.fullPath)"
@contextmenu="handleContextMenu($event, item.path)" @contextmenu="handleContextMenu($event, item.fullPath)"
> >
<Icon v-if="item.meta.icon" :icon="item.meta.icon" class="inline-block align-text-bottom mr-4px text-16px" /> <Icon v-if="item.meta.icon" :icon="item.meta.icon" class="inline-block align-text-bottom mr-4px text-16px" />
{{ item.meta.title }} {{ item.meta.title }}
@@ -77,11 +77,11 @@ function setDropdown(x: number, y: number, currentPath: string) {
} }
/** 点击右键菜单 */ /** 点击右键菜单 */
async function handleContextMenu(e: MouseEvent, path: string) { async function handleContextMenu(e: MouseEvent, fullPath: string) {
e.preventDefault(); e.preventDefault();
const { clientX, clientY } = e; const { clientX, clientY } = e;
hideDropdown(); hideDropdown();
setDropdown(clientX, clientY, path); setDropdown(clientX, clientY, fullPath);
await nextTick(); await nextTick();
showDropdown(); showDropdown();
} }
@@ -101,4 +101,5 @@ useEventListener(window, 'beforeunload', () => {
setTabRoutes(tab.tabs); setTabRoutes(tab.tabs);
}); });
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -45,16 +45,17 @@ function init() {
} }
watch( watch(
() => route.path, () => route.fullPath,
() => { () => {
tab.addTab(route); tab.addTab(route);
tab.setActiveTab(route.path); tab.setActiveTab(route.fullPath);
} }
); );
// 初始化 // 初始化
init(); init();
</script> </script>
<style scoped> <style scoped>
.global-tab { .global-tab {
box-shadow: 0 1px 2px rgb(0 21 41 / 8%); box-shadow: 0 1px 2px rgb(0 21 41 / 8%);

View File

@@ -1,19 +1,32 @@
<template> <template>
<n-divider title-placement="center">深色主题</n-divider> <n-divider title-placement="center">主题模式</n-divider>
<div class="flex-center"> <n-space vertical size="large">
<n-switch :value="theme.darkMode" @update:value="theme.setDarkMode"> <setting-menu label="深色主题">
<template #checked> <n-switch :value="theme.darkMode" @update:value="theme.setDarkMode">
<icon-mdi-white-balance-sunny class="text-14px text-primary" /> <template #checked>
</template> <icon-mdi-white-balance-sunny class="text-14px text-primary" />
<template #unchecked> </template>
<icon-mdi-moon-waning-crescent class="text-14px text-primary" /> <template #unchecked>
</template> <icon-mdi-moon-waning-crescent class="text-14px text-primary" />
</n-switch> </template>
</div> </n-switch>
</setting-menu>
<setting-menu label="跟随系统">
<n-switch :value="theme.followSystemTheme" @update:value="theme.setFollowSystemTheme">
<template #checked>
<icon-ic-baseline-do-not-disturb class="text-14px text-primary" />
</template>
<template #unchecked>
<icon-ic-round-hdr-auto class="text-14px text-primary" />
</template>
</n-switch>
</setting-menu>
</n-space>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { useThemeStore } from '@/store'; import { useThemeStore } from '@/store';
import SettingMenu from '../SettingMenu/index.vue';
const theme = useThemeStore(); const theme = useThemeStore();
</script> </script>

View File

@@ -15,4 +15,5 @@ import { useAppStore } from '@/store';
const app = useAppStore(); const app = useAppStore();
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -17,7 +17,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'; import { computed } from 'vue';
import type { FollowerPlacement } from 'vueuc'; import type { PopoverPlacement } from 'naive-ui';
import type { EnumThemeLayoutMode } from '@/enum'; import type { EnumThemeLayoutMode } from '@/enum';
interface Props { interface Props {
@@ -34,7 +34,7 @@ const props = defineProps<Props>();
type LayoutConfig = Record< type LayoutConfig = Record<
EnumType.ThemeLayoutMode, EnumType.ThemeLayoutMode,
{ {
placement: FollowerPlacement; placement: PopoverPlacement;
menuClass: string; menuClass: string;
mainClass: string; mainClass: string;
} }
@@ -65,6 +65,7 @@ const layoutConfig: LayoutConfig = {
const activeConfig = computed(() => layoutConfig[props.mode]); const activeConfig = computed(() => layoutConfig[props.mode]);
</script> </script>
<style scoped> <style scoped>
.layout-checkbox__shadow { .layout-checkbox__shadow {
box-shadow: 0 1px 2.5px rgba(0, 0, 0, 0.18); box-shadow: 0 1px 2.5px rgba(0, 0, 0, 0.18);

View File

@@ -18,4 +18,5 @@ import { LayoutCheckbox } from './components';
const theme = useThemeStore(); const theme = useThemeStore();
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -70,4 +70,5 @@ import SettingMenu from '../SettingMenu/index.vue';
const theme = useThemeStore(); const theme = useThemeStore();
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -40,4 +40,5 @@ import SettingMenu from '../SettingMenu/index.vue';
const theme = useThemeStore(); const theme = useThemeStore();
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -13,4 +13,5 @@ interface Props {
defineProps<Props>(); defineProps<Props>();
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -23,4 +23,5 @@ const props = withDefaults(defineProps<Props>(), {
const whiteColors = ['#ffffff', '#fff', 'rgb(255,255,255)']; const whiteColors = ['#ffffff', '#fff', 'rgb(255,255,255)'];
const isWhite = computed(() => whiteColors.includes(props.color)); const isWhite = computed(() => whiteColors.includes(props.color));
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -45,4 +45,5 @@ function handleClose() {
emit('close'); emit('close');
} }
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -26,4 +26,5 @@ const { bool: visible, setTrue: openModal, setFalse: closeModal } = useBoolean()
const isInOther = computed(() => isInTraditionColors(theme.themeColor)); const isInOther = computed(() => isInTraditionColors(theme.themeColor));
const otherColorBtnType = computed(() => (isInOther.value ? 'primary' : 'default')); const otherColorBtnType = computed(() => (isInOther.value ? 'primary' : 'default'));
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -56,4 +56,5 @@ onUnmounted(() => {
stopHandle(); stopHandle();
}); });
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -18,4 +18,5 @@ import { DrawerButton, DarkMode, LayoutMode, ThemeColorSelect, PageFunc, PageVie
const app = useAppStore(); const app = useAppStore();
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -1,4 +1,4 @@
import type { Router, RouteLocationNormalized, NavigationGuardNext } from 'vue-router'; import type { RouteLocationNormalized, NavigationGuardNext } from 'vue-router';
import { routeName } from '@/router'; import { routeName } from '@/router';
import { useRouteStore } from '@/store'; import { useRouteStore } from '@/store';
import { getToken } from '@/utils'; import { getToken } from '@/utils';
@@ -9,8 +9,7 @@ import { getToken } from '@/utils';
export async function createDynamicRouteGuard( export async function createDynamicRouteGuard(
to: RouteLocationNormalized, to: RouteLocationNormalized,
_from: RouteLocationNormalized, _from: RouteLocationNormalized,
next: NavigationGuardNext, next: NavigationGuardNext
router: Router
) { ) {
const route = useRouteStore(); const route = useRouteStore();
const isLogin = Boolean(getToken()); const isLogin = Boolean(getToken());
@@ -28,11 +27,14 @@ export async function createDynamicRouteGuard(
return false; return false;
} }
await route.initAuthRoute(router); await route.initAuthRoute();
if (to.name === routeName('not-found-page')) { if (to.name === routeName('not-found-page')) {
// 动态路由没有加载导致被not-found-page路由捕获等待权限路由加载好了回到之前的路由 // 动态路由没有加载导致被not-found-page路由捕获等待权限路由加载好了回到之前的路由
next({ path: to.fullPath, replace: true, query: to.query }); // 若路由是从根路由重定向过来的,重新回到根路由
const ROOT_ROUTE_NAME: AuthRoute.RouteKey = 'root';
const path = to.redirectedFrom?.name === ROOT_ROUTE_NAME ? '/' : to.fullPath;
next({ path, replace: true, query: to.query, hash: to.hash });
return false; return false;
} }
} }

View File

@@ -11,7 +11,7 @@ export function createRouterGuard(router: Router) {
// 开始 loadingBar // 开始 loadingBar
window.$loadingBar?.start(); window.$loadingBar?.start();
// 页面跳转权限处理 // 页面跳转权限处理
await createPermissionGuard(to, from, next, router); await createPermissionGuard(to, from, next);
}); });
router.afterEach(to => { router.afterEach(to => {
// 设置document title // 设置document title

View File

@@ -1,4 +1,4 @@
import type { Router, RouteLocationNormalized, NavigationGuardNext } from 'vue-router'; import type { RouteLocationNormalized, NavigationGuardNext } from 'vue-router';
import { routeName } from '@/router'; import { routeName } from '@/router';
import { useAuthStore } from '@/store'; import { useAuthStore } from '@/store';
import { exeStrategyActions, getToken } from '@/utils'; import { exeStrategyActions, getToken } from '@/utils';
@@ -8,11 +8,10 @@ import { createDynamicRouteGuard } from './dynamic';
export async function createPermissionGuard( export async function createPermissionGuard(
to: RouteLocationNormalized, to: RouteLocationNormalized,
from: RouteLocationNormalized, from: RouteLocationNormalized,
next: NavigationGuardNext, next: NavigationGuardNext
router: Router
) { ) {
// 动态路由 // 动态路由
const permission = await createDynamicRouteGuard(to, from, next, router); const permission = await createDynamicRouteGuard(to, from, next);
if (!permission) return; if (!permission) return;
// 外链路由, 从新标签打开,返回上一个路由 // 外链路由, 从新标签打开,返回上一个路由

View File

@@ -6,10 +6,13 @@ export const scrollBehavior: RouterScrollBehavior = (to, from) => {
const tab = useTabStore(); const tab = useTabStore();
if (to.hash) { if (to.hash) {
resolve({ const el = document.querySelector(to.hash);
el: to.hash, if (el) {
behavior: 'smooth' resolve({
}); el,
behavior: 'smooth'
});
}
} }
const { left, top } = tab.getTabScrollPosition(to.path); const { left, top } = tab.getTabScrollPosition(to.path);

View File

@@ -8,7 +8,7 @@ const about: AuthRoute.Route = {
singleLayout: 'basic', singleLayout: 'basic',
permissions: ['super', 'admin', 'user'], permissions: ['super', 'admin', 'user'],
icon: 'fluent:book-information-24-regular', icon: 'fluent:book-information-24-regular',
order: 8 order: 9
} }
}; };

View File

@@ -37,7 +37,7 @@ const exception: AuthRoute.Route = {
meta: { meta: {
title: '异常页', title: '异常页',
icon: 'ant-design:exception-outlined', icon: 'ant-design:exception-outlined',
order: 6 order: 7
} }
}; };

View File

@@ -0,0 +1,49 @@
const functionRoute: AuthRoute.Route = {
name: 'function',
path: '/function',
component: 'basic',
children: [
{
name: 'function_tab',
path: '/function/tab',
component: 'self',
meta: {
title: '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: '功能',
icon: 'ri:function-line',
order: 6
}
};
export default functionRoute;

View File

@@ -49,7 +49,7 @@ const multiMenu: AuthRoute.Route = {
meta: { meta: {
title: '多级菜单', title: '多级菜单',
icon: 'carbon:menu', icon: 'carbon:menu',
order: 6 order: 8
} }
}; };

View File

@@ -3,6 +3,47 @@ const plugin: AuthRoute.Route = {
path: '/plugin', path: '/plugin',
component: 'basic', component: 'basic',
children: [ children: [
{
name: 'plugin_charts',
path: '/plugin/charts',
component: 'multi',
children: [
{
name: 'plugin_charts_echarts',
path: '/plugin/charts/echarts',
component: 'self',
meta: {
title: 'ECharts',
requiresAuth: true,
icon: 'simple-icons:apacheecharts'
}
},
{
name: 'plugin_charts_d3',
path: '/plugin/charts/d3',
component: 'self',
meta: {
title: 'D3',
requiresAuth: true,
icon: 'simple-icons:d3dotjs'
}
},
{
name: 'plugin_charts_antv',
path: '/plugin/charts/antv',
component: 'self',
meta: {
title: 'AntV',
requiresAuth: true,
icon: 'ant-design:bar-chart-outlined'
}
}
],
meta: {
title: '图表',
icon: 'material-symbols:bar-chart-rounded'
}
},
{ {
name: 'plugin_map', name: 'plugin_map',
path: '/plugin/map', path: '/plugin/map',

View File

@@ -1,15 +1,18 @@
import { getLoginModuleRegExp } from '@/utils'; import { getLoginModuleRegExp } from '@/utils';
/** 根路由: / */
export const ROOT_ROUTE: AuthRoute.Route = {
name: 'root',
path: '/',
redirect: import.meta.env.VITE_ROUTE_HOME_PATH,
meta: {
title: 'Root'
}
};
/** 固定的路由 */ /** 固定的路由 */
export const constantRoutes: AuthRoute.Route[] = [ export const constantRoutes: AuthRoute.Route[] = [
{ ROOT_ROUTE,
name: 'root',
path: '/',
redirect: import.meta.env.VITE_ROUTE_HOME_PATH,
meta: {
title: 'Root'
}
},
{ {
name: 'login', name: 'login',
path: '/login', path: '/login',

View File

@@ -1,5 +1,5 @@
import { createRequest } from './request';
import { getEnvConfig } from '~/.env-config'; import { getEnvConfig } from '~/.env-config';
import { createRequest } from './request';
const envConfig = getEnvConfig(import.meta.env); const envConfig = getEnvConfig(import.meta.env);
const isHttpProxy = import.meta.env.VITE_HTTP_PROXY === 'true'; const isHttpProxy = import.meta.env.VITE_HTTP_PROXY === 'true';

View File

@@ -1,5 +1,6 @@
{ {
"darkMode": false, "darkMode": false,
"followSystemTheme": true,
"layout": { "layout": {
"minWidth": 900, "minWidth": 900,
"mode": "vertical", "mode": "vertical",

View File

@@ -30,6 +30,7 @@ const themeColorList = [
const defaultThemeSetting: Theme.Setting = { const defaultThemeSetting: Theme.Setting = {
darkMode: false, darkMode: false,
followSystemTheme: true,
layout: { layout: {
minWidth: 900, minWidth: 900,
mode: 'vertical', mode: 'vertical',

View File

@@ -1,6 +1,6 @@
import { unref, nextTick } from 'vue'; import { unref } from 'vue';
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { router as globalRouter } from '@/router'; import { router } from '@/router';
import { useRouterPush } from '@/composables'; import { useRouterPush } from '@/composables';
import { fetchLogin, fetchUserInfo } from '@/service'; import { fetchLogin, fetchUserInfo } from '@/service';
import { getUserInfo, getToken, setUserInfo, setToken, setRefreshToken, clearAuthStorage } from '@/utils'; import { getUserInfo, getToken, setUserInfo, setToken, setRefreshToken, clearAuthStorage } from '@/utils';
@@ -34,26 +34,50 @@ export const useAuthStore = defineStore('auth-store', {
const { toLogin } = useRouterPush(false); const { toLogin } = useRouterPush(false);
const { resetTabStore } = useTabStore(); const { resetTabStore } = useTabStore();
const { resetRouteStore } = useRouteStore(); const { resetRouteStore } = useRouteStore();
const route = unref(globalRouter.currentRoute); const route = unref(router.currentRoute);
clearAuthStorage(); clearAuthStorage();
this.$reset(); this.$reset();
resetTabStore();
resetRouteStore();
if (route.meta.requiresAuth) { if (route.meta.requiresAuth) {
toLogin(); toLogin();
} }
},
/**
* 处理登录后成功或失败的逻辑
* @param backendToken - 返回的token
*/
async handleActionAfterLogin(backendToken: ApiAuth.Token) {
const { toLoginRedirect } = useRouterPush(false);
nextTick(() => { const loginSuccess = await this.loginByToken(backendToken);
resetTabStore();
resetRouteStore(); if (loginSuccess) {
}); // 跳转登录后的地址
toLoginRedirect();
// 登录成功弹出欢迎提示
window.$notification?.success({
title: '登录成功!',
content: `欢迎回来,${this.userInfo.userName}!`,
duration: 3000
});
return;
}
// 不成功则重置状态
this.resetAuthStore();
}, },
/** /**
* 根据token进行登录 * 根据token进行登录
* @param backendToken - 返回的token * @param backendToken - 返回的token
*/ */
async loginByToken(backendToken: ApiAuth.Token) { async loginByToken(backendToken: ApiAuth.Token) {
const { toLoginRedirect } = useRouterPush(false); let successFlag = false;
// 先把token存储到缓存中(后面接口的请求头需要token) // 先把token存储到缓存中(后面接口的请求头需要token)
const { token, refreshToken } = backendToken; const { token, refreshToken } = backendToken;
@@ -67,21 +91,13 @@ export const useAuthStore = defineStore('auth-store', {
setUserInfo(data); setUserInfo(data);
// 更新状态 // 更新状态
Object.assign(this, { userInfo: data, token }); this.userInfo = data;
this.token = token;
// 跳转登录后的地址 successFlag = true;
toLoginRedirect();
// 登录成功弹出欢迎提示
window.$notification?.success({
title: '登录成功!',
content: `欢迎回来,${data.userName}!`,
duration: 3000
});
} else {
// 不成功则重置状态
this.resetAuthStore();
} }
return successFlag;
}, },
/** /**
* 登录 * 登录
@@ -92,12 +108,38 @@ export const useAuthStore = defineStore('auth-store', {
this.loginLoading = true; this.loginLoading = true;
const { data } = await fetchLogin(userName, password); const { data } = await fetchLogin(userName, password);
if (data) { if (data) {
await this.loginByToken(data); await this.handleActionAfterLogin(data);
} }
this.loginLoading = false; this.loginLoading = false;
}, },
updateUserRole(userRole: Auth.RoleType) { /**
this.userInfo.userRole = userRole; * 更换用户权限(切换账号)
* @param userRole
*/
async updateUserRole(userRole: Auth.RoleType) {
const { resetRouteStore, initAuthRoute } = useRouteStore();
const accounts: Record<Auth.RoleType, { userName: string; password: string }> = {
super: {
userName: 'Super',
password: 'super123'
},
admin: {
userName: 'Admin',
password: 'admin123'
},
user: {
userName: 'User01',
password: 'user01123'
}
};
const { userName, password } = accounts[userRole];
const { data } = await fetchLogin(userName, password);
if (data) {
await this.loginByToken(data);
resetRouteStore();
initAuthRoute();
}
} }
} }
}); });

View File

@@ -1,15 +1,17 @@
import type { Router } from 'vue-router';
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { routes as staticRoutes } from '@/router'; import { router, ROOT_ROUTE, constantRoutes, routes as staticRoutes } from '@/router';
import { fetchUserRoutes } from '@/service'; import { fetchUserRoutes } from '@/service';
import { import {
getUserInfo, getUserInfo,
transformAuthRouteToMenu, transformAuthRouteToMenu,
transformAuthRoutesToVueRoutes, transformAuthRoutesToVueRoutes,
transformAuthRouteToVueRoute,
transformAuthRoutesToSearchMenus, transformAuthRoutesToSearchMenus,
getCacheRoutes, getCacheRoutes,
filterAuthRoutesByUserPermission, filterAuthRoutesByUserPermission,
transformRoutePathToRouteName transformRoutePathToRouteName,
transformRouteNameToRoutePath,
getConstantRouteNames
} from '@/utils'; } from '@/utils';
import { useAuthStore } from '../auth'; import { useAuthStore } from '../auth';
import { useTabStore } from '../tab'; import { useTabStore } from '../tab';
@@ -43,15 +45,27 @@ export const useRouteStore = defineStore('route-store', {
cacheRoutes: [] cacheRoutes: []
}), }),
actions: { actions: {
/** 重置路由的store */
resetRouteStore() { resetRouteStore() {
this.resetRoutes();
this.$reset(); this.$reset();
}, },
/** 重置路由数据,保留固定路由 */
resetRoutes() {
const routes = router.getRoutes();
const constantRouteNames = getConstantRouteNames(constantRoutes);
routes.forEach(route => {
const name: AuthRoute.RouteKey = (route.name || 'root') as AuthRoute.RouteKey;
if (!constantRouteNames.includes(name)) {
router.removeRoute(name);
}
});
},
/** /**
* 处理权限路由 * 处理权限路由
* @param routes - 权限路由 * @param routes - 权限路由
* @param router - 路由实例
*/ */
handleAuthRoutes(routes: AuthRoute.Route[], router: Router) { handleAuthRoutes(routes: AuthRoute.Route[]) {
this.menus = transformAuthRouteToMenu(routes); this.menus = transformAuthRouteToMenu(routes);
this.searchMenus = transformAuthRoutesToSearchMenus(routes); this.searchMenus = transformAuthRoutesToSearchMenus(routes);
@@ -63,32 +77,35 @@ export const useRouteStore = defineStore('route-store', {
this.cacheRoutes = getCacheRoutes(vueRoutes); this.cacheRoutes = getCacheRoutes(vueRoutes);
}, },
/** /** 动态路由模式下:更新根路由的重定向 */
* 初始化动态路由 handleUpdateRootRedirect(routeKey: AuthRoute.RouteKey) {
* @param router - 路由实例 if (routeKey === 'root' || routeKey === 'not-found-page') {
*/ throw Error('routeKey的值不能为root或者not-found-page');
async initDynamicRoute(router: Router) { }
const rootRoute: AuthRoute.Route = { ...ROOT_ROUTE, redirect: transformRouteNameToRoutePath(routeKey) };
const rootRouteName: AuthRoute.RouteKey = 'root';
router.removeRoute(rootRouteName);
const rootVueRoute = transformAuthRouteToVueRoute(rootRoute)[0];
router.addRoute(rootVueRoute);
},
/** 初始化动态路由 */
async initDynamicRoute() {
const { userId } = getUserInfo(); const { userId } = getUserInfo();
const { data } = await fetchUserRoutes(userId); const { data } = await fetchUserRoutes(userId);
if (data) { if (data) {
this.routeHomeName = data.home; this.routeHomeName = data.home;
this.handleAuthRoutes(data.routes, router); this.handleUpdateRootRedirect(data.home);
this.handleAuthRoutes(data.routes);
} }
}, },
/** /** 初始化静态路由 */
* 初始化静态路由 async initStaticRoute() {
* @param router - 路由实例
*/
async initStaticRoute(router: Router) {
const auth = useAuthStore(); const auth = useAuthStore();
const routes = filterAuthRoutesByUserPermission(staticRoutes, auth.userInfo.userRole); const routes = filterAuthRoutesByUserPermission(staticRoutes, auth.userInfo.userRole);
this.handleAuthRoutes(routes, router); this.handleAuthRoutes(routes);
}, },
/** /** 初始化权限路由 */
* 初始化权限路由 async initAuthRoute() {
* @param router - 路由实例
*/
async initAuthRoute(router: Router) {
const { initHomeTab } = useTabStore(); const { initHomeTab } = useTabStore();
const { userId } = getUserInfo(); const { userId } = getUserInfo();
@@ -96,9 +113,9 @@ export const useRouteStore = defineStore('route-store', {
const isDynamicRoute = this.authRouteMode === 'dynamic'; const isDynamicRoute = this.authRouteMode === 'dynamic';
if (isDynamicRoute) { if (isDynamicRoute) {
await this.initDynamicRoute(router); await this.initDynamicRoute();
} else { } else {
await this.initStaticRoute(router); await this.initStaticRoute();
} }
initHomeTab(this.routeHomeName, router); initHomeTab(this.routeHomeName, router);

View File

@@ -1,13 +1,15 @@
import type { RouteRecordNormalized, RouteLocationNormalizedLoaded } from 'vue-router'; import type { RouteRecordNormalized, RouteLocationNormalizedLoaded } from 'vue-router';
/** /**
* 根据vue路由获取tab路由 * 根据vue路由获取tab路由
* @param route * @param route
*/ */
export function getTabRouteByVueRoute(route: RouteRecordNormalized | RouteLocationNormalizedLoaded) { export function getTabRouteByVueRoute(route: RouteRecordNormalized | RouteLocationNormalizedLoaded) {
const fullPath = hasFullPath(route) ? route.fullPath : route.path;
const tabRoute: GlobalTabRoute = { const tabRoute: GlobalTabRoute = {
name: route.name, name: route.name,
path: route.path, fullPath,
meta: route.meta, meta: route.meta,
scrollPosition: { scrollPosition: {
left: 0, left: 0,
@@ -20,17 +22,36 @@ export function getTabRouteByVueRoute(route: RouteRecordNormalized | RouteLocati
/** /**
* 获取该页签在多页签数据中的索引 * 获取该页签在多页签数据中的索引
* @param tabs - 多页签数据 * @param tabs - 多页签数据
* @param path - 该页签的路径 * @param fullPath - 该页签的路径
*/ */
export function getIndexInTabRoutes(tabs: GlobalTabRoute[], path: string) { export function getIndexInTabRoutes(tabs: GlobalTabRoute[], fullPath: string) {
return tabs.findIndex(tab => tab.path === path); return tabs.findIndex(tab => tab.fullPath === fullPath);
} }
/** /**
* 判断该页签是否在多页签数据中 * 判断该页签是否在多页签数据中
* @param tabs - 多页签数据 * @param tabs - 多页签数据
* @param path - 该页签的路径 * @param fullPath - 该页签的路径
*/ */
export function isInTabRoutes(tabs: GlobalTabRoute[], path: string) { export function isInTabRoutes(tabs: GlobalTabRoute[], fullPath: string) {
return getIndexInTabRoutes(tabs, path) > -1; return getIndexInTabRoutes(tabs, fullPath) > -1;
}
/**
* 根据路由名称获取该页签在多页签数据中的索引
* @param tabs - 多页签数据
* @param routeName - 路由名称
*/
export function getIndexInTabRoutesByRouteName(tabs: GlobalTabRoute[], routeName: string) {
return tabs.findIndex(tab => tab.name === routeName);
}
/**
* 判断路由是否有fullPath属性
* @param route 路由
*/
function hasFullPath(
route: RouteRecordNormalized | RouteLocationNormalizedLoaded
): route is RouteLocationNormalizedLoaded {
return Boolean((route as RouteLocationNormalizedLoaded).fullPath);
} }

View File

@@ -3,14 +3,14 @@ import { defineStore } from 'pinia';
import { useRouterPush } from '@/composables'; import { useRouterPush } from '@/composables';
import { getTabRoutes, clearTabRoutes } from '@/utils'; import { getTabRoutes, clearTabRoutes } from '@/utils';
import { useThemeStore } from '../theme'; import { useThemeStore } from '../theme';
import { getTabRouteByVueRoute, isInTabRoutes, getIndexInTabRoutes } from './helpers'; import { getTabRouteByVueRoute, isInTabRoutes, getIndexInTabRoutes, getIndexInTabRoutesByRouteName } from './helpers';
interface TabState { interface TabState {
/** 多页签数据 */ /** 多页签数据 */
tabs: GlobalTabRoute[]; tabs: GlobalTabRoute[];
/** 多页签首页 */ /** 多页签首页 */
homeTab: GlobalTabRoute; homeTab: GlobalTabRoute;
/** 当前激活状态的页签(路由path) */ /** 当前激活状态的页签(路由fullPath) */
activeTab: string; activeTab: string;
} }
@@ -19,7 +19,7 @@ export const useTabStore = defineStore('tab-store', {
tabs: [], tabs: [],
homeTab: { homeTab: {
name: 'root', name: 'root',
path: '/', fullPath: '/',
meta: { meta: {
title: 'Root' title: 'Root'
}, },
@@ -34,7 +34,7 @@ export const useTabStore = defineStore('tab-store', {
/** 当前激活状态的页签索引 */ /** 当前激活状态的页签索引 */
activeTabIndex(state) { activeTabIndex(state) {
const { tabs, activeTab } = state; const { tabs, activeTab } = state;
return tabs.findIndex(tab => tab.path === activeTab); return tabs.findIndex(tab => tab.fullPath === activeTab);
} }
}, },
actions: { actions: {
@@ -45,10 +45,10 @@ export const useTabStore = defineStore('tab-store', {
}, },
/** /**
* 设置当前路由对应的页签为激活状态 * 设置当前路由对应的页签为激活状态
* @param path - 路由path * @param fullPath - 路由fullPath
*/ */
setActiveTab(path: string) { setActiveTab(fullPath: string) {
this.activeTab = path; this.activeTab = fullPath;
}, },
/** /**
* 初始化首页页签路由 * 初始化首页页签路由
@@ -68,22 +68,39 @@ export const useTabStore = defineStore('tab-store', {
* @param route - 路由 * @param route - 路由
*/ */
addTab(route: RouteLocationNormalizedLoaded) { addTab(route: RouteLocationNormalizedLoaded) {
if (!isInTabRoutes(this.tabs, route.path)) { const tab = getTabRouteByVueRoute(route);
this.tabs.push(getTabRouteByVueRoute(route));
if (isInTabRoutes(this.tabs, tab.fullPath)) {
return;
} }
const index = getIndexInTabRoutesByRouteName(this.tabs, route.name as string);
if (index === -1) {
this.tabs.push(tab);
return;
}
const { multiTab = false } = route.meta;
if (!multiTab) {
this.tabs.splice(index, 1, tab);
return;
}
this.tabs.push(tab);
}, },
/** /**
* 删除多页签 * 删除多页签
* @param path - 路由path * @param fullPath - 路由fullPath
*/ */
removeTab(path: string) { removeTab(fullPath: string) {
const { routerPush } = useRouterPush(false); const { routerPush } = useRouterPush(false);
const isActive = this.activeTab === path; const isActive = this.activeTab === fullPath;
const updateTabs = this.tabs.filter(tab => tab.path !== path); const updateTabs = this.tabs.filter(tab => tab.fullPath !== fullPath);
this.tabs = updateTabs; this.tabs = updateTabs;
if (isActive && updateTabs.length) { if (isActive && updateTabs.length) {
const activePath = updateTabs[updateTabs.length - 1].path; const activePath = updateTabs[updateTabs.length - 1].fullPath;
this.setActiveTab(activePath); this.setActiveTab(activePath);
routerPush(activePath); routerPush(activePath);
} }
@@ -95,73 +112,73 @@ export const useTabStore = defineStore('tab-store', {
clearTab(excludes: string[] = []) { clearTab(excludes: string[] = []) {
const { routerPush } = useRouterPush(false); const { routerPush } = useRouterPush(false);
const homePath = this.homeTab.path; const homePath = this.homeTab.fullPath;
const remain = [homePath, ...excludes]; const remain = [homePath, ...excludes];
const hasActive = remain.includes(this.activeTab); const hasActive = remain.includes(this.activeTab);
const updateTabs = this.tabs.filter(tab => remain.includes(tab.path)); const updateTabs = this.tabs.filter(tab => remain.includes(tab.fullPath));
this.tabs = updateTabs; this.tabs = updateTabs;
if (!hasActive && updateTabs.length) { if (!hasActive && updateTabs.length) {
const activePath = updateTabs[updateTabs.length - 1].path; const activePath = updateTabs[updateTabs.length - 1].fullPath;
this.setActiveTab(activePath); this.setActiveTab(activePath);
routerPush(activePath); routerPush(activePath);
} }
}, },
/** /**
* 清除左边多页签 * 清除左边多页签
* @param path - 路由path * @param fullPath - 路由fullPath
*/ */
clearLeftTab(path: string) { clearLeftTab(fullPath: string) {
const index = getIndexInTabRoutes(this.tabs, path); const index = getIndexInTabRoutes(this.tabs, fullPath);
if (index > -1) { if (index > -1) {
const excludes = this.tabs.slice(index).map(item => item.path); const excludes = this.tabs.slice(index).map(item => item.fullPath);
this.clearTab(excludes); this.clearTab(excludes);
} }
}, },
/** /**
* 清除右边多页签 * 清除右边多页签
* @param path - 路由path * @param fullPath - 路由fullPath
*/ */
clearRightTab(path: string) { clearRightTab(fullPath: string) {
const index = getIndexInTabRoutes(this.tabs, path); const index = getIndexInTabRoutes(this.tabs, fullPath);
if (index > -1) { if (index > -1) {
const excludes = this.tabs.slice(0, index + 1).map(item => item.path); const excludes = this.tabs.slice(0, index + 1).map(item => item.fullPath);
this.clearTab(excludes); this.clearTab(excludes);
} }
}, },
/** /**
* 点击单个tab * 点击单个tab
* @param path - 路由path * @param fullPath - 路由fullPath
*/ */
handleClickTab(path: string) { handleClickTab(fullPath: string) {
const { routerPush } = useRouterPush(false); const { routerPush } = useRouterPush(false);
const isActive = this.activeTab === path; const isActive = this.activeTab === fullPath;
if (!isActive) { if (!isActive) {
this.setActiveTab(path); this.setActiveTab(fullPath);
routerPush(path); routerPush(fullPath);
} }
}, },
/** /**
* 记录tab滚动位置 * 记录tab滚动位置
* @param path - 路由path * @param fullPath - 路由fullPath
* @param position - tab当前页的滚动位置 * @param position - tab当前页的滚动位置
*/ */
recordTabScrollPosition(path: string, position: { left: number; top: number }) { recordTabScrollPosition(fullPath: string, position: { left: number; top: number }) {
const index = getIndexInTabRoutes(this.tabs, path); const index = getIndexInTabRoutes(this.tabs, fullPath);
if (index > -1) { if (index > -1) {
this.tabs[index].scrollPosition = position; this.tabs[index].scrollPosition = position;
} }
}, },
/** /**
* 获取tab滚动位置 * 获取tab滚动位置
* @param path - 路由path * @param fullPath - 路由fullPath
*/ */
getTabScrollPosition(path: string) { getTabScrollPosition(fullPath: string) {
const position = { const position = {
left: 0, left: 0,
top: 0 top: 0
}; };
const index = getIndexInTabRoutes(this.tabs, path); const index = getIndexInTabRoutes(this.tabs, fullPath);
if (index > -1) { if (index > -1) {
Object.assign(position, this.tabs[index].scrollPosition); Object.assign(position, this.tabs[index].scrollPosition);
} }
@@ -173,20 +190,27 @@ export const useTabStore = defineStore('tab-store', {
const tabs: GlobalTabRoute[] = theme.tab.isCache ? getTabRoutes() : []; const tabs: GlobalTabRoute[] = theme.tab.isCache ? getTabRoutes() : [];
const hasHome = isInTabRoutes(tabs, this.homeTab.path); const hasHome = getIndexInTabRoutesByRouteName(tabs, this.homeTab.name as string) > -1;
if (!hasHome && this.homeTab.name !== 'root') { if (!hasHome && this.homeTab.name !== 'root') {
tabs.unshift(this.homeTab); tabs.unshift(this.homeTab);
} }
const isHome = currentRoute.path === this.homeTab.path; const isHome = currentRoute.fullPath === this.homeTab.fullPath;
const hasCurrent = isInTabRoutes(tabs, currentRoute.path); const index = getIndexInTabRoutesByRouteName(tabs, currentRoute.name as string);
if (!isHome && !hasCurrent) { if (!isHome) {
const currentTab = getTabRouteByVueRoute(currentRoute); const currentTab = getTabRouteByVueRoute(currentRoute);
tabs.push(currentTab); if (!currentRoute.meta.multiTab) {
tabs.splice(index, 1, currentTab);
} else {
const hasCurrent = isInTabRoutes(tabs, currentRoute.fullPath);
if (!hasCurrent) {
tabs.push(currentTab);
}
}
} }
this.tabs = tabs; this.tabs = tabs;
this.setActiveTab(currentRoute.path); this.setActiveTab(currentRoute.fullPath);
} }
} }
}); });

View File

@@ -15,9 +15,8 @@ export function getThemeSettings() {
type ColorType = 'primary' | 'info' | 'success' | 'warning' | 'error'; type ColorType = 'primary' | 'info' | 'success' | 'warning' | 'error';
type ColorScene = '' | 'Suppl' | 'Hover' | 'Pressed' | 'Active'; type ColorScene = '' | 'Suppl' | 'Hover' | 'Pressed' | 'Active';
type ColorKey = `${ColorType}Color${ColorScene}`; type ColorKey = `${ColorType}Color${ColorScene}`;
type ThemeColor = { type ThemeColor = Partial<Record<ColorKey, string>>;
[key in ColorKey]?: string;
};
interface ColorAction { interface ColorAction {
scene: ColorScene; scene: ColorScene;
handler: (color: string) => string; handler: (color: string) => string;

View File

@@ -15,6 +15,10 @@ export const useThemeStore = defineStore('theme-store', {
/** naive-ui暗黑主题 */ /** naive-ui暗黑主题 */
naiveTheme(state) { naiveTheme(state) {
return state.darkMode ? darkTheme : undefined; return state.darkMode ? darkTheme : undefined;
},
/** 页面动画模式 */
pageAnimateMode(state) {
return state.page.animate ? state.page.animateMode : undefined;
} }
}, },
actions: { actions: {
@@ -26,6 +30,16 @@ export const useThemeStore = defineStore('theme-store', {
setDarkMode(darkMode: boolean) { setDarkMode(darkMode: boolean) {
this.darkMode = darkMode; this.darkMode = darkMode;
}, },
/** 设置自动跟随系统主题 */
setFollowSystemTheme(visible: boolean) {
this.followSystemTheme = visible;
},
/** 自动跟随系统主题 */
setAutoFollowSystemMode(darkMode: boolean) {
if (this.followSystemTheme) {
this.darkMode = darkMode;
}
},
/** 切换/关闭 暗黑模式 */ /** 切换/关闭 暗黑模式 */
toggleDarkMode() { toggleDarkMode() {
this.darkMode = !this.darkMode; this.darkMode = !this.darkMode;

View File

@@ -1,21 +0,0 @@
import { watch, onUnmounted } from 'vue';
import { useBodyScroll } from '@/hooks';
import { useAppStore } from '../modules';
/** 订阅app store */
export default function subscribeAppStore() {
const app = useAppStore();
const { scrollBodyHandler } = useBodyScroll();
// 弹窗打开时禁止滚动条
const stopHandle = watch(
() => app.settingDrawerVisible,
newValue => {
scrollBodyHandler(newValue);
}
);
onUnmounted(() => {
stopHandle();
});
}

View File

@@ -1,8 +1,6 @@
// import subscribeAppStore from './app';
import subscribeThemeStore from './theme'; import subscribeThemeStore from './theme';
/** 订阅状态 */ /** 订阅状态 */
export function subscribeStore() { export function subscribeStore() {
// subscribeAppStore();
subscribeThemeStore(); subscribeThemeStore();
} }

View File

@@ -53,7 +53,7 @@ export default function subscribeThemeStore() {
osTheme, osTheme,
newValue => { newValue => {
const isDark = newValue === 'dark'; const isDark = newValue === 'dark';
theme.setDarkMode(isDark); theme.setAutoFollowSystemMode(isDark);
}, },
{ immediate: true } { immediate: true }
); );

View File

@@ -1,103 +0,0 @@
// generated by unplugin-vue-components
// We suggest you to commit this file into source control
// Read more: https://github.com/vuejs/vue-next/pull/3399
import '@vue/runtime-core'
declare module '@vue/runtime-core' {
export interface GlobalComponents {
BetterScroll: typeof import('./../components/custom/BetterScroll.vue')['default']
CountTo: typeof import('./../components/custom/CountTo.vue')['default']
DarkModeContainer: typeof import('./../components/common/DarkModeContainer.vue')['default']
DarkModeSwitch: typeof import('./../components/common/DarkModeSwitch.vue')['default']
GithubLink: typeof import('./../components/custom/GithubLink.vue')['default']
HoverContainer: typeof import('./../components/common/HoverContainer.vue')['default']
IconAntDesignCloseOutlined: typeof import('~icons/ant-design/close-outlined')['default']
IconAntDesignEnterOutlined: typeof import('~icons/ant-design/enter-outlined')['default']
IconAntDesignSettingOutlined: typeof import('~icons/ant-design/setting-outlined')['default']
IconCustomActivity: typeof import('~icons/custom/activity')['default']
IconCustomAvatar: typeof import('~icons/custom/avatar')['default']
IconCustomBanner: typeof import('~icons/custom/banner')['default']
IconCustomCast: typeof import('~icons/custom/cast')['default']
IconCustomEmptyData: typeof import('~icons/custom/empty-data')['default']
IconCustomLogo: typeof import('~icons/custom/logo')['default']
IconCustomLogoFill: typeof import('~icons/custom/logo-fill')['default']
IconCustomNetworkError: typeof import('~icons/custom/network-error')['default']
IconCustomNoPermission: typeof import('~icons/custom/no-permission')['default']
IconCustomNotFound: typeof import('~icons/custom/not-found')['default']
IconCustomServiceError: typeof import('~icons/custom/service-error')['default']
IconGridiconsFullscreen: typeof import('~icons/gridicons/fullscreen')['default']
IconGridiconsFullscreenExit: typeof import('~icons/gridicons/fullscreen-exit')['default']
IconIcOutlineCheck: typeof import('~icons/ic/outline-check')['default']
IconLineMdMenuFoldLeft: typeof import('~icons/line-md/menu-fold-left')['default']
IconLineMdMenuUnfoldLeft: typeof import('~icons/line-md/menu-unfold-left')['default']
IconMdiArrowDownThin: typeof import('~icons/mdi/arrow-down-thin')['default']
IconMdiArrowUpThin: typeof import('~icons/mdi/arrow-up-thin')['default']
IconMdiClose: typeof import('~icons/mdi/close')['default']
IconMdiGithub: typeof import('~icons/mdi/github')['default']
IconMdiMoonWaningCrescent: typeof import('~icons/mdi/moon-waning-crescent')['default']
IconMdiPin: typeof import('~icons/mdi/pin')['default']
IconMdiPinOff: typeof import('~icons/mdi/pin-off')['default']
IconMdiRefresh: typeof import('~icons/mdi/refresh')['default']
IconMdiWechat: typeof import('~icons/mdi/wechat')['default']
IconMdiWhiteBalanceSunny: typeof import('~icons/mdi/white-balance-sunny')['default']
IconPhCaretDoubleLeftBold: typeof import('~icons/ph/caret-double-left-bold')['default']
IconPhCaretDoubleRightBold: typeof import('~icons/ph/caret-double-right-bold')['default']
IconSelect: typeof import('./../components/custom/IconSelect.vue')['default']
IconUilSearch: typeof import('~icons/uil/search')['default']
ImageVerify: typeof import('./../components/custom/ImageVerify.vue')['default']
LoadingEmptyWrapper: typeof import('./../components/business/LoadingEmptyWrapper.vue')['default']
LoginAgreement: typeof import('./../components/business/LoginAgreement.vue')['default']
NaiveProvider: typeof import('./../components/common/NaiveProvider.vue')['default']
NBreadcrumb: typeof import('naive-ui')['NBreadcrumb']
NBreadcrumbItem: typeof import('naive-ui')['NBreadcrumbItem']
NButton: typeof import('naive-ui')['NButton']
NCard: typeof import('naive-ui')['NCard']
NCheckbox: typeof import('naive-ui')['NCheckbox']
NColorPicker: typeof import('naive-ui')['NColorPicker']
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
NDataTable: typeof import('naive-ui')['NDataTable']
NDescriptions: typeof import('naive-ui')['NDescriptions']
NDescriptionsItem: typeof import('naive-ui')['NDescriptionsItem']
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
NDivider: typeof import('naive-ui')['NDivider']
NDrawer: typeof import('naive-ui')['NDrawer']
NDrawerContent: typeof import('naive-ui')['NDrawerContent']
NDropdown: typeof import('naive-ui')['NDropdown']
NEmpty: typeof import('naive-ui')['NEmpty']
NForm: typeof import('naive-ui')['NForm']
NFormItem: typeof import('naive-ui')['NFormItem']
NGradientText: typeof import('naive-ui')['NGradientText']
NGrid: typeof import('naive-ui')['NGrid']
NGridItem: typeof import('naive-ui')['NGridItem']
NInput: typeof import('naive-ui')['NInput']
NInputGroup: typeof import('naive-ui')['NInputGroup']
NInputNumber: typeof import('naive-ui')['NInputNumber']
NList: typeof import('naive-ui')['NList']
NListItem: typeof import('naive-ui')['NListItem']
NLoadingBarProvider: typeof import('naive-ui')['NLoadingBarProvider']
NMenu: typeof import('naive-ui')['NMenu']
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
NModal: typeof import('naive-ui')['NModal']
NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
NPopover: typeof import('naive-ui')['NPopover']
NScrollbar: typeof import('naive-ui')['NScrollbar']
NSelect: typeof import('naive-ui')['NSelect']
NSpace: typeof import('naive-ui')['NSpace']
NSpin: typeof import('naive-ui')['NSpin']
NStatistic: typeof import('naive-ui')['NStatistic']
NSwitch: typeof import('naive-ui')['NSwitch']
NTabPane: typeof import('naive-ui')['NTabPane']
NTabs: typeof import('naive-ui')['NTabs']
NTag: typeof import('naive-ui')['NTag']
NThing: typeof import('naive-ui')['NThing']
NTimeline: typeof import('naive-ui')['NTimeline']
NTimelineItem: typeof import('naive-ui')['NTimelineItem']
NTooltip: typeof import('naive-ui')['NTooltip']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
SystemLogo: typeof import('./../components/common/SystemLogo.vue')['default']
WebSiteLink: typeof import('./../components/custom/WebSiteLink.vue')['default']
}
}
export {}

View File

@@ -36,9 +36,18 @@ declare namespace AuthRoute {
| 'plugin_icon' | 'plugin_icon'
| 'plugin_print' | 'plugin_print'
| 'plugin_swiper' | 'plugin_swiper'
| 'plugin_charts'
| 'plugin_charts_echarts'
| 'plugin_charts_d3'
| 'plugin_charts_antv'
| 'plugin_charts_chartjs'
| 'auth-demo' | 'auth-demo'
| 'auth-demo_permission' | 'auth-demo_permission'
| 'auth-demo_super' | 'auth-demo_super'
| 'function'
| 'function_tab'
| 'function_tab-detail'
| 'function_tab-multi-detail'
| 'exception' | 'exception'
| 'exception_403' | 'exception_403'
| 'exception_404' | 'exception_404'
@@ -70,7 +79,7 @@ declare namespace AuthRoute {
type RouteMeta = { type RouteMeta = {
/** 路由标题(可用来作document.title或者菜单的名称) */ /** 路由标题(可用来作document.title或者菜单的名称) */
title: string; title: string;
/** 路由的动态路径 */ /** 路由的动态路径(需要动态路径的页面需要将path添加进范型参数) */
dynamicPath?: PathToDynamicPath<'/login'>; dynamicPath?: PathToDynamicPath<'/login'>;
/** 作为单级路由的父级路由布局组件 */ /** 作为单级路由的父级路由布局组件 */
singleLayout?: Extract<RouteComponent, 'basic' | 'blank'>; singleLayout?: Extract<RouteComponent, 'basic' | 'blank'>;
@@ -85,12 +94,16 @@ declare namespace AuthRoute {
keepAlive?: boolean; keepAlive?: boolean;
/** 菜单和面包屑对应的图标 */ /** 菜单和面包屑对应的图标 */
icon?: string; icon?: string;
/** 是否在菜单中隐藏 */ /** 是否在菜单中隐藏(一些列表、表格的详情页面需要通过参数跳转,所以不能显示在菜单中) */
hide?: boolean; hide?: boolean;
/** 外链链接 */ /** 外链链接 */
href?: string; href?: string;
/** 是否支持多个tab页签(默认一个即相同name的路由会被替换) */
multiTab?: boolean;
/** 路由顺序,可用于菜单的排序 */ /** 路由顺序,可用于菜单的排序 */
order?: number; order?: number;
/** 当前路由需要选中的菜单项(用于跳转至不在左侧菜单显示的路由且需要高亮某个菜单的情况) */
activeMenu?: RouteKey;
/** 表示是否是多级路由的中间级路由(用于转换路由数据时筛选多级路由的标识,定义路由时不用填写) */ /** 表示是否是多级路由的中间级路由(用于转换路由数据时筛选多级路由的标识,定义路由时不用填写) */
multi?: boolean; multi?: boolean;
}; };
@@ -140,9 +153,9 @@ declare namespace AuthRoute {
/** 路由path转换动态路径 */ /** 路由path转换动态路径 */
type PathToDynamicPath<Path extends RoutePath> = type PathToDynamicPath<Path extends RoutePath> =
| `${Path}/:module` | `${Path}/:${string}`
| `${Path}/:module(${string})` | `${Path}/:${string}(${string})`
| `${Path}/:module(${string})?`; | `${Path}/:${string}(${string})?`;
/** 获取一级路由(包括有子路由的一级路由) */ /** 获取一级路由(包括有子路由的一级路由) */
type GetSingleRouteKey<Key extends RouteKey> = Key extends `${infer _Left}${RouteSplitMark}${infer _Right}` type GetSingleRouteKey<Key extends RouteKey> = Key extends `${infer _Left}${RouteSplitMark}${infer _Right}`

View File

@@ -100,6 +100,8 @@ declare namespace Theme {
interface Setting { interface Setting {
/** 暗黑模式 */ /** 暗黑模式 */
darkMode: boolean; darkMode: boolean;
/** 是否自动跟随系统主题 */
followSystemTheme: boolean;
/** 布局样式 */ /** 布局样式 */
layout: Layout; layout: Layout;
/** 主题颜色 */ /** 主题颜色 */
@@ -209,7 +211,7 @@ declare namespace Theme {
/** 菜单样式 */ /** 菜单样式 */
interface Menu { interface Menu {
/** 水平模式的菜单的位置 */ /** 水平模式的菜单的位置 */
horizontalPosition: HorizontalMenuPosition; horizontalPosition: EnumType.ThemeHorizontalMenuPosition;
/** 水平模式的菜单的位置列表 */ /** 水平模式的菜单的位置列表 */
horizontalPositionList: HorizontalMenuPositionList[]; horizontalPositionList: HorizontalMenuPositionList[];
} }
@@ -274,7 +276,8 @@ type GlobalBreadcrumb = import('naive-ui').DropdownOption & {
}; };
/** 多页签Tab的路由 */ /** 多页签Tab的路由 */
interface GlobalTabRoute extends Pick<import('vue-router').RouteLocationNormalizedLoaded, 'name' | 'path' | 'meta'> { interface GlobalTabRoute
extends Pick<import('vue-router').RouteLocationNormalizedLoaded, 'name' | 'fullPath' | 'meta'> {
/** 滚动的位置 */ /** 滚动的位置 */
scrollPosition: { scrollPosition: {
left: number; left: number;

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