mirror of
https://github.com/soybeanjs/soybean-admin.git
synced 2025-10-11 20:33:41 +08:00
Compare commits
67 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
65c21812bb | ||
|
c3c975ee11 | ||
|
833018a831 | ||
|
3eb7f6f593 | ||
|
efcfa576d5 | ||
|
487213b648 | ||
|
4ee0d94f1b | ||
|
5fa822f4d4 | ||
|
8e6e787543 | ||
|
9917b5e53c | ||
|
8f3e855f41 | ||
|
808051b29d | ||
|
906aed5e75 | ||
|
2d64a2e57c | ||
|
0c70a9e083 | ||
|
08d83ecbea | ||
|
4122685803 | ||
|
434ab1c560 | ||
|
ae99e57c52 | ||
|
e3c4a6ece6 | ||
|
c8717c25b8 | ||
|
e9656c6e76 | ||
|
fd78791229 | ||
|
de09f82586 | ||
|
c7762490de | ||
|
4558c24d1c | ||
|
d9ac7e4de0 | ||
|
6a5a357f50 | ||
|
44b022aefd | ||
|
0a46ea0844 | ||
|
39854a492b | ||
|
4c2f535a9b | ||
|
be45d83766 | ||
|
d28b9039bb | ||
|
8f6d6ce3cb | ||
|
07baac7cf8 | ||
|
7487ab79b3 | ||
|
a70e4161be | ||
|
095c432363 | ||
|
028096e53f | ||
|
44ab55d594 | ||
|
4b80a66114 | ||
|
cc0bb088ec | ||
|
3e4f9e2824 | ||
|
ebd16a4d1a | ||
|
84cb07baec | ||
|
0811ffa5ae | ||
|
3f822a7d76 | ||
|
a1c7e10574 | ||
|
50d7ccd82d | ||
|
e0233061d3 | ||
|
a0c405dadd | ||
|
60f912508b | ||
|
3590b65e22 | ||
|
92b8406444 | ||
|
e7ad08685e | ||
|
14c145eef1 | ||
|
518f7eed28 | ||
|
21e63998d0 | ||
|
38ee2a62cd | ||
|
716528206e | ||
|
b81143e55e | ||
|
0243b27505 | ||
|
909c12d3c6 | ||
|
01d0bcbfd0 | ||
|
ba07b695dd | ||
|
3d8befa376 |
1
.env
1
.env
@@ -9,4 +9,5 @@ VITE_APP_DESC=SoybeanAdmin是一个中后台管理系统模版
|
||||
# 权限路由模式: static | dynamic
|
||||
VITE_AUTH_ROUTE_MODE=dynamic
|
||||
|
||||
# 路由首页(根路由重定向), 用于static模式的权限路由,dynamic模式取决于后端返回的路由首页
|
||||
VITE_ROUTE_HOME_PATH=/dashboard/analysis
|
||||
|
64
.eslintrc.js
64
.eslintrc.js
@@ -26,9 +26,47 @@ module.exports = {
|
||||
'@vue/eslint-config-prettier',
|
||||
'@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: {
|
||||
'import/extensions': 'off',
|
||||
'import/no-extraneous-dependencies': 'off',
|
||||
'import/extensions': [
|
||||
'warn',
|
||||
'ignorePackages',
|
||||
{
|
||||
js: 'never',
|
||||
jsx: 'never',
|
||||
mjs: 'never',
|
||||
ts: 'never',
|
||||
tsx: 'never'
|
||||
}
|
||||
],
|
||||
'import/no-extraneous-dependencies': ['error', { devDependencies: true, peerDependencies: true }],
|
||||
'import/order': [
|
||||
'error',
|
||||
{
|
||||
@@ -144,7 +182,7 @@ module.exports = {
|
||||
pathGroupsExcludedImportTypes: ['vue', 'vue-router', 'vuex', 'pinia', 'naive-ui']
|
||||
}
|
||||
],
|
||||
'import/no-unresolved': 'off',
|
||||
'import/no-unresolved': ['error', { ignore: ['uno.css', '~icons/*', 'virtual:svg-icons-register'] }],
|
||||
'import/prefer-default-export': 'off',
|
||||
'max-classes-per-file': 'off',
|
||||
'no-param-reassign': [
|
||||
@@ -154,7 +192,6 @@ module.exports = {
|
||||
ignorePropertyModificationsFor: ['state', 'acc', 'e']
|
||||
}
|
||||
],
|
||||
'no-plusplus': 'off',
|
||||
'no-shadow': 'off',
|
||||
'no-unused-vars': 'off',
|
||||
'no-use-before-define': 'off',
|
||||
@@ -164,7 +201,6 @@ module.exports = {
|
||||
ignores: ['index']
|
||||
}
|
||||
],
|
||||
'@typescript-eslint/ban-types': 'off',
|
||||
'@typescript-eslint/no-empty-interface': [
|
||||
'error',
|
||||
{
|
||||
@@ -175,20 +211,6 @@ module.exports = {
|
||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||
'@typescript-eslint/no-shadow': 'error',
|
||||
'@typescript-eslint/no-unused-vars': ['warn', { ignoreRestSiblings: true, varsIgnorePattern: '^_' }],
|
||||
'@typescript-eslint/no-use-before-define': ['warn', { classes: true, functions: false, typedefs: false }]
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.vue'],
|
||||
rules: {
|
||||
'no-undef': 'off'
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ['*.html'],
|
||||
rules: {
|
||||
'vue/comment-directive': 'off'
|
||||
}
|
||||
}
|
||||
]
|
||||
'@typescript-eslint/no-use-before-define': ['error', { classes: true, functions: false, typedefs: false }]
|
||||
}
|
||||
};
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@@ -28,3 +28,5 @@ stats.html
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
/src/typings/components.d.ts
|
||||
|
2
.npmrc
2
.npmrc
@@ -1 +1,3 @@
|
||||
registry=https://registry.npmmirror.com/
|
||||
shamefully-hoist=true
|
||||
strict-peer-dependencies=false
|
||||
|
46
.vscode/extensions.json
vendored
46
.vscode/extensions.json
vendored
@@ -1,36 +1,26 @@
|
||||
{
|
||||
"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",
|
||||
"antfu.iconify",
|
||||
"kisstkondoros.vscode-gutter-preview",
|
||||
"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",
|
||||
"antfu.unocss",
|
||||
"christian-kohler.path-intellisense",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"eamodio.gitlens",
|
||||
"editorconfig.editorconfig",
|
||||
"esbenp.prettier-vscode",
|
||||
"johnsoncodehk.volar",
|
||||
"johnsoncodehk.vscode-typescript-vue-plugin",
|
||||
"dariofuzinato.vue-peek",
|
||||
"wscats.vue",
|
||||
"antfu.unocss"
|
||||
"formulahendry.auto-complete-tag",
|
||||
"formulahendry.auto-close-tag",
|
||||
"formulahendry.auto-rename-tag",
|
||||
"kisstkondoros.vscode-gutter-preview",
|
||||
"lokalise.i18n-ally",
|
||||
"mhutchie.git-graph",
|
||||
"mikestead.dotenv",
|
||||
"naumovs.color-highlight",
|
||||
"pkief.material-icon-theme",
|
||||
"steoates.autoimport",
|
||||
"vue.volar",
|
||||
"vue.vscode-typescript-vue-plugin",
|
||||
"whtouche.vscode-js-console-utils",
|
||||
"zhuangtongfa.material-theme"
|
||||
]
|
||||
}
|
||||
|
97
.vscode/settings.json
vendored
97
.vscode/settings.json
vendored
@@ -1,62 +1,22 @@
|
||||
{
|
||||
"editor.quickSuggestions": {
|
||||
"strings": true
|
||||
},
|
||||
"workbench.iconTheme": "material-icon-theme",
|
||||
"workbench.colorTheme": "One Dark Pro",
|
||||
"editor.tabSize": 2,
|
||||
"editor.fontLigatures": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true
|
||||
},
|
||||
"editor.bracketPairColorization.enabled": true,
|
||||
"editor.fontLigatures": true,
|
||||
"editor.formatOnSave": false,
|
||||
"editor.guides.bracketPairs": "active",
|
||||
"git.enableSmartCommit": true,
|
||||
"path-intellisense.mappings": {
|
||||
"@": "${workspaceFolder}/src",
|
||||
"~@": "${workspaceFolder}/src",
|
||||
"editor.quickSuggestions": {
|
||||
"strings": true
|
||||
},
|
||||
"editor.tabSize": 2,
|
||||
"files.associations": {
|
||||
"*.env.*": "dotenv"
|
||||
},
|
||||
"git.enableSmartCommit": true,
|
||||
"gutterpreview.paths": {
|
||||
"@": "/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",
|
||||
"[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.files.associations": {},
|
||||
"material-icon-theme.folders.associations": {
|
||||
@@ -72,5 +32,42 @@
|
||||
"request": "api",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
40
CHANGELOG.md
40
CHANGELOG.md
@@ -2,6 +2,46 @@
|
||||
|
||||
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.6](https://github.com/honghuangdc/soybean-admin/compare/v0.9.5...v0.9.6) (2022-06-15)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **projects:** 本地svg动态渲染图标 ([c3c975e](https://github.com/honghuangdc/soybean-admin/commit/c3c975ee1142987b7ded0107bf91d0080d5651fe)), closes [#61](https://github.com/honghuangdc/soybean-admin/issues/61)
|
||||
* **projects:** 上下结构,菜单支持横向滚动 ([808051b](https://github.com/honghuangdc/soybean-admin/commit/808051b29dd682e1cbcf0e211774efb9cc12713a))
|
||||
* **projects:** 新增Antv G2图表示例 ([2d64a2e](https://github.com/honghuangdc/soybean-admin/commit/2d64a2e57c8d83c8d06f210eeefef8f31b3abeb9))
|
||||
* **projects:** 增加设置当前Tab页签名称功能 ([487213b](https://github.com/honghuangdc/soybean-admin/commit/487213b64853765e2bd186474e4607572624a33e))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **projects:** 设置tab标题导致meta属性丢失 ([efcfa57](https://github.com/honghuangdc/soybean-admin/commit/efcfa576d52a7eab644f3b4c65af153442887fab))
|
||||
* **projects:** 修复顶部菜单的位置失效问题 ([4ee0d94](https://github.com/honghuangdc/soybean-admin/commit/4ee0d94f1bde83c788fc0dcb084402359c04fb1b))
|
||||
|
||||
### [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)
|
||||
|
||||
|
||||
|
76
README.md
76
README.md
@@ -7,12 +7,12 @@
|
||||
|
||||
## 简介
|
||||
|
||||
Soybean Admin 是一个基于 Vue3、Vite、TypeScript、Naive UI 的免费中后台模版,它使用了最新的前端技术栈,内置丰富的主题配置,有着极高的代码规范,基于mock实现的动态权限路由,开箱即用的中后台前端解决方案,也可用于学习参考。
|
||||
Soybean Admin 是一个基于 Vue3、Vite、TypeScript、Naive UI 的免费中后台模版,它使用了最新的前端技术栈,内置丰富的主题配置,有着极高的代码规范,基于mock实现的动态权限路由,开箱即用的中后台前端解决方案,也可用于学习参考。
|
||||
|
||||
## 特性
|
||||
|
||||
- **最新技术栈**:使用 Vue3/vite2 等前端前沿技术开发, 使用高效率的npm包管理器pnpm
|
||||
- **TypeScript**: 应用程序级 JavaScript 的语言
|
||||
- **TypeScript**:应用程序级 JavaScript 的语言
|
||||
- **主题**:丰富可配置的主题、暗黑模式,基于原子css - unocss的动态主题颜色
|
||||
- **代码规范**:丰富的规范插件及极高的代码规范
|
||||
- **权限路由**:简易的路由配置、基于mock的动态路由能快速实现后端动态路由
|
||||
@@ -32,41 +32,65 @@ Soybean Admin 是一个基于 Vue3、Vite、TypeScript、Naive UI 的免费中
|
||||
|
||||
- [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)
|
||||
|
||||
## 项目示例图
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## 开发计划
|
||||
|
||||
- [x] 添加前端静态路由
|
||||
- [x] 集成unocss替换windicss
|
||||
- [x] 用户角色切换示例、按钮级别权限指令
|
||||
- [ ] 引入ECharts替换AntV G2Plot
|
||||
- [ ] 最近功能的有关文档更新
|
||||
- [ ] 性能优化(优化递归函数)
|
||||
- [ ] 精简版(新分支thin)
|
||||
- [ ] 表单、表格示例
|
||||
- [x] 引入ECharts替换AntV G2Plot
|
||||
- [x] 图表示例:ECharts、AntV G2
|
||||
- [x] 多页签:支持query、hash等参数,同一页面支持多个Tab
|
||||
- [ ] 缓存主题配置
|
||||
- [ ] 添加锁屏组件、全局Iframe组件
|
||||
- [ ] 示例页面完善
|
||||
- [ ] 其他UI版本
|
||||
- [ ] 表单、表格示例
|
||||
- [ ] 性能优化(优化递归函数)
|
||||
- [ ] 精简版(新分支thin)
|
||||
- [ ] 文档完善
|
||||
- [ ] i18n国际化
|
||||
- [ ] element-plus版本
|
||||
- [ ] 其他UI版本
|
||||
- [ ] soybean-admin cli工具(选择不同UI)
|
||||
- [ ] 前端可视化创建路由页面
|
||||
- [ ] 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 后台服务nodejs版: [soybean-admin-nestjs](https://github.com/honghuangdc/soybean-admin-nestjs)
|
||||
- [ ] 前端可视化创建路由页面
|
||||
|
||||
## 安装使用
|
||||
|
||||
@@ -94,11 +118,8 @@ pnpm dev
|
||||
pnpm build
|
||||
```
|
||||
|
||||
::: warning 注意
|
||||
|
||||
**本地环境需要安装 pnpm 6.x 、Node.js 14.x 和 Git**
|
||||
|
||||
:::
|
||||
|
||||
## 如何贡献
|
||||
|
||||
@@ -128,14 +149,19 @@ pnpm i -g commitizen
|
||||
|
||||
[@Soybean](https://github.com/honghuangdc)
|
||||
|
||||
## 捐赠
|
||||
如果你觉得这个项目对你有帮助,可以请Soybean喝杯饮料表示支持,Soybean开源的动力离不开各位的支持和鼓励。
|
||||

|
||||
|
||||
## 交流
|
||||
|
||||
`Soybean Admin` 是完全开源免费的项目,在帮助开发者更方便地进行中大型管理系统开发,同时也提供微信和QQ交流群,使用问题欢迎在群内提问。
|
||||
|
||||
- 本人微信号:honghuangdc,欢迎来技术交流,业务咨询。
|
||||
- 微信交流群(添加本人微信拉进群),欢迎来技术交流,业务咨询。
|
||||
|
||||
- 微信交流群:
|
||||
**微信群的人数已经满200个了,无法扫码,可以添加本人的微信再邀请进入**
|
||||
<div style="text-align:left">
|
||||
<img src="https://s2.loli.net/2022/05/16/3YGBgXnVPJdslk8.jpg" style="width:200px" />
|
||||
</div>
|
||||
|
||||
- QQ交流群 `711301266`
|
||||
|
||||
|
@@ -1,2 +1,3 @@
|
||||
export * from './plugins';
|
||||
export * from './config';
|
||||
export * from './utils';
|
||||
|
@@ -10,10 +10,9 @@ import compress from './compress';
|
||||
/**
|
||||
* vite插件
|
||||
* @param viteEnv - 环境变量配置
|
||||
* @param srcPath - src路径
|
||||
*/
|
||||
export function setupVitePlugins(viteEnv: ImportMetaEnv, srcPath: string): (PluginOption | PluginOption[])[] {
|
||||
const plugins = [...vue, html(viteEnv), ...unplugin(srcPath), unocss, mock];
|
||||
export function setupVitePlugins(viteEnv: ImportMetaEnv): (PluginOption | PluginOption[])[] {
|
||||
const plugins = [...vue, html(viteEnv), ...unplugin, unocss, mock];
|
||||
|
||||
if (viteEnv.VITE_VISUALIZER === 'true') {
|
||||
plugins.push(visualizer);
|
||||
|
@@ -4,22 +4,32 @@ import IconsResolver from 'unplugin-icons/resolver';
|
||||
import Components from 'unplugin-vue-components/vite';
|
||||
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers';
|
||||
import { FileSystemIconLoader } from 'unplugin-icons/loaders';
|
||||
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons';
|
||||
import { getSrcPath } from '../utils';
|
||||
|
||||
export default (srcPath: string) => {
|
||||
return [
|
||||
DefineOptions(),
|
||||
Icons({
|
||||
compiler: 'vue3',
|
||||
customCollections: {
|
||||
custom: FileSystemIconLoader(`${srcPath}/assets/svg`)
|
||||
},
|
||||
scale: 1,
|
||||
defaultClass: 'inline-block'
|
||||
}),
|
||||
Components({
|
||||
dts: 'src/typings/components.d.ts',
|
||||
types: [{ from: 'vue-router', names: ['RouterLink', 'RouterView'] }],
|
||||
resolvers: [NaiveUiResolver(), IconsResolver({ customCollections: ['custom'], componentPrefix: 'icon' })]
|
||||
})
|
||||
];
|
||||
};
|
||||
const srcPath = getSrcPath();
|
||||
|
||||
const customIconPath = `${srcPath}/assets/svg`;
|
||||
|
||||
export default [
|
||||
DefineOptions(),
|
||||
Icons({
|
||||
compiler: 'vue3',
|
||||
customCollections: {
|
||||
custom: FileSystemIconLoader(customIconPath)
|
||||
},
|
||||
scale: 1,
|
||||
defaultClass: 'inline-block'
|
||||
}),
|
||||
Components({
|
||||
dts: 'src/typings/components.d.ts',
|
||||
types: [{ from: 'vue-router', names: ['RouterLink', 'RouterView'] }],
|
||||
resolvers: [NaiveUiResolver(), IconsResolver({ customCollections: ['custom'], componentPrefix: 'icon' })]
|
||||
}),
|
||||
createSvgIconsPlugin({
|
||||
iconDirs: [customIconPath],
|
||||
symbolId: 'icon-custom-[dir]-[name]',
|
||||
inject: 'body-last',
|
||||
customDomId: '__CUSTOM_SVG_ICON__'
|
||||
})
|
||||
];
|
||||
|
20
build/utils/index.ts
Normal file
20
build/utils/index.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import path from 'path';
|
||||
|
||||
/**
|
||||
* 获取项目根路径
|
||||
* @descrition 结尾不带斜杠
|
||||
*/
|
||||
export function getRootPath() {
|
||||
return path.resolve(process.cwd());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取项目src路径
|
||||
* @param srcName - src目录名称(默认: "src")
|
||||
* @descrition 结尾不带斜杠
|
||||
*/
|
||||
export function getSrcPath(srcName = 'src') {
|
||||
const rootPath = getRootPath();
|
||||
|
||||
return `${rootPath}/${srcName}`;
|
||||
}
|
@@ -131,6 +131,47 @@ export const routeModel: Record<Auth.RoleType, AuthRoute.Route[]> = {
|
||||
path: '/plugin',
|
||||
component: 'basic',
|
||||
children: [
|
||||
{
|
||||
name: 'plugin_charts',
|
||||
path: '/plugin/charts',
|
||||
component: 'multi',
|
||||
children: [
|
||||
{
|
||||
name: 'plugin_charts_echarts',
|
||||
path: '/plugin/charts/echarts',
|
||||
component: 'self',
|
||||
meta: {
|
||||
title: 'ECharts',
|
||||
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',
|
||||
path: '/plugin/map',
|
||||
@@ -209,7 +250,7 @@ export const routeModel: Record<Auth.RoleType, AuthRoute.Route[]> = {
|
||||
meta: {
|
||||
title: '图标',
|
||||
requiresAuth: true,
|
||||
icon: 'ic:baseline-insert-emoticon'
|
||||
customIcon: 'custom-icon'
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -261,6 +302,53 @@ export const routeModel: Record<Auth.RoleType, AuthRoute.Route[]> = {
|
||||
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',
|
||||
path: '/exception',
|
||||
@@ -300,7 +388,7 @@ export const routeModel: Record<Auth.RoleType, AuthRoute.Route[]> = {
|
||||
meta: {
|
||||
title: '异常页',
|
||||
icon: 'ant-design:exception-outlined',
|
||||
order: 6
|
||||
order: 7
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -354,7 +442,7 @@ export const routeModel: Record<Auth.RoleType, AuthRoute.Route[]> = {
|
||||
meta: {
|
||||
title: '多级菜单',
|
||||
icon: 'carbon:menu',
|
||||
order: 7
|
||||
order: 8
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -366,7 +454,7 @@ export const routeModel: Record<Auth.RoleType, AuthRoute.Route[]> = {
|
||||
requiresAuth: true,
|
||||
singleLayout: 'basic',
|
||||
icon: 'fluent:book-information-24-regular',
|
||||
order: 8
|
||||
order: 9
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -502,6 +590,47 @@ export const routeModel: Record<Auth.RoleType, AuthRoute.Route[]> = {
|
||||
path: '/plugin',
|
||||
component: 'basic',
|
||||
children: [
|
||||
{
|
||||
name: 'plugin_charts',
|
||||
path: '/plugin/charts',
|
||||
component: 'multi',
|
||||
children: [
|
||||
{
|
||||
name: 'plugin_charts_echarts',
|
||||
path: '/plugin/charts/echarts',
|
||||
component: 'self',
|
||||
meta: {
|
||||
title: 'ECharts',
|
||||
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',
|
||||
path: '/plugin/map',
|
||||
@@ -580,7 +709,7 @@ export const routeModel: Record<Auth.RoleType, AuthRoute.Route[]> = {
|
||||
meta: {
|
||||
title: '图标',
|
||||
requiresAuth: true,
|
||||
icon: 'ic:baseline-insert-emoticon'
|
||||
customIcon: 'custom-icon'
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -622,6 +751,53 @@ export const routeModel: Record<Auth.RoleType, AuthRoute.Route[]> = {
|
||||
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',
|
||||
path: '/exception',
|
||||
@@ -661,7 +837,7 @@ export const routeModel: Record<Auth.RoleType, AuthRoute.Route[]> = {
|
||||
meta: {
|
||||
title: '异常页',
|
||||
icon: 'ant-design:exception-outlined',
|
||||
order: 6
|
||||
order: 7
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -715,7 +891,7 @@ export const routeModel: Record<Auth.RoleType, AuthRoute.Route[]> = {
|
||||
meta: {
|
||||
title: '多级菜单',
|
||||
icon: 'carbon:menu',
|
||||
order: 7
|
||||
order: 8
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -727,7 +903,7 @@ export const routeModel: Record<Auth.RoleType, AuthRoute.Route[]> = {
|
||||
requiresAuth: true,
|
||||
singleLayout: 'basic',
|
||||
icon: 'fluent:book-information-24-regular',
|
||||
order: 8
|
||||
order: 9
|
||||
}
|
||||
}
|
||||
],
|
||||
|
93
package.json
93
package.json
@@ -1,6 +1,11 @@
|
||||
{
|
||||
"name": "soybean-admin",
|
||||
"version": "0.9.4",
|
||||
"version": "0.9.6",
|
||||
"author": {
|
||||
"name": "Soybean",
|
||||
"email": "honghuangdc@gmail.com",
|
||||
"url": "https://github.com/honghuangdc"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "cross-env VITE_ENV_TYPE=dev vite",
|
||||
"dev:test": "cross-env VITE_ENV_TYPE=test vite",
|
||||
@@ -9,7 +14,7 @@
|
||||
"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:vercel": "cross-env VITE_HASH_ROUTE=true vite build",
|
||||
"preview": "vite preview --port 5050",
|
||||
"preview": "vite preview",
|
||||
"typecheck": "vue-tsc --noEmit",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
|
||||
"prepare": "husky install",
|
||||
@@ -26,76 +31,88 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@antv/g2plot": "^2.4.16",
|
||||
"@antv/data-set": "^0.11.8",
|
||||
"@antv/g2": "^4.2.3",
|
||||
"@better-scroll/core": "^2.4.2",
|
||||
"@soybeanjs/vue-admin-layout": "^1.0.3",
|
||||
"@soybeanjs/vue-admin-tab": "^1.0.1",
|
||||
"@vueuse/core": "^8.3.1",
|
||||
"@soybeanjs/vue-admin-layout": "^1.0.4",
|
||||
"@soybeanjs/vue-admin-tab": "^1.0.2",
|
||||
"@vueuse/core": "^8.6.0",
|
||||
"axios": "^0.27.2",
|
||||
"clipboard": "^2.0.10",
|
||||
"clipboard": "^2.0.11",
|
||||
"colord": "^2.9.2",
|
||||
"crypto-js": "^4.1.1",
|
||||
"dayjs": "^1.11.1",
|
||||
"dayjs": "^1.11.3",
|
||||
"echarts": "^5.3.3",
|
||||
"form-data": "^4.0.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"naive-ui": "^2.28.2",
|
||||
"pinia": "^2.0.13",
|
||||
"naive-ui": "^2.30.4",
|
||||
"pinia": "^2.0.14",
|
||||
"print-js": "^1.6.0",
|
||||
"qs": "^6.10.3",
|
||||
"swiper": "^8.1.4",
|
||||
"qs": "^6.10.5",
|
||||
"swiper": "^8.2.4",
|
||||
"ua-parser-js": "^1.0.2",
|
||||
"vditor": "^3.8.13",
|
||||
"vue": "3.2.33",
|
||||
"vue-router": "^4.0.14",
|
||||
"vditor": "^3.8.15",
|
||||
"vue": "3.2.37",
|
||||
"vue-router": "^4.0.16",
|
||||
"wangeditor": "^4.7.15",
|
||||
"xgplayer": "^2.31.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@amap/amap-jsapi-types": "^0.0.8",
|
||||
"@commitlint/cli": "^16.2.4",
|
||||
"@commitlint/config-conventional": "^16.2.4",
|
||||
"@iconify/json": "^2.1.33",
|
||||
"@commitlint/cli": "^17.0.2",
|
||||
"@commitlint/config-conventional": "^17.0.2",
|
||||
"@iconify/json": "^2.1.62",
|
||||
"@iconify/vue": "^3.2.1",
|
||||
"@types/bmapgl": "^0.0.5",
|
||||
"@types/crypto-js": "^4.1.1",
|
||||
"@types/node": "^17.0.29",
|
||||
"@types/node": "^17.0.44",
|
||||
"@types/qs": "^6.9.7",
|
||||
"@types/ua-parser-js": "^0.7.36",
|
||||
"@typescript-eslint/eslint-plugin": "^5.21.0",
|
||||
"@typescript-eslint/parser": "^5.21.0",
|
||||
"@vitejs/plugin-vue": "^2.3.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.28.0",
|
||||
"@typescript-eslint/parser": "^5.28.0",
|
||||
"@vitejs/plugin-vue": "^2.3.3",
|
||||
"@vitejs/plugin-vue-jsx": "^1.3.10",
|
||||
"@vue/eslint-config-prettier": "^7.0.0",
|
||||
"@vue/eslint-config-typescript": "^10.0.0",
|
||||
"@vue/eslint-config-typescript": "^11.0.0",
|
||||
"commitizen": "^4.2.4",
|
||||
"cross-env": "^7.0.3",
|
||||
"cz-conventional-changelog": "^3.3.0",
|
||||
"cz-customizable": "^6.3.0",
|
||||
"eslint": "^8.14.0",
|
||||
"eslint": "^8.17.0",
|
||||
"eslint-config-airbnb-base": "^15.0.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-import-resolver-alias": "^1.1.2",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"eslint-plugin-vue": "^8.7.1",
|
||||
"husky": "^7.0.4",
|
||||
"lint-staged": "^12.4.1",
|
||||
"eslint-plugin-vue": "9.1.1",
|
||||
"husky": "^8.0.1",
|
||||
"lint-staged": "^13.0.1",
|
||||
"mockjs": "^1.1.0",
|
||||
"patch-package": "^6.4.7",
|
||||
"postinstall-postinstall": "^2.1.0",
|
||||
"prettier": "^2.6.2",
|
||||
"prettier": "^2.7.0",
|
||||
"rollup-plugin-visualizer": "^5.6.0",
|
||||
"sass": "^1.51.0",
|
||||
"standard-version": "^9.3.2",
|
||||
"typescript": "^4.6.3",
|
||||
"unocss": "^0.32.1",
|
||||
"unplugin-icons": "^0.14.1",
|
||||
"unplugin-vue-components": "0.19.3",
|
||||
"sass": "^1.52.3",
|
||||
"standard-version": "^9.5.0",
|
||||
"typescript": "^4.7.3",
|
||||
"unocss": "^0.39.0",
|
||||
"unplugin-icons": "^0.14.3",
|
||||
"unplugin-vue-components": "0.19.6",
|
||||
"unplugin-vue-define-options": "^0.6.1",
|
||||
"vite": "^2.9.6",
|
||||
"vite": "^2.9.12",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vite-plugin-html": "^3.2.0",
|
||||
"vite-plugin-mock": "^2.9.6",
|
||||
"vue-tsc": "^0.34.10",
|
||||
"vueuc": "^0.4.32"
|
||||
}
|
||||
"vite-plugin-svg-icons": "^2.0.1",
|
||||
"vue-eslint-parser": "^9.0.2",
|
||||
"vue-tsc": "^0.37.8"
|
||||
},
|
||||
"homepage": "https://github.com/honghuangdc/soybean-admin",
|
||||
"repository": {
|
||||
"url": "https://github.com/honghuangdc/soybean-admin.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/honghuangdc/soybean-admin/issues"
|
||||
},
|
||||
"license": "MIT"
|
||||
}
|
||||
|
3820
pnpm-lock.yaml
generated
3820
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -20,4 +20,5 @@ const theme = useThemeStore();
|
||||
|
||||
subscribeStore();
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
1
src/assets/svg/custom-icon.svg
Normal file
1
src/assets/svg/custom-icon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--mdi" width="32" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><path fill="currentColor" d="M19 10c0 1.38-2.12 2.5-3.5 2.5s-2.75-1.12-2.75-2.5h-1.5c0 1.38-1.37 2.5-2.75 2.5S5 11.38 5 10h-.75c-.16.64-.25 1.31-.25 2a8 8 0 0 0 8 8a8 8 0 0 0 8-8c0-.69-.09-1.36-.25-2H19m-7-6C9.04 4 6.45 5.61 5.07 8h13.86C17.55 5.61 14.96 4 12 4m10 8a10 10 0 0 1-10 10A10 10 0 0 1 2 12A10 10 0 0 1 12 2a10 10 0 0 1 10 10m-10 5.23c-1.75 0-3.29-.73-4.19-1.81L9.23 14c.45.72 1.52 1.23 2.77 1.23s2.32-.51 2.77-1.23l1.42 1.42c-.9 1.08-2.44 1.81-4.19 1.81Z"></path></svg>
|
After Width: | Height: | Size: 702 B |
@@ -91,4 +91,5 @@ onUnmounted(() => {
|
||||
stopHandle();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
@@ -44,4 +44,5 @@ function handleClickPolicy() {
|
||||
emit('click-policy');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
@@ -15,4 +15,5 @@ withDefaults(defineProps<Props>(), {
|
||||
inverted: false
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<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-white-balance-sunny v-else />
|
||||
</div>
|
||||
@@ -36,4 +36,5 @@ function handleSwitch() {
|
||||
darkMode.value = !darkMode.value;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
@@ -12,7 +12,6 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { RouterLink } from 'vue-router';
|
||||
import { routeName } from '@/router';
|
||||
|
||||
type ExceptionType = '403' | '404' | '500';
|
||||
@@ -26,4 +25,5 @@ defineProps<Props>();
|
||||
|
||||
const routeHomePath = routeName('root');
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
@@ -2,36 +2,44 @@
|
||||
<div v-if="showTooltip">
|
||||
<n-tooltip :placement="placement" trigger="hover">
|
||||
<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>
|
||||
</div>
|
||||
</template>
|
||||
{{ tooltipContent }}
|
||||
</n-tooltip>
|
||||
</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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import type { FollowerPlacement } from 'vueuc';
|
||||
import type { PopoverPlacement } from 'naive-ui';
|
||||
|
||||
interface Props {
|
||||
/** tooltip显示文本 */
|
||||
tooltipContent?: string;
|
||||
/** tooltip的位置 */
|
||||
placement?: FollowerPlacement;
|
||||
placement?: PopoverPlacement;
|
||||
/** class类 */
|
||||
contentClass?: string;
|
||||
/** 反转模式下 */
|
||||
inverted?: boolean;
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
tooltipContent: '',
|
||||
placement: 'bottom',
|
||||
contentClass: ''
|
||||
contentClass: '',
|
||||
inverted: false
|
||||
});
|
||||
|
||||
const showTooltip = computed(() => Boolean(props.tooltipContent));
|
||||
|
||||
const contentClassName = computed(
|
||||
() => `${props.contentClass} ${props.inverted ? 'hover:bg-primary' : 'hover:bg-[#f6f6f6]'}`
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
@@ -13,4 +13,5 @@ withDefaults(defineProps<Props>(), {
|
||||
fill: false
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
@@ -44,4 +44,5 @@ onMounted(() => {
|
||||
|
||||
defineExpose({ instance });
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
@@ -106,3 +106,5 @@ onMounted(() => {
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
@@ -66,6 +66,7 @@ function handleChange(iconItem: string) {
|
||||
modelValue.value = iconItem;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.n-input-wrapper) {
|
||||
padding-right: 0;
|
||||
|
@@ -36,4 +36,5 @@ watch(imgCode, newValue => {
|
||||
|
||||
defineExpose({ getImgCode });
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
24
src/components/custom/SvgIcon.vue
Normal file
24
src/components/custom/SvgIcon.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<svg aria-hidden="true" width="1em" height="1em" class="inline-block">
|
||||
<use :xlink:href="symbolId" fill="currentColor" />
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
interface Props {
|
||||
/** 前缀 */
|
||||
prefix?: string;
|
||||
/** 图标名称(图片的文件名) */
|
||||
icon: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
prefix: 'icon-custom'
|
||||
});
|
||||
|
||||
const symbolId = computed(() => `#${props.prefix}-${props.icon}`);
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
@@ -17,4 +17,5 @@ interface Props {
|
||||
|
||||
defineProps<Props>();
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
163
src/composables/echarts.ts
Normal file
163
src/composables/echarts.ts
Normal 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
|
||||
};
|
||||
}
|
@@ -1,3 +1,4 @@
|
||||
export * from './system';
|
||||
export * from './router';
|
||||
export * from './layout';
|
||||
export * from './echarts';
|
||||
|
@@ -1,13 +1,13 @@
|
||||
import { computed } from 'vue';
|
||||
import { useAppStore, useThemeStore } from '@/store';
|
||||
|
||||
type LayoutMode = 'vertical' | 'horizontal';
|
||||
type LayoutHeaderProps = Record<EnumType.ThemeLayoutMode, GlobalHeaderProps>;
|
||||
|
||||
export function useBasicLayout() {
|
||||
const app = useAppStore();
|
||||
const theme = useThemeStore();
|
||||
|
||||
type LayoutMode = 'vertical' | 'horizontal';
|
||||
const mode = computed(() => {
|
||||
const vertical: LayoutMode = 'vertical';
|
||||
const horizontal: LayoutMode = 'horizontal';
|
||||
|
@@ -3,7 +3,6 @@ export enum EnumUserRole {
|
||||
super = '超级管理员',
|
||||
admin = '管理员',
|
||||
user = '普通用户'
|
||||
// custom = '自定义角色'
|
||||
}
|
||||
|
||||
/** 登录模块 */
|
||||
|
@@ -16,7 +16,7 @@ export enum EnumStorageKey {
|
||||
/** 用户信息 */
|
||||
'user-info' = '__USER_INFO__',
|
||||
/** 多页签路由信息 */
|
||||
'tab-routes' = '__TAB_ROUTES__'
|
||||
'multi-tab-routes' = '__MULTI_TAB_ROUTES__'
|
||||
}
|
||||
|
||||
/** 数据类型 */
|
||||
@@ -31,5 +31,6 @@ export enum EnumDataType {
|
||||
date = '[object Date]',
|
||||
regexp = '[object RegExp]',
|
||||
set = '[object Set]',
|
||||
map = '[object Map]'
|
||||
map = '[object Map]',
|
||||
file = '[object File]'
|
||||
}
|
||||
|
@@ -52,6 +52,7 @@ function draw(dom: HTMLCanvasElement, width: number, height: number) {
|
||||
|
||||
ctx.fillStyle = randomColor(180, 230);
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
|
||||
for (let i = 0; i < 4; i += 1) {
|
||||
const text = NUMBER_STRING[randomNum(0, NUMBER_STRING.length)];
|
||||
imgCode += text;
|
||||
@@ -81,5 +82,6 @@ function draw(dom: HTMLCanvasElement, width: number, height: number) {
|
||||
ctx.fillStyle = randomColor(150, 200);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
return imgCode;
|
||||
}
|
||||
|
@@ -7,6 +7,7 @@ import useCountDown from './useCountDown';
|
||||
export default function useSmsCode() {
|
||||
const { loading, startLoading, endLoading } = useLoading();
|
||||
const { counts, start, isCounting } = useCountDown(60);
|
||||
|
||||
const initLabel = '获取验证码';
|
||||
const countingLabel = (second: number) => `${second}秒后重新获取`;
|
||||
const label = computed(() => {
|
||||
@@ -40,6 +41,7 @@ export default function useSmsCode() {
|
||||
async function getSmsCode(phone: string) {
|
||||
const valid = isPhoneValid(phone);
|
||||
if (!valid || loading.value) return;
|
||||
|
||||
startLoading();
|
||||
const { data } = await fetchSmsCode(phone);
|
||||
if (data) {
|
||||
|
@@ -3,7 +3,5 @@ import useBoolean from './useBoolean';
|
||||
import useLoading from './useLoading';
|
||||
import useLoadingEmpty from './useLoadingEmpty';
|
||||
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 };
|
||||
|
@@ -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
|
||||
};
|
||||
}
|
@@ -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
|
||||
};
|
||||
}
|
@@ -13,9 +13,12 @@ export default function useReload() {
|
||||
async function handleReload(duration = 0) {
|
||||
setFalse();
|
||||
await nextTick();
|
||||
setTimeout(() => {
|
||||
setTrue();
|
||||
}, duration);
|
||||
|
||||
if (duration > 0) {
|
||||
setTimeout(() => {
|
||||
setTrue();
|
||||
}, duration);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
@@ -10,6 +10,7 @@
|
||||
:sider-width="siderWidth"
|
||||
:sider-collapsed-width="siderCollapsedWidth"
|
||||
:sider-collapse="app.siderCollapse"
|
||||
:add-main-overflow-hidden="addMainOverflowHidden"
|
||||
:fixed-footer="theme.footer.fixed"
|
||||
>
|
||||
<template #header>
|
||||
@@ -21,7 +22,7 @@
|
||||
<template #sider>
|
||||
<global-sider />
|
||||
</template>
|
||||
<global-content />
|
||||
<global-content @hide-main-overflow="setAddMainOverflowHidden" />
|
||||
<template #footer>
|
||||
<global-footer />
|
||||
</template>
|
||||
@@ -33,11 +34,15 @@
|
||||
import AdminLayout from '@soybeanjs/vue-admin-layout';
|
||||
import { useAppStore, useThemeStore } from '@/store';
|
||||
import { useBasicLayout } from '@/composables';
|
||||
import { useBoolean } from '@/hooks';
|
||||
import { SettingDrawer, GlobalHeader, GlobalTab, GlobalSider, GlobalContent, GlobalFooter } from '../common';
|
||||
|
||||
const app = useAppStore();
|
||||
const theme = useThemeStore();
|
||||
|
||||
const { mode, headerProps, siderVisible, siderWidth, siderCollapsedWidth } = useBasicLayout();
|
||||
|
||||
const { bool: addMainOverflowHidden, setBool: setAddMainOverflowHidden } = useBoolean();
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
@@ -5,4 +5,5 @@
|
||||
<script setup lang="ts">
|
||||
import { GlobalContent } from '../common';
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
@@ -4,9 +4,15 @@
|
||||
class="h-full bg-[#f6f9f8] dark:bg-[#101014] transition duration-300 ease-in-out"
|
||||
>
|
||||
<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">
|
||||
<component :is="Component" v-if="app.reloadFlag" :key="route.path" />
|
||||
<component :is="Component" v-if="app.reloadFlag" :key="route.fullPath" />
|
||||
</keep-alive>
|
||||
</transition>
|
||||
</router-view>
|
||||
@@ -14,7 +20,6 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { RouterView } from 'vue-router';
|
||||
import { useAppStore, useThemeStore, useRouteStore } from '@/store';
|
||||
|
||||
interface Props {
|
||||
@@ -22,12 +27,27 @@ interface Props {
|
||||
showPadding?: boolean;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
/** 禁止主体溢出 */
|
||||
(e: 'hide-main-overflow', hidden: boolean): void;
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
showPadding: true
|
||||
});
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const app = useAppStore();
|
||||
const theme = useThemeStore();
|
||||
const routeStore = useRouteStore();
|
||||
|
||||
function handleBeforeLeave() {
|
||||
emit('hide-main-overflow', true);
|
||||
}
|
||||
function handleAfterEnter() {
|
||||
emit('hide-main-overflow', false);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
@@ -5,4 +5,5 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<style scoped></style>
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<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 v-else class="text-18px" />
|
||||
</hover-container>
|
||||
@@ -7,7 +7,10 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useFullscreen } from '@vueuse/core';
|
||||
import { useThemeStore } from '@/store';
|
||||
|
||||
const { isFullscreen, toggle } = useFullscreen();
|
||||
const theme = useThemeStore();
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
@@ -2,7 +2,7 @@
|
||||
<hover-container
|
||||
tooltip-content="github"
|
||||
class="w-40px h-full"
|
||||
content-class="hover:text-primary"
|
||||
:inverted="theme.header.inverted"
|
||||
@click="handleClickLink"
|
||||
>
|
||||
<icon-mdi-github class="text-20px" />
|
||||
@@ -10,8 +10,12 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useThemeStore } from '@/store';
|
||||
|
||||
const theme = useThemeStore();
|
||||
function handleClickLink() {
|
||||
window.open('https://github.com/honghuangdc/soybean-admin', '_blank');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
@@ -17,8 +17,9 @@
|
||||
:is="breadcrumb.icon"
|
||||
v-if="theme.header.crumb.showIcon"
|
||||
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>
|
||||
</n-breadcrumb-item>
|
||||
</template>
|
||||
@@ -46,4 +47,5 @@ function dropdownSelect(key: string) {
|
||||
routerPush({ name: key });
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
@@ -1,11 +1,17 @@
|
||||
<template>
|
||||
<n-menu
|
||||
:value="activeKey"
|
||||
mode="horizontal"
|
||||
:options="menus"
|
||||
:inverted="theme.header.inverted"
|
||||
@update:value="handleUpdateMenu"
|
||||
/>
|
||||
<div class="flex-1-hidden h-full px-10px">
|
||||
<n-scrollbar :x-scrollable="true" class="flex-1-hidden h-full" content-class="h-full">
|
||||
<div class="flex-y-center h-full" :style="{ justifyContent: theme.menu.horizontalPosition }">
|
||||
<n-menu
|
||||
:value="activeKey"
|
||||
mode="horizontal"
|
||||
:options="menus"
|
||||
:inverted="theme.header.inverted"
|
||||
@update:value="handleUpdateMenu"
|
||||
/>
|
||||
</div>
|
||||
</n-scrollbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -21,11 +27,16 @@ const theme = useThemeStore();
|
||||
const { routerPush } = useRouterPush();
|
||||
|
||||
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) {
|
||||
const menuItem = item as GlobalMenuOption;
|
||||
routerPush(menuItem.routePath);
|
||||
}
|
||||
</script>
|
||||
<style scoped></style>
|
||||
|
||||
<style scoped>
|
||||
:deep(.n-menu-item-content-header) {
|
||||
overflow: inherit !important;
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,13 +1,15 @@
|
||||
<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-fold-left v-else class="text-16px" />
|
||||
</hover-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useAppStore } from '@/store';
|
||||
import { useAppStore, useThemeStore } from '@/store';
|
||||
|
||||
const app = useAppStore();
|
||||
const theme = useThemeStore();
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<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" />
|
||||
</hover-container>
|
||||
</template>
|
||||
@@ -9,4 +9,5 @@ import { useThemeStore } from '@/store';
|
||||
|
||||
const theme = useThemeStore();
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<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" />
|
||||
<span class="pl-8px text-16px font-medium">{{ auth.userInfo.userName }}</span>
|
||||
</hover-container>
|
||||
@@ -8,12 +8,13 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useAuthStore } from '@/store';
|
||||
import { useAuthStore, useThemeStore } from '@/store';
|
||||
import { iconifyRender } from '@/utils';
|
||||
|
||||
type DropdownKey = 'user-center' | 'logout';
|
||||
|
||||
const auth = useAuthStore();
|
||||
const theme = useThemeStore();
|
||||
|
||||
const options = [
|
||||
{
|
||||
@@ -47,4 +48,5 @@ function handleDropdown(optionKey: string) {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
@@ -5,9 +5,7 @@
|
||||
<menu-collapse v-if="showMenuCollapse" />
|
||||
<global-breadcrumb v-if="theme.header.crumb.visible" />
|
||||
</div>
|
||||
<div v-else class="flex-1-hidden flex-y-center h-full" :style="{ justifyContent: theme.menu.horizontalPosition }">
|
||||
<header-menu />
|
||||
</div>
|
||||
<header-menu v-else />
|
||||
<div class="flex justify-end h-full">
|
||||
<global-search />
|
||||
<github-site />
|
||||
@@ -45,6 +43,7 @@ defineProps<Props>();
|
||||
|
||||
const theme = useThemeStore();
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.global-header {
|
||||
box-shadow: 0 1px 2px rgb(0 21 41 / 8%);
|
||||
|
@@ -8,7 +8,6 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { RouterLink } from 'vue-router';
|
||||
import { routePath } from '@/router';
|
||||
import { useAppInfo } from '@/composables';
|
||||
|
||||
@@ -22,4 +21,5 @@ defineProps<Props>();
|
||||
const { title } = useAppInfo();
|
||||
const routeHomePath = routePath('root');
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
@@ -17,6 +17,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup></script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.icon {
|
||||
box-shadow: inset 0 -2px #cdcde6, inset 0 0 1px 1px #fff, 0 1px 2px 1px #1e235a66;
|
||||
|
@@ -134,4 +134,5 @@ onKeyStroke('Enter', handleEnter);
|
||||
onKeyStroke('ArrowUp', handleUp);
|
||||
onKeyStroke('ArrowDown', handleDown);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
@@ -59,4 +59,5 @@ function handleTo() {
|
||||
emit('enter');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
@@ -3,7 +3,7 @@
|
||||
<hover-container
|
||||
class="w-40px h-full"
|
||||
tooltip-content="搜索"
|
||||
content-class="hover:text-primary"
|
||||
:inverted="theme.header.inverted"
|
||||
@click="handleSearch"
|
||||
>
|
||||
<icon-uil-search class="text-20px" />
|
||||
@@ -13,13 +13,16 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useThemeStore } from '@/store';
|
||||
import { useBoolean } from '@/hooks';
|
||||
import { SearchModal } from './components';
|
||||
|
||||
const { bool: show, toggle } = useBoolean();
|
||||
const theme = useThemeStore();
|
||||
|
||||
function handleSearch() {
|
||||
toggle();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
@@ -10,4 +10,5 @@ import { useAppStore } from '@/store';
|
||||
|
||||
const app = useAppStore();
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
@@ -42,4 +42,5 @@ const { bool: isHover, setTrue, setFalse } = useBoolean();
|
||||
|
||||
const isActive = computed(() => props.routeName === props.activeRouteName);
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
@@ -53,7 +53,7 @@ const { title } = useAppInfo();
|
||||
|
||||
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[]>([]);
|
||||
|
||||
function handleUpdateMenu(_key: string, item: MenuOption) {
|
||||
@@ -73,6 +73,7 @@ watch(
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.drawer-shadow {
|
||||
box-shadow: 2px 0 8px 0 rgb(29 35 41 / 5%);
|
||||
|
@@ -58,7 +58,7 @@ const firstDegreeMenus = computed(() =>
|
||||
|
||||
function getActiveParentRouteName() {
|
||||
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);
|
||||
if (flag) {
|
||||
setActiveParentRouteName(item.routeName);
|
||||
@@ -101,4 +101,5 @@ watch(
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
@@ -29,7 +29,7 @@ const theme = useThemeStore();
|
||||
const routeStore = useRouteStore();
|
||||
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[]>([]);
|
||||
|
||||
function handleUpdateMenu(_key: string, item: MenuOption) {
|
||||
@@ -49,4 +49,5 @@ watch(
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
@@ -17,4 +17,5 @@ const theme = useThemeStore();
|
||||
const isHorizontalMix = computed(() => theme.layout.mode === 'horizontal-mix');
|
||||
const showTitle = computed(() => !app.siderCollapse && theme.layout.mode !== 'vertical-mix');
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
@@ -12,6 +12,7 @@ const theme = useThemeStore();
|
||||
|
||||
const isVerticalMix = computed(() => theme.layout.mode === 'vertical-mix');
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.global-sider {
|
||||
box-shadow: 2px 0 8px 0 rgb(29 35 41 / 5%);
|
||||
|
@@ -19,4 +19,5 @@ function handleRefresh() {
|
||||
}, 1000);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
@@ -69,7 +69,7 @@ const options = computed<Option[]>(() => [
|
||||
{
|
||||
label: '关闭',
|
||||
key: 'close-current',
|
||||
disabled: props.currentPath === tab.homeTab.path,
|
||||
disabled: props.currentPath === tab.homeTab.fullPath,
|
||||
icon: iconifyRender('ant-design:close-outlined')
|
||||
},
|
||||
{
|
||||
@@ -131,4 +131,5 @@ function handleDropdown(optionKey: string) {
|
||||
hide();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
@@ -3,15 +3,15 @@
|
||||
<component
|
||||
:is="activeComponent"
|
||||
v-for="(item, index) in tab.tabs"
|
||||
:key="item.path"
|
||||
:is-active="tab.activeTab === item.path"
|
||||
:key="item.fullPath"
|
||||
:is-active="tab.activeTab === item.fullPath"
|
||||
:primary-color="theme.themeColor"
|
||||
:closable="item.path !== tab.homeTab.path"
|
||||
:closable="item.name !== tab.homeTab.name"
|
||||
:dark-mode="theme.darkMode"
|
||||
:class="{ '!mr-0': isChromeMode && index === tab.tabs.length - 1, 'mr-10px': !isChromeMode }"
|
||||
@click="tab.handleClickTab(item.path)"
|
||||
@close="tab.removeTab(item.path)"
|
||||
@contextmenu="handleContextMenu($event, item.path)"
|
||||
@click="tab.handleClickTab(item.fullPath)"
|
||||
@close="tab.removeTab(item.fullPath)"
|
||||
@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" />
|
||||
{{ 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();
|
||||
const { clientX, clientY } = e;
|
||||
hideDropdown();
|
||||
setDropdown(clientX, clientY, path);
|
||||
setDropdown(clientX, clientY, fullPath);
|
||||
await nextTick();
|
||||
showDropdown();
|
||||
}
|
||||
@@ -101,4 +101,5 @@ useEventListener(window, 'beforeunload', () => {
|
||||
setTabRoutes(tab.tabs);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
@@ -45,16 +45,17 @@ function init() {
|
||||
}
|
||||
|
||||
watch(
|
||||
() => route.path,
|
||||
() => route.fullPath,
|
||||
() => {
|
||||
tab.addTab(route);
|
||||
tab.setActiveTab(route.path);
|
||||
tab.setActiveTab(route.fullPath);
|
||||
}
|
||||
);
|
||||
|
||||
// 初始化
|
||||
init();
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.global-tab {
|
||||
box-shadow: 0 1px 2px rgb(0 21 41 / 8%);
|
||||
|
@@ -1,19 +1,32 @@
|
||||
<template>
|
||||
<n-divider title-placement="center">深色主题</n-divider>
|
||||
<div class="flex-center">
|
||||
<n-switch :value="theme.darkMode" @update:value="theme.setDarkMode">
|
||||
<template #checked>
|
||||
<icon-mdi-white-balance-sunny class="text-14px text-primary" />
|
||||
</template>
|
||||
<template #unchecked>
|
||||
<icon-mdi-moon-waning-crescent class="text-14px text-primary" />
|
||||
</template>
|
||||
</n-switch>
|
||||
</div>
|
||||
<n-divider title-placement="center">主题模式</n-divider>
|
||||
<n-space vertical size="large">
|
||||
<setting-menu label="深色主题">
|
||||
<n-switch :value="theme.darkMode" @update:value="theme.setDarkMode">
|
||||
<template #checked>
|
||||
<icon-mdi-white-balance-sunny class="text-14px text-primary" />
|
||||
</template>
|
||||
<template #unchecked>
|
||||
<icon-mdi-moon-waning-crescent class="text-14px text-primary" />
|
||||
</template>
|
||||
</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>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useThemeStore } from '@/store';
|
||||
import SettingMenu from '../SettingMenu/index.vue';
|
||||
|
||||
const theme = useThemeStore();
|
||||
</script>
|
||||
|
@@ -15,4 +15,5 @@ import { useAppStore } from '@/store';
|
||||
|
||||
const app = useAppStore();
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
@@ -17,7 +17,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import type { FollowerPlacement } from 'vueuc';
|
||||
import type { PopoverPlacement } from 'naive-ui';
|
||||
import type { EnumThemeLayoutMode } from '@/enum';
|
||||
|
||||
interface Props {
|
||||
@@ -34,7 +34,7 @@ const props = defineProps<Props>();
|
||||
type LayoutConfig = Record<
|
||||
EnumType.ThemeLayoutMode,
|
||||
{
|
||||
placement: FollowerPlacement;
|
||||
placement: PopoverPlacement;
|
||||
menuClass: string;
|
||||
mainClass: string;
|
||||
}
|
||||
@@ -65,6 +65,7 @@ const layoutConfig: LayoutConfig = {
|
||||
|
||||
const activeConfig = computed(() => layoutConfig[props.mode]);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.layout-checkbox__shadow {
|
||||
box-shadow: 0 1px 2.5px rgba(0, 0, 0, 0.18);
|
||||
|
@@ -18,4 +18,5 @@ import { LayoutCheckbox } from './components';
|
||||
|
||||
const theme = useThemeStore();
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
@@ -70,4 +70,5 @@ import SettingMenu from '../SettingMenu/index.vue';
|
||||
|
||||
const theme = useThemeStore();
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
@@ -40,4 +40,5 @@ import SettingMenu from '../SettingMenu/index.vue';
|
||||
|
||||
const theme = useThemeStore();
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
@@ -13,4 +13,5 @@ interface Props {
|
||||
|
||||
defineProps<Props>();
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
@@ -23,4 +23,5 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
const whiteColors = ['#ffffff', '#fff', 'rgb(255,255,255)'];
|
||||
const isWhite = computed(() => whiteColors.includes(props.color));
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
@@ -45,4 +45,5 @@ function handleClose() {
|
||||
emit('close');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
@@ -26,4 +26,5 @@ const { bool: visible, setTrue: openModal, setFalse: closeModal } = useBoolean()
|
||||
const isInOther = computed(() => isInTraditionColors(theme.themeColor));
|
||||
const otherColorBtnType = computed(() => (isInOther.value ? 'primary' : 'default'));
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
@@ -56,4 +56,5 @@ onUnmounted(() => {
|
||||
stopHandle();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
@@ -18,4 +18,5 @@ import { DrawerButton, DarkMode, LayoutMode, ThemeColorSelect, PageFunc, PageVie
|
||||
|
||||
const app = useAppStore();
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
@@ -2,6 +2,7 @@ import 'uno.css';
|
||||
import 'swiper/css';
|
||||
import 'swiper/css/navigation';
|
||||
import 'swiper/css/pagination';
|
||||
import 'virtual:svg-icons-register';
|
||||
import '../styles/css/global.css';
|
||||
|
||||
/** import static assets: css, js , font and so on. - [引入静态资源,css、js和字体文件等] */
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import type { Router, RouteLocationNormalized, NavigationGuardNext } from 'vue-router';
|
||||
import type { RouteLocationNormalized, NavigationGuardNext } from 'vue-router';
|
||||
import { routeName } from '@/router';
|
||||
import { useRouteStore } from '@/store';
|
||||
import { getToken } from '@/utils';
|
||||
@@ -9,8 +9,7 @@ import { getToken } from '@/utils';
|
||||
export async function createDynamicRouteGuard(
|
||||
to: RouteLocationNormalized,
|
||||
_from: RouteLocationNormalized,
|
||||
next: NavigationGuardNext,
|
||||
router: Router
|
||||
next: NavigationGuardNext
|
||||
) {
|
||||
const route = useRouteStore();
|
||||
const isLogin = Boolean(getToken());
|
||||
@@ -28,11 +27,14 @@ export async function createDynamicRouteGuard(
|
||||
return false;
|
||||
}
|
||||
|
||||
await route.initAuthRoute(router);
|
||||
await route.initAuthRoute();
|
||||
|
||||
if (to.name === routeName('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;
|
||||
}
|
||||
}
|
||||
|
@@ -11,7 +11,7 @@ export function createRouterGuard(router: Router) {
|
||||
// 开始 loadingBar
|
||||
window.$loadingBar?.start();
|
||||
// 页面跳转权限处理
|
||||
await createPermissionGuard(to, from, next, router);
|
||||
await createPermissionGuard(to, from, next);
|
||||
});
|
||||
router.afterEach(to => {
|
||||
// 设置document title
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import type { Router, RouteLocationNormalized, NavigationGuardNext } from 'vue-router';
|
||||
import type { RouteLocationNormalized, NavigationGuardNext } from 'vue-router';
|
||||
import { routeName } from '@/router';
|
||||
import { useAuthStore } from '@/store';
|
||||
import { exeStrategyActions, getToken } from '@/utils';
|
||||
@@ -8,11 +8,10 @@ import { createDynamicRouteGuard } from './dynamic';
|
||||
export async function createPermissionGuard(
|
||||
to: RouteLocationNormalized,
|
||||
from: RouteLocationNormalized,
|
||||
next: NavigationGuardNext,
|
||||
router: Router
|
||||
next: NavigationGuardNext
|
||||
) {
|
||||
// 动态路由
|
||||
const permission = await createDynamicRouteGuard(to, from, next, router);
|
||||
const permission = await createDynamicRouteGuard(to, from, next);
|
||||
if (!permission) return;
|
||||
|
||||
// 外链路由, 从新标签打开,返回上一个路由
|
||||
|
@@ -6,10 +6,13 @@ export const scrollBehavior: RouterScrollBehavior = (to, from) => {
|
||||
const tab = useTabStore();
|
||||
|
||||
if (to.hash) {
|
||||
resolve({
|
||||
el: to.hash,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
const el = document.querySelector(to.hash);
|
||||
if (el) {
|
||||
resolve({
|
||||
el,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const { left, top } = tab.getTabScrollPosition(to.path);
|
||||
|
@@ -8,7 +8,7 @@ const about: AuthRoute.Route = {
|
||||
singleLayout: 'basic',
|
||||
permissions: ['super', 'admin', 'user'],
|
||||
icon: 'fluent:book-information-24-regular',
|
||||
order: 8
|
||||
order: 9
|
||||
}
|
||||
};
|
||||
|
||||
|
@@ -37,7 +37,7 @@ const exception: AuthRoute.Route = {
|
||||
meta: {
|
||||
title: '异常页',
|
||||
icon: 'ant-design:exception-outlined',
|
||||
order: 6
|
||||
order: 7
|
||||
}
|
||||
};
|
||||
|
||||
|
49
src/router/modules/function.ts
Normal file
49
src/router/modules/function.ts
Normal 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;
|
@@ -49,7 +49,7 @@ const multiMenu: AuthRoute.Route = {
|
||||
meta: {
|
||||
title: '多级菜单',
|
||||
icon: 'carbon:menu',
|
||||
order: 6
|
||||
order: 8
|
||||
}
|
||||
};
|
||||
|
||||
|
@@ -3,6 +3,47 @@ const plugin: AuthRoute.Route = {
|
||||
path: '/plugin',
|
||||
component: 'basic',
|
||||
children: [
|
||||
{
|
||||
name: 'plugin_charts',
|
||||
path: '/plugin/charts',
|
||||
component: 'multi',
|
||||
children: [
|
||||
{
|
||||
name: 'plugin_charts_echarts',
|
||||
path: '/plugin/charts/echarts',
|
||||
component: 'self',
|
||||
meta: {
|
||||
title: 'ECharts',
|
||||
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',
|
||||
path: '/plugin/map',
|
||||
|
@@ -1,15 +1,18 @@
|
||||
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[] = [
|
||||
{
|
||||
name: 'root',
|
||||
path: '/',
|
||||
redirect: import.meta.env.VITE_ROUTE_HOME_PATH,
|
||||
meta: {
|
||||
title: 'Root'
|
||||
}
|
||||
},
|
||||
ROOT_ROUTE,
|
||||
{
|
||||
name: 'login',
|
||||
path: '/login',
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { createRequest } from './request';
|
||||
import { getEnvConfig } from '~/.env-config';
|
||||
import { createRequest } from './request';
|
||||
|
||||
const envConfig = getEnvConfig(import.meta.env);
|
||||
const isHttpProxy = import.meta.env.VITE_HTTP_PROXY === 'true';
|
||||
|
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"darkMode": false,
|
||||
"followSystemTheme": true,
|
||||
"layout": {
|
||||
"minWidth": 900,
|
||||
"mode": "vertical",
|
||||
|
@@ -30,6 +30,7 @@ const themeColorList = [
|
||||
|
||||
const defaultThemeSetting: Theme.Setting = {
|
||||
darkMode: false,
|
||||
followSystemTheme: true,
|
||||
layout: {
|
||||
minWidth: 900,
|
||||
mode: 'vertical',
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { unref, nextTick } from 'vue';
|
||||
import { unref } from 'vue';
|
||||
import { defineStore } from 'pinia';
|
||||
import { router as globalRouter } from '@/router';
|
||||
import { router } from '@/router';
|
||||
import { useRouterPush } from '@/composables';
|
||||
import { fetchLogin, fetchUserInfo } from '@/service';
|
||||
import { getUserInfo, getToken, setUserInfo, setToken, setRefreshToken, clearAuthStorage } from '@/utils';
|
||||
@@ -34,26 +34,50 @@ export const useAuthStore = defineStore('auth-store', {
|
||||
const { toLogin } = useRouterPush(false);
|
||||
const { resetTabStore } = useTabStore();
|
||||
const { resetRouteStore } = useRouteStore();
|
||||
const route = unref(globalRouter.currentRoute);
|
||||
const route = unref(router.currentRoute);
|
||||
|
||||
clearAuthStorage();
|
||||
this.$reset();
|
||||
|
||||
resetTabStore();
|
||||
resetRouteStore();
|
||||
|
||||
if (route.meta.requiresAuth) {
|
||||
toLogin();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 处理登录后成功或失败的逻辑
|
||||
* @param backendToken - 返回的token
|
||||
*/
|
||||
async handleActionAfterLogin(backendToken: ApiAuth.Token) {
|
||||
const { toLoginRedirect } = useRouterPush(false);
|
||||
|
||||
nextTick(() => {
|
||||
resetTabStore();
|
||||
resetRouteStore();
|
||||
});
|
||||
const loginSuccess = await this.loginByToken(backendToken);
|
||||
|
||||
if (loginSuccess) {
|
||||
// 跳转登录后的地址
|
||||
toLoginRedirect();
|
||||
|
||||
// 登录成功弹出欢迎提示
|
||||
window.$notification?.success({
|
||||
title: '登录成功!',
|
||||
content: `欢迎回来,${this.userInfo.userName}!`,
|
||||
duration: 3000
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// 不成功则重置状态
|
||||
this.resetAuthStore();
|
||||
},
|
||||
/**
|
||||
* 根据token进行登录
|
||||
* @param backendToken - 返回的token
|
||||
*/
|
||||
async loginByToken(backendToken: ApiAuth.Token) {
|
||||
const { toLoginRedirect } = useRouterPush(false);
|
||||
let successFlag = false;
|
||||
|
||||
// 先把token存储到缓存中(后面接口的请求头需要token)
|
||||
const { token, refreshToken } = backendToken;
|
||||
@@ -67,21 +91,13 @@ export const useAuthStore = defineStore('auth-store', {
|
||||
setUserInfo(data);
|
||||
|
||||
// 更新状态
|
||||
Object.assign(this, { userInfo: data, token });
|
||||
this.userInfo = data;
|
||||
this.token = token;
|
||||
|
||||
// 跳转登录后的地址
|
||||
toLoginRedirect();
|
||||
|
||||
// 登录成功弹出欢迎提示
|
||||
window.$notification?.success({
|
||||
title: '登录成功!',
|
||||
content: `欢迎回来,${data.userName}!`,
|
||||
duration: 3000
|
||||
});
|
||||
} else {
|
||||
// 不成功则重置状态
|
||||
this.resetAuthStore();
|
||||
successFlag = true;
|
||||
}
|
||||
|
||||
return successFlag;
|
||||
},
|
||||
/**
|
||||
* 登录
|
||||
@@ -92,12 +108,38 @@ export const useAuthStore = defineStore('auth-store', {
|
||||
this.loginLoading = true;
|
||||
const { data } = await fetchLogin(userName, password);
|
||||
if (data) {
|
||||
await this.loginByToken(data);
|
||||
await this.handleActionAfterLogin(data);
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@@ -1,15 +1,17 @@
|
||||
import type { Router } from 'vue-router';
|
||||
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 {
|
||||
getUserInfo,
|
||||
transformAuthRouteToMenu,
|
||||
transformAuthRoutesToVueRoutes,
|
||||
transformAuthRouteToVueRoute,
|
||||
transformAuthRoutesToSearchMenus,
|
||||
getCacheRoutes,
|
||||
filterAuthRoutesByUserPermission,
|
||||
transformRoutePathToRouteName
|
||||
transformRoutePathToRouteName,
|
||||
transformRouteNameToRoutePath,
|
||||
getConstantRouteNames
|
||||
} from '@/utils';
|
||||
import { useAuthStore } from '../auth';
|
||||
import { useTabStore } from '../tab';
|
||||
@@ -43,15 +45,27 @@ export const useRouteStore = defineStore('route-store', {
|
||||
cacheRoutes: []
|
||||
}),
|
||||
actions: {
|
||||
/** 重置路由的store */
|
||||
resetRouteStore() {
|
||||
this.resetRoutes();
|
||||
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 router - 路由实例
|
||||
*/
|
||||
handleAuthRoutes(routes: AuthRoute.Route[], router: Router) {
|
||||
handleAuthRoutes(routes: AuthRoute.Route[]) {
|
||||
this.menus = transformAuthRouteToMenu(routes);
|
||||
this.searchMenus = transformAuthRoutesToSearchMenus(routes);
|
||||
|
||||
@@ -63,32 +77,35 @@ export const useRouteStore = defineStore('route-store', {
|
||||
|
||||
this.cacheRoutes = getCacheRoutes(vueRoutes);
|
||||
},
|
||||
/**
|
||||
* 初始化动态路由
|
||||
* @param router - 路由实例
|
||||
*/
|
||||
async initDynamicRoute(router: Router) {
|
||||
/** 动态路由模式下:更新根路由的重定向 */
|
||||
handleUpdateRootRedirect(routeKey: AuthRoute.RouteKey) {
|
||||
if (routeKey === 'root' || routeKey === 'not-found-page') {
|
||||
throw Error('routeKey的值不能为root或者not-found-page');
|
||||
}
|
||||
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 { data } = await fetchUserRoutes(userId);
|
||||
if (data) {
|
||||
this.routeHomeName = data.home;
|
||||
this.handleAuthRoutes(data.routes, router);
|
||||
this.handleUpdateRootRedirect(data.home);
|
||||
this.handleAuthRoutes(data.routes);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 初始化静态路由
|
||||
* @param router - 路由实例
|
||||
*/
|
||||
async initStaticRoute(router: Router) {
|
||||
/** 初始化静态路由 */
|
||||
async initStaticRoute() {
|
||||
const auth = useAuthStore();
|
||||
const routes = filterAuthRoutesByUserPermission(staticRoutes, auth.userInfo.userRole);
|
||||
this.handleAuthRoutes(routes, router);
|
||||
this.handleAuthRoutes(routes);
|
||||
},
|
||||
/**
|
||||
* 初始化权限路由
|
||||
* @param router - 路由实例
|
||||
*/
|
||||
async initAuthRoute(router: Router) {
|
||||
/** 初始化权限路由 */
|
||||
async initAuthRoute() {
|
||||
const { initHomeTab } = useTabStore();
|
||||
const { userId } = getUserInfo();
|
||||
|
||||
@@ -96,9 +113,9 @@ export const useRouteStore = defineStore('route-store', {
|
||||
|
||||
const isDynamicRoute = this.authRouteMode === 'dynamic';
|
||||
if (isDynamicRoute) {
|
||||
await this.initDynamicRoute(router);
|
||||
await this.initDynamicRoute();
|
||||
} else {
|
||||
await this.initStaticRoute(router);
|
||||
await this.initStaticRoute();
|
||||
}
|
||||
|
||||
initHomeTab(this.routeHomeName, router);
|
||||
|
@@ -1,13 +1,15 @@
|
||||
import type { RouteRecordNormalized, RouteLocationNormalizedLoaded } from 'vue-router';
|
||||
|
||||
/**
|
||||
* 根据vue路由获取tab路由
|
||||
* 根据vue路由获取tab路由
|
||||
* @param route
|
||||
*/
|
||||
export function getTabRouteByVueRoute(route: RouteRecordNormalized | RouteLocationNormalizedLoaded) {
|
||||
const fullPath = hasFullPath(route) ? route.fullPath : route.path;
|
||||
|
||||
const tabRoute: GlobalTabRoute = {
|
||||
name: route.name,
|
||||
path: route.path,
|
||||
fullPath,
|
||||
meta: route.meta,
|
||||
scrollPosition: {
|
||||
left: 0,
|
||||
@@ -20,17 +22,36 @@ export function getTabRouteByVueRoute(route: RouteRecordNormalized | RouteLocati
|
||||
/**
|
||||
* 获取该页签在多页签数据中的索引
|
||||
* @param tabs - 多页签数据
|
||||
* @param path - 该页签的路径
|
||||
* @param fullPath - 该页签的路径
|
||||
*/
|
||||
export function getIndexInTabRoutes(tabs: GlobalTabRoute[], path: string) {
|
||||
return tabs.findIndex(tab => tab.path === path);
|
||||
export function getIndexInTabRoutes(tabs: GlobalTabRoute[], fullPath: string) {
|
||||
return tabs.findIndex(tab => tab.fullPath === fullPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断该页签是否在多页签数据中
|
||||
* @param tabs - 多页签数据
|
||||
* @param path - 该页签的路径
|
||||
* @param fullPath - 该页签的路径
|
||||
*/
|
||||
export function isInTabRoutes(tabs: GlobalTabRoute[], path: string) {
|
||||
return getIndexInTabRoutes(tabs, path) > -1;
|
||||
export function isInTabRoutes(tabs: GlobalTabRoute[], fullPath: string) {
|
||||
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);
|
||||
}
|
||||
|
@@ -3,14 +3,14 @@ import { defineStore } from 'pinia';
|
||||
import { useRouterPush } from '@/composables';
|
||||
import { getTabRoutes, clearTabRoutes } from '@/utils';
|
||||
import { useThemeStore } from '../theme';
|
||||
import { getTabRouteByVueRoute, isInTabRoutes, getIndexInTabRoutes } from './helpers';
|
||||
import { getTabRouteByVueRoute, isInTabRoutes, getIndexInTabRoutes, getIndexInTabRoutesByRouteName } from './helpers';
|
||||
|
||||
interface TabState {
|
||||
/** 多页签数据 */
|
||||
tabs: GlobalTabRoute[];
|
||||
/** 多页签首页 */
|
||||
homeTab: GlobalTabRoute;
|
||||
/** 当前激活状态的页签(路由path) */
|
||||
/** 当前激活状态的页签(路由fullPath) */
|
||||
activeTab: string;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ export const useTabStore = defineStore('tab-store', {
|
||||
tabs: [],
|
||||
homeTab: {
|
||||
name: 'root',
|
||||
path: '/',
|
||||
fullPath: '/',
|
||||
meta: {
|
||||
title: 'Root'
|
||||
},
|
||||
@@ -34,7 +34,7 @@ export const useTabStore = defineStore('tab-store', {
|
||||
/** 当前激活状态的页签索引 */
|
||||
activeTabIndex(state) {
|
||||
const { tabs, activeTab } = state;
|
||||
return tabs.findIndex(tab => tab.path === activeTab);
|
||||
return tabs.findIndex(tab => tab.fullPath === activeTab);
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
@@ -45,10 +45,20 @@ export const useTabStore = defineStore('tab-store', {
|
||||
},
|
||||
/**
|
||||
* 设置当前路由对应的页签为激活状态
|
||||
* @param path - 路由path
|
||||
* @param fullPath - 路由fullPath
|
||||
*/
|
||||
setActiveTab(path: string) {
|
||||
this.activeTab = path;
|
||||
setActiveTab(fullPath: string) {
|
||||
this.activeTab = fullPath;
|
||||
},
|
||||
/**
|
||||
* 设置当前路由对应的页签title
|
||||
* @param title - tab名称
|
||||
*/
|
||||
setActiveTabTitle(title: string) {
|
||||
const item = this.tabs.find(tab => tab.fullPath === this.activeTab);
|
||||
if (item) {
|
||||
item.meta.title = title;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 初始化首页页签路由
|
||||
@@ -68,22 +78,39 @@ export const useTabStore = defineStore('tab-store', {
|
||||
* @param route - 路由
|
||||
*/
|
||||
addTab(route: RouteLocationNormalizedLoaded) {
|
||||
if (!isInTabRoutes(this.tabs, route.path)) {
|
||||
this.tabs.push(getTabRouteByVueRoute(route));
|
||||
const tab = 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 isActive = this.activeTab === path;
|
||||
const updateTabs = this.tabs.filter(tab => tab.path !== path);
|
||||
const isActive = this.activeTab === fullPath;
|
||||
const updateTabs = this.tabs.filter(tab => tab.fullPath !== fullPath);
|
||||
this.tabs = updateTabs;
|
||||
if (isActive && updateTabs.length) {
|
||||
const activePath = updateTabs[updateTabs.length - 1].path;
|
||||
const activePath = updateTabs[updateTabs.length - 1].fullPath;
|
||||
this.setActiveTab(activePath);
|
||||
routerPush(activePath);
|
||||
}
|
||||
@@ -95,73 +122,73 @@ export const useTabStore = defineStore('tab-store', {
|
||||
clearTab(excludes: string[] = []) {
|
||||
const { routerPush } = useRouterPush(false);
|
||||
|
||||
const homePath = this.homeTab.path;
|
||||
const homePath = this.homeTab.fullPath;
|
||||
const remain = [homePath, ...excludes];
|
||||
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;
|
||||
if (!hasActive && updateTabs.length) {
|
||||
const activePath = updateTabs[updateTabs.length - 1].path;
|
||||
const activePath = updateTabs[updateTabs.length - 1].fullPath;
|
||||
this.setActiveTab(activePath);
|
||||
routerPush(activePath);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 清除左边多页签
|
||||
* @param path - 路由path
|
||||
* @param fullPath - 路由fullPath
|
||||
*/
|
||||
clearLeftTab(path: string) {
|
||||
const index = getIndexInTabRoutes(this.tabs, path);
|
||||
clearLeftTab(fullPath: string) {
|
||||
const index = getIndexInTabRoutes(this.tabs, fullPath);
|
||||
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);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 清除右边多页签
|
||||
* @param path - 路由path
|
||||
* @param fullPath - 路由fullPath
|
||||
*/
|
||||
clearRightTab(path: string) {
|
||||
const index = getIndexInTabRoutes(this.tabs, path);
|
||||
clearRightTab(fullPath: string) {
|
||||
const index = getIndexInTabRoutes(this.tabs, fullPath);
|
||||
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);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 点击单个tab
|
||||
* @param path - 路由path
|
||||
* @param fullPath - 路由fullPath
|
||||
*/
|
||||
handleClickTab(path: string) {
|
||||
handleClickTab(fullPath: string) {
|
||||
const { routerPush } = useRouterPush(false);
|
||||
|
||||
const isActive = this.activeTab === path;
|
||||
const isActive = this.activeTab === fullPath;
|
||||
if (!isActive) {
|
||||
this.setActiveTab(path);
|
||||
routerPush(path);
|
||||
this.setActiveTab(fullPath);
|
||||
routerPush(fullPath);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 记录tab滚动位置
|
||||
* @param path - 路由path
|
||||
* @param fullPath - 路由fullPath
|
||||
* @param position - tab当前页的滚动位置
|
||||
*/
|
||||
recordTabScrollPosition(path: string, position: { left: number; top: number }) {
|
||||
const index = getIndexInTabRoutes(this.tabs, path);
|
||||
recordTabScrollPosition(fullPath: string, position: { left: number; top: number }) {
|
||||
const index = getIndexInTabRoutes(this.tabs, fullPath);
|
||||
if (index > -1) {
|
||||
this.tabs[index].scrollPosition = position;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 获取tab滚动位置
|
||||
* @param path - 路由path
|
||||
* @param fullPath - 路由fullPath
|
||||
*/
|
||||
getTabScrollPosition(path: string) {
|
||||
getTabScrollPosition(fullPath: string) {
|
||||
const position = {
|
||||
left: 0,
|
||||
top: 0
|
||||
};
|
||||
const index = getIndexInTabRoutes(this.tabs, path);
|
||||
const index = getIndexInTabRoutes(this.tabs, fullPath);
|
||||
if (index > -1) {
|
||||
Object.assign(position, this.tabs[index].scrollPosition);
|
||||
}
|
||||
@@ -173,20 +200,27 @@ export const useTabStore = defineStore('tab-store', {
|
||||
|
||||
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') {
|
||||
tabs.unshift(this.homeTab);
|
||||
}
|
||||
|
||||
const isHome = currentRoute.path === this.homeTab.path;
|
||||
const hasCurrent = isInTabRoutes(tabs, currentRoute.path);
|
||||
if (!isHome && !hasCurrent) {
|
||||
const isHome = currentRoute.fullPath === this.homeTab.fullPath;
|
||||
const index = getIndexInTabRoutesByRouteName(tabs, currentRoute.name as string);
|
||||
if (!isHome) {
|
||||
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.setActiveTab(currentRoute.path);
|
||||
this.setActiveTab(currentRoute.fullPath);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@@ -15,9 +15,8 @@ export function getThemeSettings() {
|
||||
type ColorType = 'primary' | 'info' | 'success' | 'warning' | 'error';
|
||||
type ColorScene = '' | 'Suppl' | 'Hover' | 'Pressed' | 'Active';
|
||||
type ColorKey = `${ColorType}Color${ColorScene}`;
|
||||
type ThemeColor = {
|
||||
[key in ColorKey]?: string;
|
||||
};
|
||||
type ThemeColor = Partial<Record<ColorKey, string>>;
|
||||
|
||||
interface ColorAction {
|
||||
scene: ColorScene;
|
||||
handler: (color: string) => string;
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user