From 47953b9a80adc99ef772d1429691eb069185e532 Mon Sep 17 00:00:00 2001
From: autumnal_wind@yeah.net <autumnal_wind@yeah.net>
Date: 星期三, 24 四月 2024 09:17:24 +0800
Subject: [PATCH] feat: 项目初始化
---
src/assets/icons/svg/component.svg | 1
src/assets/icons/svg/clipboard.svg | 1
src/assets/styles/index.scss | 208
src/assets/icons/svg/download.svg | 1
src/assets/icons/svg/eye.svg | 1
src/directive/index.ts | 9
src/api/system/dept/index.ts | 62
src/layout/index.vue | 124
src/assets/images/light.svg | 39
src/views/system/tenant/index.vue | 347
src/views/system/user/authRole.vue | 127
src/views/monitor/operlog/index.vue | 302
src/assets/icons/svg/cascader.svg | 1
src/layout/components/TopBar/search.vue | 158
src/components/LangSelect/index.vue | 40
src/components/BuildCode/index.vue | 64
src/assets/icons/svg/tool.svg | 1
src/assets/icons/svg/radio.svg | 1
src/views/monitor/cache/index.vue | 191
src/assets/401_images/401.gif | 0
src/layout/components/notice/index.vue | 134
src/store/modules/notice.ts | 42
src/views/error/404.vue | 223
src/api/system/dept/types.ts | 45
src/api/monitor/online/types.ts | 15
src/components/SizeSelect/index.vue | 41
src/views/system/oss/index.vue | 335
bin/run-web.bat | 12
src/assets/icons/svg/user.svg | 1
src/components/Breadcrumb/index.vue | 64
.env.development | 30
src/assets/icons/svg/form.svg | 1
src/directive/permission/index.ts | 44
vite/plugins/compression.ts | 27
src/assets/icons/svg/time.svg | 1
src/utils/validate.ts | 92
src/components/TopNav/index.vue | 195
src/assets/icons/svg/chart.svg | 1
src/assets/icons/svg/switch.svg | 1
src/assets/icons/svg/upload.svg | 1
src/assets/styles/mixin.scss | 60
src/views/system/role/index.vue | 502 +
src/views/system/goodsIn/index.vue | 323
src/types/element.d.ts | 34
src/views/system/oss/config.vue | 341
src/assets/icons/svg/log.svg | 1
src/api/system/dict/type/types.ts | 18
src/layout/components/index.ts | 4
src/components/SunSailGit/index.vue | 13
vite/plugins/components.ts | 17
src/assets/styles/variables.module.scss | 93
src/lang/en_US.ts | 28
src/assets/icons/svg/theme.svg | 1
src/plugins/svgicon.ts | 10
src/assets/icons/svg/date-range.svg | 1
vite.config.ts | 118
src/assets/icons/svg/guide.svg | 1
src/views/error/401.vue | 76
src/enums/layout/LayoutEnum.ts | 4
src/views/system/dict/data.vue | 289
src/views/tool/gen/editTable.vue | 187
src/api/system/dict/type/index.ts | 62
src/api/system/post/types.ts | 23
src/views/tool/gen/importTable.vue | 123
src/api/system/post/index.ts | 46
src/assets/icons/svg/job.svg | 1
src/assets/icons/svg/checkbox.svg | 1
src/views/system/dept/index.vue | 309
src/store/modules/user.ts | 88
src/assets/icons/svg/qq.svg | 1
src/utils/i18n.ts | 16
src/main.ts | 57
src/utils/index.ts | 318
src/plugins/index.ts | 24
src/api/system/goodsIn/types.ts | 146
src/utils/theme.ts | 52
src/views/system/user/profile/userInfo.vue | 65
src/components/iFrame/index.vue | 26
src/api/system/goodsIn/index.ts | 63
src/api/tool/gen/types.ts | 180
src/assets/icons/svg/language.svg | 1
src/api/tool/gen/index.ts | 86
src/assets/icons/svg/github.svg | 1
src/assets/icons/svg/dict.svg | 1
src/assets/styles/btn.scss | 99
src/assets/icons/svg/row.svg | 1
src/assets/icons/svg/link.svg | 1
src/views/system/user/profile/thirdParty.vue | 140
src/assets/icons/svg/wechat.svg | 1
src/plugins/modal.ts | 81
src/assets/icons/svg/druid.svg | 1
src/views/register.vue | 232
.prettierignore | 9
.eslintrc.js | 42
src/layout/components/Sidebar/Logo.vue | 94
index.html | 217
bin/package.bat | 12
src/assets/icons/svg/input.svg | 1
src/assets/icons/svg/validCode.svg | 1
src/views/monitor/powerjob/index.vue | 9
LICENSE | 20
src/utils/scroll-to.ts | 65
vite/plugins/icons.ts | 8
src/assets/icons/svg/icon.svg | 1
src/assets/icons/svg/number.svg | 1
src/types/router.d.ts | 36
src/plugins/download.ts | 65
src/utils/permission.ts | 51
src/views/tool/gen/basicInfoForm.vue | 49
src/assets/icons/svg/exit-fullscreen.svg | 1
src/assets/icons/svg/rate.svg | 1
src/components/Pagination/index.vue | 88
src/assets/icons/svg/money.svg | 1
src/lang/index.ts | 35
src/assets/icons/svg/phone.svg | 1
src/assets/icons/svg/size.svg | 1
src/assets/icons/svg/system.svg | 2
src/utils/jsencrypt.ts | 21
tsconfig.json | 26
src/utils/auth.ts | 9
src/utils/dict.ts | 27
src/plugins/cache.ts | 77
src/api/menu.ts | 11
src/components/FileUpload/index.vue | 217
src/views/system/dict/index.vue | 243
src/assets/404_images/404.png | 0
src/assets/images/dark.svg | 39
.gitignore | 34
src/api/system/dict/data/index.ts | 53
src/layout/components/SocialCallback/index.vue | 87
src/types/global.d.ts | 91
src/api/system/dict/data/types.ts | 26
src/api/demo/tree/types.ts | 80
src/views/redirect/index.vue | 14
vite/plugins/svg-icon.ts | 10
src/utils/websocket.ts | 141
src/views/system/role/selectUser.vue | 132
src/components/HeaderSearch/index.vue | 193
src/assets/icons/svg/education.svg | 1
src/assets/icons/svg/international.svg | 1
src/api/demo/tree/index.ts | 62
src/assets/icons/svg/pdf.svg | 1
src/assets/404_images/404_cloud.png | 0
src/animate.ts | 48
src/api/system/tenantPackage/index.ts | 67
src/components/SvgIcon/index.vue | 40
src/components/RightToolbar/index.vue | 104
src/views/monitor/online/index.vue | 116
src/types/module.d.ts | 32
src/api/system/social/auth.ts | 24
src/components/IconSelect/index.vue | 106
src/views/system/user/index.vue | 622 +
src/views/login.vue | 283
src/views/tool/gen/genInfoForm.vue | 291
src/assets/icons/svg/search.svg | 1
src/layout/components/IframeToggle/index.vue | 27
src/api/system/tenantPackage/types.ts | 20
package.json | 80
src/App.vue | 21
src/assets/images/login-background.jpg | 0
src/api/system/menu/index.ts | 70
src/assets/icons/svg/shopping.svg | 1
src/store/modules/dict.ts | 78
src/assets/icons/svg/message.svg | 1
src/assets/icons/svg/drag.svg | 1
src/assets/icons/svg/textarea.svg | 1
src/assets/icons/svg/button.svg | 1
src/assets/icons/svg/peoples.svg | 1
src/views/monitor/logininfor/index.vue | 204
src/assets/icons/svg/tab.svg | 1
src/assets/styles/ruoyi.scss | 290
html/ie.html | 60
src/views/basis/sysParam/config/index.vue | 257
src/store/modules/permission.ts | 148
src/api/system/menu/types.ts | 69
src/assets/icons/svg/list.svg | 1
src/assets/icons/svg/online.svg | 1
src/utils/errorCode.ts | 7
src/assets/icons/svg/maxkey.svg | 3
src/views/system/tenantPackage/index.vue | 332
src/settings.ts | 55
src/assets/icons/svg/eye-open.svg | 1
src/components/ImageUpload/index.vue | 216
src/plugins/auth.ts | 60
src/assets/icons/svg/404.svg | 1
src/assets/icons/svg/tree-table.svg | 1
src/utils/crypto.ts | 66
vite/plugins/setup-extend.ts | 5
src/enums/RespEnum.ts | 90
src/assets/icons/svg/documentation.svg | 1
src/layout/components/Sidebar/Link.vue | 40
src/api/system/ossConfig/types.ts | 38
src/lang/zh_CN.ts | 27
src/components/Hamburger/index.vue | 35
src/views/tool/gen/index.vue | 251
src/components/ParentView/index.vue | 3
src/assets/icons/svg/password.svg | 1
src/components/ImagePreview/index.vue | 80
src/api/monitor/loginInfo/types.ts | 20
src/assets/icons/svg/question.svg | 1
src/assets/icons/svg/zip.svg | 1
src/assets/icons/svg/people.svg | 1
src/api/system/notice/types.ts | 26
src/assets/icons/svg/gitee.svg | 1
src/assets/icons/svg/company.svg | 1
src/assets/icons/svg/edit.svg | 1
src/components/Editor/index.vue | 237
src/api/system/ossConfig/index.ts | 60
src/api/system/notice/index.ts | 45
src/assets/icons/svg/skill.svg | 1
src/views/system/menu/index.vue | 410 +
src/assets/icons/svg/server.svg | 1
src/layout/components/Sidebar/index.vue | 55
vite/plugins/index.ts | 22
src/assets/icons/svg/excel.svg | 1
src/assets/icons/svg/table.svg | 1
src/assets/styles/sidebar.scss | 230
src/assets/images/profile.jpg | 0
.eslintrc-auto-import.json | 312
src/store/modules/tagsView.ts | 209
src/api/login.ts | 105
src/assets/styles/element-ui.scss | 116
src/layout/components/TagsView/index.vue | 335
src/types/axios.d.ts | 10
src/api/types.ts | 59
src/assets/icons/svg/redis-list.svg | 2
src/permission.ts | 63
src/api/monitor/loginInfo/index.ts | 36
src/components/TreeSelect/index.vue | 158
src/assets/icons/svg/star.svg | 1
src/assets/icons/svg/code.svg | 1
src/views/system/user/profile/resetPwd.vue | 66
src/layout/components/AppMain.vue | 80
src/utils/ruoyi.ts | 247
.eslintignore | 17
src/assets/icons/svg/post.svg | 1
vite/plugins/auto-import.ts | 24
src/views/index.vue | 173
src/assets/icons/svg/fullscreen.svg | 1
src/components/BuildCode/render.vue | 62
src/store/modules/app.ts | 72
src/assets/icons/svg/build.svg | 1
src/api/system/client/types.ts | 135
src/assets/icons/svg/dashboard.svg | 1
src/views/system/user/profile/userAvatar.vue | 183
src/components/Screenfull/index.vue | 9
src/api/system/client/index.ts | 80
src/utils/dynamicTitle.ts | 14
src/enums/MenuTypeEnum.ts | 15
src/assets/icons/svg/date.svg | 1
src/directive/common/copyText.ts | 66
src/layout/components/InnerLink/index.vue | 18
vite/plugins/unocss.ts | 14
src/views/system/role/authUser.vue | 150
src/enums/SettingTypeEnum.ts | 16
src/plugins/tab.ts | 86
public/favicon.ico | 0
src/router/index.ts | 184
.editorconfig | 21
src/types/setting.d.ts | 70
src/assets/icons/svg/nested.svg | 1
src/views/tool/build/index.vue | 3
src/assets/icons/svg/monitor.svg | 2
src/components/IconSelect/requireIcons.ts | 7
src/views/system/post/index.vue | 237
src/utils/request.ts | 198
src/assets/icons/svg/example.svg | 1
src/views/system/user/profile/index.vue | 103
src/assets/styles/transition.scss | 49
src/types/env.d.ts | 78
src/assets/icons/svg/time-range.svg | 1
src/assets/icons/svg/logininfor.svg | 1
src/api/system/config/index.ts | 74
src/layout/components/TagsView/ScrollPane.vue | 101
src/api/monitor/operlog/index.ts | 28
src/api/monitor/operlog/types.ts | 53
.env.production | 33
src/views/system/notice/index.vue | 245
src/api/system/oss/index.ts | 28
src/enums/SideThemeEnum.ts | 4
src/layout/components/Settings/index.vue | 272
src/api/system/oss/types.ts | 22
bin/build.bat | 12
src/assets/icons/svg/lock.svg | 1
src/components/SunSailDoc/index.vue | 13
src/types/vform3-builds.d.ts | 4
src/api/system/config/types.ts | 23
src/layout/components/Navbar.vue | 296
src/api/monitor/online/index.ts | 20
src/assets/icons/svg/bug.svg | 1
src/views/monitor/admin/index.vue | 9
src/store/modules/settings.ts | 54
src/assets/icons/svg/email.svg | 1
src/layout/components/Sidebar/SidebarItem.vue | 108
src/assets/icons/svg/redis.svg | 1
src/views/system/client/index.vue | 324
src/assets/icons/svg/color.svg | 1
src/api/system/user/types.ts | 84
src/components/DictTag/index.vue | 85
src/api/monitor/cache/types.ts | 7
src/views/demo/tree/index.vue | 271
src/assets/icons/svg/swagger.svg | 1
src/assets/icons/svg/slider.svg | 1
commitlint.config.js | 22
src/api/demo/demo/index.ts | 62
src/assets/logo/logo.png | 0
src/api/demo/demo/types.ts | 90
src/api/system/user/index.ts | 215
src/store/index.ts | 3
src/api/system/tenant/types.ts | 46
src/utils/propTypes.ts | 25
src/views/demo/demo/index.vue | 262
src/assets/icons/svg/tree.svg | 1
src/api/monitor/cache/index.ts | 59
src/api/system/role/index.ts | 144
src/api/system/role/types.ts | 52
src/assets/icons/svg/select.svg | 1
.prettierrc.cjs | 46
src/api/system/tenant/index.ts | 92
319 files changed, 22,435 insertions(+), 9 deletions(-)
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..5b1a804
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,21 @@
+# 告诉EditorConfig插件,这是根文件,不用继续往上查找
+root = true
+
+# 匹配全部文件
+[*]
+# 缩进风格,可选space、tab
+indent_style = space
+# 缩进的空格数
+indent_size = 2
+# 设置字符集
+charset = utf-8
+# 结尾换行符,可选lf、cr、crlf
+end_of_line = lf
+# 在文件结尾插入新行
+trim_trailing_whitespace = true
+# 删除一行中的前后空格
+insert_final_newline = true
+
+[*.md]
+insert_final_newline = false
+trim_trailing_whitespace = false
diff --git a/.env.development b/.env.development
new file mode 100644
index 0000000..512c187
--- /dev/null
+++ b/.env.development
@@ -0,0 +1,30 @@
+# 页面标题
+VITE_APP_TITLE=通用多租户管理系统
+
+# 开发环境配置
+VITE_APP_ENV='development'
+
+# 开发环境
+VITE_APP_BASE_API='/dev-api'
+
+# 应用访问路径 例如使用前缀 /admin/
+VITE_APP_CONTEXT_PATH='/'
+
+# 监控地址
+VITE_APP_MONITRO_ADMIN='http://localhost:9090/admin/applications'
+
+# powerjob 控制台地址
+VITE_APP_POWERJOB_ADMIN='http://localhost:7700/'
+
+VITE_APP_PORT=8220
+
+# 接口加密传输 RSA 公钥与后端解密私钥对应 如更换需前后端一同更换
+VITE_APP_RSA_PUBLIC_KEY='MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ=='
+# 接口响应解密 RSA 私钥与后端加密公钥对应 如更换需前后端一同更换
+VITE_APP_RSA_PRIVATE_KEY='MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAmc3CuPiGL/LcIIm7zryCEIbl1SPzBkr75E2VMtxegyZ1lYRD+7TZGAPkvIsBcaMs6Nsy0L78n2qh+lIZMpLH8wIDAQABAkEAk82Mhz0tlv6IVCyIcw/s3f0E+WLmtPFyR9/WtV3Y5aaejUkU60JpX4m5xNR2VaqOLTZAYjW8Wy0aXr3zYIhhQQIhAMfqR9oFdYw1J9SsNc+CrhugAvKTi0+BF6VoL6psWhvbAiEAxPPNTmrkmrXwdm/pQQu3UOQmc2vCZ5tiKpW10CgJi8kCIFGkL6utxw93Ncj4exE/gPLvKcT+1Emnoox+O9kRXss5AiAMtYLJDaLEzPrAWcZeeSgSIzbL+ecokmFKSDDcRske6QIgSMkHedwND1olF8vlKsJUGK3BcdtM8w4Xq7BpSBwsloE='
+
+# 客户端id
+VITE_APP_CLIENT_ID='e5cd7e4891bf95d1d19206ce24a7b32e'
+
+# websocket 开关(开发环境默认关闭ws 因vite的bug导致如ws无法连接则会崩溃)
+VITE_APP_WEBSOCKET=false
diff --git a/.env.production b/.env.production
new file mode 100644
index 0000000..705913c
--- /dev/null
+++ b/.env.production
@@ -0,0 +1,33 @@
+# 页面标题
+VITE_APP_TITLE = 通用多租户管理系统
+
+# 生产环境配置
+VITE_APP_ENV = 'production'
+
+# 应用访问路径 例如使用前缀 /admin/
+VITE_APP_CONTEXT_PATH = '/'
+
+# 监控地址
+VITE_APP_MONITRO_ADMIN = '/admin/applications'
+
+# powerjob 控制台地址
+VITE_APP_POWERJOB_ADMIN = '/powerjob'
+
+# 生产环境
+VITE_APP_BASE_API = '/prod-api'
+
+# 是否在打包时开启压缩,支持 gzip 和 brotli
+VITE_BUILD_COMPRESS = gzip
+
+VITE_APP_PORT = 80
+
+# 接口加密传输 RSA 公钥与后端解密私钥对应 如更换需前后端一同更换
+VITE_APP_RSA_PUBLIC_KEY = 'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ=='
+# 接口响应解密 RSA 私钥与后端加密公钥对应 如更换需前后端一同更换
+VITE_APP_RSA_PRIVATE_KEY = 'MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAmc3CuPiGL/LcIIm7zryCEIbl1SPzBkr75E2VMtxegyZ1lYRD+7TZGAPkvIsBcaMs6Nsy0L78n2qh+lIZMpLH8wIDAQABAkEAk82Mhz0tlv6IVCyIcw/s3f0E+WLmtPFyR9/WtV3Y5aaejUkU60JpX4m5xNR2VaqOLTZAYjW8Wy0aXr3zYIhhQQIhAMfqR9oFdYw1J9SsNc+CrhugAvKTi0+BF6VoL6psWhvbAiEAxPPNTmrkmrXwdm/pQQu3UOQmc2vCZ5tiKpW10CgJi8kCIFGkL6utxw93Ncj4exE/gPLvKcT+1Emnoox+O9kRXss5AiAMtYLJDaLEzPrAWcZeeSgSIzbL+ecokmFKSDDcRske6QIgSMkHedwND1olF8vlKsJUGK3BcdtM8w4Xq7BpSBwsloE='
+
+# 客户端id
+VITE_APP_CLIENT_ID = 'e5cd7e4891bf95d1d19206ce24a7b32e'
+
+# websocket 开关
+VITE_APP_WEBSOCKET = true
diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 0000000..c4d771a
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1,17 @@
+*.sh
+node_modules
+*.md
+*.woff
+*.ttf
+.vscode
+.idea
+dist
+/public
+/docs
+.husky
+.local
+/bin
+.eslintrc.js
+prettier.config.js
+src/assets
+tailwind.config.js
\ No newline at end of file
diff --git a/.eslintrc-auto-import.json b/.eslintrc-auto-import.json
new file mode 100644
index 0000000..a6661b6
--- /dev/null
+++ b/.eslintrc-auto-import.json
@@ -0,0 +1,312 @@
+{
+ "globals": {
+ "ComponentInternalInstance": true,
+ "TransferKey": true,
+ "ElFormRules": true,
+ "CheckboxValueType": true,
+ "PropType": true,
+ "DateModelType": true,
+ "UploadFile": true,
+ "ElFormInstance": true,
+ "ElTableInstance": true,
+ "ElTreeInstance": true,
+ "ElTreeSelectInstance": true,
+ "ElSelectInstance": true,
+ "ElUploadInstance": true,
+ "ElCardInstance": true,
+ "ElDialogInstance": true,
+ "ElInputInstance": true,
+ "ElInputNumberInstance": true,
+ "ElRadioInstance": true,
+ "ElRadioGroupInstance": true,
+ "ElRadioButtonInstance": true,
+ "ElCheckboxInstance": true,
+ "ElCheckboxGroupInstance": true,
+ "ElSwitchInstance": true,
+ "ElDatePickerInstance": true,
+ "ElTimePickerInstance": true,
+ "ElTimeSelectInstance": true,
+ "ElScrollbarInstance": true,
+ "ElCascaderInstance": true,
+ "ElColorPickerInstance": true,
+ "ElRateInstance": true,
+ "ElSliderInstance": true,
+ "useRouter": true,
+ "useRoute": true,
+ "EffectScope": true,
+ "ElTable": true,
+ "ElSelect": true,
+ "ElUpload": true,
+ "ElForm": true,
+ "ElTree": true,
+ "ElMessage": true,
+ "ElMessageBox": true,
+ "asyncComputed": true,
+ "autoResetRef": true,
+ "computed": true,
+ "computedAsync": true,
+ "computedEager": true,
+ "computedInject": true,
+ "computedWithControl": true,
+ "controlledComputed": true,
+ "controlledRef": true,
+ "createApp": true,
+ "createEventHook": true,
+ "createGlobalState": true,
+ "createInjectionState": true,
+ "createReactiveFn": true,
+ "createSharedComposable": true,
+ "createUnrefFn": true,
+ "customRef": true,
+ "debouncedRef": true,
+ "debouncedWatch": true,
+ "defineAsyncComponent": true,
+ "defineComponent": true,
+ "eagerComputed": true,
+ "effectScope": true,
+ "extendRef": true,
+ "getCurrentInstance": true,
+ "getCurrentScope": true,
+ "h": true,
+ "ignorableWatch": true,
+ "inject": true,
+ "isDefined": true,
+ "isProxy": true,
+ "isReactive": true,
+ "isReadonly": true,
+ "isRef": true,
+ "makeDestructurable": true,
+ "markRaw": true,
+ "nextTick": true,
+ "onActivated": true,
+ "onBeforeMount": true,
+ "onBeforeUnmount": true,
+ "onBeforeUpdate": true,
+ "onClickOutside": true,
+ "onDeactivated": true,
+ "onErrorCaptured": true,
+ "onKeyStroke": true,
+ "onLongPress": true,
+ "onMounted": true,
+ "onRenderTracked": true,
+ "onRenderTriggered": true,
+ "onScopeDispose": true,
+ "onServerPrefetch": true,
+ "onStartTyping": true,
+ "onUnmounted": true,
+ "onUpdated": true,
+ "pausableWatch": true,
+ "provide": true,
+ "reactify": true,
+ "reactifyObject": true,
+ "reactive": true,
+ "reactiveComputed": true,
+ "reactiveOmit": true,
+ "reactivePick": true,
+ "readonly": true,
+ "ref": true,
+ "refAutoReset": true,
+ "refDebounced": true,
+ "refDefault": true,
+ "refThrottled": true,
+ "refWithControl": true,
+ "resolveComponent": true,
+ "resolveDirective": true,
+ "resolveRef": true,
+ "resolveUnref": true,
+ "shallowReactive": true,
+ "shallowReadonly": true,
+ "shallowRef": true,
+ "syncRef": true,
+ "syncRefs": true,
+ "templateRef": true,
+ "throttledRef": true,
+ "throttledWatch": true,
+ "toRaw": true,
+ "toReactive": true,
+ "toRef": true,
+ "toRefs": true,
+ "triggerRef": true,
+ "tryOnBeforeMount": true,
+ "tryOnBeforeUnmount": true,
+ "tryOnMounted": true,
+ "tryOnScopeDispose": true,
+ "tryOnUnmounted": true,
+ "unref": true,
+ "unrefElement": true,
+ "until": true,
+ "useActiveElement": true,
+ "useArrayEvery": true,
+ "useArrayFilter": true,
+ "useArrayFind": true,
+ "useArrayFindIndex": true,
+ "useArrayFindLast": true,
+ "useArrayJoin": true,
+ "useArrayMap": true,
+ "useArrayReduce": true,
+ "useArraySome": true,
+ "useArrayUnique": true,
+ "useAsyncQueue": true,
+ "useAsyncState": true,
+ "useAttrs": true,
+ "useBase64": true,
+ "useBattery": true,
+ "useBluetooth": true,
+ "useBreakpoints": true,
+ "useBroadcastChannel": true,
+ "useBrowserLocation": true,
+ "useCached": true,
+ "useClipboard": true,
+ "useCloned": true,
+ "useColorMode": true,
+ "useConfirmDialog": true,
+ "useCounter": true,
+ "useCssModule": true,
+ "useCssVar": true,
+ "useCssVars": true,
+ "useCurrentElement": true,
+ "useCycleList": true,
+ "useDark": true,
+ "useDateFormat": true,
+ "useDebounce": true,
+ "useDebounceFn": true,
+ "useDebouncedRefHistory": true,
+ "useDeviceMotion": true,
+ "useDeviceOrientation": true,
+ "useDevicePixelRatio": true,
+ "useDevicesList": true,
+ "useDisplayMedia": true,
+ "useDocumentVisibility": true,
+ "useDraggable": true,
+ "useDropZone": true,
+ "useElementBounding": true,
+ "useElementByPoint": true,
+ "useElementHover": true,
+ "useElementSize": true,
+ "useElementVisibility": true,
+ "useEventBus": true,
+ "useEventListener": true,
+ "useEventSource": true,
+ "useEyeDropper": true,
+ "useFavicon": true,
+ "useFetch": true,
+ "useFileDialog": true,
+ "useFileSystemAccess": true,
+ "useFocus": true,
+ "useFocusWithin": true,
+ "useFps": true,
+ "useFullscreen": true,
+ "useGamepad": true,
+ "useGeolocation": true,
+ "useIdle": true,
+ "useImage": true,
+ "useInfiniteScroll": true,
+ "useIntersectionObserver": true,
+ "useInterval": true,
+ "useIntervalFn": true,
+ "useKeyModifier": true,
+ "useLastChanged": true,
+ "useLocalStorage": true,
+ "useMagicKeys": true,
+ "useManualRefHistory": true,
+ "useMediaControls": true,
+ "useMediaQuery": true,
+ "useMemoize": true,
+ "useMemory": true,
+ "useMounted": true,
+ "useMouse": true,
+ "useMouseInElement": true,
+ "useMousePressed": true,
+ "useMutationObserver": true,
+ "useNavigatorLanguage": true,
+ "useNetwork": true,
+ "useNow": true,
+ "useObjectUrl": true,
+ "useOffsetPagination": true,
+ "useOnline": true,
+ "usePageLeave": true,
+ "useParallax": true,
+ "usePermission": true,
+ "usePointer": true,
+ "usePointerLock": true,
+ "usePointerSwipe": true,
+ "usePreferredColorScheme": true,
+ "usePreferredContrast": true,
+ "usePreferredDark": true,
+ "usePreferredLanguages": true,
+ "usePreferredReducedMotion": true,
+ "usePrevious": true,
+ "useRafFn": true,
+ "useRefHistory": true,
+ "useResizeObserver": true,
+ "useScreenOrientation": true,
+ "useScreenSafeArea": true,
+ "useScriptTag": true,
+ "useScroll": true,
+ "useScrollLock": true,
+ "useSessionStorage": true,
+ "useShare": true,
+ "useSlots": true,
+ "useSorted": true,
+ "useSpeechRecognition": true,
+ "useSpeechSynthesis": true,
+ "useStepper": true,
+ "useStorage": true,
+ "useStorageAsync": true,
+ "useStyleTag": true,
+ "useSupported": true,
+ "useSwipe": true,
+ "useTemplateRefsList": true,
+ "useTextDirection": true,
+ "useTextSelection": true,
+ "useTextareaAutosize": true,
+ "useThrottle": true,
+ "useThrottleFn": true,
+ "useThrottledRefHistory": true,
+ "useTimeAgo": true,
+ "useTimeout": true,
+ "useTimeoutFn": true,
+ "useTimeoutPoll": true,
+ "useTimestamp": true,
+ "useTitle": true,
+ "useToNumber": true,
+ "useToString": true,
+ "useToggle": true,
+ "useTransition": true,
+ "useUrlSearchParams": true,
+ "useUserMedia": true,
+ "useVModel": true,
+ "useVModels": true,
+ "useVibrate": true,
+ "useVirtualList": true,
+ "useWakeLock": true,
+ "useWebNotification": true,
+ "useWebSocket": true,
+ "useWebWorker": true,
+ "useWebWorkerFn": true,
+ "useWindowFocus": true,
+ "useWindowScroll": true,
+ "useWindowSize": true,
+ "watch": true,
+ "watchArray": true,
+ "watchAtMost": true,
+ "watchDebounced": true,
+ "watchEffect": true,
+ "watchIgnorable": true,
+ "watchOnce": true,
+ "watchPausable": true,
+ "watchPostEffect": true,
+ "watchSyncEffect": true,
+ "watchThrottled": true,
+ "watchTriggerable": true,
+ "watchWithFilter": true,
+ "whenever": true,
+ "ImportOption": true,
+ "TreeType": true,
+ "FieldOption": true,
+ "PageData": true,
+ "storeToRefs": true,
+ "DictDataOption": true,
+ "UploadOption": true
+ }
+}
diff --git a/.eslintrc.js b/.eslintrc.js
new file mode 100644
index 0000000..a1b32e3
--- /dev/null
+++ b/.eslintrc.js
@@ -0,0 +1,42 @@
+module.exports = {
+ env: {
+ browser: true,
+ es2021: true,
+ node: true
+ },
+ parser: 'vue-eslint-parser',
+ extends: [
+ 'eslint:recommended',
+ 'plugin:vue/vue3-essential',
+ 'plugin:@typescript-eslint/recommended',
+ './.eslintrc-auto-import.json',
+ 'plugin:prettier/recommended'
+ ],
+ parserOptions: {
+ ecmaVersion: '2020',
+ sourceType: 'module',
+ parser: '@typescript-eslint/parser'
+ },
+ plugins: ['vue', '@typescript-eslint'],
+ rules: {
+ 'vue/multi-word-component-names': 'off',
+ '@typescript-eslint/no-empty-function': 'off',
+ '@typescript-eslint/no-explicit-any': 'off',
+ 'vue/no-v-model-argument': 'off',
+ '@typescript-eslint/ban-types': [
+ 'error',
+ {
+ // 关闭空类型检查 {}
+ extendDefaults: true,
+ types: {
+ '{}': false,
+ Function: false
+ }
+ }
+ ]
+ },
+ globals: {
+ DialogOption: 'readonly',
+ OptionType: 'readonly'
+ }
+};
diff --git a/.gitignore b/.gitignore
index 32858aa..40df474 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,12 +1,28 @@
-*.class
+.DS_Store
+.history
+node_modules/
+dist/
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+**/*.log
-# Mobile Tools for Java (J2ME)
-.mtj.tmp/
+tests/**/coverage/
+tests/e2e/reports
+selenium-debug.log
-# Package Files #
-*.jar
-*.war
-*.ear
+# Editor directories and files
+.idea
+.vscode
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.local
-# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
-hs_err_pid*
+package-lock.json
+yarn.lock
+
+# 编译生成的文件
+auto-imports.d.ts
+components.d.ts
diff --git a/.prettierignore b/.prettierignore
new file mode 100644
index 0000000..d251d2e
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1,9 @@
+/dist/*
+.local
+.output.js
+/node_modules/**
+
+**/*.svg
+**/*.sh
+
+/public/*
\ No newline at end of file
diff --git a/.prettierrc.cjs b/.prettierrc.cjs
new file mode 100644
index 0000000..61d2d92
--- /dev/null
+++ b/.prettierrc.cjs
@@ -0,0 +1,46 @@
+/**
+ * 代码格式化配置
+ */
+module.exports = {
+ // 一行最多多少个字符
+ printWidth: 150,
+ // 指定每个缩进级别的空格数
+ tabWidth: 2,
+ // 使用制表符而不是空格缩进行
+ useTabs: false,
+ // 在语句末尾是否需要分号
+ semi: true,
+ // 是否使用单引号
+ singleQuote: true,
+ // 更改引用对象属性的时间 可选值"<as-needed|consistent|preserve>"
+ quoteProps: 'as-needed',
+ // 在JSX中使用单引号而不是双引号
+ jsxSingleQuote: false,
+ // 多行时尽可能打印尾随逗号。(例如,单行数组永远不会出现逗号结尾。) 可选值"<none|es5|all>",默认none
+ trailingComma: 'none',
+ // 在对象文字中的括号之间打印空格
+ bracketSpacing: true,
+ // jsx 标签的反尖括号需要换行
+ jsxBracketSameLine: false,
+ embeddedLanguageFormatting: 'off',
+ // 在单独的箭头函数参数周围包括括号 always:(x) => x \ avoid:x => x
+ arrowParens: 'always',
+ // 这两个选项可用于格式化以给定字符偏移量(分别包括和不包括)开始和结束的代码
+ rangeStart: 0,
+ rangeEnd: Infinity,
+ // 指定要使用的解析器,不需要写文件开头的 @prettier
+ requirePragma: false,
+ // 不需要自动在文件开头插入 @prettier
+ insertPragma: false,
+ // 使用默认的折行标准 always\never\preserve
+ proseWrap: 'preserve',
+ // 指定HTML文件的全局空格敏感度 css\strict\ignore
+ htmlWhitespaceSensitivity: 'css',
+ // Vue文件脚本和样式标签缩进
+ vueIndentScriptAndStyle: false,
+ // 在 windows 操作系统中换行符通常是回车 (CR) 加换行分隔符 (LF),也就是回车换行(CRLF),
+ // 然而在 Linux 和 Unix 中只使用简单的换行分隔符 (LF)。
+ // 对应的控制字符为 "\n" (LF) 和 "\r\n"(CRLF)。auto意为保持现有的行尾
+ // 换行符使用 lf 结尾是 可选值"<auto|lf|crlf|cr>"
+ endOfLine: 'auto'
+};
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..0c9e9dd
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,20 @@
+The MIT License (MIT)
+
+Copyright (c) 2019 SunSail-Vue-Plus
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/bin/build.bat b/bin/build.bat
new file mode 100644
index 0000000..ecbb454
--- /dev/null
+++ b/bin/build.bat
@@ -0,0 +1,12 @@
+@echo off
+echo.
+echo [��Ϣ] ���Web���̣�����dist�ļ���
+echo.
+
+%~d0
+cd %~dp0
+
+cd ..
+yarn build:prod
+
+pause
\ No newline at end of file
diff --git a/bin/package.bat b/bin/package.bat
new file mode 100644
index 0000000..f5b24e0
--- /dev/null
+++ b/bin/package.bat
@@ -0,0 +1,12 @@
+@echo off
+echo.
+echo [��Ϣ] ��װWeb���̣�����node_modules�ļ���
+echo.
+
+%~d0
+cd %~dp0
+
+cd ..
+yarn --registry=https://registry.npmmirror.com
+
+pause
\ No newline at end of file
diff --git a/bin/run-web.bat b/bin/run-web.bat
new file mode 100644
index 0000000..d2fe397
--- /dev/null
+++ b/bin/run-web.bat
@@ -0,0 +1,12 @@
+@echo off
+echo.
+echo [��Ϣ] ʹ�� Vite �������� Web ���̡�
+echo.
+
+%~d0
+cd %~dp0
+
+cd ..
+yarn dev
+
+pause
\ No newline at end of file
diff --git a/commitlint.config.js b/commitlint.config.js
new file mode 100644
index 0000000..3b75ed5
--- /dev/null
+++ b/commitlint.config.js
@@ -0,0 +1,22 @@
+module.exports = {
+ extends: ['@commitlint/config-conventional'],
+ rules: {
+ 'type-enum': [
+ 2,
+ 'always',
+ [
+ 'feat', // 新功能 feature
+ 'fix', // 修复 bug
+ 'docs', // 文档注释
+ 'style', // 代码格式
+ 'refactor', // 重构
+ 'perf', // 性能优化
+ 'test', // 增加测试
+ 'chore', // 构建过程或辅助工具的变动
+ 'revert', // 回退
+ 'build' // 打包
+ ]
+ ],
+ 'subject-case': [0]
+ }
+};
diff --git a/html/ie.html b/html/ie.html
new file mode 100644
index 0000000..87d262d
--- /dev/null
+++ b/html/ie.html
@@ -0,0 +1,60 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+ <head>
+ <meta charset="UTF-8" />
+ <title>请升级您的浏览器</title>
+ <meta http-equiv="X-UA-Compatible" content="IE=Edge,chrome=1" />
+ <meta name="renderer" content="webkit" />
+ <base target="_blank" />
+ <style type="text/css">
+ html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video{border:0;font-size:100%;font:inherit;vertical-align:baseline;margin:0;padding:0}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{display:block}body{line-height:1}ol,ul{list-style:none}blockquote,q{quotes:none}blockquote:before,blockquote:after,q:before,q:after{content:none}table{border-collapse:collapse;border-spacing:0}
+ a{text-decoration:none;color:#0072c6;}a:hover{text-decoration:none;color:#004d8c;}
+ body{width:960px;margin:0 auto;padding:10px;font-size:14px;line-height:24px;color:#454545;font-family:'Microsoft YaHei UI','Microsoft YaHei',DengXian,SimSun,'Segoe UI',Tahoma,Helvetica,sans-serif;overflow-y:scroll}
+ h1{font-size:40px;line-height:80px;font-weight:100;margin-bottom:10px;}
+ h2{font-size:20px;line-height:25px;font-weight:100;margin:10px 0;}
+ em{color:red}
+ p{margin-bottom:10px;}
+ hr{margin:20px 0;border:0;border-top:1px solid #dadada}
+ span{display:block;font-size:12px;line-height:12px;}
+ .clean{clear:both;}
+ .browser{padding:10px 10px;}
+ .browser li{width:auto;padding:0 80px;margin-top:30px;height:34px;line-height:22px;float:left;list-style:none;background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACIAAADMCAYAAAAWCXEwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAKTWlDQ1BQaG90b3Nob3AgSUNDIHByb2ZpbGUAAHjanVN3WJP3Fj7f92UPVkLY8LGXbIEAIiOsCMgQWaIQkgBhhBASQMWFiApWFBURnEhVxILVCkidiOKgKLhnQYqIWotVXDjuH9yntX167+3t+9f7vOec5/zOec8PgBESJpHmomoAOVKFPDrYH49PSMTJvYACFUjgBCAQ5svCZwXFAADwA3l4fnSwP/wBr28AAgBw1S4kEsfh/4O6UCZXACCRAOAiEucLAZBSAMguVMgUAMgYALBTs2QKAJQAAGx5fEIiAKoNAOz0ST4FANipk9wXANiiHKkIAI0BAJkoRyQCQLsAYFWBUiwCwMIAoKxAIi4EwK4BgFm2MkcCgL0FAHaOWJAPQGAAgJlCLMwAIDgCAEMeE80DIEwDoDDSv+CpX3CFuEgBAMDLlc2XS9IzFLiV0Bp38vDg4iHiwmyxQmEXKRBmCeQinJebIxNI5wNMzgwAABr50cH+OD+Q5+bk4eZm52zv9MWi/mvwbyI+IfHf/ryMAgQAEE7P79pf5eXWA3DHAbB1v2upWwDaVgBo3/ldM9sJoFoK0Hr5i3k4/EAenqFQyDwdHAoLC+0lYqG9MOOLPv8z4W/gi372/EAe/tt68ABxmkCZrcCjg/1xYW52rlKO58sEQjFu9+cj/seFf/2OKdHiNLFcLBWK8ViJuFAiTcd5uVKRRCHJleIS6X8y8R+W/QmTdw0ArIZPwE62B7XLbMB+7gECiw5Y0nYAQH7zLYwaC5EAEGc0Mnn3AACTv/mPQCsBAM2XpOMAALzoGFyolBdMxggAAESggSqwQQcMwRSswA6cwR28wBcCYQZEQAwkwDwQQgbkgBwKoRiWQRlUwDrYBLWwAxqgEZrhELTBMTgN5+ASXIHrcBcGYBiewhi8hgkEQcgIE2EhOogRYo7YIs4IF5mOBCJhSDSSgKQg6YgUUSLFyHKkAqlCapFdSCPyLXIUOY1cQPqQ28ggMor8irxHMZSBslED1AJ1QLmoHxqKxqBz0XQ0D12AlqJr0Rq0Hj2AtqKn0UvodXQAfYqOY4DRMQ5mjNlhXIyHRWCJWBomxxZj5Vg1Vo81Yx1YN3YVG8CeYe8IJAKLgBPsCF6EEMJsgpCQR1hMWEOoJewjtBK6CFcJg4Qxwicik6hPtCV6EvnEeGI6sZBYRqwm7iEeIZ4lXicOE1+TSCQOyZLkTgohJZAySQtJa0jbSC2kU6Q+0hBpnEwm65Btyd7kCLKArCCXkbeQD5BPkvvJw+S3FDrFiOJMCaIkUqSUEko1ZT/lBKWfMkKZoKpRzame1AiqiDqfWkltoHZQL1OHqRM0dZolzZsWQ8ukLaPV0JppZ2n3aC/pdLoJ3YMeRZfQl9Jr6Afp5+mD9HcMDYYNg8dIYigZaxl7GacYtxkvmUymBdOXmchUMNcyG5lnmA+Yb1VYKvYqfBWRyhKVOpVWlX6V56pUVXNVP9V5qgtUq1UPq15WfaZGVbNQ46kJ1Bar1akdVbupNq7OUndSj1DPUV+jvl/9gvpjDbKGhUaghkijVGO3xhmNIRbGMmXxWELWclYD6yxrmE1iW7L57Ex2Bfsbdi97TFNDc6pmrGaRZp3mcc0BDsax4PA52ZxKziHODc57LQMtPy2x1mqtZq1+rTfaetq+2mLtcu0W7eva73VwnUCdLJ31Om0693UJuja6UbqFutt1z+o+02PreekJ9cr1Dund0Uf1bfSj9Rfq79bv0R83MDQINpAZbDE4Y/DMkGPoa5hpuNHwhOGoEctoupHEaKPRSaMnuCbuh2fjNXgXPmasbxxirDTeZdxrPGFiaTLbpMSkxeS+Kc2Ua5pmutG003TMzMgs3KzYrMnsjjnVnGueYb7ZvNv8jYWlRZzFSos2i8eW2pZ8ywWWTZb3rJhWPlZ5VvVW16xJ1lzrLOtt1ldsUBtXmwybOpvLtqitm63Edptt3xTiFI8p0in1U27aMez87ArsmuwG7Tn2YfYl9m32zx3MHBId1jt0O3xydHXMdmxwvOuk4TTDqcSpw+lXZxtnoXOd8zUXpkuQyxKXdpcXU22niqdun3rLleUa7rrStdP1o5u7m9yt2W3U3cw9xX2r+00umxvJXcM970H08PdY4nHM452nm6fC85DnL152Xlle+70eT7OcJp7WMG3I28Rb4L3Le2A6Pj1l+s7pAz7GPgKfep+Hvqa+It89viN+1n6Zfgf8nvs7+sv9j/i/4XnyFvFOBWABwQHlAb2BGoGzA2sDHwSZBKUHNQWNBbsGLww+FUIMCQ1ZH3KTb8AX8hv5YzPcZyya0RXKCJ0VWhv6MMwmTB7WEY6GzwjfEH5vpvlM6cy2CIjgR2yIuB9pGZkX+X0UKSoyqi7qUbRTdHF09yzWrORZ+2e9jvGPqYy5O9tqtnJ2Z6xqbFJsY+ybuIC4qriBeIf4RfGXEnQTJAntieTE2MQ9ieNzAudsmjOc5JpUlnRjruXcorkX5unOy553PFk1WZB8OIWYEpeyP+WDIEJQLxhP5aduTR0T8oSbhU9FvqKNolGxt7hKPJLmnVaV9jjdO31D+miGT0Z1xjMJT1IreZEZkrkj801WRNberM/ZcdktOZSclJyjUg1plrQr1zC3KLdPZisrkw3keeZtyhuTh8r35CP5c/PbFWyFTNGjtFKuUA4WTC+oK3hbGFt4uEi9SFrUM99m/ur5IwuCFny9kLBQuLCz2Lh4WfHgIr9FuxYji1MXdy4xXVK6ZHhp8NJ9y2jLspb9UOJYUlXyannc8o5Sg9KlpUMrglc0lamUycturvRauWMVYZVkVe9ql9VbVn8qF5VfrHCsqK74sEa45uJXTl/VfPV5bdra3kq3yu3rSOuk626s91m/r0q9akHV0IbwDa0b8Y3lG19tSt50oXpq9Y7NtM3KzQM1YTXtW8y2rNvyoTaj9nqdf13LVv2tq7e+2Sba1r/dd3vzDoMdFTve75TsvLUreFdrvUV99W7S7oLdjxpiG7q/5n7duEd3T8Wej3ulewf2Re/ranRvbNyvv7+yCW1SNo0eSDpw5ZuAb9qb7Zp3tXBaKg7CQeXBJ9+mfHvjUOihzsPcw83fmX+39QjrSHkr0jq/dawto22gPaG97+iMo50dXh1Hvrf/fu8x42N1xzWPV56gnSg98fnkgpPjp2Snnp1OPz3Umdx590z8mWtdUV29Z0PPnj8XdO5Mt1/3yfPe549d8Lxw9CL3Ytslt0utPa49R35w/eFIr1tv62X3y+1XPK509E3rO9Hv03/6asDVc9f41y5dn3m978bsG7duJt0cuCW69fh29u0XdwruTNxdeo94r/y+2v3qB/oP6n+0/rFlwG3g+GDAYM/DWQ/vDgmHnv6U/9OH4dJHzEfVI0YjjY+dHx8bDRq98mTOk+GnsqcTz8p+Vv9563Or59/94vtLz1j82PAL+YvPv655qfNy76uprzrHI8cfvM55PfGm/K3O233vuO+638e9H5ko/ED+UPPR+mPHp9BP9z7nfP78L/eE8/sl0p8zAAAAIGNIUk0AAHolAACAgwAA+f8AAIDpAAB1MAAA6mAAADqYAAAXb5JfxUYAAC7ESURBVHja5Lx5dFRV1rBfgHwYRQQVtB26ZWhtabtfeUGxGxFbUGZF8RMHGkVbRkekVYiKisicVhE0gEwBokgDAhEMMSSQkAECwcxkrlRSqVTqJqnxzs/vj5t7qUyAvr9e37fWV2vtleSm6p6n9t5nn733OVU2RaUaEP5PiqJSbeMXPBTA5/Xhzk9Vnd9vo3HFx21E2LYJX9IRgh6npvyCe9uaqS4K4C3IpXHFx9S99CTuJ8Z0KLVjRlA7ZgTuJ8ZgXxmJL+kIlwAkXBQk6HFq9pWRVA8fSvXwodYgdS892a6EA1UNvouqwXdR99KTeAtyfz2IL+kI1cOHYh9wqwVwKWJqpXbMCOv19gG3Imzb1JF2OgZxfr/NukH4jcNVfyEAE8IU+4BbKet1PfaVke3BtA/i/H6b8aIBt7a4mWmaC0nr55vmqRp8F5V33Mm5LhHtwbQF8SUdsSDCb1I1+K42g1xIWgOYYh9wK+e6RCBs29QxSIWus37aJM51iWjx4so77mwD1d5AHQ1eecedlN9yuyVlva6nrNf14Q7cEmRn4W7u3T2E9ME3UX7L7W1uZg5Weced1s3sA2613ql5LXzQjuRclwjcT4wxTXQeRHC7GLdnHPeensiCVwa3e0PznZk3EbZtwluQa0kofz8NcVNxr++Ce30XnNuv61Bcu7viXt8Fvyu7JYipjfGHxzD+8Bh2j+7fAiZcC+Y0zPDIbCyD6DyV6DyVeDcIQR2C39J4oieNJ3oSOnkVcnZ35Ozu6MVdDHF0N6S4C43OqJYg/0ydzb27hzDx0FjuPT2R+asfa6OVsl7X40s6QoWus/CQk6fWZPHChhxe3lbMCxtyrN9TyxSQSwidvMoC0XK6tRGybPjSRmOuNUKVo4Zxe8YxIu4+Jh4ay/jDY7j39MQWWjnXJYLGFR9Toes8tSaLiavTrIHDxfxfapkCwW8hy9YuhCmhk1fR1FRnaCS1NM4yy8RDYy2tjIkZRXq/HtYsCnqc2sJDTkYsTrU00J6YkEJQR7M/eEGY0MmrcOenqjZA2JmyzTJLuJiOe65LBHUvPUmGR2bE4lQmrk7jqTVZHcrE1WkMWpRIdJ4KnpUXBCHLRl3e16EWIOEaMU00/vAY9na/gsYVH/NdgYe+8w9bMBeSQYsSWXjICcFvL2ga+dhlFwcJ10rjio/ZklprgbSWiavTWvzdd/5hXt5W/OtATC201sq9u4eQ+PVijmSW0nf+YQYtSmTQosR2gUYsTmXQokT6zj9saeRCpmkJ0hxD2gOZeGgsI+Lu45+ps7FXlFmDmDDtSd/5h+k7/zCpZQpa9cwOQciyIR+77LyzFhXlMyZmFOP2jLP8orVWRsTdR2ppHFtSa+k6ZZM1WHvSdcomwyxySceayO4OWTY88TdirygzUkWf18eL2//RQiutYcwYE/Q4tagDOUQ8uo6uUzbRZ3qMJV2nbCLi0XU8tSbrolNXzu6OfOyylgEN4NOkaO5acw/j9ozr0ET37h5imehIZimPL91rAfSZHsOQBfuISS7E7vaTETeX0MmrOoQInbwK+dhlNKWsahni0zPSuGvNPW1M1BrI1NrOwt0WkCn2ijJSS+MYt2ccuQk3oxd36RCi8URPY+HLT1VbgGiSzPsx71laCddMe2Yygf6ZOtuScXvG0XfJn/n8YL+LQnjibyQ34WZ8Xl/bfKSoKL+FVi4EYwKZcu/uIQzaPoExMaPQcrq1ADFX33AI1+6u1OV9HVI6ShU/TYqm75I/dwjTHtDEQ2MZt2ccg7ZPaGGScIDWEBlxc42UoSMQ00StYdoDCgcbtH0Cbx+8p40ZTIBwiFM7RmB3+y+exZvT2YRpDdR6ZoVrw1xRWwN44m/Euf06A6Ki7NLrmnDNmH7TEdSg7RP4/GA/yLK1GdwEKNzSk1M7RlDlqPl1JefOlG2MXTGmXaAxMaMsB/XE34h4tH+7ANlrB7T2iV8OAlDlqOH9mPcsIBPKlF3R16Ad7GwlxoVberYAKCrKv1ghfmkg5sPldLIzZVsLqLErxpC9doAlp3aMICNurlGyVpRdSAu/HqS1Q58rd1JUlI87P1UtKsrHXlGG3e1HCOoov+x2wiX3RxT+o49L1IgutXxVUCfDIxNfLraQDI+M3e3/NdCXbhohqBNfLrIsVzZqmoT6dmXG0SBLTrmJLxd/CVRLECXcDGFaSC1TmHE0yKg4B0P2uxiy38WoOAePHaptAfHYoVqG7HcxcGc5o+IcfFfgsbQUPoYoSa213BbE78oGucTSwpJTbobFFjNgbQHdvi6g8/Z6Om+vZ8h+VxsQE7T/97UMWFvA+Og0UvIryfDIZBQ4CeXvt8a5IAhAY/RImlJWUaHrPHaolhuXFXHN+8e58qNcbomq5P6t3xG973WePLzPgnnsUG0LiP7f1zJwZzk3LisyctfSOFxOJ4lfLzYToQubxu/KpmpWBFWzInguOokrP8ql7/zDRMxLpFfUabasHwlZNnITbmbgznI6b6+3Bu7/fa2lrW5fF9Ar6jQD1hYwLLaYx5fupdi+EiGok748koa4qa010xKkKWUV2UM7kd6vB7tH9yfpnUFkLzQiZOGWnmgHO9N4oie9ok5bA4YPbkqvqNNc8/5xIuYl8tSaLOLLRXambENXF+PxNJD0ziAanVHhYaEliH1lJD/1iqD0qSsIzu2M/N550TZ3QjvYmS3rR1qDtwdhgpgwnabGMj46zRjQsxJdXYw7P1X1pY0GuaRjkMKxPah5qxuV8y6nct7l1LzVDfdyo6miHexM+ou9mblwKfdv/Y77t37HNe8fbwMQDhIxL5FOU2PZklqLJjUYdU7wWxBuN+ricBAF0KQG6pcNovZpw0fCQao/MEBcu7tSOLYHjnu7EZzbmeDczqyfNokrP8ptMXi4XDnzAJ0n72TIgn1oUoMB4VlpgIjj24I0payi9KkrqHj+Ssth2wM5c38f8p68D2nbHKRtc3h86d42A/eZHsOVMw9Y0nXKJmxDvyS1NA70z8Gz0qh5hNvbzpr6ZYMofzyiBUwLkOVdjfR/eVcao0dSl/d1aHx0GhHzEi0TXDnzAJ2mxtJpaixdp2yypM/0GLrcs5D3Y94ztNDsK7qjuxmzDBBz2rYGqZoVQc1b3dr4yfppk+g8eWeLd91aAxGPrqPbyKV0G7mUiEfXMWdz+nmQ0Jsgn1AbT/SkMXrkeZC6vK9DpU9d0S5I5bzLqf6gq6UV7WBn5q9+zDJBuEQ8us4SE6LLPQvpcs9CjmSW4ndlo1XPNBxWLiE34WbSX+wNapEBEsrfT/njERSO7WGBmDA1b3Wj9KkrSO/Xg1WjBjJl/CT+8sQ8a0BT/eGDhwN0uWchXe94ia07YkE+oSLc3gxyQt2yfiSrRg0E+YRqgRSO7UHh2B4UT7ragqmcdznFk67mp14ROO7txpTxk7AN/bLFgN1GLsU29EvrejiACdG59xQjKgu3GzVP9UwIvcmCVwYb102NmBHVBDFNVDUrgjP39yF98E0E5xox5Dcj5lsDhwOYQObg4dK59xR2RV8D4njEo/0NIEd3dkVfgy9t9HkfMTWSO6pXG63kjupF8aSrqXj+SoJzO1M573KmjJ/Eb0bM5y9PzGPBK4Mp3GKUEFvWj+Q3I+a3AOjcewp/eWKesUQ0T1mz2att7oSU9+F5EE2SqXvpSbKHdrIGNmHCoapmRVgh33LezZ3QNncyloGDnVnwyuA2IFvWj0Q+dplREzu6Wy0r9/KubVvg9pWRpPfrwZn7+1haMSHCxdSM/J4RWWufjiC9Xw/m9PgtN9w0uo1JbrhpNI0njAXTrAIbT/TEvb4LjdEj2641vqQjpPfrQfrgm1qYKHxKlz51BbmjerFj4G2WtAYwtWDKglcGG2ZoXrldu43AWDUrAmnbnLaRVZMayHvyPn7qZThoa38pfeoKap+OIDi3M6tGDeSGm0a3GTT82g03jeaGm0bj3H4d8rHLrN0I93LDpDsG3kb68si2a425hfZTrwjSB9/UBiZcM+YM6ghoyvhJpL/Ym+yFhknc67tYQVF+z3gjc3r8Fuf32zpOFTMeHXpRGDNfMYF2j+7PqlEDWTVqIOkv9rZ8SNvcCff6LlTOu9yK1Okv9mZOj9+S8ehQNKmBDhs17vxU9adeES1gwoHKH49oFyhcwhfKynmXWzOu4vkryR7aieyhnQjl7+84QzNNJGzbxN7uV1gw7WmntYZaLw2mmNdrn44ge2gnztzfx9od7zBnDa9t0pdHtgsTDhRustaaCndwEyLj0aG481PVS9r3FSUJj6eBrConMZHvnodpntrh2gkHCgcLl/TBN7G3+xXGLMlIo0LXjU7ixeoaUZIQ3C7OlTtJya8kJvJddgy8DctvWgGFaylcHPd2Y2/3K5jT47esGjWQrTtiyapy4nI6jUrvUmpfUytFRfmkZ6SxdUcs66dNYsfA2ywNtQBrJeb/dgy8jZjId/kx4YgF4fP6Ln1L3uyhhWvnSGYpOw6lEBP5LuunTWLDAw+x4YGHrAi74YGHWD9tEuunTSIm8l227ohtAyBK0i8/pNDagTVJxuf1YXf7OVfuJKvKMF16RhrpGWkcySwlJb+SrCqn1awRgjqaJP9nO0b/Zxo1v+ahS0ZqKJ9QCX5rJMyhN42aRj6h/udB5BKjiAp+i64uNrJ2M0Vs3rUiy4aU92G42X49iCYZDZjUMoX4ctFIcILfGgVU6E0LwEyCxKP98aWNxpc2GvFof+RjlyHlfdjxWnOxh93tJya5kIWHnDx2qJbnopP4NCmaYvtKC0LL6WYkQps70RA3laaUVbjzU1V7RRn2ijK8BbkWUJsM7VIAog7k8MyuPKtD1AJA/9zQQpYN9/oubFk/kpkLl7J4a0KbtrdZa/vSRrfMWS8GcSSzlGd25TH5VIjptTpR9T5SS+OMsrHZD3RHd7SDnTm1YwSzY2KsTtL46DSei07iSGZpm/tKeR8a5gnf0+vI8zfE5zAstpjptTrvifBJeeZ5LTQDkGXDtbsr0fte59mjDmaWaUyv1ZlZpvH3XJlRcQ6Grj5OTHJhy/t7VhrpwMVAog7kMCrOwcs+nZWaccak2L7S0oLpC6d2jGDJiUyWN8E6FVZqsLwJ5ruwYO5O9jFoUSIb4nPOT+/gtxf3kZjkQobFFreAaHRGGZoQbm+hhWd25fHsUQevHilgbo7bAmoNM2S/i6Grj3Mks9Tolcgn1Hb39MzHuXInw9edZrJd4z3xPISuLrYgCrf0ZOuOWKLzVFLLFDIKmlfr5EJmHMxhfoWvDczkUyELxl5RduFUUZNkIvdm8+BpkZd9eocQPyYc6XDnocpRQ+TebObmuFmptdTK5FMhBqwt4K1vMi4cWTMKnIyKczDZrvFJeWaHEBdrbVc5aphxMIflTR1rJaPA2TFI1IEc7k72tZwdYRCLtyZc6h4MMcmF7WrlwRSRAWsLiNyb3T6Iz+vjmV15jIpztIHwxN/I7JgY4svFS47CHk9DG62Y5hm4s5zx0Wntb0CnlikMiy3m06ToFpFSO9iZnSnbeGZXHkcyS8kocF6SHMksZc7m9AuaJyW/si3IltRaZsfEGNM09KZVs2bEzWV5EyzLlXn1SEG7MuNgTruy5JS73dlzd7IvPMi1BIlJLmRnyjbLJFawar7ZHi5NdrSS9jRyd7KPXlGnzQDXyjSlcYY2mk1SuKUnS05kslI7f9M9/HKgdaoh74nn/cR02NV7M9t2A9A/t/qf2uZOvB/zHvNdxk3Mm0bV+36VzK8wxHTWVutPmEbkE6q1hjQ3/yefCvGeeB7k1SPGlLsUeeubDOtnezJnczpvfZPBuXJnGEjzAqSri9FyulG4pSf3b/3OCvErNQNmxsEczpU70ST5kuWXJc9yiZXemQ3du5N9TK/VedmnW1qZm+M+v3r+gpTS42nA42nA5XRa4vE0hFd8zSDBb63cInvtAAYtSuTuZB+T7ZoFYy7tz+zK6+igQZtHRoGTyL3ZLab4M7vyGB+dxpAF+1i8NaEliLmWyNndsa+MZPi60/T/vpaJhTKT7ZqllZWaoZW3vsnA42m4IMS5cifPRScxN8fNeyK87NOZXqszsdDITa55/3i4dgVb0OPUTG2IR/vjzk9Vt6Qau5R3J/uYWCi3MJEJM2dzOkcyS80Q3WKrPia50IIIX2cmnwrxYIpIr6jTPBed1Mo0apFgpv0NcVMR3C5ESWLO5nS6fV3Ag6fFdmHmV/iYcTCHyL3ZRB3IsSRybzbP7MpjfoWvXYj+39cyZME+c7aEgTQ36smy0RA31dostrv9DF193IIJ9xcTxgSam+O2xAQwg9fMMo2JhTIPnjYgBi1KbC+RPq8REyR8iT9X7rRgWptpvssYLBwqHGB6rc7fc2ULYsh+F4MWJbLjUErH09c8ytcaxNTMCxtyGLC2oIUDT6/VO5TJdkMLJsTAneUMWpTYNotvE0eaj3rKxy6zun2t69mdKdt4fOley4lN35ls11pIOIC51D8XnWQu9xcGUQCteibyscuM5n31TKNqD5fm1H9DfA7PRScxdPVxhsUWMyy22Dq4MGS/i2GxxQxfd9oC2HEopb1WVcdtCU2Sqcv7OmTWpGbRLOV9SCh/P0GPUwvPvDIKnMQkFxK5N5s5m9N5LjqJ56KTeOubDFbvzSQlv7LN1P5FxzZ8Xp918v8SWk5WsWStLbr0a5oLHRdY/+GjPP8vtq7+0yCiJOHz+hDcLlxOJ2bzxeV0Irhdlk/9x0B8Xh9VjhoEt6s5rZTaFU1qQHC7qHLU/PpZ05EGqhw1uJxO0CVESSIlv5KoAznM2ZxufTJgzuZ0og7kkJJfaR1mcjmdVDlqflkc6ahSs1eUWdMzJrmQQYsSrYMJNy4raiHmYQWzD2IC2SvKLpa/dAzi8/qsc6cZBU6GLNjHlTMPcEtUJVMSdd45qRGdp7KxDOvDPu+c1JhxNMgtUZVcOfMAQxbss0K7vaLsQqbq+GCtCbEhPodOU2O58qNcZhwNsrMK4t0Xlp1VMONokCs/yqXT1FgrE7sATPvbJK0hblxWxDsnNWugvc7zcqFry3JlbomqbANzSdskpk9kFDjpOmWTpQnzne6sMgbbWWWYY8kpN0tOuYnOU1v8z9TcOyc1blxWRNcpmwwz6dLFjxr7vD5rY+eO13YSMS+Rh/co1iAby4wBluXKLDnl5rsCD1lVxk7FdwUelpxysyxXbvHcjWUwYb9CxLxE7nhtp7X10spELUHMMiHqQA6dJ+9k8KYaJh1u6ZRLTrnZklrb+hS3lURtSa1lySm39fyNZTAlUWfwpho6T95p1rqtS5LzICapJsmWNkbEBpiSqLMs1/gY3DsntfAuT4tDlkrYtci92bxzUmNjmaG9KYk6I2IDbbTStsBqjhma1EBKfiVdp2xiwNoCHt6jMOmwxjsnNev46KWUkaIksfCQk2W5Mu+c1Jh0WGPCfoUBawvoOmWT1d4Miy3nQczIuXpvJp2mxjJ4Uw0T9hsg09KM6fhcdBIxyYWXJM9FJzHjaJBpaTDpsAEzeFMNnabGGhVec+RtA1LlqAFd4vGley0Q8wZTEnWmpWGdWX3sUC3PHnW0K+b/n0qoZ1oaTEszfCQc5PGle0GXwv0k7PxI87S9EMjMMo35rvMdILPDbErrzlA4iOmw4SBh0/iXgUxLg8mnQvw9V2Zmmdau/D1XtpoxpiYe3qPw8B6FW6IqreOCvwpkWhqMinMwaFEi46PTfrFMXG38HLr6OHe8ttPykXZNYzrr4q0JdJoay4C1BS2cdfCmGuZsTrd6Hv/T5ozZJ7no9L1xWZE1fU0bD193unXx3GESFZNcyIb4nDazaUN8Dh6PkTy1O307CmgT9itM2K9YWnkuOumi26wTV6dZR43NXOXKj3LpPHknEY+us0DaDWiWnwCr92bSdcomBm+q4eE9ShsThTXh2jRn5mxOZ/CmmjYzZkRsgE5TY40Q33bhu/iiF66VcJjh604TuTfbUnnk3myGrzttQZgzZtJhzQrvfabHWGNccNELnz2tfSUcJjxADVhbwIC1BdYsMyOp+fyH9yhWGnAks/TS0gDTV4qK8q2NxU5TY7klqrIFTDhQ6+gZ/hwzdoSbpKgo/9LPj5hnR8yUwEwVw810MRkRG7BSRXPpLyrKv/RUsT2YI5mlLZLnEbEBK1q2lhGxASt5vuO1nZY5ioryL5TJX7icENwuioryjV1rr4+oAzkMWbDvouXEkAX7iDqQg8/rQ5MaLgZxaQWWJslWSWkWWBkFzl9UYP2PvgjFPNrj8/osM/2YcIQfE46QnpFmfL7K7SLocWpBj1Mz6+D0jLQWzzPb3b/6aI8SVnCbvXTTVOZxno6kqCjfKlPNUH4pIP9XPGz/N319UFnrf2iKLGi6LmggqCBoIOi6JuiqIqCrgqIrgqyrgoYu6JpiiK4LKgigCpquCCEdQdVVAU0VdP2iMGW29tplmtbcQNQ1QEXXNDQdQGsWHZBbvdQsKkTQfaiaBJrc/PyLPpQ2zqqbL9U10GV0TUbTZUCyQAoaJPaVinx5RmbVKZnVWRpf56r8WKlQFww2Q4bf8VdMXwsEtfkdGb97xSAb8yRG7df4zYYQ3deEsK2WsK1UsK1U6LIqxJWfKQzcEODVw0GS7KbG1F8Pout6C7WuL5Dpv1PBtlLEFgWXfyHTY61Ery91rvkiwLWfB7h6jcxV/5LoskLF9gl0+tjLI7FesuuxzKnrHeqneQdL143Bjacj6wqg4ZFUph8JYvusCdsXIldvhGvXi/T+SuS6dQrXrZO4fp3Ib76UuH5NiD6fi1z/mcgNnwa5epWMbbHG1StEvsoSjbeoq2i60h6MYNN1XTAhNF1vdlBoVFSG7/Nh+1Ti2o1Brl8v03uDyDVfN3DDVz5u+FKh15cKvdbp9FoHvT5X6PW5wjVr4LrPda6NkugTJdL1EwXbIpkVx5sdGaXZ8S9gGgNIJ6ipPHgghO3TED23h+ixTafXZpmb1ofos0ml+9dw1VcaV3wapMvKIF1WSVz+qULPzxV6faZw9Wc613yq0Xt1iN9Ehei+WMG2QObz03JHDtxsGk07P2XRmZ/hx7ZG5rqtMjdubqTHFonrNov8doPMZRvA9pmPqz8X+MNWhb/tkrg/VuGWaJXLPmmk85Imen6m0+sz6BMlcsNqP9etVujysU63jwIcrwy1N6UFm6Zrgma4KKBxrE7lyq999PnaT58dcMNWjV5bFa7d6sP2lcj/+szP6/FNHK2SqQtpSKqIKItUN2psyJH52yYXtkV+uq9UuP5fMj1XqVy9WuWGFSE6LQgxbHMQv6kVXW92B12wKZouSEjGNNMVJvwgYdugcGOsym+2q/TZqnD9dh3bVz5u3h4guVJtnpJa808zkJlBMMS7SQG6vB/gimUKvVdK9Fmu0nu5zLXLZGzvaWzLDhggmoysqwYIKoKqG+rKqVO5douP62JUfvutxg2xCn1iZTpv0rgpRuF0XQAIgRJElSUURUWWZWRZRpFlgrIKeIEg7yaC7X2FXkslei+XDVkmY1sQ4pFNDaA3hwcdNF0XbGjNZwNQWXZaxrZV5XexMjftFLnpW4ne34rYNvjZUywBQUJqEEkMoEk6oqIgySqipCCKEt6Qis8fRNEaAB+TtijYInV6Lwtx7VKRPstkIj5S6PGBRGFtwFCgApquCDYFTQANXZeZkiARsVPnlu9kfhcr0/cbiYivA4w94DM0oet4VQVJUQiJGiFRIiTKBEMSAX+QhoBIvU/C1SQCfpIKGrl8kZerFitcu0Tkuk9ErlsiYXtDYuMpYyobE0gVbIouC6DiDsgMiwtx406Z/rs0+u6WGPCNSI8tIZbnSoCCEvITkBRkWSMUkAgEJbz+EE2+IA3eAPUNjTR6fNTWSni9PuoFN/d8KtBpkcg1n3jp82GQ3h/6sc33seAHb/P6pYOmCTY0VQCNEkHhrgMhfrdL5k/fafT/XqT/boU+sRI/2r0AhESFYFDCF1TwBSWa/CE8TQHcjQFcDX6cdQGq63w43PWU1AoEmup4emMjtvl+enzop/d7Aa57N4Btvo/Z37jCHBzBpuqaAHDOHWDo/iD99in8+XuZO/er/H6fxsB/h0irDgGqoYGAhOAL0eALUd/oo87TRK2nCUddI3anQKXTTUl1DUVVNXga6nh2mwvb6066L3Bz3btOekU2YXtd5MVNDmuVVtEFm6brAmiUu4OMPODnjv0idx+UGHpQ4q6DEnf928+h0iCg0egN0OgXqW8MUCd4cXm81LgbqHIJlDs9lFd5KK90U1hWQ3GlgLOqlrs/rsQ2q45rFjq57q0yukc6sL3iYc62akBDR0fRNcGmq5oAQYSAyuQEibsPhnjgkMYD8T4ePOTnv/ZrfJrtBTWE4A3ibvRTJ3hx1jdRXddApbOeMoebEruL3Ao3p8vqOVVSR1JuDZkFtbywvgDb0zl0eqmanm+Wct2bFdhmlvP2Po/hH6qIrmiCTdNUAVVElTVeyfTz10My435UGHNE5JGfJIYf1ZiV4kFo8uILBKirD+LwBHC43Dhq6ymurqfAUU9ORS05RSU0NHmQVRW/JCMqOho6354U6DEri04z8+nxWim2fxSx8ZgLEAlJCqoiCzZZUwVZVECDjUVNDD8s8sRRlSmJOs8mwbPHZJ466iO2yI8aDNJU56a8tpGqaicOZx2FVfWcLa8lq7CMBn8IHfAGZQKSik/SQAoBOjEZtdiezqTTS/l0fzmPrFIBNB9CUCcoSYJN0TTBKymgS5TXBXn8pwCTj8lMT1WZmarx+nGR2Rk680+GOFleh9/bgMtZR3V1HYWVLvJKajiTV0pVjRsV8IVEgrLaLApeERSCAAx5/xS2+48zZvlZAmKIQFMTHq+PppAi2DRdFQIyyKIfRImoMz6ePO7lpUyJl08r/PN0iMjTEh9kS6zNEUgp92GvaaDAXstZh4DLG0JoChAMyviDMn5Jxi/K+EISIVnFr0h4JWPZ33a8mNteSCI6vhpZbMDhaqChyYfHHxRsmhYURBECoRDoMvkukVfTFN7IlHk7W+aDXIlVOSHW5ob4qhi2F4v8WNLIiSov5wLgkVVERSUYMqa2LyTjF1UCkkpQ1vGLImJAxCsai2SdKFJQ6aG0ooqK+gBuVxOCTxBsuq4IkqQSFCVCkgyqzg8lXt5J9/H+WViVJ7G+KMSOEoVdJSp77DJxdRrH3Rq5goLDJyMERRqCIt6QbPiHqBAQFSRJJSCrhGSZJklDUs/nIefsNRRXe3DWefE0NjUf21BURFEiGDRWVH9I5Nu8Rt7Pk/lXocbWIpFvKzT2VSr8YJdIcEqk1Svke2TsPhV3SMYTEmkISngDCr6QTFBSCUkqQUnFL2kEJUNLflFF1aGuyUepow6HuxG34DdyVkVRkCQFUVLxBWR0ScEfFPmuuIG1hTIxpSr/rpA46FBIqJHJdGmcqVPJa1Co9MrUBiTcQQlPQKYhoNAUUvCJCn5JJSApBCTZEr8oEVJU/IpKiaOOmnov9Q1+QyOqqiErGqKiIYk6/mAATQ4QalRItPvZU+EnvkrmxxqJRJdIVp1KTr1GQaNChVei2idTE9BwBRTqAzKeoEyjKNMkKvglhaCkNAMZogAeX4DS6npcDQE8jYHmM0aajqLqyLJOSNbwSTJev0woEKCxyU9OdZCEkgAJ1UGSBYWsBo3cRihq0qj0KVT5ZBwBjdqQRn1IRhBVGiWVRlklqOiIikZQ1hAV4ytjJE2n0ummqt6LU/AjNAYEm64jaBqoqo6iaEiKhiirBESVhkAQr9eH0ChSUu3nVGkdGY4mUmt8ZLoC5DWoFDUplHpVKnw6VT6ZWn+IuqCEJ6TQEFINzUgSflXFJ8nUe304XALVdQ3UNwaob/TT5A0ZILoO4TCyrBKSZHxBGcEfxNPgpdETwO32U+ZoIKesnrPlHrLtbn6urCfPXk+B3U2R3cO5qgbOVTVQUilwrkKgtEqguLKe4sp6yhwNlNg9VLkEhKYgjd4QTX6RYFA+X2Dpuo6maaiqiqqqKIqGKKn4QwrekERjIIC70YenMUBjk0S9EKK23our3ovb48Xj8SI0BfD4ROq9IdyNQeoa/Lg8AZxuPzV1PuobRASfguAN0egP4Q1KBEMykqwKNkAxMnpDNM1oSxhQGrKiI6oqTapIkyTiDYUIiDLBkEwoICOGjHghKxqKqqCoEooqEVJFgkqIkBIiKIsEpBB+MYA/FMAXkgiICiHRmK2KoilWo6bZRIKu61bjRdd1QdEQVBVBkzRBlVRBFhVBVTRBUXRBknVB1hAUECQQNF0XUHVB13RB0XRBVDRBUjRBUTVBUlRBlBRBlGQhJGuCJOuCouiCpuqCqqpl/7Eemqor5HnS2Ja/hPezpvCP1PuYlfo3vvo5EnfA0baH9qs+CKZpBIIh7DUuyuw1lNprqHDU4mnwoqoamq5xyn2YVTkv8cKJO3n+TH+eTB7Ao/H9eSr+TnbmrfyfgdiddZzKKaK0yklhuYN6oWVfvabay+6Tu3gzaSJPpPZm9E9XMmnvH1n60wKSanZypuEg35WuZlrCMLb9vPSXgzicdWTkFLX7vya5Dq/spk62s8v1AW+cu53ns29kSd6z/Fi9mZ/L8tpqVFfZeHYxBe7MSwdJy85v8Xd1oJwDFRtZlTeTD88+wcKsMSzMGsv8rL8wNbMnc7LuJN6xg6AcsF6TW1xBkzfQct9P8pDrSkfT1QuDKKrKz8UV1t+V3kKi89/m1YyhvHlyMPOz/ouFZ4fwYe59fJAzjLfO3s66wuep8p7jbF0iUTkzOe76/rzZ6jxUVteGtch06gL2C4PIikJFtcv6e3/ZeuamDOHNU//NivwxfFY8jnXlE/iyYiKflz/Eh4WD2Gv/CL/YQIJjI2+dvJvXTt7FtJS+LPt5OvVBY383KEoUlFaGzSz5wqb5ubC0WSsyG3PfZUbKnXzw8wOsKX6EdWUT+NI+nq8cY1nrGMnikjuJd0Xhld1sr3iTt37+IyuLHmZN0WMszxnPzLSBvJnxMMWNPxv7vUITLrdw8VlzMswnNud+xD+O3cGy3LF8ce5R1pZN4IuKsXzlGM0X1SP4uPJ2jgpraJAcfFb+CJHnbuOz8pF8UT6OL0om8nnRJFblPcrLaXfxxolROHzGd2idq7xIHBEavTQFQwAcLNvMP5Lu5JOcsawpmsRnJROIKnuYtVWjWVP9Vz6q7McRz0pUTSa2Zh6LSgeytOJPfGa/j3UVY1lTMoFPz01kdcEjLM95hNmp/8UHmU+j6MYnlrJyz3UMknHW0IbDW8rLyfexIGs4nxU8zqqi8Xx07gGiKkfyheN+ltnvJEFYGdYOFWlUqjniWcGK8iFElQ1jTek4Pi2awOqCR1iZ9wgfnx3Hs4l9+aHc+BqH2voGRFFqC+JpaEKSjOR2Y84iZqX8majcx1ieN57Xc+/hvXPD+aziAZaX30VGY0yH0/1s00E+KR7KquL7+ezceFbnT2BFzkSW5Uzg7VP38UbKQ3hCdc1aKWoLktHsG06/nbnJ9/H+6VGsyJnIC9l38kreMNaUPsKSkkHsdy26aABMcK3lw4L/5l9FY1mdP56lOeP55Ox4Psh+mOeT7+BAyUZj17O8qiWIKMkUlNoBOFQaw4zkQSw+M5bZp+7in7mPsKnkFVade4DPSsfTJNVeFCSk+lhbPIVl+Q+wMnccS8+OY/GZsXxwZjTTj9/OkqwXACi3O/H5A+dBKhy1lFQac33t2bf5R/KdvJnxFxadnkSyYzuf5j3BssIR/Kt4DBvLp/NF2dOsqXiSNRVPsKbyCeNnxZN8XjaFz4ufJrr4Bf5V8Agr8h5iWc5YPs4ey4enR/P+6YeYnfZn3kh9CAUfqgz2Gtd5kLOFpZTYjUMHH516jmlJA3jjxHCO2XexteBtFpwZyqqC0awo+huLCv7Eu4W38V7x73mvtD/vl/Xl/bJ+vFfye94tuo2F+X/g3dw/szT/b6zIHcMnZ0fz0ZmHWXT6ISKzRvJq5mBeSh5MSeNZyzyyrBggWTlFlNsNssiMKYz9oQe7i/9FmmM/r6bezZKfx7Is5yGW5f+NFYUjWHXuflaXDmN12V+JKhtGVNkwVpX9lZXFw1lRNILl+Q/ySc6DfHRmFIuyRhF5ciRvZ/6NNzPvZ3baIJ5N+AM/1xsfXcg9V47XH2wLMidpFE/9eAcVQg7Lsp7j9fShfHTmIT4+M4rIrKG8ljGAeSf78eaZfszP7sc/z/bln9n9mH+mH29m9eO1jP7MPfF7ZibfxvSE3zP1UD+eiruVxw/cxIT9fRj+764Mje3M6bqjAOQVl+MPhgyQvHPllFQapnkhfgRf5y7haNV3PJvwe945+QDvnnyAf2bcQ0zR22S7fySzbj+Z7n2cdO/jZP1eTtbvI9O9j8y6fWS49pHm3Edq9T6OV+0luXIPRyt2k1C+i/jybzhYupUfSrfjV40wX1zhQNN0A8RR66bEbjjr5p+Xc9IRz9snJvJ88h94O/N+3s64j1dS7mJLXuT/v0e/vT6qa93nnVXXdXLOlRtJi6qSWLmL8Yd682rGvcxLG8qbJ4byRuoQXj56L+UNuRcdoDHk5kDJNvaXbuZA2Rb2l21hX9nX7C3byNaCKJKr4pqnbw3+QLBlQDttxn4dPsh4hseP3sjcjP/m5dRBvJYymNdTBjMtvh8rT865KMja0wsZvqsr4/f3ZNyBnjx88CpGxV3BiAM2bt5iY8PPKwz/KKlsG1lDooTgCRJAYPKR/jyb2pcZaQOZdfyPzDn+J145/l/MSfojU364lW05yzuE2F30FU/80JcZSQN5+fifmH38Tmam3MGM1Dt4LOE6pv90DyHFCGLZ+SXtL3pn88rJCR5hbPy1TEq6jqnJv2XGsduZdfwPzD52By8n/5FZSX9g8sGbeDflGU7VHMUTqKMhVM/Z2hMsSZ/JY3G38I/E25l77I/MOv4HZhy/nRkptzE1+Rbu+beNhMrvjLEKSi+cj0T+8AaPZfTi2eQ/8Gj89fz96C3MSB7AjOTfMzPpNmYn3c7MowN4/IdrmXKoPy8l3MtLP/2Fpw7fxiMHr+HFxH7MTrqNmUm/56XkAbyY3I/pyb/jr/tsRJ542hqnOGydaRdkxv6J/DXBxvflX/Fd0Rru2W3jmYTrmZnUnxlJ/ZhxtB+zjg5g1tH+vJBwM1Pjr+fZ+Ot5PuFmZiX2Y9ZR43kvJfXlpeR+PJ90M3/da2Nm4gME5MZ2c5F2QV5OeYA/7rZxrOYgANE/f8S933ViTFxXZiX1ZfbRvsxK7MusxFuZnXgrs8JkZuKtzEi8lZlHf8espL48Gd+Lu3fbeDVpLA1BY+kvc7T7ZTktQUQlyLQjg/nzv20cyo+zrsdX7OKR/bcybLeNp368hpd+uok5ib9lbuKtzfI75ib+jtmJv2PGT7fwfMJveOj7zty/O4JPs+YjKsYUdTc04Wloav/YRusLz/04lAeTIsgsPENewfnc0is1EH32Qx47MICH913F+O//F+O/t/H4wW7877gIHtnfhXHfd2Hs91cyZl9v3k19lgLPaev15TV1NDR6Oz4/0vrC26ceYVhcL45X/GB4d2Eljf7Q+cJI9pHqiGPVqVeZd+wRZicOZ0bCvbyS9DAfpD3PnnNfUuO3ny9NVI2T+eVI8oVPGrUB2ZsfzX1HehJTtMK6FgyJZOYW0+gXf1EIz8wro9LhvKTn2lrugkMoFOS5n/7C0APXYK8tb3GepMrh5HB8Cmknz5JbXEpBSQVlFbVU2N0UlVWRW1RK1s95/JCQzMkzPyPLMpqm4ff7CQQChEIhJElCURQ0TcPsVOm6fn6tCT+oUOkq4bGE27n/qzv4KeMIwVCQQCBAbV0ttXW1VFRWkJ19lrS0DJKSj5F4NInk5OOcPHmK/Px8amtrcbvd1NTU4HQ6cbvdNDU1WTCyLKOqaguYDmvfgNzE4bIYdpWv4UT5EezuMkQl9B877PT/DQC7cLwx8LR3hQAAAABJRU5ErkJggg==) no-repeat;padding-left:40px}
+ .browser .browser-firefox{background-position:0 -34px}
+ .browser .browser-ie{background-position:0 -68px;margin-left:0px}
+ .browser .browser-360{background-position:0 -170px;margin-left: -27px}
+ </style>
+ </head>
+ <body style="margin-top:50px">
+ <h1>请升级您的浏览器,以便我们更好的为您提供服务!</h1>
+ <p>您正在使用 Internet Explorer 的早期版本(IE11以下版本或使用该内核的浏览器)。这意味着在升级浏览器前,您将无法访问此网站。</p>
+ <hr />
+ <h2>请注意:微软公司对Windows XP 及 Internet Explorer 早期版本的支持已经结束</h2>
+ <p>
+ 自 2016 年 1 月 12 日起,Microsoft 不再为 IE 11
+ 以下版本提供相应支持和更新。没有关键的浏览器安全更新,您的电脑可能易受有害病毒、间谍软件和其他恶意软件的攻击,它们可以窃取或损害您的业务数据和信息。请参阅
+ <a href="https://www.microsoft.com/zh-cn/WindowsForBusiness/End-of-IE-support"
+ >微软对 Internet Explorer 早期版本的支持将于 2016 年 1 月 12 日结束的说明</a
+ >
+ 。
+ </p>
+ <hr />
+ <h2>您可以选择更先进的浏览器</h2>
+ <p>推荐使用以下浏览器的最新版本。如果您的电脑已有以下浏览器的最新版本则直接使用该浏览器访问即可。</p>
+ <ul class="browser">
+ <li class="browser-chrome">
+ <a href="https://www.google.cn/chrome/browser/desktop/index.html?hl=zh-CN&standalone=1"> 谷歌浏览器<span>Google Chrome</span></a>
+ </li>
+ <li class="browser-firefox">
+ <a href="https://www.mozilla.org/zh-CN/firefox/new/"> 火狐浏览器<span>Mozilla Firefox</span></a>
+ </li>
+ <li class="browser-ie">
+ <a href="https://windows.microsoft.com/zh-cn/internet-explorer/download-ie"> IE 11 浏览器<span>Internet Explorer</span></a>
+ </li>
+ <li class="browser-360">
+ <a href="http://se.360.cn/"> 360安全浏览器<span>360 Chrome</span></a>
+ </li>
+ <div class="clean"></div>
+ </ul>
+ <hr />
+ </body>
+</html>
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..4ad18a3
--- /dev/null
+++ b/index.html
@@ -0,0 +1,217 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
+ <meta name="renderer" content="webkit" />
+ <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
+ <link rel="icon" href="/favicon.ico" />
+ <title>通用多租户管理系统</title>
+ <!--[if lt IE 11
+ ]><script>
+ window.location.href='/html/ie.html';
+ </script><!
+ [endif]-->
+ <style>
+ html,
+ body,
+ #app {
+ height: 100%;
+ margin: 0px;
+ padding: 0px;
+ }
+
+ .chromeframe {
+ margin: 0.2em 0;
+ background: #ccc;
+ color: #000;
+ padding: 0.2em 0;
+ }
+
+ #loader-wrapper {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ z-index: 999999;
+ }
+
+ #loader {
+ display: block;
+ position: relative;
+ left: 50%;
+ top: 50%;
+ width: 150px;
+ height: 150px;
+ margin: -75px 0 0 -75px;
+ border-radius: 50%;
+ border: 3px solid transparent;
+ border-top-color: #FFF;
+ -webkit-animation: spin 2s linear infinite;
+ -ms-animation: spin 2s linear infinite;
+ -moz-animation: spin 2s linear infinite;
+ -o-animation: spin 2s linear infinite;
+ animation: spin 2s linear infinite;
+ z-index: 1001;
+ }
+
+ #loader:before {
+ content: "";
+ position: absolute;
+ top: 5px;
+ left: 5px;
+ right: 5px;
+ bottom: 5px;
+ border-radius: 50%;
+ border: 3px solid transparent;
+ border-top-color: #FFF;
+ -webkit-animation: spin 3s linear infinite;
+ -moz-animation: spin 3s linear infinite;
+ -o-animation: spin 3s linear infinite;
+ -ms-animation: spin 3s linear infinite;
+ animation: spin 3s linear infinite;
+ }
+
+ #loader:after {
+ content: "";
+ position: absolute;
+ top: 15px;
+ left: 15px;
+ right: 15px;
+ bottom: 15px;
+ border-radius: 50%;
+ border: 3px solid transparent;
+ border-top-color: #FFF;
+ -moz-animation: spin 1.5s linear infinite;
+ -o-animation: spin 1.5s linear infinite;
+ -ms-animation: spin 1.5s linear infinite;
+ -webkit-animation: spin 1.5s linear infinite;
+ animation: spin 1.5s linear infinite;
+ }
+
+
+ @-webkit-keyframes spin {
+ 0% {
+ -webkit-transform: rotate(0deg);
+ -ms-transform: rotate(0deg);
+ transform: rotate(0deg);
+ }
+
+ 100% {
+ -webkit-transform: rotate(360deg);
+ -ms-transform: rotate(360deg);
+ transform: rotate(360deg);
+ }
+ }
+
+ @keyframes spin {
+ 0% {
+ -webkit-transform: rotate(0deg);
+ -ms-transform: rotate(0deg);
+ transform: rotate(0deg);
+ }
+
+ 100% {
+ -webkit-transform: rotate(360deg);
+ -ms-transform: rotate(360deg);
+ transform: rotate(360deg);
+ }
+ }
+
+
+ #loader-wrapper .loader-section {
+ position: fixed;
+ top: 0;
+ width: 51%;
+ height: 100%;
+ background: #7171C6;
+ z-index: 1000;
+ -webkit-transform: translateX(0);
+ -ms-transform: translateX(0);
+ transform: translateX(0);
+ }
+
+ #loader-wrapper .loader-section.section-left {
+ left: 0;
+ }
+
+ #loader-wrapper .loader-section.section-right {
+ right: 0;
+ }
+
+
+ .loaded #loader-wrapper .loader-section.section-left {
+ -webkit-transform: translateX(-100%);
+ -ms-transform: translateX(-100%);
+ transform: translateX(-100%);
+ -webkit-transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
+ transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
+ }
+
+ .loaded #loader-wrapper .loader-section.section-right {
+ -webkit-transform: translateX(100%);
+ -ms-transform: translateX(100%);
+ transform: translateX(100%);
+ -webkit-transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
+ transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
+ }
+
+ .loaded #loader {
+ opacity: 0;
+ -webkit-transition: all 0.3s ease-out;
+ transition: all 0.3s ease-out;
+ }
+
+ .loaded #loader-wrapper {
+ visibility: hidden;
+ -webkit-transform: translateY(-100%);
+ -ms-transform: translateY(-100%);
+ transform: translateY(-100%);
+ -webkit-transition: all 0.3s 1s ease-out;
+ transition: all 0.3s 1s ease-out;
+ }
+
+ .no-js #loader-wrapper {
+ display: none;
+ }
+
+ .no-js h1 {
+ color: #222222;
+ }
+
+ #loader-wrapper .load_title {
+ font-family: 'Open Sans';
+ color: #FFF;
+ font-size: 19px;
+ width: 100%;
+ text-align: center;
+ z-index: 9999999999999;
+ position: absolute;
+ top: 60%;
+ opacity: 1;
+ line-height: 30px;
+ }
+
+ #loader-wrapper .load_title span {
+ font-weight: normal;
+ font-style: italic;
+ font-size: 13px;
+ color: #FFF;
+ opacity: 0.5;
+ }
+ </style>
+ </head>
+
+ <body>
+ <div id="app">
+ <div id="loader-wrapper">
+ <div id="loader"></div>
+ <div class="loader-section section-left"></div>
+ <div class="loader-section section-right"></div>
+ <div class="load_title">正在加载系统资源,请耐心等待</div>
+ </div>
+ </div>
+ <script type="module" src="/src/main.ts"></script>
+ </body>
+</html>
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..92dbaab
--- /dev/null
+++ b/package.json
@@ -0,0 +1,80 @@
+{
+ "name": "hs-vue-ts",
+ "version": "1.0.0",
+ "description": "通用多租户管理系统",
+ "author": "YuBo",
+ "license": "MIT",
+ "scripts": {
+ "dev": "vite serve --mode development",
+ "build:prod": "vite build --mode production &&vue-tsc --noEmit",
+ "preview": "vite preview",
+ "lint": "eslint src/**/*.{ts,js,vue} --fix",
+ "prepare": "husky install",
+ "prettier": "prettier --write ."
+ },
+ "dependencies": {
+ "@element-plus/icons-vue": "2.1.0",
+ "@vueup/vue-quill": "1.2.0",
+ "@vueuse/core": "9.5.0",
+ "animate.css": "4.1.1",
+ "await-to-js": "^3.0.0",
+ "axios": "^1.3.4",
+ "echarts": "5.4.0",
+ "element-plus": "2.2.27",
+ "file-saver": "2.0.5",
+ "fuse.js": "6.6.2",
+ "js-cookie": "3.0.1",
+ "jsencrypt": "3.3.1",
+ "crypto-js": "^4.1.1",
+ "nprogress": "0.2.0",
+ "path-browserify": "1.0.1",
+ "path-to-regexp": "6.2.0",
+ "pinia": "2.0.22",
+ "screenfull": "6.0.0",
+ "vform3-builds": "3.0.8",
+ "vue": "3.4.21",
+ "vue-cropper": "1.0.3",
+ "vue-i18n": "9.2.2",
+ "vue-router": "4.1.4",
+ "vue-types": "^5.0.3"
+ },
+ "devDependencies": {
+ "@iconify/json": "^2.2.40",
+ "@intlify/unplugin-vue-i18n": "0.8.2",
+ "@types/crypto-js": "^4.1.1",
+ "@types/file-saver": "2.0.5",
+ "@types/js-cookie": "3.0.3",
+ "@types/node": "18.14.2",
+ "@types/nprogress": "0.2.0",
+ "@types/path-browserify": "^1.0.0",
+ "@typescript-eslint/eslint-plugin": "5.56.0",
+ "@typescript-eslint/parser": "5.56.0",
+ "@unocss/preset-attributify": "^0.50.6",
+ "@unocss/preset-icons": "^0.50.6",
+ "@unocss/preset-uno": "^0.50.6",
+ "@vitejs/plugin-vue": "5.0.4",
+ "@vue/compiler-sfc": "3.2.45",
+ "autoprefixer": "10.4.14",
+ "eslint": "8.36.0",
+ "eslint-config-prettier": "8.8.0",
+ "eslint-plugin-prettier": "4.2.1",
+ "eslint-plugin-vue": "9.9.0",
+ "fast-glob": "^3.2.11",
+ "husky": "7.0.4",
+ "postcss": "^8.4.21",
+ "prettier": "2.8.6",
+ "sass": "1.56.1",
+ "typescript": "4.9.5",
+ "unocss": "^0.50.6",
+ "unplugin-auto-import": "0.13.0",
+ "unplugin-icons": "0.15.1",
+ "unplugin-vue-components": "0.23.0",
+ "vite": "^5.1.6",
+ "vite-plugin-compression": "0.5.1",
+ "vite-plugin-svg-icons": "2.0.1",
+ "unplugin-vue-setup-extend-plus": "0.4.9",
+ "vitest": "^0.29.7",
+ "vue-eslint-parser": "9.1.0",
+ "vue-tsc": "0.35.0"
+ }
+}
diff --git a/public/favicon.ico b/public/favicon.ico
new file mode 100644
index 0000000..3f919d8
--- /dev/null
+++ b/public/favicon.ico
Binary files differ
diff --git a/src/App.vue b/src/App.vue
new file mode 100644
index 0000000..41ffdd4
--- /dev/null
+++ b/src/App.vue
@@ -0,0 +1,21 @@
+<template>
+ <el-config-provider :locale="appStore.locale" :size="size">
+ <router-view />
+ </el-config-provider>
+</template>
+
+<script setup lang="ts">
+import useSettingsStore from '@/store/modules/settings'
+import { handleThemeStyle } from '@/utils/theme'
+import useAppStore from '@/store/modules/app';
+
+const appStore = useAppStore();
+const size = computed(() => appStore.size as any);
+
+onMounted(() => {
+ nextTick(() => {
+ // 初始化主题样式
+ handleThemeStyle(useSettingsStore().theme)
+ })
+})
+</script>
diff --git a/src/animate.ts b/src/animate.ts
new file mode 100644
index 0000000..7a23df8
--- /dev/null
+++ b/src/animate.ts
@@ -0,0 +1,48 @@
+// 前缀
+const animatePrefix = 'animate__animated ';
+// 开启随机动画 随机动画值
+const animateList: string[] = [
+ animatePrefix + 'animate__pulse',
+ animatePrefix + 'animate__rubberBand',
+ animatePrefix + 'animate__bounceIn',
+ animatePrefix + 'animate__bounceInLeft',
+ animatePrefix + 'animate__fadeIn',
+ animatePrefix + 'animate__fadeInLeft',
+ animatePrefix + 'animate__fadeInDown',
+ animatePrefix + 'animate__fadeInUp',
+ animatePrefix + 'animate__flipInX',
+ animatePrefix + 'animate__lightSpeedInLeft',
+ animatePrefix + 'animate__rotateInDownLeft',
+ animatePrefix + 'animate__rollIn',
+ animatePrefix + 'animate__rotateInDownLeft',
+ animatePrefix + 'animate__zoomIn',
+ animatePrefix + 'animate__zoomInDown',
+ animatePrefix + 'animate__slideInLeft',
+ animatePrefix + 'animate__lightSpeedIn'
+];
+// 关闭随机动画后的默认效果
+const defaultAnimate = animatePrefix + 'animate__fadeIn';
+// 搜索隐藏显示动画
+const searchAnimate = {
+ enter: '',
+ leave: ''
+};
+
+// 菜单搜索动画
+const menuSearchAnimate = {
+ enter: animatePrefix + 'animate__fadeIn',
+ leave: animatePrefix + 'animate__fadeOut'
+};
+// logo动画
+const logoAnimate = {
+ enter: animatePrefix + 'animate__fadeIn',
+ leave: animatePrefix + 'animate__fadeOut'
+};
+
+export default {
+ animateList,
+ defaultAnimate,
+ searchAnimate,
+ menuSearchAnimate,
+ logoAnimate
+};
diff --git a/src/api/demo/demo/index.ts b/src/api/demo/demo/index.ts
new file mode 100644
index 0000000..7441720
--- /dev/null
+++ b/src/api/demo/demo/index.ts
@@ -0,0 +1,62 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { DemoVO, DemoForm, DemoQuery } from '@/api/demo/demo/types';
+
+/**
+ * 查询测试单列表
+ * @param query
+ * @returns {*}
+ */
+export const listDemo = (query?: DemoQuery): AxiosPromise<DemoVO[]> => {
+ return request({
+ url: '/demo/demo/list',
+ method: 'get',
+ params: query
+ });
+};
+
+/**
+ * 查询测试单详细
+ * @param id
+ */
+export const getDemo = (id: string | number): AxiosPromise<DemoVO> => {
+ return request({
+ url: '/demo/demo/' + id,
+ method: 'get'
+ });
+};
+
+/**
+ * 新增测试单
+ * @param data
+ */
+export const addDemo = (data: DemoForm) => {
+ return request({
+ url: '/demo/demo',
+ method: 'post',
+ data: data
+ });
+};
+
+/**
+ * 修改测试单
+ * @param data
+ */
+export const updateDemo = (data: DemoForm) => {
+ return request({
+ url: '/demo/demo',
+ method: 'put',
+ data: data
+ });
+};
+
+/**
+ * 删除测试单
+ * @param id
+ */
+export const delDemo = (id: string | number | Array<string | number>) => {
+ return request({
+ url: '/demo/demo/' + id,
+ method: 'delete'
+ });
+};
diff --git a/src/api/demo/demo/types.ts b/src/api/demo/demo/types.ts
new file mode 100644
index 0000000..ea51d32
--- /dev/null
+++ b/src/api/demo/demo/types.ts
@@ -0,0 +1,90 @@
+export interface DemoVO {
+ /**
+ * 主键
+ */
+ id: string | number;
+
+ /**
+ * 部门id
+ */
+ deptId: string | number;
+
+ /**
+ * 用户id
+ */
+ userId: string | number;
+
+ /**
+ * 排序号
+ */
+ orderNum: number;
+
+ /**
+ * key键
+ */
+ testKey: string;
+
+ /**
+ * 值
+ */
+ value: string;
+}
+
+export interface DemoForm extends BaseEntity {
+ /**
+ * 主键
+ */
+ id?: string | number;
+
+ /**
+ * 部门id
+ */
+ deptId?: string | number;
+
+ /**
+ * 用户id
+ */
+ userId?: string | number;
+
+ /**
+ * 排序号
+ */
+ orderNum?: number;
+
+ /**
+ * key键
+ */
+ testKey?: string;
+
+ /**
+ * 值
+ */
+ value?: string;
+}
+
+export interface DemoQuery extends PageQuery {
+ /**
+ * 部门id
+ */
+ deptId?: string | number;
+
+ /**
+ * 用户id
+ */
+ userId?: string | number;
+
+ /**
+ * 排序号
+ */
+ orderNum?: number;
+
+ /**
+ * key键
+ */
+ testKey?: string;
+
+ /**
+ * 值
+ */
+ value?: string;
+}
diff --git a/src/api/demo/tree/index.ts b/src/api/demo/tree/index.ts
new file mode 100644
index 0000000..562deb6
--- /dev/null
+++ b/src/api/demo/tree/index.ts
@@ -0,0 +1,62 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { TreeVO, TreeForm, TreeQuery } from '@/api/demo/tree/types';
+
+/**
+ * 查询测试树列表
+ * @param query
+ * @returns {*}
+ */
+export const listTree = (query?: TreeQuery): AxiosPromise<TreeVO[]> => {
+ return request({
+ url: '/demo/tree/list',
+ method: 'get',
+ params: query
+ });
+};
+
+/**
+ * 查询测试树详细
+ * @param id
+ */
+export const getTree = (id: string | number): AxiosPromise<TreeVO> => {
+ return request({
+ url: '/demo/tree/' + id,
+ method: 'get'
+ });
+};
+
+/**
+ * 新增测试树
+ * @param data
+ */
+export const addTree = (data: TreeForm) => {
+ return request({
+ url: '/demo/tree',
+ method: 'post',
+ data: data
+ });
+};
+
+/**
+ * 修改测试树
+ * @param data
+ */
+export const updateTree = (data: TreeForm) => {
+ return request({
+ url: '/demo/tree',
+ method: 'put',
+ data: data
+ });
+};
+
+/**
+ * 删除测试树
+ * @param id
+ */
+export const delTree = (id: string | number | Array<string | number>) => {
+ return request({
+ url: '/demo/tree/' + id,
+ method: 'delete'
+ });
+};
diff --git a/src/api/demo/tree/types.ts b/src/api/demo/tree/types.ts
new file mode 100644
index 0000000..e164d8b
--- /dev/null
+++ b/src/api/demo/tree/types.ts
@@ -0,0 +1,80 @@
+export interface TreeVO {
+ /**
+ * 主键
+ */
+ id: string | number;
+
+ /**
+ * 父id
+ */
+ parentId: string | number;
+
+ /**
+ * 部门id
+ */
+ deptId: string | number;
+
+ /**
+ * 用户id
+ */
+ userId: string | number;
+
+ /**
+ * 值
+ */
+ treeName: string;
+
+ /**
+ * 子对象
+ */
+ children: TreeVO[];
+}
+
+export interface TreeForm extends BaseEntity {
+ /**
+ * 主键
+ */
+ id?: string | number;
+
+ /**
+ * 父id
+ */
+ parentId?: string | number;
+
+ /**
+ * 部门id
+ */
+ deptId?: string | number;
+
+ /**
+ * 用户id
+ */
+ userId?: string | number;
+
+ /**
+ * 值
+ */
+ treeName?: string;
+}
+
+export interface TreeQuery {
+ /**
+ * 父id
+ */
+ parentId?: string | number;
+
+ /**
+ * 部门id
+ */
+ deptId?: string | number;
+
+ /**
+ * 用户id
+ */
+ userId?: string | number;
+
+ /**
+ * 值
+ */
+ treeName?: string;
+}
diff --git a/src/api/login.ts b/src/api/login.ts
new file mode 100644
index 0000000..100a5e9
--- /dev/null
+++ b/src/api/login.ts
@@ -0,0 +1,105 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { LoginData, LoginResult, VerifyCodeResult, TenantInfo } from './types';
+import { UserInfo } from '@/api/system/user/types';
+
+// pc端固定客户端授权id
+const clientId = import.meta.env.VITE_APP_CLIENT_ID;
+
+/**
+ * @param data {LoginData}
+ * @returns
+ */
+export function login(data: LoginData): AxiosPromise<LoginResult> {
+ const params = {
+ ...data,
+ clientId: data.clientId || clientId,
+ grantType: data.grantType || 'password'
+ };
+ return request({
+ url: '/auth/login',
+ headers: {
+ isToken: false,
+ isEncrypt: true
+ },
+ method: 'post',
+ data: params
+ });
+}
+
+// 注册方法
+export function register(data: any) {
+ const params = {
+ ...data,
+ clientId: clientId,
+ grantType: 'password'
+ };
+ return request({
+ url: '/auth/register',
+ headers: {
+ isToken: false,
+ isEncrypt: true
+ },
+ method: 'post',
+ data: params
+ });
+}
+
+/**
+ * 注销
+ */
+export function logout() {
+ return request({
+ url: '/auth/logout',
+ method: 'post'
+ });
+}
+
+/**
+ * 获取验证码
+ */
+export function getCodeImg(): AxiosPromise<VerifyCodeResult> {
+ return request({
+ url: '/auth/code',
+ headers: {
+ isToken: false
+ },
+ method: 'get',
+ timeout: 20000
+ });
+}
+
+/**
+ * 第三方登录
+ */
+export function callback(data: LoginData): AxiosPromise<any> {
+ const LoginData = {
+ ...data,
+ clientId: clientId,
+ grantType: 'social'
+ };
+ return request({
+ url: '/auth/social/callback',
+ method: 'post',
+ data: LoginData
+ });
+}
+
+// 获取用户详细信息
+export function getInfo(): AxiosPromise<UserInfo> {
+ return request({
+ url: '/system/user/getInfo',
+ method: 'get'
+ });
+}
+
+// 获取租户列表
+export function getTenantList(): AxiosPromise<TenantInfo> {
+ return request({
+ url: '/auth/tenant/list',
+ headers: {
+ isToken: false
+ },
+ method: 'get'
+ });
+}
diff --git a/src/api/menu.ts b/src/api/menu.ts
new file mode 100644
index 0000000..a3ae80e
--- /dev/null
+++ b/src/api/menu.ts
@@ -0,0 +1,11 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { RouteRecordRaw } from 'vue-router';
+
+// 获取路由
+export function getRouters(): AxiosPromise<RouteRecordRaw[]> {
+ return request({
+ url: '/system/menu/getRouters',
+ method: 'get'
+ });
+}
diff --git a/src/api/monitor/cache/index.ts b/src/api/monitor/cache/index.ts
new file mode 100644
index 0000000..e45d6fb
--- /dev/null
+++ b/src/api/monitor/cache/index.ts
@@ -0,0 +1,59 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { CacheVO } from './types';
+
+// 查询缓存详细
+export function getCache(): AxiosPromise<CacheVO> {
+ return request({
+ url: '/monitor/cache',
+ method: 'get'
+ });
+}
+
+// 查询缓存名称列表
+export function listCacheName() {
+ return request({
+ url: '/monitor/cache/getNames',
+ method: 'get'
+ });
+}
+
+// 查询缓存键名列表
+export function listCacheKey(cacheName: string) {
+ return request({
+ url: '/monitor/cache/getKeys/' + cacheName,
+ method: 'get'
+ });
+}
+
+// 查询缓存内容
+export function getCacheValue(cacheName: string, cacheKey: string) {
+ return request({
+ url: '/monitor/cache/getValue/' + cacheName + '/' + cacheKey,
+ method: 'get'
+ });
+}
+
+// 清理指定名称缓存
+export function clearCacheName(cacheName: string) {
+ return request({
+ url: '/monitor/cache/clearCacheName/' + cacheName,
+ method: 'delete'
+ });
+}
+
+// 清理指定键名缓存
+export function clearCacheKey(cacheName: string, cacheKey: string) {
+ return request({
+ url: '/monitor/cache/clearCacheKey/' + cacheName + '/' + cacheKey,
+ method: 'delete'
+ });
+}
+
+// 清理全部缓存
+export function clearCacheAll() {
+ return request({
+ url: '/monitor/cache/clearCacheAll',
+ method: 'delete'
+ });
+}
diff --git a/src/api/monitor/cache/types.ts b/src/api/monitor/cache/types.ts
new file mode 100644
index 0000000..4017b65
--- /dev/null
+++ b/src/api/monitor/cache/types.ts
@@ -0,0 +1,7 @@
+export interface CacheVO {
+ commandStats: Array<{ name: string; value: string }>;
+
+ dbSize: number;
+
+ info: { [key: string]: string };
+}
diff --git a/src/api/monitor/loginInfo/index.ts b/src/api/monitor/loginInfo/index.ts
new file mode 100644
index 0000000..f8877c9
--- /dev/null
+++ b/src/api/monitor/loginInfo/index.ts
@@ -0,0 +1,36 @@
+import request from '@/utils/request';
+import { LoginInfoQuery, LoginInfoVO } from './types';
+import { AxiosPromise } from 'axios';
+
+// 查询登录日志列表
+export function list(query: LoginInfoQuery): AxiosPromise<LoginInfoVO[]> {
+ return request({
+ url: '/monitor/logininfor/list',
+ method: 'get',
+ params: query
+ });
+}
+
+// 删除登录日志
+export function delLoginInfo(infoId: string | number | Array<string | number>) {
+ return request({
+ url: '/monitor/logininfor/' + infoId,
+ method: 'delete'
+ });
+}
+
+// 解锁用户登录状态
+export function unlockLoginInfo(userName: string | Array<string>) {
+ return request({
+ url: '/monitor/logininfor/unlock/' + userName,
+ method: 'get'
+ });
+}
+
+// 清空登录日志
+export function cleanLoginInfo() {
+ return request({
+ url: '/monitor/logininfor/clean',
+ method: 'delete'
+ });
+}
diff --git a/src/api/monitor/loginInfo/types.ts b/src/api/monitor/loginInfo/types.ts
new file mode 100644
index 0000000..202c779
--- /dev/null
+++ b/src/api/monitor/loginInfo/types.ts
@@ -0,0 +1,20 @@
+export interface LoginInfoVO {
+ infoId: string | number;
+ tenantId: string | number;
+ userName: string;
+ status: string;
+ ipaddr: string;
+ loginLocation: string;
+ browser: string;
+ os: string;
+ msg: string;
+ loginTime: string;
+}
+
+export interface LoginInfoQuery extends PageQuery {
+ ipaddr: string;
+ userName: string;
+ status: string;
+ orderByColumn: string;
+ isAsc: string;
+}
diff --git a/src/api/monitor/online/index.ts b/src/api/monitor/online/index.ts
new file mode 100644
index 0000000..3d9034a
--- /dev/null
+++ b/src/api/monitor/online/index.ts
@@ -0,0 +1,20 @@
+import request from '@/utils/request';
+import { OnlineQuery, OnlineVO } from './types';
+import { AxiosPromise } from 'axios';
+
+// 查询在线用户列表
+export function list(query: OnlineQuery): AxiosPromise<OnlineVO[]> {
+ return request({
+ url: '/monitor/online/list',
+ method: 'get',
+ params: query
+ });
+}
+
+// 强退用户
+export function forceLogout(tokenId: string) {
+ return request({
+ url: '/monitor/online/' + tokenId,
+ method: 'delete'
+ });
+}
diff --git a/src/api/monitor/online/types.ts b/src/api/monitor/online/types.ts
new file mode 100644
index 0000000..8c0ec27
--- /dev/null
+++ b/src/api/monitor/online/types.ts
@@ -0,0 +1,15 @@
+export interface OnlineQuery extends PageQuery {
+ ipaddr: string;
+ userName: string;
+}
+
+export interface OnlineVO extends BaseEntity {
+ tokenId: string;
+ deptName: string;
+ userName: string;
+ ipaddr: string;
+ loginLocation: string;
+ browser: string;
+ os: string;
+ loginTime: number;
+}
diff --git a/src/api/monitor/operlog/index.ts b/src/api/monitor/operlog/index.ts
new file mode 100644
index 0000000..7ac3453
--- /dev/null
+++ b/src/api/monitor/operlog/index.ts
@@ -0,0 +1,28 @@
+import request from '@/utils/request';
+import { OperLogQuery, OperLogVO } from './types';
+import { AxiosPromise } from 'axios';
+
+// 查询操作日志列表
+export function list(query: OperLogQuery): AxiosPromise<OperLogVO[]> {
+ return request({
+ url: '/monitor/operlog/list',
+ method: 'get',
+ params: query
+ });
+}
+
+// 删除操作日志
+export function delOperlog(operId: string | number | Array<string | number>) {
+ return request({
+ url: '/monitor/operlog/' + operId,
+ method: 'delete'
+ });
+}
+
+// 清空操作日志
+export function cleanOperlog() {
+ return request({
+ url: '/monitor/operlog/clean',
+ method: 'delete'
+ });
+}
diff --git a/src/api/monitor/operlog/types.ts b/src/api/monitor/operlog/types.ts
new file mode 100644
index 0000000..10f65c7
--- /dev/null
+++ b/src/api/monitor/operlog/types.ts
@@ -0,0 +1,53 @@
+export interface OperLogQuery extends PageQuery {
+ operIp: string;
+ title: string;
+ operName: string;
+ businessType: string;
+ status: string;
+ orderByColumn: string;
+ isAsc: string;
+}
+
+export interface OperLogVO extends BaseEntity {
+ operId: string | number;
+ tenantId: string;
+ title: string;
+ businessType: number;
+ businessTypes: number[] | undefined;
+ method: string;
+ requestMethod: string;
+ operatorType: number;
+ operName: string;
+ deptName: string;
+ operUrl: string;
+ operIp: string;
+ operLocation: string;
+ operParam: string;
+ jsonResult: string;
+ status: number;
+ errorMsg: string;
+ operTime: string;
+ costTime: number;
+}
+
+export interface OperLogForm {
+ operId: number | string | undefined;
+ tenantId: string | number | undefined;
+ title: string;
+ businessType: number;
+ businessTypes: number[] | undefined;
+ method: string;
+ requestMethod: string;
+ operatorType: number;
+ operName: string;
+ deptName: string;
+ operUrl: string;
+ operIp: string;
+ operLocation: string;
+ operParam: string;
+ jsonResult: string;
+ status: number;
+ errorMsg: string;
+ operTime: string;
+ costTime: number;
+}
diff --git a/src/api/system/client/index.ts b/src/api/system/client/index.ts
new file mode 100644
index 0000000..06544da
--- /dev/null
+++ b/src/api/system/client/index.ts
@@ -0,0 +1,80 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { ClientVO, ClientForm, ClientQuery } from '@/api/system/client/types';
+
+/**
+ * 查询客户端管理列表
+ * @param query
+ * @returns {*}
+ */
+
+export const listClient = (query?: ClientQuery): AxiosPromise<ClientVO[]> => {
+ return request({
+ url: '/system/client/list',
+ method: 'get',
+ params: query
+ });
+};
+
+/**
+ * 查询客户端管理详细
+ * @param id
+ */
+export const getClient = (id: string | number): AxiosPromise<ClientVO> => {
+ return request({
+ url: '/system/client/' + id,
+ method: 'get'
+ });
+};
+
+/**
+ * 新增客户端管理
+ * @param data
+ */
+export const addClient = (data: ClientForm) => {
+ return request({
+ url: '/system/client',
+ method: 'post',
+ data: data
+ });
+};
+
+/**
+ * 修改客户端管理
+ * @param data
+ */
+export const updateClient = (data: ClientForm) => {
+ return request({
+ url: '/system/client',
+ method: 'put',
+ data: data
+ });
+};
+
+/**
+ * 删除客户端管理
+ * @param id
+ */
+export const delClient = (id: string | number | Array<string | number>) => {
+ return request({
+ url: '/system/client/' + id,
+ method: 'delete'
+ });
+};
+
+/**
+ * 状态修改
+ * @param id ID
+ * @param status 状态
+ */
+export function changeStatus(id: number | string, status: string) {
+ const data = {
+ id,
+ status
+ };
+ return request({
+ url: '/system/client/changeStatus',
+ method: 'put',
+ data: data
+ });
+}
diff --git a/src/api/system/client/types.ts b/src/api/system/client/types.ts
new file mode 100644
index 0000000..d7cb926
--- /dev/null
+++ b/src/api/system/client/types.ts
@@ -0,0 +1,135 @@
+export interface ClientVO {
+ /**
+ * id
+ */
+ id: string | number;
+
+ /**
+ * 客户端id
+ */
+ clientId: string | number;
+
+ /**
+ * 客户端key
+ */
+ clientKey: string;
+
+ /**
+ * 客户端秘钥
+ */
+ clientSecret: string;
+
+ /**
+ * 授权类型
+ */
+ grantTypeList: string[];
+
+ /**
+ * 设备类型
+ */
+ deviceType: string;
+
+ /**
+ * token活跃超时时间
+ */
+ activeTimeout: number;
+
+ /**
+ * token固定超时
+ */
+ timeout: number;
+
+ /**
+ * 状态(0正常 1停用)
+ */
+ status: string;
+}
+
+export interface ClientForm extends BaseEntity {
+ /**
+ * id
+ */
+ id?: string | number;
+
+ /**
+ * 客户端id
+ */
+ clientId?: string | number;
+
+ /**
+ * 客户端key
+ */
+ clientKey?: string;
+
+ /**
+ * 客户端秘钥
+ */
+ clientSecret?: string;
+
+ /**
+ * 授权类型
+ */
+ grantTypeList?: string[];
+
+ /**
+ * 设备类型
+ */
+ deviceType?: string;
+
+ /**
+ * token活跃超时时间
+ */
+ activeTimeout?: number;
+
+ /**
+ * token固定超时
+ */
+ timeout?: number;
+
+ /**
+ * 状态(0正常 1停用)
+ */
+ status?: string;
+}
+
+export interface ClientQuery extends PageQuery {
+ /**
+ * 客户端id
+ */
+ clientId?: string | number;
+
+ /**
+ * 客户端key
+ */
+ clientKey?: string;
+
+ /**
+ * 客户端秘钥
+ */
+ clientSecret?: string;
+
+ /**
+ * 授权类型
+ */
+ grantType?: string;
+
+ /**
+ * 设备类型
+ */
+ deviceType?: string;
+
+ /**
+ * token活跃超时时间
+ */
+ activeTimeout?: number;
+
+ /**
+ * token固定超时
+ */
+ timeout?: number;
+
+ /**
+ * 状态(0正常 1停用)
+ */
+ status?: string;
+}
diff --git a/src/api/system/config/index.ts b/src/api/system/config/index.ts
new file mode 100644
index 0000000..e52a256
--- /dev/null
+++ b/src/api/system/config/index.ts
@@ -0,0 +1,74 @@
+import request from '@/utils/request';
+import { ConfigForm, ConfigQuery, ConfigVO } from './types';
+import { AxiosPromise } from 'axios';
+
+// 查询参数列表
+export function listConfig(query: ConfigQuery): AxiosPromise<ConfigVO[]> {
+ return request({
+ url: '/system/config/list',
+ method: 'get',
+ params: query
+ });
+}
+
+// 查询参数详细
+export function getConfig(configId: string | number): AxiosPromise<ConfigVO> {
+ return request({
+ url: '/system/config/' + configId,
+ method: 'get'
+ });
+}
+
+// 根据参数键名查询参数值
+export function getConfigKey(configKey: string): AxiosPromise<String> {
+ return request({
+ url: '/system/config/configKey/' + configKey,
+ method: 'get'
+ });
+}
+
+// 新增参数配置
+export function addConfig(data: ConfigForm) {
+ return request({
+ url: '/system/config',
+ method: 'post',
+ data: data
+ });
+}
+
+// 修改参数配置
+export function updateConfig(data: ConfigForm) {
+ return request({
+ url: '/system/config',
+ method: 'put',
+ data: data
+ });
+}
+
+// 修改参数配置
+export function updateConfigByKey(key: string, value: any) {
+ return request({
+ url: '/system/config/updateByKey',
+ method: 'put',
+ data: {
+ configKey: key,
+ configValue: value
+ }
+ });
+}
+
+// 删除参数配置
+export function delConfig(configId: string | number | Array<string | number>) {
+ return request({
+ url: '/system/config/' + configId,
+ method: 'delete'
+ });
+}
+
+// 刷新参数缓存
+export function refreshCache() {
+ return request({
+ url: '/system/config/refreshCache',
+ method: 'delete'
+ });
+}
diff --git a/src/api/system/config/types.ts b/src/api/system/config/types.ts
new file mode 100644
index 0000000..b68f073
--- /dev/null
+++ b/src/api/system/config/types.ts
@@ -0,0 +1,23 @@
+export interface ConfigVO extends BaseEntity {
+ configId: number | string;
+ configName: string;
+ configKey: string;
+ configValue: string;
+ configType: string;
+ remark: string;
+}
+
+export interface ConfigForm {
+ configId: number | string | undefined;
+ configName: string;
+ configKey: string;
+ configValue: string;
+ configType: string;
+ remark: string;
+}
+
+export interface ConfigQuery extends PageQuery {
+ configName: string;
+ configKey: string;
+ configType: string;
+}
diff --git a/src/api/system/dept/index.ts b/src/api/system/dept/index.ts
new file mode 100644
index 0000000..7e097fd
--- /dev/null
+++ b/src/api/system/dept/index.ts
@@ -0,0 +1,62 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { DeptForm, DeptQuery, DeptVO } from './types';
+
+// 查询部门列表
+export const listDept = (query?: DeptQuery) => {
+ return request({
+ url: '/system/dept/list',
+ method: 'get',
+ params: query
+ });
+};
+
+// 查询部门列表(排除节点)
+export const listDeptExcludeChild = (deptId: string | number): AxiosPromise<DeptVO[]> => {
+ return request({
+ url: '/system/dept/list/exclude/' + deptId,
+ method: 'get'
+ });
+};
+
+// 查询部门详细
+export const getDept = (deptId: string | number): AxiosPromise<DeptVO> => {
+ return request({
+ url: '/system/dept/' + deptId,
+ method: 'get'
+ });
+};
+
+// 查询部门下拉树结构
+export const treeselect = (): AxiosPromise<DeptVO[]> => {
+ return request({
+ url: '/system/dept/treeselect',
+ method: 'get'
+ });
+};
+
+// 新增部门
+export const addDept = (data: DeptForm) => {
+ return request({
+ url: '/system/dept',
+ method: 'post',
+ data: data
+ });
+};
+
+// 修改部门
+export const updateDept = (data: DeptForm) => {
+ return request({
+ url: '/system/dept',
+ method: 'put',
+ data: data
+ });
+};
+
+// 删除部门
+export const delDept = (deptId: number | string) => {
+ return request({
+ url: '/system/dept/' + deptId,
+ method: 'delete'
+ });
+};
diff --git a/src/api/system/dept/types.ts b/src/api/system/dept/types.ts
new file mode 100644
index 0000000..867dbde
--- /dev/null
+++ b/src/api/system/dept/types.ts
@@ -0,0 +1,45 @@
+/**
+ * 部门查询参数
+ */
+export interface DeptQuery extends PageQuery {
+ deptName?: string;
+ status?: number;
+}
+
+/**
+ * 部门类型
+ */
+export interface DeptVO extends BaseEntity {
+ id: number | string;
+ parentName: string;
+ parentId: number | string;
+ children: DeptVO[];
+ deptId: number | string;
+ deptName: string;
+ orderNum: number;
+ leader: string;
+ phone: string;
+ email: string;
+ status: string;
+ delFlag: string;
+ ancestors: string;
+ menuId: string | number;
+}
+
+/**
+ * 部门表单类型
+ */
+export interface DeptForm {
+ parentName?: string;
+ parentId?: number | string;
+ children?: DeptForm[];
+ deptId?: number | string;
+ deptName?: string;
+ orderNum?: number;
+ leader?: string;
+ phone?: string;
+ email?: string;
+ status?: string;
+ delFlag?: string;
+ ancestors?: string;
+}
diff --git a/src/api/system/dict/data/index.ts b/src/api/system/dict/data/index.ts
new file mode 100644
index 0000000..7692abc
--- /dev/null
+++ b/src/api/system/dict/data/index.ts
@@ -0,0 +1,53 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { DictDataForm, DictDataQuery, DictDataVO } from './types';
+// 根据字典类型查询字典数据信息
+export function getDicts(dictType: string): AxiosPromise<DictDataVO[]> {
+ return request({
+ url: '/system/dict/data/type/' + dictType,
+ method: 'get'
+ });
+}
+
+// 查询字典数据列表
+export function listData(query: DictDataQuery): AxiosPromise<DictDataVO[]> {
+ return request({
+ url: '/system/dict/data/list',
+ method: 'get',
+ params: query
+ });
+}
+
+// 查询字典数据详细
+export function getData(dictCode: string | number): AxiosPromise<DictDataVO> {
+ return request({
+ url: '/system/dict/data/' + dictCode,
+ method: 'get'
+ });
+}
+
+// 新增字典数据
+export function addData(data: DictDataForm) {
+ return request({
+ url: '/system/dict/data',
+ method: 'post',
+ data: data
+ });
+}
+
+// 修改字典数据
+export function updateData(data: DictDataForm) {
+ return request({
+ url: '/system/dict/data',
+ method: 'put',
+ data: data
+ });
+}
+
+// 删除字典数据
+export function delData(dictCode: string | number | Array<string | number>) {
+ return request({
+ url: '/system/dict/data/' + dictCode,
+ method: 'delete'
+ });
+}
diff --git a/src/api/system/dict/data/types.ts b/src/api/system/dict/data/types.ts
new file mode 100644
index 0000000..e4abb9b
--- /dev/null
+++ b/src/api/system/dict/data/types.ts
@@ -0,0 +1,26 @@
+export interface DictDataQuery extends PageQuery {
+ dictName: string;
+ dictType: string;
+ dictLabel: string;
+}
+
+export interface DictDataVO extends BaseEntity {
+ dictCode: string;
+ dictLabel: string;
+ dictValue: string;
+ cssClass: string;
+ listClass: ElTagType;
+ dictSort: number;
+ remark: string;
+}
+
+export interface DictDataForm {
+ dictType?: string;
+ dictCode: string | undefined;
+ dictLabel: string;
+ dictValue: string;
+ cssClass: string;
+ listClass: ElTagType;
+ dictSort: number;
+ remark: string;
+}
diff --git a/src/api/system/dict/type/index.ts b/src/api/system/dict/type/index.ts
new file mode 100644
index 0000000..7dc3d66
--- /dev/null
+++ b/src/api/system/dict/type/index.ts
@@ -0,0 +1,62 @@
+import request from '@/utils/request';
+import { DictTypeForm, DictTypeVO, DictTypeQuery } from './types';
+import { AxiosPromise } from 'axios';
+
+// 查询字典类型列表
+export function listType(query: DictTypeQuery): AxiosPromise<DictTypeVO[]> {
+ return request({
+ url: '/system/dict/type/list',
+ method: 'get',
+ params: query
+ });
+}
+
+// 查询字典类型详细
+export function getType(dictId: number | string): AxiosPromise<DictTypeVO> {
+ return request({
+ url: '/system/dict/type/' + dictId,
+ method: 'get'
+ });
+}
+
+// 新增字典类型
+export function addType(data: DictTypeForm) {
+ return request({
+ url: '/system/dict/type',
+ method: 'post',
+ data: data
+ });
+}
+
+// 修改字典类型
+export function updateType(data: DictTypeForm) {
+ return request({
+ url: '/system/dict/type',
+ method: 'put',
+ data: data
+ });
+}
+
+// 删除字典类型
+export function delType(dictId: string | number | Array<string | number>) {
+ return request({
+ url: '/system/dict/type/' + dictId,
+ method: 'delete'
+ });
+}
+
+// 刷新字典缓存
+export function refreshCache() {
+ return request({
+ url: '/system/dict/type/refreshCache',
+ method: 'delete'
+ });
+}
+
+// 获取字典选择框列表
+export function optionselect(): AxiosPromise<DictTypeVO[]> {
+ return request({
+ url: '/system/dict/type/optionselect',
+ method: 'get'
+ });
+}
diff --git a/src/api/system/dict/type/types.ts b/src/api/system/dict/type/types.ts
new file mode 100644
index 0000000..4987dbf
--- /dev/null
+++ b/src/api/system/dict/type/types.ts
@@ -0,0 +1,18 @@
+export interface DictTypeVO extends BaseEntity {
+ dictId: number | string;
+ dictName: string;
+ dictType: string;
+ remark: string;
+}
+
+export interface DictTypeForm {
+ dictId: number | string | undefined;
+ dictName: string;
+ dictType: string;
+ remark: string;
+}
+
+export interface DictTypeQuery extends PageQuery {
+ dictName: string;
+ dictType: string;
+}
diff --git a/src/api/system/goodsIn/index.ts b/src/api/system/goodsIn/index.ts
new file mode 100644
index 0000000..2554c32
--- /dev/null
+++ b/src/api/system/goodsIn/index.ts
@@ -0,0 +1,63 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { GoodsInVO, GoodsInForm, GoodsInQuery } from '@/api/system/goodsIn/types';
+
+/**
+ * 查询入库登记列表
+ * @param query
+ * @returns {*}
+ */
+
+export const listGoodsIn = (query?: GoodsInQuery): AxiosPromise<GoodsInVO[]> => {
+ return request({
+ url: '/system/goodsIn/list',
+ method: 'get',
+ params: query
+ });
+};
+
+/**
+ * 查询入库登记详细
+ * @param inId
+ */
+export const getGoodsIn = (inId: string | number): AxiosPromise<GoodsInVO> => {
+ return request({
+ url: '/system/goodsIn/' + inId,
+ method: 'get'
+ });
+};
+
+/**
+ * 新增入库登记
+ * @param data
+ */
+export const addGoodsIn = (data: GoodsInForm) => {
+ return request({
+ url: '/system/goodsIn',
+ method: 'post',
+ data: data
+ });
+};
+
+/**
+ * 修改入库登记
+ * @param data
+ */
+export const updateGoodsIn = (data: GoodsInForm) => {
+ return request({
+ url: '/system/goodsIn',
+ method: 'put',
+ data: data
+ });
+};
+
+/**
+ * 删除入库登记
+ * @param inId
+ */
+export const delGoodsIn = (inId: string | number | Array<string | number>) => {
+ return request({
+ url: '/system/goodsIn/' + inId,
+ method: 'delete'
+ });
+};
diff --git a/src/api/system/goodsIn/types.ts b/src/api/system/goodsIn/types.ts
new file mode 100644
index 0000000..9976446
--- /dev/null
+++ b/src/api/system/goodsIn/types.ts
@@ -0,0 +1,146 @@
+export interface GoodsInVO {
+ /**
+ * 入库登记id
+ */
+ inId: string | number;
+
+ /**
+ * 柜号
+ */
+ cabinetCode: string;
+
+ /**
+ * 门号
+ */
+ doorCode: string;
+
+ /**
+ * 物品名称
+ */
+ goodsName: string;
+
+ /**
+ * 物品编码
+ */
+ goodsCode: string;
+
+ /**
+ * 入库数量
+ */
+ inAmount: number;
+
+ /**
+ * 库存数量
+ */
+ repAmount: number;
+
+ /**
+ * 入库时间
+ */
+ inTime: string;
+
+ /**
+ * 入库人
+ */
+ inOperator: string;
+
+}
+
+export interface GoodsInForm extends BaseEntity {
+ /**
+ * 入库登记id
+ */
+ inId?: string | number;
+
+ /**
+ * 柜号
+ */
+ cabinetCode?: string;
+
+ /**
+ * 门号
+ */
+ doorCode?: string;
+
+ /**
+ * 物品名称
+ */
+ goodsName?: string;
+
+ /**
+ * 物品编码
+ */
+ goodsCode?: string;
+
+ /**
+ * 入库数量
+ */
+ inAmount?: number;
+
+ /**
+ * 库存数量
+ */
+ repAmount?: number;
+
+ /**
+ * 入库时间
+ */
+ inTime?: string;
+
+ /**
+ * 入库人
+ */
+ inOperator?: string;
+
+}
+
+export interface GoodsInQuery extends PageQuery {
+
+ /**
+ * 柜号
+ */
+ cabinetCode?: string;
+
+ /**
+ * 门号
+ */
+ doorCode?: string;
+
+ /**
+ * 物品名称
+ */
+ goodsName?: string;
+
+ /**
+ * 物品编码
+ */
+ goodsCode?: string;
+
+ /**
+ * 入库数量
+ */
+ inAmount?: number;
+
+ /**
+ * 库存数量
+ */
+ repAmount?: number;
+
+ /**
+ * 入库时间
+ */
+ inTime?: string;
+
+ /**
+ * 入库人
+ */
+ inOperator?: string;
+
+ /**
+ * 日期范围参数
+ */
+ params?: any;
+}
+
+
+
diff --git a/src/api/system/menu/index.ts b/src/api/system/menu/index.ts
new file mode 100644
index 0000000..7a0cf74
--- /dev/null
+++ b/src/api/system/menu/index.ts
@@ -0,0 +1,70 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { MenuQuery, MenuVO, MenuForm, MenuTreeOption, RoleMenuTree } from './types';
+
+// 查询菜单列表
+export const listMenu = (query?: MenuQuery): AxiosPromise<MenuVO[]> => {
+ return request({
+ url: '/system/menu/list',
+ method: 'get',
+ params: query
+ });
+};
+
+// 查询菜单详细
+export const getMenu = (menuId: string | number): AxiosPromise<MenuVO> => {
+ return request({
+ url: '/system/menu/' + menuId,
+ method: 'get'
+ });
+};
+
+// 查询菜单下拉树结构
+export const treeselect = (): AxiosPromise<MenuTreeOption[]> => {
+ return request({
+ url: '/system/menu/treeselect',
+ method: 'get'
+ });
+};
+
+// 根据角色ID查询菜单下拉树结构
+export const roleMenuTreeselect = (roleId: string | number): AxiosPromise<RoleMenuTree> => {
+ return request({
+ url: '/system/menu/roleMenuTreeselect/' + roleId,
+ method: 'get'
+ });
+};
+
+// 根据角色ID查询菜单下拉树结构
+export const tenantPackageMenuTreeselect = (packageId: string | number): AxiosPromise<RoleMenuTree> => {
+ return request({
+ url: '/system/menu/tenantPackageMenuTreeselect/' + packageId,
+ method: 'get'
+ });
+};
+
+// 新增菜单
+export const addMenu = (data: MenuForm) => {
+ return request({
+ url: '/system/menu',
+ method: 'post',
+ data: data
+ });
+};
+
+// 修改菜单
+export const updateMenu = (data: MenuForm) => {
+ return request({
+ url: '/system/menu',
+ method: 'put',
+ data: data
+ });
+};
+
+// 删除菜单
+export const delMenu = (menuId: string | number) => {
+ return request({
+ url: '/system/menu/' + menuId,
+ method: 'delete'
+ });
+};
diff --git a/src/api/system/menu/types.ts b/src/api/system/menu/types.ts
new file mode 100644
index 0000000..ca19840
--- /dev/null
+++ b/src/api/system/menu/types.ts
@@ -0,0 +1,69 @@
+import { MenuTypeEnum } from '@/enums/MenuTypeEnum';
+
+/**
+ * 菜单树形结构类型
+ */
+export interface MenuTreeOption {
+ id: string | number;
+ label: string;
+ parentId: string | number;
+ weight: number;
+ children?: MenuTreeOption[];
+}
+
+export interface RoleMenuTree {
+ menus: MenuTreeOption[];
+ checkedKeys: string[];
+}
+
+/**
+ * 菜单查询参数类型
+ */
+export interface MenuQuery {
+ keywords?: string;
+ menuName?: string;
+ status?: string;
+}
+
+/**
+ * 菜单视图对象类型
+ */
+export interface MenuVO extends BaseEntity {
+ parentName: string;
+ parentId: string | number;
+ children: MenuVO[];
+ menuId: string | number;
+ menuName: string;
+ orderNum: number;
+ path: string;
+ component: string;
+ queryParam: string;
+ isFrame: string;
+ isCache: string;
+ menuType: MenuTypeEnum;
+ visible: string;
+ status: string;
+ icon: string;
+ remark: string;
+}
+
+export interface MenuForm {
+ parentName?: string;
+ parentId?: string | number;
+ children?: MenuForm[];
+ menuId?: string | number;
+ menuName: string;
+ orderNum: number;
+ path: string;
+ component?: string;
+ queryParam?: string;
+ isFrame?: string;
+ isCache?: string;
+ menuType?: MenuTypeEnum;
+ visible?: string;
+ status?: string;
+ icon?: string;
+ remark?: string;
+ query?: string;
+ perms?: string;
+}
diff --git a/src/api/system/notice/index.ts b/src/api/system/notice/index.ts
new file mode 100644
index 0000000..285d1f4
--- /dev/null
+++ b/src/api/system/notice/index.ts
@@ -0,0 +1,45 @@
+import request from '@/utils/request';
+import { NoticeForm, NoticeQuery, NoticeVO } from './types';
+import { AxiosPromise } from 'axios';
+// 查询公告列表
+export function listNotice(query: NoticeQuery): AxiosPromise<NoticeVO[]> {
+ return request({
+ url: '/system/notice/list',
+ method: 'get',
+ params: query
+ });
+}
+
+// 查询公告详细
+export function getNotice(noticeId: string | number): AxiosPromise<NoticeVO> {
+ return request({
+ url: '/system/notice/' + noticeId,
+ method: 'get'
+ });
+}
+
+// 新增公告
+export function addNotice(data: NoticeForm) {
+ return request({
+ url: '/system/notice',
+ method: 'post',
+ data: data
+ });
+}
+
+// 修改公告
+export function updateNotice(data: NoticeForm) {
+ return request({
+ url: '/system/notice',
+ method: 'put',
+ data: data
+ });
+}
+
+// 删除公告
+export function delNotice(noticeId: string | number | Array<string | number>) {
+ return request({
+ url: '/system/notice/' + noticeId,
+ method: 'delete'
+ });
+}
diff --git a/src/api/system/notice/types.ts b/src/api/system/notice/types.ts
new file mode 100644
index 0000000..abfd5b2
--- /dev/null
+++ b/src/api/system/notice/types.ts
@@ -0,0 +1,26 @@
+export interface NoticeVO extends BaseEntity {
+ noticeId: number;
+ noticeTitle: string;
+ noticeType: string;
+ noticeContent: string;
+ status: string;
+ remark: string;
+ createByName: string;
+}
+
+export interface NoticeQuery extends PageQuery {
+ noticeTitle: string;
+ createByName: string;
+ status: string;
+ noticeType: string;
+}
+
+export interface NoticeForm {
+ noticeId: number | string | undefined;
+ noticeTitle: string;
+ noticeType: string;
+ noticeContent: string;
+ status: string;
+ remark: string;
+ createByName: string;
+}
diff --git a/src/api/system/oss/index.ts b/src/api/system/oss/index.ts
new file mode 100644
index 0000000..4472112
--- /dev/null
+++ b/src/api/system/oss/index.ts
@@ -0,0 +1,28 @@
+import request from '@/utils/request';
+import { OssQuery, OssVO } from './types';
+import { AxiosPromise } from 'axios';
+
+// 查询OSS对象存储列表
+export function listOss(query: OssQuery): AxiosPromise<OssVO[]> {
+ return request({
+ url: '/resource/oss/list',
+ method: 'get',
+ params: query
+ });
+}
+
+// 查询OSS对象基于id串
+export function listByIds(ossId: string | number): AxiosPromise<OssVO[]> {
+ return request({
+ url: '/resource/oss/listByIds/' + ossId,
+ method: 'get'
+ });
+}
+
+// 删除OSS对象存储
+export function delOss(ossId: string | number | Array<string | number>) {
+ return request({
+ url: '/resource/oss/' + ossId,
+ method: 'delete'
+ });
+}
diff --git a/src/api/system/oss/types.ts b/src/api/system/oss/types.ts
new file mode 100644
index 0000000..bc0bc1f
--- /dev/null
+++ b/src/api/system/oss/types.ts
@@ -0,0 +1,22 @@
+export interface OssVO extends BaseEntity {
+ ossId: string | number;
+ fileName: string;
+ originalName: string;
+ fileSuffix: string;
+ url: string;
+ createByName: string;
+ service: string;
+}
+
+export interface OssQuery extends PageQuery {
+ fileName: string;
+ originalName: string;
+ fileSuffix: string;
+ createTime: string;
+ service: string;
+ orderByColumn: string;
+ isAsc: string;
+}
+export interface OssForm {
+ file: undefined | string;
+}
diff --git a/src/api/system/ossConfig/index.ts b/src/api/system/ossConfig/index.ts
new file mode 100644
index 0000000..d0faefe
--- /dev/null
+++ b/src/api/system/ossConfig/index.ts
@@ -0,0 +1,60 @@
+import request from '@/utils/request';
+import { OssConfigForm, OssConfigQuery, OssConfigVO } from './types';
+import { AxiosPromise } from 'axios';
+
+// 查询对象存储配置列表
+export function listOssConfig(query: OssConfigQuery): AxiosPromise<OssConfigVO[]> {
+ return request({
+ url: '/resource/oss/config/list',
+ method: 'get',
+ params: query
+ });
+}
+
+// 查询对象存储配置详细
+export function getOssConfig(ossConfigId: string | number): AxiosPromise<OssConfigVO> {
+ return request({
+ url: '/resource/oss/config/' + ossConfigId,
+ method: 'get'
+ });
+}
+
+// 新增对象存储配置
+export function addOssConfig(data: OssConfigForm) {
+ return request({
+ url: '/resource/oss/config',
+ method: 'post',
+ data: data
+ });
+}
+
+// 修改对象存储配置
+export function updateOssConfig(data: OssConfigForm) {
+ return request({
+ url: '/resource/oss/config',
+ method: 'put',
+ data: data
+ });
+}
+
+// 删除对象存储配置
+export function delOssConfig(ossConfigId: string | number | Array<string | number>) {
+ return request({
+ url: '/resource/oss/config/' + ossConfigId,
+ method: 'delete'
+ });
+}
+
+// 对象存储状态修改
+export function changeOssConfigStatus(ossConfigId: string | number, status: string, configKey: string) {
+ const data = {
+ ossConfigId,
+ status,
+ configKey
+ };
+ return request({
+ url: '/resource/oss/config/changeStatus',
+ method: 'put',
+ data: data
+ });
+}
diff --git a/src/api/system/ossConfig/types.ts b/src/api/system/ossConfig/types.ts
new file mode 100644
index 0000000..d227bd0
--- /dev/null
+++ b/src/api/system/ossConfig/types.ts
@@ -0,0 +1,38 @@
+export interface OssConfigVO extends BaseEntity {
+ ossConfigId: number | string;
+ configKey: string;
+ accessKey: string;
+ secretKey: string;
+ bucketName: string;
+ prefix: string;
+ endpoint: string;
+ domain: string;
+ isHttps: string;
+ region: string;
+ status: string;
+ ext1: string;
+ remark: string;
+ accessPolicy: string;
+}
+
+export interface OssConfigQuery extends PageQuery {
+ configKey: string;
+ bucketName: string;
+ status: string;
+}
+
+export interface OssConfigForm {
+ ossConfigId: string | number | undefined;
+ configKey: string;
+ accessKey: string;
+ secretKey: string;
+ bucketName: string;
+ prefix: string;
+ endpoint: string;
+ domain: string;
+ isHttps: string;
+ accessPolicy: string;
+ region: string;
+ status: string;
+ remark: string;
+}
diff --git a/src/api/system/post/index.ts b/src/api/system/post/index.ts
new file mode 100644
index 0000000..3e523ab
--- /dev/null
+++ b/src/api/system/post/index.ts
@@ -0,0 +1,46 @@
+import request from '@/utils/request';
+import { PostForm, PostQuery, PostVO } from './types';
+import { AxiosPromise } from 'axios';
+
+// 查询岗位列表
+export function listPost(query: PostQuery): AxiosPromise<PostVO[]> {
+ return request({
+ url: '/system/post/list',
+ method: 'get',
+ params: query
+ });
+}
+
+// 查询岗位详细
+export function getPost(postId: string | number): AxiosPromise<PostVO> {
+ return request({
+ url: '/system/post/' + postId,
+ method: 'get'
+ });
+}
+
+// 新增岗位
+export function addPost(data: PostForm) {
+ return request({
+ url: '/system/post',
+ method: 'post',
+ data: data
+ });
+}
+
+// 修改岗位
+export function updatePost(data: PostForm) {
+ return request({
+ url: '/system/post',
+ method: 'put',
+ data: data
+ });
+}
+
+// 删除岗位
+export function delPost(postId: string | number | (string | number)[]) {
+ return request({
+ url: '/system/post/' + postId,
+ method: 'delete'
+ });
+}
diff --git a/src/api/system/post/types.ts b/src/api/system/post/types.ts
new file mode 100644
index 0000000..84e6786
--- /dev/null
+++ b/src/api/system/post/types.ts
@@ -0,0 +1,23 @@
+export interface PostVO extends BaseEntity {
+ postId: number | string;
+ postCode: string;
+ postName: string;
+ postSort: number;
+ status: string;
+ remark: string;
+}
+
+export interface PostForm {
+ postId: number | string | undefined;
+ postCode: string;
+ postName: string;
+ postSort: number;
+ status: string;
+ remark: string;
+}
+
+export interface PostQuery extends PageQuery {
+ postCode: string;
+ postName: string;
+ status: string;
+}
diff --git a/src/api/system/role/index.ts b/src/api/system/role/index.ts
new file mode 100644
index 0000000..4e8b612
--- /dev/null
+++ b/src/api/system/role/index.ts
@@ -0,0 +1,144 @@
+import { UserVO } from '@/api/system/user/types';
+import { UserQuery } from '@/api/system/user/types';
+import { AxiosPromise } from 'axios';
+import { RoleQuery, RoleVO, RoleDeptTree } from './types';
+import request from '@/utils/request';
+
+export const listRole = (query: RoleQuery): AxiosPromise<RoleVO[]> => {
+ return request({
+ url: '/system/role/list',
+ method: 'get',
+ params: query
+ });
+};
+
+/**
+ * 查询角色详细
+ */
+export const getRole = (roleId: string | number): AxiosPromise<RoleVO> => {
+ return request({
+ url: '/system/role/' + roleId,
+ method: 'get'
+ });
+};
+
+/**
+ * 新增角色
+ */
+export const addRole = (data: any) => {
+ return request({
+ url: '/system/role',
+ method: 'post',
+ data: data
+ });
+};
+
+/**
+ * 修改角色
+ * @param data
+ */
+export const updateRole = (data: any) => {
+ return request({
+ url: '/system/role',
+ method: 'put',
+ data: data
+ });
+};
+
+/**
+ * 角色数据权限
+ */
+export const dataScope = (data: any) => {
+ return request({
+ url: '/system/role/dataScope',
+ method: 'put',
+ data: data
+ });
+};
+
+/**
+ * 角色状态修改
+ */
+export const changeRoleStatus = (roleId: string | number, status: string) => {
+ const data = {
+ roleId,
+ status
+ };
+ return request({
+ url: '/system/role/changeStatus',
+ method: 'put',
+ data: data
+ });
+};
+
+/**
+ * 删除角色
+ */
+export const delRole = (roleId: Array<string | number> | string | number) => {
+ return request({
+ url: '/system/role/' + roleId,
+ method: 'delete'
+ });
+};
+
+/**
+ * 查询角色已授权用户列表
+ */
+export const allocatedUserList = (query: UserQuery): AxiosPromise<UserVO[]> => {
+ return request({
+ url: '/system/role/authUser/allocatedList',
+ method: 'get',
+ params: query
+ });
+};
+
+/**
+ * 查询角色未授权用户列表
+ */
+export const unallocatedUserList = (query: UserQuery): AxiosPromise<UserVO[]> => {
+ return request({
+ url: '/system/role/authUser/unallocatedList',
+ method: 'get',
+ params: query
+ });
+};
+
+/**
+ * 取消用户授权角色
+ */
+export const authUserCancel = (data: any) => {
+ return request({
+ url: '/system/role/authUser/cancel',
+ method: 'put',
+ data: data
+ });
+};
+
+/**
+ * 批量取消用户授权角色
+ */
+export const authUserCancelAll = (data: any) => {
+ return request({
+ url: '/system/role/authUser/cancelAll',
+ method: 'put',
+ params: data
+ });
+};
+
+/**
+ * 授权用户选择
+ */
+export const authUserSelectAll = (data: any) => {
+ return request({
+ url: '/system/role/authUser/selectAll',
+ method: 'put',
+ params: data
+ });
+};
+// 根据角色ID查询部门树结构
+export const deptTreeSelect = (roleId: string | number): AxiosPromise<RoleDeptTree> => {
+ return request({
+ url: '/system/role/deptTree/' + roleId,
+ method: 'get'
+ });
+};
diff --git a/src/api/system/role/types.ts b/src/api/system/role/types.ts
new file mode 100644
index 0000000..7dbb6ff
--- /dev/null
+++ b/src/api/system/role/types.ts
@@ -0,0 +1,52 @@
+/**
+ * 菜单树形结构类型
+ */
+export interface DeptTreeOption {
+ id: string;
+ label: string;
+ parentId: string;
+ weight: number;
+ children?: DeptTreeOption[];
+}
+
+export interface RoleDeptTree {
+ checkedKeys: string[];
+ depts: DeptTreeOption[];
+}
+
+export interface RoleVO extends BaseEntity {
+ roleId: string | number;
+ roleName: string;
+ roleKey: string;
+ roleSort: number;
+ dataScope: string;
+ menuCheckStrictly: boolean;
+ deptCheckStrictly: boolean;
+ status: string;
+ delFlag: string;
+ remark?: any;
+ flag: boolean;
+ menuIds?: Array<string | number>;
+ deptIds?: Array<string | number>;
+ admin: boolean;
+}
+
+export interface RoleQuery extends PageQuery {
+ roleName: string;
+ roleKey: string;
+ status: string;
+}
+
+export interface RoleForm {
+ roleName: string;
+ roleKey: string;
+ roleSort: number;
+ status: string;
+ menuCheckStrictly: boolean;
+ deptCheckStrictly: boolean;
+ remark: string;
+ dataScope?: string;
+ roleId: string | undefined;
+ menuIds: Array<string | number>;
+ deptIds: Array<string | number>;
+}
diff --git a/src/api/system/social/auth.ts b/src/api/system/social/auth.ts
new file mode 100644
index 0000000..17a46d3
--- /dev/null
+++ b/src/api/system/social/auth.ts
@@ -0,0 +1,24 @@
+import request from '@/utils/request';
+
+// 绑定账号
+export function authBinding(source: string) {
+ return request({
+ url: '/auth/binding/' + source,
+ method: 'get'
+ });
+}
+
+// 解绑账号
+export function authUnlock(authId: string) {
+ return request({
+ url: '/auth/unlock/' + authId,
+ method: 'delete'
+ });
+}
+//获取授权列表
+export function getAuthList() {
+ return request({
+ url: '/system/social/list',
+ method: 'get'
+ });
+}
diff --git a/src/api/system/tenant/index.ts b/src/api/system/tenant/index.ts
new file mode 100644
index 0000000..4531a0a
--- /dev/null
+++ b/src/api/system/tenant/index.ts
@@ -0,0 +1,92 @@
+import request from '@/utils/request';
+import { TenantForm, TenantQuery, TenantVO } from './types';
+import { AxiosPromise } from 'axios';
+
+// 查询租户列表
+export function listTenant(query: TenantQuery): AxiosPromise<TenantVO[]> {
+ return request({
+ url: '/system/tenant/list',
+ method: 'get',
+ params: query
+ });
+}
+
+// 查询租户详细
+export function getTenant(id: string | number): AxiosPromise<TenantVO> {
+ return request({
+ url: '/system/tenant/' + id,
+ method: 'get'
+ });
+}
+
+// 新增租户
+export function addTenant(data: TenantForm) {
+ return request({
+ url: '/system/tenant',
+ method: 'post',
+ headers: {
+ isEncrypt: true
+ },
+ data: data
+ });
+}
+
+// 修改租户
+export function updateTenant(data: TenantForm) {
+ return request({
+ url: '/system/tenant',
+ method: 'put',
+ data: data
+ });
+}
+
+// 租户状态修改
+export function changeTenantStatus(id: string | number, tenantId: string | number, status: string) {
+ const data = {
+ id,
+ tenantId,
+ status
+ };
+ return request({
+ url: '/system/tenant/changeStatus',
+ method: 'put',
+ data: data
+ });
+}
+
+// 删除租户
+export function delTenant(id: string | number | Array<string | number>) {
+ return request({
+ url: '/system/tenant/' + id,
+ method: 'delete'
+ });
+}
+
+// 动态切换租户
+export function dynamicTenant(tenantId: string | number) {
+ return request({
+ url: '/system/tenant/dynamic/' + tenantId,
+ method: 'get'
+ });
+}
+
+// 清除动态租户
+export function dynamicClear() {
+ return request({
+ url: '/system/tenant/dynamic/clear',
+ method: 'get'
+ });
+}
+
+// 同步租户套餐
+export function syncTenantPackage(tenantId: string | number, packageId: string | number) {
+ const data = {
+ tenantId,
+ packageId
+ };
+ return request({
+ url: '/system/tenant/syncTenantPackage',
+ method: 'get',
+ params: data
+ });
+}
diff --git a/src/api/system/tenant/types.ts b/src/api/system/tenant/types.ts
new file mode 100644
index 0000000..25395fa
--- /dev/null
+++ b/src/api/system/tenant/types.ts
@@ -0,0 +1,46 @@
+export interface TenantVO extends BaseEntity {
+ id: number | string;
+ tenantId: number | string;
+ username: string;
+ contactUserName: string;
+ contactPhone: string;
+ companyName: string;
+ licenseNumber: string;
+ address: string;
+ domain: string;
+ intro: string;
+ remark: string;
+ packageId: string | number;
+ expireTime: string;
+ accountCount: number;
+ status: string;
+}
+
+export interface TenantQuery extends PageQuery {
+ tenantId: string | number;
+
+ contactUserName: string;
+
+ contactPhone: string;
+
+ companyName: string;
+}
+
+export interface TenantForm {
+ id: number | string | undefined;
+ tenantId: number | string | undefined;
+ username: string;
+ password: string;
+ contactUserName: string;
+ contactPhone: string;
+ companyName: string;
+ licenseNumber: string;
+ domain: string;
+ address: string;
+ intro: string;
+ remark: string;
+ packageId: string | number;
+ expireTime: string;
+ accountCount: number;
+ status: string;
+}
diff --git a/src/api/system/tenantPackage/index.ts b/src/api/system/tenantPackage/index.ts
new file mode 100644
index 0000000..70d0dab
--- /dev/null
+++ b/src/api/system/tenantPackage/index.ts
@@ -0,0 +1,67 @@
+import request from '@/utils/request';
+import { TenantPkgForm, TenantPkgQuery, TenantPkgVO } from './types';
+import { AxiosPromise } from 'axios';
+
+// 查询租户套餐列表
+export function listTenantPackage(query?: TenantPkgQuery): AxiosPromise<TenantPkgVO[]> {
+ return request({
+ url: '/system/tenant/package/list',
+ method: 'get',
+ params: query
+ });
+}
+
+// 查询租户套餐下拉选列表
+export function selectTenantPackage(): AxiosPromise<TenantPkgVO[]> {
+ return request({
+ url: '/system/tenant/package/selectList',
+ method: 'get'
+ });
+}
+
+// 查询租户套餐详细
+export function getTenantPackage(packageId: string | number): AxiosPromise<TenantPkgVO> {
+ return request({
+ url: '/system/tenant/package/' + packageId,
+ method: 'get'
+ });
+}
+
+// 新增租户套餐
+export function addTenantPackage(data: TenantPkgForm) {
+ return request({
+ url: '/system/tenant/package',
+ method: 'post',
+ data: data
+ });
+}
+
+// 修改租户套餐
+export function updateTenantPackage(data: TenantPkgForm) {
+ return request({
+ url: '/system/tenant/package',
+ method: 'put',
+ data: data
+ });
+}
+
+// 租户套餐状态修改
+export function changePackageStatus(packageId: number | string, status: string) {
+ const data = {
+ packageId,
+ status
+ };
+ return request({
+ url: '/system/tenant/package/changeStatus',
+ method: 'put',
+ data: data
+ });
+}
+
+// 删除租户套餐
+export function delTenantPackage(packageId: string | number | Array<string | number>) {
+ return request({
+ url: '/system/tenant/package/' + packageId,
+ method: 'delete'
+ });
+}
diff --git a/src/api/system/tenantPackage/types.ts b/src/api/system/tenantPackage/types.ts
new file mode 100644
index 0000000..c24b8fc
--- /dev/null
+++ b/src/api/system/tenantPackage/types.ts
@@ -0,0 +1,20 @@
+export interface TenantPkgVO extends BaseEntity {
+ packageId: string | number;
+ packageName: string;
+ menuIds: string;
+ remark: string;
+ menuCheckStrictly: boolean;
+ status: string;
+}
+
+export interface TenantPkgQuery extends PageQuery {
+ packageName: string;
+}
+
+export interface TenantPkgForm {
+ packageId: string | number | undefined;
+ packageName: string;
+ menuIds: string;
+ remark: string;
+ menuCheckStrictly: boolean;
+}
diff --git a/src/api/system/user/index.ts b/src/api/system/user/index.ts
new file mode 100644
index 0000000..fdcafc2
--- /dev/null
+++ b/src/api/system/user/index.ts
@@ -0,0 +1,215 @@
+import { DeptVO } from './../dept/types';
+import { RoleVO } from '@/api/system/role/types';
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { UserForm, UserQuery, UserVO, UserInfoVO } from './types';
+import { parseStrEmpty } from '@/utils/ruoyi';
+
+/**
+ * 查询用户列表
+ * @param query
+ */
+export const listUser = (query: UserQuery): AxiosPromise<UserVO[]> => {
+ return request({
+ url: '/system/user/list',
+ method: 'get',
+ params: query
+ });
+};
+
+/**
+ * 获取用户详情
+ * @param userId
+ */
+export const getUser = (userId?: string | number): AxiosPromise<UserInfoVO> => {
+ return request({
+ url: '/system/user/' + parseStrEmpty(userId),
+ method: 'get'
+ });
+};
+
+/**
+ * 新增用户
+ */
+export const addUser = (data: UserForm) => {
+ return request({
+ url: '/system/user',
+ method: 'post',
+ data: data
+ });
+};
+
+/**
+ * 修改用户
+ */
+export const updateUser = (data: UserForm) => {
+ return request({
+ url: '/system/user',
+ method: 'put',
+ data: data
+ });
+};
+
+/**
+ * 删除用户
+ * @param userId 用户ID
+ */
+export const delUser = (userId: Array<string | number> | string | number) => {
+ return request({
+ url: '/system/user/' + userId,
+ method: 'delete'
+ });
+};
+
+/**
+ * 用户密码重置
+ * @param userId 用户ID
+ * @param password 密码
+ */
+export const resetUserPwd = (userId: string | number, password: string) => {
+ const data = {
+ userId,
+ password
+ };
+ return request({
+ url: '/system/user/resetPwd',
+ method: 'put',
+ headers: {
+ isEncrypt: true
+ },
+ data: data
+ });
+};
+
+/**
+ * 用户状态修改
+ * @param userId 用户ID
+ * @param status 用户状态
+ */
+export const changeUserStatus = (userId: number | string, status: string) => {
+ const data = {
+ userId,
+ status
+ };
+ return request({
+ url: '/system/user/changeStatus',
+ method: 'put',
+ data: data
+ });
+};
+
+/**
+ * 查询用户个人信息
+ */
+export const getUserProfile = (): AxiosPromise<UserInfoVO> => {
+ return request({
+ url: '/system/user/profile',
+ method: 'get'
+ });
+};
+
+/**
+ * 修改用户个人信息
+ * @param data 用户信息
+ */
+export const updateUserProfile = (data: UserForm) => {
+ return request({
+ url: '/system/user/profile',
+ method: 'put',
+ data: data
+ });
+};
+
+/**
+ * 用户密码重置
+ * @param oldPassword 旧密码
+ * @param newPassword 新密码
+ */
+export const updateUserPwd = (oldPassword: string, newPassword: string) => {
+ const data = {
+ oldPassword,
+ newPassword
+ };
+ return request({
+ url: '/system/user/profile/updatePwd',
+ method: 'put',
+ headers: {
+ isEncrypt: true
+ },
+ data: data
+ });
+};
+
+/**
+ * 用户头像上传
+ * @param data 头像文件
+ */
+export const uploadAvatar = (data: FormData) => {
+ return request({
+ url: '/system/user/profile/avatar',
+ method: 'post',
+ data: data
+ });
+};
+
+/**
+ * 查询授权角色
+ * @param userId 用户ID
+ */
+export const getAuthRole = (userId: string | number): AxiosPromise<{ user: UserVO; roles: RoleVO[] }> => {
+ return request({
+ url: '/system/user/authRole/' + userId,
+ method: 'get'
+ });
+};
+
+/**
+ * 保存授权角色
+ * @param data 用户ID
+ */
+export const updateAuthRole = (data: { userId: string; roleIds: string }) => {
+ return request({
+ url: '/system/user/authRole',
+ method: 'put',
+ params: data
+ });
+};
+
+/**
+ * 查询当前部门的所有用户信息
+ * @param deptId
+ */
+export const listUserByDeptId = (deptId: string | number): AxiosPromise<UserVO[]> => {
+ return request({
+ url: '/system/user/list/dept/' + deptId,
+ method: 'get'
+ });
+};
+
+/**
+ * 查询部门下拉树结构
+ */
+export const deptTreeSelect = (): AxiosPromise<DeptVO[]> => {
+ return request({
+ url: '/system/user/deptTree',
+ method: 'get'
+ });
+};
+
+export default {
+ listUser,
+ getUser,
+ addUser,
+ updateUser,
+ delUser,
+ resetUserPwd,
+ changeUserStatus,
+ getUserProfile,
+ updateUserProfile,
+ updateUserPwd,
+ uploadAvatar,
+ getAuthRole,
+ updateAuthRole,
+ deptTreeSelect,
+ listUserByDeptId
+};
diff --git a/src/api/system/user/types.ts b/src/api/system/user/types.ts
new file mode 100644
index 0000000..77e9c32
--- /dev/null
+++ b/src/api/system/user/types.ts
@@ -0,0 +1,84 @@
+import { DeptVO } from './../dept/types';
+import { RoleVO } from '@/api/system/role/types';
+import { PostVO } from '@/api/system/post/types';
+
+/**
+ * 用户信息
+ */
+export interface UserInfo {
+ user: UserVO;
+ roles: string[];
+ permissions: string[];
+}
+
+/**
+ * 用户查询对象类型
+ */
+export interface UserQuery extends PageQuery {
+ userName?: string;
+ phonenumber?: string;
+ status?: string;
+ deptId?: string | number;
+ roleId?: string | number;
+}
+
+/**
+ * 用户返回对象
+ */
+export interface UserVO extends BaseEntity {
+ userId: string | number;
+ deptId: number;
+ userName: string;
+ nickName: string;
+ userType: string;
+ email: string;
+ phonenumber: string;
+ sex: string;
+ avatar: string;
+ status: string;
+ delFlag: string;
+ loginIp: string;
+ loginDate: string;
+ remark: string;
+ dept: DeptVO;
+ roles: RoleVO[];
+ roleIds: any;
+ postIds: any;
+ roleId: any;
+ admin: boolean;
+}
+
+/**
+ * 用户表单类型
+ */
+export interface UserForm {
+ id?: string;
+ userId?: string;
+ deptId?: number;
+ userName: string;
+ nickName?: string;
+ password: string;
+ phonenumber?: string;
+ email?: string;
+ sex?: string;
+ status: string;
+ remark?: string;
+ postIds: string[];
+ roleIds: string[];
+}
+
+export interface UserInfoVO {
+ user: UserVO;
+ roles: RoleVO[];
+ roleIds: string[];
+ posts: PostVO[];
+ postIds: string[];
+ roleGroup: string;
+ postGroup: string;
+}
+
+export interface ResetPwdForm {
+ oldPassword: string;
+ newPassword: string;
+ confirmPassword: string;
+}
diff --git a/src/api/tool/gen/index.ts b/src/api/tool/gen/index.ts
new file mode 100644
index 0000000..b91df6a
--- /dev/null
+++ b/src/api/tool/gen/index.ts
@@ -0,0 +1,86 @@
+import request from '@/utils/request';
+import { DbTableQuery, DbTableVO, TableQuery, TableVO, GenTableVO, DbTableForm } from './types';
+import { AxiosPromise } from 'axios';
+
+// 查询生成表数据
+export const listTable = (query: TableQuery): AxiosPromise<TableVO[]> => {
+ return request({
+ url: '/tool/gen/list',
+ method: 'get',
+ params: query
+ });
+};
+// 查询db数据库列表
+export const listDbTable = (query: DbTableQuery): AxiosPromise<DbTableVO[]> => {
+ return request({
+ url: '/tool/gen/db/list',
+ method: 'get',
+ params: query
+ });
+};
+
+// 查询表详细信息
+export const getGenTable = (tableId: string | number): AxiosPromise<GenTableVO> => {
+ return request({
+ url: '/tool/gen/' + tableId,
+ method: 'get'
+ });
+};
+
+// 修改代码生成信息
+export const updateGenTable = (data: DbTableForm) => {
+ return request({
+ url: '/tool/gen',
+ method: 'put',
+ data: data
+ });
+};
+
+// 导入表
+export const importTable = (data: { tables: string; dataName: string }) => {
+ return request({
+ url: '/tool/gen/importTable',
+ method: 'post',
+ params: data
+ });
+};
+
+// 预览生成代码
+export const previewTable = (tableId: string | number) => {
+ return request({
+ url: '/tool/gen/preview/' + tableId,
+ method: 'get'
+ });
+};
+
+// 删除表数据
+export const delTable = (tableId: string | number | Array<string | number>) => {
+ return request({
+ url: '/tool/gen/' + tableId,
+ method: 'delete'
+ });
+};
+
+// 生成代码(自定义路径)
+export const genCode = (tableId: string | number) => {
+ return request({
+ url: '/tool/gen/genCode/' + tableId,
+ method: 'get'
+ });
+};
+
+// 同步数据库
+export const synchDb = (tableId: string | number) => {
+ return request({
+ url: '/tool/gen/synchDb/' + tableId,
+ method: 'get'
+ });
+};
+
+// 获取数据源名称
+export const getDataNames = () => {
+ return request({
+ url: '/tool/gen/getDataNames',
+ method: 'get'
+ });
+};
diff --git a/src/api/tool/gen/types.ts b/src/api/tool/gen/types.ts
new file mode 100644
index 0000000..7f7b62f
--- /dev/null
+++ b/src/api/tool/gen/types.ts
@@ -0,0 +1,180 @@
+export interface TableVO extends BaseEntity {
+ createDept: number | string;
+ tableId: string | number;
+ dataName: string;
+ tableName: string;
+ tableComment: string;
+ subTableName?: any;
+ subTableFkName?: any;
+ className: string;
+ tplCategory: string;
+ packageName: string;
+ moduleName: string;
+ businessName: string;
+ functionName: string;
+ functionAuthor: string;
+ genType: string;
+ genPath: string;
+ pkColumn?: any;
+ columns?: any;
+ options?: any;
+ remark?: any;
+ treeCode?: any;
+ treeParentCode?: any;
+ treeName?: any;
+ menuIds?: any;
+ parentMenuId?: any;
+ parentMenuName?: any;
+ tree: boolean;
+ crud: boolean;
+}
+
+export interface TableQuery extends PageQuery {
+ tableName: string;
+ tableComment: string;
+ dataName: string;
+}
+
+export interface DbColumnVO extends BaseEntity {
+ createDept?: any;
+ columnId?: any;
+ tableId?: any;
+ columnName?: any;
+ columnComment?: any;
+ columnType?: any;
+ javaType?: any;
+ javaField?: any;
+ isPk?: any;
+ isIncrement?: any;
+ isRequired?: any;
+ isInsert?: any;
+ isEdit?: any;
+ isList?: any;
+ isQuery?: any;
+ queryType?: any;
+ htmlType?: any;
+ dictType?: any;
+ sort?: any;
+ increment: boolean;
+ capJavaField?: any;
+ usableColumn: boolean;
+ superColumn: boolean;
+ list: boolean;
+ pk: boolean;
+ insert: boolean;
+ edit: boolean;
+ query: boolean;
+ required: boolean;
+}
+
+export interface DbTableVO {
+ createDept?: any;
+ tableId?: any;
+ tableName: string;
+ tableComment: string;
+ subTableName?: any;
+ subTableFkName?: any;
+ className?: any;
+ tplCategory?: any;
+ packageName?: any;
+ moduleName?: any;
+ businessName?: any;
+ functionName?: any;
+ functionAuthor?: any;
+ genType?: any;
+ genPath?: any;
+ pkColumn?: any;
+ columns: DbColumnVO[];
+ options?: any;
+ remark?: any;
+ treeCode?: any;
+ treeParentCode?: any;
+ treeName?: any;
+ menuIds?: any;
+ parentMenuId?: any;
+ parentMenuName?: any;
+ tree: boolean;
+ crud: boolean;
+}
+
+export interface DbTableQuery extends PageQuery {
+ dataName: string;
+ tableName: string;
+ tableComment: string;
+}
+
+export interface GenTableVO {
+ info: DbTableVO;
+ rows: DbColumnVO[];
+ tables: DbTableVO[];
+}
+
+export interface DbColumnForm extends BaseEntity {
+ createDept: number;
+ columnId: string;
+ tableId: string;
+ columnName: string;
+ columnComment: string;
+ columnType: string;
+ javaType: string;
+ javaField: string;
+ isPk: string;
+ isIncrement: string;
+ isRequired: string;
+ isInsert?: any;
+ isEdit: string;
+ isList: string;
+ isQuery?: any;
+ queryType: string;
+ htmlType: string;
+ dictType: string;
+ sort: number;
+ increment: boolean;
+ capJavaField: string;
+ usableColumn: boolean;
+ superColumn: boolean;
+ list: boolean;
+ pk: boolean;
+ insert: boolean;
+ edit: boolean;
+ query: boolean;
+ required: boolean;
+}
+
+export interface DbParamForm {
+ treeCode?: any;
+ treeName?: any;
+ treeParentCode?: any;
+ parentMenuId: string;
+}
+
+export interface DbTableForm extends BaseEntity {
+ createDept?: any;
+ tableId: string | string;
+ tableName: string;
+ tableComment: string;
+ subTableName?: any;
+ subTableFkName?: any;
+ className: string;
+ tplCategory: string;
+ packageName: string;
+ moduleName: string;
+ businessName: string;
+ functionName: string;
+ functionAuthor: string;
+ genType: string;
+ genPath: string;
+ pkColumn?: any;
+ columns: DbColumnForm[];
+ options: string;
+ remark?: any;
+ treeCode?: any;
+ treeParentCode?: any;
+ treeName?: any;
+ menuIds?: any;
+ parentMenuId: string;
+ parentMenuName?: any;
+ tree: boolean;
+ crud: boolean;
+ params: DbParamForm;
+}
diff --git a/src/api/types.ts b/src/api/types.ts
new file mode 100644
index 0000000..617286c
--- /dev/null
+++ b/src/api/types.ts
@@ -0,0 +1,59 @@
+/**
+ * 注册
+ */
+export type RegisterForm = {
+ tenantId: string;
+ username: string;
+ password: string;
+ confirmPassword?: string;
+ code?: string;
+ uuid?: string;
+ userType?: string;
+};
+
+/**
+ * 登录请求
+ */
+export interface LoginData {
+ tenantId?: string;
+ username?: string;
+ password?: string;
+ rememberMe?: boolean;
+ socialCode?: string;
+ socialState?: string;
+ source?: string;
+ code?: string;
+ uuid?: string;
+ clientId: string;
+ grantType: string;
+}
+
+/**
+ * 登录响应
+ */
+export interface LoginResult {
+ access_token: string;
+}
+
+/**
+ * 验证码返回
+ */
+export interface VerifyCodeResult {
+ captchaEnabled: boolean;
+ uuid?: string;
+ img?: string;
+}
+
+/**
+ * 租户
+ */
+export interface TenantVO {
+ companyName: string;
+ domain: any;
+ tenantId: string;
+}
+
+export interface TenantInfo {
+ tenantEnabled: boolean;
+ voList: TenantVO[];
+}
diff --git a/src/assets/401_images/401.gif b/src/assets/401_images/401.gif
new file mode 100644
index 0000000..cd6e0d9
--- /dev/null
+++ b/src/assets/401_images/401.gif
Binary files differ
diff --git a/src/assets/404_images/404.png b/src/assets/404_images/404.png
new file mode 100644
index 0000000..3d8e230
--- /dev/null
+++ b/src/assets/404_images/404.png
Binary files differ
diff --git a/src/assets/404_images/404_cloud.png b/src/assets/404_images/404_cloud.png
new file mode 100644
index 0000000..c6281d0
--- /dev/null
+++ b/src/assets/404_images/404_cloud.png
Binary files differ
diff --git a/src/assets/icons/svg/404.svg b/src/assets/icons/svg/404.svg
new file mode 100644
index 0000000..6df5019
--- /dev/null
+++ b/src/assets/icons/svg/404.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M121.718 73.272v9.953c3.957-7.584 6.199-16.05 6.199-24.995C127.917 26.079 99.273 0 63.958 0 28.644 0 0 26.079 0 58.23c0 .403.028.806.028 1.21l22.97-25.953h13.34l-19.76 27.187h6.42V53.77l13.728-19.477v49.361H22.998V73.272H2.158c5.951 20.284 23.608 36.208 45.998 41.399-1.44 3.3-5.618 11.263-12.565 12.674-8.607 1.764 23.358.428 46.163-13.178 17.519-4.611 31.938-15.849 39.77-30.513h-13.506V73.272H85.02V59.464l22.998-25.977h13.008l-19.429 27.187h6.421v-7.433l13.727-19.402v39.433h-.027zm-78.24 2.822a10.516 10.516 0 0 1-.996-4.535V44.548c0-1.613.332-3.124.996-4.535a11.66 11.66 0 0 1 2.713-3.68c1.134-1.032 2.49-1.864 4.04-2.468 1.55-.605 3.21-.908 4.982-.908h11.292c1.77 0 3.431.303 4.981.908 1.522.604 2.85 1.41 3.986 2.418l-12.26 16.303v-2.898a1.96 1.96 0 0 0-.665-1.512c-.443-.403-.996-.604-1.66-.604-.665 0-1.218.201-1.661.604a1.96 1.96 0 0 0-.664 1.512v9.071L44.364 77.606a10.556 10.556 0 0 1-.886-1.512zm35.73-4.535c0 1.613-.332 3.124-.997 4.535a11.66 11.66 0 0 1-2.712 3.68c-1.134 1.032-2.49 1.864-4.04 2.469-1.55.604-3.21.907-4.982.907H55.185c-1.77 0-3.431-.303-4.981-.907-1.55-.605-2.906-1.437-4.041-2.47a12.49 12.49 0 0 1-1.384-1.512l13.727-18.217v6.375c0 .605.222 1.109.665 1.512.442.403.996.604 1.66.604.664 0 1.218-.201 1.66-.604a1.96 1.96 0 0 0 .665-1.512V53.87L75.97 36.838c.913.932 1.66 1.99 2.214 3.175.664 1.41.996 2.922.996 4.535v27.011h.028z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/bug.svg b/src/assets/icons/svg/bug.svg
new file mode 100644
index 0000000..05a150d
--- /dev/null
+++ b/src/assets/icons/svg/bug.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M127.88 73.143c0 1.412-.506 2.635-1.518 3.669-1.011 1.033-2.209 1.55-3.592 1.55h-17.887c0 9.296-1.783 17.178-5.35 23.645l16.609 17.044c1.011 1.034 1.517 2.257 1.517 3.67 0 1.412-.506 2.635-1.517 3.668-.958 1.033-2.155 1.55-3.593 1.55-1.438 0-2.635-.517-3.593-1.55l-15.811-16.063a15.49 15.49 0 0 1-1.196 1.06c-.532.434-1.65 1.208-3.353 2.322a50.104 50.104 0 0 1-5.192 2.974c-1.758.87-3.94 1.658-6.546 2.364-2.607.706-5.189 1.06-7.748 1.06V47.044H58.89v73.062c-2.716 0-5.417-.367-8.106-1.102-2.688-.734-5.003-1.631-6.945-2.692a66.769 66.769 0 0 1-5.268-3.179c-1.571-1.057-2.73-1.94-3.476-2.65L33.9 109.34l-14.611 16.877c-1.066 1.14-2.344 1.711-3.833 1.711-1.277 0-2.422-.434-3.434-1.304-1.012-.978-1.557-2.187-1.635-3.627-.079-1.44.333-2.705 1.236-3.794l16.129-18.51c-3.087-6.197-4.63-13.644-4.63-22.342H5.235c-1.383 0-2.58-.517-3.592-1.55S.125 74.545.125 73.132c0-1.412.506-2.635 1.518-3.668 1.012-1.034 2.21-1.55 3.592-1.55h17.887V43.939L9.308 29.833c-1.012-1.033-1.517-2.256-1.517-3.669 0-1.412.505-2.635 1.517-3.668 1.012-1.034 2.21-1.55 3.593-1.55s2.58.516 3.593 1.55l13.813 14.106h67.396l13.814-14.106c1.012-1.034 2.21-1.55 3.592-1.55 1.384 0 2.581.516 3.593 1.55 1.012 1.033 1.518 2.256 1.518 3.668 0 1.413-.506 2.636-1.518 3.67l-13.814 14.105v23.975h17.887c1.383 0 2.58.516 3.593 1.55 1.011 1.033 1.517 2.256 1.517 3.668l-.005.01zM89.552 26.175H38.448c0-7.23 2.489-13.386 7.466-18.469C50.892 2.623 56.92.082 64 .082c7.08 0 13.108 2.541 18.086 7.624 4.977 5.083 7.466 11.24 7.466 18.469z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/build.svg b/src/assets/icons/svg/build.svg
new file mode 100644
index 0000000..97c4688
--- /dev/null
+++ b/src/assets/icons/svg/build.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1568899741379" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2054" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M960 591.424V368.96c0-0.288 0.16-0.512 0.16-0.768S960 367.68 960 367.424V192a32 32 0 0 0-32-32H96a32 32 0 0 0-32 32v175.424c0 0.288-0.16 0.512-0.16 0.768s0.16 0.48 0.16 0.768v222.464c0 0.288-0.16 0.512-0.16 0.768s0.16 0.48 0.16 0.768V864a32 32 0 0 0 32 32h832a32 32 0 0 0 32-32v-271.04c0-0.288 0.16-0.512 0.16-0.768S960 591.68 960 591.424z m-560-31.232v-160H608v160h-208z m208 64V832h-208v-207.808H608z m-480-224h208v160H128v-160z m544 0h224v160h-224v-160zM896 224v112.192H128V224h768zM128 624.192h208V832H128v-207.808zM672 832v-207.808h224V832h-224z" p-id="2055"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/button.svg b/src/assets/icons/svg/button.svg
new file mode 100644
index 0000000..904fddc
--- /dev/null
+++ b/src/assets/icons/svg/button.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1588670460195" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1314" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M230.4 307.712c13.824 0 25.088-11.264 25.088-25.088 0-100.352 81.92-182.272 182.272-182.272s182.272 81.408 182.272 182.272c0 13.824 11.264 25.088 25.088 25.088s25.088-11.264 24.576-25.088c0-127.488-103.936-231.936-231.936-231.936S205.824 154.624 205.824 282.624c-0.512 14.336 10.752 25.088 24.576 25.088z m564.736 234.496c-11.264 0-21.504 2.048-31.232 6.144 0-44.544-40.448-81.92-88.064-81.92-14.848 0-28.16 3.584-39.936 10.24-13.824-28.16-44.544-48.128-78.848-48.128-12.288 0-24.576 2.56-35.328 7.68V284.16c0-45.568-37.888-81.92-84.48-81.92s-84.48 36.864-84.48 81.92v348.672l-69.12-112.64c-18.432-28.16-58.368-36.864-91.136-19.968-26.624 14.336-46.592 47.104-30.208 88.064 3.072 8.192 76.8 205.312 171.52 311.296 0 0 28.16 24.576 43.008 58.88 4.096 9.728 13.312 15.36 22.528 15.36 3.072 0 6.656-0.512 9.728-2.048 12.288-5.12 18.432-19.968 12.8-32.256-19.456-44.544-53.76-74.752-53.76-74.752C281.6 768 209.408 573.44 208.384 570.88c-5.12-12.8-2.56-20.992 7.168-26.112 9.216-4.608 21.504-4.608 26.112 2.56l113.152 184.32c4.096 8.704 12.8 14.336 22.528 14.336 13.824 0 25.088-10.752 25.088-25.088V284.16c0-17.92 15.36-32.256 34.816-32.256s34.816 14.336 34.816 32.256v284.16c0 13.824 10.24 25.088 24.576 25.088 13.824 0 25.088-11.264 25.088-25.088v-57.344c0-17.92 15.36-32.768 34.816-32.768 19.968 0 37.376 15.36 37.376 32.768v95.232c0 7.168 3.072 13.312 7.68 17.92 4.608 4.608 10.752 7.168 17.92 7.168 13.824 0 24.576-11.264 24.576-25.088V547.84c0-18.432 13.824-32.256 32.256-32.256 20.48 0 38.912 15.36 38.912 32.256v95.232c0 13.824 11.264 25.088 25.088 25.088s24.576-11.264 25.088-25.088v-18.944c0-18.944 12.8-32.256 30.72-32.256 18.432 0 22.528 18.944 22.528 31.744 0 1.024-11.776 99.84-50.688 173.056-30.72 58.368-45.056 112.128-51.2 146.944-2.56 13.312 6.656 26.112 19.968 28.672 1.536 0 3.072 0.512 4.608 0.512 11.776 0 22.016-8.192 24.064-20.48 5.632-31.232 18.432-79.36 46.08-132.608 43.52-81.92 55.808-186.88 56.32-193.536-0.512-50.688-29.696-83.968-72.704-83.968z"></path></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/cascader.svg b/src/assets/icons/svg/cascader.svg
new file mode 100644
index 0000000..e256024
--- /dev/null
+++ b/src/assets/icons/svg/cascader.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1576153230908" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="971" xmlns:xlink="http://www.w3.org/1999/xlink" width="81" height="81"><defs><style type="text/css"></style></defs><path d="M772.87036133 734.06115723c-43.34106445 0-80.00793458 27.93273926-93.76831055 66.57714843H475.90991211c-56.60705567 0-102.66723633-46.06018067-102.66723633-102.66723633V600.82446289h305.859375c13.76037598 38.64440918 50.42724609 66.57714844 93.76831055 66.57714844 55.12390137 0 99.94812012-44.82421875 99.94812012-99.94812012S827.9942627 467.50537109 772.87036133 467.50537109c-43.34106445 0-80.00793458 27.93273926-93.76831055 66.57714844H373.24267578V401.01062011h321.92687989c55.12390137 0 99.94812012-44.82421875 99.94812011-99.94812011V190.07312011C795.11767578 134.94921875 750.29345703 90.125 695.16955567 90.125H251.12963867C196.0057373 90.125 151.18151855 134.94921875 151.18151855 190.07312011V301.0625c0 55.12390137 44.82421875 99.94812012 99.94812012 99.94812012h55.53588867v296.96044921c0 93.35632325 75.97045898 169.32678223 169.32678224 169.32678223h203.19213866c13.76037598 38.64440918 50.42724609 66.57714844 93.76831055 66.57714844 55.12390137 0 99.94812012-44.82421875 99.94812012-99.94812012s-44.90661622-99.86572266-100.03051758-99.86572265z m0-199.89624024c18.37463379 0 33.28857422 14.91394043 33.28857422 33.28857423s-14.91394043 33.28857422-33.28857422 33.28857421-33.28857422-14.91394043-33.28857422-33.28857421 14.91394043-33.28857422 33.28857422-33.28857422zM217.75866699 301.0625V190.07312011c0-18.37463379 14.91394043-33.28857422 33.28857423-33.28857421h444.03991698c18.37463379 0 33.28857422 14.91394043 33.28857422 33.28857422V301.0625c0 18.37463379-14.91394043 33.28857422-33.28857422 33.28857422H251.12963867c-18.37463379 0-33.37097168-14.91394043-33.37097168-33.28857422z m555.11169434 566.23535156c-18.37463379 0-33.28857422-14.91394043-33.28857422-33.28857422 0-18.37463379 14.91394043-33.28857422 33.28857422-33.28857422s33.28857422 14.91394043 33.28857422 33.28857422c0.08239747 18.29223633-14.91394043 33.28857422-33.28857422 33.28857422z" p-id="972"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/chart.svg b/src/assets/icons/svg/chart.svg
new file mode 100644
index 0000000..27728fb
--- /dev/null
+++ b/src/assets/icons/svg/chart.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M0 54.857h36.571V128H0V54.857zM91.429 27.43H128V128H91.429V27.429zM45.714 0h36.572v128H45.714V0z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/checkbox.svg b/src/assets/icons/svg/checkbox.svg
new file mode 100644
index 0000000..013fd3a
--- /dev/null
+++ b/src/assets/icons/svg/checkbox.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1575982282951" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="902" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M828.40625 90.125H195.59375C137.375 90.125 90.125 137.375 90.125 195.59375v632.8125c0 58.21875 47.25 105.46875 105.46875 105.46875h632.8125c58.21875 0 105.46875-47.25 105.46875-105.46875V195.59375c0-58.21875-47.25-105.46875-105.46875-105.46875z m52.734375 738.28125c0 29.16-23.57015625 52.734375-52.734375 52.734375H195.59375c-29.109375 0-52.734375-23.574375-52.734375-52.734375V195.59375c0-29.109375 23.625-52.734375 52.734375-52.734375h632.8125c29.16 0 52.734375 23.625 52.734375 52.734375v632.8125z" p-id="903"></path><path d="M421.52890625 709.55984375a36.28125 36.28125 0 0 1-27.55265625-12.66890625L205.17453125 476.613125a36.28546875 36.28546875 0 0 1 55.10109375-47.22890625l164.986875 192.4846875 342.16171875-298.48078125a36.2896875 36.2896875 0 0 1 47.70984375 54.68765625L445.3859375 700.6203125a36.3234375 36.3234375 0 0 1-23.85703125 8.93953125z" p-id="904"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/clipboard.svg b/src/assets/icons/svg/clipboard.svg
new file mode 100644
index 0000000..90923ff
--- /dev/null
+++ b/src/assets/icons/svg/clipboard.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M54.857 118.857h64V73.143H89.143c-1.902 0-3.52-.668-4.855-2.002-1.335-1.335-2.002-2.954-2.002-4.855V36.57H54.857v82.286zM73.143 16v-4.571a2.2 2.2 0 0 0-.677-1.61 2.198 2.198 0 0 0-1.609-.676H20.571c-.621 0-1.158.225-1.609.676a2.198 2.198 0 0 0-.676 1.61V16a2.2 2.2 0 0 0 .676 1.61c.451.45.988.676 1.61.676h50.285c.622 0 1.158-.226 1.61-.677.45-.45.676-.987.676-1.609zm18.286 48h21.357L91.43 42.642V64zM128 73.143v48c0 1.902-.667 3.52-2.002 4.855-1.335 1.335-2.953 2.002-4.855 2.002H52.57c-1.901 0-3.52-.667-4.854-2.002-1.335-1.335-2.003-2.953-2.003-4.855v-11.429H6.857c-1.902 0-3.52-.667-4.855-2.002C.667 106.377 0 104.759 0 102.857v-96c0-1.902.667-3.52 2.002-4.855C3.337.667 4.955 0 6.857 0h77.714c1.902 0 3.52.667 4.855 2.002 1.335 1.335 2.003 2.953 2.003 4.855V30.29c1 .622 1.856 1.29 2.569 2.003l29.147 29.147c1.335 1.335 2.478 3.145 3.429 5.43.95 2.287 1.426 4.383 1.426 6.291v-.018z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/code.svg b/src/assets/icons/svg/code.svg
new file mode 100644
index 0000000..5f9c5ab
--- /dev/null
+++ b/src/assets/icons/svg/code.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1546567861908" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2422" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M318.577778 819.2L17.066667 512l301.511111-307.2 45.511111 45.511111L96.711111 512l267.377778 261.688889zM705.422222 819.2l-45.511111-45.511111L927.288889 512l-267.377778-261.688889 45.511111-45.511111L1006.933333 512zM540.785778 221.866667l55.751111 11.150222L483.157333 802.133333l-55.751111-11.093333z" p-id="2423"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/color.svg b/src/assets/icons/svg/color.svg
new file mode 100644
index 0000000..44a81aa
--- /dev/null
+++ b/src/assets/icons/svg/color.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1577252187056" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2508" xmlns:xlink="http://www.w3.org/1999/xlink" width="81" height="81"><defs><style type="text/css"></style></defs><path d="M747.59340925 691.12859384c11.51396329 0.25305413 22.43746719-0.21087818 40.74171707-1.51832482 29.35428085-2.10878421 35.84933734-2.36183835 46.47761114-0.8856895 24.71495444 3.37405491 41.12129828 21.76265671 32.47528161 47.95376084-85.57447632 258.19957947-442.00123984 249.76444099-628.67084683 50.73735554-153.47733892-159.33976008-153.09775772-414.41833795 0.92786545-573.42069196 159.71934128-162.67163983 424.03439521-166.59397897 565.78689185 0.63263534 80.38686649 94.81095318 108.34934958 169.16669549 89.11723508 230.57450162-15.01454608 47.99593598-50.61082928 77.68762207-119.77896259 114.63352789-4.89237973 2.65706845-29.35428085 15.52065436-35.84933652 19.02123633-46.94154346 25.30541465-63.51659033 41.20565021-62.20914449 58.45550757 2.95229856 39.13904114 24.16667102 52.7196135 70.98168823 53.81618115z m44.41100207 50.10472101c-19.82257471 1.43397372-32.05352527 1.940082-45.63409763 1.6448519-70.34905207-1.60267593-115.98314969-30.91478165-121.38163769-101.64341492-3.45840683-46.05585397 24.7571304-73.13264758 89.24376132-107.96976837 6.7902866-3.66928501 31.37871396-16.57504688 36.06021551-19.06341229 57.69634516-30.83042972 85.15271997-53.73183005 94.76877722-84.47790866 12.77923398-40.78389304-9.10994898-98.94417051-79.24812286-181.6507002-121.17075953-142.97559219-350.14258521-139.60153647-489.2380134 2.06660824-134.49827774 138.84237405-134.79350784 362.12048163-0.42175717 501.637667 158.53842169 168.99799328 451.9968783 181.18676788 534.57688175-11.80919339-4.68150156 0.2952301-10.71262573 0.67481131-18.72600705 1.26527069z" p-id="2509"></path><path d="M346.03865637 637.18588562a78.82636652 78.82636652 0 0 0 78.32025825-79.29029883c0-43.69401562-35.005823-79.29029883-78.32025825-79.29029882a78.82636652 78.82636652 0 0 0-78.36243338 79.29029882c0 43.69401562 35.005823 79.29029883 78.36243338 79.29029883z m0-51.7495729a27.07679361 27.07679361 0 0 1-26.5706845-27.54072593c0-15.30977536 11.97789643-27.54072593 26.5706845-27.54072592 14.55061295 0 26.57068533 12.23095057 26.57068533 27.54072592a27.07679361 27.07679361 0 0 1-26.57068533 27.54072593zM475.7289063 807.11174353a78.82636652 78.82636652 0 0 0 78.3624334-79.29029882c0-43.69401562-34.96364785-79.29029883-78.32025825-79.29029883a78.82636652 78.82636652 0 0 0-78.32025742 79.29029883c0 43.69401562 34.96364785 79.29029883 78.32025742 79.29029882z m0-51.74957208a27.07679361 27.07679361 0 0 1-26.57068532-27.54072674c0-15.30977536 12.06224753-27.54072593 26.57068532-27.54072593 14.59278892 0 26.57068533 12.23095057 26.57068453 27.54072593a27.07679361 27.07679361 0 0 1-26.57068453 27.54072674zM601.24376214 377.21492718a78.82636652 78.82636652 0 0 0 78.32025742-79.29029883c0-43.69401562-34.96364785-79.29029883-78.32025742-79.29029882a78.82636652 78.82636652 0 0 0-78.32025823 79.29029883c0 43.69401562 34.96364785 79.29029883 78.32025824 79.29029883z m1e-8-51.74957208a27.07679361 27.07679361 0 0 1-26.57068534-27.54072675c0-15.30977536 11.97789643-27.54072593 26.57068534-27.54072591 14.55061295 0 26.57068533 12.23095057 26.57068451 27.54072592a27.07679361 27.07679361 0 0 1-26.57068451 27.54072674zM378.80916809 433.85687983a78.82636652 78.82636652 0 0 0 78.32025824-79.29029883c0-43.69401562-34.96364785-79.29029883-78.32025824-79.29029802a78.82636652 78.82636652 0 0 0-78.32025742 79.29029802c0 43.69401562 34.96364785 79.29029883 78.32025742 79.29029883z m0-51.74957209a27.07679361 27.07679361 0 0 1-26.57068451-27.54072674c0-15.30977536 11.97789643-27.54072593 26.57068451-27.54072593 14.55061295 0 26.57068533 12.23095057 26.57068533 27.54072593a27.07679361 27.07679361 0 0 1-26.57068533 27.54072674z" p-id="2510"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/company.svg b/src/assets/icons/svg/company.svg
new file mode 100644
index 0000000..fcf1394
--- /dev/null
+++ b/src/assets/icons/svg/company.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1675914273096" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2417" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M1001.7 969.6H890.4V399.4c0-27.7-17.4-52.8-43.3-62.5L580 236.7c-14.9-5.6-31.1-5.5-45.7-0.4V76.6c0-21.9-10.7-42.4-28.7-54.9s-41-15.3-61.5-7.6L176.9 114.3c-25.9 9.7-43.3 34.9-43.3 62.5v792.8H22.3C10 969.6 0 979.6 0 991.9s10 22.3 22.3 22.3H1001.8c12.3 0 22.3-10 22.3-22.3s-10.1-22.3-22.4-22.3zM178.1 176.8c0-9.2 5.8-17.6 14.4-20.8L459.7 55.8c7-2.6 14.4-1.7 20.5 2.5s9.6 10.9 9.6 18.3v893H178.1V176.8z m356.2 792.8V299.3c0-7.4 3.5-14.1 9.6-18.3 6.1-4.2 13.6-5.2 20.5-2.5l267.1 100.2c8.6 3.2 14.4 11.6 14.4 20.8v570.2H534.3z" p-id="2418"></path><path d="M391.8 346.3H258.2c-12.3 0-22.3 10-22.3 22.3s10 22.3 22.3 22.3h133.6c12.3 0 22.3-10 22.3-22.3s-10-22.3-22.3-22.3zM748 479.9H614.4c-12.3 0-22.3 10-22.3 22.3s10 22.3 22.3 22.3H748c12.3 0 22.3-10 22.3-22.3s-10-22.3-22.3-22.3zM748 613.4H614.4c-12.3 0-22.3 10-22.3 22.3s10 22.3 22.3 22.3H748c12.3 0 22.3-10 22.3-22.3s-10-22.3-22.3-22.3zM391.8 613.4H258.2c-12.3 0-22.3 10-22.3 22.3s10 22.3 22.3 22.3h133.6c12.3 0 22.3-10 22.3-22.3s-10-22.3-22.3-22.3z" p-id="2419"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/component.svg b/src/assets/icons/svg/component.svg
new file mode 100644
index 0000000..29c3458
--- /dev/null
+++ b/src/assets/icons/svg/component.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1575804206892" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3145" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M826.56 470.016c-32.896 0-64.384 12.288-89.984 35.52l0-104.96c0-62.208-50.496-112.832-112.64-113.088L623.936 287.04 519.552 287.104C541.824 262.72 554.56 230.72 554.56 197.12c0-73.536-59.904-133.44-133.504-133.44-73.472 0-133.376 59.904-133.376 133.44 0 32.896 12.224 64.256 35.52 89.984L175.232 287.104l0 0.576C113.728 288.704 64 338.88 64 400.576l0.32 0 0.32 116.48C60.864 544.896 70.592 577.728 100.8 588.48c12.736 4.608 37.632 7.488 60.864-25.28 12.992-18.368 34.24-29.248 56.64-29.248 38.336 0 69.504 31.104 69.504 69.312 0 38.4-31.168 69.504-69.504 69.504-22.656 0-44.032-11.264-57.344-30.4C138.688 610.112 112.576 615.36 102.464 619.136c-29.824 10.752-39.104 43.776-38.144 67.392l0 160.384L64 846.912C64 909.248 114.752 960 177.216 960l446.272 0c62.4 0 113.152-50.752 113.152-113.152l0-145.024c24.384 22.272 56.384 35.008 89.984 35.008 73.536 0 133.44-59.904 133.44-133.504C960 529.92 900.096 470.016 826.56 470.016zM826.56 672.896c-22.72 0-44.032-11.264-57.344-30.4-22.272-32.384-48.448-27.136-58.56-23.36-29.824 10.752-39.04 43.776-38.08 67.392l0 160.384c0 27.136-22.016 49.152-49.152 49.152L177.216 896.064C150.08 896 128 873.984 128 846.848l0.32 0 0-145.024c24.384 22.272 56.384 35.008 89.984 35.008 73.6 0 133.504-59.904 133.504-133.504 0-73.472-59.904-133.376-133.504-133.376-32.896 0-64.32 12.288-89.984 35.52l0-104.96L128 400.512c0-27.072 22.08-49.152 49.216-49.152L177.216 351.04 334.656 350.72c3.776 0.512 7.616 0.832 11.52 0.832 24.896 0 50.752-10.816 60.032-37.056 4.544-12.736 7.424-37.568-25.344-60.736C362.624 240.768 351.68 219.52 351.68 197.12c0-38.272 31.104-69.44 69.376-69.44 38.336 0 69.504 31.168 69.504 69.44 0 22.72-11.264 44.032-30.528 57.472C427.968 276.736 433.088 302.784 436.8 313.024c10.752 29.888 43.072 39.232 67.392 38.08l119.232 0 0 0.384c27.136 0 49.152 22.08 49.152 49.152l0.256 116.48c-3.776 27.84 6.016 60.736 36.224 71.488 12.736 4.608 37.632 7.488 60.8-25.28 13.056-18.368 34.24-29.248 56.704-29.248C864.832 534.016 896 565.12 896 603.392 896 641.728 864.832 672.896 826.56 672.896z" p-id="3146"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/dashboard.svg b/src/assets/icons/svg/dashboard.svg
new file mode 100644
index 0000000..5317d37
--- /dev/null
+++ b/src/assets/icons/svg/dashboard.svg
@@ -0,0 +1 @@
+<svg width="128" height="100" xmlns="http://www.w3.org/2000/svg"><path d="M27.429 63.638c0-2.508-.893-4.65-2.679-6.424-1.786-1.775-3.94-2.662-6.464-2.662-2.524 0-4.679.887-6.465 2.662-1.785 1.774-2.678 3.916-2.678 6.424 0 2.508.893 4.65 2.678 6.424 1.786 1.775 3.94 2.662 6.465 2.662 2.524 0 4.678-.887 6.464-2.662 1.786-1.775 2.679-3.916 2.679-6.424zm13.714-31.801c0-2.508-.893-4.65-2.679-6.424-1.785-1.775-3.94-2.662-6.464-2.662-2.524 0-4.679.887-6.464 2.662-1.786 1.774-2.679 3.916-2.679 6.424 0 2.508.893 4.65 2.679 6.424 1.785 1.774 3.94 2.662 6.464 2.662 2.524 0 4.679-.888 6.464-2.662 1.786-1.775 2.679-3.916 2.679-6.424zM71.714 65.98l7.215-27.116c.285-1.23.107-2.378-.536-3.443-.643-1.064-1.56-1.762-2.75-2.094-1.19-.33-2.333-.177-3.429.462-1.095.639-1.81 1.573-2.143 2.804l-7.214 27.116c-2.857.237-5.405 1.266-7.643 3.088-2.238 1.822-3.738 4.152-4.5 6.992-.952 3.644-.476 7.098 1.429 10.364 1.905 3.265 4.69 5.37 8.357 6.317 3.667.947 7.143.474 10.429-1.42 3.285-1.892 5.404-4.66 6.357-8.305.762-2.84.619-5.607-.429-8.305-1.047-2.697-2.762-4.85-5.143-6.46zm47.143-2.342c0-2.508-.893-4.65-2.678-6.424-1.786-1.775-3.94-2.662-6.465-2.662-2.524 0-4.678.887-6.464 2.662-1.786 1.774-2.679 3.916-2.679 6.424 0 2.508.893 4.65 2.679 6.424 1.786 1.775 3.94 2.662 6.464 2.662 2.524 0 4.679-.887 6.465-2.662 1.785-1.775 2.678-3.916 2.678-6.424zm-45.714-45.43c0-2.509-.893-4.65-2.679-6.425C68.68 10.01 66.524 9.122 64 9.122c-2.524 0-4.679.887-6.464 2.661-1.786 1.775-2.679 3.916-2.679 6.425 0 2.508.893 4.65 2.679 6.424 1.785 1.774 3.94 2.662 6.464 2.662 2.524 0 4.679-.888 6.464-2.662 1.786-1.775 2.679-3.916 2.679-6.424zm32 13.629c0-2.508-.893-4.65-2.679-6.424-1.785-1.775-3.94-2.662-6.464-2.662-2.524 0-4.679.887-6.464 2.662-1.786 1.774-2.679 3.916-2.679 6.424 0 2.508.893 4.65 2.679 6.424 1.785 1.774 3.94 2.662 6.464 2.662 2.524 0 4.679-.888 6.464-2.662 1.786-1.775 2.679-3.916 2.679-6.424zM128 63.638c0 12.351-3.357 23.78-10.071 34.286-.905 1.372-2.19 2.058-3.858 2.058H13.93c-1.667 0-2.953-.686-3.858-2.058C3.357 87.465 0 76.037 0 63.638c0-8.613 1.69-16.847 5.071-24.703C8.452 31.08 13 24.312 18.714 18.634c5.715-5.68 12.524-10.199 20.429-13.559C47.048 1.715 55.333.035 64 .035c8.667 0 16.952 1.68 24.857 5.04 7.905 3.36 14.714 7.88 20.429 13.559 5.714 5.678 10.262 12.446 13.643 20.301 3.38 7.856 5.071 16.09 5.071 24.703z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/date-range.svg b/src/assets/icons/svg/date-range.svg
new file mode 100644
index 0000000..fda571e
--- /dev/null
+++ b/src/assets/icons/svg/date-range.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1579774833889" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1376" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M887.466667 192.853333h-100.693334V119.466667c0-10.24-6.826667-17.066667-17.066666-17.066667s-17.066667 6.826667-17.066667 17.066667v73.386666H303.786667V119.466667c0-10.24-6.826667-17.066667-17.066667-17.066667s-17.066667 6.826667-17.066667 17.066667v73.386666H168.96c-46.08 0-85.333333 37.546667-85.333333 85.333334V836.266667c0 46.08 37.546667 85.333333 85.333333 85.333333H887.466667c46.08 0 85.333333-37.546667 85.333333-85.333333V278.186667c0-47.786667-37.546667-85.333333-85.333333-85.333334z m-718.506667 34.133334h100.693333v66.56c0 10.24 6.826667 17.066667 17.066667 17.066666s17.066667-6.826667 17.066667-17.066666v-66.56h450.56v66.56c0 10.24 6.826667 17.066667 17.066666 17.066666s17.066667-6.826667 17.066667-17.066666v-66.56H887.466667c27.306667 0 51.2 22.186667 51.2 51.2v88.746666H117.76v-88.746666c0-29.013333 22.186667-51.2 51.2-51.2zM887.466667 887.466667H168.96c-27.306667 0-51.2-22.186667-51.2-51.2V401.066667H938.666667V836.266667c0 27.306667-22.186667 51.2-51.2 51.2z" p-id="1377"></path><path d="M858.453333 493.226667H327.68c-10.24 0-17.066667 6.826667-17.066667 17.066666v114.346667h-116.053333c-10.24 0-17.066667 6.826667-17.066667 17.066667v133.12c0 10.24 6.826667 17.066667 17.066667 17.066666H460.8c10.24 0 17.066667-6.826667 17.066667-17.066666v-114.346667h380.586666c10.24 0 17.066667-6.826667 17.066667-17.066667v-133.12c0-10.24-6.826667-17.066667-17.066667-17.066666z m-413.013333 34.133333v97.28h-98.986667v-97.28h98.986667z m-230.4 131.413333h98.986667v98.986667h-98.986667v-98.986667z m131.413333 97.28v-97.28h98.986667v97.28h-98.986667z m133.12-228.693333h97.28v98.986667h-97.28v-98.986667z m131.413334 0h98.986666v98.986667h-98.986666v-98.986667z m230.4 97.28h-98.986667v-98.986667h98.986667v98.986667z" p-id="1378"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/date.svg b/src/assets/icons/svg/date.svg
new file mode 100644
index 0000000..52dc73e
--- /dev/null
+++ b/src/assets/icons/svg/date.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1577186573535" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1068" xmlns:xlink="http://www.w3.org/1999/xlink" width="81" height="81"><defs><style type="text/css"></style></defs><path d="M479.85714249 608.42857168h64.28571502c19.28571417 0 32.14285751-12.85714249 32.14285664-32.14285751s-12.85714249-32.14285751-32.14285664-32.14285664h-64.28571504c-19.28571417 0-32.14285751 12.85714249-32.14285664 32.14285662s12.85714249 32.14285751 32.14285664 32.14285753z m-2e-8 122.14285665h64.28571504c19.28571417 0 32.14285751-12.85714249 32.14285664-32.14285665s-12.85714249-32.14285751-32.14285664-32.14285751h-64.28571504c-19.28571417 0-32.14285751 12.85714249-32.14285664 32.14285751s12.85714249 32.14285751 32.14285664 32.14285664z m353.57142921-559.28571416h-128.57142921v-32.14285664c0-19.28571417-12.85714249-32.14285751-32.14285664-32.14285753s-32.14285751 12.85714249-32.14285751 32.14285753v32.14285664h-257.14285665v-32.14285664c0-19.28571417-12.85714249-32.14285751-32.14285752-32.14285753s-32.14285751 12.85714249-32.14285664 32.14285753v32.14285664h-128.57142919c-70.71428585 0-128.57142832 57.85714249-128.57142832 122.14285751v501.42857081c0 70.71428585 57.85714249 128.57142832 128.57142832 122.14285751h642.85714335c70.71428585 0 128.57142832-57.85714249 128.57142833-122.14285751v-501.42857081c0-70.71428585-57.85714249-122.14285753-128.57142833-122.14285751z m64.28571415 623.57142832c0 32.14285751-32.14285751 64.28571415-64.28571416 64.28571504h-642.85714335c-32.14285751 0-64.28571415-25.71428583-64.28571417-64.28571504v-372.85714249h771.42857168v372.85714249z m0-437.14285664h-771.42857168v-64.28571417c0-32.14285751 32.14285751-64.28571415 64.28571417-64.28571415h128.57142919v32.14285664c0 19.28571417 12.85714249 32.14285751 32.14285664 32.14285751s32.14285751-12.85714249 32.14285753-32.14285751v-32.14285664h257.14285665v32.14285664c0 19.28571417 12.85714249 32.14285751 32.1428575 32.14285751s32.14285751-12.85714249 32.14285664-32.14285751v-32.14285664h128.57142921c32.14285751 0 64.28571415 25.71428583 64.28571415 64.28571415v64.28571417z m-610.71428583 372.85714247h64.28571415c19.28571417 0 32.14285751-12.85714249 32.14285753-32.14285664s-12.85714249-32.14285751-32.14285753-32.14285751h-64.28571415c-19.28571417 0-32.14285751 12.85714249-32.14285751 32.14285751s12.85714249 32.14285751 32.14285751 32.14285665z m385.71428583-122.14285664h64.28571417c19.28571417 0 32.14285751-12.85714249 32.14285751-32.14285751s-12.85714249-32.14285751-32.14285751-32.14285664h-64.28571415c-19.28571417 0-32.14285751 12.85714249-32.14285753 32.14285664s12.85714249 32.14285751 32.14285753 32.14285751z m-385.71428583 0h64.28571415c19.28571417 0 32.14285751-12.85714249 32.14285753-32.14285751s-12.85714249-32.14285751-32.14285753-32.14285664h-64.28571415c-19.28571417 0-32.14285751 12.85714249-32.14285751 32.14285664s12.85714249 32.14285751 32.14285751 32.14285751z m385.71428583 122.14285665h64.28571417c19.28571417 0 32.14285751-12.85714249 32.14285751-32.14285665s-12.85714249-32.14285751-32.14285751-32.14285751h-64.28571415c-19.28571417 0-32.14285751 12.85714249-32.14285753 32.14285751s12.85714249 32.14285751 32.14285753 32.14285665z" p-id="1069"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/dict.svg b/src/assets/icons/svg/dict.svg
new file mode 100644
index 0000000..4849377
--- /dev/null
+++ b/src/assets/icons/svg/dict.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1566035680909" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3601" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M1002.0848 744.672l-33.568 10.368c0.96 7.264 2.144 14.304 2.144 21.76 0 7.328-1.184 14.432-2.368 21.568l33.792 10.56c7.936 2.24 14.496 7.616 18.336 14.752 3.84 7.328 4.672 15.808 1.952 23.552-5.376 16-23.168 24.672-39.936 19.68l-34.176-10.624c-7.136 12.8-15.776 24.672-26.208 35.2l20.8 27.488a28.96 28.96 0 0 1 5.824 22.816 29.696 29.696 0 0 1-12.704 19.616 32.544 32.544 0 0 1-44.416-6.752l-20.8-27.552c-13.696 6.56-28.192 11.2-43.008 13.888v33.632c0 16.736-14.112 30.432-31.648 30.432-17.6 0-31.872-13.696-31.872-30.432v-33.632a167.616 167.616 0 0 1-42.88-13.888l-20.928 27.552c-10.72 13.76-30.08 16.64-44.288 6.752a29.632 29.632 0 0 1-12.704-19.616 29.28 29.28 0 0 1 5.696-22.816l20.896-27.808a166.72 166.72 0 0 1-27.008-34.688l-33.376 10.432c-16.8 5.184-34.56-3.552-39.936-19.616a29.824 29.824 0 0 1 20.224-38.24l33.472-10.432c-0.8-7.264-2.016-14.304-2.016-21.824 0-7.36 1.184-14.496 2.304-21.632l-33.792-10.368c-16.672-5.376-25.632-22.496-20.224-38.432 5.376-16 23.136-24.672 39.936-19.68l34.016 10.752c7.328-12.672 15.84-24.8 26.336-35.328l-20.8-27.552a29.44 29.44 0 0 1 6.944-42.432 32.704 32.704 0 0 1 44.384 6.752l20.832 27.616c13.696-6.432 28.224-11.2 43.104-13.952v-33.568c0-16.736 14.048-30.432 31.648-30.432 17.536 0 31.808 13.568 31.808 30.432v33.504c15.072 2.688 29.344 7.808 42.848 14.016l20.992-27.616a32.48 32.48 0 0 1 44.224-6.752 29.568 29.568 0 0 1 7.136 42.432l-21.024 27.808c10.432 10.432 19.872 21.888 27.04 34.752l33.376-10.432c16.768-5.12 34.56 3.68 39.936 19.68 5.536 15.936-3.712 33.056-20.32 38.304z m-206.016-74.432c-61.344 0-111.136 47.808-111.136 106.56 0 58.88 49.792 106.496 111.136 106.496 61.312 0 111.104-47.616 111.104-106.496 0-58.752-49.792-106.56-111.104-106.56z" p-id="3602"></path><path d="M802.7888 57.152h-76.448c0-22.08-21.024-38.24-42.848-38.24H39.3968a39.68 39.68 0 0 0-39.36 40.032v795.616s41.888 120.192 110.752 120.192H673.2848a227.488 227.488 0 0 1-107.04-97.44H117.6368s-40.608-13.696-40.608-41.248l470.304-0.256 1.664 3.36a227.68 227.68 0 0 1-12.64-73.632c0-60.576 24-118.624 66.88-161.44a228.352 228.352 0 0 1 123.552-63.392l-3.2 0.288 2.144-424.672h38.208l0.576 421.024c27.04 0 52.672 4.8 76.64 13.344V101.536c0.032 0-6.304-44.384-38.368-44.384zM149.7648 514.336H72.3888v-77.408H149.7648v77.408z m0-144.32H72.3888v-77.44H149.7648v77.44z m0-137.248H72.3888v-77.44H149.7648v77.44z m501.856 281.568H206.0848v-77.408h445.536v77.408z m0-144.32H206.0848v-77.44h445.536v77.44z m0-137.248H206.0848v-77.44h445.536v77.44z" p-id="3603"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/documentation.svg b/src/assets/icons/svg/documentation.svg
new file mode 100644
index 0000000..7043122
--- /dev/null
+++ b/src/assets/icons/svg/documentation.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M71.984 44.815H115.9L71.984 9.642v35.173zM16.094.05h63.875l47.906 38.37v76.74c0 3.392-1.682 6.645-4.677 9.044-2.995 2.399-7.056 3.746-11.292 3.746H16.094c-4.236 0-8.297-1.347-11.292-3.746-2.995-2.399-4.677-5.652-4.677-9.044V12.84C.125 5.742 7.23.05 16.094.05zm71.86 102.32V89.58h-71.86v12.79h71.86zm23.952-25.58V64H16.094v12.79h95.812z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/download.svg b/src/assets/icons/svg/download.svg
new file mode 100644
index 0000000..c896951
--- /dev/null
+++ b/src/assets/icons/svg/download.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1569915748289" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3062" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M768.35456 416a256 256 0 1 0-512 0 192 192 0 1 0 0 384v64a256 256 0 0 1-58.88-505.216 320.128 320.128 0 0 1 629.76 0A256.128 256.128 0 0 1 768.35456 864v-64a192 192 0 0 0 0-384z m-512 384h64v64H256.35456v-64z m448 0h64v64h-64v-64z" fill="#333333" p-id="3063"></path><path d="M539.04256 845.248V512.192a32.448 32.448 0 0 0-32-32.192c-17.664 0-32 14.912-32 32.192v333.056l-36.096-36.096a32.192 32.192 0 0 0-45.056 0.192 31.616 31.616 0 0 0-0.192 45.056l90.88 90.944a31.36 31.36 0 0 0 22.528 9.088 30.08 30.08 0 0 0 22.4-9.088l90.88-90.88a32.192 32.192 0 0 0-0.192-45.12 31.616 31.616 0 0 0-45.056-0.192l-36.096 36.096z" fill="#333333" p-id="3064"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/drag.svg b/src/assets/icons/svg/drag.svg
new file mode 100644
index 0000000..4185d3c
--- /dev/null
+++ b/src/assets/icons/svg/drag.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M73.137 29.08h-9.209 29.7L63.886.093 34.373 29.08h20.49v27.035H27.238v17.948h27.625v27.133h18.274V74.063h27.41V56.115h-27.41V29.08zm-9.245 98.827l27.518-26.711H36.59l27.302 26.71zM.042 64.982l27.196 27.029V38.167L.042 64.982zm100.505-26.815V92.01l27.41-27.029-27.41-26.815z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/druid.svg b/src/assets/icons/svg/druid.svg
new file mode 100644
index 0000000..a2b4b4e
--- /dev/null
+++ b/src/assets/icons/svg/druid.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1566036347051" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5853" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M832 128H192a64.19 64.19 0 0 0-64 64v640a64.19 64.19 0 0 0 64 64h640a64.19 64.19 0 0 0 64-64V192a64.19 64.19 0 0 0-64-64z m0 703.89l-0.11 0.11H192.11l-0.11-0.11V768h640zM832 544H720L605.6 696.54 442.18 435.07 333.25 544H192v-64h114.75l147.07-147.07L610.4 583.46 688 480h144z m0-288H192v-63.89l0.11-0.11h639.78l0.11 0.11z" p-id="5854"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/edit.svg b/src/assets/icons/svg/edit.svg
new file mode 100644
index 0000000..d26101f
--- /dev/null
+++ b/src/assets/icons/svg/edit.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M106.133 67.2a4.797 4.797 0 0 0-4.8 4.8c0 .187.014.36.027.533h-.027V118.4H9.6V26.667h50.133c2.654 0 4.8-2.147 4.8-4.8 0-2.654-2.146-4.8-4.8-4.8H9.6a9.594 9.594 0 0 0-9.6 9.6V118.4c0 5.307 4.293 9.6 9.6 9.6h91.733c5.307 0 9.6-4.293 9.6-9.6V72.533h-.026c.013-.173.026-.346.026-.533 0-2.653-2.146-4.8-4.8-4.8z"/><path d="M125.16 13.373L114.587 2.8c-3.747-3.747-9.854-3.72-13.6.027l-52.96 52.96a4.264 4.264 0 0 0-.907 1.36L33.813 88.533c-.746 1.76-.226 3.534.907 4.68 1.133 1.147 2.92 1.667 4.693.92l31.4-13.293c.507-.213.96-.52 1.36-.907l52.96-52.96c3.747-3.746 3.774-9.853.027-13.6zM66.107 72.4l-18.32 7.76 7.76-18.32L92.72 24.667l10.56 10.56L66.107 72.4zm52.226-52.227l-8.266 8.267-10.56-10.56 8.266-8.267.027-.026 10.56 10.56-.027.026z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/education.svg b/src/assets/icons/svg/education.svg
new file mode 100644
index 0000000..7bfb01d
--- /dev/null
+++ b/src/assets/icons/svg/education.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M88.883 119.565c-7.284 0-19.434 2.495-21.333 8.25v.127c-4.232.13-5.222 0-7.108 0-1.895-5.76-14.045-8.256-21.333-8.256H0V0h42.523c9.179 0 17.109 5.47 21.47 13.551C68.352 5.475 76.295 0 85.478 0H128v119.57l-39.113-.005h-.004zM60.442 24.763c0-9.651-8.978-16.507-17.777-16.507H7.108V111.43H39.11c7.054-.14 18.177.082 21.333 6.12v-4.628c-.134-5.722-.004-13.522 0-13.832V27.413l.004-2.655-.004.005zm60.442-16.517h-35.55c-8.802 0-17.78 6.856-17.78 16.493v74.259c.004.32.138 8.115 0 13.813v4.627c3.155-6.022 14.279-6.26 21.333-6.114h32V8.25l-.003-.005z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/email.svg b/src/assets/icons/svg/email.svg
new file mode 100644
index 0000000..74d25e2
--- /dev/null
+++ b/src/assets/icons/svg/email.svg
@@ -0,0 +1 @@
+<svg width="128" height="96" xmlns="http://www.w3.org/2000/svg"><path d="M64.125 56.975L120.188.912A12.476 12.476 0 0 0 115.5 0h-103c-1.588 0-3.113.3-4.513.838l56.138 56.137z"/><path d="M64.125 68.287l-62.3-62.3A12.42 12.42 0 0 0 0 12.5v71C0 90.4 5.6 96 12.5 96h103c6.9 0 12.5-5.6 12.5-12.5v-71a12.47 12.47 0 0 0-1.737-6.35L64.125 68.287z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/example.svg b/src/assets/icons/svg/example.svg
new file mode 100644
index 0000000..46f42b5
--- /dev/null
+++ b/src/assets/icons/svg/example.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M96.258 57.462h31.421C124.794 27.323 100.426 2.956 70.287.07v31.422a32.856 32.856 0 0 1 25.971 25.97zm-38.796-25.97V.07C27.323 2.956 2.956 27.323.07 57.462h31.422a32.856 32.856 0 0 1 25.97-25.97zm12.825 64.766v31.421c30.46-2.885 54.507-27.253 57.713-57.712H96.579c-2.886 13.466-13.146 23.726-26.292 26.291zM31.492 70.287H.07c2.886 30.46 27.253 54.507 57.713 57.713V96.579c-13.466-2.886-23.726-13.146-26.291-26.292z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/excel.svg b/src/assets/icons/svg/excel.svg
new file mode 100644
index 0000000..74d97b8
--- /dev/null
+++ b/src/assets/icons/svg/excel.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M78.208 16.576v8.384h38.72v5.376h-38.72v8.704h38.72v5.376h-38.72v8.576h38.72v5.376h-38.72v8.576h38.72v5.376h-38.72v8.576h38.72v5.376h-38.72v8.512h38.72v5.376h-38.72v11.136H128v-94.72H78.208zM0 114.368L72.128 128V0L0 13.632v100.736z"/><path d="M28.672 82.56h-11.2l14.784-23.488-14.08-22.592h11.52l8.192 14.976 8.448-14.976h11.136l-14.08 22.208L58.368 82.56H46.656l-8.768-15.68z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/exit-fullscreen.svg b/src/assets/icons/svg/exit-fullscreen.svg
new file mode 100644
index 0000000..485c128
--- /dev/null
+++ b/src/assets/icons/svg/exit-fullscreen.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M49.217 41.329l-.136-35.24c-.06-2.715-2.302-4.345-5.022-4.405h-3.65c-2.712-.06-4.866 2.303-4.806 5.016l.152 19.164-24.151-23.79a6.698 6.698 0 0 0-9.499 0 6.76 6.76 0 0 0 0 9.526l23.93 23.713-18.345.074c-2.712-.069-5.228 1.813-5.64 5.02v3.462c.069 2.721 2.31 4.97 5.022 5.03l35.028-.207c.052.005.087.025.133.025l2.457.054a4.626 4.626 0 0 0 3.436-1.38c.88-.874 1.205-2.096 1.169-3.462l-.262-2.465c0-.048.182-.081.182-.136h.002zm52.523 51.212l18.32-.073c2.713.06 5.224-1.609 5.64-4.815v-3.462c-.068-2.722-2.317-4.97-5.021-5.04l-34.58.21c-.053 0-.086-.021-.138-.021l-2.451-.06a4.64 4.64 0 0 0-3.445 1.381c-.885.868-1.201 2.094-1.174 3.46l.27 2.46c.005.06-.177.095-.177.141l.141 34.697c.069 2.713 2.31 4.338 5.022 4.397l3.45.006c2.705.062 4.867-2.31 4.8-5.026l-.153-18.752 24.151 23.946a6.69 6.69 0 0 0 9.494 0 6.747 6.747 0 0 0 0-9.523L101.74 92.54v.001zM48.125 80.662a4.636 4.636 0 0 0-3.437-1.382l-2.457.06c-.05 0-.082.022-.137.022l-35.025-.21c-2.712.07-4.957 2.318-5.022 5.04v3.462c.409 3.206 2.925 4.874 5.633 4.814l18.554.06-24.132 23.928c-2.62 2.626-2.62 6.89 0 9.524a6.694 6.694 0 0 0 9.496 0l24.155-23.79-.155 18.866c-.06 2.722 2.094 5.093 4.801 5.025h3.65c2.72-.069 4.962-1.685 5.022-4.406l.141-34.956c0-.05-.182-.082-.182-.136l.262-2.46c.03-1.366-.286-2.592-1.166-3.46h-.001zM80.08 47.397a4.62 4.62 0 0 0 3.443 1.374l2.45-.054c.055 0 .088-.02.143-.028l35.08.21c2.712-.062 4.953-2.312 5.021-5.033l.009-3.463c-.417-3.211-2.937-5.084-5.64-5.025l-18.615-.073 23.917-23.715c2.63-2.623 2.63-6.879.008-9.513a6.691 6.691 0 0 0-9.494 0L92.251 26.016l.155-19.312c.065-2.713-2.097-5.085-4.802-5.025h-3.45c-2.713.069-4.954 1.693-5.022 4.406l-.139 35.247c0 .054.18.088.18.136l-.267 2.465c-.028 1.366.288 2.588 1.174 3.463v.001z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/eye-open.svg b/src/assets/icons/svg/eye-open.svg
new file mode 100644
index 0000000..88dcc98
--- /dev/null
+++ b/src/assets/icons/svg/eye-open.svg
@@ -0,0 +1 @@
+<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="128" height="128"><defs><style/></defs><path d="M512 128q69.675 0 135.51 21.163t115.498 54.997 93.483 74.837 73.685 82.006 51.67 74.837 32.17 54.827L1024 512q-2.347 4.992-6.315 13.483T998.87 560.17t-31.658 51.669-44.331 59.99-56.832 64.34-69.504 60.16-82.347 51.5-94.848 34.687T512 896q-69.675 0-135.51-21.163t-115.498-54.826-93.483-74.326-73.685-81.493-51.67-74.496-32.17-54.997L0 513.707q2.347-4.992 6.315-13.483t18.816-34.816 31.658-51.84 44.331-60.33 56.832-64.683 69.504-60.331 82.347-51.84 94.848-34.816T512 128.085zm0 85.333q-46.677 0-91.648 12.331t-81.152 31.83-70.656 47.146-59.648 54.485-48.853 57.686-37.675 52.821-26.325 43.99q12.33 21.674 26.325 43.52t37.675 52.351 48.853 57.003 59.648 53.845T339.2 767.02t81.152 31.488T512 810.667t91.648-12.331 81.152-31.659 70.656-46.848 59.648-54.186 48.853-57.344 37.675-52.651T927.957 512q-12.33-21.675-26.325-43.648t-37.675-52.65-48.853-57.345-59.648-54.186-70.656-46.848-81.152-31.659T512 213.334zm0 128q70.656 0 120.661 50.006T682.667 512 632.66 632.661 512 682.667 391.339 632.66 341.333 512t50.006-120.661T512 341.333zm0 85.334q-35.328 0-60.33 25.002T426.666 512t25.002 60.33T512 597.334t60.33-25.002T597.334 512t-25.002-60.33T512 426.666z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/eye.svg b/src/assets/icons/svg/eye.svg
new file mode 100644
index 0000000..16ed2d8
--- /dev/null
+++ b/src/assets/icons/svg/eye.svg
@@ -0,0 +1 @@
+<svg width="128" height="64" xmlns="http://www.w3.org/2000/svg"><path d="M127.072 7.994c1.37-2.208.914-5.152-.914-6.87-2.056-1.717-4.797-1.226-6.396.982-.229.245-25.586 32.382-55.74 32.382-29.24 0-55.74-32.382-55.968-32.627-1.6-1.963-4.57-2.208-6.397-.49C-.17 3.086-.399 6.275 1.2 8.238c.457.736 5.94 7.36 14.62 14.72L4.17 35.96c-1.828 1.963-1.6 5.152.228 6.87.457.98 1.6 1.471 2.742 1.471s2.284-.49 3.198-1.472l12.564-13.983c5.94 4.416 13.021 8.587 20.788 11.53l-4.797 17.418c-.685 2.699.686 5.397 3.198 6.133h1.37c2.057 0 3.884-1.472 4.341-3.68L52.6 42.83c3.655.736 7.538 1.227 11.422 1.227 3.883 0 7.767-.49 11.422-1.227l4.797 17.173c.457 2.208 2.513 3.68 4.34 3.68.457 0 .914 0 1.143-.246 2.513-.736 3.883-3.434 3.198-6.133l-4.797-17.172c7.767-2.944 14.848-7.114 20.788-11.53l12.336 13.738c.913.981 2.056 1.472 3.198 1.472s2.284-.49 3.198-1.472c1.828-1.963 1.828-4.906.228-6.87l-11.65-13.001c9.366-7.36 14.849-14.474 14.849-14.474z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/form.svg b/src/assets/icons/svg/form.svg
new file mode 100644
index 0000000..dcbaa18
--- /dev/null
+++ b/src/assets/icons/svg/form.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M84.068 23.784c-1.02 0-1.877-.32-2.572-.96a8.588 8.588 0 0 1-1.738-2.237 11.524 11.524 0 0 1-1.042-2.621c-.232-.895-.348-1.641-.348-2.238V0h.278c.834 0 1.622.085 2.363.256.742.17 1.645.575 2.711 1.214 1.066.64 2.363 1.535 3.892 2.686 1.53 1.15 3.453 2.664 5.77 4.54 2.502 2.045 4.494 3.771 5.977 5.178 1.483 1.406 2.618 2.6 3.406 3.58.787.98 1.274 1.812 1.46 2.494.185.682.277 1.278.277 1.79v2.046H84.068zM127.3 84.01c.278.682.464 1.535.556 2.558.093 1.023-.37 2.003-1.39 2.94-.463.427-.88.832-1.25 1.215-.372.384-.696.704-.974.96a6.69 6.69 0 0 1-.973.767l-11.816-10.741a44.331 44.331 0 0 0 1.877-1.535 31.028 31.028 0 0 1 1.737-1.406c1.112-.938 2.317-1.343 3.615-1.215 1.297.128 2.363.405 3.197.83.927.427 1.923 1.173 2.989 2.239 1.065 1.065 1.876 2.195 2.432 3.388zM78.23 95.902c2.038 0 3.752-.511 5.143-1.534l-26.969 25.83H18.037c-1.761 0-3.684-.47-5.77-1.407a24.549 24.549 0 0 1-5.838-3.709 21.373 21.373 0 0 1-4.518-5.306c-1.204-2.003-1.807-4.07-1.807-6.202V16.495c0-1.79.44-3.665 1.32-5.626A18.41 18.41 0 0 1 5.04 5.562a21.798 21.798 0 0 1 5.213-3.964C12.198.533 14.237 0 16.37 0h53.24v15.984c0 1.62.278 3.367.834 5.242a16.704 16.704 0 0 0 2.572 5.179c1.159 1.577 2.665 2.898 4.518 3.964 1.853 1.066 4.078 1.598 6.673 1.598h20.295v42.325L85.458 92.45c1.02-1.364 1.529-2.856 1.529-4.476 0-2.216-.857-4.113-2.572-5.69-1.714-1.577-3.776-2.366-6.186-2.366H26.1c-2.409 0-4.448.789-6.116 2.366-1.668 1.577-2.502 3.474-2.502 5.69 0 2.217.834 4.092 2.502 5.626 1.668 1.535 3.707 2.302 6.117 2.302h52.13zM26.1 47.951c-2.41 0-4.449.789-6.117 2.366-1.668 1.577-2.502 3.473-2.502 5.69 0 2.216.834 4.092 2.502 5.626 1.668 1.534 3.707 2.302 6.117 2.302h52.13c2.409 0 4.47-.768 6.185-2.302 1.715-1.534 2.572-3.41 2.572-5.626 0-2.217-.857-4.113-2.572-5.69-1.714-1.577-3.776-2.366-6.186-2.366H26.1zm52.407 64.063l1.807-1.663 3.476-3.196a479.75 479.75 0 0 0 4.587-4.284 500.757 500.757 0 0 1 5.004-4.667c3.985-3.666 8.48-7.758 13.485-12.276l11.677 10.741-13.485 12.404-5.004 4.603-4.587 4.22a179.46 179.46 0 0 0-3.267 3.068c-.88.853-1.367 1.322-1.46 1.407-.463.341-.973.703-1.529 1.087-.556.383-1.112.703-1.668.959-.556.256-1.413.575-2.572.959a83.5 83.5 0 0 1-3.545 1.087 72.2 72.2 0 0 1-3.475.895c-1.112.256-1.946.426-2.502.511-1.112.17-1.854.043-2.224-.383-.371-.426-.464-1.151-.278-2.174.092-.511.278-1.279.556-2.302.278-1.023.602-2.067.973-3.132l1.042-3.005c.325-.938.58-1.577.765-1.918a10.157 10.157 0 0 1 2.224-2.941z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/fullscreen.svg b/src/assets/icons/svg/fullscreen.svg
new file mode 100644
index 0000000..0e86b6f
--- /dev/null
+++ b/src/assets/icons/svg/fullscreen.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M38.47 52L52 38.462l-23.648-23.67L43.209 0H.035L0 43.137l14.757-14.865L38.47 52zm74.773 47.726L89.526 76 76 89.536l23.648 23.672L84.795 128h43.174L128 84.863l-14.757 14.863zM89.538 52l23.668-23.648L128 43.207V.038L84.866 0 99.73 14.76 76 38.472 89.538 52zM38.46 76L14.792 99.651 0 84.794v43.173l43.137.033-14.865-14.757L52 89.53 38.46 76z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/gitee.svg b/src/assets/icons/svg/gitee.svg
new file mode 100644
index 0000000..6324608
--- /dev/null
+++ b/src/assets/icons/svg/gitee.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1686919908144" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2521" width="200" height="200" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M512 992C246.895625 992 32 777.104375 32 512S246.895625 32 512 32s480 214.895625 480 480-214.895625 480-480 480z m242.9521875-533.3278125h-272.56875a23.7121875 23.7121875 0 0 0-23.71125 23.7121875l-0.024375 59.255625c0 13.08 10.6078125 23.7121875 23.6878125 23.7121875h165.96c13.104375 0 23.7121875 10.6078125 23.7121875 23.6878125v11.855625a71.1121875 71.1121875 0 0 1-71.1121875 71.1121875h-225.215625a23.7121875 23.7121875 0 0 1-23.6878125-23.7121875V423.1278125a71.1121875 71.1121875 0 0 1 71.0878125-71.1121875h331.824375a23.7121875 23.7121875 0 0 0 23.6878125-23.71125l0.0721875-59.2565625a23.7121875 23.7121875 0 0 0-23.68875-23.7121875H423.08a177.76875 177.76875 0 0 0-177.76875 177.7921875V754.953125c0 13.1034375 10.60875 23.7121875 23.713125 23.7121875h349.63125a159.984375 159.984375 0 0 0 159.984375-159.984375V482.36a23.7121875 23.7121875 0 0 0-23.7121875-23.6878125z" fill="#515151" p-id="2522"></path></svg>
diff --git a/src/assets/icons/svg/github.svg b/src/assets/icons/svg/github.svg
new file mode 100644
index 0000000..db0a0d4
--- /dev/null
+++ b/src/assets/icons/svg/github.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1581238998885" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4187" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M511.542857 14.057143C228.914286 13.942857 0 242.742857 0 525.142857 0 748.457143 143.2 938.285714 342.628571 1008c26.857143 6.742857 22.742857-12.342857 22.742858-25.371429v-88.571428c-155.085714 18.171429-161.371429-84.457143-171.771429-101.6C172.571429 756.571429 122.857143 747.428571 137.714286 730.285714c35.314286-18.171429 71.314286 4.571429 113.028571 66.171429 30.171429 44.685714 89.028571 37.142857 118.857143 29.714286 6.514286-26.857143 20.457143-50.857143 39.657143-69.485715-160.685714-28.8-227.657143-126.857143-227.657143-243.428571 0-56.571429 18.628571-108.571429 55.2-150.514286-23.314286-69.142857 2.171429-128.342857 5.6-137.142857 66.4-5.942857 135.428571 47.542857 140.8 51.771429 37.714286-10.171429 80.8-15.542857 129.028571-15.542858 48.457143 0 91.657143 5.6 129.714286 15.885715 12.914286-9.828571 76.914286-55.771429 138.628572-50.171429 3.314286 8.8 28.228571 66.628571 6.285714 134.857143 37.028571 42.057143 55.885714 94.514286 55.885714 151.2 0 116.8-67.428571 214.971429-228.571428 243.314286a145.714286 145.714286 0 0 1 43.542857 104v128.571428c0.914286 10.285714 0 20.457143 17.142857 20.457143 202.4-68.228571 348.114286-259.428571 348.114286-484.685714 0-282.514286-229.028571-511.2-511.428572-511.2z" p-id="4188"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/guide.svg b/src/assets/icons/svg/guide.svg
new file mode 100644
index 0000000..b271001
--- /dev/null
+++ b/src/assets/icons/svg/guide.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M1.482 70.131l36.204 16.18 69.932-65.485-61.38 70.594 46.435 18.735c1.119.425 2.397-.17 2.797-1.363v-.085L127.998.047 1.322 65.874c-1.12.597-1.519 1.959-1.04 3.151.32.511.72.937 1.2 1.107zm44.676 57.821L64.22 107.26l-18.062-7.834v28.527z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/icon.svg b/src/assets/icons/svg/icon.svg
new file mode 100644
index 0000000..82be8ee
--- /dev/null
+++ b/src/assets/icons/svg/icon.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M115.147.062a13 13 0 0 1 4.94.945c1.55.63 2.907 1.526 4.069 2.688a13.148 13.148 0 0 1 2.761 4.069c.678 1.55 1.017 3.245 1.017 5.086v102.3c0 3.681-1.187 6.733-3.56 9.155-2.373 2.422-5.352 3.633-8.937 3.633H12.992c-3.875 0-7-1.26-9.373-3.779-2.373-2.518-3.56-5.667-3.56-9.445V12.704c0-3.39 1.163-6.345 3.488-8.863C5.872 1.32 8.972.062 12.847.062h102.3zM81.434 109.047c1.744 0 3.003-.412 3.778-1.235.775-.824 1.163-1.914 1.163-3.27 0-1.26-.388-2.325-1.163-3.197-.775-.872-2.034-1.307-3.778-1.307H72.57c.097-.194.145-.485.145-.872V27.09h9.01c1.743 0 2.954-.436 3.633-1.308.678-.872 1.017-1.938 1.017-3.197 0-1.26-.34-2.325-1.017-3.197-.679-.872-1.89-1.308-3.633-1.308H46.268c-1.743 0-2.954.436-3.632 1.308-.678.872-1.018 1.938-1.018 3.197 0 1.26.34 2.325 1.018 3.197.678.872 1.889 1.308 3.632 1.308h8.138v72.075c0 .193.024.339.073.436.048.096.072.242.072.436H46.56c-1.744 0-3.003.435-3.778 1.307-.775.872-1.163 1.938-1.163 3.197 0 1.356.388 2.446 1.163 3.27.775.823 2.034 1.235 3.778 1.235h34.875z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/input.svg b/src/assets/icons/svg/input.svg
new file mode 100644
index 0000000..ab91381
--- /dev/null
+++ b/src/assets/icons/svg/input.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1575802859706" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3102" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M896 224H128c-35.2 0-64 28.8-64 64v448c0 35.2 28.8 64 64 64h768c35.2 0 64-28.8 64-64V288c0-35.2-28.8-64-64-64z m0 480c0 19.2-12.8 32-32 32H160c-19.2 0-32-12.8-32-32V320c0-19.2 12.8-32 32-32h704c19.2 0 32 12.8 32 32v384z" p-id="3103"></path><path d="M224 352c-19.2 0-32 12.8-32 32v256c0 16 12.8 32 32 32s32-12.8 32-32V384c0-16-12.8-32-32-32z" p-id="3104"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/international.svg b/src/assets/icons/svg/international.svg
new file mode 100644
index 0000000..e9b56ee
--- /dev/null
+++ b/src/assets/icons/svg/international.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M83.287 103.01c-1.57-3.84-6.778-10.414-15.447-19.548-2.327-2.444-2.182-4.306-1.338-9.862v-.64c.553-3.81 1.513-6.05 14.313-8.087 6.516-1.018 8.203 1.57 10.589 5.178l.785 1.193a12.625 12.625 0 0 0 6.43 5.207c1.134.524 2.53 1.164 4.421 2.24 4.596 2.53 4.596 5.41 4.596 11.753v.727a26.91 26.91 0 0 1-5.178 17.454 59.055 59.055 0 0 1-19.025 11.026c3.49-6.546.814-14.313 0-16.553l-.146-.087zM64 5.12a58.502 58.502 0 0 1 25.484 5.818 54.313 54.313 0 0 0-12.859 10.327c-.93 1.28-1.716 2.473-2.472 3.579-2.444 3.694-3.637 5.352-5.818 5.614a25.105 25.105 0 0 1-4.219 0c-4.276-.29-10.094-.64-11.956 4.422-1.193 3.23-1.396 11.956 2.444 16.495.66 1.077.778 2.4.32 3.578a7.01 7.01 0 0 1-2.066 3.229 18.938 18.938 0 0 1-2.909-2.91 18.91 18.91 0 0 0-8.32-6.603c-1.25-.349-2.647-.64-3.985-.93-3.782-.786-8.03-1.688-9.019-3.812a14.895 14.895 0 0 1-.727-5.818 21.935 21.935 0 0 0-1.396-9.25 8.873 8.873 0 0 0-5.557-4.946A58.705 58.705 0 0 1 64 5.12zM0 64c0 35.346 28.654 64 64 64 35.346 0 64-28.654 64-64 0-35.346-28.654-64-64-64C28.654 0 0 28.654 0 64z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/job.svg b/src/assets/icons/svg/job.svg
new file mode 100644
index 0000000..2a93a25
--- /dev/null
+++ b/src/assets/icons/svg/job.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1566036191400" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5472" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M934.912 1016.832H192c-14.336 0-25.6-11.264-25.6-25.6v-189.44c0-14.336 11.264-25.6 25.6-25.6s25.6 11.264 25.6 25.6v163.84h691.712V64H217.6v148.48c0 14.336-11.264 25.6-25.6 25.6s-25.6-11.264-25.6-25.6v-174.08c0-14.336 11.264-25.6 25.6-25.6h742.912c14.336 0 25.6 11.264 25.6 25.6v952.832c0 14.336-11.264 25.6-25.6 25.6z" p-id="5473"></path><path d="M232.96 371.2h-117.76c-14.336 0-25.6-11.264-25.6-25.6s11.264-25.6 25.6-25.6h117.76c14.336 0 25.6 11.264 25.6 25.6s-11.264 25.6-25.6 25.6zM232.96 540.16h-117.76c-14.336 0-25.6-11.264-25.6-25.6s11.264-25.6 25.6-25.6h117.76c14.336 0 25.6 11.264 25.6 25.6s-11.264 25.6-25.6 25.6zM232.96 698.88h-117.76c-14.336 0-25.6-11.264-25.6-25.6s11.264-25.6 25.6-25.6h117.76c14.336 0 25.6 11.264 25.6 25.6s-11.264 25.6-25.6 25.6zM574.464 762.88c-134.144 0-243.2-109.056-243.2-243.2S440.32 276.48 574.464 276.48s243.2 109.056 243.2 243.2-109.056 243.2-243.2 243.2z m0-435.2c-105.984 0-192 86.016-192 192S468.48 711.68 574.464 711.68s192-86.016 192-192S680.448 327.68 574.464 327.68z" p-id="5474"></path><path d="M663.04 545.28h-87.04c-14.336 0-25.6-11.264-25.6-25.6s11.264-25.6 25.6-25.6h87.04c14.336 0 25.6 11.264 25.6 25.6s-11.264 25.6-25.6 25.6z" p-id="5475"></path><path d="M576 545.28c-14.336 0-25.6-11.264-25.6-25.6v-87.04c0-14.336 11.264-25.6 25.6-25.6s25.6 11.264 25.6 25.6v87.04c0 14.336-11.264 25.6-25.6 25.6z" p-id="5476"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/language.svg b/src/assets/icons/svg/language.svg
new file mode 100644
index 0000000..0082b57
--- /dev/null
+++ b/src/assets/icons/svg/language.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M84.742 36.8c2.398 7.2 5.595 12.8 11.19 18.4 4.795-4.8 7.992-11.2 10.39-18.4h-21.58zm-52.748 40h20.78l-10.39-28-10.39 28z"/><path d="M111.916 0H16.009C7.218 0 .025 7.2.025 16v96c0 8.8 7.193 16 15.984 16h95.907c8.791 0 15.984-7.2 15.984-16V16c0-8.8-6.394-16-15.984-16zM72.754 103.2c-1.598 1.6-3.197 1.6-4.795 1.6-.8 0-2.398 0-3.197-.8-.8-.8-1.599 0-1.599-.8s-.799-1.6-1.598-3.2c-.8-1.6-.8-2.4-1.599-4l-3.196-8.8H28.797L25.6 96c-1.598 3.2-2.398 5.6-3.197 7.2-.8 1.6-2.398 1.6-4.795 1.6-1.599 0-3.197-.8-4.796-1.6-1.598-1.6-2.397-2.4-2.397-4 0-.8 0-1.6.799-3.2.8-1.6.8-2.4 1.598-4l17.583-44.8c.8-1.6.8-3.2 1.599-4.8.799-1.6 1.598-3.2 2.397-4 .8-.8 1.599-2.4 3.197-3.2 1.599-.8 3.197-.8 4.796-.8 1.598 0 3.196 0 4.795.8 1.598.8 2.398 1.6 3.197 3.2.799.8 1.598 2.4 2.397 4 .8 1.6 1.599 3.2 2.398 5.6l17.583 44c1.598 3.2 2.398 5.6 2.398 7.2-.8.8-1.599 2.4-2.398 4zM116.711 72c-8.791-3.2-15.185-7.2-20.78-12-5.594 5.6-12.787 9.6-21.579 12l-2.397-4c8.791-2.4 15.984-5.6 21.579-11.2C87.939 51.2 83.144 44 81.545 36h-7.992v-3.2h21.58c-1.6-2.4-3.198-5.6-4.796-8l2.397-.8c1.599 2.4 3.997 5.6 5.595 8.8h19.98v4h-7.992c-2.397 8-6.393 15.2-11.189 20 5.595 4.8 11.988 8.8 20.78 11.2l-3.197 4z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/link.svg b/src/assets/icons/svg/link.svg
new file mode 100644
index 0000000..48197ba
--- /dev/null
+++ b/src/assets/icons/svg/link.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M115.625 127.937H.063V12.375h57.781v12.374H12.438v90.813h90.813V70.156h12.374z"/><path d="M116.426 2.821l8.753 8.753-56.734 56.734-8.753-8.745z"/><path d="M127.893 37.982h-12.375V12.375H88.706V0h39.187z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/list.svg b/src/assets/icons/svg/list.svg
new file mode 100644
index 0000000..20259ed
--- /dev/null
+++ b/src/assets/icons/svg/list.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M1.585 12.087c0 6.616 3.974 11.98 8.877 11.98 4.902 0 8.877-5.364 8.877-11.98 0-6.616-3.975-11.98-8.877-11.98-4.903 0-8.877 5.364-8.877 11.98zM125.86.107H35.613c-1.268 0-2.114 1.426-2.114 2.852v18.255c0 1.712 1.057 2.853 2.114 2.853h90.247c1.268 0 2.114-1.426 2.114-2.853V2.96c0-1.711-1.057-2.852-2.114-2.852zM.106 62.86c0 6.615 3.974 11.979 8.876 11.979 4.903 0 8.877-5.364 8.877-11.98 0-6.616-3.974-11.98-8.877-11.98-4.902 0-8.876 5.364-8.876 11.98zM124.17 50.88H33.921c-1.268 0-2.114 1.425-2.114 2.851v18.256c0 1.711 1.057 2.852 2.114 2.852h90.247c1.268 0 2.114-1.426 2.114-2.852V53.73c0-1.426-.846-2.852-2.114-2.852zM.106 115.913c0 6.616 3.974 11.98 8.876 11.98 4.903 0 8.877-5.364 8.877-11.98 0-6.616-3.974-11.98-8.877-11.98-4.902 0-8.876 5.364-8.876 11.98zm124.064-11.98H33.921c-1.268 0-2.114 1.426-2.114 2.853v18.255c0 1.711 1.057 2.852 2.114 2.852h90.247c1.268 0 2.114-1.426 2.114-2.852v-18.255c0-1.427-.846-2.853-2.114-2.853z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/lock.svg b/src/assets/icons/svg/lock.svg
new file mode 100644
index 0000000..74fee54
--- /dev/null
+++ b/src/assets/icons/svg/lock.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M119.88 49.674h-7.987V39.52C111.893 17.738 90.45.08 63.996.08 37.543.08 16.1 17.738 16.1 39.52v10.154H8.113c-4.408 0-7.987 2.94-7.987 6.577v65.13c0 3.637 3.57 6.577 7.987 6.577H119.88c4.407 0 7.987-2.94 7.987-6.577v-65.13c-.008-3.636-3.58-6.577-7.987-6.577zm-23.953 0H32.065V39.52c0-14.524 14.301-26.295 31.931-26.295 17.63 0 31.932 11.777 31.932 26.295v10.153z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/log.svg b/src/assets/icons/svg/log.svg
new file mode 100644
index 0000000..d879d33
--- /dev/null
+++ b/src/assets/icons/svg/log.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1566035943711" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4805" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M208.736 566.336H64.384v59.328h144.352v-59.328z m0-336.096H165.44V74.592c0-7.968 4.896-14.848 10.464-14.848h502.016V0.448H175.936c-38.72 1.248-69.248 34.368-68.192 74.144v155.648H64.384V289.6h144.352V230.24z m0 168.096H64.384v59.328h144.352v-59.328z m714.656 76.576h-57.76v474.496c0 7.936-4.896 14.848-10.464 14.848H175.936c-5.568 0-10.464-6.912-10.464-14.848v-155.68h43.296v-59.296H64.384v59.296h43.328v155.68c-1.024 39.776 29.472 72.896 68.192 74.144h679.232c38.72-1.184 69.248-34.368 68.256-74.144V474.912z m14.944-290.336l-83.072-85.312a71.264 71.264 0 0 0-52.544-21.728 71.52 71.52 0 0 0-51.616 23.872L386.528 507.264a30.496 30.496 0 0 0-6.176 10.72L308.16 740.512a30.016 30.016 0 0 0 6.976 30.24c7.712 7.968 19.2 10.752 29.568 7.2l216.544-74.112a28.736 28.736 0 0 0 12.128-7.936L940.448 287.456a75.552 75.552 0 0 0-2.112-102.88z m-557.12 518.272l39.104-120.64 78.336 80.416-117.44 40.224z m170.048-70.016l-103.552-106.016 200.16-222.4 103.52 106.304-200.128 222.112zM897.952 247.072l-0.256 0.224-107.136 119.168-103.52-106.528 106.432-118.624a14.144 14.144 0 0 1 10.304-4.736 13.44 13.44 0 0 1 10.464 4.288l83.264 85.696c5.472 5.6 5.664 14.72 0.448 20.512z" p-id="4806"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/logininfor.svg b/src/assets/icons/svg/logininfor.svg
new file mode 100644
index 0000000..267f844
--- /dev/null
+++ b/src/assets/icons/svg/logininfor.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1566036016814" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5261" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M896 128h-85.333333a42.666667 42.666667 0 0 0 0 85.333333h42.666666v640H170.666667V213.333333h42.666666a42.666667 42.666667 0 0 0 0-85.333333H128a42.666667 42.666667 0 0 0-42.666667 42.666667v725.333333a42.666667 42.666667 0 0 0 42.666667 42.666667h768a42.666667 42.666667 0 0 0 42.666667-42.666667V170.666667a42.666667 42.666667 0 0 0-42.666667-42.666667z" p-id="5262"></path><path d="M341.333333 298.666667a42.666667 42.666667 0 0 0 42.666667-42.666667V128a42.666667 42.666667 0 0 0-85.333333 0v128a42.666667 42.666667 0 0 0 42.666666 42.666667zM512 298.666667a42.666667 42.666667 0 0 0 42.666667-42.666667V128a42.666667 42.666667 0 0 0-85.333334 0v128a42.666667 42.666667 0 0 0 42.666667 42.666667zM682.666667 298.666667a42.666667 42.666667 0 0 0 42.666666-42.666667V128a42.666667 42.666667 0 0 0-85.333333 0v128a42.666667 42.666667 0 0 0 42.666667 42.666667zM341.333333 768a42.666667 42.666667 0 0 0 42.666667-42.666667 128 128 0 0 1 256 0 42.666667 42.666667 0 0 0 85.333333 0 213.333333 213.333333 0 0 0-107.52-184.32A128 128 0 0 0 640 469.333333a128 128 0 0 0-256 0 128 128 0 0 0 22.186667 71.68A213.333333 213.333333 0 0 0 298.666667 725.333333a42.666667 42.666667 0 0 0 42.666666 42.666667z m128-298.666667a42.666667 42.666667 0 1 1 42.666667 42.666667 42.666667 42.666667 0 0 1-42.666667-42.666667z" p-id="5263"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/maxkey.svg b/src/assets/icons/svg/maxkey.svg
new file mode 100644
index 0000000..f8f8a7d
--- /dev/null
+++ b/src/assets/icons/svg/maxkey.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="256" height="256" viewBox="0 0 256 256">
+ <image id="图层_1" data-name="图层 1" x="32" y="25" width="192" height="206" xlink:href="data:img/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMAAAADOCAYAAABo1g13AAAazklEQVR4nO2dS1NcR5bH/3kLYZl22OXdbCRdVtOoF4Zd9zxCsJudYNc7wScQRFvYO8Fmoo16AvgEwK53QquZHXjh3lJaWHinBH8AlxQdtiyZmxN5MqsooB6Z95V5b+UvQmGZKMGt4pw8jzwPhoA3xPFqPOxZON/h4beVL0EBCoaE+mJyFhBNMBYD4h4Ya0JACnuz548NbfrDhFIIAQ6wMwjB0ZBf+63F+U67np9ovgQFyImuoLMkBtgXAJuXX3b4SFJBWgBrQyTfIkIrKMZNggKkJI6/nEeCWbDoAQRmHQu7DS0wcCTiBRpocf6sVZHnLoSgAIbQCZ9MLGqBn4e92+Ir0n06JivR+HA8bnFGUIAhqFOeLQLsYYVO+GwwcQwhXiD67XAclCEowDV6hP5RjU75tLSA5KDOyhAUQLk3TSSTj8GwqP35wHWkZUhwgMaHwzoF0mOtAHTaC/YUgjI2ATPaADtE9OtmHazC2CmAOu0nlsGiR+G0z4i2CvzHZ/uVfQsePEMpdN0cYDX49rnDIcRmFRWh9goQBL9UKqcItVWAIPhOqYwi1FIB4vjJYyRsIwi+c7xXhFopgMrhR3tjc2lVFWSwzD6s+Jg1qoUCUJmCuLUX0pm+w/Z9S582PHiGTMR3159CNPYB9vsKv41xYRaisdj89N/etN/+w4sivMpagDh+MosLtg+GLzx4nIA1fliDSloAdeqzv4PhXzx4nEA6vLAGlbIAuunkMJz6dcOdNaiMBVCpzQl56ocMT/1Q1uDzP71st/9RqhJ4bwH0hdZTfaEVqD8b/Hxrs6x36bUCqC6syaOQ1x8zGA7B3q+V4RJ5qwDxnfVFMOyF29yxhSN6v1C0EkQ+frqU5WF4HoR/rJHW/4QOwgLxLgiO765vA/jag0cJuOc2GP7c/Ozf0X7z3bdFPI03CiCD3eZn//m/APuzB48T8Iv5opTACwXQ+f3/A9gfPXicgJ/MNz/7j7j95rsXeT6d8yA4ZHoClrTwdmqBtzdyacx3qgBB+AMpyU0JnClAEP5ARnJRAicKEIS/CwejSc+yc6rzi3xDA22vIGQ6+DOaKk1/vzJdepzJrASlK8AYCn+bhtEK8S0iObpc/rlo5XXBQ2XhSkFmIaIH9LmO17iXTEpQqgKMifC3AXGISLyUQ2ddTV+m9tCOUtRrmG8/UitBaQqgi9pOain8NFCWyRP+0Ndx492Zp4w9qKeFYPv8/JsV639VzMPcJL6z3qpVHT81eosXwCf7nOeTkiuLOP46RpIsg+GRjiVqAtvh59+sWf0ay3jjuryhDuXMUtB3fT7pbdGTNJYBPKrWkw/Eqpy6cAWgwjZgo+ifUygVPu1NIauACz0suOJWIcIy51sHJi8tVAF0SfPzIn9GoSjB3+T8b8eVfQ8piOMnyxVXhDai93MmmbbCFEBnfE4qmX0YU8G/TsUVgePt1NyozFAhClDZjI/M17NkZdwF/zqVVQTGjvnZNwtDX1LEz43vru9XLKii4JafbzmLVcRpV7gGCRmZczYDJ3N0dOZoo4LB8tCgOHcF0INpd/L+voVB7k5jhfO/FipY4jWa+BkxInk5hVlEuEf/FWiCWbqJgm6XW1RGIfASAi25LJv9AYVnpkgRRHJUKWsQJQuDrHquCqD9/td5fs8CaSPCCudbh0X8CBL4X+gGdh4Ryrl86iiGVAqGQzaDwly5+O66tAZPi/r+OTMwHshXAe6uv66E31/QqU9ujIDMfD0kwfcBOWFBQDaRHOftPlXKGjAc8rOtpZtfzokK5fvX+PlWbi6aPumXvRL6wUiLcMBmkOu8/gpZgxu/+1wUoBKuD2V4xFJeN7jiFPMQkDeoD619ePdwrQybeVkFyhQlbNvztPeN+4F8xqKoCk9/IZcnWshD+KXgi1PI93tEtTTVE35oN1Uq72txir2eDFRqOH+2jyiao4PGX5oQH+31Pl1mC+C/6yN2+fmzzHVIdOIrM1/PJRwqVljLahF0XPDc64pTgSX+o0p+ZFKACrg+m1lz+7UX/Jvs5+EaxXef7ADscUHPmJVuViibCyQmt314NwNYyyL8MrgVP2CbXJ3xEX70uEaZglptdUsbcmtJjE9/ofeX2gLEd54sg7E9g5eWTyRWyCdNifgBj5Fgo6L+fZ5IK7CS5T7B6wxRJObSWwDGfH1TqYVfBoMU4ArsBOEnZHB8JC0hpXtToK2wb5agTe4xf9ZKpQA68PXv8iOb8Mu05smYuTtmCKziHU7SZov8UgKxi2hquuMeW7tAHje2p77gIl9fhAUcRshM0X2k+pydukMDStztFcDPSs9U2R4y6++oYSec+jYw7LDfw6r3tkPp8kMXoFgbVPNlpQCepj3TCb8y52E4V1pU0d1SmnRpfG/9pIR7At2/PbUzrI11wupbJpN+XXgxcczPntkL//f04T8Pwp8BJcBH4hQL1krA3i8AkyfFFdHJrZNs06TY0dgCeHf6k2mT5Q12FZ0k/IzKGEKWJx/k52+tBLrBJt+W2RStrOZZIO9O/xTCf0q1O0H48yXWlsDqNKffXSRSxRF9aFMG8OzZwMaXQRhZAA99f+uMD538EaU5A8WQzhJkK5kw8vOHYRYDXNya92efJI3Asxd+Br8rVqtPnComiD5sQEw+tI4HcmpqMnOBfLn1lX5/xKwuVMg0R3ge3J5SUEpgcWvM+U5b9mkYPxzDS+rxJXcne0ffSAWgmh9fsiUU4Ji/6ZDqdEKs71aM0X0aow62Nrm+Z1uzeY6tGW0BIl8uvdh+ijKHwyD8TpjXlbTG0F0OGzTVolu+kPu0kaExgAp+mftbUkp5Wrs+0m2rzzTqqiGwKl7hzKpsgiVrENFlrEZ+vrzFLW4Q8dDQ1puyB8siN3FKNe1+lmqPE2pu0ZxNUExZIcYeljWhb5QL9KDoBxgJA7cU/rhC82rqjaBg2CoopqwQm5orazzlQBdIzY33wH9mie3Wj73g93tFjF/pQDK69KKsUIkMtgBqaYJjZOBrfhJQJ1eo7PQPGQ+c+vl7GeYCPSzxOfpjkfMn1yep+CKOerOXtqusSPoqQHznL4vuBxzJ09/iokOEHl7PkfcD3jUd9Y8BWGOx9Ce5TtTYNX2peEXzOKu844pTDjzBWXdxdofL6dH3ujuAq6roEb0HrxgUBLvN/sg6f/7f5rlfZnfp4gGcBtbKYVS30WLTsAr89BBeqQiL+nfle9CvxjD+a3HTqtNyQwG8yP4wGC04w2XO3/+sjyAhP8hjbLnOq3N9090Z3uXfpkf1njfT9hCXwY2LMOcTvWTe/2xr2vTl4hR+j2RXM/t3cRs7tie99Y9SdyC+bHHZLOM9Z+VmEOx6mTUTNpkf30//TXyMaTaDjTIEQVoGNkOfyTRZGzdI61bae87KFQugl9v95PSJomjaNPvj8en/EsCiq31eHXRyYLukzyjzFDkXXLUAFw3HlxXihbHwv6IA0D/hF9hlM5h1LfwSdp9ihIVCrYGgeh85VXq6asKPGwrAIrcKEMF8XxeDX5OHVcC3wu77levucYvyn8wmsEsunsdB7iiuKYBj/7/9OyMF0MGeT1frMq25kPfqoTyRPrlU0Jy+5TH1/97HahX8/GFcTYMKl7X/4sWord6XL8WqPz3Kuhn8D15vRiGkgopT+mvaUvFK+vnD6CqAzv+7w879cV+nhK7/u5SX8FMDEiZiXLCe2Ia10UjawG+tPColUylBiancsum1AI5X2jSMThV96eNL8LuSZTk1Zd0ubslMzQOALSLRJQ7XrVsiPdVJ2aDEAXYMIV50VvykQSuBWd9EhH0k+S3T841LBUiYO6FiaBkXvqk9vD6wqbMs1qhW01urSJjtkr0YEHIl67JSBuwjen/Qu/XQ+COfwYb4AV/Q59kfb8sX8uQyCHYZAAvxrfFr/XB/jnVQaYU88Wm3QjJ5om/bsxS1qVvfZPIovrueLvP0CwXFV5Wnk82aoaC+1sKPKwogmDsXKGKm7k/siftjnU2J4yezSvBJcfKs5pSfx7bc0q9iCHPYnBJ2+ge6bkffXHubzcobUgDyRV3W/7enTE8aH1Kf1v5wHD95jISdFKy8tLiEFM0CfcpvglHzeiXKF/JEW4AJd6e/9P/N05+u+xS4reujXB5W1kWRHGNzEsfrVsVwJPg1DXJHoRTgwmEALMSZ8WtdX9RZ3qbSye9iiXiCHVtLMK4oBWAOFQDMKI3ogf/PbXxj5fOXdvJfp4mEHdnGBONIJwh290FFhnl096v3jQvK9EQ9q/mYBdCE+CgMBxuBtgDCYa/mhJnvyZwrgHlmRC0TcX/6CjGfOkU6JigFEMxZBsi499etBTg2DRK12+FTa+LTuLkRpmUMwLULZLFIweFEAdnAbopvq6SkK/Tpz8EKDKCjAG5OCCbMFcBlo74wuxH18PTv4FfvhEdE+hLMDQJvTH6uOOnOxikfhrZxwZtcJeUnTar2DdwgAm479A8js/7jSaenv3m1pzfLRPqQMPfDzjxkAnjXlBLmBsNLsMhhmYagBveRqIECHiwTGQh7GMdfpi6hLvC52kUuwBjFBDARMgTDMYxTHJaTmBEjibzclEml3ZFYcqEI5ouyi0AYB8EuexXM6pRclpNUn1jdXJdfvuFWAaqB4UVdUICMNCGi0me8BgUI+IMQwQIExprS49GgAAF/kGtRSyYowGjMfHvzgD4wCIvByHnhVgFMA0fhsE1PGJrlxgdnuewa0EaEpbJWo/YyocqRXU7EMsA0FVkMptkdvy2A3LaflH/CjqQhLecnLc4N22JzZuCe4HJgphWe7oTLsA1TTm2L730lh1b5eRss2DH/cWtspj2YEgHv/J8C8M6hBbBpxLGZb1Q2kTAv6R4jorI3c1+BJUbuBc2vcRUHCDR1P/Joovd+jgmXa6d4+lGKdaYTBDsSLovbU5dxgOE4FjpMmNmQr1IRLAj/ANwqgF2Njzv3wmYcI7vwK9CUwW/EjHcujxudpnhnQWYcf+1/KhSYN13zT6k8lm5obiEIHFht3B8zOk3x5sOpcicxCzJZ+jHkuWCz5p+9X3FoVXueA5yfb/nWo+wVHRfI3Qlh3uvr1re22ElGsUB0kdc6orS0waIFx8/gPdoCuLzGZ0Z5dhpL4vhGWO8lNoLz/zksZDGdKRFWguszGm0BEofm2uriyHWeffRGlR60+1G+EkRiJaQ9zVAK0Ljl0r+OLQY3uU4xxuKV3RrUkpWgrYT/mdWNr7RsxncdNYMUQJtKd1ag+U/TQNj9qRbhqa2wkBJEF0uUkiwKSneKhRTCH+uFeUe2yl0HeqtBHQbCZiM79HhCt36tqg61HjpLMYEKSvPe2q42u7CpOdumcp3a7TTKx2DYFqd4bRPrVJ1eBTAa/1EILDKf+28zprA45tOcltLS8vOtZUTRdA6K0AbELqJoTlqYVNWUv1JMc92akUUQp/Sn9m5Rd99iHH+5iqT8puQub6c+N9kUo9ek+jLeI9MiOXUJeDEPgUeGS8rbYKIFJgvbPtnPUkIsfsBjCIyuXWLYgcBuXTfI9CrAvNO5MUKs8B/N/FfxCj85G5XYC6Mivbm8hEONLxRNgMkZPur9yRQ1Lcq+aKVZh9oP8YpWzdrsL+B6N1rtyql7FEBONps0G1VYzKPs8/NvjC6PxCmtHbJKSRYI15agEiek+B6zYDhKeYBwJLQZvzbdb90YQJdFO3xjYtE4HZp4VGujfOajKvjL5D6mF37Qe41wUqf44FpPsNOGjqZpOlSfQD6VHSsl+N75FpuBkM+PTMLfi8wSyWyRL1Y4NVcVQCRuhUo0bD5Q3/pbO6ejV0IhU53iB2wbBbz2bFQ9bcp6/8d9HIA23k5Nm+4Nlh++J5vjryIv7ATWXMcFZJEiClyLXy8rq3UFlqqWLbpiAdzHAbTOx+Y0yftSKR8EZVlOXN2sdk/9CCelCD+6O9xeVy0+6DMXyHljt/mSiXeUo/azqV/QVptSb1ZJ8KUL9gteQzgra1jWSYFKxAfs+hec3wdIomTBdEgSnbIM7i7wzOF61epB3m4CZXdU3/IjL+5HLvH+/uCGAkAtLPjJ2eI80MS4Y372jXEzh7exwGCOqaRD4DhNTp1qeN6Ry/EADIseLBEfjicxUT8GKcC+822HNlbAr/IIO6QLp9o9pXCc9RT7ce1GdQ4iqeD3aE6R7wI/mH02A9edclfoPxlOiGMw5lYBksYj01y/rMcRp/Ta6m1CVALe/7n7HU+i8CcqEoe95/3pPxy38eHQfVO3WNZ7d01Z8TYgDoAs2u1C7iIy0VcB9IAn9/Ue4iPjunvyLxnC/Bt/2WTT/h1Qg8ej+zDgSYh5mwXPbAYbzsenBG4Ske/vZSZoyH6A31pezLaxK4+QPvJScIW8gmMSa6YPRNUIJTJQAdStsHB/0yqtwJ0nxhdJOtXm3xz88WXByvVJJh/Hd9dfW8Z/qRm+ISYSfpQdM7ZtMTkC7L7qYir2oQIGbNrk/rXQb6i9wZOv47tf7RWtCEMVQM259GLacROf/mLnCv0a4gGnqDZKu7GMYvJal5pYVoqwXlhZxegdYezCk8UKYtUqIFY7BZa8X11UTzg+thP+OH7yeMgF3wa5RRausCl9b4KvPhiVSL92WhpxCcfbqTnTcmlkbwEM2GPdIkpuTjJ5YiRjcpUq+7CSV3/0SAugS6R98adjW1eIam0YWYJA8aTrj04mj4wPWDk9I8f4wGxNqlr940lqUbpC61ZlGnp0iVc1KLVDpZ6thV/79ykEmeKDo/jueqay75EuUIf47pMdgBmPCC+YNqL3c7ZmUNflW091C4z6YCneWrCtbI3vrNuOZxkEhxCbpmN1ejFXADnEKUle5/CweWEdDyAoQRGkGpWi/f6jfMvY2T6iXzdtDkbjTfF6gK5PLYgyHrBuhKEr+XBbnBfK57cX/mb+wo+etOlX26bxgbECqFdHnq3bEctpcsTsPg7BMBdSpJl4mXogmJjcK7aBSaxSfGCQNjV2gTp40SxzkzV+vmVdaqubtws4iWrPAW5jNU11pz6wyjxIOaJkZVBzlb0C+HUvcEmEZc63Urlo4hV2bHaAjTmb1je8GgfC30P/+MBaAaDeiE+zOTu09YKIVOUPeoTJ03BhNhApOCtpp2G7Ff4rbODt1G4neZJOAXy1AlmVILhE/ZFN7R9hJW1Di0fC36GbNk2lAPBhn8BgMikB/Js+7Q6VKVuhpEFKPBT+S6Jo2i4L1APnf9vxpFL0Ok0k7MT2trgX7ePmscWlushy8o8xXVvhl+P4+V95agVQ38ODtslBJNjPUkYr03tshi7NVsYsXXpM6c376bI8HbwWflooyDaRNgboxbMSiX5s8POtzIqqb5BT1q1UgmOd4clk1VV8+NE2XUr5Ss8q2ewKIN+wmDyB8FgwZBD3ZmrFtmyiHzVUhFwEH53yhotJeclYzkDeNDBxzM+edacOZnOBuiNUEr8rLeXczE9/PsmjfFaWUrAZTOtmGx9joNGoaXTSt5c3uZkW/XVQM2Unj7wWfloy2Lgiq5ktQIcKuEKgD0CItTRVg4Og1KmgAb0PK2AV1EzSj7Gf54wev4PdK9yoGMhPAargCnVh+3j78VoeLlEv3SnNfimDPN2/1XM5cw3myaKKW3uGK17dwnDIz7ZuNEblpgDwZbS6OUNrRLKiL9XmSRlUr2tZCsHplJcDAW7jsKhpbNTDm7ANDy9DbyKzPixa0BXNV8hVAeD3BdkA7GvI09Az0nye/GQ5+VlQv3JaAeJ6T7FcTfRSC3yr6PGDlTr1O0RY4nyr731G7gogie99dSQHWhXxvQsidUdRHmhr0WshrloLFbS2kchbbrRdzNnX5S8yxlutxKl/ySY/3xoYnxSjAJWKB65QqFtUVXTr4nblUr8ML/nZ1tBdCoUoAJSPOIuEmXf7e0U5bpHvUEwn2NNKuTsdhvj9V19WINWLB64znopQacHvEIk5k4LIQhUA1bkfGI4cxpSwXf5j/0CqDpDbenFrEREeVVrwFcYdgoUrACgoXn+utxhWHQqW0fhwXBeroF3VhxUMbgcxNOi9TjkKoILiowovd7uJLCVIxAu5TkpPz6sMKqMzsQzGHtbgtO9B7PLzZ1aDskpRAHTmConkqIKZodFcKoO3lkHN4ZlYrJ/QawwyPv3/WYnUWgkuadG2fcGO0XjfcqUQqjLz1jytVQWrQp1Sepi8CJya59y+tKVUBcD4KEEvnG5p5W2tvLVtCA584Hm5TWql0K0YF3SrPAvG7kHQ2tXxaO43THcO/ucOqPYdQW60lXIIWc4gf3lvAHapFEKoXyhjPQeFuKe+JitQ6evNsf4MMwo/XCkAghIEspKD8COPhpi00CVFFM3RGwkEbMhJ+OFSAdAZuMuihaAEAWMo4M1H+OHSBeplDAPjQBoyZHsG4YUC4FIJntfqsiyQJwf8fCv3SRNOXaBelDv0fkE3awcCvWwWIfzwyQL04unw3YAbUo2+N6Xh4y+1/ea74+bnf3oDwf7Lg8cJuICmtyVL/OzZ34v86V5agA4hOB5TVKZnMa9MzzC8VgBcKsF2TcqpAyOxr+jMgvcK0CHEBbVHNvyvDJreUBSVUQAEl6i+yI471lgpw+W5TqUUAN1mjlsblW+zDEAXBG4WmeUZReUUoINq3I72gjWoKA5P/V4qqwC4HNa0GmKDSuH81O+l0grQIcQGFUHe8rNozfWp30stFKBDHD9ZVvNsgiJ4BeX1k1UfJ+7VSgEQ3CLf8Mrd6UftFKADuUVJIu8OUm+LDKRGCv4uoqmdPEuXi6C2CtAhKEKpVEbwO9ReAToERSiUygl+h7FRgA6kCPhtESJ6HILlzLQQJQfAJ/tVE/wOY6cAvaisUS2GwZYLXWKJzTrsURhrBejQdY8YHgSrMBCZuz+oopszjKAA19CL/mT73cMws0j69uIAkTis69acoABDiOO/LCJpLI6ZZeCAeFFnoe8lKIAh2jLMg4kHNYsZ2mCiBSZeAL8djts2nKAAKYjjjSbwTznacVGtPGWzFXKXegReTrL+pFUnn96WoAA5QbNOgVkkNKXZF6XQk6nFmZpMXZ/NNnkRFKBAupYCoomkof4rlUOipjtniSva6jSXy7JFi6ZLR4LjgrXRkCf77/g4n+ymBAXwAFpm0WWij1LIsekftDA320GwcwLA/wMQTIdh+nm6rAAAAABJRU5ErkJggg=="/>
+</svg>
diff --git a/src/assets/icons/svg/message.svg b/src/assets/icons/svg/message.svg
new file mode 100644
index 0000000..14ca817
--- /dev/null
+++ b/src/assets/icons/svg/message.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M0 20.967v59.59c0 11.59 8.537 20.966 19.075 20.966h28.613l1 26.477L76.8 101.523h32.125c10.538 0 19.075-9.377 19.075-20.966v-59.59C128 9.377 119.463 0 108.925 0h-89.85C8.538 0 0 9.377 0 20.967zm82.325 33.1c0-5.524 4.013-9.935 9.037-9.935 5.026 0 9.038 4.41 9.038 9.934 0 5.524-4.025 9.934-9.038 9.934-5.024 0-9.037-4.41-9.037-9.934zm-27.613 0c0-5.524 4.013-9.935 9.038-9.935s9.037 4.41 9.037 9.934c0 5.524-4.025 9.934-9.037 9.934-5.025 0-9.038-4.41-9.038-9.934zm-27.1 0c0-5.524 4.013-9.935 9.038-9.935s9.038 4.41 9.038 9.934c0 5.524-4.026 9.934-9.05 9.934-5.013 0-9.025-4.41-9.025-9.934z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/money.svg b/src/assets/icons/svg/money.svg
new file mode 100644
index 0000000..c1580de
--- /dev/null
+++ b/src/assets/icons/svg/money.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M54.122 127.892v-28.68H7.513V87.274h46.609v-12.4H7.513v-12.86h38.003L.099 0h22.6l32.556 45.07c3.617 5.144 6.44 9.611 8.487 13.385 1.788-3.05 4.89-7.779 9.301-14.186L103.93 0h24.01L82.385 62.013h38.34v12.862h-46.41v12.4h46.41v11.937h-46.41v28.68H54.123z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/monitor.svg b/src/assets/icons/svg/monitor.svg
new file mode 100644
index 0000000..bc308cb
--- /dev/null
+++ b/src/assets/icons/svg/monitor.svg
@@ -0,0 +1,2 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1543827393750" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4695" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css">@font-face { font-family: rbicon; src: url("chrome-extension://dipiagiiohfljcicegpgffpbnjmgjcnf/fonts/rbicon.woff2") format("woff2"); font-weight: normal; font-style: normal; }
+</style></defs><path d="M64 64V640H896V64H64zM0 0h960v704H0V0z" p-id="4696"></path><path d="M192 896H768v64H192zM448 640H512v256h-64z" p-id="4697"></path><path d="M479.232 561.604267l309.9904-348.330667-47.803733-42.5472-259.566934 291.669333L303.957333 240.008533 163.208533 438.6048l52.224 37.009067 91.6224-129.28z" p-id="4698"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/nested.svg b/src/assets/icons/svg/nested.svg
new file mode 100644
index 0000000..06713a8
--- /dev/null
+++ b/src/assets/icons/svg/nested.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M.002 9.2c0 5.044 3.58 9.133 7.998 9.133 4.417 0 7.997-4.089 7.997-9.133 0-5.043-3.58-9.132-7.997-9.132S.002 4.157.002 9.2zM31.997.066h95.981V18.33H31.997V.066zm0 45.669c0 5.044 3.58 9.132 7.998 9.132 4.417 0 7.997-4.088 7.997-9.132 0-3.263-1.524-6.278-3.998-7.91-2.475-1.63-5.524-1.63-7.998 0-2.475 1.632-4 4.647-4 7.91zM63.992 36.6h63.986v18.265H63.992V36.6zm-31.995 82.2c0 5.043 3.58 9.132 7.998 9.132 4.417 0 7.997-4.089 7.997-9.132 0-5.044-3.58-9.133-7.997-9.133s-7.998 4.089-7.998 9.133zm31.995-9.131h63.986v18.265H63.992V109.67zm0-27.404c0 5.044 3.58 9.133 7.998 9.133 4.417 0 7.997-4.089 7.997-9.133 0-3.263-1.524-6.277-3.998-7.909-2.475-1.631-5.524-1.631-7.998 0-2.475 1.632-4 4.646-4 7.91zm31.995-9.13h31.991V91.4H95.987V73.135z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/number.svg b/src/assets/icons/svg/number.svg
new file mode 100644
index 0000000..ad5ce9a
--- /dev/null
+++ b/src/assets/icons/svg/number.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1575802851180" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2867" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M279.272727 791.272727h512a46.545455 46.545455 0 0 1 0 93.090909H279.272727a46.545455 46.545455 0 0 1 0-93.090909z m33.838546-617.984V651.636364H193.722182V395.170909c0-37.003636-0.884364-59.298909-2.653091-66.746182a24.948364 24.948364 0 0 0-14.615273-16.989091c-8.005818-3.863273-25.786182-5.771636-53.341091-5.771636h-11.822545v-55.854545c57.716364-12.381091 101.562182-37.888 131.490909-76.520728h70.283636z m303.709091 396.8V651.636364H354.164364v-68.235637c77.777455-127.255273 124.043636-206.010182 138.705454-236.218182 14.661818-30.254545 22.016-53.853091 22.016-70.74909 0-13.032727-2.234182-22.714182-6.656-29.137455-4.421818-6.376727-11.170909-9.588364-20.247273-9.588364a22.248727 22.248727 0 0 0-20.200727 10.612364c-4.468364 7.121455-6.656 21.178182-6.656 42.263273v45.521454H354.164364v-17.454545c0-26.763636 1.396364-47.941818 4.142545-63.348364 2.746182-15.499636 9.541818-30.72 20.386909-45.661091 10.798545-14.987636 24.901818-26.298182 42.216727-33.978182 17.361455-7.68 38.167273-11.543273 62.37091-11.543272 47.476364 0 83.316364 11.776 107.706181 35.328 24.296727 23.552 36.445091 53.341091 36.445091 89.367272 0 27.368727-6.842182 56.32-20.48 86.853819-13.730909 30.533818-54.039273 95.325091-121.018182 194.420363h130.885819z m270.615272-189.393454c18.152727 6.097455 31.650909 16.104727 40.494546 29.975272 8.843636 13.917091 13.312 46.452364 13.312 97.652364 0 38.027636-4.328727 67.490909-13.032727 88.529455-8.657455 20.945455-23.598545 36.910545-44.869819 47.848727-21.271273 10.938182-48.593455 16.384-81.873454 16.384-37.794909 0-67.490909-6.330182-89.088-19.083636-21.550545-12.660364-35.746909-28.253091-42.542546-46.638546-6.795636-18.432-10.193455-50.362182-10.193454-95.883636v-37.841455h119.389091v77.730909c0 20.666182 1.210182 33.838545 3.723636 39.424 2.420364 5.585455 7.912727 8.424727 16.337455 8.424728 9.309091 0 15.36-3.537455 18.338909-10.612364 2.932364-7.121455 4.421818-25.6 4.421818-55.575273v-33.047273c0-18.338909-2.048-31.744-6.190546-40.215272a30.72 30.72 0 0 0-18.338909-16.709818c-8.052364-2.653091-23.738182-4.189091-46.964363-4.561455V357.050182c28.392727 0 45.893818-1.070545 52.596363-3.258182a22.946909 22.946909 0 0 0 14.475637-14.149818c2.932364-7.307636 4.421818-18.711273 4.421818-34.257455v-26.624c0-16.756364-1.722182-27.741091-5.12-33.047272-3.490909-5.352727-8.843636-8.005818-16.151273-8.005819-8.285091 0-13.963636 2.792727-16.989091 8.378182-3.025455 5.632-4.561455 17.640727-4.561454 35.933091v39.284364h-119.389091v-40.773818c0-45.661091 10.472727-76.567273 31.325091-92.625455 20.898909-16.058182 54.085818-24.064 99.607272-24.064 56.878545 0 95.511273 11.170909 115.805091 33.373091 20.293818 22.248727 30.394182 53.201455 30.394182 92.765091 0 26.810182-3.630545 46.173091-10.891636 58.088727-7.307636 11.915636-20.107636 22.807273-38.446546 32.628364z" p-id="2868"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/online.svg b/src/assets/icons/svg/online.svg
new file mode 100644
index 0000000..330a202
--- /dev/null
+++ b/src/assets/icons/svg/online.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1568899557259" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="535" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M356.246145 681.56286c-68.156286-41.949414-107.246583-103.84102-107.246583-169.805384 0-65.966411 39.090297-127.860063 107.246583-169.809477 12.046361-7.414877 15.800871-23.190165 8.385994-35.236526-7.413853-12.046361-23.191188-15.801894-35.236526-8.387018-39.640836 24.399713-72.539106 56.044434-95.137801 91.515297-23.86657 37.461193-36.481889 79.620385-36.481889 121.917724 0 42.297338 12.615319 84.454484 36.481889 121.914654 22.598694 35.469839 55.496965 67.11456 95.137801 91.51325 4.185322 2.576685 8.821923 3.804652 13.400195 3.804652 8.598842 0 16.998139-4.329609 21.836331-12.190647C372.047016 704.752002 368.291482 688.976714 356.246145 681.56286zM263.943926 754.580874c-92.603071-61.111846-145.713686-149.623739-145.713686-242.840794 0-93.195565 53.094242-181.682899 145.667637-242.774279 11.805884-7.79043 15.061021-23.677259 7.269567-35.483142-7.79043-11.805884-23.677259-15.062044-35.483142-7.269567C128.487861 296.954249 67.006602 401.024489 67.006602 511.74008c0 110.73708 61.496609 214.830857 168.721703 285.593504 4.343935 2.867304 9.240455 4.238534 14.08274 4.238534 8.317433 0 16.476253-4.046153 21.400403-11.507078C279.003923 778.258133 275.748786 762.372328 263.943926 754.580874zM788.660552 226.213092c-11.80486-7.791453-27.692712-4.536316-35.483142 7.269567-7.79043 11.805884-4.536316 27.692712 7.269567 35.483142 92.575442 61.092403 145.670707 149.579737 145.670707 242.774279 0 93.216032-53.111638 181.727924-145.715733 242.840794-11.805884 7.79043-15.059997 23.678282-7.269567 35.484166 4.925173 7.461949 13.081946 11.507078 21.400403 11.507078 4.841262 0 9.739828-1.37123 14.083763-4.238534 107.22714-70.761624 168.724773-174.857447 168.724773-285.593504C957.341323 401.025513 895.860063 296.955272 788.660552 226.213092zM790.090111 633.67213c23.865547-37.459147 36.480866-79.617315 36.480866-121.914654 0-42.298362-12.615319-84.45653-36.480866-121.917724-22.598694-35.470863-55.496965-67.115584-95.139847-91.515297-12.047384-7.413853-27.821649-3.659343-35.236526 8.387018-7.414877 12.045337-3.659343 27.821649 8.385994 35.236526 68.156286 41.949414 107.247606 103.842043 107.247606 169.809477 0 65.964364-39.090297 127.85597-107.247606 169.804361-12.045337 7.414877-15.800871 23.190165-8.385994 35.237549 4.838192 7.861038 13.236466 12.190647 21.835308 12.190647 4.579295 0 9.215896-1.227967 13.400195-3.804652C734.591099 700.786691 767.490394 669.142993 790.090111 633.67213zM567.129086 518.274914c24.12342-17.150612 39.887452-45.305859 39.887452-77.07133 0-52.128241-42.452881-94.538143-94.634334-94.538143-52.18043 0-94.633311 42.408879-94.633311 94.538143 0 31.695886 15.696494 59.797921 39.730886 76.958766-49.875944 21.128203-84.917018 70.234621-84.917018 127.301338 0 2.366907 0.061398 4.762467 0.182149 7.119141l1.249457 24.296359 276.373515 0 1.238201-24.308639c0.119727-2.358721 0.181125-4.750187 0.181125-7.106862C651.786185 588.497255 616.865861 539.465538 567.129086 518.274914zM512.381182 397.889079c23.937179 0 43.411719 19.430538 43.411719 43.314505 0 23.882943-19.47454 43.313481-43.411719 43.313481-23.936155 0-43.409672-19.430538-43.409672-43.313481C468.971509 417.320641 488.445026 397.889079 512.381182 397.889079zM426.08884 625.656573c9.119705-38.542828 44.254923-67.337641 86.085634-67.337641s76.966952 28.794813 86.085634 67.337641L426.08884 625.656573z" p-id="536"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/password.svg b/src/assets/icons/svg/password.svg
new file mode 100644
index 0000000..6c64def
--- /dev/null
+++ b/src/assets/icons/svg/password.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1575802846045" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2750" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M868.593046 403.832442c-30.081109-28.844955-70.037123-44.753273-112.624057-44.753273L265.949606 359.079168c-42.554188 0-82.510202 15.908318-112.469538 44.690852-30.236652 28.782533-46.857191 67.222007-46.857191 108.198258l0 294.079782c0 40.977273 16.619516 79.414701 46.702672 108.136859 29.959336 28.844955 70.069869 44.814672 112.624057 44.814672l490.019383 0c42.585911 0 82.696444-15.969717 112.624057-44.814672 30.082132-28.844955 46.579875-67.222007 46.579875-108.136859L915.172921 511.968278C915.171897 471.053426 898.675178 432.677397 868.593046 403.832442zM841.821309 806.049083c0 22.098297-8.882298 42.772152-25.099654 58.306964-16.154935 15.661701-37.81935 24.203238-60.752666 24.203238L265.949606 888.559285c-22.934339 0-44.567032-8.54256-60.877509-24.264637-16.186657-15.474436-25.067932-36.148291-25.067932-58.246589L180.004165 511.968278c0-22.035876 8.881274-42.772152 25.192775-58.307987 16.186657-15.536858 37.81935-24.139793 60.753689-24.139793l490.019383 0c22.933315 0 44.597731 8.602935 60.752666 24.139793 16.21838 15.535835 25.099654 36.272112 25.099654 58.307987L841.822332 806.049083zM510.974136 135.440715c114.914216 0 208.318536 89.75214 208.318536 200.055338l73.350588 0c0-149.113109-126.366036-270.496667-281.669124-270.496667-155.333788 0-281.699824 121.383558-281.699824 270.496667l73.350588 0C302.623877 225.193879 396.059919 135.440715 510.974136 135.440715zM474.299865 747.244792l73.350588 0L547.650453 629.576859l-73.350588 0L474.299865 747.244792z" p-id="2751"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/pdf.svg b/src/assets/icons/svg/pdf.svg
new file mode 100644
index 0000000..957aa0c
--- /dev/null
+++ b/src/assets/icons/svg/pdf.svg
@@ -0,0 +1 @@
+<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="128" height="128"><path d="M869.073 277.307H657.111V65.344l211.962 211.963zm-238.232 26.27V65.344l-476.498-.054v416.957h714.73v-178.67H630.841zm-335.836 360.57c-5.07-3.064-10.944-5.133-17.61-6.201-6.67-1.064-13.603-1.6-20.81-1.6h-48.821v85.641h48.822c7.206 0 14.14-.532 20.81-1.6 6.665-1.065 12.54-3.133 17.609-6.202 5.064-3.063 9.134-7.406 12.208-13.007 3.065-5.602 4.6-12.937 4.6-22.011 0-9.07-1.535-16.408-4.6-22.01-3.074-5.603-7.144-9.94-12.208-13.01zM35.82 541.805v416.904h952.358V541.805H35.821zm331.421 191.179c-3.6 11.071-9.343 20.879-17.209 29.413-7.874 8.542-18.078 15.408-30.617 20.61-12.544 5.206-27.747 7.807-45.621 7.807h-66.036v102.45h-62.831V607.517h128.867c17.874 0 33.077 2.6 45.62 7.802 12.541 5.207 22.745 12.076 30.618 20.615 7.866 8.538 13.604 18.277 17.21 29.212 3.6 10.943 5.401 22.278 5.401 34.018 0 11.477-1.8 22.752-5.402 33.819zM644.9 806.417c-5.343 17.61-13.408 32.818-24.212 45.627-10.807 12.803-24.283 22.879-40.423 30.213-16.146 7.343-35.155 11.007-57.03 11.007h-123.26V607.518h123.26c18.41 0 35.552 2.941 51.428 8.808 15.873 5.869 29.618 14.671 41.22 26.412 11.608 11.744 20.674 26.411 27.217 44.02 6.535 17.61 9.803 38.288 9.803 62.035 0 20.81-2.67 40.02-8.003 57.624zm245.362-146.07h-138.07v66.03h119.66v48.829h-119.66v118.058h-62.83V607.518h200.9v52.829h-.001zm-318.2 25.611c-6.402-8.266-14.877-14.604-25.412-19.01-10.544-4.402-23.551-6.602-39.019-6.602h-44.825v180.088h56.029c9.07 0 17.872-1.463 26.415-4.401 8.535-2.932 16.14-7.802 22.812-14.609 6.665-6.8 12.007-15.667 16.007-26.61 4.003-10.94 6.003-24.275 6.003-40.021 0-14.408-1.4-27.416-4.202-39.019-2.8-11.607-7.406-21.542-13.808-29.816zm0 0"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/people.svg b/src/assets/icons/svg/people.svg
new file mode 100644
index 0000000..2bd54ae
--- /dev/null
+++ b/src/assets/icons/svg/people.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M104.185 95.254c8.161 7.574 13.145 17.441 13.145 28.28 0 1.508-.098 2.998-.285 4.466h-10.784c.238-1.465.403-2.948.403-4.465 0-8.983-4.36-17.115-11.419-23.216C86 104.66 75.355 107.162 64 107.162c-11.344 0-21.98-2.495-31.22-6.83-7.064 6.099-11.444 14.218-11.444 23.203 0 1.517.165 3 .403 4.465H10.955a35.444 35.444 0 0 1-.285-4.465c0-10.838 4.974-20.713 13.127-28.291C9.294 85.42.003 70.417.003 53.58.003 23.99 28.656.001 64 .001s63.997 23.988 63.997 53.58c0 16.842-9.299 31.85-23.812 41.673zM64 36.867c-29.454 0-53.33-10.077-53.33 15.342 0 25.418 23.876 46.023 53.33 46.023 29.454 0 53.33-20.605 53.33-46.023 0-25.419-23.876-15.342-53.33-15.342zm24.888 25.644c-3.927 0-7.111-2.665-7.111-5.953 0-3.288 3.184-5.954 7.11-5.954 3.928 0 7.111 2.666 7.111 5.954s-3.183 5.953-7.11 5.953zm-3.556 16.372c0 4.11-9.55 7.442-21.332 7.442-11.781 0-21.332-3.332-21.332-7.442 0-1.06.656-2.064 1.8-2.976 3.295 2.626 10.79 4.465 19.532 4.465 8.743 0 16.237-1.84 19.531-4.465 1.145.912 1.801 1.916 1.801 2.976zm-46.22-16.372c-3.927 0-7.11-2.665-7.11-5.953 0-3.288 3.183-5.954 7.11-5.954 3.927 0 7.111 2.666 7.111 5.954s-3.184 5.953-7.11 5.953z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/peoples.svg b/src/assets/icons/svg/peoples.svg
new file mode 100644
index 0000000..aab852e
--- /dev/null
+++ b/src/assets/icons/svg/peoples.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M95.648 118.762c0 5.035-3.563 9.121-7.979 9.121H7.98c-4.416 0-7.979-4.086-7.979-9.121C0 100.519 15.408 83.47 31.152 76.75c-9.099-6.43-15.216-17.863-15.216-30.987v-9.128c0-20.16 14.293-36.518 31.893-36.518s31.894 16.358 31.894 36.518v9.122c0 13.137-6.123 24.556-15.216 30.993 15.738 6.726 31.141 23.769 31.141 42.012z"/><path d="M106.032 118.252h15.867c3.376 0 6.101-3.125 6.101-6.972 0-13.957-11.787-26.984-23.819-32.123 6.955-4.919 11.638-13.66 11.638-23.704v-6.985c0-15.416-10.928-27.926-24.39-27.926-1.674 0-3.306.193-4.89.561 1.936 4.713 3.018 9.974 3.018 15.526v9.121c0 13.137-3.056 23.111-11.066 30.993 14.842 4.41 27.312 23.42 27.541 41.509z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/phone.svg b/src/assets/icons/svg/phone.svg
new file mode 100644
index 0000000..ab8e8c4
--- /dev/null
+++ b/src/assets/icons/svg/phone.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1567417214476" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2266" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M761.503029 2.90619 242.121921 2.90619c-32.405037 0-58.932204 26.060539-58.932204 58.527998l0 902.302287c0 32.156374 26.217105 58.216913 58.932204 58.216913l519.381108 0c32.344662 0 58.591443-26.060539 58.591443-58.216913L820.094472 61.123103C820.094472 28.966729 793.847691 2.90619 761.503029 2.90619M452.878996 61.123103l98.147344 0c6.780427 0 12.31549 5.536087 12.31549 12.253068 0 6.748704-5.535063 12.253068-12.31549 12.253068l-98.147344 0c-6.779404 0-12.345166-5.504364-12.345166-12.253068C440.532807 66.659189 446.099592 61.123103 452.878996 61.123103M501.641583 980.593398c-29.636994 0-53.987588-23.946388-53.987588-53.677527 0-29.356608 24.039509-53.614082 53.987588-53.614082 29.91738 0 53.987588 23.883967 53.987588 53.614082C555.629171 956.647009 531.559986 980.593398 501.641583 980.593398M766.35657 803.142893c0 16.23373-13.186324 29.107945-29.233811 29.107945l-470.618521 0c-16.35755 0-29.325909-13.186324-29.325909-29.107945L237.178329 163.500794c0-16.232706 13.279445-29.138644 29.325909-29.138644l470.246037 0c16.420995 0 29.357632 13.1853 29.357632 29.138644l0 639.642099L766.35657 803.142893zM766.35657 803.142893" p-id="2267"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/post.svg b/src/assets/icons/svg/post.svg
new file mode 100644
index 0000000..2922c61
--- /dev/null
+++ b/src/assets/icons/svg/post.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1566035724641" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3998" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M136.4 434.3h77.7c21.5 0 38.9-17.4 38.9-38.9s-17.4-38.9-38.9-38.9h-77.7c-21.5 0-38.9 17.4-38.9 38.9s17.4 38.9 38.9 38.9zM252.9 628.6c0-21.5-17.4-38.9-38.9-38.9h-77.7c-21.5 0-38.9 17.4-38.9 38.9s17.4 38.9 38.9 38.9H214c21.5-0.1 38.9-17.5 38.9-38.9z" p-id="3999"></path><path d="M874.7 97.5H227c-28.6 0-51.8 23.2-51.8 51.8v194.3h38.9c28.6 0 51.8 23.2 51.8 51.8 0 28.6-23.2 51.8-51.8 51.8h-38.9v129.5h38.9c28.6 0 51.8 23.2 51.8 51.8 0 28.6-23.2 51.8-51.8 51.8h-38.9v194.3c0 28.6 23.2 51.8 51.8 51.8h647.7c28.6 0 51.8-23.2 51.8-51.8V149.3c0-28.6-23.2-51.8-51.8-51.8z m-311.3 723c-15.6 0-146.7-71.6-146.7-91 0-19.4 102-368.6 102-368.6l-83.6-104s-12.3-23.1 24.6-23.1h208.9c36.9 0 18.4 23.1 18.4 23.1l-79 104s102 351.3 102 368.6c0.1 17.3-131 91-146.6 91z m169.2-253.6l-27.9 40.2-74.5-240 103.4 171.7c4.6 7.9 4.2 20.6-1 28.1z" p-id="4000"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/qq.svg b/src/assets/icons/svg/qq.svg
new file mode 100644
index 0000000..ee13d4e
--- /dev/null
+++ b/src/assets/icons/svg/qq.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M18.448 57.545l-.244-.744-.198-.968-.132-.53v-2.181l.236-.859.24-.908.317-.953.428-1.06.561-1.103.794-1.104v-.773l.077-.724.123-.984.34-1.106.313-1.194.25-.548.289-.511.371-.569.405-.423v-2.73l.234-1.407.236-1.633.42-1.955.577-2.035.43-1.118.426-1.217.468-1.135.559-1.216.57-1.332.655-1.247.737-1.331.929-1.33.43-.762.457-.624.995-1.406 1.025-1.403 1.163-1.444 1.246-1.405 1.352-1.384 1.41-1.423 1.708-1.536 1.083-.934 1.322-1.008 1.34-.89 1.448-.855 1.392-.76 1.57-.63 1.667-.775 1.657-.532 1.653-.552 1.787-.548 1.785-.417 1.876-.347L59.128.68l1.879-.245 1.876-.252 2.002-.106h5.912l1.97.243 1.981.231 2.019.207 1.874.441 1.979.413 1.857.475 2.035.53 1.862.646 1.782.738 1.904.78 1.736.853 1.689.95 1.655 1.044 1.425.971.662.548.693.401 1.323 1.1 1.115 1.064 1.112 1.1 1.083 1.214.894 1.178 1.064 1.217.74 1.306.752 1.162.798 1.352.661 1.175 1.113 2.489.546 1.286.428 1.192.428 1.294.384 1.217.267 1.047.347 1.231.607 2.198.388 1.924.253 1.861.217 1.497.342 2.28.077.362.274.41.737 1.18.473.8.42.832.534.892.472 1.07.307 1.093.334 1.2.252 1.232.115.605.106.746v.648l-.106.643v.8l-.192.774-.35 1.5-.403.76-.299.852v.213l.142.264.4.623 1.746 2.53 1.377 1.9.66 1.267.889 1.389.774 1.52.893 1.627.894 1.828 1.006 2.069.567 1.268.518 1.239.447 1.307.44 1.175.336 1.235.342 1.16.432 2.261.343 2.31.235 2.05v2.891l-.158 1.025-.226 1.768-.308 1.59-.48 1.44-.18.588-.336.707-.28.493-.375.607-.33.383-.42.494-.375.4-.401.34-.48.207-.432.207-.355.114h-.543l-.346-.114-.66-.32-.302-.212-.317-.223-.347-.304-.35-.342-.579-.63-.684-.89-.539-.917-.538-.734-.526-.855-.741-1.517-.833-1.579-.098-.055h-.138l-.338.247-.196.415-.326.516-.567 1.533-.856 2.182-1.096 2.626-.824 1.308-.864 1.366-1.027 1.536-1.09 1.503-.557.68-.676.743-1.555 1.497.136.135.21.214.777.446 3.235 1.524 1.41.779 1.347.756 1.332.953 1.187.982.574.443.432.511.445.593.367.643.198.533.242.64.105.554.115.647-.115.433v.44l-.105.454-.242.415-.092.325-.22.394-.587.784-.543.627-.42.47-.35.348-.893.638-1.01.556-1.077.532-1.155.511-1.287.495-.693.207-.608.167-1.496.342-1.545.325-1.552.323-1.689.27-1.74.072-1.785.21h-5.539l-1.998-.114-1.86-.168-2.005-.27-1.99-.209-2.095-.286-2.03-.495-1.981-.374-1.968-.552-2.019-.707-1.98-.585-1.044-.342-.927-.323-.586-.223-.582-.12h-1.647l-1.904-.131-.962-.096-1.24-.135-.795.705-1.085.665-1.471.701-1.628.875-.99.475-1.033.376-2.281.914-1.24.305-1.3.343-1.803.344-1.13.086-1.193.1-1.246.135-1.45.053h-5.926l-3.346-.053-3.25-.321-1.644-.23-1.589-.23-1.546-.227-1.547-.305-1.442-.456-1.434-.325-1.294-.51-1.223-.474-1.142-.533-.99-.583-.984-.71-.336-.343-.44-.415-.334-.362-.3-.417-.278-.415-.215-.42-.311-.89-.109-.46-.138-.51v-.473l.138-.533v-.53l.109-.53v-1.069l.052-.564.259-.647.215-.646.39-.779.286-.3.236-.348.615-.738.49-.38.464-.266.428-.338.676-.21.543-.324.676-.341.77-.227.775-.231.897-.192.85-.11 1.008-.13 1.093-.081.284-.092h.063l.137-.115v-.13l-.2-.266-.58-.27-1.45-1.231-.975-.761-1.127-.967-1.136-1.082-1.181-1.382-1.36-1.558-.508-.843-.672-.87-.58-1.007-.522-1.1-.704-1.047-.459-1.194-.547-1.192-.546-1.33-.397-1.273-.378-1.575-.112-.057h-.115l-.059-.113h-.14l-.23.113-.114.057-.158.264-.057.321-.119.286-.206.477-.664 1.157-.345.701-.546.612-.58.736-.641.816-.677.724-.795.701-.734.658-.814.524-.89.546-.855.325-1.008.247-.99.095h-.233l-.228-.095-.18-.384-.29-.188-.38-.912-.237-.493-.255-.707-.21-.734-.113-.724-.313-1.648-.12-.972v-3.185l.12-2.379.196-1.214.23-1.252.21-1.347.374-1.254.42-1.443.431-1.407.578-1.448.545-1.38.754-1.4.699-1.52.855-1.425 1.006-1.538 1.023-1.382 1.069-1.538.891-1.071 1.142-1.227 1.202-1.237.56-.59.678-.662.985-.836 1.012-.853 1.647-1.446 1.242-.889z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/question.svg b/src/assets/icons/svg/question.svg
new file mode 100644
index 0000000..cf75bd4
--- /dev/null
+++ b/src/assets/icons/svg/question.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1581238842264" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1409" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M512 0C229.233778 0 0 229.233778 0 512s229.233778 512 512 512 512-229.233778 512-512A512 512 0 0 0 512 0z m0 938.666667C276.366222 938.666667 85.333333 747.633778 85.333333 512 85.333333 276.366222 276.366222 85.333333 512 85.333333c235.633778 0 426.666667 191.032889 426.666667 426.666667a426.666667 426.666667 0 0 1-426.666667 426.666667z m0-717.653334a170.666667 170.666667 0 0 0-170.666667 170.666667 42.666667 42.666667 0 0 0 85.333334 0 85.333333 85.333333 0 1 1 85.333333 85.333333 42.666667 42.666667 0 0 0-42.666667 42.666667v111.36a42.666667 42.666667 0 0 0 85.333334 0v-74.24A170.666667 170.666667 0 0 0 512 221.013333z m-42.666667 542.293334a42.666667 42.666667 0 1 0 85.333334 0 42.666667 42.666667 0 0 0-85.333334 0z" p-id="1410"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/radio.svg b/src/assets/icons/svg/radio.svg
new file mode 100644
index 0000000..0cde345
--- /dev/null
+++ b/src/assets/icons/svg/radio.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1575966775973" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="879" xmlns:xlink="http://www.w3.org/1999/xlink" width="81" height="81"><defs><style type="text/css"></style></defs><path d="M507.39346659 71.84873358c241.53533667 0 437.39770766 195.85422109 437.39770767 437.37442191 0 241.53766571-195.86237099 437.38955776-437.39770767 437.38955776-241.50040803 0-437.34997219-195.85189205-437.34997219-437.38955776C70.0434944 267.70295467 265.89189347 71.84873358 507.39346659 71.84873358L507.39346659 71.84873358zM507.39346659 282.81899805c-125.00686734 0-226.37039389 101.38914133-226.37039388 226.41813048 0 125.01268821 101.36352768 226.39717262 226.37039388 226.39717262 125.04295993 0 226.42395136-101.38448441 226.42395136-226.39717262C733.81625401 384.20813938 632.43642653 282.81899805 507.39346659 282.81899805L507.39346659 282.81899805zM507.39346659 120.78172615c-214.46664192 0-388.42047261 173.95150279-388.4204726 388.44026539 0 214.51204949 173.95499463 388.46122325 388.4204726 388.46122325 214.52369237 0 388.46005817-173.94800981 388.46005818-388.46122325C895.85236082 294.73322894 721.91715897 120.78172615 507.39346659 120.78172615z" p-id="880"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/rate.svg b/src/assets/icons/svg/rate.svg
new file mode 100644
index 0000000..aa3b14d
--- /dev/null
+++ b/src/assets/icons/svg/rate.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1577246781606" class="icon" viewBox="0 0 1069 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1098" xmlns:xlink="http://www.w3.org/1999/xlink" width="84.5595703125" height="81"><defs><style type="text/css"></style></defs><path d="M633.72929961 378.02038203l9.49872568 18.68789795 20.78025469 2.79745225 206.61592412 27.33248408a11.46496817 11.46496817 0 0 1 6.6095543 19.47324902l-147.2675168 147.35350284-14.89299345 14.89299345 3.8006376 20.68280244 37.84585956 204.89044571a11.46496817 11.46496817 0 0 1-16.4808914 12.2961788L554.68980898 751.84713388l-18.68789794-9.49299345-18.48726123 9.99171915-183.23885392 99.34968163a11.46496817 11.46496817 0 0 1-16.78471347-11.8662416l32.5433127-205.79617881 3.29617793-20.78598692-15.19108243-14.49172002-151.03375839-143.48407587a11.46496817 11.46496817 0 0 1 6.09936328-19.63949062l205.79617881-32.63503185 20.78598691-3.2961788L428.87898125 380.72038203 518.59235674 192.64331182a11.46496817 11.46496817 0 0 1 20.56815264-0.26369385l94.56879023 185.63503183zM496.64840732 85.52038203l-121.75796162 254.98089229L95.76433145 384.76178369A34.3949045 34.3949045 0 0 0 77.46050938 443.66879023l204.87324901 194.66369385-44.16879023 279.1146498a34.3949045 34.3949045 0 0 0 50.36560489 35.61592325l248.4-134.67898038 251.84522285 128.27579591a34.3949045 34.3949045 0 0 0 49.43694287-36.89426777l-51.30573223-277.85350284 199.73120977-199.90891758a34.3949045 34.3949045 0 0 0-19.82866201-58.40827998l-280.11783428-37.03184736L558.32993633 84.71210205a34.3949045 34.3949045 0 0 0-61.68152901 0.80254775z" p-id="1099"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/redis-list.svg b/src/assets/icons/svg/redis-list.svg
new file mode 100644
index 0000000..98a15b2
--- /dev/null
+++ b/src/assets/icons/svg/redis-list.svg
@@ -0,0 +1,2 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1656035183065" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3395" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css">@font-face { font-family: feedback-iconfont; src: url("//at.alicdn.com/t/font_1031158_u69w8yhxdu.woff2?t=1630033759944") format("woff2"), url("//at.alicdn.com/t/font_1031158_u69w8yhxdu.woff?t=1630033759944") format("woff"), url("//at.alicdn.com/t/font_1031158_u69w8yhxdu.ttf?t=1630033759944") format("truetype"); }
+</style></defs><path d="M958.88 730.06H65.12c-18.28 0-33.12-14.82-33.12-33.12V68.91c0-18.29 14.83-33.12 33.12-33.12h893.77c18.28 0 33.12 14.82 33.12 33.12v628.03c-0.01 18.3-14.84 33.12-33.13 33.12zM98.23 663.83h827.53v-561.8H98.23v561.8z" p-id="3396"></path><path d="M512 954.55c-18.28 0-33.12-14.82-33.12-33.12V733.92c0-18.29 14.83-33.12 33.12-33.12s33.12 14.82 33.12 33.12v187.51c0 18.3-14.84 33.12-33.12 33.12z" p-id="3397"></path><path d="M762.01 988.21H261.99c-18.28 0-33.12-14.82-33.12-33.12 0-18.29 14.83-33.12 33.12-33.12h500.03c18.28 0 33.12 14.82 33.12 33.12-0.01 18.29-14.84 33.12-33.13 33.12zM514.74 578.55c-21.63 0-43.31-3.87-64.21-11.65-45.95-17.13-82.49-51.13-102.86-95.74-5.07-11.08-0.19-24.19 10.89-29.26 11.08-5.09 24.19-0.18 29.26 10.91 15.5 33.88 43.25 59.7 78.14 72.71 34.93 12.99 72.79 11.64 106.66-3.85 33.22-15.17 58.8-42.26 72.03-76.3 4.42-11.37 17.21-17.01 28.57-12.58 11.36 4.42 16.99 17.22 12.57 28.58-17.42 44.82-51.1 80.5-94.82 100.47-24.34 11.12-50.25 16.71-76.23 16.71z" p-id="3398"></path><path d="M325.27 528.78c-1.66 0-3.34-0.18-5.02-0.57-11.88-2.77-19.28-14.63-16.49-26.51l18.84-81c1.34-5.82 5-10.84 10.13-13.92 5.09-3.09 11.3-3.96 17.03-2.41l80.51 21.43c11.79 3.14 18.8 15.23 15.67 27.02-3.15 11.79-15.42 18.75-27.02 15.65l-58.49-15.57-13.69 58.81c-2.37 10.2-11.45 17.07-21.47 17.07zM360.8 351.01c-2.65 0-5.37-0.49-8-1.51-11.36-4.41-16.99-17.21-12.59-28.57 17.4-44.79 51.06-80.47 94.8-100.48 92.15-42.06 201.25-1.39 243.31 90.68 5.07 11.08 0.19 24.19-10.89 29.26-11.13 5.07-24.19 0.17-29.26-10.91-31.97-69.91-114.9-100.82-184.79-68.86-33.22 15.19-58.8 42.28-71.99 76.29-3.41 8.74-11.75 14.1-20.59 14.1z" p-id="3399"></path><path d="M684.68 376.74c-1.47 0-2.95-0.15-4.42-0.44l-81.61-16.68c-11.94-2.45-19.64-14.11-17.21-26.06 2.44-11.96 14.1-19.64 26.04-17.22l59.29 12.12 10.23-59.5c2.05-12 13.52-20.19 25.48-18.01 12.03 2.06 20.09 13.48 18.02 25.5l-14.08 81.96a22.089 22.089 0 0 1-9.29 14.49c-3.7 2.51-8.03 3.84-12.45 3.84z" p-id="3400"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/redis.svg b/src/assets/icons/svg/redis.svg
new file mode 100644
index 0000000..2f1d62d
--- /dev/null
+++ b/src/assets/icons/svg/redis.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1605865043777" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="856" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M1023.786667 611.84c-0.426667 9.770667-13.354667 20.693333-39.893334 34.56-54.613333 28.458667-337.749333 144.896-397.994666 176.298667-60.288 31.402667-93.738667 31.104-141.354667 8.32-47.616-22.741333-348.842667-144.469333-403.114667-170.368-27.093333-12.970667-40.917333-23.893333-41.386666-34.218667v103.509333c0 10.325333 14.250667 21.290667 41.386666 34.261334 54.272 25.941333 355.541333 147.626667 403.114667 170.368 47.616 22.784 81.066667 23.082667 141.354667-8.362667 60.245333-31.402667 343.338667-147.797333 397.994666-176.298667 27.776-14.464 40.106667-25.728 40.106667-35.925333v-102.058667l-0.213333-0.085333z m0-168.746667c-0.512 9.770667-13.397333 20.650667-39.893334 34.517334-54.613333 28.458667-337.749333 144.896-397.994666 176.298666-60.288 31.402667-93.738667 31.104-141.354667 8.362667-47.616-22.741333-348.842667-144.469333-403.114667-170.410667-27.093333-12.928-40.917333-23.893333-41.386666-34.176v103.509334c0 10.325333 14.250667 21.248 41.386666 34.218666 54.272 25.941333 355.498667 147.626667 403.114667 170.368 47.616 22.784 81.066667 23.082667 141.354667-8.32 60.245333-31.402667 343.338667-147.84 397.994666-176.298666 27.776-14.506667 40.106667-25.770667 40.106667-35.968v-102.058667l-0.256-0.042667z m0-175.018666c0.469333-10.410667-13.141333-19.541333-40.533334-29.610667-53.248-19.498667-334.634667-131.498667-388.522666-151.253333-53.888-19.712-75.818667-18.901333-139.093334 3.84C392.234667 113.706667 92.629333 231.253333 39.338667 252.074667c-26.666667 10.496-39.68 20.181333-39.253334 30.506666V386.133333c0 10.325333 14.250667 21.248 41.386667 34.218667 54.272 25.941333 355.498667 147.669333 403.114667 170.410667 47.616 22.741333 81.066667 23.04 141.354666-8.362667 60.245333-31.402667 343.338667-147.84 397.994667-176.298667 27.776-14.506667 40.106667-25.770667 40.106667-35.968V268.074667h-0.341334zM366.677333 366.08l237.269334-36.437333-71.68 105.088-165.546667-68.650667z m524.8-94.634667l-140.330666 55.466667-15.232 5.973333-140.245334-55.466666 155.392-61.44 140.373334 55.466666z m-411.989333-101.674666l-22.954667-42.325334 71.594667 27.989334 67.498667-22.101334-18.261334 43.733334 68.778667 25.770666-88.704 9.216-19.882667 47.786667-32.085333-53.290667-102.4-9.216 76.416-27.562666z m-176.768 59.733333c70.058667 0 126.805333 21.973333 126.805333 49.109333s-56.746667 49.152-126.805333 49.152-126.848-22.058667-126.848-49.152c0-27.136 56.789333-49.152 126.848-49.152z" p-id="857"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/row.svg b/src/assets/icons/svg/row.svg
new file mode 100644
index 0000000..0780992
--- /dev/null
+++ b/src/assets/icons/svg/row.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1579339929870" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1182" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M152 854.856875h325.7146875V237.715625H134.856875v600q0 6.99375 5.0746875 12.0684375T152 854.856875z m737.143125-17.1421875v-600H546.284375v617.1421875H872q6.99375 0 12.0684375-5.07375t5.0746875-12.0684375z m68.5715625-651.429375V837.715625q0 35.3821875-25.16625 60.5484375T872 923.4284375H152q-35.383125 0-60.5484375-25.1653125T66.284375 837.7146875V186.284375q0-35.3821875 25.16625-60.5484375T152 100.5715625h720q35.383125 0 60.5484375 25.1653125t25.16625 60.5484375z" p-id="1183"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/search.svg b/src/assets/icons/svg/search.svg
new file mode 100644
index 0000000..84233dd
--- /dev/null
+++ b/src/assets/icons/svg/search.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M124.884 109.812L94.256 79.166c-.357-.357-.757-.629-1.129-.914a50.366 50.366 0 0 0 8.186-27.59C101.327 22.689 78.656 0 50.67 0 22.685 0 0 22.688 0 50.663c0 27.989 22.685 50.663 50.656 50.663 10.186 0 19.643-3.03 27.6-8.201.286.385.557.771.9 1.114l30.628 30.632a10.633 10.633 0 0 0 7.543 3.129c2.728 0 5.457-1.043 7.543-3.115 4.171-4.157 4.171-10.915.014-15.073M50.671 85.338C31.557 85.338 16 69.78 16 50.663c0-19.102 15.557-34.661 34.67-34.661 19.115 0 34.657 15.559 34.657 34.675 0 19.102-15.557 34.661-34.656 34.661"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/select.svg b/src/assets/icons/svg/select.svg
new file mode 100644
index 0000000..d628382
--- /dev/null
+++ b/src/assets/icons/svg/select.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1575803481213" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="804" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M62 511.97954521C62 263.86590869 263.90681826 62 511.97954521 62s449.97954521 201.825 449.97954521 449.97954521c0 248.19545479-201.90681826 449.97954521-449.97954521 449.97954521C263.90681826 962 62 760.175 62 511.97954521M901.98636348 511.97954521c0-215.24318174-175.00909131-390.41590869-390.00681827-390.41590869-215.03863652 0-389.96590869 175.17272695-389.96590868 390.41590869 0 215.28409131 175.00909131 390.45681826 389.96590868 390.45681826C727.01818174 902.47727305 901.98636348 727.30454521 901.98636348 511.97954521M264.17272695 430.28409131c0-5.76818174 2.12727305-11.51590869 6.64772696-15.87272696 8.71363652-8.75454521 22.88863652-8.75454521 31.725 0l209.4340913 208.22727305L721.45454521 414.53409131c8.75454521-8.71363652 22.97045479-8.71363652 31.90909132 0 8.71363652 8.75454521 8.71363652 22.88863652 0 31.60227304L511.97954521 685.74090869 270.71818174 446.01363653C266.27954521 441.77954521 264.17272695 436.05227305 264.17272695 430.28409131" p-id="805"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/server.svg b/src/assets/icons/svg/server.svg
new file mode 100644
index 0000000..eb287e3
--- /dev/null
+++ b/src/assets/icons/svg/server.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1547360688278" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6717" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M890 120H134a70 70 0 0 0-70 70v500a70 70 0 0 0 70 70h756a70 70 0 0 0 70-70V190a70 70 0 0 0-70-70z m-10 520a40 40 0 0 1-40 40H712V448a40 40 0 0 0-80 0v232h-80V368a40 40 0 0 0-80 0v312h-80V512a40 40 0 0 0-80 0v168H184a40 40 0 0 1-40-40V240a40 40 0 0 1 40-40h656a40 40 0 0 1 40 40zM696 824H328a40 40 0 0 0 0 80h368a40 40 0 0 0 0-80z" p-id="6718"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/shopping.svg b/src/assets/icons/svg/shopping.svg
new file mode 100644
index 0000000..87513e7
--- /dev/null
+++ b/src/assets/icons/svg/shopping.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M42.913 101.36c1.642 0 3.198.332 4.667.996a12.28 12.28 0 0 1 3.89 2.772c1.123 1.184 1.987 2.582 2.592 4.193.605 1.612.908 3.318.908 5.118 0 1.8-.303 3.507-.908 5.118-.605 1.611-1.469 3.01-2.593 4.194a13.3 13.3 0 0 1-3.889 2.843 10.582 10.582 0 0 1-4.667 1.066c-1.729 0-3.306-.355-4.732-1.066a13.604 13.604 0 0 1-3.825-2.843c-1.123-1.185-1.988-2.583-2.593-4.194a14.437 14.437 0 0 1-.907-5.118c0-1.8.302-3.506.907-5.118.605-1.61 1.47-3.009 2.593-4.193a12.515 12.515 0 0 1 3.825-2.772c1.426-.664 3.003-.996 4.732-.996zm53.932.285c1.643 0 3.22.331 4.733.995a11.386 11.386 0 0 1 3.889 2.772c1.08 1.185 1.945 2.583 2.593 4.194.648 1.61.972 3.317.972 5.118 0 1.8-.324 3.506-.972 5.117-.648 1.611-1.513 3.01-2.593 4.194a12.253 12.253 0 0 1-3.89 2.843 11 11 0 0 1-4.732 1.066 10.58 10.58 0 0 1-4.667-1.066 12.478 12.478 0 0 1-3.824-2.843c-1.08-1.185-1.945-2.583-2.593-4.194a13.581 13.581 0 0 1-.973-5.117c0-1.801.325-3.507.973-5.118.648-1.611 1.512-3.01 2.593-4.194a11.559 11.559 0 0 1 3.824-2.772 11.212 11.212 0 0 1 4.667-.995zm21.781-80.747c2.42 0 4.3.355 5.64 1.066 1.34.71 2.29 1.587 2.852 2.63a6.427 6.427 0 0 1 .778 3.34c-.044 1.185-.195 2.204-.454 3.057-.26.853-.8 2.606-1.62 5.26a589.268 589.268 0 0 1-2.788 8.743 1236.373 1236.373 0 0 0-3.047 9.453c-.994 3.128-1.75 5.592-2.269 7.393-1.123 3.79-2.55 6.42-4.278 7.89-1.728 1.469-3.846 2.203-6.352 2.203H39.023l1.945 12.795h65.342c4.148 0 6.223 1.943 6.223 5.828 0 1.896-.41 3.53-1.232 4.905-.821 1.374-2.442 2.061-4.862 2.061H38.505c-1.729 0-3.176-.426-4.343-1.28-1.167-.852-2.14-1.966-2.917-3.34a21.277 21.277 0 0 1-1.88-4.478 44.128 44.128 0 0 1-1.102-4.55c-.087-.568-.324-1.942-.713-4.122-.39-2.18-.865-4.904-1.426-8.174l-1.88-10.947c-.692-4.027-1.383-8.079-2.075-12.154-1.642-9.572-3.5-20.234-5.574-31.986H6.87c-1.296 0-2.377-.356-3.24-1.067a9.024 9.024 0 0 1-2.14-2.558 10.416 10.416 0 0 1-1.167-3.2C.108 8.53 0 7.488 0 6.54c0-1.896.583-3.46 1.75-4.69C2.917.615 4.494 0 6.482 0h13.095c1.728 0 3.111.284 4.148.853 1.037.569 1.858 1.28 2.463 2.132a8.548 8.548 0 0 1 1.297 2.701c.26.948.475 1.754.648 2.417.173.758.346 1.825.519 3.199.173 1.374.345 2.772.518 4.193.26 1.706.519 3.507.778 5.403h88.678z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/size.svg b/src/assets/icons/svg/size.svg
new file mode 100644
index 0000000..ddb25b8
--- /dev/null
+++ b/src/assets/icons/svg/size.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M0 54.857h54.796v18.286H36.531V128H18.265V73.143H0V54.857zm127.857-36.571H91.935V128H72.456V18.286H36.534V0h91.326l-.003 18.286z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/skill.svg b/src/assets/icons/svg/skill.svg
new file mode 100644
index 0000000..a3b7312
--- /dev/null
+++ b/src/assets/icons/svg/skill.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M31.652 93.206h33.401c1.44 2.418 3.077 4.663 4.93 6.692h-38.33v-6.692zm0-10.586h28.914a44.8 44.8 0 0 1-1.264-6.688h-27.65v6.688zm0-17.27H59.39c.288-2.286.714-4.532 1.34-6.687H31.65v6.687h.003zm53.913 44.84v5.85c0 2.798-2.095 5.075-4.667 5.075h-70.07c-2.576 0-4.663-2.277-4.663-5.075V31.26l23.22-20.96v22.25H17.16v6.688h18.39V6.688h45.348c2.576 0 4.667 2.277 4.667 5.066v20.009c1.987-.675 4.053-1.128 6.17-1.445v-18.56C91.738 5.28 86.874 0 80.902 0H31.15L0 28.118v87.917c0 6.48 4.859 11.759 10.832 11.759h70.07c5.974 0 10.837-5.27 10.837-11.759v-4.41c-2.117-.312-4.183-.765-6.17-1.435h-.004zM23.279 58.667h-7.96v6.688h7.96v-6.688zm-7.956 41.23h7.96v-6.691h-7.96v6.692zm7.956-23.96h-7.96v6.687h7.96v-6.688zm89.718-15.042l-4.896-4.07-12.447 17.613-11.19-9.305-3.762 5.311 16.091 13.38 16.204-22.929zM128 70.978c0-18.632-13.97-33.782-31.147-33.782-17.168 0-31.135 15.155-31.135 33.782 0 18.628 13.97 33.783 31.135 33.783 17.172 0 31.143-15.15 31.143-33.783H128zm-6.17 0c0 14.933-11.203 27.1-24.981 27.1-13.77 0-24.987-12.158-24.987-27.1 0-14.941 11.195-27.099 24.987-27.099 13.778 0 24.982 12.158 24.982 27.1z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/slider.svg b/src/assets/icons/svg/slider.svg
new file mode 100644
index 0000000..fbe4f39
--- /dev/null
+++ b/src/assets/icons/svg/slider.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1577185310368" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1238" xmlns:xlink="http://www.w3.org/1999/xlink" width="81" height="81"><defs><style type="text/css"></style></defs><path d="M951.453125 476.84375H523.671875a131.8359375 131.8359375 0 0 0-254.1796875 0H72.546875v70.3125h196.9453125a131.8359375 131.8359375 0 0 0 254.1796875 0H951.453125z" p-id="1239"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/star.svg b/src/assets/icons/svg/star.svg
new file mode 100644
index 0000000..6cf86e6
--- /dev/null
+++ b/src/assets/icons/svg/star.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M70.66 4.328l14.01 29.693c1.088 2.29 3.177 3.882 5.603 4.25l31.347 4.76c6.087.926 8.528 8.756 4.117 13.247L103.05 79.395c-1.75 1.78-2.544 4.352-2.132 6.867l5.352 32.641c1.043 6.337-5.33 11.182-10.778 8.19l-28.039-15.409a7.13 7.13 0 0 0-6.91 0l-28.039 15.41c-5.448 2.99-11.821-1.854-10.777-8.19l5.352-32.642c.415-2.515-.387-5.088-2.136-6.867L2.264 56.278C-2.146 51.787.286 43.957 6.38 43.031l31.343-4.76c2.419-.368 4.51-1.96 5.595-4.25L57.334 4.328c2.728-5.77 10.605-5.77 13.325 0z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/swagger.svg b/src/assets/icons/svg/swagger.svg
new file mode 100644
index 0000000..05d4e7b
--- /dev/null
+++ b/src/assets/icons/svg/swagger.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1566036776944" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6463" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M64 223.995345h168.001164v47.997673c0 26.428509 18.878836 47.997673 41.984 47.997673h140.036654c23.095855 0 41.984-21.569164 41.984-47.997673v-47.997673h504.003491a32.004655 32.004655 0 0 0 0-64.009309H455.996509V111.988364c0-26.428509-18.878836-47.997673-41.984-47.997673H273.985164c-23.095855 0-41.984 21.569164-41.984 47.997673v47.997672H64a32.004655 32.004655 0 0 0 0 64.009309zM288.004655 128h111.997672V256H288.004655V128zM960 479.995345H791.998836v-47.997672c0-26.372655-18.878836-47.997673-41.984-47.997673H609.978182c-23.095855 0-41.984 21.634327-41.984 47.997673v47.997672H64a32.004655 32.004655 0 0 0 0 64.00931h504.003491v47.997672c0 26.363345 18.878836 47.997673 41.984 47.997673h140.036654c23.095855 0 41.984-21.634327 41.984-47.997673v-47.997672h168.001164a32.004655 32.004655 0 1 0-0.009309-64.00931zM735.995345 576H623.997673v-128h111.997672v128zM960 800.293236v-0.288581H455.996509v-47.997673c0-26.363345-18.878836-47.997673-41.984-47.997673H274.050327c-23.105164 0-41.984 21.634327-41.984 47.997673v47.997673H64v0.288581a32.004655 32.004655 0 0 0 0 64.009309c0.986764 0 1.917673-0.195491 2.885818-0.288581h165.115346v47.997672c0 26.363345 18.878836 47.997673 41.984 47.997673h140.036654c23.095855 0 41.984-21.634327 41.984-47.997673v-47.997672h501.108364c0.968145 0.093091 1.899055 0.288582 2.895127 0.288581a32.004655 32.004655 0 1 0-0.009309-64.009309zM400.002327 896H288.004655V768h111.997672v128z" fill="" p-id="6464"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/switch.svg b/src/assets/icons/svg/switch.svg
new file mode 100644
index 0000000..0ba61e3
--- /dev/null
+++ b/src/assets/icons/svg/switch.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1576042673958" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1110" xmlns:xlink="http://www.w3.org/1999/xlink" width="81" height="81"><defs><style type="text/css"></style></defs><path d="M692 792H332c-150 0-270-120-270-270s120-270 270-270h360c150 0 270 120 270 270 0 147-120 270-270 270zM332 312c-117 0-210 93-210 210s93 210 210 210h360c117 0 210-93 210-210s-93-210-210-210H332z" p-id="1111"></path><path d="M341 522m-150 0a150 150 0 1 0 300 0 150 150 0 1 0-300 0Z" p-id="1112"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/system.svg b/src/assets/icons/svg/system.svg
new file mode 100644
index 0000000..5992593
--- /dev/null
+++ b/src/assets/icons/svg/system.svg
@@ -0,0 +1,2 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1543827724451" class="icon" style="" viewBox="0 0 1084 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="10233" xmlns:xlink="http://www.w3.org/1999/xlink" width="211.71875" height="200"><defs><style type="text/css">@font-face { font-family: rbicon; src: url("chrome-extension://dipiagiiohfljcicegpgffpbnjmgjcnf/fonts/rbicon.woff2") format("woff2"); font-weight: normal; font-style: normal; }
+</style></defs><path d="M1080.09609 434.500756c-4.216302-23.731757-26.9241-47.945376-50.595623-53.185637l-17.648235-4.095836a175.940257 175.940257 0 0 1-101.612877-80.832531 177.807476 177.807476 0 0 1-18.732427-129.801867l5.541425-16.684509c7.10748-23.129428-2.108151-54.992624-20.599646-70.833873 0 0-16.624276-14.094495-63.244529-41.199293-46.800951-26.984332-66.858502-34.513443-66.858502-34.513443-22.76803-8.372371-54.631227-0.361397-71.255503 17.407304l-12.287509 13.251234a173.470708 173.470708 0 0 1-120.465769 48.065842A174.13327 174.13327 0 0 1 421.329029 33.590675L409.583617 20.761071C393.140039 2.99237 361.096144-4.898138 338.267881 3.353767c0 0-20.358715 7.529111-67.099434 34.513443-46.800951 27.34573-63.244529 41.440225-63.244529 41.440225-18.431263 15.66055-27.646894 47.222582-20.539413 70.592941l5.059562 16.865207a178.048407 178.048407 0 0 1-18.672194 129.621169 174.916297 174.916297 0 0 1-102.275439 81.073463l-17.045906 3.854904c-23.310126 5.42096-46.258856 29.333415-50.595623 53.185637 0 0-3.854905 21.382674-3.854905 75.712737 0 54.330062 3.854905 75.712736 3.854905 75.712736 4.216302 23.972688 26.9241 47.945376 50.595623 53.185637l16.624276 3.854905a174.253736 174.253736 0 0 1 102.395904 81.314394c23.310126 40.837896 28.911785 87.337683 18.732427 129.801867l-4.81863 16.443578c-7.10748 23.129428 2.108151 54.992624 20.599646 70.833872 0 0 16.624276 14.094495 63.244529 41.199293 46.800951 27.104798 66.918735 34.513443 66.918735 34.513443 22.707798 8.372371 54.631227 0.361397 71.255503-17.407303l11.624947-12.588673a175.096996 175.096996 0 0 1 242.256662 0.120465l11.624947 12.648906c16.383345 17.708468 48.427239 25.598976 71.255503 17.347071 0 0 20.358715-7.529111 67.159666-34.513443 46.740719-27.104798 63.124063-41.199293 63.124064-41.199293 18.491496-15.600317 27.707127-47.463513 20.599646-70.833873l-5.059562-17.106139a176.723284 176.723284 0 0 1 18.672194-129.139305 176.060722 176.060722 0 0 1 102.395904-81.314394l16.68451-3.854905c23.310126-5.42096 46.258856-29.333415 50.595623-53.185637 0 0 3.854905-21.382674 3.854904-75.712737-0.240932-54.330062-4.095836-75.833202-4.095836-75.833202z m-537.819428 293.334149c-119.261112 0-216.175824-97.336342-216.175824-217.621412a216.657687 216.657687 0 0 1 216.236057-217.320249c119.200879 0 216.115591 97.276109 216.11559 217.56118-0.240932 120.044139-96.974945 217.320248-216.175823 217.320249z" p-id="10234"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/tab.svg b/src/assets/icons/svg/tab.svg
new file mode 100644
index 0000000..b4b48e4
--- /dev/null
+++ b/src/assets/icons/svg/tab.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M78.921.052H49.08c-1.865 0-3.198 1.599-3.198 3.464v6.661c0 1.865 1.6 3.464 3.198 3.464h29.84c1.865 0 3.198-1.599 3.198-3.464V3.516C82.385 1.65 80.786.052 78.92.052zm45.563 0H94.642c-1.865 0-3.464 1.599-3.464 3.464v6.661c0 1.865 1.599 3.464 3.464 3.464h29.842c1.865-.266 3.464-1.599 3.464-3.464V3.516c0-1.865-1.599-3.464-3.464-3.464zm0 22.382H40.02c-1.866 0-3.464-1.599-3.464-3.464V3.516c0-1.865-1.599-3.464-3.464-3.464H3.516C1.65.052.052 1.651.052 3.516V124.75c0 1.598 1.599 3.197 3.464 3.197h120.968c1.865 0 3.464-1.599 3.464-3.464V25.898c0-1.865-1.599-3.464-3.464-3.464z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/table.svg b/src/assets/icons/svg/table.svg
new file mode 100644
index 0000000..0e3dc9d
--- /dev/null
+++ b/src/assets/icons/svg/table.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M.006.064h127.988v31.104H.006V.064zm0 38.016h38.396v41.472H.006V38.08zm0 48.384h38.396v41.472H.006V86.464zM44.802 38.08h38.396v41.472H44.802V38.08zm0 48.384h38.396v41.472H44.802V86.464zM89.598 38.08h38.396v41.472H89.598zm0 48.384h38.396v41.472H89.598z"/><path d="M.006.064h127.988v31.104H.006V.064zm0 38.016h38.396v41.472H.006V38.08zm0 48.384h38.396v41.472H.006V86.464zM44.802 38.08h38.396v41.472H44.802V38.08zm0 48.384h38.396v41.472H44.802V86.464zM89.598 38.08h38.396v41.472H89.598zm0 48.384h38.396v41.472H89.598z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/textarea.svg b/src/assets/icons/svg/textarea.svg
new file mode 100644
index 0000000..2709f29
--- /dev/null
+++ b/src/assets/icons/svg/textarea.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1575802855098" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2984" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M896 160H128c-35.2 0-64 28.8-64 64v576c0 35.2 28.8 64 64 64h768c35.2 0 64-28.8 64-64V224c0-35.2-28.8-64-64-64z m0 608c0 16-12.8 32-32 32H160c-19.2 0-32-12.8-32-32V256c0-16 12.8-32 32-32h704c19.2 0 32 12.8 32 32v512z" p-id="2985"></path><path d="M224 288c-19.2 0-32 12.8-32 32v256c0 16 12.8 32 32 32s32-12.8 32-32V320c0-16-12.8-32-32-32z m608 480c19.2 0 32-12.8 32-32V608L704 768h128z" p-id="2986"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/theme.svg b/src/assets/icons/svg/theme.svg
new file mode 100644
index 0000000..5982a2f
--- /dev/null
+++ b/src/assets/icons/svg/theme.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M125.5 36.984L95.336 2.83C93.735 1.018 91.565 0 89.3 0c-2.263 0-4.433 1.018-6.033 2.83l-3.786 4.286c-1.6 1.812-3.77 2.83-6.032 2.831H54.553c-2.263 0-4.434-1.018-6.033-2.83L44.734 2.83C43.134 1.018 40.964 0 38.701 0c-2.263 0-4.434 1.018-6.034 2.83L2.5 36.984C.9 38.796 0 41.254 0 43.815c0 2.562.899 5.02 2.5 6.831L14.565 64.31c2.178 2.468 5.367 3.403 8.33 2.444 1.35-.435 2.709.592 2.709 2.18v49.407c0 5.313 3.84 9.66 8.532 9.66h59.726c4.693 0 8.532-4.347 8.532-9.66V68.934c0-1.59 1.36-2.616 2.71-2.181 2.962.96 6.15.024 8.329-2.444L125.5 50.646c1.6-1.811 2.499-4.269 2.499-6.83 0-2.563-.899-5.02-2.5-6.832z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/time-range.svg b/src/assets/icons/svg/time-range.svg
new file mode 100644
index 0000000..13c1202
--- /dev/null
+++ b/src/assets/icons/svg/time-range.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1579774825624" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1248" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M498.595712 482.290351 345.420077 482.290351l0 57.307194 210.477712 0L555.897789 274.196942l-57.301054 0L498.596735 482.290351zM498.595712 482.290351" p-id="1249"></path><path d="M577.685002 644.98478l379.879913 0 0 57.302077L577.685002 702.286858 577.685002 644.98478 577.685002 644.98478zM577.685002 644.98478" p-id="1250"></path><path d="M577.685002 773.764795l379.879913 0 0 57.307194L577.685002 831.071989 577.685002 773.764795 577.685002 773.764795zM577.685002 773.764795" p-id="1251"></path><path d="M577.685002 902.549927l379.879913 0 0 57.307194L577.685002 959.857121 577.685002 902.549927 577.685002 902.549927zM577.685002 902.549927" p-id="1252"></path><path d="M102.523001 382.290823c4.450359 2.615571 9.470699 3.954055 14.530948 3.954055 2.969635 0 5.952572-0.461511 8.836249-1.394766l190.809767-61.886489c15.052834-4.882194 23.297612-21.040199 18.415418-36.08894-4.882194-15.052834-21.040199-23.297612-36.093033-18.415418L175.676092 308.458257c15.994276-26.115797 35.170011-50.537 57.370639-72.743768 73.767074-73.767074 171.845857-114.388237 276.16783-114.388237 104.32095 0 202.39564 40.622186 276.16169 114.388237s114.393353 171.845857 114.393353 276.16783c0 26.427906-2.615571 52.449559-7.709589 77.780481l58.302871 0c4.464685-25.499767 6.708795-51.470255 6.708795-77.780481 0-60.449767-11.845793-119.102608-35.204803-174.336584-22.559808-53.334719-54.850236-101.226472-95.968725-142.349055-41.122583-41.122583-89.017406-73.408917-142.348032-95.968725C628.317169 75.866898 569.659211 64.021106 509.215584 64.021106c-60.448744 0-119.106702 11.845793-174.336584 35.207873-53.334719 22.559808-101.230566 54.846142-142.349055 95.968725-23.980157 23.980157-44.934398 50.278103-62.727647 78.601172l-20.738323-105.655342c-3.043313-15.527648-18.105357-25.642007-33.631982-22.599717-15.527648 3.048429-25.64303 18.105357-22.599717 33.637098l36.102243 183.932126C90.51348 371.153158 95.460142 378.13313 102.523001 382.290823L102.523001 382.290823zM102.523001 382.290823" p-id="1253"></path><path d="M126.020158 587.9416 67.768453 587.9416c5.759167 33.679054 15.368012 66.544579 28.789697 98.278327 22.559808 53.333696 54.850236 101.225449 95.971795 142.348032 41.122583 41.122583 89.014336 73.408917 142.349055 95.968725 54.112432 22.88829 111.517863 34.71157 170.668031 35.18229L505.547031 902.395408c-102.94972-0.941442-199.594851-41.445948-272.499277-114.349351C177.545672 732.543975 140.810003 663.275355 126.020158 587.9416L126.020158 587.9416zM126.020158 587.9416" p-id="1254"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/time.svg b/src/assets/icons/svg/time.svg
new file mode 100644
index 0000000..b376e32
--- /dev/null
+++ b/src/assets/icons/svg/time.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1577099827399" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1008" xmlns:xlink="http://www.w3.org/1999/xlink" width="81" height="81"><defs><style type="text/css"></style></defs><path d="M520 559h204c17.673 0 32 14.327 32 32 0 17.673-14.327 32-32 32H488c-17.673 0-32-14.327-32-32 0-0.167 0.001-0.334 0.004-0.5a32.65 32.65 0 0 1-0.004-0.5V277c0-17.673 14.327-32 32-32 17.673 0 32 14.327 32 32v282z m-8 401C264.576 960 64 759.424 64 512S264.576 64 512 64s448 200.576 448 448-200.576 448-448 448z m0-64c212.077 0 384-171.923 384-384S724.077 128 512 128 128 299.923 128 512s171.923 384 384 384z" p-id="1009"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/tool.svg b/src/assets/icons/svg/tool.svg
new file mode 100644
index 0000000..48e0e35
--- /dev/null
+++ b/src/assets/icons/svg/tool.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1553828490559" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1684" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M898.831744 900.517641 103.816972 900.517641c-36.002982 0-65.363683-29.286-65.363683-65.313541l0-554.949184c0-36.041868 29.361725-65.326844 65.363683-65.326844l795.015795 0c36.002982 0 65.198931 29.284977 65.198931 65.326844l0 554.949184C964.030675 871.231641 934.834726 900.517641 898.831744 900.517641L898.831744 900.517641zM103.816972 255.593236c-13.576203 0-24.711821 11.085476-24.711821 24.662703l0 554.949184c0 13.576203 11.136641 24.662703 24.711821 24.662703l795.015795 0c13.577227 0 24.547069-11.086499 24.547069-24.662703l0-554.949184c0-13.577227-10.970866-24.662703-24.547069-24.662703L103.816972 255.593236 103.816972 255.593236zM664.346245 251.774257c-11.161201 0-20.332071-9.080819-20.332071-20.332071l0-101.278661c0-13.576203-11.047614-24.623817-24.699542-24.623817L383.181611 105.539708c-13.576203 0-24.712845 11.04659-24.712845 24.623817l0 101.278661c0 11.252275-9.041934 20.332071-20.332071 20.332071-11.20111 0-20.319791-9.080819-20.319791-20.332071l0-101.278661c0-35.989679 29.323862-65.275679 65.364707-65.275679l236.133022 0c36.06745 0 65.402569 29.284977 65.402569 65.275679l0 101.278661C684.717202 242.694461 675.636383 251.774257 664.346245 251.774257L664.346245 251.774257zM413.233044 521.725502 75.694471 521.725502c-11.163247 0-20.333094-9.117658-20.333094-20.35663 0-11.252275 9.169847-20.332071 20.333094-20.332071l337.538573 0c11.277858 0 20.319791 9.080819 20.319791 20.332071C433.552835 512.607844 424.510902 521.725502 413.233044 521.725502L413.233044 521.725502zM912.894018 521.725502 575.367725 521.725502c-11.213389 0-20.332071-9.117658-20.332071-20.35663 0-11.252275 9.118682-20.332071 20.332071-20.332071l337.526293 0c11.290137 0 20.332071 9.080819 20.332071 20.332071C933.226089 512.607844 924.184155 521.725502 912.894018 521.725502L912.894018 521.725502zM557.56322 634.217552 445.085496 634.217552c-11.213389 0-20.332071-9.079796-20.332071-20.331048l0-168.763658c0-11.251252 9.118682-20.332071 20.332071-20.332071l112.478747 0c11.290137 0 20.370956 9.080819 20.370956 20.332071l0 168.763658C577.934177 625.137757 568.853357 634.217552 557.56322 634.217552L557.56322 634.217552zM465.417567 593.514525l71.827909 0L537.245476 465.454918l-71.827909 0L465.417567 593.514525 465.417567 593.514525z" p-id="1685"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/tree-table.svg b/src/assets/icons/svg/tree-table.svg
new file mode 100644
index 0000000..8aafdb8
--- /dev/null
+++ b/src/assets/icons/svg/tree-table.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M44.8 0h79.543C126.78 0 128 1.422 128 4.267v23.466c0 2.845-1.219 4.267-3.657 4.267H44.8c-2.438 0-3.657-1.422-3.657-4.267V4.267C41.143 1.422 42.362 0 44.8 0zm22.857 48h56.686c2.438 0 3.657 1.422 3.657 4.267v23.466c0 2.845-1.219 4.267-3.657 4.267H67.657C65.22 80 64 78.578 64 75.733V52.267C64 49.422 65.219 48 67.657 48zm0 48h56.686c2.438 0 3.657 1.422 3.657 4.267v23.466c0 2.845-1.219 4.267-3.657 4.267H67.657C65.22 128 64 126.578 64 123.733v-23.466C64 97.422 65.219 96 67.657 96zM50.286 68.267c2.02 0 3.657-1.91 3.657-4.267 0-2.356-1.638-4.267-3.657-4.267H17.37V32h6.4c2.02 0 3.658-1.91 3.658-4.267V4.267C27.429 1.91 25.79 0 23.77 0H3.657C1.637 0 0 1.91 0 4.267v23.466C0 30.09 1.637 32 3.657 32h6.4v80c0 2.356 1.638 4.267 3.657 4.267h36.572c2.02 0 3.657-1.91 3.657-4.267 0-2.356-1.638-4.267-3.657-4.267H17.37V68.267h32.915z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/tree.svg b/src/assets/icons/svg/tree.svg
new file mode 100644
index 0000000..dd4b7dd
--- /dev/null
+++ b/src/assets/icons/svg/tree.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M126.713 90.023c.858.985 1.287 2.134 1.287 3.447v29.553c0 1.423-.429 2.6-1.287 3.53-.858.93-1.907 1.395-3.146 1.395H97.824c-1.145 0-2.146-.465-3.004-1.395-.858-.93-1.287-2.107-1.287-3.53V93.47c0-.875.19-1.696.572-2.462.382-.766.906-1.368 1.573-1.806a3.84 3.84 0 0 1 2.146-.657h9.725V69.007a3.84 3.84 0 0 0-.43-1.806 3.569 3.569 0 0 0-1.143-1.313 2.714 2.714 0 0 0-1.573-.492h-36.47v23.149h9.725c1.144 0 2.145.492 3.004 1.478.858.985 1.287 2.134 1.287 3.447v29.553c0 .876-.191 1.696-.573 2.463-.38.766-.905 1.368-1.573 1.806a3.84 3.84 0 0 1-2.145.656H51.915a3.84 3.84 0 0 1-2.145-.656c-.668-.438-1.216-1.04-1.645-1.806a4.96 4.96 0 0 1-.644-2.463V93.47c0-1.313.43-2.462 1.288-3.447.858-.986 1.907-1.478 3.146-1.478h9.582v-23.15h-37.9c-.953 0-1.74.356-2.359 1.068-.62.711-.93 1.56-.93 2.544v19.538h9.726c1.239 0 2.264.492 3.074 1.478.81.985 1.216 2.134 1.216 3.447v29.553c0 1.423-.405 2.6-1.216 3.53-.81.93-1.835 1.395-3.074 1.395H4.29c-.476 0-.93-.082-1.358-.246a4.1 4.1 0 0 1-1.144-.657 4.658 4.658 0 0 1-.93-1.067 5.186 5.186 0 0 1-.643-1.395 5.566 5.566 0 0 1-.215-1.56V93.47c0-.437.048-.875.143-1.313a3.95 3.95 0 0 1 .429-1.15c.19-.328.429-.656.715-.984.286-.329.572-.602.858-.821.286-.22.62-.383 1.001-.493.382-.11.763-.164 1.144-.164h9.726V61.619c0-.985.31-1.833.93-2.544.619-.712 1.358-1.068 2.216-1.068h44.335V39.62h-9.582c-1.24 0-2.288-.492-3.146-1.477a5.09 5.09 0 0 1-1.287-3.448V5.14c0-1.423.429-2.627 1.287-3.612.858-.985 1.907-1.477 3.146-1.477h25.743c.763 0 1.478.246 2.145.739a5.17 5.17 0 0 1 1.573 1.888c.382.766.573 1.587.573 2.462v29.553c0 1.313-.43 2.463-1.287 3.448-.859.985-1.86 1.477-3.004 1.477h-9.725v18.389h42.762c.954 0 1.74.355 2.36 1.067.62.711.93 1.56.93 2.545v26.925h9.582c1.239 0 2.288.492 3.146 1.478z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/upload.svg b/src/assets/icons/svg/upload.svg
new file mode 100644
index 0000000..bae49c0
--- /dev/null
+++ b/src/assets/icons/svg/upload.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1577540289643" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7922" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M530.944 458.24l4.8 3.456 122.176 106.816a32 32 0 0 1-37.44 51.584l-4.672-3.392L546.56 556.16v280.704a32 32 0 0 1-26.24 31.488l-5.76 0.512a32 32 0 0 1-31.424-26.24l-0.512-5.76-0.064-280.704-69.12 60.48a32 32 0 0 1-40.96 0.896l-4.16-3.968a32 32 0 0 1-0.96-40.96l4.032-4.16 122.176-106.816a32 32 0 0 1 37.312-3.456zM497.92 128c128.128 0 239.168 82.304 275.52 199.04 123.968 11.264 221.312 113.088 221.312 237.44 0 128.128-103.68 232.96-234.88 238.272h-5.888l-35.52 0.192a32 32 0 0 1-0.192-64l35.264-0.128 4.672-0.064c96.384-3.84 172.544-80.896 172.544-174.272 0-96.128-80.512-174.464-179.584-174.464h-1.984a32 32 0 0 1-32-25.28C695.872 264.96 604.736 192 497.92 192 381.824 192 285.44 277.76 274.816 388.48a32 32 0 0 1-28.352 28.8c-83.968 9.152-147.84 78.208-147.84 159.552l0.192 7.936c3.84 85.76 77.056 154.112 166.592 154.112h45.632a32 32 0 0 1 0 64h-45.632C142.016 802.944 40.32 708.032 34.88 586.88l-0.192-9.28c0-106.88 76.352-197.184 179.968-219.904C239.488 226.112 357.76 128 497.856 128z" p-id="7923"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/user.svg b/src/assets/icons/svg/user.svg
new file mode 100644
index 0000000..0ba0716
--- /dev/null
+++ b/src/assets/icons/svg/user.svg
@@ -0,0 +1 @@
+<svg width="130" height="130" xmlns="http://www.w3.org/2000/svg"><path d="M63.444 64.996c20.633 0 37.359-14.308 37.359-31.953 0-17.649-16.726-31.952-37.359-31.952-20.631 0-37.36 14.303-37.358 31.952 0 17.645 16.727 31.953 37.359 31.953zM80.57 75.65H49.434c-26.652 0-48.26 18.477-48.26 41.27v2.664c0 9.316 21.608 9.325 48.26 9.325H80.57c26.649 0 48.256-.344 48.256-9.325v-2.663c0-22.794-21.605-41.271-48.256-41.271z" stroke="#979797"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/validCode.svg b/src/assets/icons/svg/validCode.svg
new file mode 100644
index 0000000..cfb1021
--- /dev/null
+++ b/src/assets/icons/svg/validCode.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1569580729849" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1939" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M513.3 958.5c-142.2 0-397.9-222.1-401.6-440.5V268c1.7-39.6 31.7-72.3 71.1-77.3 49-4.6 97.1-16.5 142.7-35.3 47.8-14 91.9-38.3 129.4-71.1 30.3-24.4 72.9-26.3 105.3-4.6 39.9 30.7 83.8 55.9 130.5 74.6 48.6 14.7 98.2 25.9 148.4 33.7 38.5 7.6 67.1 40.3 69.5 79.5 3.3 84.9 2.5 169.9-2.6 254.7-33.7 281.6-253.7 436.4-392.7 436.3z m-0.1-813.7c-7.2-0.2-14.3 2-20 6.4-39.7 35.2-86.8 61.1-137.7 75.7-46.8 19.2-96.2 31-146.6 35.2-11 3.2-18.8 13-19.5 24.4v230.1c3.5 180.3 223.3 361 323.9 361s287.3-120.2 317.6-360.5c7.3-142.7 0-228.6 0-229.6-1.3-13.3-11-24.3-24-27.3-49.6-7.7-98.6-19-146.5-33.7-46.3-19.5-89.7-45.3-129-76.7-5.8-3.8-12.7-5.5-19.5-4.9l1.3-0.1z" fill="#C6CCDA" p-id="1940"></path><path d="M750.1 428L490.7 673.2c-11.7 11.1-29.5 12.9-43.1 4.2l-6.8-5.8-141.2-149.4c-9.3-9.3-12.7-22.9-9-35.5 3.8-12.6 14.1-22.1 27-24.8 12.9-2.7 26.1 1.9 34.6 11.9L469 597.5l233.7-221c14.6-12.8 36.8-11.6 49.9 2.7 13.2 14.2 11.5 35.3-2.5 48.8" fill="#C6CCDA" p-id="1941"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/wechat.svg b/src/assets/icons/svg/wechat.svg
new file mode 100644
index 0000000..c586e55
--- /dev/null
+++ b/src/assets/icons/svg/wechat.svg
@@ -0,0 +1 @@
+<svg width="128" height="110" xmlns="http://www.w3.org/2000/svg"><path d="M86.635 33.334c1.467 0 2.917.113 4.358.283C87.078 14.392 67.58.111 45.321.111 20.44.111.055 17.987.055 40.687c0 13.104 6.781 23.863 18.115 32.209l-4.527 14.352 15.82-8.364c5.666 1.182 10.207 2.395 15.858 2.395 1.42 0 2.829-.073 4.227-.189-.886-3.19-1.398-6.53-1.398-9.996 0-20.845 16.98-37.76 38.485-37.76zm-24.34-12.936c3.407 0 5.665 2.363 5.665 5.954 0 3.576-2.258 5.97-5.666 5.97-3.392 0-6.795-2.395-6.795-5.97 0-3.591 3.403-5.954 6.795-5.954zM30.616 32.323c-3.393 0-6.818-2.395-6.818-5.971 0-3.591 3.425-5.954 6.818-5.954 3.392 0 5.65 2.363 5.65 5.954 0 3.576-2.258 5.97-5.65 5.97z"/><path d="M127.945 70.52c0-19.075-18.108-34.623-38.448-34.623-21.537 0-38.5 15.548-38.5 34.623 0 19.108 16.963 34.622 38.5 34.622 4.508 0 9.058-1.2 13.584-2.395l12.414 7.167-3.404-11.923c9.087-7.184 15.854-16.712 15.854-27.471zm-50.928-5.97c-2.254 0-4.53-2.362-4.53-4.773 0-2.378 2.276-4.771 4.53-4.771 3.422 0 5.665 2.393 5.665 4.771 0 2.41-2.243 4.773-5.665 4.773zm24.897 0c-2.24 0-4.498-2.362-4.498-4.773 0-2.378 2.258-4.771 4.498-4.771 3.392 0 5.665 2.393 5.665 4.771 0 2.41-2.273 4.773-5.665 4.773z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/zip.svg b/src/assets/icons/svg/zip.svg
new file mode 100644
index 0000000..f806fc4
--- /dev/null
+++ b/src/assets/icons/svg/zip.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M78.527 116.793c.178.008.348.024.527.024h40.233c4.711-.005 8.53-3.677 8.534-8.21V18.895c-.004-4.532-3.823-8.204-8.534-8.209H79.054c-.179 0-.353.016-.527.024V0L0 10.082v107.406l78.527 10.342v-11.037zm0-101.362c.174-.024.348-.052.527-.052h40.233c2.018 0 3.659 1.578 3.659 3.52v89.713c-.003 1.942-1.64 3.517-3.659 3.519H79.054c-.179 0-.353-.028-.527-.052V15.431zM30.262 75.757l-18.721-.46V72.37l11.3-16.673v-.148l-10.266.164v-4.51l17.504-.44v3.264L18.696 70.76v.144l11.566.176v4.678zm9.419.231l-5.823-.144V50.671l5.823-.144v25.461zm22.255-11.632c-2.168 1.922-5.353 2.76-9.02 2.736-.702.004-1.402-.04-2.097-.131v9.303l-5.997-.148V50.743c1.852-.352 4.473-.647 8.218-.743 3.838-.096 6.608.539 8.48 1.913 1.807 1.306 3.032 3.5 3.032 6.112s-.926 4.833-2.612 6.331h-.004zM53.36 54.45c-.856-.01-1.71.083-2.541.275v7.682c.523.116 1.167.152 2.06.152 3.301-.004 5.36-1.614 5.36-4.314 0-2.425-1.772-3.843-4.875-3.791l-.004-.004zm39.847-37.066h9.564v3.795h-9.564v-3.795zm-9.568 5.68h9.564v3.8h-9.564v-3.8zm9.568 6.216h9.564v3.799h-9.564V29.28zm0 12h9.564v3.794h-9.564V41.28zm-9.568-6.096h9.564v3.795h-9.564v-3.795zm9.472 47.064c2.512 0 4.921-.96 6.697-2.67 1.776-1.708 2.773-4.026 2.772-6.442l-1.748-15.263c0-5.033-2.492-9.112-7.725-9.112-5.232 0-7.72 4.079-7.72 9.112l-1.752 15.263c-.001 2.417.996 4.735 2.773 6.444 1.777 1.71 4.187 2.669 6.7 2.668h.003zm-3.135-16.75h6.27v12.743h-6.27V65.5z"/></svg>
\ No newline at end of file
diff --git a/src/assets/images/dark.svg b/src/assets/images/dark.svg
new file mode 100644
index 0000000..f646bd7
--- /dev/null
+++ b/src/assets/images/dark.svg
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="52px" height="45px" viewBox="0 0 52 45" version="1.1"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink">
+ <defs>
+ <filter x="-9.4%" y="-6.2%" width="118.8%" height="122.5%" filterUnits="objectBoundingBox" id="filter-1">
+ <feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
+ <feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
+ <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0" type="matrix" in="shadowBlurOuter1" result="shadowMatrixOuter1"></feColorMatrix>
+ <feMerge>
+ <feMergeNode in="shadowMatrixOuter1"></feMergeNode>
+ <feMergeNode in="SourceGraphic"></feMergeNode>
+ </feMerge>
+ </filter>
+ <rect id="path-2" x="0" y="0" width="48" height="40" rx="4"></rect>
+ <filter x="-4.2%" y="-2.5%" width="108.3%" height="110.0%" filterUnits="objectBoundingBox" id="filter-4">
+ <feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
+ <feGaussianBlur stdDeviation="0.5" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
+ <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix>
+ </filter>
+ </defs>
+ <g id="配置面板" width="48" height="40" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <g id="setting-copy-2" width="48" height="40" transform="translate(-1190.000000, -136.000000)">
+ <g id="Group-8" width="48" height="40" transform="translate(1167.000000, 0.000000)">
+ <g id="Group-5-Copy-5" filter="url(#filter-1)" transform="translate(25.000000, 137.000000)">
+ <mask id="mask-3" fill="white">
+ <use xlink:href="#path-2"></use>
+ </mask>
+ <g id="Rectangle-18">
+ <use fill="black" fill-opacity="1" filter="url(#filter-4)" xlink:href="#path-2"></use>
+ <use fill="#F0F2F5" fill-rule="evenodd" xlink:href="#path-2"></use>
+ </g>
+ <rect id="Rectangle-11" fill="#FFFFFF" mask="url(#mask-3)" x="0" y="0" width="48" height="10"></rect>
+ <rect id="Rectangle-18" fill="#303648" mask="url(#mask-3)" x="0" y="0" width="16" height="40"></rect>
+ </g>
+ </g>
+ </g>
+ </g>
+</svg>
\ No newline at end of file
diff --git a/src/assets/images/light.svg b/src/assets/images/light.svg
new file mode 100644
index 0000000..ab7cc08
--- /dev/null
+++ b/src/assets/images/light.svg
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="52px" height="45px" viewBox="0 0 52 45" version="1.1"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink">
+ <defs>
+ <filter x="-9.4%" y="-6.2%" width="118.8%" height="122.5%" filterUnits="objectBoundingBox" id="filter-1">
+ <feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
+ <feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
+ <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0" type="matrix" in="shadowBlurOuter1" result="shadowMatrixOuter1"></feColorMatrix>
+ <feMerge>
+ <feMergeNode in="shadowMatrixOuter1"></feMergeNode>
+ <feMergeNode in="SourceGraphic"></feMergeNode>
+ </feMerge>
+ </filter>
+ <rect id="path-2" x="0" y="0" width="48" height="40" rx="4"></rect>
+ <filter x="-4.2%" y="-2.5%" width="108.3%" height="110.0%" filterUnits="objectBoundingBox" id="filter-4">
+ <feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
+ <feGaussianBlur stdDeviation="0.5" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
+ <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix>
+ </filter>
+ </defs>
+ <g id="配置面板" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <g id="setting-copy-2" transform="translate(-1254.000000, -136.000000)">
+ <g id="Group-8" transform="translate(1167.000000, 0.000000)">
+ <g id="Group-5" filter="url(#filter-1)" transform="translate(89.000000, 137.000000)">
+ <mask id="mask-3" fill="white">
+ <use xlink:href="#path-2"></use>
+ </mask>
+ <g id="Rectangle-18">
+ <use fill="black" fill-opacity="1" filter="url(#filter-4)" xlink:href="#path-2"></use>
+ <use fill="#F0F2F5" fill-rule="evenodd" xlink:href="#path-2"></use>
+ </g>
+ <rect id="Rectangle-18" fill="#FFFFFF" mask="url(#mask-3)" x="0" y="0" width="16" height="40"></rect>
+ <rect id="Rectangle-11" fill="#FFFFFF" mask="url(#mask-3)" x="0" y="0" width="48" height="10"></rect>
+ </g>
+ </g>
+ </g>
+ </g>
+</svg>
\ No newline at end of file
diff --git a/src/assets/images/login-background.jpg b/src/assets/images/login-background.jpg
new file mode 100644
index 0000000..fa6408b
--- /dev/null
+++ b/src/assets/images/login-background.jpg
Binary files differ
diff --git a/src/assets/images/profile.jpg b/src/assets/images/profile.jpg
new file mode 100644
index 0000000..f4fde57
--- /dev/null
+++ b/src/assets/images/profile.jpg
Binary files differ
diff --git a/src/assets/logo/logo.png b/src/assets/logo/logo.png
new file mode 100644
index 0000000..3f919d8
--- /dev/null
+++ b/src/assets/logo/logo.png
Binary files differ
diff --git a/src/assets/styles/btn.scss b/src/assets/styles/btn.scss
new file mode 100644
index 0000000..a1ccd01
--- /dev/null
+++ b/src/assets/styles/btn.scss
@@ -0,0 +1,99 @@
+@import './variables.module.scss';
+
+@mixin colorBtn($color) {
+ background: $color;
+
+ &:hover {
+ color: $color;
+
+ &:before,
+ &:after {
+ background: $color;
+ }
+ }
+}
+
+.blue-btn {
+ @include colorBtn($blue);
+}
+
+.light-blue-btn {
+ @include colorBtn($light-blue);
+}
+
+.red-btn {
+ @include colorBtn($red);
+}
+
+.pink-btn {
+ @include colorBtn($pink);
+}
+
+.green-btn {
+ @include colorBtn($green);
+}
+
+.tiffany-btn {
+ @include colorBtn($tiffany);
+}
+
+.yellow-btn {
+ @include colorBtn($yellow);
+}
+
+.pan-btn {
+ font-size: 14px;
+ color: #fff;
+ padding: 14px 36px;
+ border-radius: 8px;
+ border: none;
+ outline: none;
+ transition: 600ms ease all;
+ position: relative;
+ display: inline-block;
+
+ &:hover {
+ background: #fff;
+
+ &:before,
+ &:after {
+ width: 100%;
+ transition: 600ms ease all;
+ }
+ }
+
+ &:before,
+ &:after {
+ content: '';
+ position: absolute;
+ top: 0;
+ right: 0;
+ height: 2px;
+ width: 0;
+ transition: 400ms ease all;
+ }
+
+ &::after {
+ right: inherit;
+ top: inherit;
+ left: 0;
+ bottom: 0;
+ }
+}
+
+.custom-button {
+ display: inline-block;
+ line-height: 1;
+ white-space: nowrap;
+ cursor: pointer;
+ background: #fff;
+ color: #fff;
+ -webkit-appearance: none;
+ text-align: center;
+ box-sizing: border-box;
+ outline: 0;
+ margin: 0;
+ padding: 10px 15px;
+ font-size: 14px;
+ border-radius: 4px;
+}
diff --git a/src/assets/styles/element-ui.scss b/src/assets/styles/element-ui.scss
new file mode 100644
index 0000000..43c093c
--- /dev/null
+++ b/src/assets/styles/element-ui.scss
@@ -0,0 +1,116 @@
+// cover some element-ui styles
+
+.el-divider--horizontal {
+ margin-bottom: 10px;
+ margin-top: 10px;
+}
+
+.el-breadcrumb__inner,
+.el-breadcrumb__inner a {
+ font-weight: 400 !important;
+}
+
+.el-upload {
+ input[type='file'] {
+ display: none !important;
+ }
+}
+
+.el-upload__input {
+ display: none;
+}
+
+.cell {
+ .el-tag {
+ margin-right: 0px;
+ }
+}
+
+.small-padding {
+ .cell {
+ padding-left: 5px;
+ padding-right: 5px;
+ }
+}
+
+.fixed-width {
+ .el-button--mini {
+ padding: 7px 10px;
+ width: 60px;
+ }
+}
+
+.status-col {
+ .cell {
+ padding: 0 10px;
+ text-align: center;
+
+ .el-tag {
+ margin-right: 0px;
+ }
+ }
+}
+
+/*-------------Dialog-------------**/
+.el-overlay {
+ overflow: hidden;
+
+ .el-overlay-dialog {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+ height: 100%;
+
+ .el-dialog {
+ margin: 0 auto !important;
+
+ .el-dialog__body {
+ padding: 15px !important;
+ }
+ }
+ }
+}
+
+.el-dialog__body {
+ max-height: calc(90vh - 111px) !important;
+ overflow-y: auto;
+ overflow-x: hidden;
+}
+
+// refine element ui upload
+.upload-container {
+ .el-upload {
+ width: 100%;
+
+ .el-upload-dragger {
+ width: 100%;
+ height: 200px;
+ }
+ }
+}
+
+// dropdown
+.el-dropdown-menu {
+ a {
+ display: block;
+ }
+}
+
+// fix date-picker ui bug in filter-item
+.el-range-editor.el-input__inner {
+ display: inline-flex !important;
+}
+
+// to fix el-date-picker css style
+.el-range-separator {
+ box-sizing: content-box;
+}
+
+.el-menu--collapse > div > .el-submenu > .el-submenu__title .el-submenu__icon-arrow {
+ display: none;
+}
+
+.el-dropdown .el-dropdown-link {
+ color: var(--el-color-primary) !important;
+}
diff --git a/src/assets/styles/index.scss b/src/assets/styles/index.scss
new file mode 100644
index 0000000..48865fb
--- /dev/null
+++ b/src/assets/styles/index.scss
@@ -0,0 +1,208 @@
+@import './variables.module.scss';
+@import './mixin.scss';
+@import './transition.scss';
+@import './element-ui.scss';
+@import './sidebar.scss';
+@import './btn.scss';
+@import './ruoyi.scss';
+@import 'animate.css';
+@import 'element-plus/dist/index.css';
+
+body {
+ height: 100%;
+ margin: 0;
+ -moz-osx-font-smoothing: grayscale;
+ -webkit-font-smoothing: antialiased;
+ text-rendering: optimizeLegibility;
+ font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
+}
+
+label {
+ font-weight: 700;
+}
+
+html {
+ height: 100%;
+ box-sizing: border-box;
+}
+
+html.dark .svg-icon,
+html.dark svg {
+ fill: var(--el-text-color-regular);
+}
+
+#app {
+ height: 100%;
+}
+
+*,
+*:before,
+*:after {
+ box-sizing: inherit;
+}
+
+.no-padding {
+ padding: 0px !important;
+}
+
+.padding-content {
+ padding: 4px 0;
+}
+
+a:focus,
+a:active {
+ outline: none;
+}
+
+a,
+a:focus,
+a:hover {
+ cursor: pointer;
+ color: inherit;
+ text-decoration: none;
+}
+
+div:focus {
+ outline: none;
+}
+
+.fr {
+ float: right;
+}
+
+.fl {
+ float: left;
+}
+
+.pr-5 {
+ padding-right: 5px;
+}
+
+.pl-5 {
+ padding-left: 5px;
+}
+
+.block {
+ display: block;
+}
+
+.pointer {
+ cursor: pointer;
+}
+
+.inlineBlock {
+ display: block;
+}
+
+.clearfix {
+ &:after {
+ visibility: hidden;
+ display: block;
+ font-size: 0;
+ content: ' ';
+ clear: both;
+ height: 0;
+ }
+}
+
+aside {
+ background: #eef1f6;
+ padding: 8px 24px;
+ margin-bottom: 20px;
+ border-radius: 2px;
+ display: block;
+ line-height: 32px;
+ font-size: 16px;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
+ sans-serif;
+ color: #2c3e50;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+
+ a {
+ color: #337ab7;
+ cursor: pointer;
+
+ &:hover {
+ color: rgb(32, 160, 255);
+ }
+ }
+}
+
+//main-container全局样式
+.app-container {
+ padding: 20px;
+}
+
+// search面板样式
+.panel,
+.search {
+ margin-bottom: 0.75rem;
+ border-radius: 0.25rem;
+ border: 1px solid var(--el-border-color-light);
+ background-color: var(--el-bg-color-overlay);
+ padding: 0.75rem;
+ transition: all ease 0.3s;
+
+ &:hover {
+ box-shadow: 0 2px 12px #0000001a;
+ transition: all ease 0.3s;
+ }
+}
+
+.components-container {
+ margin: 30px 50px;
+ position: relative;
+}
+
+.pagination-container {
+ margin-top: 30px;
+}
+
+.text-center {
+ text-align: center;
+}
+
+.sub-navbar {
+ height: 50px;
+ line-height: 50px;
+ position: relative;
+ width: 100%;
+ text-align: right;
+ padding-right: 20px;
+ transition: 600ms ease position;
+ background: linear-gradient(90deg, rgba(32, 182, 249, 1) 0%, rgba(32, 182, 249, 1) 0%, rgba(33, 120, 241, 1) 100%, rgba(33, 120, 241, 1) 100%);
+
+ .subtitle {
+ font-size: 20px;
+ color: #fff;
+ }
+
+ &.draft {
+ background: #d0d0d0;
+ }
+
+ &.deleted {
+ background: #d0d0d0;
+ }
+}
+
+.link-type,
+.link-type:focus {
+ color: #337ab7;
+ cursor: pointer;
+
+ &:hover {
+ color: rgb(32, 160, 255);
+ }
+}
+
+.filter-container {
+ padding-bottom: 10px;
+
+ .filter-item {
+ display: inline-block;
+ vertical-align: middle;
+ margin-bottom: 10px;
+ }
+}
diff --git a/src/assets/styles/mixin.scss b/src/assets/styles/mixin.scss
new file mode 100644
index 0000000..5250e71
--- /dev/null
+++ b/src/assets/styles/mixin.scss
@@ -0,0 +1,60 @@
+@mixin clearfix {
+ &:after {
+ content: '';
+ display: table;
+ clear: both;
+ }
+}
+
+@mixin scrollBar {
+ &::-webkit-scrollbar-track-piece {
+ background: #d3dce6;
+ }
+
+ &::-webkit-scrollbar {
+ width: 6px;
+ }
+
+ &::-webkit-scrollbar-thumb {
+ background: #99a9bf;
+ border-radius: 20px;
+ }
+}
+
+@mixin relative {
+ position: relative;
+ width: 100%;
+ height: 100%;
+}
+
+@mixin pct($pct) {
+ width: #{$pct};
+ position: relative;
+ margin: 0 auto;
+}
+
+@mixin triangle($width, $height, $color, $direction) {
+ $width: $width/2;
+ $color-border-style: $height solid $color;
+ $transparent-border-style: $width solid transparent;
+ height: 0;
+ width: 0;
+
+ @if $direction==up {
+ border-bottom: $color-border-style;
+ border-left: $transparent-border-style;
+ border-right: $transparent-border-style;
+ } @else if $direction==right {
+ border-left: $color-border-style;
+ border-top: $transparent-border-style;
+ border-bottom: $transparent-border-style;
+ } @else if $direction==down {
+ border-top: $color-border-style;
+ border-left: $transparent-border-style;
+ border-right: $transparent-border-style;
+ } @else if $direction==left {
+ border-right: $color-border-style;
+ border-top: $transparent-border-style;
+ border-bottom: $transparent-border-style;
+ }
+}
diff --git a/src/assets/styles/ruoyi.scss b/src/assets/styles/ruoyi.scss
new file mode 100644
index 0000000..d2ecb24
--- /dev/null
+++ b/src/assets/styles/ruoyi.scss
@@ -0,0 +1,290 @@
+/**
+ * 通用css样式布局处理
+ * Copyright (c) 2019 ruoyi
+ */
+
+/** 基础通用 **/
+.pt5 {
+ padding-top: 5px;
+}
+.pr5 {
+ padding-right: 5px;
+}
+.pb5 {
+ padding-bottom: 5px;
+}
+.mt5 {
+ margin-top: 5px;
+}
+.mr5 {
+ margin-right: 5px;
+}
+.mb5 {
+ margin-bottom: 5px;
+}
+.mb8 {
+ margin-bottom: 8px;
+}
+.ml5 {
+ margin-left: 5px;
+}
+.mt10 {
+ margin-top: 10px;
+}
+.mr10 {
+ margin-right: 10px;
+}
+.mb10 {
+ margin-bottom: 10px;
+}
+.ml10 {
+ margin-left: 10px;
+}
+.mt20 {
+ margin-top: 20px;
+}
+.mr20 {
+ margin-right: 20px;
+}
+.mb20 {
+ margin-bottom: 20px;
+}
+.ml20 {
+ margin-left: 20px;
+}
+
+.h1,
+.h2,
+.h3,
+.h4,
+.h5,
+.h6,
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+ font-family: inherit;
+ font-weight: 500;
+ line-height: 1.1;
+ color: inherit;
+}
+
+.el-form .el-form-item__label {
+ font-weight: 700;
+}
+.el-dialog:not(.is-fullscreen) {
+ margin-top: 6vh !important;
+}
+
+.el-dialog.scrollbar .el-dialog__body {
+ overflow: auto;
+ overflow-x: hidden;
+ max-height: 70vh;
+ padding: 10px 20px 0;
+}
+
+.el-table {
+ .el-table__header-wrapper,
+ .el-table__fixed-header-wrapper {
+ th {
+ word-break: break-word;
+ background-color: $table-header-bg !important;
+ color: $table-header-text-color;
+ height: 40px !important;
+ font-size: 13px;
+ }
+ }
+ .el-table__body-wrapper {
+ .el-button [class*='el-icon-'] + span {
+ margin-left: 1px;
+ }
+ }
+}
+
+/** 表单布局 **/
+.form-header {
+ font-size: 15px;
+ color: #6379bb;
+ border-bottom: 1px solid #ddd;
+ margin: 8px 10px 25px 10px;
+ padding-bottom: 5px;
+}
+
+/** 表格布局 **/
+.pagination-container {
+ // position: relative;
+ height: 25px;
+ margin-bottom: 10px;
+ margin-top: 15px;
+ padding: 10px 20px !important;
+}
+
+/* tree border */
+.tree-border {
+ margin-top: 5px;
+ border: 1px solid #e5e6e7;
+ background: #ffffff none;
+ border-radius: 4px;
+ width: 100%;
+}
+
+.pagination-container .el-pagination {
+ //right: 0;
+ //position: absolute;
+}
+
+@media (max-width: 768px) {
+ .pagination-container .el-pagination > .el-pagination__jump {
+ display: none !important;
+ }
+ .pagination-container .el-pagination > .el-pagination__sizes {
+ display: none !important;
+ }
+}
+
+.el-table .fixed-width .el-button--small {
+ padding-left: 0;
+ padding-right: 0;
+ width: inherit;
+}
+
+/** 表格更多操作下拉样式 */
+.el-table .el-dropdown-link {
+ cursor: pointer;
+ color: #409eff;
+ margin-left: 10px;
+}
+
+.el-table .el-dropdown,
+.el-icon-arrow-down {
+ font-size: 12px;
+}
+
+.el-tree-node__content > .el-checkbox {
+ margin-right: 8px;
+}
+
+.list-group-striped > .list-group-item {
+ border-left: 0;
+ border-right: 0;
+ border-radius: 0;
+ padding-left: 0;
+ padding-right: 0;
+}
+
+.list-group {
+ padding-left: 0px;
+ list-style: none;
+}
+
+.list-group-item {
+ border-bottom: 1px solid #e7eaec;
+ border-top: 1px solid #e7eaec;
+ margin-bottom: -1px;
+ padding: 11px 0px;
+ font-size: 13px;
+}
+
+.pull-right {
+ float: right !important;
+}
+
+.el-card__header {
+ padding: 14px 15px 7px !important;
+ min-height: 40px;
+}
+
+.el-card__body {
+ padding: 15px 20px 20px 20px !important;
+}
+
+.card-box {
+ padding-right: 15px;
+ padding-left: 15px;
+ margin-bottom: 10px;
+}
+
+/* button color */
+.el-button--cyan.is-active,
+.el-button--cyan:active {
+ background: #20b2aa;
+ border-color: #20b2aa;
+ color: #ffffff;
+}
+
+.el-button--cyan:focus,
+.el-button--cyan:hover {
+ background: #48d1cc;
+ border-color: #48d1cc;
+ color: #ffffff;
+}
+
+.el-button--cyan {
+ background-color: #20b2aa;
+ border-color: #20b2aa;
+ color: #ffffff;
+}
+
+/* text color */
+.text-navy {
+ color: #1ab394;
+}
+
+.text-primary {
+ color: inherit;
+}
+
+.text-success {
+ color: #1c84c6;
+}
+
+.text-info {
+ color: #23c6c8;
+}
+
+.text-warning {
+ color: #f8ac59;
+}
+
+.text-danger {
+ color: #ed5565;
+}
+
+.text-muted {
+ color: #888888;
+}
+
+/* image */
+.img-circle {
+ border-radius: 50%;
+}
+
+.img-lg {
+ width: 120px;
+ height: 120px;
+}
+
+.avatar-upload-preview {
+ position: absolute;
+ top: 50%;
+ transform: translate(50%, -50%);
+ width: 200px;
+ height: 200px;
+ border-radius: 50%;
+ box-shadow: 0 0 4px #ccc;
+ overflow: hidden;
+}
+
+/* 拖拽列样式 */
+.sortable-ghost {
+ opacity: 0.8;
+ color: #fff !important;
+ background: #42b983 !important;
+}
+
+/* 表格右侧工具栏样式 */
+.top-right-btn {
+ margin-left: auto;
+}
diff --git a/src/assets/styles/sidebar.scss b/src/assets/styles/sidebar.scss
new file mode 100644
index 0000000..d47a2bd
--- /dev/null
+++ b/src/assets/styles/sidebar.scss
@@ -0,0 +1,230 @@
+#app {
+ .main-container {
+ height: 100%;
+ transition: margin-left 0.28s;
+ margin-left: $base-sidebar-width;
+ position: relative;
+ }
+
+ .sidebarHide {
+ margin-left: 0 !important;
+ }
+
+ .sidebar-container {
+ -webkit-transition: width 0.28s;
+ transition: width 0.28s;
+ width: $base-sidebar-width !important;
+ background-color: $base-menu-background;
+ height: 100%;
+ position: fixed;
+ font-size: 0;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ z-index: 1001;
+ overflow: hidden;
+ -webkit-box-shadow: 2px 0 6px rgba(0, 21, 41, 0.35);
+ box-shadow: 2px 0 6px rgba(0, 21, 41, 0.35);
+
+ // reset element-ui css
+ .horizontal-collapse-transition {
+ transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out;
+ }
+
+ .scrollbar-wrapper {
+ overflow-x: hidden !important;
+ }
+
+ .el-scrollbar__bar.is-vertical {
+ right: 0;
+ }
+
+ .el-scrollbar {
+ height: 100%;
+ }
+
+ &.has-logo {
+ .el-scrollbar {
+ height: calc(100% - 50px);
+ }
+ }
+
+ .is-horizontal {
+ display: none;
+ }
+
+ a {
+ display: inline-block;
+ width: 100%;
+ overflow: hidden;
+ }
+
+ .svg-icon {
+ margin-right: 16px;
+ }
+
+ .el-menu {
+ border: none;
+ height: 100%;
+ width: 100% !important;
+ }
+
+ .el-menu-item,
+ .menu-title {
+ overflow: hidden !important;
+ text-overflow: ellipsis !important;
+ white-space: nowrap !important;
+ }
+
+ .el-menu-item .el-menu-tooltip__trigger {
+ display: inline-block !important;
+ }
+
+ // menu hover
+ .theme-dark .sub-menu-title-noDropdown,
+ .theme-dark .el-sub-menu__title {
+ &:hover {
+ background-color: $base-sub-menu-title-hover !important;
+ }
+ }
+ .sub-menu-title-noDropdown,
+ .el-sub-menu__title {
+ &:hover {
+ background-color: rgba(0, 0, 0, 0.05) !important;
+ }
+ }
+
+ & .theme-dark .is-active > .el-sub-menu__title {
+ color: $base-menu-color-active !important;
+ }
+
+ & .nest-menu .el-sub-menu > .el-sub-menu__title,
+ & .el-sub-menu .el-menu-item {
+ min-width: $base-sidebar-width !important;
+ &:hover {
+ background-color: rgba(0, 0, 0, 0.1) !important;
+ }
+ }
+
+
+ & .theme-dark .nest-menu .el-sub-menu > .el-sub-menu__title,
+ & .theme-dark .el-sub-menu .el-menu-item {
+ background-color: $base-sub-menu-background !important;
+
+ &:hover {
+ background-color: $base-sub-menu-hover !important;
+ }
+ }
+
+ & .theme-dark .nest-menu .el-sub-menu > .el-sub-menu__title,
+ & .theme-dark .el-menu-item {
+ &:hover {
+ // you can use $sub-menuHover
+ background-color: $base-menu-hover !important;
+ }
+ }
+ & .nest-menu .el-sub-menu > .el-sub-menu__title,
+ & .el-menu-item {
+ &:hover {
+ // you can use $sub-menuHover
+ background-color: rgba(0, 0, 0, 0.04) !important;
+ }
+ }
+ }
+
+ .hideSidebar {
+ .sidebar-container {
+ width: 54px !important;
+ }
+
+ .main-container {
+ margin-left: 54px;
+ }
+
+ .sub-menu-title-noDropdown {
+ padding: 0 !important;
+ position: relative;
+
+ .el-tooltip {
+ padding: 0 !important;
+
+ .svg-icon {
+ margin-left: 20px;
+ }
+ }
+ }
+
+ .el-sub-menu {
+ overflow: hidden;
+
+ & > .el-sub-menu__title {
+ padding: 0 !important;
+
+ .svg-icon {
+ margin-left: 20px;
+ }
+ }
+ }
+
+ .el-menu--collapse {
+ .el-sub-menu {
+ & > .el-sub-menu__title {
+ & > span {
+ height: 0;
+ width: 0;
+ overflow: hidden;
+ visibility: hidden;
+ display: inline-block;
+ }
+ & > i {
+ height: 0;
+ width: 0;
+ overflow: hidden;
+ visibility: hidden;
+ display: inline-block;
+ }
+ }
+ }
+ }
+ }
+
+ .el-menu--collapse .el-menu .el-sub-menu {
+ min-width: $base-sidebar-width !important;
+ }
+
+ // mobile responsive
+ .mobile {
+ .main-container {
+ margin-left: 0px;
+ }
+
+ .sidebar-container {
+ transition: transform 0.28s;
+ width: $base-sidebar-width !important;
+ }
+
+ &.hideSidebar {
+ .sidebar-container {
+ pointer-events: none;
+ transition-duration: 0.3s;
+ transform: translate3d(-$base-sidebar-width, 0, 0);
+ }
+ }
+ }
+
+ .withoutAnimation {
+ .main-container,
+ .sidebar-container {
+ transition: none;
+ }
+ }
+}
+
+// when menu collapsed
+.el-menu--vertical {
+ & > .el-menu {
+ .svg-icon {
+ margin-right: 16px;
+ }
+ }
+}
diff --git a/src/assets/styles/transition.scss b/src/assets/styles/transition.scss
new file mode 100644
index 0000000..468ad3c
--- /dev/null
+++ b/src/assets/styles/transition.scss
@@ -0,0 +1,49 @@
+// global transition css
+
+/* fade */
+.fade-enter-active,
+.fade-leave-active {
+ transition: opacity 0.28s;
+}
+
+.fade-enter,
+.fade-leave-active {
+ opacity: 0;
+}
+
+/* fade-transform */
+.fade-transform--move,
+.fade-transform-leave-active,
+.fade-transform-enter-active {
+ transition: all 0.5s;
+}
+
+.fade-transform-enter {
+ opacity: 0;
+ transform: translateX(-30px);
+}
+
+.fade-transform-leave-to {
+ opacity: 0;
+ transform: translateX(30px);
+}
+
+/* breadcrumb transition */
+.breadcrumb-enter-active,
+.breadcrumb-leave-active {
+ transition: all 0.5s;
+}
+
+.breadcrumb-enter,
+.breadcrumb-leave-active {
+ opacity: 0;
+ transform: translateX(20px);
+}
+
+.breadcrumb-move {
+ transition: all 0.5s;
+}
+
+.breadcrumb-leave-active {
+ position: absolute;
+}
diff --git a/src/assets/styles/variables.module.scss b/src/assets/styles/variables.module.scss
new file mode 100644
index 0000000..d07d3d4
--- /dev/null
+++ b/src/assets/styles/variables.module.scss
@@ -0,0 +1,93 @@
+// 全局SCSS变量
+:root {
+ --menuBg: #304156;
+ --menuColor: #bfcbd9;
+ --menuActiveText: #f4f4f5;
+ --menuHover: #263445;
+
+ --subMenuBg: #1f2d3d;
+ --subMenuActiveText: #f4f4f5;
+ --subMenuHover: #001528;
+ --subMenuTitleHover: #293444;
+
+ --fixedHeaderBg: #ffffff;
+ --tableHeaderBg: #f8f8f9;
+ --tableHeaderTextColor: #515a6e;
+}
+html.dark {
+ --menuBg: #1d1e1f;
+ --menuColor: #bfcbd9;
+ --menuActiveText: #f4f4f5;
+ --menuHover: #171819;
+
+ --subMenuBg: #1d1e1f;
+ --subMenuActiveText: #1d1e1f;
+ --subMenuHover: #171819;
+ --subMenuTitleHover: #171819;
+
+ --fixedHeaderBg: #171819;
+ --tableHeaderBg: var(--el-bg-color);
+ --tableHeaderTextColor: var(--el-text-color);
+
+ // 覆盖ele 高亮当前行的标准暗色
+ .el-tree-node__content {
+ --el-color-primary-light-9: #262727;
+ }
+}
+
+// base color
+$blue: #324157;
+$light-blue: #3a71a8;
+$red: #c03639;
+$pink: #e65d6e;
+$green: #30b08f;
+$tiffany: #4ab7bd;
+$yellow: #fec171;
+$panGreen: #30b08f;
+
+// 默认菜单主题风格
+$base-menu-color: var(--menuColor);
+$base-menu-hover: var(--menuHover);
+$base-menu-color-active: var(--menuActiveText);
+$base-menu-background: var(--menuBg);
+$base-logo-title-color: #ffffff;
+
+$base-menu-light-color: rgba(0, 0, 0, 0.7);
+$base-menu-light-background: #ffffff;
+$base-logo-light-title-color: #001529;
+
+$base-sub-menu-background: var(--subMenuBg);
+$base-sub-menu-hover: var(--subMenuHover);
+$base-sub-menu-title-hover: var(--subMenuTitleHover);
+// 表单头背景色和标题颜色
+$fixed-header-bg: var(--fixedHeaderBg);
+$table-header-bg: var(--tableHeaderBg);
+$table-header-text-color: var(--tableHeaderTextColor);
+
+$--color-primary: #409eff;
+$--color-success: #67c23a;
+$--color-warning: #e6a23c;
+$--color-danger: #f56c6c;
+$--color-info: #909399;
+
+$base-sidebar-width: 200px;
+
+// the :export directive is the magic sauce for webpack
+// https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass
+:export {
+ menuColor: $base-menu-color;
+ menuLightColor: $base-menu-light-color;
+ menuColorActive: $base-menu-color-active;
+ menuBackground: $base-menu-background;
+ menuLightBackground: $base-menu-light-background;
+ subMenuBackground: $base-sub-menu-background;
+ subMenuHover: $base-sub-menu-hover;
+ sideBarWidth: $base-sidebar-width;
+ logoTitleColor: $base-logo-title-color;
+ logoLightTitleColor: $base-logo-light-title-color;
+ primaryColor: $--color-primary;
+ successColor: $--color-success;
+ dangerColor: $--color-danger;
+ infoColor: $--color-info;
+ warningColor: $--color-warning;
+}
diff --git a/src/components/Breadcrumb/index.vue b/src/components/Breadcrumb/index.vue
new file mode 100644
index 0000000..b96829a
--- /dev/null
+++ b/src/components/Breadcrumb/index.vue
@@ -0,0 +1,64 @@
+<template>
+ <el-breadcrumb class="app-breadcrumb" separator="/">
+ <transition-group name="breadcrumb">
+ <el-breadcrumb-item v-for="(item, index) in levelList" :key="item.path">
+ <span v-if="item.redirect === 'noRedirect' || index == levelList.length - 1" class="no-redirect">{{
+ item.meta?.title }}</span>
+ <a v-else @click.prevent="handleLink(item)">{{ item.meta?.title }}</a>
+ </el-breadcrumb-item>
+ </transition-group>
+ </el-breadcrumb>
+</template>
+
+<script setup lang="ts">
+import { RouteLocationMatched } from 'vue-router'
+
+const route = useRoute();
+const router = useRouter();
+const levelList = ref<RouteLocationMatched[]>([])
+
+const getBreadcrumb = () => {
+ // only show routes with meta.title
+ let matched = route.matched.filter(item => item.meta && item.meta.title);
+ const first = matched[0]
+ // 判断是否为首页
+ if (!isDashboard(first)) {
+ matched = ([{ path: '/index', meta: { title: '首页' } }] as any).concat(matched)
+ }
+ levelList.value = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)
+}
+const isDashboard = (route: RouteLocationMatched) => {
+ const name = route && route.name as string
+ if (!name) {
+ return false
+ }
+ return name.trim() === 'Index'
+}
+const handleLink = (item: RouteLocationMatched) => {
+ const { redirect, path } = item
+ redirect ? router.push(redirect as string) : router.push(path)
+}
+
+watchEffect(() => {
+ // if you go to the redirect page, do not update the breadcrumbs
+ if (route.path.startsWith('/redirect/')) return
+ getBreadcrumb()
+})
+onMounted(() => {
+ getBreadcrumb();
+})
+</script>
+
+<style lang="scss" scoped>
+.app-breadcrumb.el-breadcrumb {
+ display: inline-block;
+ font-size: 14px;
+ line-height: 50px;
+ margin-left: 8px;
+
+ .no-redirect {
+ color: #97a8be;
+ cursor: text;
+ }
+}
+</style>
diff --git a/src/components/BuildCode/index.vue b/src/components/BuildCode/index.vue
new file mode 100644
index 0000000..b23665c
--- /dev/null
+++ b/src/components/BuildCode/index.vue
@@ -0,0 +1,64 @@
+<!-- 代码构建 -->
+<script setup lang="ts">
+
+const props = defineProps({
+ showBtn: {
+ type: Boolean,
+ default: false
+ },
+ formJson: {
+ type: Object,
+ default: undefined
+ }
+})
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const buildRef = ref();
+const emits = defineEmits(['reJson', 'saveDesign']);
+
+
+
+//获取表单json
+const getJson = () => {
+ const formJson = JSON.stringify(buildRef.value.getFormJson())
+ const fieldJson = JSON.stringify(buildRef.value.getFieldWidgets())
+ let data = {
+ formJson, fieldJson
+ }
+ emits("saveDesign", data)
+}
+
+onMounted(() => {
+ if (props.formJson) {
+ buildRef.value.setFormJson(props.formJson)
+ }
+})
+</script>
+
+<template>
+ <div>
+ <v-form-designer
+ class="build"
+ ref="buildRef"
+ :designer-config="{ importJsonButton: true, exportJsonButton: true, exportCodeButton: true, generateSFCButton: true, formTemplates: true }"
+ >
+ <template #customToolButtons v-if="showBtn">
+ <el-button link type="primary" icon="Select" @click="getJson">保存</el-button>
+ </template>
+ </v-form-designer>
+ </div>
+</template>
+
+<style lang="scss">
+.build {
+ margin: 0 !important;
+ overflow-y: auto !important;
+
+ & header.main-header {
+ display: none;
+ }
+
+ & .right-toolbar-con {
+ text-align: right !important;
+ }
+}
+</style>
diff --git a/src/components/BuildCode/render.vue b/src/components/BuildCode/render.vue
new file mode 100644
index 0000000..62f3d86
--- /dev/null
+++ b/src/components/BuildCode/render.vue
@@ -0,0 +1,62 @@
+<!-- 动态表单渲染 -->
+<script setup name="Render">
+
+const props = defineProps({
+ formJson: {
+ type: [String, Object],
+ default: ""
+ },
+ formData: {
+ type: [String, Object],
+ default: ""
+ },
+ isView: {
+ type: Boolean,
+ default: false
+ }
+})
+
+const vFormRef = ref(null)
+// 获取表单数据-异步
+const getFormData = () => {
+ return vFormRef.value.getFormData()
+}
+
+/**
+ * 设置表单内容
+ * @param {表单配置} formConf
+ * formConfig:{ formTemplate:表单模板,formData:表单数据,hiddenField:需要隐藏的字段字符串集合,disabledField:需要禁用的自读字符串集合}
+ */
+const initForm = (formConf) => {
+ const { formTemplate, formData, hiddenField, disabledField } = toRaw(formConf)
+ if (formTemplate) {
+ vFormRef.value.setFormJson(formTemplate)
+ if (formData) {
+ vFormRef.value.setFormData(formData)
+ }
+ if (disabledField && disabledField.length > 0) {
+ setTimeout(() => {
+ vFormRef.value.disableWidgets(disabledField)
+ }, 200)
+ }
+ if (hiddenField && hiddenField.length > 0) {
+ setTimeout(() => {
+ vFormRef.value.hideWidgets(hiddenField)
+ }, 200)
+ }
+ if (props.isView) {
+ console.log(props.isView)
+ setTimeout(() => {
+ vFormRef.value.disableForm()
+ }, 100)
+ }
+ }
+}
+defineExpose({ getFormData, initForm })
+</script>
+
+<template>
+ <div class="">
+ <v-form-render ref="vFormRef" :form-json="formJson" :form-data="formData" />
+ </div>
+</template>
diff --git a/src/components/DictTag/index.vue b/src/components/DictTag/index.vue
new file mode 100644
index 0000000..45b288b
--- /dev/null
+++ b/src/components/DictTag/index.vue
@@ -0,0 +1,85 @@
+<template>
+ <div>
+ <template v-for="(item, index) in options">
+ <template v-if="values.includes(item.value)">
+ <span v-if="(item.elTagType === 'default' || item.elTagType === '') && (item.elTagClass === '' || item.elTagClass == null)"
+ :key="item.value" :index="index" :class="item.elTagClass">
+ {{ item.label + " " }}
+ </span>
+ <el-tag
+ v-else
+ :disable-transitions="true"
+ :key="item.value + ''"
+ :index="index"
+ :type="(item.elTagType === 'primary' || item.elTagType === 'default')? '' : item.elTagType"
+ :class="item.elTagClass"
+ >
+ {{ item.label + " " }}
+ </el-tag>
+ </template>
+ </template>
+ <template v-if="unmatch && showValue">
+ {{ unmatchArray }}
+ </template>
+ </div>
+</template>
+
+<script setup lang="ts">
+import { propTypes } from '@/utils/propTypes';
+
+
+const props = defineProps({
+ // 数据
+ options: {
+ type: Array as PropType<DictDataOption[]>,
+ default: null,
+ },
+ // 当前的值
+ value: [Number, String, Array] as PropType<number | string | Array<number | string>>,
+ // 当未找到匹配的数据时,显示value
+ showValue: propTypes.bool.def(true),
+ separator: propTypes.string.def(","),
+});
+const values = computed(() => {
+ if (props.value === '' || props.value === null || typeof props.value === "undefined") return []
+ return Array.isArray(props.value) ? props.value.map(item => '' + item) : String(props.value).split(props.separator);
+});
+
+const unmatch = computed(() => {
+ if (props.options?.length == 0 || props.value === '' || props.value === null || typeof props.value === "undefined") return false
+ // 传入值为非数组
+ values.value.forEach(item => {
+ if (!props.options.some(v => v.value === item)) {
+ return true // 如果有未匹配项,将标志设置为true
+ }
+ })
+ return false // 返回标志的值
+});
+
+const unmatchArray = computed(() => {
+// 记录未匹配的项
+ const itemUnmatchArray: Array<string | number> = [];
+ if (props.value !== '' && props.value !== null && typeof props.value !== "undefined") {
+ values.value.forEach(item => {
+ if (!props.options.some(v => v.value === item)) {
+ itemUnmatchArray.push(item);
+ }
+ })
+ }
+ // 没有value不显示
+ return handleArray(itemUnmatchArray);
+});
+
+const handleArray = (array: Array<string | number>) => {
+ if (array.length === 0) return "";
+ return array.reduce((pre, cur) => {
+ return pre + " " + cur;
+ });
+}
+</script>
+
+<style scoped>
+.el-tag + .el-tag {
+ margin-left: 10px;
+}
+</style>
diff --git a/src/components/Editor/index.vue b/src/components/Editor/index.vue
new file mode 100644
index 0000000..b5299b4
--- /dev/null
+++ b/src/components/Editor/index.vue
@@ -0,0 +1,237 @@
+<template>
+ <div>
+ <el-upload
+ :action="upload.url"
+ :before-upload="handleBeforeUpload"
+ :on-success="handleUploadSuccess"
+ :on-error="handleUploadError"
+ class="editor-img-uploader"
+ name="file"
+ :show-file-list="false"
+ :headers="upload.headers"
+ ref="uploadRef"
+ v-if="type === 'url'"
+ >
+ </el-upload>
+ <div class="editor">
+ <quill-editor
+ ref="quillEditorRef"
+ v-model:content="content"
+ contentType="html"
+ @textChange="(e: any) => $emit('update:modelValue', content)"
+ :options="options"
+ :style="styles"
+ />
+ </div>
+ </div>
+</template>
+
+<script setup lang="ts">
+import { QuillEditor, Quill } from '@vueup/vue-quill';
+import '@vueup/vue-quill/dist/vue-quill.snow.css';
+import { propTypes } from '@/utils/propTypes';
+import { globalHeaders } from "@/utils/request";
+
+const props = defineProps({
+ /* 编辑器的内容 */
+ modelValue: propTypes.string,
+ /* 高度 */
+ height: propTypes.number.def(400),
+ /* 最小高度 */
+ minHeight: propTypes.number.def(400),
+ /* 只读 */
+ readOnly: propTypes.bool.def(false),
+ /* 上传文件大小限制(MB) */
+ fileSize: propTypes.number.def(5),
+ /* 类型(base64格式、url格式) */
+ type: propTypes.string.def('url')
+});
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+
+const upload = reactive<UploadOption>({
+ headers: globalHeaders(),
+ url: import.meta.env.VITE_APP_BASE_API + '/resource/oss/upload'
+})
+const quillEditorRef = ref();
+
+const options = ref({
+ theme: "snow",
+ bounds: document.body,
+ debug: "warn",
+ modules: {
+ // 工具栏配置
+ toolbar: {
+ container: [
+ ["bold", "italic", "underline", "strike"], // 加粗 斜体 下划线 删除线
+ ["blockquote", "code-block"], // 引用 代码块
+ [{ list: "ordered" }, { list: "bullet" }], // 有序、无序列表
+ [{ indent: "-1" }, { indent: "+1" }], // 缩进
+ [{ size: ["small", false, "large", "huge"] }], // 字体大小
+ [{ header: [1, 2, 3, 4, 5, 6, false] }], // 标题
+ [{ color: [] }, { background: [] }], // 字体颜色、字体背景颜色
+ [{ align: [] }], // 对齐方式
+ ["clean"], // 清除文本格式
+ ["link", "image", "video"] // 链接、图片、视频
+ ],
+ handlers: {
+ image: function (value: any) {
+ if (value) {
+ // 调用element图片上传
+ (document.querySelector(".editor-img-uploader>.el-upload") as HTMLDivElement)?.click();
+ } else {
+ Quill.format("image", true);
+ }
+ },
+ },
+ }
+ },
+ placeholder: "请输入内容",
+ readOnly: props.readOnly,
+});
+
+const styles = computed(() => {
+ let style: any = {};
+ if (props.minHeight) {
+ style.minHeight = `${props.minHeight}px`;
+ }
+ if (props.height) {
+ style.height = `${props.height}px`;
+ }
+ return style;
+})
+
+const content = ref("");
+watch(() => props.modelValue, (v) => {
+ if (v !== content.value) {
+ content.value = v === undefined ? "<p></p>" : v;
+ }
+}, { immediate: true });
+
+// 图片上传成功返回图片地址
+const handleUploadSuccess = (res: any) => {
+ // 如果上传成功
+ if (res.code === 200) {
+ // 获取富文本实例
+ let quill = toRaw(quillEditorRef.value).getQuill();
+ // 获取光标位置
+ let length = quill.selection.savedRange.index;
+ // 插入图片,res为服务器返回的图片链接地址
+ quill.insertEmbed(length, "image", res.data.url);
+ // 调整光标到最后
+ quill.setSelection(length + 1);
+ proxy?.$modal.closeLoading();
+ } else {
+ proxy?.$modal.loading(res.msg);
+ proxy?.$modal.closeLoading();
+ }
+}
+
+// 图片上传前拦截
+const handleBeforeUpload = (file: any) => {
+ const type = ["image/jpeg", "image/jpg", "image/png", "image/svg"];
+ const isJPG = type.includes(file.type);
+ //检验文件格式
+ if (!isJPG) {
+ proxy?.$modal.msgError(`图片格式错误!`);
+ return false;
+ }
+ // 校检文件大小
+ if (props.fileSize) {
+ const isLt = file.size / 1024 / 1024 < props.fileSize;
+ if (!isLt) {
+ proxy?.$modal.msgError(`上传文件大小不能超过 ${props.fileSize} MB!`);
+ return false;
+ }
+ }
+ proxy?.$modal.loading('正在上传文件,请稍候...');
+ return true;
+}
+
+// 图片失败拦截
+const handleUploadError = (err: any) => {
+ console.error(err);
+ proxy?.$modal.msgError('上传文件失败');
+}
+</script>
+
+<style>
+.editor-img-uploader {
+ display: none;
+}
+.editor,
+.ql-toolbar {
+ white-space: pre-wrap !important;
+ line-height: normal !important;
+}
+.quill-img {
+ display: none;
+}
+.ql-snow .ql-tooltip[data-mode="link"]::before {
+ content: "请输入链接地址:";
+}
+.ql-snow .ql-tooltip.ql-editing a.ql-action::after {
+ border-right: 0;
+ content: "保存";
+ padding-right: 0;
+}
+.ql-snow .ql-tooltip[data-mode="video"]::before {
+ content: "请输入视频地址:";
+}
+.ql-snow .ql-picker.ql-size .ql-picker-label::before,
+.ql-snow .ql-picker.ql-size .ql-picker-item::before {
+ content: "14px";
+}
+.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="small"]::before,
+.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="small"]::before {
+ content: "10px";
+}
+.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="large"]::before,
+.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="large"]::before {
+ content: "18px";
+}
+.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="huge"]::before,
+.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="huge"]::before {
+ content: "32px";
+}
+.ql-snow .ql-picker.ql-header .ql-picker-label::before,
+.ql-snow .ql-picker.ql-header .ql-picker-item::before {
+ content: "文本";
+}
+.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="1"]::before,
+.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="1"]::before {
+ content: "标题1";
+}
+.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="2"]::before,
+.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="2"]::before {
+ content: "标题2";
+}
+.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="3"]::before,
+.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="3"]::before {
+ content: "标题3";
+}
+.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="4"]::before,
+.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="4"]::before {
+ content: "标题4";
+}
+.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="5"]::before,
+.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="5"]::before {
+ content: "标题5";
+}
+.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="6"]::before,
+.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="6"]::before {
+ content: "标题6";
+}
+.ql-snow .ql-picker.ql-font .ql-picker-label::before,
+.ql-snow .ql-picker.ql-font .ql-picker-item::before {
+ content: "标准字体";
+}
+.ql-snow .ql-picker.ql-font .ql-picker-label[data-value="serif"]::before,
+.ql-snow .ql-picker.ql-font .ql-picker-item[data-value="serif"]::before {
+ content: "衬线字体";
+}
+.ql-snow .ql-picker.ql-font .ql-picker-label[data-value="monospace"]::before,
+.ql-snow .ql-picker.ql-font .ql-picker-item[data-value="monospace"]::before {
+ content: "等宽字体";
+}
+</style>
diff --git a/src/components/FileUpload/index.vue b/src/components/FileUpload/index.vue
new file mode 100644
index 0000000..be99ac0
--- /dev/null
+++ b/src/components/FileUpload/index.vue
@@ -0,0 +1,217 @@
+<template>
+ <div class="upload-file">
+ <el-upload
+ multiple
+ :action="uploadFileUrl"
+ :before-upload="handleBeforeUpload"
+ :file-list="fileList"
+ :limit="limit"
+ :on-error="handleUploadError"
+ :on-exceed="handleExceed"
+ :on-success="handleUploadSuccess"
+ :show-file-list="false"
+ :headers="headers"
+ class="upload-file-uploader"
+ ref="fileUploadRef"
+ >
+ <!-- 上传按钮 -->
+ <el-button type="primary">选取文件</el-button>
+ </el-upload>
+ <!-- 上传提示 -->
+ <div class="el-upload__tip" v-if="showTip">
+ 请上传
+ <template v-if="fileSize">
+ 大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b>
+ </template>
+ <template v-if="fileType">
+ 格式为 <b style="color: #f56c6c">{{ fileType.join("/") }}</b>
+ </template>
+ 的文件
+ </div>
+ <!-- 文件列表 -->
+ <transition-group class="upload-file-list el-upload-list el-upload-list--text" name="el-fade-in-linear" tag="ul">
+ <li :key="file.uid" class="el-upload-list__item ele-upload-list__item-content" v-for="(file, index) in fileList">
+ <el-link :href="`${file.url}`" :underline="false" target="_blank">
+ <span class="el-icon-document"> {{ getFileName(file.name) }} </span>
+ </el-link>
+ <div class="ele-upload-list__item-content-action">
+ <el-link :underline="false" @click="handleDelete(index)" type="danger">删除</el-link>
+ </div>
+ </li>
+ </transition-group>
+ </div>
+</template>
+
+<script setup lang="ts">
+import { listByIds, delOss } from "@/api/system/oss";
+import { propTypes } from '@/utils/propTypes';
+import { globalHeaders } from "@/utils/request";
+
+const props = defineProps({
+ modelValue: [String, Object, Array],
+ // 数量限制
+ limit: propTypes.number.def(5),
+ // 大小限制(MB)
+ fileSize: propTypes.number.def(5),
+ // 文件类型, 例如['png', 'jpg', 'jpeg']
+ fileType: propTypes.array.def(["doc", "xls", "ppt", "txt", "pdf"]),
+ // 是否显示提示
+ isShowTip: propTypes.bool.def(true),
+});
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const emit = defineEmits(['update:modelValue']);
+const number = ref(0);
+const uploadList = ref<any[]>([]);
+
+const baseUrl = import.meta.env.VITE_APP_BASE_API;
+const uploadFileUrl = ref(baseUrl + "/resource/oss/upload"); // 上传文件服务器地址
+const headers = ref(globalHeaders());
+
+const fileList = ref<any[]>([]);
+const showTip = computed(
+ () => props.isShowTip && (props.fileType || props.fileSize)
+);
+
+const fileUploadRef = ref<ElUploadInstance>();
+
+watch(() => props.modelValue, async val => {
+ if (val) {
+ let temp = 1;
+ // 首先将值转为数组
+ let list = [];
+ if (Array.isArray(val)) {
+ list = val;
+ } else {
+ const res = await listByIds(val as string)
+ list = res.data.map((oss) => {
+ const data = { name: oss.originalName, url: oss.url, ossId: oss.ossId };
+ return data;
+ });
+ }
+ // 然后将数组转为对象数组
+ fileList.value = list.map(item => {
+ item = { name: item.name, url: item.url, ossId: item.ossId };
+ item.uid = item.uid || new Date().getTime() + temp++;
+ return item;
+ });
+ } else {
+ fileList.value = [];
+ return [];
+ }
+}, { deep: true, immediate: true });
+
+// 上传前校检格式和大小
+const handleBeforeUpload = (file: any) => {
+ // 校检文件类型
+ if (props.fileType.length) {
+ const fileName = file.name.split('.');
+ const fileExt = fileName[fileName.length - 1];
+ const isTypeOk = props.fileType.indexOf(fileExt) >= 0;
+ if (!isTypeOk) {
+ proxy?.$modal.msgError(`文件格式不正确, 请上传${props.fileType.join("/")}格式文件!`);
+ return false;
+ }
+ }
+ // 校检文件大小
+ if (props.fileSize) {
+ const isLt = file.size / 1024 / 1024 < props.fileSize;
+ if (!isLt) {
+ proxy?.$modal.msgError(`上传文件大小不能超过 ${props.fileSize} MB!`);
+ return false;
+ }
+ }
+ proxy?.$modal.loading("正在上传文件,请稍候...");
+ number.value++;
+ return true;
+}
+
+// 文件个数超出
+const handleExceed = () => {
+ proxy?.$modal.msgError(`上传文件数量不能超过 ${props.limit} 个!`);
+}
+
+// 上传失败
+const handleUploadError = () => {
+ proxy?.$modal.msgError("上传文件失败");
+}
+
+// 上传成功回调
+const handleUploadSuccess = (res: any, file: UploadFile) => {
+ if (res.code === 200) {
+ uploadList.value.push({ name: res.data.fileName, url: res.data.url, ossId: res.data.ossId });
+ uploadedSuccessfully();
+ } else {
+ number.value--;
+ proxy?.$modal.closeLoading();
+ proxy?.$modal.msgError(res.msg);
+ fileUploadRef.value?.handleRemove(file);
+ uploadedSuccessfully();
+ }
+}
+
+// 删除文件
+const handleDelete = (index: number) => {
+ let ossId = fileList.value[index].ossId;
+ delOss(ossId);
+ fileList.value.splice(index, 1);
+ emit("update:modelValue", listToString(fileList.value));
+}
+
+// 上传结束处理
+const uploadedSuccessfully = () => {
+ if (number.value > 0 && uploadList.value.length === number.value) {
+ fileList.value = fileList.value.filter(f => f.url !== undefined).concat(uploadList.value);
+ uploadList.value = [];
+ number.value = 0;
+ emit("update:modelValue", listToString(fileList.value));
+ proxy?.$modal.closeLoading();
+ }
+}
+
+// 获取文件名称
+const getFileName = (name: string) => {
+ // 如果是url那么取最后的名字 如果不是直接返回
+ if (name.lastIndexOf("/") > -1) {
+ return name.slice(name.lastIndexOf("/") + 1);
+ } else {
+ return name;
+ }
+}
+
+// 对象转成指定字符串分隔
+const listToString = (list: any[], separator?: string) => {
+ let strs = "";
+ separator = separator || ",";
+ list.forEach(item => {
+ if (item.ossId) {
+ strs += item.ossId + separator;
+ }
+ })
+ return strs != "" ? strs.substring(0, strs.length - 1) : "";
+}
+</script>
+
+<style scoped lang="scss">
+.upload-file-uploader {
+ margin-bottom: 5px;
+}
+
+.upload-file-list .el-upload-list__item {
+ border: 1px solid #e4e7ed;
+ line-height: 2;
+ margin-bottom: 10px;
+ position: relative;
+}
+
+.upload-file-list .ele-upload-list__item-content {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ color: inherit;
+}
+
+.ele-upload-list__item-content-action .el-link {
+ margin-right: 10px;
+}
+</style>
diff --git a/src/components/Hamburger/index.vue b/src/components/Hamburger/index.vue
new file mode 100644
index 0000000..489ac43
--- /dev/null
+++ b/src/components/Hamburger/index.vue
@@ -0,0 +1,35 @@
+<template>
+ <div style="padding: 0 15px;" @click="toggleClick">
+ <svg :class="{ 'is-active': isActive }" class="hamburger" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="64" height="64">
+ <path
+ d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM142.4 642.1L298.7 519a8.84 8.84 0 0 0 0-13.9L142.4 381.9c-5.8-4.6-14.4-.5-14.4 6.9v246.3a8.9 8.9 0 0 0 14.4 7z"
+ />
+ </svg>
+ </div>
+</template>
+
+<script setup lang="ts">
+import { propTypes } from '@/utils/propTypes';
+
+defineProps({
+ isActive: propTypes.bool.def(false)
+})
+
+const emit = defineEmits(['toggleClick'])
+const toggleClick = () => {
+ emit('toggleClick');
+}
+</script>
+
+<style scoped>
+.hamburger {
+ display: inline-block;
+ vertical-align: middle;
+ width: 20px;
+ height: 20px;
+}
+
+.hamburger.is-active {
+ transform: rotate(180deg);
+}
+</style>
diff --git a/src/components/HeaderSearch/index.vue b/src/components/HeaderSearch/index.vue
new file mode 100644
index 0000000..b922c6c
--- /dev/null
+++ b/src/components/HeaderSearch/index.vue
@@ -0,0 +1,193 @@
+<template>
+ <div :class="{ 'show': show }" class="header-search">
+ <svg-icon class-name="search-icon" icon-class="search" @click.stop="click"/>
+ <el-select
+ ref="headerSearchSelectRef"
+ v-model="search"
+ :remote-method="querySearch"
+ filterable
+ default-first-option
+ remote
+ placeholder="Search"
+ class="header-search-select"
+ @change="change"
+ >
+ <el-option v-for="option in options" :key="option.item.path" :value="option.item"
+ :label="option.item.title.join(' > ')"/>
+ </el-select>
+ </div>
+</template>
+
+<script setup lang="ts" name="HeaderSearch">
+import Fuse from 'fuse.js';
+import {getNormalPath} from '@/utils/ruoyi';
+import {isHttp} from '@/utils/validate';
+import usePermissionStore from '@/store/modules/permission';
+import {RouteOption} from 'vue-router';
+
+type Router = Array<{
+ path: string;
+ title: string[];
+}>
+
+const search = ref('');
+const options = ref<any>([]);
+const searchPool = ref<Router>([]);
+const show = ref(false);
+const fuse = ref();
+const headerSearchSelectRef = ref<ElSelectInstance>();
+const router = useRouter();
+const routes = computed(() => usePermissionStore().routes);
+
+const click = () => {
+ show.value = !show.value
+ if (show.value) {
+ headerSearchSelectRef.value && headerSearchSelectRef.value.focus()
+ }
+};
+const close = () => {
+ headerSearchSelectRef.value && headerSearchSelectRef.value.blur()
+ options.value = []
+ show.value = false
+}
+const change = (val: any) => {
+ const path = val.path;
+ const query = val.query;
+ if (isHttp(path)) {
+ // http(s):// 路径新窗口打开
+ const pindex = path.indexOf("http");
+ window.open(path.substr(pindex, path.length), "_blank");
+ } else {
+ if (query) {
+ router.push({ path: path, query: JSON.parse(query) });
+ } else {
+ router.push(path)
+ }
+ }
+ search.value = ''
+ options.value = []
+ nextTick(() => {
+ show.value = false
+ })
+}
+const initFuse = (list: Router) => {
+ fuse.value = new Fuse(list, {
+ shouldSort: true,
+ threshold: 0.4,
+ location: 0,
+ distance: 100,
+ minMatchCharLength: 1,
+ keys: [{
+ name: 'title',
+ weight: 0.7
+ }, {
+ name: 'path',
+ weight: 0.3
+ }]
+ })
+}
+// Filter out the routes that can be displayed in the sidebar
+// And generate the internationalized title
+const generateRoutes = (routes: RouteOption[], basePath = '', prefixTitle: string[] = []) => {
+ let res: Router = []
+ routes.forEach(r => {
+ // skip hidden router
+ if (!r.hidden) {
+ const p = r.path.length > 0 && r.path[0] === '/' ? r.path : '/' + r.path;
+ const data = {
+ path: !isHttp(r.path) ? getNormalPath(basePath + p) : r.path,
+ title: [...prefixTitle],
+ query: ''
+ }
+ if (r.meta && r.meta.title) {
+ data.title = [...data.title, r.meta.title];
+ if (r.redirect !== 'noRedirect') {
+ // only push the routes with title
+ // special case: need to exclude parent router without redirect
+ res.push(data);
+ }
+ }
+
+ if (r.query) {
+ data.query = r.query
+ }
+
+ // recursive child routes
+ if (r.children) {
+ const tempRoutes = generateRoutes(r.children, data.path, data.title);
+ if (tempRoutes.length >= 1) {
+ res = [...res, ...tempRoutes];
+ }
+ }
+ }
+ })
+ return res;
+}
+const querySearch = (query: string) => {
+ if (query !== '') {
+ options.value = fuse.value.search(query)
+ } else {
+ options.value = []
+ }
+}
+
+onMounted(() => {
+ searchPool.value = generateRoutes(routes.value);
+})
+
+// watchEffect(() => {
+// searchPool.value = generateRoutes(routes.value)
+// })
+
+watch(show, (value) => {
+ if (value) {
+ document.body.addEventListener('click', close)
+ } else {
+ document.body.removeEventListener('click', close)
+ }
+})
+
+watch(searchPool, (list) => {
+ initFuse(list)
+})
+</script>
+
+<style lang="scss" scoped>
+.header-search {
+ font-size: 0 !important;
+
+ .search-icon {
+ cursor: pointer;
+ font-size: 18px;
+ vertical-align: middle;
+ }
+
+ .header-search-select {
+ font-size: 18px;
+ transition: width 0.2s;
+ width: 0;
+ overflow: hidden;
+ background: transparent;
+ border-radius: 0;
+ display: inline-block;
+ vertical-align: middle;
+
+ :deep(.el-input__inner) {
+ border-radius: 0;
+ border: 0;
+ padding-left: 0;
+ padding-right: 0;
+ box-shadow: none !important;
+ border-bottom: 1px solid #d9d9d9;
+ vertical-align: middle;
+ }
+ }
+
+ &.show {
+ .header-search-select {
+ width: 210px;
+ margin-left: 10px;
+ }
+ }
+}
+</style>
diff --git a/src/components/IconSelect/index.vue b/src/components/IconSelect/index.vue
new file mode 100644
index 0000000..1cd0023
--- /dev/null
+++ b/src/components/IconSelect/index.vue
@@ -0,0 +1,106 @@
+<template>
+ <div class="relative" :style="{ width: width }">
+ <el-input v-model="modelValue" readonly @click="visible = !visible" placeholder="点击选择图标">
+ <template #prepend>
+ <svg-icon :icon-class="modelValue" />
+ </template>
+ </el-input>
+
+ <el-popover shadow="none" :visible="visible" placement="bottom-end" trigger="click" :width="450">
+ <template #reference>
+ <div @click="visible = !visible" class="cursor-pointer text-[#999] absolute right-[10px] top-0 height-[32px] leading-[32px]">
+ <i-ep-caret-top v-show="visible"></i-ep-caret-top>
+ <i-ep-caret-bottom v-show="!visible"></i-ep-caret-bottom>
+ </div>
+ </template>
+
+ <el-input class="p-2" v-model="filterValue" placeholder="搜索图标" clearable @input="filterIcons" />
+
+ <el-scrollbar height="w-[200px]">
+ <ul class="icon-list">
+ <el-tooltip v-for="(iconName, index) in iconNames" :key="index" :content="iconName" placement="bottom" effect="light">
+ <li :class="['icon-item', {active: modelValue == iconName}]" @click="selectedIcon(iconName)">
+ <svg-icon color="var(--el-text-color-regular)" :icon-class="iconName" />
+ </li>
+ </el-tooltip>
+ </ul>
+ </el-scrollbar>
+ </el-popover>
+ </div>
+</template>
+
+<script setup lang="ts">
+import icons from '@/components/IconSelect/requireIcons';
+import { propTypes } from '@/utils/propTypes';
+
+const props = defineProps({
+ modelValue: propTypes.string.isRequired,
+ width: propTypes.string.def('400px')
+});
+
+const emit = defineEmits(['update:modelValue']);
+const visible = ref(false);
+const { modelValue, width } = toRefs(props);
+const iconNames = ref<string[]>(icons);
+
+const filterValue = ref('');
+
+/**
+ * 筛选图标
+ */
+const filterIcons = () => {
+ if (filterValue.value) {
+ iconNames.value = icons.filter(iconName =>
+ iconName.includes(filterValue.value)
+ );
+ } else {
+ iconNames.value = icons;
+ }
+}
+/**
+ * 选择图标
+ * @param iconName 选择的图标名称
+ */
+const selectedIcon = (iconName: string) => {
+ emit('update:modelValue', iconName);
+ visible.value = false;
+}
+</script>
+
+<style scoped lang="scss">
+.el-scrollbar {
+ max-height: calc(50vh - 100px)!important;
+ overflow-y: auto;
+}
+.el-divider--horizontal {
+ margin: 10px auto !important;
+}
+.icon-list {
+ display: flex;
+ flex-wrap: wrap;
+ padding-left: 10px;
+ margin-top: 10px;
+
+ .icon-item {
+ cursor: pointer;
+ width: 10%;
+ margin: 0 10px 10px 0;
+ padding: 5px;
+ display: flex;
+ flex-direction: column;
+ justify-items: center;
+ align-items: center;
+ border: 1px solid #ccc;
+ &:hover {
+ border-color: var(--el-color-primary);
+ color: var(--el-color-primary);
+ transition: all 0.2s;
+ transform: scaleX(1.1);
+ }
+ }
+ .active {
+ border-color: var(--el-color-primary);
+ color: var(--el-color-primary);
+ }
+}
+</style>
diff --git a/src/components/IconSelect/requireIcons.ts b/src/components/IconSelect/requireIcons.ts
new file mode 100644
index 0000000..9f06d69
--- /dev/null
+++ b/src/components/IconSelect/requireIcons.ts
@@ -0,0 +1,7 @@
+const icons: string[] = [];
+const modules = import.meta.glob('./../../assets/icons/svg/*.svg');
+for (const path in modules) {
+ const p = path.split('assets/icons/svg/')[1].split('.svg')[0];
+ icons.push(p);
+}
+export default icons;
diff --git a/src/components/ImagePreview/index.vue b/src/components/ImagePreview/index.vue
new file mode 100644
index 0000000..723ccdb
--- /dev/null
+++ b/src/components/ImagePreview/index.vue
@@ -0,0 +1,80 @@
+<template>
+ <el-image :src="`${realSrc}`" fit="cover" :style="`width:${realWidth};height:${realHeight};`" :preview-src-list="realSrcList" preview-teleported>
+ <template #error>
+ <div class="image-slot">
+ <el-icon><picture-filled /></el-icon>
+ </div>
+ </template>
+ </el-image>
+</template>
+
+<script setup lang="ts">
+import { propTypes } from '@/utils/propTypes';
+
+const props = defineProps({
+ src: propTypes.string.def(''),
+ width: {
+ type: [Number, String],
+ default: ""
+ },
+ height: {
+ type: [Number, String],
+ default: ""
+ }
+});
+
+const realSrc = computed(() => {
+ if (!props.src) {
+ return;
+ }
+ let real_src = props.src.split(",")[0];
+ return real_src;
+});
+
+const realSrcList = computed(() => {
+ if (!props.src) {
+ return;
+ }
+ let real_src_list = props.src.split(",");
+ let srcList: string[] = [];
+ real_src_list.forEach(item => {
+ return srcList.push(item);
+ });
+ return srcList;
+});
+
+const realWidth = computed(() =>
+ typeof props.width == "string" ? props.width : `${props.width}px`
+);
+
+const realHeight = computed(() =>
+ typeof props.height == "string" ? props.height : `${props.height}px`
+);
+</script>
+
+<style lang="scss" scoped>
+.el-image {
+ border-radius: 5px;
+ background-color: #ebeef5;
+ box-shadow: 0 0 5px 1px #ccc;
+
+ :deep(.el-image__inner) {
+ transition: all 0.3s;
+ cursor: pointer;
+
+ &:hover {
+ transform: scale(1.2);
+ }
+ }
+
+ :deep(.image-slot) {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 100%;
+ height: 100%;
+ color: #909399;
+ font-size: 30px;
+ }
+}
+</style>
diff --git a/src/components/ImageUpload/index.vue b/src/components/ImageUpload/index.vue
new file mode 100644
index 0000000..d8ed32b
--- /dev/null
+++ b/src/components/ImageUpload/index.vue
@@ -0,0 +1,216 @@
+<template>
+ <div class="component-upload-image">
+ <el-upload
+ multiple
+ :action="uploadImgUrl"
+ list-type="picture-card"
+ :on-success="handleUploadSuccess"
+ :before-upload="handleBeforeUpload"
+ :limit="limit"
+ :on-error="handleUploadError"
+ :on-exceed="handleExceed"
+ ref="imageUpload"
+ :before-remove="handleDelete"
+ :show-file-list="true"
+ :headers="headers"
+ :file-list="fileList"
+ :on-preview="handlePictureCardPreview"
+ :class="{ hide: fileList.length >= limit }"
+ >
+ <el-icon class="avatar-uploader-icon">
+ <plus />
+ </el-icon>
+ </el-upload>
+ <!-- 上传提示 -->
+ <div class="el-upload__tip" v-if="showTip">
+ 请上传
+ <template v-if="fileSize">
+ 大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b>
+ </template>
+ <template v-if="fileType">
+ 格式为 <b style="color: #f56c6c">{{ fileType.join("/") }}</b>
+ </template>
+ 的文件
+ </div>
+
+ <el-dialog v-model="dialogVisible" title="预览" width="800px" append-to-body>
+ <img :src="dialogImageUrl" style="display: block; max-width: 100%; margin: 0 auto" />
+ </el-dialog>
+ </div>
+</template>
+
+<script setup lang="ts">
+import { listByIds, delOss } from "@/api/system/oss";
+import { ComponentInternalInstance } from "vue";
+import { OssVO } from "@/api/system/oss/types";
+import { propTypes } from '@/utils/propTypes';
+import {globalHeaders} from "@/utils/request";
+
+const props = defineProps({
+ modelValue: [String, Object, Array],
+ // 图片数量限制
+ limit: propTypes.number.def(5),
+ // 大小限制(MB)
+ fileSize: propTypes.number.def(5),
+ // 文件类型, 例如['png', 'jpg', 'jpeg']
+ fileType: propTypes.array.def(["png", "jpg", "jpeg"]),
+ // 是否显示提示
+ isShowTip: {
+ type: Boolean,
+ default: true
+ },
+});
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const emit = defineEmits(['update:modelValue']);
+const number = ref(0);
+const uploadList = ref<any[]>([]);
+const dialogImageUrl = ref("");
+const dialogVisible = ref(false);
+
+const baseUrl = import.meta.env.VITE_APP_BASE_API;
+const uploadImgUrl = ref(baseUrl + "/resource/oss/upload"); // 上传的图片服务器地址
+const headers = ref(globalHeaders());
+
+const fileList = ref<any[]>([]);
+const showTip = computed(
+ () => props.isShowTip && (props.fileType || props.fileSize)
+);
+
+const imageUploadRef = ref<ElUploadInstance>();
+
+watch(() => props.modelValue, async val => {
+ if (val) {
+ // 首先将值转为数组
+ let list: OssVO[] = [];
+ if (Array.isArray(val)) {
+ list = val as OssVO[];
+ } else {
+ const res = await listByIds(val as string)
+ list = res.data
+ }
+ // 然后将数组转为对象数组
+ fileList.value = list.map(item => {
+ // 字符串回显处理 如果此处存的是url可直接回显 如果存的是id需要调用接口查出来
+ let itemData;
+ if (typeof item === "string") {
+ itemData = { name: item, url: item };
+ } else {
+ // 此处name使用ossId 防止删除出现重名
+ itemData = { name: item.ossId, url: item.url, ossId: item.ossId };
+ }
+ return itemData;
+ });
+ } else {
+ fileList.value = [];
+ return [];
+ }
+}, { deep: true, immediate: true });
+
+/** 上传前loading加载 */
+const handleBeforeUpload = (file: any) => {
+ let isImg = false;
+ if (props.fileType.length) {
+ let fileExtension = "";
+ if (file.name.lastIndexOf(".") > -1) {
+ fileExtension = file.name.slice(file.name.lastIndexOf(".") + 1);
+ }
+ isImg = props.fileType.some((type: any) => {
+ if (file.type.indexOf(type) > -1) return true;
+ if (fileExtension && fileExtension.indexOf(type) > -1) return true;
+ return false;
+ });
+ } else {
+ isImg = file.type.indexOf("image") > -1;
+ }
+ if (!isImg) {
+ proxy?.$modal.msgError(
+ `文件格式不正确, 请上传${props.fileType.join("/")}图片格式文件!`
+ );
+ return false;
+ }
+ if (props.fileSize) {
+ const isLt = file.size / 1024 / 1024 < props.fileSize;
+ if (!isLt) {
+ proxy?.$modal.msgError(`上传头像图片大小不能超过 ${props.fileSize} MB!`);
+ return false;
+ }
+ }
+ proxy?.$modal.loading("正在上传图片,请稍候...");
+ number.value++;
+}
+
+// 文件个数超出
+const handleExceed = () => {
+ proxy?.$modal.msgError(`上传文件数量不能超过 ${props.limit} 个!`);
+}
+
+// 上传成功回调
+const handleUploadSuccess = (res: any, file: UploadFile) => {
+ if (res.code === 200) {
+ uploadList.value.push({ name: res.data.fileName, url: res.data.url, ossId: res.data.ossId });
+ uploadedSuccessfully();
+ } else {
+ number.value--;
+ proxy?.$modal.closeLoading();
+ proxy?.$modal.msgError(res.msg);
+ imageUploadRef.value?.handleRemove(file);
+ uploadedSuccessfully();
+ }
+}
+
+// 删除图片
+const handleDelete = (file: UploadFile): boolean => {
+ const findex = fileList.value.map(f => f.name).indexOf(file.name);
+ if (findex > -1 && uploadList.value.length === number.value) {
+ let ossId = fileList.value[findex].ossId;
+ delOss(ossId);
+ fileList.value.splice(findex, 1);
+ emit("update:modelValue", listToString(fileList.value));
+ return false;
+ }
+ return true;
+}
+
+// 上传结束处理
+const uploadedSuccessfully = () => {
+ if (number.value > 0 && uploadList.value.length === number.value) {
+ fileList.value = fileList.value.filter(f => f.url !== undefined).concat(uploadList.value);
+ uploadList.value = [];
+ number.value = 0;
+ emit("update:modelValue", listToString(fileList.value));
+ proxy?.$modal.closeLoading();
+ }
+}
+
+// 上传失败
+const handleUploadError = () => {
+ proxy?.$modal.msgError("上传图片失败");
+ proxy?.$modal.closeLoading();
+}
+
+// 预览
+const handlePictureCardPreview = (file: any) => {
+ dialogImageUrl.value = file.url;
+ dialogVisible.value = true;
+}
+
+// 对象转成指定字符串分隔
+const listToString = (list: any[], separator?: string) => {
+ let strs = "";
+ separator = separator || ",";
+ for (let i in list) {
+ if (undefined !== list[i].ossId && list[i].url.indexOf("blob:") !== 0) {
+ strs += list[i].ossId + separator;
+ }
+ }
+ return strs != "" ? strs.substring(0, strs.length - 1) : "";
+}
+</script>
+
+<style scoped lang="scss">
+// .el-upload--picture-card 控制加号部分
+:deep(.hide .el-upload--picture-card) {
+ display: none;
+}
+</style>
diff --git a/src/components/LangSelect/index.vue b/src/components/LangSelect/index.vue
new file mode 100644
index 0000000..a30a2f6
--- /dev/null
+++ b/src/components/LangSelect/index.vue
@@ -0,0 +1,40 @@
+<template>
+ <el-dropdown trigger="click" @command="handleLanguageChange">
+ <div class="lang-select--style">
+ <svg-icon icon-class="language" />
+ </div>
+ <template #dropdown>
+ <el-dropdown-menu>
+ <el-dropdown-item :disabled="appStore.language === 'zh_CN'" command="zh_CN"> 中文 </el-dropdown-item>
+ <el-dropdown-item :disabled="appStore.language === 'en_US'" command="en_US"> English </el-dropdown-item>
+ </el-dropdown-menu>
+ </template>
+ </el-dropdown>
+</template>
+
+<script setup lang="ts">
+import { useI18n } from 'vue-i18n';
+import SvgIcon from '@/components/SvgIcon/index.vue';
+import { useAppStore } from '@/store/modules/app';
+
+const appStore = useAppStore();
+const { locale } = useI18n();
+
+
+const message: any = {
+ zh_CN: '切换语言成功!',
+ en_US: 'Switch Language Successful!',
+}
+const handleLanguageChange = (lang: string) => {
+ locale.value = lang;
+ appStore.changeLanguage(lang);
+ ElMessage.success(message[lang] || '切换语言成功!');
+}
+</script>
+
+<style lang="scss" scoped>
+.lang-select--style {
+ font-size: 18px;
+ line-height: 50px;
+}
+</style>
diff --git a/src/components/Pagination/index.vue b/src/components/Pagination/index.vue
new file mode 100644
index 0000000..df73ceb
--- /dev/null
+++ b/src/components/Pagination/index.vue
@@ -0,0 +1,88 @@
+<template>
+ <div :class="{ 'hidden': hidden }" class="pagination-container">
+ <el-pagination
+ :background="background"
+ v-model:current-page="currentPage"
+ v-model:page-size="pageSize"
+ :layout="layout"
+ :page-sizes="pageSizes"
+ :pager-count="pagerCount"
+ :total="total"
+ @size-change="handleSizeChange"
+ @current-change="handleCurrentChange"
+ />
+ </div>
+</template>
+
+<script lang="ts">
+export default {
+ name: 'Pagination'
+}
+</script>
+
+<script setup lang="ts">
+import { scrollTo } from '@/utils/scroll-to'
+import { propTypes } from "@/utils/propTypes";
+
+const props = defineProps({
+ total: propTypes.number,
+ page: propTypes.number.def(1),
+ limit: propTypes.number.def(20),
+ pageSizes: {
+ type: Array as PropType<number[]>,
+ default: () => [10, 20, 30, 50]
+ },
+ // 移动端页码按钮的数量端默认值5
+ pagerCount: propTypes.number.def(document.body.clientWidth < 992 ? 5 : 7),
+ layout: propTypes.string.def('total, sizes, prev, pager, next, jumper'),
+ background: propTypes.bool.def(true),
+ autoScroll: propTypes.bool.def(true),
+ hidden: propTypes.bool.def(false),
+ float: propTypes.string.def('right')
+})
+
+const emit = defineEmits(['update:page', 'update:limit', 'pagination']);
+const currentPage = computed({
+ get() {
+ return props.page
+ },
+ set(val) {
+ emit('update:page', val)
+ }
+})
+const pageSize = computed({
+ get() {
+ return props.limit
+ },
+ set(val){
+ emit('update:limit', val)
+ }
+})
+function handleSizeChange(val: number) {
+ if (currentPage.value * val > props.total) {
+ currentPage.value = 1
+ }
+ emit('pagination', { page: currentPage.value, limit: val })
+ if (props.autoScroll) {
+ scrollTo(0, 800)
+ }
+}
+function handleCurrentChange(val: number) {
+ emit('pagination', { page: val, limit: pageSize.value })
+ if (props.autoScroll) {
+ scrollTo(0, 800)
+ }
+}
+</script>
+
+<style lang="scss" scoped>
+.pagination-container {
+ padding: 32px 16px;
+ .el-pagination{
+ float: v-bind(float);
+ }
+}
+.pagination-container.hidden {
+ display: none;
+}
+</style>
diff --git a/src/components/ParentView/index.vue b/src/components/ParentView/index.vue
new file mode 100644
index 0000000..98240ae
--- /dev/null
+++ b/src/components/ParentView/index.vue
@@ -0,0 +1,3 @@
+<template>
+ <router-view />
+</template>
diff --git a/src/components/RightToolbar/index.vue b/src/components/RightToolbar/index.vue
new file mode 100644
index 0000000..6be382f
--- /dev/null
+++ b/src/components/RightToolbar/index.vue
@@ -0,0 +1,104 @@
+<template>
+ <div class="top-right-btn" :style="style">
+ <el-row>
+ <el-tooltip class="item" effect="dark" :content="showSearch ? '隐藏搜索' : '显示搜索'" placement="top" v-if="search">
+ <el-button circle icon="Search" @click="toggleSearch()" />
+ </el-tooltip>
+ <el-tooltip class="item" effect="dark" content="刷新" placement="top">
+ <el-button circle icon="Refresh" @click="refresh()" />
+ </el-tooltip>
+ <el-tooltip class="item" effect="dark" content="显示/隐藏列" placement="top" v-if="columns">
+ <div class="show-btn">
+ <el-popover placement="bottom" trigger="click">
+ <div class="tree-header">显示/隐藏列</div>
+ <el-tree
+ ref="columnRef"
+ :data="columns"
+ show-checkbox
+ @check="columnChange"
+ node-key="key"
+ :props="{ label: 'label', children: 'children' }"
+ ></el-tree>
+ <template #reference>
+ <el-button circle icon="Menu" />
+ </template>
+ </el-popover>
+ </div>
+ </el-tooltip>
+ </el-row>
+ </div>
+</template>
+
+<script setup lang="ts">
+import { propTypes } from '@/utils/propTypes';
+
+const props = defineProps({
+ showSearch: propTypes.bool.def(true),
+ columns: {
+ type: Array as PropType<FieldOption[]>,
+ },
+ search: propTypes.bool.def(true),
+ gutter: propTypes.number.def(10),
+})
+
+const columnRef = ref<ElTreeInstance>();
+const emits = defineEmits(['update:showSearch', 'queryTable']);
+
+const style = computed(() => {
+ const ret: any = {};
+ if (props.gutter) {
+ ret.marginRight = `${props.gutter / 2}px`;
+ }
+ return ret;
+});
+
+// 搜索
+function toggleSearch() {
+ emits("update:showSearch", !props.showSearch);
+}
+
+// 刷新
+function refresh() {
+ emits("queryTable");
+}
+
+// 更改数据列的显示和隐藏
+function columnChange(...args: any[]) {
+ props.columns?.forEach((item) => {
+ item.visible = args[1].checkedKeys.includes(item.key);
+ })
+}
+
+// 显隐列初始默认隐藏列
+onMounted(() => {
+ props.columns?.forEach((item) => {
+ if (item.visible) {
+ columnRef.value?.setChecked(item.key, true, false);
+ // value.value.push(item.key);
+ }
+ })
+})
+</script>
+
+<style lang="scss" scoped>
+:deep(.el-transfer__button) {
+ border-radius: 50%;
+ display: block;
+ margin-left: 0px;
+}
+:deep(.el-transfer__button:first-child) {
+ margin-bottom: 10px;
+}
+
+.my-el-transfer {
+ text-align: center;
+}
+.tree-header{
+ width: 100%;
+ line-height: 24px;
+ text-align: center;
+}
+.show-btn {
+ margin-left: 12px;
+}
+</style>
diff --git a/src/components/Screenfull/index.vue b/src/components/Screenfull/index.vue
new file mode 100644
index 0000000..ce0b373
--- /dev/null
+++ b/src/components/Screenfull/index.vue
@@ -0,0 +1,9 @@
+<template>
+ <div>
+ <svg-icon :icon-class="isFullscreen ? 'exit-fullscreen' : 'fullscreen'" @click="toggle" />
+ </div>
+</template>
+
+<script setup lang="ts">
+const { isFullscreen, toggle } = useFullscreen();
+</script>
diff --git a/src/components/SizeSelect/index.vue b/src/components/SizeSelect/index.vue
new file mode 100644
index 0000000..058ff71
--- /dev/null
+++ b/src/components/SizeSelect/index.vue
@@ -0,0 +1,41 @@
+<template>
+ <div>
+ <el-dropdown trigger="click" @command="handleSetSize">
+ <div class="size-icon--style">
+ <svg-icon class-name="size-icon" icon-class="size" />
+ </div>
+ <template #dropdown>
+ <el-dropdown-menu>
+ <el-dropdown-item v-for="item of sizeOptions" :key="item.value" :disabled="size === item.value" :command="item.value">
+ {{ item.label }}
+ </el-dropdown-item>
+ </el-dropdown-menu>
+ </template>
+ </el-dropdown>
+ </div>
+</template>
+
+<script setup lang="ts">
+import useAppStore from "@/store/modules/app";
+
+const appStore = useAppStore();
+const size = computed(() => appStore.size);
+
+const sizeOptions = ref([
+ { label: "较大", value: "large" },
+ { label: "默认", value: "default" },
+ { label: "稍小", value: "small" },
+]);
+
+const handleSetSize = (size: string) => {
+ appStore.setSize(size);
+}
+</script>
+
+<style lang="scss" scoped>
+.size-icon--style {
+ font-size: 18px;
+ line-height: 50px;
+ padding-right: 7px;
+}
+</style>
diff --git a/src/components/SunSailDoc/index.vue b/src/components/SunSailDoc/index.vue
new file mode 100644
index 0000000..6c7ac0d
--- /dev/null
+++ b/src/components/SunSailDoc/index.vue
@@ -0,0 +1,13 @@
+<template>
+ <div>
+ <svg-icon icon-class="question" @click="goto" />
+ </div>
+</template>
+
+<script setup>
+const url = ref('https://plus-doc.dromara.org/');
+
+function goto() {
+ window.open(url.value)
+}
+</script>
diff --git a/src/components/SunSailGit/index.vue b/src/components/SunSailGit/index.vue
new file mode 100644
index 0000000..82393eb
--- /dev/null
+++ b/src/components/SunSailGit/index.vue
@@ -0,0 +1,13 @@
+<template>
+ <div>
+ <svg-icon icon-class="github" @click="goto" />
+ </div>
+</template>
+
+<script setup>
+const url = ref('https://gitee.com/dromara/RuoYi-Vue-Plus');
+
+function goto() {
+ window.open(url.value)
+}
+</script>
diff --git a/src/components/SvgIcon/index.vue b/src/components/SvgIcon/index.vue
new file mode 100644
index 0000000..3a7540f
--- /dev/null
+++ b/src/components/SvgIcon/index.vue
@@ -0,0 +1,40 @@
+<template>
+ <svg :class="svgClass" aria-hidden="true">
+ <use :xlink:href="iconName" :fill="color" />
+ </svg>
+</template>
+
+<script setup lang="ts">
+import { propTypes } from '@/utils/propTypes';
+
+const props = defineProps({
+ iconClass: propTypes.string.isRequired,
+ className: propTypes.string.def(''),
+ color: propTypes.string.def(''),
+})
+const iconName = computed(() => `#icon-${props.iconClass}`);
+const svgClass = computed(() => {
+ if (props.className) {
+ return `svg-icon ${props.className}`
+ }
+ return 'svg-icon'
+})
+</script>
+
+<style scope lang="scss">
+.sub-el-icon,
+.nav-icon {
+ display: inline-block;
+ font-size: 15px;
+ margin-right: 12px;
+ position: relative;
+}
+
+.svg-icon {
+ width: 1em;
+ height: 1em;
+ position: relative;
+ fill: currentColor;
+ vertical-align: -2px;
+}
+</style>
diff --git a/src/components/TopNav/index.vue b/src/components/TopNav/index.vue
new file mode 100644
index 0000000..227fccc
--- /dev/null
+++ b/src/components/TopNav/index.vue
@@ -0,0 +1,195 @@
+<template>
+ <el-menu :default-active="activeMenu" mode="horizontal" @select="handleSelect" :ellipsis="false">
+ <template v-for="(item, index) in topMenus">
+ <el-menu-item :style="{'--theme': theme}" :index="item.path" :key="index" v-if="index < visibleNumber"
+ ><svg-icon
+ v-if="item.meta && item.meta.icon && item.meta.icon !== '#'"
+ :icon-class="item.meta ? item.meta.icon : '' " /> {{ item.meta?.title }}</el-menu-item
+ >
+ </template>
+
+ <!-- 顶部菜单超出数量折叠 -->
+ <el-sub-menu :style="{'--theme': theme}" index="more" v-if="topMenus.length > visibleNumber">
+ <template #title>更多菜单</template>
+ <template v-for="(item, index) in topMenus">
+ <el-menu-item :index="item.path" :key="index" v-if="index >= visibleNumber"
+ ><svg-icon :icon-class="item.meta ? item.meta.icon : '' " /> {{ item.meta?.title }}</el-menu-item
+ >
+ </template>
+ </el-sub-menu>
+ </el-menu>
+</template>
+
+<script setup lang="ts">
+import { constantRoutes } from '@/router';
+import { isHttp } from '@/utils/validate';
+import useAppStore from '@/store/modules/app';
+import useSettingsStore from '@/store/modules/settings';
+import usePermissionStore from '@/store/modules/permission';
+import { RouteOption } from 'vue-router';
+
+// 顶部栏初始数
+const visibleNumber = ref<number>(-1);
+// 当前激活菜单的 index
+const currentIndex = ref<string>();
+// 隐藏侧边栏路由
+const hideList = ['/index', '/user/profile'];
+
+const appStore = useAppStore()
+const settingsStore = useSettingsStore()
+const permissionStore = usePermissionStore()
+const route = useRoute();
+const router = useRouter();
+
+// 主题颜色
+const theme = computed(() => settingsStore.theme);
+// 所有的路由信息
+const routers = computed(() => permissionStore.topbarRouters);
+
+// 顶部显示菜单
+const topMenus = computed(() => {
+ let topMenus:RouteOption[] = [];
+ routers.value.map((menu) => {
+ if (menu.hidden !== true) {
+ // 兼容顶部栏一级菜单内部跳转
+ if (menu.path === "/") {
+ topMenus.push(menu.children? menu.children[0] : menu);
+ } else {
+ topMenus.push(menu);
+ }
+ }
+ })
+ return topMenus;
+})
+
+// 设置子路由
+const childrenMenus = computed(() => {
+ let childrenMenus:RouteOption[] = [];
+ routers.value.map((router) => {
+ router.children?.forEach((item) => {
+ if (item.parentPath === undefined) {
+ if(router.path === "/") {
+ item.path = "/" + item.path;
+ } else {
+ if(!isHttp(item.path)) {
+ item.path = router.path + "/" + item.path;
+ }
+ }
+ item.parentPath = router.path;
+ }
+ childrenMenus.push(item);
+ })
+ })
+ return constantRoutes.concat(childrenMenus);
+})
+
+// 默认激活的菜单
+const activeMenu = computed(() => {
+ const path = route.path;
+ let activePath = path;
+ if (path !== undefined && path.lastIndexOf("/") > 0 && hideList.indexOf(path) === -1) {
+ const tmpPath = path.substring(1, path.length);
+ activePath = "/" + tmpPath.substring(0, tmpPath.indexOf("/"));
+ if (!route.meta.link) {
+ appStore.toggleSideBarHide(false);
+ }
+ } else if(!route.children) {
+ activePath = path;
+ appStore.toggleSideBarHide(true);
+ }
+ activeRoutes(activePath);
+ return activePath;
+})
+
+const setVisibleNumber = () => {
+ const width = document.body.getBoundingClientRect().width / 3;
+ visibleNumber.value = parseInt(String(width / 85));
+}
+
+const handleSelect = (key: string) => {
+ currentIndex.value = key;
+ const route = routers.value.find(item => item.path === key);
+ if (isHttp(key)) {
+ // http(s):// 路径新窗口打开
+ window.open(key, "_blank");
+ } else if (!route || !route.children) {
+ // 没有子路由路径内部打开
+ const routeMenu = childrenMenus.value.find(item => item.path === key);
+ if (routeMenu && routeMenu.query) {
+ let query = JSON.parse(routeMenu.query);
+ router.push({ path: key, query: query });
+ } else {
+ router.push({ path: key });
+ }
+ appStore.toggleSideBarHide(true);
+ } else {
+ // 显示左侧联动菜单
+ activeRoutes(key);
+ appStore.toggleSideBarHide(false);
+ }
+}
+
+const activeRoutes = (key: string) => {
+ let routes:RouteOption[] = [];
+ if (childrenMenus.value && childrenMenus.value.length > 0) {
+ childrenMenus.value.map((item) => {
+ if (key == item.parentPath || (key == "index" && "" == item.path)) {
+ routes.push(item);
+ }
+ });
+ }
+ if(routes.length > 0) {
+ permissionStore.setSidebarRouters(routes);
+ } else {
+ appStore.toggleSideBarHide(true);
+ }
+ return routes;
+}
+
+onMounted(() => {
+ window.addEventListener('resize', setVisibleNumber)
+})
+onBeforeUnmount(() => {
+ window.removeEventListener('resize', setVisibleNumber)
+})
+
+onMounted(() => {
+ setVisibleNumber()
+})
+</script>
+
+<style lang="scss">
+.topmenu-container.el-menu--horizontal > .el-menu-item {
+ float: left;
+ height: 50px !important;
+ line-height: 50px !important;
+ color: #999093 !important;
+ padding: 0 5px !important;
+ margin: 0 10px !important;
+}
+
+.topmenu-container.el-menu--horizontal > .el-menu-item.is-active, .el-menu--horizontal > .el-sub-menu.is-active .el-submenu__title {
+ border-bottom: 2px solid #{'var(--theme)'} !important;
+ color: #303133;
+}
+
+/* sub-menu item */
+.topmenu-container.el-menu--horizontal > .el-sub-menu .el-sub-menu__title {
+ float: left;
+ height: 50px !important;
+ line-height: 50px !important;
+ color: #999093 !important;
+ padding: 0 5px !important;
+ margin: 0 10px !important;
+}
+
+/* 背景色隐藏 */
+.topmenu-container.el-menu--horizontal>.el-menu-item:not(.is-disabled):focus, .topmenu-container.el-menu--horizontal>.el-menu-item:not(.is-disabled):hover, .topmenu-container.el-menu--horizontal>.el-submenu .el-submenu__title:hover {
+ background-color: #ffffff !important;
+}
+
+/* 图标右间距 */
+.topmenu-container .svg-icon {
+ margin-right: 4px;
+}
+</style>
diff --git a/src/components/TreeSelect/index.vue b/src/components/TreeSelect/index.vue
new file mode 100644
index 0000000..3076804
--- /dev/null
+++ b/src/components/TreeSelect/index.vue
@@ -0,0 +1,158 @@
+<template>
+ <div class="el-tree-select">
+ <el-select
+ style="width: 100%"
+ v-model="valueId"
+ ref="treeSelect"
+ :filterable="true"
+ :clearable="true"
+ @clear="clearHandle"
+ :filter-method="selectFilterData"
+ :placeholder="placeholder"
+ >
+ <el-option :value="valueId" :label="valueTitle">
+ <el-tree
+ id="tree-option"
+ ref="selectTree"
+ :accordion="accordion"
+ :data="options"
+ :props="objMap"
+ :node-key="objMap.value"
+ :expand-on-click-node="false"
+ :default-expanded-keys="defaultExpandedKey"
+ :filter-node-method="filterNode"
+ @node-click="handleNodeClick"
+ ></el-tree>
+ </el-option>
+ </el-select>
+ </div>
+</template>
+
+<script setup lang="ts">
+
+const props = defineProps({
+ /* 配置项 */
+ objMap: {
+ type: Object,
+ default: () => {
+ return {
+ value: 'id', // ID字段名
+ label: 'label', // 显示名称
+ children: 'children' // 子级字段名
+ }
+ }
+ },
+ /* 自动收起 */
+ accordion: {
+ type: Boolean,
+ default: () => {
+ return false
+ }
+ },
+ /**当前双向数据绑定的值 */
+ value: {
+ type: [String, Number],
+ default: ''
+ },
+ /**当前的数据 */
+ options: {
+ type: Array,
+ default: () => []
+ },
+ /**输入框内部的文字 */
+ placeholder: {
+ type: String,
+ default: ''
+ }
+})
+
+
+const selectTree = ref<ElTreeSelectInstance>();
+
+const emit = defineEmits(['update:value']);
+
+const valueId = computed({
+ get: () => props.value,
+ set: (val) => {
+ emit('update:value', val)
+ }
+});
+const valueTitle = ref('');
+const defaultExpandedKey = ref<any[]>([]);
+
+const initHandle = () => {
+ nextTick(() => {
+ const selectedValue = valueId.value;
+ if (selectedValue !== null && typeof (selectedValue) !== 'undefined') {
+ const node = selectTree.value?.getNode(selectedValue)
+ if (node) {
+ valueTitle.value = node.data[props.objMap.label]
+ selectTree.value?.setCurrentKey(selectedValue) // 设置默认选中
+ defaultExpandedKey.value = [selectedValue] // 设置默认展开
+ }
+ } else {
+ clearHandle()
+ }
+ })
+}
+const handleNodeClick = (node: any) => {
+ valueTitle.value = node[props.objMap.label]
+ valueId.value = node[props.objMap.value];
+ defaultExpandedKey.value = [];
+ selectTree.value?.blur()
+ selectFilterData('')
+}
+const selectFilterData = (val: any) => {
+ selectTree.value?.filter(val)
+}
+const filterNode = (value: any, data: any) => {
+ if (!value) return true
+ return data[props.objMap['label']].indexOf(value) !== -1
+}
+const clearHandle = () => {
+ valueTitle.value = ''
+ valueId.value = ''
+ defaultExpandedKey.value = [];
+ clearSelected()
+}
+const clearSelected = () => {
+ const allNode = document.querySelectorAll('#tree-option .el-tree-node')
+ allNode.forEach((element) => element.classList.remove('is-current'))
+}
+
+onMounted(() => {
+ initHandle()
+})
+
+watch(valueId, () => {
+ initHandle();
+})
+</script>
+
+<style lang="scss" scoped>
+@import "@/assets/styles/variables.module.scss";
+
+.el-scrollbar .el-scrollbar__view .el-select-dropdown__item {
+ padding: 0;
+ background-color: #fff;
+ height: auto;
+}
+
+.el-select-dropdown__item.selected {
+ font-weight: normal;
+}
+
+ul li .el-tree .el-tree-node__content {
+ height: auto;
+ padding: 0 20px;
+ box-sizing: border-box;
+}
+
+:deep(.el-tree-node__content:hover),
+:deep(.el-tree-node__content:active),
+:deep(.is-current > div:first-child),
+:deep(.el-tree-node__content:focus) {
+ background-color: mix(#fff, $--color-primary, 90%);
+ color: $--color-primary;
+}
+</style>
diff --git a/src/components/iFrame/index.vue b/src/components/iFrame/index.vue
new file mode 100644
index 0000000..6679f08
--- /dev/null
+++ b/src/components/iFrame/index.vue
@@ -0,0 +1,26 @@
+<template>
+ <div v-loading="loading" :style="'height:' + height">
+ <iframe :src="url" frameborder="no" style="width: 100%; height: 100%" scrolling="auto" />
+ </div>
+</template>
+
+<script setup lang="ts">
+import { propTypes } from '@/utils/propTypes';
+
+const props = defineProps({
+ src: propTypes.string.isRequired
+})
+
+const height = ref(document.documentElement.clientHeight - 94.5 + "px;")
+const loading = ref(true)
+const url = computed(() => props.src)
+
+onMounted(() => {
+ setTimeout(() => {
+ loading.value = false;
+ }, 300);
+ window.onresize = function temp() {
+ height.value = document.documentElement.clientHeight - 94.5 + "px;";
+ };
+})
+</script>
diff --git a/src/directive/common/copyText.ts b/src/directive/common/copyText.ts
new file mode 100644
index 0000000..b6805f8
--- /dev/null
+++ b/src/directive/common/copyText.ts
@@ -0,0 +1,66 @@
+/**
+ * v-copyText 复制文本内容
+ * Copyright (c) 2022 ruoyi
+ */
+
+export default {
+ beforeMount(el: any, { value, arg }: any) {
+ if (arg === 'callback') {
+ el.$copyCallback = value;
+ } else {
+ el.$copyValue = value;
+ const handler = () => {
+ copyTextToClipboard(el.$copyValue);
+ if (el.$copyCallback) {
+ el.$copyCallback(el.$copyValue);
+ }
+ };
+ el.addEventListener('click', handler);
+ el.$destroyCopy = () => el.removeEventListener('click', handler);
+ }
+ }
+};
+
+function copyTextToClipboard(input: string, { target = document.body } = {}) {
+ const element = document.createElement('textarea');
+ const previouslyFocusedElement = document.activeElement as HTMLInputElement;
+ element.value = input;
+ // Prevent keyboard from showing on mobile
+ element.setAttribute('readonly', '');
+
+ element.style.contain = 'strict';
+ element.style.position = 'absolute';
+ element.style.left = '-9999px';
+ element.style.fontSize = '12pt'; // Prevent zooming on iOS
+
+ const selection = document.getSelection();
+ let originalRange;
+ if (selection) {
+ originalRange = selection?.rangeCount > 0 && selection.getRangeAt(0);
+ }
+ target.append(element);
+ element.select();
+
+ // Explicit selection workaround for iOS
+ element.selectionStart = 0;
+ element.selectionEnd = input.length;
+
+ let isSuccess = false;
+ try {
+ isSuccess = document.execCommand('copy');
+ } catch (err) {
+ console.error(err);
+ }
+ element.remove();
+
+ if (originalRange) {
+ selection?.removeAllRanges();
+ selection?.addRange(originalRange);
+ }
+
+ // Get the focus back on the previously focused element, if any
+ if (previouslyFocusedElement) {
+ previouslyFocusedElement.focus();
+ }
+ return isSuccess;
+}
diff --git a/src/directive/index.ts b/src/directive/index.ts
new file mode 100644
index 0000000..ef25ee8
--- /dev/null
+++ b/src/directive/index.ts
@@ -0,0 +1,9 @@
+import copyText from './common/copyText';
+import { hasPermi, hasRoles } from './permission';
+import { App } from 'vue';
+
+export default (app: App) => {
+ app.directive('copyText', copyText);
+ app.directive('hasPermi', hasPermi);
+ app.directive('hasRoles', hasRoles);
+};
diff --git a/src/directive/permission/index.ts b/src/directive/permission/index.ts
new file mode 100644
index 0000000..7e9ddeb
--- /dev/null
+++ b/src/directive/permission/index.ts
@@ -0,0 +1,44 @@
+import { Directive, DirectiveBinding } from 'vue';
+import useUserStore from '@/store/modules/user';
+/**
+ * 操作权限处理
+ */
+export const hasPermi: Directive = {
+ mounted(el: HTMLElement, binding: DirectiveBinding) {
+ const { permissions } = useUserStore();
+ // 「其他角色」按钮权限校验
+ const { value } = binding;
+ if (value && value instanceof Array && value.length > 0) {
+ const hasPermission = permissions.some((permi) => {
+ return permi === '*:*:*' || value.includes(permi);
+ });
+ if (!hasPermission) {
+ el.parentNode && el.parentNode.removeChild(el);
+ return false;
+ }
+ } else {
+ throw new Error("check perms! Like v-has-permi=\"['system:user:add','system:user:edit']\"");
+ }
+ }
+};
+
+/**
+ * 角色权限处理
+ */
+export const hasRoles: Directive = {
+ mounted(el: HTMLElement, binding: DirectiveBinding) {
+ const { value } = binding;
+ const { roles } = useUserStore();
+ if (value && value instanceof Array && value.length > 0) {
+ const hasRole = roles.some((role) => {
+ return role === 'admin' || value.includes(role);
+ });
+ if (!hasRole) {
+ el.parentNode && el.parentNode.removeChild(el);
+ return false;
+ }
+ } else {
+ throw new Error("check roles! Like v-has-roles=\"['admin','test']\"");
+ }
+ }
+};
diff --git a/src/enums/MenuTypeEnum.ts b/src/enums/MenuTypeEnum.ts
new file mode 100644
index 0000000..cecebd2
--- /dev/null
+++ b/src/enums/MenuTypeEnum.ts
@@ -0,0 +1,15 @@
+export enum MenuTypeEnum {
+ /**
+ * 目录
+ */
+ M = 'M',
+ /**
+ * 菜单
+ */
+ C = 'C',
+
+ /**
+ * 按钮
+ */
+ F = 'F'
+}
diff --git a/src/enums/RespEnum.ts b/src/enums/RespEnum.ts
new file mode 100644
index 0000000..ce60a51
--- /dev/null
+++ b/src/enums/RespEnum.ts
@@ -0,0 +1,90 @@
+export enum HttpStatus {
+ /**
+ * 操作成功
+ */
+ SUCCESS = 200,
+ /**
+ * 对象创建成功
+ */
+ CREATED = 201,
+ /**
+ * 请求已经被接受
+ */
+ ACCEPTED = 202,
+ /**
+ * 操作已经执行成功,但是没有返回数据
+ */
+ NO_CONTENT = 204,
+ /**
+ * 资源已经被移除
+ */
+ MOVED_PERM = 301,
+ /**
+ * 重定向
+ */
+ SEE_OTHER = 303,
+ /**
+ * 资源没有被修改
+ */
+ NOT_MODIFIED = 304,
+ /**
+ * 参数列表错误(缺少,格式不匹配)
+ */
+ PARAM_ERROR = 400,
+ /**
+ * 未授权
+ */
+ UNAUTHORIZED = 401,
+ /**
+ * 访问受限,授权过期
+ */
+ FORBIDDEN = 403,
+ /**
+ * 资源,服务未找到
+ */
+ NOT_FOUND = 404,
+ /**
+ * 不允许的http方法
+ */
+ BAD_METHOD = 405,
+ /**
+ * 资源冲突,或者资源被锁
+ */
+ CONFLICT = 409,
+ /**
+ * 不支持的数据,媒体类型
+ */
+ UNSUPPORTED_TYPE = 415,
+ /**
+ * 系统内部错误
+ */
+ SERVER_ERROR = 500,
+ /**
+ * 接口未实现
+ */
+ NOT_IMPLEMENTED = 501,
+ /**
+ * 服务不可用,过载或者维护
+ */
+ BAD_GATEWAY = 502,
+ /**
+ * 网关超时
+ */
+ GATEWAY_TIMEOUT = 504,
+ /**
+ * 未知错误
+ */
+ UNKNOWN_ERROR = 520,
+ /**
+ * 服务未知错误
+ */
+ SERVICE_ERROR = 521,
+ /**
+ * 数据库未知错误
+ */
+ DATABASE_ERROR = 522,
+ /**
+ * 系统警告消息
+ */
+ WARN = 601
+}
diff --git a/src/enums/SettingTypeEnum.ts b/src/enums/SettingTypeEnum.ts
new file mode 100644
index 0000000..bf4ec43
--- /dev/null
+++ b/src/enums/SettingTypeEnum.ts
@@ -0,0 +1,16 @@
+export enum SettingTypeEnum {
+ TITLE = 'title',
+ THEME = 'theme',
+ SIDE_THEME = 'sideTheme',
+ SHOW_SETTINGS = 'showSettings',
+ TOP_NAV = 'topNav',
+ TAGS_VIEW = 'tagsView',
+ FIXED_HEADER = 'fixedHeader',
+ SIDEBAR_LOGO = 'sidebarLogo',
+ DYNAMIC_TITLE = 'dynamicTitle',
+ ANIMATION_ENABLE = 'animationEnable',
+ LAYOUT = 'layout',
+ DARK = 'dark',
+
+ LAYOUT_SETTING = 'layout-setting'
+}
diff --git a/src/enums/SideThemeEnum.ts b/src/enums/SideThemeEnum.ts
new file mode 100644
index 0000000..f172858
--- /dev/null
+++ b/src/enums/SideThemeEnum.ts
@@ -0,0 +1,4 @@
+export enum SideThemeEnum {
+ DARK = 'theme-dark',
+ LIGHT = 'theme-light'
+}
diff --git a/src/enums/layout/LayoutEnum.ts b/src/enums/layout/LayoutEnum.ts
new file mode 100644
index 0000000..0aa601b
--- /dev/null
+++ b/src/enums/layout/LayoutEnum.ts
@@ -0,0 +1,4 @@
+export enum ThemeEnum {
+ DARK = 'theme-dark',
+ LIGHT = 'theme-light'
+}
diff --git a/src/lang/en_US.ts b/src/lang/en_US.ts
new file mode 100644
index 0000000..034ea91
--- /dev/null
+++ b/src/lang/en_US.ts
@@ -0,0 +1,28 @@
+export default {
+ // 路由国际化
+ route: {
+ dashboard: 'Dashboard',
+ document: 'Document'
+ },
+ // 登录页面国际化
+ login: {
+ username: 'Username',
+ password: 'Password',
+ login: 'Login',
+ code: 'Verification Code',
+ copyright: ''
+ },
+ // 导航栏国际化
+ navbar: {
+ full: 'Full Screen',
+ language: 'Language',
+ dashboard: 'Dashboard',
+ document: 'Document',
+ message: 'Message',
+ layoutSize: 'Layout Size',
+ selectTenant: 'Select Tenant',
+ layoutSetting: 'Layout Setting',
+ personalCenter: 'Personal Center',
+ logout: 'Logout'
+ }
+};
diff --git a/src/lang/index.ts b/src/lang/index.ts
new file mode 100644
index 0000000..dddb86b
--- /dev/null
+++ b/src/lang/index.ts
@@ -0,0 +1,35 @@
+// 自定义国际化配置
+import { createI18n } from 'vue-i18n';
+
+// 本地语言包
+import enUSLocale from './en_US';
+import zhCNLocale from './zh_CN';
+
+const messages = {
+ zh_CN: {
+ ...zhCNLocale
+ },
+ en_US: {
+ ...enUSLocale
+ }
+};
+
+/**
+ * 获取当前语言
+ * @returns zh-cn|en ...
+ */
+export const getLanguage = () => {
+ const language = useStorage('language', 'zh_CN');
+ if (language.value) {
+ return language.value;
+ }
+ return 'zh_CN';
+};
+
+const i18n = createI18n({
+ legacy: false,
+ locale: getLanguage(),
+ messages
+});
+
+export default i18n;
diff --git a/src/lang/zh_CN.ts b/src/lang/zh_CN.ts
new file mode 100644
index 0000000..666a400
--- /dev/null
+++ b/src/lang/zh_CN.ts
@@ -0,0 +1,27 @@
+export default {
+ // 路由国际化
+ route: {
+ dashboard: '首页',
+ document: '项目文档'
+ },
+ // 登录页面国际化
+ login: {
+ username: '用户名',
+ password: '密码',
+ login: '登 录',
+ code: '请输入验证码',
+ copyright: ''
+ },
+ navbar: {
+ full: '全屏',
+ language: '语言',
+ dashboard: '首页',
+ document: '项目文档',
+ message: '消息',
+ layoutSize: '布局大小',
+ selectTenant: '选择租户',
+ layoutSetting: '布局设置',
+ personalCenter: '个人中心',
+ logout: '退出登录'
+ }
+};
diff --git a/src/layout/components/AppMain.vue b/src/layout/components/AppMain.vue
new file mode 100644
index 0000000..8a73232
--- /dev/null
+++ b/src/layout/components/AppMain.vue
@@ -0,0 +1,80 @@
+<template>
+ <section class="app-main">
+ <router-view v-slot="{ Component, route }">
+ <transition :enter-active-class="animante" mode="out-in">
+ <keep-alive :include="tagsViewStore.cachedViews">
+ <component v-if="!route.meta.link" :is="Component" :key="route.path" />
+ </keep-alive>
+ </transition>
+ </router-view>
+ <iframe-toggle />
+ </section>
+</template>
+
+<script setup name="AppMain" lang="ts">
+import useTagsViewStore from '@/store/modules/tagsView';
+import useSettingsStore from '@/store/modules/settings';
+import IframeToggle from './IframeToggle/index.vue'
+import { ComponentInternalInstance } from "vue";
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const tagsViewStore = useTagsViewStore();
+
+// 随机动画集合
+const animante = ref<string>('');
+const animationEnable = ref(useSettingsStore().animationEnable);
+watch(()=> useSettingsStore().animationEnable, (val) => {
+ animationEnable.value = val;
+ if (val) {
+ animante.value = proxy?.animate.animateList[Math.round(Math.random() * proxy?.animate.animateList.length)] as string;
+ } else {
+ animante.value = proxy?.animate.defaultAnimate as string;
+ }
+}, { immediate: true });
+</script>
+
+<style lang="scss" scoped>
+.app-main {
+ /* 50= navbar 50 */
+ min-height: calc(100vh - 50px);
+ width: 100%;
+ position: relative;
+ overflow: hidden;
+}
+
+.fixed-header+.app-main {
+ padding-top: 50px;
+}
+
+.hasTagsView {
+ .app-main {
+ /* 84 = navbar + tags-view = 50 + 34 */
+ min-height: calc(100vh - 84px);
+ }
+
+ .fixed-header+.app-main {
+ padding-top: 84px;
+ }
+}
+</style>
+<style lang="scss">
+// fix css style bug in open el-dialog
+.el-popup-parent--hidden {
+ .fixed-header {
+ padding-right: 6px;
+ }
+}
+
+::-webkit-scrollbar {
+ width: 6px;
+ height: 6px;
+}
+
+::-webkit-scrollbar-track {
+ background-color: #f1f1f1;
+}
+
+::-webkit-scrollbar-thumb {
+ background-color: #c0c0c0;
+ border-radius: 3px;
+}
+</style>
diff --git a/src/layout/components/IframeToggle/index.vue b/src/layout/components/IframeToggle/index.vue
new file mode 100644
index 0000000..efb2b7a
--- /dev/null
+++ b/src/layout/components/IframeToggle/index.vue
@@ -0,0 +1,27 @@
+<template>
+ <transition-group name="fade-transform" mode="out-in">
+ <inner-link
+ v-for="(item, index) in tagsViewStore.iframeViews"
+ :key="item.path"
+ :iframeId="'iframe' + index"
+ v-show="route.path === item.path"
+ :src="iframeUrl(item.meta ? item.meta.link : '', item.query)"
+ ></inner-link>
+ </transition-group>
+</template>
+
+<script setup lang="ts">
+import InnerLink from "../InnerLink/index.vue";
+import useTagsViewStore from '@/store/modules/tagsView';
+
+const route = useRoute();
+const tagsViewStore = useTagsViewStore();
+
+function iframeUrl(url: string, query: any) {
+ if (Object.keys(query).length > 0) {
+ let params = Object.keys(query).map((key) => key + "=" + query[key]).join("&");
+ return url + "?" + params;
+ }
+ return url;
+}
+</script>
diff --git a/src/layout/components/InnerLink/index.vue b/src/layout/components/InnerLink/index.vue
new file mode 100644
index 0000000..fa2869c
--- /dev/null
+++ b/src/layout/components/InnerLink/index.vue
@@ -0,0 +1,18 @@
+<template>
+ <div :style="'height:' + height">
+ <iframe :id="iframeId" style="width: 100%; height: 100%" :src="src" frameborder="no"></iframe>
+ </div>
+</template>
+
+<script setup lang="ts">
+const props = defineProps({
+ src: {
+ type: String,
+ default: "/"
+ },
+ iframeId: {
+ type: String
+ }
+});
+const height = ref(document.documentElement.clientHeight - 94.5 + "px");
+</script>
diff --git a/src/layout/components/Navbar.vue b/src/layout/components/Navbar.vue
new file mode 100644
index 0000000..7818fd4
--- /dev/null
+++ b/src/layout/components/Navbar.vue
@@ -0,0 +1,296 @@
+<template>
+ <div class="navbar">
+ <hamburger id="hamburger-container" :is-active="appStore.sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" />
+ <breadcrumb id="breadcrumb-container" class="breadcrumb-container" v-if="!settingsStore.topNav" />
+ <top-nav id="topmenu-container" class="topmenu-container" v-if="settingsStore.topNav" />
+
+ <div class="right-menu flex align-center">
+ <template v-if="appStore.device !== 'mobile'">
+ <el-select
+ v-model="companyName"
+ clearable
+ filterable
+ reserve-keyword
+ :placeholder="$t('navbar.selectTenant')"
+ v-if="userId === 1 && tenantEnabled"
+ @change="dynamicTenantEvent"
+ @clear="dynamicClearEvent"
+ >
+ <el-option v-for="item in tenantList" :key="item.tenantId" :label="item.companyName" :value="item.tenantId"> </el-option>
+ <template #prefix><svg-icon icon-class="company" class="el-input__icon input-icon" /></template>
+ </el-select>
+
+ <!-- <header-search id="header-search" class="right-menu-item" /> -->
+ <search-menu ref="searchMenuRef" />
+ <el-tooltip content="搜索" effect="dark" placement="bottom">
+ <div class="right-menu-item hover-effect" @click="openSearchMenu">
+ <svg-icon class-name="search-icon" icon-class="search" />
+ </div>
+ </el-tooltip>
+ <!-- 消息 -->
+ <el-tooltip :content="$t('navbar.message')" effect="dark" placement="bottom">
+ <div>
+ <el-popover placement="bottom" trigger="click" transition="el-zoom-in-top" :width="300" :persistent="false">
+ <template #reference>
+ <el-badge :value="newNotice > 0 ? newNotice : ''" :max="99">
+ <svg-icon icon-class="message" />
+ </el-badge>
+ </template>
+ <template #default>
+ <notice></notice>
+ </template>
+ </el-popover>
+ </div>
+ </el-tooltip>
+ <el-tooltip content="Github" effect="dark" placement="bottom">
+ <ruo-yi-git id="ruoyi-git" class="right-menu-item hover-effect" />
+ </el-tooltip>
+
+ <el-tooltip :content="$t('navbar.document')" effect="dark" placement="bottom">
+ <ruo-yi-doc id="ruoyi-doc" class="right-menu-item hover-effect" />
+ </el-tooltip>
+
+ <el-tooltip :content="$t('navbar.full')" effect="dark" placement="bottom">
+ <screenfull id="screenfull" class="right-menu-item hover-effect" />
+ </el-tooltip>
+
+ <el-tooltip :content="$t('navbar.language')" effect="dark" placement="bottom">
+ <lang-select id="lang-select" class="right-menu-item hover-effect" />
+ </el-tooltip>
+
+ <el-tooltip :content="$t('navbar.layoutSize')" effect="dark" placement="bottom">
+ <size-select id="size-select" class="right-menu-item hover-effect" />
+ </el-tooltip>
+ </template>
+ <div class="avatar-container">
+ <el-dropdown @command="handleCommand" class="right-menu-item hover-effect" trigger="click">
+ <div class="avatar-wrapper">
+ <img :src="userStore.avatar" class="user-avatar" />
+ <el-icon><caret-bottom /></el-icon>
+ </div>
+ <template #dropdown>
+ <el-dropdown-menu>
+ <router-link to="/user/profile" v-if="!dynamic">
+ <el-dropdown-item>{{ $t('navbar.personalCenter') }}</el-dropdown-item>
+ </router-link>
+ <el-dropdown-item command="setLayout" v-if="settingsStore.showSettings">
+ <span>{{ $t('navbar.layoutSetting') }}</span>
+ </el-dropdown-item>
+ <el-dropdown-item divided command="logout">
+ <span>{{ $t('navbar.logout') }}</span>
+ </el-dropdown-item>
+ </el-dropdown-menu>
+ </template>
+ </el-dropdown>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script setup lang="ts">
+import SearchMenu from './TopBar/search.vue';
+import useAppStore from '@/store/modules/app';
+import useUserStore from '@/store/modules/user';
+import useSettingsStore from '@/store/modules/settings';
+import { getTenantList } from "@/api/login";
+import { dynamicClear, dynamicTenant } from "@/api/system/tenant";
+import { ComponentInternalInstance } from "vue";
+import { TenantVO } from "@/api/types";
+import notice from './notice/index.vue';
+import useNoticeStore from '@/store/modules/notice';
+
+const appStore = useAppStore();
+const userStore = useUserStore();
+const settingsStore = useSettingsStore();
+const noticeStore = storeToRefs(useNoticeStore());
+const newNotice = ref(<number>0);
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+
+const userId = ref(userStore.userId);
+const companyName = ref(undefined);
+const tenantList = ref<TenantVO[]>([]);
+// 是否切换了租户
+const dynamic = ref(false);
+// 租户开关
+const tenantEnabled = ref(true);
+// 搜索菜单
+const searchMenuRef = ref<InstanceType<typeof SearchMenu>>();
+
+const openSearchMenu = () => {
+ searchMenuRef.value?.openSearch();
+}
+
+// 动态切换
+const dynamicTenantEvent = async (tenantId: string) => {
+ if (companyName.value != null && companyName.value !== '') {
+ await dynamicTenant(tenantId);
+ dynamic.value = true;
+ proxy?.$tab.closeAllPage();
+ proxy?.$router.push('/');
+ }
+}
+
+const dynamicClearEvent = async () => {
+ await dynamicClear();
+ dynamic.value = false;
+ proxy?.$tab.closeAllPage();
+ proxy?.$router.push('/');
+}
+
+/** 租户列表 */
+const initTenantList = async () => {
+ const { data } = await getTenantList();
+ tenantEnabled.value = data.tenantEnabled === undefined ? true : data.tenantEnabled;
+ if (tenantEnabled.value) {
+ tenantList.value = data.voList;
+ }
+}
+
+defineExpose({
+ initTenantList,
+})
+
+const toggleSideBar = () => {
+ appStore.toggleSideBar(false);
+}
+
+const logout = async () => {
+ await ElMessageBox.confirm('确定注销并退出系统吗?', '提示', {
+ confirmButtonText: '确定',
+ cancelButtonText: '取消',
+ type: 'warning'
+ })
+ await userStore.logout()
+ location.href = import.meta.env.VITE_APP_CONTEXT_PATH + 'index';
+}
+
+const emits = defineEmits(['setLayout'])
+const setLayout = () => {
+ emits('setLayout');
+}
+// 定义Command方法对象 通过key直接调用方法
+const commandMap: {[key: string]: any} = {
+ setLayout,
+ logout
+};
+const handleCommand = (command: string) => {
+ // 判断是否存在该方法
+ if (commandMap[command]) {
+ commandMap[command]();
+ }
+}
+
+//用深度监听 消息
+watch(() => noticeStore.state.value.notices, (newVal, oldVal) => {
+ newNotice.value = newVal.filter((item: any) => !item.read).length;
+}, { deep: true });
+</script>
+
+<style lang="scss" scoped>
+
+:deep(.el-select .el-input__wrapper) {
+ height:30px;
+}
+
+:deep(.el-badge__content.is-fixed){
+ top: 12px;
+}
+
+.flex {
+ display: flex;
+}
+
+.align-center {
+ align-items: center;
+}
+
+.navbar {
+ height: 50px;
+ overflow: hidden;
+ position: relative;
+ //background: #fff;
+ box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
+
+ .hamburger-container {
+ line-height: 46px;
+ height: 100%;
+ float: left;
+ cursor: pointer;
+ transition: background 0.3s;
+ -webkit-tap-highlight-color: transparent;
+
+ &:hover {
+ background: rgba(0, 0, 0, 0.025);
+ }
+ }
+
+ .breadcrumb-container {
+ float: left;
+ }
+
+ .topmenu-container {
+ position: absolute;
+ left: 50px;
+ }
+
+ .errLog-container {
+ display: inline-block;
+ vertical-align: top;
+ }
+
+ .right-menu {
+ float: right;
+ height: 100%;
+ line-height: 50px;
+ display: flex;
+
+ &:focus {
+ outline: none;
+ }
+
+ .right-menu-item {
+ display: inline-block;
+ padding: 0 8px;
+ height: 100%;
+ font-size: 18px;
+ color: #5a5e66;
+ vertical-align: text-bottom;
+
+ &.hover-effect {
+ cursor: pointer;
+ transition: background 0.3s;
+
+ &:hover {
+ background: rgba(0, 0, 0, 0.025);
+ }
+ }
+ }
+
+ .avatar-container {
+ margin-right: 40px;
+
+ .avatar-wrapper {
+ margin-top: 5px;
+ position: relative;
+
+ .user-avatar {
+ cursor: pointer;
+ width: 40px;
+ height: 40px;
+ border-radius: 10px;
+ margin-top: 10px;
+ }
+
+ i {
+ cursor: pointer;
+ position: absolute;
+ right: -20px;
+ top: 25px;
+ font-size: 12px;
+ }
+ }
+ }
+ }
+}
+</style>
diff --git a/src/layout/components/Settings/index.vue b/src/layout/components/Settings/index.vue
new file mode 100644
index 0000000..d5522ae
--- /dev/null
+++ b/src/layout/components/Settings/index.vue
@@ -0,0 +1,272 @@
+<template>
+ <el-drawer v-model="showSettings" :withHeader="false" direction="rtl" size="300px" close-on-click-modal>
+ <h3 class="drawer-title">主题风格设置</h3>
+
+ <div class="setting-drawer-block-checbox">
+ <div class="setting-drawer-block-checbox-item" @click="handleTheme(SideThemeEnum.DARK)">
+ <img src="@/assets/images/dark.svg" alt="dark" />
+ <div v-if="sideTheme === 'theme-dark'" class="setting-drawer-block-checbox-selectIcon" style="display: block;">
+ <i aria-label="图标: check" class="anticon anticon-check">
+ <svg viewBox="64 64 896 896" data-icon="check" width="1em" height="1em" :fill="theme" aria-hidden="true" focusable="false" class>
+ <path
+ d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 0 0-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z"
+ />
+ </svg>
+ </i>
+ </div>
+ </div>
+ <div class="setting-drawer-block-checbox-item" @click="handleTheme(SideThemeEnum.LIGHT)">
+ <img src="@/assets/images/light.svg" alt="light" />
+ <div v-if="sideTheme === 'theme-light'" class="setting-drawer-block-checbox-selectIcon" style="display: block;">
+ <i aria-label="图标: check" class="anticon anticon-check">
+ <svg viewBox="64 64 896 896" data-icon="check" width="1em" height="1em" :fill="theme" aria-hidden="true" focusable="false" class>
+ <path
+ d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 0 0-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z"
+ />
+ </svg>
+ </i>
+ </div>
+ </div>
+ </div>
+ <div class="drawer-item">
+ <span>主题颜色</span>
+ <span class="comp-style">
+ <el-color-picker v-model="theme" :predefine="predefineColors" @change="themeChange" />
+ </span>
+ </div>
+ <div class="drawer-item">
+ <span>深色模式</span>
+ <span class="comp-style">
+ <el-switch v-model="isDark" @change="toggleDark" class="drawer-switch" />
+ </span>
+ </div>
+
+ <el-divider />
+
+ <h3 class="drawer-title">系统布局配置</h3>
+
+ <div class="drawer-item">
+ <span>开启 TopNav</span>
+ <span class="comp-style">
+ <el-switch v-model="topNav" class="drawer-switch" />
+ </span>
+ </div>
+
+ <div class="drawer-item">
+ <span>开启 Tags-Views</span>
+ <span class="comp-style">
+ <el-switch v-model="tagsView" class="drawer-switch" />
+ </span>
+ </div>
+
+ <div class="drawer-item">
+ <span>固定 Header</span>
+ <span class="comp-style">
+ <el-switch v-model="fixedHeader" class="drawer-switch" />
+ </span>
+ </div>
+
+ <div class="drawer-item">
+ <span>显示 Logo</span>
+ <span class="comp-style">
+ <el-switch v-model="sidebarLogo" class="drawer-switch" />
+ </span>
+ </div>
+
+ <div class="drawer-item">
+ <span>动态标题</span>
+ <span class="comp-style">
+ <el-switch v-model="dynamicTitle" class="drawer-switch" />
+ </span>
+ </div>
+
+ <el-divider />
+
+ <el-button type="primary" plain icon="DocumentAdd" @click="saveSetting">保存配置</el-button>
+ <el-button plain icon="Refresh" @click="resetSetting">重置配置</el-button>
+ </el-drawer>
+</template>
+
+<script setup lang="ts">
+import { useDynamicTitle } from '@/utils/dynamicTitle'
+import useAppStore from '@/store/modules/app'
+import useSettingsStore from '@/store/modules/settings'
+import usePermissionStore from '@/store/modules/permission'
+import { handleThemeStyle } from '@/utils/theme'
+import { ComponentInternalInstance } from "vue";
+import { SettingTypeEnum } from "@/enums/SettingTypeEnum";
+import { SideThemeEnum } from "@/enums/SideThemeEnum";
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const appStore = useAppStore()
+const settingsStore = useSettingsStore()
+const permissionStore = usePermissionStore()
+
+
+const showSettings = ref(false);
+const theme = ref(settingsStore.theme);
+const sideTheme = ref(settingsStore.sideTheme);
+const storeSettings = computed(() => settingsStore);
+const predefineColors = ref(["#409EFF", "#ff4500", "#ff8c00", "#ffd700", "#90ee90", "#00ced1", "#1e90ff", "#c71585"]);
+
+// 是否暗黑模式
+const isDark = useDark({
+ storageKey: 'useDarkKey',
+ valueDark: 'dark',
+ valueLight: 'light',
+});
+watch(isDark, ()=> {
+ if (isDark.value) {
+ settingsStore.changeSetting({ key: SettingTypeEnum.SIDE_THEME, value: SideThemeEnum.DARK })
+ } else {
+ settingsStore.changeSetting({ key: SettingTypeEnum.SIDE_THEME, value: sideTheme.value })
+ }
+})
+const toggleDark = () => useToggle(isDark);
+
+/** 是否需要topNav */
+const topNav = computed({
+ get: () => storeSettings.value.topNav,
+ set: (val) => {
+ settingsStore.changeSetting({ key: SettingTypeEnum.TOP_NAV, value: val })
+ if (!val) {
+ appStore.toggleSideBarHide(false);
+ permissionStore.setSidebarRouters(permissionStore.defaultRoutes);
+ }
+ }
+})
+/** 是否需要tagview */
+const tagsView = computed({
+ get: () => storeSettings.value.tagsView,
+ set: (val) => {
+ settingsStore.changeSetting({ key: SettingTypeEnum.TAGS_VIEW, value: val })
+ }
+})
+/**是否需要固定头部 */
+const fixedHeader = computed({
+ get: () => storeSettings.value.fixedHeader,
+ set: (val) => {
+ settingsStore.changeSetting({ key: SettingTypeEnum.FIXED_HEADER, value: val })
+ }
+})
+/**是否需要侧边栏的logo */
+const sidebarLogo = computed({
+ get: () => storeSettings.value.sidebarLogo,
+ set: (val) => {
+ settingsStore.changeSetting({ key: SettingTypeEnum.SIDEBAR_LOGO, value: val })
+ }
+})
+/**是否需要侧边栏的动态网页的title */
+const dynamicTitle = computed({
+ get: () => storeSettings.value.dynamicTitle,
+ set: (val) => {
+ settingsStore.changeSetting({ key: SettingTypeEnum.DYNAMIC_TITLE, value: val })
+ // 动态设置网页标题
+ useDynamicTitle()
+ }
+})
+
+const themeChange = (val: string | null) => {
+ settingsStore.changeSetting({ key: SettingTypeEnum.THEME, value: val })
+ theme.value = val;
+ if (val) {
+ handleThemeStyle(val);
+ }
+}
+const handleTheme = (val: string) => {
+ sideTheme.value = val;
+ if (isDark.value && val === SideThemeEnum.LIGHT) {
+ // 暗黑模式颜色不变
+ settingsStore.changeSetting({ key: SettingTypeEnum.SIDE_THEME, value: SideThemeEnum.DARK })
+ return
+ }
+ settingsStore.changeSetting({ key: SettingTypeEnum.SIDE_THEME, value: val })
+}
+const saveSetting = () => {
+ proxy?.$modal.loading("正在保存到本地,请稍候...");
+ let layoutSetting = {
+ "topNav": storeSettings.value.topNav,
+ "tagsView": storeSettings.value.tagsView,
+ "fixedHeader": storeSettings.value.fixedHeader,
+ "sidebarLogo": storeSettings.value.sidebarLogo,
+ "dynamicTitle": storeSettings.value.dynamicTitle,
+ "sideTheme": storeSettings.value.sideTheme,
+ "theme": storeSettings.value.theme
+ };
+ localStorage.setItem("layout-setting", JSON.stringify(layoutSetting));
+ setTimeout(() => {proxy?.$modal.closeLoading()}, 1000)
+}
+const resetSetting = () => {
+ proxy?.$modal.loading("正在清除设置缓存并刷新,请稍候...");
+ localStorage.removeItem("layout-setting")
+ setTimeout("window.location.reload()", 1000)
+}
+const openSetting = () => {
+ showSettings.value = true;
+}
+
+defineExpose({
+ openSetting,
+})
+</script>
+
+<style lang="scss" scoped>
+.setting-drawer-title {
+ margin-bottom: 12px;
+ color: rgba(0, 0, 0, 0.85);
+ line-height: 22px;
+ font-weight: bold;
+ .drawer-title {
+ font-size: 14px;
+ }
+}
+.setting-drawer-block-checbox {
+ display: flex;
+ justify-content: flex-start;
+ align-items: center;
+ margin-top: 10px;
+ margin-bottom: 20px;
+
+ .setting-drawer-block-checbox-item {
+ position: relative;
+ margin-right: 16px;
+ border-radius: 2px;
+ cursor: pointer;
+
+ img {
+ width: 48px;
+ height: 48px;
+ }
+
+ .custom-img {
+ width: 48px;
+ height: 38px;
+ border-radius: 5px;
+ box-shadow: 1px 1px 2px #898484;
+ }
+
+ .setting-drawer-block-checbox-selectIcon {
+ position: absolute;
+ top: 0;
+ right: 0;
+ width: 100%;
+ height: 100%;
+ padding-top: 15px;
+ padding-left: 24px;
+ color: #1890ff;
+ font-weight: 700;
+ font-size: 14px;
+ }
+ }
+}
+
+.drawer-item {
+ padding: 12px 0;
+ font-size: 14px;
+
+ .comp-style {
+ float: right;
+ margin: -3px 8px 0px 0px;
+ }
+}
+</style>
diff --git a/src/layout/components/Sidebar/Link.vue b/src/layout/components/Sidebar/Link.vue
new file mode 100644
index 0000000..4c6e8b3
--- /dev/null
+++ b/src/layout/components/Sidebar/Link.vue
@@ -0,0 +1,40 @@
+<template>
+ <component :is="type" v-bind="linkProps()">
+ <slot />
+ </component>
+</template>
+
+<script setup lang="ts">
+import { isExternal } from '@/utils/validate'
+
+const props = defineProps({
+ to: {
+ type: [String, Object],
+ required: true
+ }
+})
+
+const isExt = computed(() => {
+ return isExternal(props.to as string)
+})
+
+const type = computed(() => {
+ if (isExt.value) {
+ return 'a'
+ }
+ return 'router-link'
+})
+
+function linkProps() {
+ if (isExt.value) {
+ return {
+ href: props.to,
+ target: '_blank',
+ rel: 'noopener'
+ }
+ }
+ return {
+ to: props.to
+ }
+}
+</script>
diff --git a/src/layout/components/Sidebar/Logo.vue b/src/layout/components/Sidebar/Logo.vue
new file mode 100644
index 0000000..254fa73
--- /dev/null
+++ b/src/layout/components/Sidebar/Logo.vue
@@ -0,0 +1,94 @@
+<template>
+ <div
+ class="sidebar-logo-container"
+ :class="{ 'collapse': collapse }"
+ :style="{ backgroundColor: sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground }"
+ >
+ <transition :enter-active-class="proxy?.animate.logoAnimate.enter" mode="out-in">
+ <router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/">
+ <img v-if="logo" :src="logo" class="sidebar-logo"/>
+ <h1 v-else class="sidebar-title"
+ :style="{ color: sideTheme === 'theme-dark' ? variables.logoTitleColor : variables.logoLightTitleColor }">
+ {{ title }}
+ </h1>
+ </router-link>
+ <router-link v-else key="expand" class="sidebar-logo-link" to="/">
+ <img v-if="logo" :src="logo" class="sidebar-logo"/>
+ <h1 class="sidebar-title"
+ :style="{ color: sideTheme === 'theme-dark' ? variables.logoTitleColor : variables.logoLightTitleColor }">
+ {{ title }}
+ </h1>
+ </router-link>
+ </transition>
+ </div>
+</template>
+
+<script setup lang="ts">
+import variables from '@/assets/styles/variables.module.scss'
+import logo from '@/assets/logo/logo.png'
+import useSettingsStore from '@/store/modules/settings'
+import {ComponentInternalInstance} from 'vue'
+
+const {proxy} = getCurrentInstance() as ComponentInternalInstance;
+
+defineProps({
+ collapse: {
+ type: Boolean,
+ required: true
+ }
+})
+
+const title = ref('通用后台管理系统');
+const settingsStore = useSettingsStore();
+const sideTheme = computed(() => settingsStore.sideTheme);
+</script>
+
+<style lang="scss" scoped>
+.sidebarLogoFade-enter-active {
+ transition: opacity 1.5s;
+}
+
+.sidebarLogoFade-enter,
+.sidebarLogoFade-leave-to {
+ opacity: 0;
+}
+
+.sidebar-logo-container {
+ position: relative;
+ width: 100%;
+ height: 50px;
+ line-height: 50px;
+ background: #2b2f3a;
+ text-align: center;
+ overflow: hidden;
+
+ & .sidebar-logo-link {
+ height: 100%;
+ width: 100%;
+
+ & .sidebar-logo {
+ width: 32px;
+ height: 32px;
+ vertical-align: middle;
+ margin-right: 12px;
+ }
+
+ & .sidebar-title {
+ display: inline-block;
+ margin: 0;
+ color: #fff;
+ font-weight: 600;
+ line-height: 50px;
+ font-size: 14px;
+ font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif;
+ vertical-align: middle;
+ }
+ }
+
+ &.collapse {
+ .sidebar-logo {
+ margin-right: 0px;
+ }
+ }
+}
+</style>
diff --git a/src/layout/components/Sidebar/SidebarItem.vue b/src/layout/components/Sidebar/SidebarItem.vue
new file mode 100644
index 0000000..4459cdf
--- /dev/null
+++ b/src/layout/components/Sidebar/SidebarItem.vue
@@ -0,0 +1,108 @@
+<template>
+ <div v-if="!item.hidden">
+ <template v-if="hasOneShowingChild(item, item.children) && (!onlyOneChild.children || onlyOneChild.noShowingChildren) && !item.alwaysShow">
+ <app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path, onlyOneChild.query)">
+ <el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{ 'submenu-title-noDropdown': !isNest }">
+ <svg-icon :icon-class="onlyOneChild.meta.icon || (item.meta && item.meta.icon)" />
+ <template #title>
+ <span class="menu-title" :title="hasTitle(onlyOneChild.meta.title)">{{ onlyOneChild.meta.title }}</span>
+ </template>
+ </el-menu-item>
+ </app-link>
+ </template>
+
+ <el-sub-menu v-else ref="subMenu" :index="resolvePath(item.path)" teleported>
+ <template v-if="item.meta" #title>
+ <svg-icon :icon-class="item.meta ? item.meta.icon : '' " />
+ <span class="menu-title" :title="hasTitle(item.meta?.title)">{{ item.meta?.title }}</span>
+ </template>
+
+ <sidebar-item
+ v-for="(child, index) in item.children"
+ :key="child.path + index"
+ :is-nest="true"
+ :item="child"
+ :base-path="resolvePath(child.path)"
+ class="nest-menu"
+ />
+ </el-sub-menu>
+ </div>
+</template>
+
+<script setup lang="ts">
+import { isExternal } from '@/utils/validate'
+import AppLink from './Link.vue'
+import { getNormalPath } from '@/utils/ruoyi'
+import { RouteOption } from "vue-router";
+
+const props = defineProps({
+ // route object
+ item: {
+ type: Object as PropType<RouteOption>,
+ required: true
+ },
+ isNest: {
+ type: Boolean,
+ default: false
+ },
+ basePath: {
+ type: String,
+ default: ''
+ }
+})
+
+const onlyOneChild = ref<any>({});
+
+const hasOneShowingChild = (parent: RouteOption, children?:RouteOption[]) => {
+ if (!children) {
+ children = [];
+ }
+ const showingChildren = children.filter(item => {
+ if (item.hidden) {
+ return false
+ } else {
+ // Temp set(will be used if only has one showing child)
+ onlyOneChild.value = item
+ return true
+ }
+ })
+
+ // When there is only one child router, the child router is displayed by default
+ if (showingChildren.length === 1) {
+ return true
+ }
+
+ // Show parent if there are no child router to display
+ if (showingChildren.length === 0) {
+ onlyOneChild.value = { ...parent, path: '', noShowingChildren: true }
+ if (parent.name === '2222') {
+ console.log(onlyOneChild.value)
+ }
+ return true
+ }
+
+
+ return false
+};
+
+const resolvePath = (routePath:string, routeQuery?:string): any => {
+ if (isExternal(routePath)) {
+ return routePath
+ }
+ if (isExternal(props.basePath)) {
+ return props.basePath
+ }
+ if (routeQuery) {
+ let query = JSON.parse(routeQuery);
+ return { path: getNormalPath(props.basePath + '/' + routePath), query: query }
+ }
+ return getNormalPath(props.basePath + '/' + routePath)
+}
+
+const hasTitle = (title: string | undefined): string => {
+ if(!title || title.length <= 5) {
+ return "";
+ }
+ return title;
+}
+</script>
diff --git a/src/layout/components/Sidebar/index.vue b/src/layout/components/Sidebar/index.vue
new file mode 100644
index 0000000..fc290b1
--- /dev/null
+++ b/src/layout/components/Sidebar/index.vue
@@ -0,0 +1,55 @@
+<template>
+ <div :class="{ 'has-logo': showLogo }" :style="{ backgroundColor: bgColor }">
+ <logo v-if="showLogo" :collapse="isCollapse" />
+ <el-scrollbar :class="sideTheme" wrap-class="scrollbar-wrapper">
+ <transition :enter-active-class="proxy?.animate.menuSearchAnimate.enter" mode="out-in">
+ <el-menu
+ :default-active="activeMenu"
+ :collapse="isCollapse"
+ :background-color="bgColor"
+ :text-color="textColor"
+ :unique-opened="true"
+ :active-text-color="theme"
+ :collapse-transition="false"
+ mode="vertical"
+ >
+ <sidebar-item v-for="(route, index) in sidebarRouters" :key="route.path + index" :item="route" :base-path="route.path" />
+ </el-menu>
+ </transition>
+ </el-scrollbar>
+ </div>
+</template>
+
+<script setup lang="ts">
+import Logo from './Logo.vue'
+import SidebarItem from './SidebarItem.vue'
+import variables from '@/assets/styles/variables.module.scss'
+import useAppStore from '@/store/modules/app'
+import useSettingsStore from '@/store/modules/settings'
+import usePermissionStore from '@/store/modules/permission'
+import { RouteOption } from "vue-router";
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+
+const route = useRoute();
+const appStore = useAppStore()
+const settingsStore = useSettingsStore()
+const permissionStore = usePermissionStore()
+
+const sidebarRouters = computed<RouteOption[]>(() => permissionStore.sidebarRouters);
+const showLogo = computed(() => settingsStore.sidebarLogo);
+const sideTheme = computed(() => settingsStore.sideTheme);
+const theme = computed(() => settingsStore.theme);
+const isCollapse = computed(() => !appStore.sidebar.opened);
+
+const activeMenu = computed(() => {
+ const { meta, path } = route;
+ // if set path, the sidebar will highlight the path you set
+ if (meta.activeMenu) {
+ return meta.activeMenu;
+ }
+ return path;
+})
+
+const bgColor = computed(() => sideTheme.value === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground);
+const textColor = computed(() => sideTheme.value === 'theme-dark' ? variables.menuColor : variables.menuLightColor);
+</script>
diff --git a/src/layout/components/SocialCallback/index.vue b/src/layout/components/SocialCallback/index.vue
new file mode 100644
index 0000000..e3a50e0
--- /dev/null
+++ b/src/layout/components/SocialCallback/index.vue
@@ -0,0 +1,87 @@
+<template>
+ <div v-loading="loading" class="social-callback"></div>
+</template>
+
+<script setup lang="ts">
+import { login, callback } from '@/api/login';
+import { setToken, getToken } from '@/utils/auth';
+import { LoginData } from '@/api/types';
+
+const route = useRoute();
+const loading = ref(true);
+
+
+/**
+ * 接收Route传递的参数
+ * @param {Object} route.query.
+ */
+const code = route.query.code as string;
+const state = route.query.state as string;
+const source = route.query.source as string;
+const tenantId = localStorage.getItem("tenantId") ? localStorage.getItem("tenantId") as string : '000000';
+
+
+const processResponse = async (res: any) => {
+ if (res.code !== 200) {
+ throw new Error(res.msg);
+ }
+ if (res.data !== null) {
+ setToken(res.data.access_token);
+ }
+ ElMessage.success(res.msg);
+ setTimeout(() => {
+ location.href = import.meta.env.VITE_APP_CONTEXT_PATH + 'index';
+ }, 2000);
+};
+
+const handleError = (error: any) => {
+ ElMessage.error(error.message);
+ setTimeout(() => {
+ location.href = import.meta.env.VITE_APP_CONTEXT_PATH + 'index';
+ }, 2000);
+};
+
+const callbackByCode = async (data: LoginData) => {
+ try {
+ const res = await callback(data);
+ await processResponse(res);
+ loading.value = false;
+ } catch (error) {
+ handleError(error);
+ }
+};
+
+const loginByCode = async (data: LoginData) => {
+ console.log(2)
+ try {
+ const res = await login(data);
+ await processResponse(res);
+ loading.value = false;
+ } catch (error) {
+ handleError(error);
+ }
+};
+
+const init = async () => {
+ const data: LoginData = {
+ socialCode: code,
+ socialState: state,
+ tenantId: tenantId,
+ source: source,
+ clientId: 'e5cd7e4891bf95d1d19206ce24a7b32e',
+ grantType: 'social'
+ };
+
+ if (!getToken()) {
+ await loginByCode(data);
+ } else {
+ await callbackByCode(data);
+ }
+};
+
+onMounted(() => {
+ nextTick(() => {
+ init();
+ });
+});
+</script>
diff --git a/src/layout/components/TagsView/ScrollPane.vue b/src/layout/components/TagsView/ScrollPane.vue
new file mode 100644
index 0000000..b50c628
--- /dev/null
+++ b/src/layout/components/TagsView/ScrollPane.vue
@@ -0,0 +1,101 @@
+<template>
+ <el-scrollbar ref="scrollContainerRef" :vertical="false" class="scroll-container" @wheel.prevent="handleScroll">
+ <slot />
+ </el-scrollbar>
+</template>
+
+<script setup lang="ts">
+import useTagsViewStore from '@/store/modules/tagsView'
+import { TagView } from 'vue-router'
+const tagAndTagSpacing = ref(4);
+
+const scrollContainerRef = ref<ElScrollbarInstance>()
+const scrollWrapper = computed(() => scrollContainerRef.value?.$refs.wrapRef as any);
+
+onMounted(() => {
+ scrollWrapper.value?.addEventListener('scroll', emitScroll, true)
+})
+onBeforeUnmount(() => {
+ scrollWrapper.value?.removeEventListener('scroll', emitScroll)
+})
+
+const handleScroll = (e: WheelEvent) => {
+ const eventDelta = (e as any).wheelDelta || - e.deltaY * 40
+ const $scrollWrapper = scrollWrapper.value;
+ $scrollWrapper.scrollLeft = $scrollWrapper.scrollLeft + eventDelta / 4
+}
+const emits = defineEmits(['scroll'])
+const emitScroll = () => {
+ emits('scroll')
+}
+
+const tagsViewStore = useTagsViewStore()
+const visitedViews = computed(() => tagsViewStore.visitedViews);
+
+const moveToTarget = (currentTag: TagView) => {
+ const $container = scrollContainerRef.value?.$el
+ const $containerWidth = $container.offsetWidth
+ const $scrollWrapper = scrollWrapper.value;
+
+ let firstTag = null
+ let lastTag = null
+
+ // find first tag and last tag
+ if (visitedViews.value.length > 0) {
+ firstTag = visitedViews.value[0]
+ lastTag = visitedViews.value[visitedViews.value.length - 1]
+ }
+
+ if (firstTag === currentTag) {
+ $scrollWrapper.scrollLeft = 0
+ } else if (lastTag === currentTag) {
+ $scrollWrapper.scrollLeft = $scrollWrapper.scrollWidth - $containerWidth
+ } else {
+ const tagListDom: any = document.getElementsByClassName('tags-view-item');
+ const currentIndex = visitedViews.value.findIndex(item => item === currentTag)
+ let prevTag = null
+ let nextTag = null
+
+ for (const k in tagListDom) {
+ if (k !== 'length' && Object.hasOwnProperty.call(tagListDom, k)) {
+ if (tagListDom[k].dataset.path === visitedViews.value[currentIndex - 1].path) {
+ prevTag = tagListDom[k];
+ }
+ if (tagListDom[k].dataset.path === visitedViews.value[currentIndex + 1].path) {
+ nextTag = tagListDom[k];
+ }
+ }
+ }
+
+ // the tag's offsetLeft after of nextTag
+ const afterNextTagOffsetLeft = nextTag.offsetLeft + nextTag.offsetWidth + tagAndTagSpacing.value
+
+ // the tag's offsetLeft before of prevTag
+ const beforePrevTagOffsetLeft = prevTag.offsetLeft - tagAndTagSpacing.value
+ if (afterNextTagOffsetLeft > $scrollWrapper.scrollLeft + $containerWidth) {
+ $scrollWrapper.scrollLeft = afterNextTagOffsetLeft - $containerWidth
+ } else if (beforePrevTagOffsetLeft < $scrollWrapper.scrollLeft) {
+ $scrollWrapper.scrollLeft = beforePrevTagOffsetLeft
+ }
+ }
+}
+
+defineExpose({
+ moveToTarget,
+})
+</script>
+
+<style lang="scss" scoped>
+.scroll-container {
+ white-space: nowrap;
+ position: relative;
+ overflow: hidden;
+ width: 100%;
+ :deep(.el-scrollbar__bar) {
+ bottom: 0px;
+ }
+ :deep(.el-scrollbar__wrap) {
+ height: 49px;
+ }
+}
+</style>
diff --git a/src/layout/components/TagsView/index.vue b/src/layout/components/TagsView/index.vue
new file mode 100644
index 0000000..1df486e
--- /dev/null
+++ b/src/layout/components/TagsView/index.vue
@@ -0,0 +1,335 @@
+<template>
+ <div id="tags-view-container" class="tags-view-container">
+ <scroll-pane ref="scrollPaneRef" class="tags-view-wrapper" @scroll="handleScroll">
+ <router-link
+ v-for="tag in visitedViews"
+ :key="tag.path"
+ :data-path="tag.path"
+ :class="isActive(tag) ? 'active' : ''"
+ :to="{ path: tag.path ? tag.path : '', query: tag.query, fullPath: tag.fullPath ? tag.fullPath : '' }"
+ class="tags-view-item"
+ :style="activeStyle(tag)"
+ @click.middle="!isAffix(tag) ? closeSelectedTag(tag) : ''"
+ @contextmenu.prevent="openMenu(tag, $event)"
+ >
+ {{ tag.title }}
+ <span v-if="!isAffix(tag)" @click.prevent.stop="closeSelectedTag(tag)">
+ <close class="el-icon-close" style="width: 1em; height: 1em;vertical-align: middle;" />
+ </span>
+ </router-link>
+ </scroll-pane>
+ <ul v-show="visible" :style="{ left: left + 'px', top: top + 'px' }" class="contextmenu">
+ <li @click="refreshSelectedTag(selectedTag)"><refresh-right style="width: 1em; height: 1em;" /> 刷新页面</li>
+ <li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)"><close style="width: 1em; height: 1em;" /> 关闭当前</li>
+ <li @click="closeOthersTags"><circle-close style="width: 1em; height: 1em;" /> 关闭其他</li>
+ <li v-if="!isFirstView()" @click="closeLeftTags"><back style="width: 1em; height: 1em;" /> 关闭左侧</li>
+ <li v-if="!isLastView()" @click="closeRightTags"><right style="width: 1em; height: 1em;" /> 关闭右侧</li>
+ <li @click="closeAllTags(selectedTag)"><circle-close style="width: 1em; height: 1em;" /> 全部关闭</li>
+ </ul>
+ </div>
+</template>
+
+<script setup lang="ts">
+import ScrollPane from './ScrollPane.vue'
+import { getNormalPath } from '@/utils/ruoyi'
+import useTagsViewStore from "@/store/modules/tagsView";
+import useSettingsStore from '@/store/modules/settings'
+import usePermissionStore from '@/store/modules/permission'
+import { ComponentInternalInstance } from "vue";
+import { RouteOption, TagView, RouteLocationRaw } from "vue-router";
+
+const visible = ref(false);
+const top = ref(0);
+const left = ref(0);
+const selectedTag = ref<TagView>({});
+const affixTags = ref<TagView[]>([]);
+const scrollPaneRef = ref(ScrollPane);
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const route = useRoute();
+const router = useRouter();
+
+const visitedViews = computed(() => useTagsViewStore().visitedViews);
+const routes = computed(() => usePermissionStore().routes);
+const theme = computed(() => useSettingsStore().theme);
+
+watch(route, () => {
+ addTags();
+ moveToCurrentTag();
+})
+watch(visible, (value) => {
+ if (value) {
+ document.body.addEventListener('click', closeMenu);
+ } else {
+ document.body.removeEventListener('click', closeMenu);
+ }
+})
+
+const isActive = (r: TagView): boolean => {
+ return r.path === route.path;
+}
+const activeStyle = (tag: TagView) => {
+ if (!isActive(tag)) return {};
+ return {
+ "background-color": theme.value,
+ "border-color": theme.value
+ };
+}
+const isAffix = (tag: TagView) => {
+ return tag.meta && tag.meta.affix;
+}
+const isFirstView = () => {
+ try {
+ return selectedTag.value.fullPath === '/index' || selectedTag.value.fullPath === visitedViews.value[1].fullPath;
+ } catch (err) {
+ return false;
+ }
+}
+const isLastView = () => {
+ try {
+ return selectedTag.value.fullPath === visitedViews.value[visitedViews.value.length - 1].fullPath;
+ } catch (err) {
+ return false;
+ }
+}
+const filterAffixTags = (routes:RouteOption [], basePath = '') => {
+ let tags:TagView[] = []
+ routes.forEach(route => {
+ if (route.meta && route.meta.affix) {
+ const tagPath = getNormalPath(basePath + '/' + route.path);
+ tags.push({
+ fullPath: tagPath,
+ path: tagPath,
+ name: route.name,
+ meta: { ...route.meta }
+ })
+ }
+ if (route.children) {
+ const tempTags = filterAffixTags(route.children, route.path);
+ if (tempTags.length >= 1) {
+ tags = [...tags, ...tempTags];
+ }
+ }
+ })
+ return tags
+}
+const initTags = () => {
+ const res = filterAffixTags(routes.value);
+ affixTags.value = res;
+ for (const tag of res) {
+ // Must have tag name
+ if (tag.name) {
+ useTagsViewStore().addVisitedView(tag);
+ }
+ }
+}
+const addTags = () => {
+ const { name } = route;
+ if(route.query.title) {
+ route.meta.title = route.query.title;
+ }
+ if (name) {
+ useTagsViewStore().addView(route);
+ if (route.meta.link) {
+ useTagsViewStore().addIframeView(route);
+ }
+ }
+ return false
+}
+const moveToCurrentTag = () => {
+ nextTick(() => {
+ for (const r of visitedViews.value) {
+ if (r.path === route.path) {
+ scrollPaneRef.value.moveToTarget(r);
+ // when query is different then update
+ if (r.fullPath !== route.fullPath) {
+ useTagsViewStore().updateVisitedView(route);
+ }
+ }
+ }
+ })
+}
+const refreshSelectedTag = (view: TagView) => {
+ proxy?.$tab.refreshPage(view);
+ if (route.meta.link) {
+ useTagsViewStore().delIframeView(route);
+ }
+}
+const closeSelectedTag = (view: TagView) => {
+ proxy?.$tab.closePage(view).then(({ visitedViews }: any) => {
+ if (isActive(view)) {
+ toLastView(visitedViews, view);
+ }
+ })
+}
+const closeRightTags = () => {
+ proxy?.$tab.closeRightPage(selectedTag.value).then(visitedViews => {
+ if (!visitedViews.find(i => i.fullPath === route.fullPath)) {
+ toLastView(visitedViews);
+ }
+ })
+}
+const closeLeftTags = () => {
+ proxy?.$tab.closeLeftPage(selectedTag.value).then(visitedViews => {
+ if (!visitedViews.find(i => i.fullPath === route.fullPath)) {
+ toLastView(visitedViews);
+ }
+ })
+}
+const closeOthersTags = () => {
+ router.push(selectedTag.value as RouteLocationRaw).catch(() => { });
+ proxy?.$tab.closeOtherPage(selectedTag.value).then(() => {
+ moveToCurrentTag();
+ })
+}
+const closeAllTags = (view: TagView) => {
+ proxy?.$tab.closeAllPage().then(({ visitedViews }) => {
+ if (affixTags.value.some(tag => tag.path === route.path)) {
+ return;
+ }
+ toLastView(visitedViews, view);
+ })
+}
+const toLastView = (visitedViews:TagView[], view?: TagView) => {
+ const latestView = visitedViews.slice(-1)[0];
+ if (latestView) {
+ router.push(latestView.fullPath as string);
+ } else {
+ // now the default is to redirect to the home page if there is no tags-view,
+ // you can adjust it according to your needs.
+ if (view?.name === 'Dashboard') {
+ // to reload home page
+ router.replace({ path: '/redirect' + view?.fullPath });
+ } else {
+ router.push('/');
+ }
+ }
+}
+const openMenu = (tag: TagView, e: MouseEvent) => {
+ const menuMinWidth = 105;
+ const offsetLeft = proxy?.$el.getBoundingClientRect().left; // container margin left
+ const offsetWidth = proxy?.$el.offsetWidth; // container width
+ const maxLeft = offsetWidth - menuMinWidth; // left boundary
+ const l = e.clientX - offsetLeft + 15; // 15: margin right
+
+ if (l > maxLeft) {
+ left.value = maxLeft;
+ } else {
+ left.value = l;
+ }
+
+ top.value = e.clientY
+ visible.value = true;
+ selectedTag.value = tag;
+}
+const closeMenu = () => {
+ visible.value = false;
+}
+const handleScroll = () => {
+ closeMenu();
+}
+
+
+onMounted(() => {
+ initTags();
+ addTags();
+})
+</script>
+
+<style lang="scss" scoped>
+.tags-view-container {
+ height: 34px;
+ width: 100%;
+ background-color: var(--el-bg-color);
+ border: 1px solid var(--el-border-color-light);
+ box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12), 0 0 3px 0 rgba(0, 0, 0, 0.04);
+ .tags-view-wrapper {
+ .tags-view-item {
+ display: inline-block;
+ position: relative;
+ cursor: pointer;
+ height: 26px;
+ line-height: 23px;
+ background-color: var(--el-bg-color);
+ border: 1px solid var(--el-border-color-light);
+ color: #495060;
+ padding: 0 8px;
+ font-size: 12px;
+ margin-left: 5px;
+ margin-top: 4px;
+ &:hover {
+ color: var(--el-color-primary);
+ }
+ &:first-of-type {
+ margin-left: 15px;
+ }
+ &:last-of-type {
+ margin-right: 15px;
+ }
+ &.active {
+ background-color: #42b983;
+ color: #fff;
+ border-color: #42b983;
+ &::before {
+ content: "";
+ background: #fff;
+ display: inline-block;
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ position: relative;
+ margin-right: 5px;
+ }
+ }
+ }
+ }
+ .contextmenu {
+ margin: 0;
+ background: var(--el-bg-color);
+ z-index: 3000;
+ position: absolute;
+ list-style-type: none;
+ padding: 5px 0;
+ border-radius: 4px;
+ font-size: 12px;
+ font-weight: 400;
+ box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, 0.3);
+ li {
+ margin: 0;
+ padding: 7px 16px;
+ cursor: pointer;
+ &:hover {
+ background: #eee;
+ }
+ }
+ }
+}
+</style>
+
+<style lang="scss">
+//reset element css of el-icon-close
+.tags-view-wrapper {
+ .tags-view-item {
+ .el-icon-close {
+ width: 16px;
+ height: 16px;
+ vertical-align: 2px;
+ border-radius: 50%;
+ text-align: center;
+ transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
+ transform-origin: 100% 50%;
+ &:before {
+ transform: scale(0.6);
+ display: inline-block;
+ vertical-align: -3px;
+ }
+ &:hover {
+ background-color: #b4bccc;
+ color: #fff;
+ width: 12px !important;
+ height: 12px !important;
+ }
+ }
+ }
+}
+</style>
diff --git a/src/layout/components/TopBar/search.vue b/src/layout/components/TopBar/search.vue
new file mode 100644
index 0000000..9dee360
--- /dev/null
+++ b/src/layout/components/TopBar/search.vue
@@ -0,0 +1,158 @@
+<template>
+ <div class="layout-search-dialog">
+ <el-dialog v-model="state.isShowSearch" destroy-on-close :show-close="false">
+ <template #footer>
+ <el-autocomplete
+ v-model="state.menuQuery"
+ :fetch-suggestions="menuSearch"
+ placeholder="搜索"
+ ref="layoutMenuAutocompleteRef"
+ @select="onHandleSelect"
+ :fit-input-width="true"
+ >
+ <template #prefix>
+ <svg-icon class-name="search-icon" icon-class="search" />
+ </template>
+ <template #default="{ item }">
+ <div>
+ <svg-icon :icon-class="item.icon" class="mr5" />
+ {{ item.title }}
+ </div>
+ </template>
+ </el-autocomplete>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup lang="ts" name="layoutBreadcrumbSearch">
+import { getNormalPath } from '@/utils/ruoyi';
+import { isHttp } from '@/utils/validate';
+import usePermissionStore from '@/store/modules/permission';
+import { RouteOption } from 'vue-router';
+type Router = Array<{
+ path: string;
+ icon: string;
+ title: string[];
+}>
+type SearchState<T = any> = {
+ isShowSearch: boolean;
+ menuQuery: string;
+ menuList: T[];
+};
+// 定义变量内容
+const layoutMenuAutocompleteRef = ref();
+const router = useRouter();
+const routes = computed(() => usePermissionStore().routes);
+const state = reactive<SearchState>({
+ isShowSearch: false,
+ menuQuery: '',
+ menuList: [],
+});
+
+// 搜索弹窗打开
+const openSearch = () => {
+ state.menuQuery = '';
+ state.isShowSearch = true;
+ state.menuList = generateRoutes(routes.value);
+ nextTick(() => {
+ setTimeout(() => {
+ layoutMenuAutocompleteRef.value.focus();
+ });
+ });
+};
+// 搜索弹窗关闭
+const closeSearch = () => {
+ state.isShowSearch = false;
+};
+// 菜单搜索数据过滤
+const menuSearch = (queryString: string, cb: Function) => {
+ let options = state.menuList.filter((item) => {
+ return item.title.indexOf(queryString) > -1;
+ });
+ cb(options);
+};
+
+// Filter out the routes that can be displayed in the sidebar
+// And generate the internationalized title
+const generateRoutes = (routes: RouteOption[], basePath = '', prefixTitle: string[] = []) => {
+ let res: Router = []
+ routes.forEach(r => {
+ // skip hidden router
+ if (!r.hidden) {
+ const p = r.path.length > 0 && r.path[0] === '/' ? r.path : '/' + r.path;
+ const data: any = {
+ path: !isHttp(r.path) ? getNormalPath(basePath + p) : r.path,
+ icon: r.meta?.icon,
+ title: [...prefixTitle]
+ }
+ if (r.meta && r.meta.title) {
+ data.title = [...data.title, r.meta.title];
+ if (r.redirect !== 'noRedirect') {
+ // only push the routes with title
+ // special case: need to exclude parent router without redirect
+ res.push(data);
+ }
+ }
+ // recursive child routes
+ if (r.children) {
+ const tempRoutes = generateRoutes(r.children, data.path, data.title);
+ if (tempRoutes.length >= 1) {
+ res = [...res, ...tempRoutes];
+ }
+ }
+ }
+ })
+ res.forEach((item: any) => {
+ if (item.title instanceof Array) {
+ item.title = item.title.join('/');
+ }
+ });
+ return res;
+}
+// 当前菜单选中时
+const onHandleSelect = (val: any) => {
+ const paths = val.path;
+ if (isHttp(paths)) {
+ // http(s):// 路径新窗口打开
+ const pindex = paths.indexOf("http");
+ window.open(paths.substring(pindex, paths.length), "_blank");
+ } else {
+ router.push(paths);
+ }
+ state.menuQuery = ''
+ closeSearch();
+
+};
+
+// 暴露变量
+defineExpose({
+ openSearch
+});
+</script>
+
+<style scoped lang="scss">
+.layout-search-dialog {
+ position: relative;
+ :deep(.el-dialog) {
+ .el-dialog__header,
+ .el-dialog__body {
+ display: none;
+ }
+ .el-dialog__footer {
+ width: 100%;
+ position: absolute;
+ left: 50%;
+ transform: translateX(-50%);
+ top: -53vh;
+ }
+ }
+ :deep(.el-autocomplete) {
+ width: 560px;
+ position: absolute;
+ top: 150px;
+ left: 50%;
+ transform: translateX(-50%);
+ }
+}
+</style>
diff --git a/src/layout/components/index.ts b/src/layout/components/index.ts
new file mode 100644
index 0000000..47c83e1
--- /dev/null
+++ b/src/layout/components/index.ts
@@ -0,0 +1,4 @@
+export { default as AppMain } from './AppMain.vue';
+export { default as Navbar } from './Navbar.vue';
+export { default as Settings } from './Settings/index.vue';
+export { default as TagsView } from './TagsView/index.vue';
diff --git a/src/layout/components/notice/index.vue b/src/layout/components/notice/index.vue
new file mode 100644
index 0000000..ef4a6a9
--- /dev/null
+++ b/src/layout/components/notice/index.vue
@@ -0,0 +1,134 @@
+<template>
+ <div class="layout-navbars-breadcrumb-user-news" v-loading="state.loading">
+ <div class="head-box">
+ <div class="head-box-title">通知公告</div>
+ <div class="head-box-btn" @click="readAll">全部已读</div>
+ </div>
+ <div class="content-box" v-loading="state.loading">
+ <template v-if="newsList.length > 0">
+ <div class="content-box-item" v-for="(v, k) in newsList" :key="k" @click="onNewsClick(k)">
+ <div class="item-conten">
+ <div>{{ v.message }}</div>
+ <div class="content-box-msg"></div>
+ <div class="content-box-time">{{ v.time }}</div>
+ </div>
+ <!-- 已读/未读 -->
+ <span v-if="v.read" class="el-tag el-tag--success el-tag--mini read">已读</span>
+ <span v-else class="el-tag el-tag--danger el-tag--mini read">未读</span>
+ </div>
+ </template>
+ <el-empty :description="'消息为空'" v-else></el-empty>
+ </div>
+ <div class="foot-box" @click="onGoToGiteeClick" v-if="newsList.length > 0">前往gitee</div>
+ </div>
+</template>
+
+<script setup lang="ts" name="layoutBreadcrumbUserNews">
+import { ref } from "vue";
+import { storeToRefs } from 'pinia'
+import { nextTick, onMounted, reactive } from "vue";
+import useNoticeStore from '@/store/modules/notice';
+
+const noticeStore = storeToRefs(useNoticeStore());
+const {readAll} = useNoticeStore();
+
+// 定义变量内容
+const state = reactive({
+ loading: false,
+});
+const newsList =ref([]) as any;
+
+/**
+ * 初始化数据
+ * @returns
+ */
+const getTableData = async () => {
+ state.loading = true;
+ newsList.value = noticeStore.state.value.notices;
+ state.loading = false;
+};
+
+
+//点击消息,写入已读
+const onNewsClick = (item: any) => {
+ newsList.value[item].read = true;
+ //并且写入pinia
+ noticeStore.state.value.notices = newsList.value;
+};
+
+// 前往通知中心点击
+const onGoToGiteeClick = () => {
+ window.open("https://gitee.com/dromara/RuoYi-Vue-Plus/tree/5.X/");
+};
+
+onMounted(() => {
+ nextTick(() => {
+ getTableData();
+ });
+});
+</script>
+
+<style scoped lang="scss">
+.layout-navbars-breadcrumb-user-news {
+ .head-box {
+ display: flex;
+ border-bottom: 1px solid var(--el-border-color-lighter);
+ box-sizing: border-box;
+ color: var(--el-text-color-primary);
+ justify-content: space-between;
+ height: 35px;
+ align-items: center;
+ .head-box-btn {
+ color: var(--el-color-primary);
+ font-size: 13px;
+ cursor: pointer;
+ opacity: 0.8;
+ &:hover {
+ opacity: 1;
+ }
+ }
+ }
+ .content-box {
+ height: 300px;
+ overflow: auto;
+ font-size: 13px;
+ .content-box-item {
+ padding-top: 12px;
+ display: flex;
+ &:last-of-type {
+ padding-bottom: 12px;
+ }
+ .content-box-msg {
+ color: var(--el-text-color-secondary);
+ margin-top: 5px;
+ margin-bottom: 5px;
+ }
+ .content-box-time {
+ color: var(--el-text-color-secondary);
+ }
+ .item-conten {
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ }
+ }
+ }
+ .foot-box {
+ height: 35px;
+ color: var(--el-color-primary);
+ font-size: 13px;
+ cursor: pointer;
+ opacity: 0.8;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-top: 1px solid var(--el-border-color-lighter);
+ &:hover {
+ opacity: 1;
+ }
+ }
+ :deep(.el-empty__description p) {
+ font-size: 13px;
+ }
+}
+</style>
diff --git a/src/layout/index.vue b/src/layout/index.vue
new file mode 100644
index 0000000..8239ed7
--- /dev/null
+++ b/src/layout/index.vue
@@ -0,0 +1,124 @@
+<template>
+ <div :class="classObj" class="app-wrapper" :style="{ '--current-color': theme }">
+ <div v-if="device === 'mobile' && sidebar.opened" class="drawer-bg" @click="handleClickOutside" />
+ <side-bar v-if="!sidebar.hide" class="sidebar-container" />
+ <div :class="{ hasTagsView: needTagsView, sidebarHide: sidebar.hide }" class="main-container">
+ <!-- <el-scrollbar>
+ <div :class="{ 'fixed-header': fixedHeader }">
+ <navbar ref="navbarRef" @setLayout="setLayout" />
+ <tags-view v-if="needTagsView" />
+ </div>
+ <app-main />
+ <settings ref="settingRef" />
+ </el-scrollbar> -->
+ <div :class="{ 'fixed-header': fixedHeader }">
+ <navbar ref="navbarRef" @setLayout="setLayout" />
+ <tags-view v-if="needTagsView" />
+ </div>
+ <app-main />
+ <settings ref="settingRef" />
+ </div>
+ </div>
+</template>
+
+<script setup lang="ts">
+import SideBar from './components/Sidebar/index.vue'
+import { AppMain, Navbar, Settings, TagsView } from './components'
+import useAppStore from '@/store/modules/app'
+import useSettingsStore from '@/store/modules/settings'
+
+const settingsStore = useSettingsStore()
+const theme = computed(() => settingsStore.theme);
+const sidebar = computed(() => useAppStore().sidebar);
+const device = computed(() => useAppStore().device);
+const needTagsView = computed(() => settingsStore.tagsView);
+const fixedHeader = computed(() => settingsStore.fixedHeader);
+
+const classObj = computed(() => ({
+ hideSidebar: !sidebar.value.opened,
+ openSidebar: sidebar.value.opened,
+ withoutAnimation: sidebar.value.withoutAnimation,
+ mobile: device.value === 'mobile'
+}))
+
+const { width } = useWindowSize();
+const WIDTH = 992; // refer to Bootstrap's responsive design
+
+watchEffect(() => {
+ if (device.value === 'mobile' && sidebar.value.opened) {
+ useAppStore().closeSideBar({ withoutAnimation: false })
+ }
+ if (width.value - 1 < WIDTH) {
+ useAppStore().toggleDevice('mobile')
+ useAppStore().closeSideBar({ withoutAnimation: true })
+ } else {
+ useAppStore().toggleDevice('desktop')
+ }
+})
+
+const navbarRef = ref(Navbar);
+const settingRef = ref(Settings);
+
+onMounted(() => {
+ nextTick(() => {
+ navbarRef.value.initTenantList();
+ })
+})
+
+const handleClickOutside = () => {
+ useAppStore().closeSideBar({ withoutAnimation: false })
+}
+
+const setLayout = () => {
+ settingRef.value.openSetting();
+}
+</script>
+
+<style lang="scss" scoped>
+ @import "@/assets/styles/mixin.scss";
+ @import "@/assets/styles/variables.module.scss";
+
+.app-wrapper {
+ @include clearfix;
+ position: relative;
+ height: 100%;
+ width: 100%;
+
+ &.mobile.openSidebar {
+ position: fixed;
+ top: 0;
+ }
+}
+
+.drawer-bg {
+ background: #000;
+ opacity: 0.3;
+ width: 100%;
+ top: 0;
+ height: 100%;
+ position: absolute;
+ z-index: 999;
+}
+
+.fixed-header {
+ position: fixed;
+ top: 0;
+ right: 0;
+ z-index: 9;
+ width: calc(100% - #{$base-sidebar-width});
+ transition: width 0.28s;
+ background: $fixed-header-bg;
+}
+
+.hideSidebar .fixed-header {
+ width: calc(100% - 54px);
+}
+
+.sidebarHide .fixed-header {
+ width: 100%;
+}
+
+.mobile .fixed-header {
+ width: 100%;
+}
+</style>
diff --git a/src/main.ts b/src/main.ts
new file mode 100644
index 0000000..0ad939a
--- /dev/null
+++ b/src/main.ts
@@ -0,0 +1,57 @@
+import { createApp } from 'vue';
+// global css
+import 'uno.css';
+import '@/assets/styles/index.scss';
+import 'element-plus/theme-chalk/dark/css-vars.css';
+
+// App、router、store
+import App from './App.vue';
+import store from './store';
+import router from './router';
+
+// 自定义指令
+import directive from './directive';
+
+// 注册插件
+import plugins from './plugins/index'; // plugins
+import { download } from '@/utils/request';
+
+// 预设动画
+import animate from './animate';
+
+// svg图标
+import 'virtual:svg-icons-register';
+import ElementIcons from '@/plugins/svgicon';
+
+// permission control
+import './permission';
+
+import { useDict } from '@/utils/dict';
+import { getConfigKey, updateConfigByKey } from '@/api/system/config';
+import { parseTime, addDateRange, handleTree, selectDictLabel, selectDictLabels } from '@/utils/ruoyi';
+
+// 国际化
+import i18n from '@/lang/index';
+
+const app = createApp(App);
+// 全局方法挂载
+app.config.globalProperties.useDict = useDict;
+app.config.globalProperties.getConfigKey = getConfigKey;
+app.config.globalProperties.updateConfigByKey = updateConfigByKey;
+app.config.globalProperties.download = download;
+app.config.globalProperties.parseTime = parseTime;
+app.config.globalProperties.handleTree = handleTree;
+app.config.globalProperties.addDateRange = addDateRange;
+app.config.globalProperties.selectDictLabel = selectDictLabel;
+app.config.globalProperties.selectDictLabels = selectDictLabels;
+app.config.globalProperties.animate = animate;
+
+app.use(ElementIcons);
+app.use(router);
+app.use(store);
+app.use(i18n);
+app.use(plugins);
+// 自定义指令
+directive(app);
+
+app.mount('#app');
diff --git a/src/permission.ts b/src/permission.ts
new file mode 100644
index 0000000..c819ff4
--- /dev/null
+++ b/src/permission.ts
@@ -0,0 +1,63 @@
+import { to as tos } from 'await-to-js';
+import router from './router';
+import NProgress from 'nprogress';
+import 'nprogress/nprogress.css';
+import { getToken } from '@/utils/auth';
+import { isHttp } from '@/utils/validate';
+import { isRelogin } from '@/utils/request';
+import useUserStore from '@/store/modules/user';
+import useSettingsStore from '@/store/modules/settings';
+import usePermissionStore from '@/store/modules/permission';
+
+NProgress.configure({ showSpinner: false });
+const whiteList = ['/login', '/register', '/social-callback'];
+
+router.beforeEach(async (to, from, next) => {
+ NProgress.start();
+ if (getToken()) {
+ to.meta.title && useSettingsStore().setTitle(to.meta.title as string);
+ /* has token*/
+ if (to.path === '/login') {
+ next({ path: '/' });
+ NProgress.done();
+ } else if (whiteList.indexOf(to.path) !== -1) {
+ next()
+ } else {
+ if (useUserStore().roles.length === 0) {
+ isRelogin.show = true;
+ // 判断当前用户是否已拉取完user_info信息
+ const [err] = await tos(useUserStore().getInfo());
+ if (err) {
+ await useUserStore().logout();
+ ElMessage.error(err);
+ next({ path: '/' });
+ } else {
+ isRelogin.show = false;
+ const accessRoutes = await usePermissionStore().generateRoutes();
+ // 根据roles权限生成可访问的路由表
+ accessRoutes.forEach((route) => {
+ if (!isHttp(route.path)) {
+ router.addRoute(route); // 动态添加可访问路由表
+ }
+ });
+ next({ ...to, replace: true }); // hack方法 确保addRoutes已完成
+ }
+ } else {
+ next();
+ }
+ }
+ } else {
+ // 没有token
+ if (whiteList.indexOf(to.path) !== -1) {
+ // 在免登录白名单,直接进入
+ next();
+ } else {
+ next(`/login?redirect=${to.fullPath}`); // 否则全部重定向到登录页
+ NProgress.done();
+ }
+ }
+});
+
+router.afterEach(() => {
+ NProgress.done();
+});
diff --git a/src/plugins/auth.ts b/src/plugins/auth.ts
new file mode 100644
index 0000000..74c91ee
--- /dev/null
+++ b/src/plugins/auth.ts
@@ -0,0 +1,60 @@
+import useUserStore from '@/store/modules/user';
+
+const authPermission = (permission: string): boolean => {
+ const all_permission = '*:*:*';
+ const permissions: string[] = useUserStore().permissions;
+ if (permission && permission.length > 0) {
+ return permissions.some((v) => {
+ return all_permission === v || v === permission;
+ });
+ } else {
+ return false;
+ }
+};
+
+const authRole = (role: string): boolean => {
+ const super_admin = 'admin';
+ const roles = useUserStore().roles;
+ if (role && role.length > 0) {
+ return roles.some((v) => {
+ return super_admin === v || v === role;
+ });
+ } else {
+ return false;
+ }
+};
+
+export default {
+ // 验证用户是否具备某权限
+ hasPermi(permission: string): boolean {
+ return authPermission(permission);
+ },
+ // 验证用户是否含有指定权限,只需包含其中一个
+ hasPermiOr(permissions: string[]): boolean {
+ return permissions.some((item) => {
+ return authPermission(item);
+ });
+ },
+ // 验证用户是否含有指定权限,必须全部拥有
+ hasPermiAnd(permissions: string[]): boolean {
+ return permissions.every((item) => {
+ return authPermission(item);
+ });
+ },
+ // 验证用户是否具备某角色
+ hasRole(role: string): boolean {
+ return authRole(role);
+ },
+ // 验证用户是否含有指定角色,只需包含其中一个
+ hasRoleOr(roles: string[]): boolean {
+ return roles.some((item) => {
+ return authRole(item);
+ });
+ },
+ // 验证用户是否含有指定角色,必须全部拥有
+ hasRoleAnd(roles: string[]): boolean {
+ return roles.every((item) => {
+ return authRole(item);
+ });
+ }
+};
diff --git a/src/plugins/cache.ts b/src/plugins/cache.ts
new file mode 100644
index 0000000..cf43680
--- /dev/null
+++ b/src/plugins/cache.ts
@@ -0,0 +1,77 @@
+const sessionCache = {
+ set(key: string, value: any) {
+ if (!sessionStorage) {
+ return;
+ }
+ if (key != null && value != null) {
+ sessionStorage.setItem(key, value);
+ }
+ },
+ get(key: string) {
+ if (!sessionStorage) {
+ return null;
+ }
+ if (key == null) {
+ return null;
+ }
+ return sessionStorage.getItem(key);
+ },
+ setJSON(key: string, jsonValue: any) {
+ if (jsonValue != null) {
+ this.set(key, JSON.stringify(jsonValue));
+ }
+ },
+ getJSON(key: string) {
+ const value = this.get(key);
+ if (value != null) {
+ return JSON.parse(value);
+ }
+ },
+ remove(key: string) {
+ sessionStorage.removeItem(key);
+ }
+};
+const localCache = {
+ set(key: string, value: any) {
+ if (!localStorage) {
+ return;
+ }
+ if (key != null && value != null) {
+ localStorage.setItem(key, value);
+ }
+ },
+ get(key: string) {
+ if (!localStorage) {
+ return null;
+ }
+ if (key == null) {
+ return null;
+ }
+ return localStorage.getItem(key);
+ },
+ setJSON(key: string, jsonValue: any) {
+ if (jsonValue != null) {
+ this.set(key, JSON.stringify(jsonValue));
+ }
+ },
+ getJSON(key: string) {
+ const value = this.get(key);
+ if (value != null) {
+ return JSON.parse(value);
+ }
+ },
+ remove(key: string) {
+ localStorage.removeItem(key);
+ }
+};
+
+export default {
+ /**
+ * 会话级缓存
+ */
+ session: sessionCache,
+ /**
+ * 本地缓存
+ */
+ local: localCache
+};
diff --git a/src/plugins/download.ts b/src/plugins/download.ts
new file mode 100644
index 0000000..ef66b3a
--- /dev/null
+++ b/src/plugins/download.ts
@@ -0,0 +1,65 @@
+import axios from 'axios';
+import FileSaver from 'file-saver';
+import errorCode from '@/utils/errorCode';
+import { blobValidate } from '@/utils/ruoyi';
+import { LoadingInstance } from 'element-plus/es/components/loading/src/loading';
+import { globalHeaders } from '@/utils/request';
+
+const baseURL = import.meta.env.VITE_APP_BASE_API;
+let downloadLoadingInstance: LoadingInstance;
+export default {
+ async oss(ossId: string | number) {
+ const url = baseURL + '/resource/oss/download/' + ossId;
+ downloadLoadingInstance = ElLoading.service({ text: '正在下载数据,请稍候', background: 'rgba(0, 0, 0, 0.7)' });
+ try {
+ const res = await axios({
+ method: 'get',
+ url: url,
+ responseType: 'blob',
+ headers: globalHeaders()
+ });
+ const isBlob = blobValidate(res.data);
+ if (isBlob) {
+ const blob = new Blob([res.data], { type: 'application/octet-stream' });
+ FileSaver.saveAs(blob, decodeURIComponent(res.headers['download-filename'] as string));
+ } else {
+ this.printErrMsg(res.data);
+ }
+ downloadLoadingInstance.close();
+ } catch (r) {
+ console.error(r);
+ ElMessage.error('下载文件出现错误,请联系管理员!');
+ downloadLoadingInstance.close();
+ }
+ },
+ async zip(url: string, name: string) {
+ url = baseURL + url;
+ downloadLoadingInstance = ElLoading.service({ text: '正在下载数据,请稍候', background: 'rgba(0, 0, 0, 0.7)' });
+ try {
+ const res = await axios({
+ method: 'get',
+ url: url,
+ responseType: 'blob',
+ headers: globalHeaders()
+ });
+ const isBlob = blobValidate(res.data);
+ if (isBlob) {
+ const blob = new Blob([res.data], { type: 'application/zip' });
+ FileSaver.saveAs(blob, name);
+ } else {
+ this.printErrMsg(res.data);
+ }
+ downloadLoadingInstance.close();
+ } catch (r) {
+ console.error(r);
+ ElMessage.error('下载文件出现错误,请联系管理员!');
+ downloadLoadingInstance.close();
+ }
+ },
+ async printErrMsg(data: any) {
+ const resText = await data.text();
+ const rspObj = JSON.parse(resText);
+ const errMsg = errorCode[rspObj.code] || rspObj.msg || errorCode['default'];
+ ElMessage.error(errMsg);
+ }
+};
diff --git a/src/plugins/index.ts b/src/plugins/index.ts
new file mode 100644
index 0000000..09d550a
--- /dev/null
+++ b/src/plugins/index.ts
@@ -0,0 +1,24 @@
+import modal from './modal';
+import tab from './tab';
+import download from './download';
+import cache from './cache';
+import auth from './auth';
+
+import { App } from 'vue';
+
+export default function installPlugin(app: App) {
+ // 页签操作
+ app.config.globalProperties.$tab = tab;
+
+ // 模态框对象
+ app.config.globalProperties.$modal = modal;
+
+ // 缓存对象
+ app.config.globalProperties.$cache = cache;
+
+ // 下载文件
+ app.config.globalProperties.$download = download;
+
+ // 认证对象
+ app.config.globalProperties.$auth = auth;
+}
diff --git a/src/plugins/modal.ts b/src/plugins/modal.ts
new file mode 100644
index 0000000..a8b0548
--- /dev/null
+++ b/src/plugins/modal.ts
@@ -0,0 +1,81 @@
+import { MessageBoxData } from 'element-plus';
+import { LoadingInstance } from 'element-plus/es/components/loading/src/loading';
+let loadingInstance: LoadingInstance;
+export default {
+ // 消息提示
+ msg(content: any) {
+ ElMessage.info(content);
+ },
+ // 错误消息
+ msgError(content: any) {
+ ElMessage.error(content);
+ },
+ // 成功消息
+ msgSuccess(content: any) {
+ ElMessage.success(content);
+ },
+ // 警告消息
+ msgWarning(content: any) {
+ ElMessage.warning(content);
+ },
+ // 弹出提示
+ alert(content: any) {
+ ElMessageBox.alert(content, '系统提示');
+ },
+ // 错误提示
+ alertError(content: any) {
+ ElMessageBox.alert(content, '系统提示', { type: 'error' });
+ },
+ // 成功提示
+ alertSuccess(content: any) {
+ ElMessageBox.alert(content, '系统提示', { type: 'success' });
+ },
+ // 警告提示
+ alertWarning(content: any) {
+ ElMessageBox.alert(content, '系统提示', { type: 'warning' });
+ },
+ // 通知提示
+ notify(content: any) {
+ ElNotification.info(content);
+ },
+ // 错误通知
+ notifyError(content: any) {
+ ElNotification.error(content);
+ },
+ // 成功通知
+ notifySuccess(content: any) {
+ ElNotification.success(content);
+ },
+ // 警告通知
+ notifyWarning(content: any) {
+ ElNotification.warning(content);
+ },
+ // 确认窗体
+ confirm(content: any): Promise<MessageBoxData> {
+ return ElMessageBox.confirm(content, '系统提示', {
+ confirmButtonText: '确定',
+ cancelButtonText: '取消',
+ type: 'warning'
+ });
+ },
+ // 提交内容
+ prompt(content: any) {
+ return ElMessageBox.prompt(content, '系统提示', {
+ confirmButtonText: '确定',
+ cancelButtonText: '取消',
+ type: 'warning'
+ });
+ },
+ // 打开遮罩层
+ loading(content: string) {
+ loadingInstance = ElLoading.service({
+ lock: true,
+ text: content,
+ background: 'rgba(0, 0, 0, 0.7)'
+ });
+ },
+ // 关闭遮罩层
+ closeLoading() {
+ loadingInstance.close();
+ }
+};
diff --git a/src/plugins/svgicon.ts b/src/plugins/svgicon.ts
new file mode 100644
index 0000000..8c68609
--- /dev/null
+++ b/src/plugins/svgicon.ts
@@ -0,0 +1,10 @@
+import * as ElementPlusIconsVue from '@element-plus/icons-vue';
+import { App } from 'vue';
+
+export default {
+ install: (app: App) => {
+ for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
+ app.component(key, component);
+ }
+ }
+};
diff --git a/src/plugins/tab.ts b/src/plugins/tab.ts
new file mode 100644
index 0000000..97442ff
--- /dev/null
+++ b/src/plugins/tab.ts
@@ -0,0 +1,86 @@
+import { useTagsViewStore } from '@/store/modules/tagsView';
+import router from '@/router';
+import { TagView, RouteLocationRaw } from 'vue-router';
+
+export default {
+ /**
+ * 刷新当前tab页签
+ * @param obj 标签对象
+ */
+ async refreshPage(obj?: TagView): Promise<void> {
+ const { path, query, matched } = router.currentRoute.value;
+ if (obj === undefined) {
+ matched.forEach((m) => {
+ if (m.components && m.components.default && m.components.default.name) {
+ if (!['Layout', 'ParentView'].includes(m.components.default.name)) {
+ obj = { name: m.components.default.name, path: path, query: query };
+ }
+ }
+ });
+ }
+ let query1: undefined | {} = {};
+ let path1: undefined | string = '';
+ if (obj) {
+ query1 = obj.query;
+ path1 = obj.path;
+ }
+ await useTagsViewStore().delCachedView(obj);
+ await router.replace({
+ path: '/redirect' + path1,
+ query: query1
+ });
+ },
+ // 关闭当前tab页签,打开新页签
+ closeOpenPage(obj: RouteLocationRaw): void {
+ useTagsViewStore().delView(router.currentRoute.value);
+ if (obj !== undefined) {
+ router.push(obj);
+ }
+ },
+ // 关闭指定tab页签
+ async closePage(obj?: TagView): Promise<{ visitedViews: TagView[]; cachedViews: string[] } | any> {
+ if (obj === undefined) {
+ // prettier-ignore
+ const { visitedViews } = await useTagsViewStore().delView(router.currentRoute.value) as any
+ const latestView = visitedViews.slice(-1)[0];
+ if (latestView) {
+ return router.push(latestView.fullPath);
+ }
+ return router.push('/');
+ }
+ return useTagsViewStore().delView(obj);
+ },
+ // 关闭所有tab页签
+ closeAllPage() {
+ return useTagsViewStore().delAllViews();
+ },
+ // 关闭左侧tab页签
+ closeLeftPage(obj?: TagView) {
+ return useTagsViewStore().delLeftTags(obj || router.currentRoute.value);
+ },
+ // 关闭右侧tab页签
+ closeRightPage(obj?: TagView) {
+ return useTagsViewStore().delRightTags(obj || router.currentRoute.value);
+ },
+ // 关闭其他tab页签
+ closeOtherPage(obj?: TagView) {
+ return useTagsViewStore().delOthersViews(obj || router.currentRoute.value);
+ },
+ /**
+ * 打开tab页签
+ * @param url 路由地址
+ * @param title 标题
+ * @param query 参数
+ */
+ openPage(url: string, title?: string, query?: any) {
+ const obj = { path: url, query: { ...query, title } };
+ return router.push(obj);
+ },
+ /**
+ * 修改tab页签
+ * @param obj 标签对象
+ */
+ updatePage(obj: TagView) {
+ return useTagsViewStore().updateVisitedView(obj);
+ }
+};
diff --git a/src/router/index.ts b/src/router/index.ts
new file mode 100644
index 0000000..271385c
--- /dev/null
+++ b/src/router/index.ts
@@ -0,0 +1,184 @@
+import { createWebHistory, createRouter, RouteOption } from 'vue-router';
+/* Layout */
+import Layout from '@/layout/index.vue';
+
+/**
+ * Note: 路由配置项
+ *
+ * hidden: true // 当设置 true 的时候该路由不会再侧边栏出现 如401,login等页面,或者如一些编辑页面/edit/1
+ * alwaysShow: true // 当你一个路由下面的 children 声明的路由大于1个时,自动会变成嵌套的模式--如组件页面
+ * // 只有一个时,会将那个子路由当做根路由显示在侧边栏--如引导页面
+ * // 若你想不管路由下面的 children 声明的个数都显示你的根路由
+ * // 你可以设置 alwaysShow: true,这样它就会忽略之前定义的规则,一直显示根路由
+ * redirect: noRedirect // 当设置 noRedirect 的时候该路由在面包屑导航中不可被点击
+ * name:'router-name' // 设定路由的名字,一定要填写不然使用<keep-alive>时会出现各种问题
+ * query: '{"id": 1, "name": "ry"}' // 访问路由的默认传递参数
+ * roles: ['admin', 'common'] // 访问路由的角色权限
+ * permissions: ['a:a:a', 'b:b:b'] // 访问路由的菜单权限
+ * meta : {
+ noCache: true // 如果设置为true,则不会被 <keep-alive> 缓存(默认 false)
+ title: 'title' // 设置该路由在侧边栏和面包屑中展示的名字
+ icon: 'svg-name' // 设置该路由的图标,对应路径src/assets/icons/svg
+ breadcrumb: false // 如果设置为false,则不会在breadcrumb面包屑中显示
+ activeMenu: '/system/user' // 当路由设置了该属性,则会高亮相对应的侧边栏。
+ }
+ */
+
+// 公共路由
+export const constantRoutes: RouteOption[] = [
+ {
+ path: '/redirect',
+ component: Layout,
+ hidden: true,
+ children: [
+ {
+ path: '/redirect/:path(.*)',
+ component: () => import('@/views/redirect/index.vue')
+ }
+ ]
+ },
+ {
+ path: '/social-callback',
+ hidden: true,
+ component: () => import('@/layout/components/SocialCallback/index.vue')
+ },
+ {
+ path: '/login',
+ component: () => import('@/views/login.vue'),
+ hidden: true
+ },
+ {
+ path: '/register',
+ component: () => import('@/views/register.vue'),
+ hidden: true
+ },
+ {
+ path: '/:pathMatch(.*)*',
+ component: () => import('@/views/error/404.vue'),
+ hidden: true
+ },
+ {
+ path: '/401',
+ component: () => import('@/views/error/401.vue'),
+ hidden: true
+ },
+ {
+ path: '',
+ component: Layout,
+ redirect: '/index',
+ children: [
+ {
+ path: '/index',
+ component: () => import('@/views/index.vue'),
+ name: 'Index',
+ meta: { title: '首页', icon: 'dashboard', affix: true }
+ }
+ ]
+ },
+ {
+ path: '/user',
+ component: Layout,
+ hidden: true,
+ redirect: 'noredirect',
+ children: [
+ {
+ path: 'profile',
+ component: () => import('@/views/system/user/profile/index.vue'),
+ name: 'Profile',
+ meta: { title: '个人中心', icon: 'user' }
+ }
+ ]
+ }
+];
+
+// 动态路由,基于用户权限动态去加载
+export const dynamicRoutes: RouteOption[] = [
+ {
+ path: '/system/user-auth',
+ component: Layout,
+ hidden: true,
+ permissions: ['system:user:edit'],
+ children: [
+ {
+ path: 'role/:userId(\\d+)',
+ component: () => import('@/views/system/user/authRole.vue'),
+ name: 'AuthRole',
+ meta: { title: '分配角色', activeMenu: '/system/user', icon: '' }
+ }
+ ]
+ },
+ {
+ path: '/system/role-auth',
+ component: Layout,
+ hidden: true,
+ permissions: ['system:role:edit'],
+ children: [
+ {
+ path: 'user/:roleId(\\d+)',
+ component: () => import('@/views/system/role/authUser.vue'),
+ name: 'AuthUser',
+ meta: { title: '分配用户', activeMenu: '/system/role', icon: '' }
+ }
+ ]
+ },
+ {
+ path: '/system/dict-data',
+ component: Layout,
+ hidden: true,
+ permissions: ['system:dict:list'],
+ children: [
+ {
+ path: 'index/:dictId(\\d+)',
+ component: () => import('@/views/system/dict/data.vue'),
+ name: 'Data',
+ meta: { title: '字典数据', activeMenu: '/system/dict', icon: '' }
+ }
+ ]
+ },
+ {
+ path: '/system/oss-config',
+ component: Layout,
+ hidden: true,
+ permissions: ['system:ossConfig:list'],
+ children: [
+ {
+ path: 'index',
+ component: () => import('@/views/system/oss/config.vue'),
+ name: 'OssConfig',
+ meta: { title: '配置管理', activeMenu: '/system/oss', icon: '' }
+ }
+ ]
+ },
+ {
+ path: '/tool/gen-edit',
+ component: Layout,
+ hidden: true,
+ permissions: ['tool:gen:edit'],
+ children: [
+ {
+ path: 'index/:tableId(\\d+)',
+ component: () => import('@/views/tool/gen/editTable.vue'),
+ name: 'GenEdit',
+ meta: { title: '修改生成配置', activeMenu: '/tool/gen', icon: '' }
+ }
+ ]
+ }
+];
+
+/**
+ * 创建路由
+ */
+const router = createRouter({
+ history: createWebHistory(import.meta.env.VITE_APP_CONTEXT_PATH),
+ routes: constantRoutes,
+ // 刷新时,滚动条位置还原
+ scrollBehavior(to, from, savedPosition) {
+ if (savedPosition) {
+ return savedPosition;
+ } else {
+ return { top: 0 };
+ }
+ }
+});
+
+export default router;
diff --git a/src/settings.ts b/src/settings.ts
new file mode 100644
index 0000000..9cebde7
--- /dev/null
+++ b/src/settings.ts
@@ -0,0 +1,55 @@
+const setting: DefaultSettings = {
+ /**
+ * 网页标题
+ */
+ title: import.meta.env.VITE_APP_TITLE,
+
+ theme: '#409EFF',
+
+ /**
+ * 侧边栏主题 深色主题theme-dark,浅色主题theme-light
+ */
+ sideTheme: 'theme-dark',
+ /**
+ * 是否系统布局配置
+ */
+ showSettings: true,
+
+ /**
+ * 是否显示顶部导航
+ */
+ topNav: false,
+
+ /**
+ * 是否显示 tagsView
+ */
+ tagsView: true,
+
+ /**
+ * 是否固定头部
+ */
+ fixedHeader: false,
+
+ /**
+ * 是否显示logo
+ */
+ sidebarLogo: true,
+
+ /**
+ * 是否显示动态标题
+ */
+ dynamicTitle: false,
+
+ /**
+ * @type {string | array} 'production' | ['production', 'development']
+ * @description Need show err logs component.
+ * The default is only used in the production env
+ * If you want to also use it in dev, you can pass ['production', 'development']
+ */
+ errorLog: 'production',
+
+ animationEnable: false,
+
+ dark: false
+};
+export default setting;
diff --git a/src/store/index.ts b/src/store/index.ts
new file mode 100644
index 0000000..069d54e
--- /dev/null
+++ b/src/store/index.ts
@@ -0,0 +1,3 @@
+const store = createPinia();
+
+export default store;
diff --git a/src/store/modules/app.ts b/src/store/modules/app.ts
new file mode 100644
index 0000000..90866c1
--- /dev/null
+++ b/src/store/modules/app.ts
@@ -0,0 +1,72 @@
+import zhCN from 'element-plus/es/locale/lang/zh-cn';
+import enUS from 'element-plus/es/locale/lang/en';
+
+export const useAppStore = defineStore('app', () => {
+ const sidebarStatus = useStorage('sidebarStatus', '1');
+ const sidebar = reactive({
+ opened: sidebarStatus.value ? !!+sidebarStatus.value : true,
+ withoutAnimation: false,
+ hide: false
+ });
+ const device = ref<string>('desktop');
+ const size = useStorage('size', 'default');
+
+ // 语言
+ const language = useStorage('language', 'zh_CN');
+ const languageObj: any = {
+ en_US: enUS,
+ zh_CN: zhCN
+ };
+ const locale = computed(() => {
+ return languageObj[language.value];
+ });
+
+ const toggleSideBar = (withoutAnimation: boolean) => {
+ if (sidebar.hide) {
+ return false;
+ }
+
+ sidebar.opened = !sidebar.opened;
+ sidebar.withoutAnimation = withoutAnimation;
+ if (sidebar.opened) {
+ sidebarStatus.value = '1';
+ } else {
+ sidebarStatus.value = '0';
+ }
+ };
+
+ const closeSideBar = ({ withoutAnimation }: any): void => {
+ sidebarStatus.value = '0';
+ sidebar.opened = false;
+ sidebar.withoutAnimation = withoutAnimation;
+ };
+ const toggleDevice = (d: string): void => {
+ device.value = d;
+ };
+ const setSize = (s: string): void => {
+ size.value = s;
+ };
+ const toggleSideBarHide = (status: boolean): void => {
+ sidebar.hide = status;
+ };
+
+ const changeLanguage = (val: string): void => {
+ language.value = val;
+ };
+
+ return {
+ device,
+ sidebar,
+ language,
+ locale,
+ size,
+ changeLanguage,
+ toggleSideBar,
+ closeSideBar,
+ toggleDevice,
+ setSize,
+ toggleSideBarHide
+ };
+});
+
+export default useAppStore;
diff --git a/src/store/modules/dict.ts b/src/store/modules/dict.ts
new file mode 100644
index 0000000..2f937b9
--- /dev/null
+++ b/src/store/modules/dict.ts
@@ -0,0 +1,78 @@
+export const useDictStore = defineStore('dict', () => {
+ const dict = ref<
+ Array<{
+ key: string;
+ value: DictDataOption[];
+ }>
+ >([]);
+
+ /**
+ * 获取字典
+ * @param _key 字典key
+ */
+ const getDict = (_key: string): DictDataOption[] | null => {
+ if (_key == null && _key == '') {
+ return null;
+ }
+ try {
+ for (let i = 0; i < dict.value.length; i++) {
+ if (dict.value[i].key == _key) {
+ return dict.value[i].value;
+ }
+ }
+ } catch (e) {
+ return null;
+ }
+ return null;
+ };
+
+ /**
+ * 设置字典
+ * @param _key 字典key
+ * @param _value 字典value
+ */
+ const setDict = (_key: string, _value: DictDataOption[]) => {
+ if (_key !== null && _key !== '') {
+ dict.value.push({
+ key: _key,
+ value: _value
+ });
+ }
+ };
+
+ /**
+ * 删除字典
+ * @param _key
+ */
+ const removeDict = (_key: string): boolean => {
+ let bln = false;
+ try {
+ for (let i = 0; i < dict.value.length; i++) {
+ if (dict.value[i].key == _key) {
+ dict.value.splice(i, 1);
+ return true;
+ }
+ }
+ } catch (e) {
+ bln = false;
+ }
+ return bln;
+ };
+
+ /**
+ * 清空字典
+ */
+ const cleanDict = (): void => {
+ dict.value = [];
+ };
+
+ return {
+ dict,
+ getDict,
+ setDict,
+ removeDict,
+ cleanDict
+ };
+});
+
+export default useDictStore;
diff --git a/src/store/modules/notice.ts b/src/store/modules/notice.ts
new file mode 100644
index 0000000..f3f8e5a
--- /dev/null
+++ b/src/store/modules/notice.ts
@@ -0,0 +1,42 @@
+import { defineStore } from 'pinia';
+
+interface NoticeItem {
+ title?: string;
+ read: boolean;
+ message: any;
+ time: string;
+}
+
+export const useNoticeStore = defineStore('notice', () => {
+ const state = reactive({
+ notices: [] as NoticeItem[]
+ });
+
+ const addNotice = (notice: NoticeItem) => {
+ state.notices.push(notice);
+ };
+
+ const removeNotice = (notice: NoticeItem) => {
+ state.notices.splice(state.notices.indexOf(notice), 1);
+ };
+
+ //实现全部已读
+ const readAll = () => {
+ state.notices.forEach((item) => {
+ item.read = true;
+ });
+ };
+
+ const clearNotice = () => {
+ state.notices = [];
+ };
+ return {
+ state,
+ addNotice,
+ removeNotice,
+ readAll,
+ clearNotice
+ };
+});
+
+export default useNoticeStore;
diff --git a/src/store/modules/permission.ts b/src/store/modules/permission.ts
new file mode 100644
index 0000000..ed64e46
--- /dev/null
+++ b/src/store/modules/permission.ts
@@ -0,0 +1,148 @@
+import { defineStore } from 'pinia';
+import router, { constantRoutes, dynamicRoutes } from '@/router';
+import store from '@/store';
+import { getRouters } from '@/api/menu';
+import Layout from '@/layout/index.vue';
+import ParentView from '@/components/ParentView/index.vue';
+import InnerLink from '@/layout/components/InnerLink/index.vue';
+import auth from '@/plugins/auth';
+import { RouteOption } from 'vue-router';
+// 匹配views里面所有的.vue文件
+const modules = import.meta.glob('./../../views/**/*.vue');
+
+export const usePermissionStore = defineStore('permission', () => {
+ const routes = ref<RouteOption[]>([]);
+ const addRoutes = ref<RouteOption[]>([]);
+ const defaultRoutes = ref<RouteOption[]>([]);
+ const topbarRouters = ref<RouteOption[]>([]);
+ const sidebarRouters = ref<RouteOption[]>([]);
+
+ const setRoutes = (newRoutes: RouteOption[]): void => {
+ addRoutes.value = newRoutes;
+ routes.value = constantRoutes.concat(newRoutes);
+ };
+ const setDefaultRoutes = (routes: RouteOption[]): void => {
+ defaultRoutes.value = constantRoutes.concat(routes);
+ };
+ const setTopbarRoutes = (routes: RouteOption[]): void => {
+ topbarRouters.value = routes;
+ };
+ const setSidebarRouters = (routes: RouteOption[]): void => {
+ sidebarRouters.value = routes;
+ };
+ const generateRoutes = async (): Promise<RouteOption[]> => {
+ const res = await getRouters();
+ const { data } = res;
+ const sdata = JSON.parse(JSON.stringify(data));
+ const rdata = JSON.parse(JSON.stringify(data));
+ const defaultData = JSON.parse(JSON.stringify(data));
+ const sidebarRoutes = filterAsyncRouter(sdata);
+ const rewriteRoutes = filterAsyncRouter(rdata, undefined, true);
+ const defaultRoutes = filterAsyncRouter(defaultData);
+ const asyncRoutes = filterDynamicRoutes(dynamicRoutes);
+ asyncRoutes.forEach((route) => {
+ router.addRoute(route);
+ });
+ setRoutes(rewriteRoutes);
+ setSidebarRouters(constantRoutes.concat(sidebarRoutes));
+ setDefaultRoutes(sidebarRoutes);
+ setTopbarRoutes(defaultRoutes);
+ return new Promise<RouteOption[]>((resolve) => resolve(rewriteRoutes));
+ };
+
+ /**
+ * 遍历后台传来的路由字符串,转换为组件对象
+ * @param asyncRouterMap 后台传来的路由字符串
+ * @param lastRouter 上一级路由
+ * @param type 是否是重写路由
+ */
+ const filterAsyncRouter = (asyncRouterMap: RouteOption[], lastRouter?: RouteOption, type = false): RouteOption[] => {
+ return asyncRouterMap.filter((route) => {
+ if (type && route.children) {
+ route.children = filterChildren(route.children, undefined);
+ }
+ if (route.component) {
+ // Layout ParentView 组件特殊处理
+ if (route.component === 'Layout') {
+ route.component = Layout;
+ } else if (route.component === 'ParentView') {
+ route.component = ParentView;
+ } else if (route.component === 'InnerLink') {
+ route.component = InnerLink;
+ } else {
+ route.component = loadView(route.component);
+ }
+ }
+ if (route.children != null && route.children && route.children.length) {
+ route.children = filterAsyncRouter(route.children, route, type);
+ } else {
+ delete route.children;
+ delete route.redirect;
+ }
+ return true;
+ });
+ };
+ const filterChildren = (childrenMap: RouteOption[], lastRouter?: RouteOption): RouteOption[] => {
+ let children: RouteOption[] = [];
+ childrenMap.forEach((el) => {
+ if (el.children && el.children.length) {
+ if (el.component === 'ParentView' && !lastRouter) {
+ el.children.forEach((c) => {
+ c.path = el.path + '/' + c.path;
+ if (c.children && c.children.length) {
+ children = children.concat(filterChildren(c.children, c));
+ return;
+ }
+ children.push(c);
+ });
+ return;
+ }
+ }
+ if (lastRouter) {
+ el.path = lastRouter.path + '/' + el.path;
+ if (el.children && el.children.length) {
+ children = children.concat(filterChildren(el.children, el))
+ return
+ }
+ }
+ children = children.concat(el);
+ });
+ return children;
+ };
+ return { routes, setRoutes, generateRoutes, setSidebarRouters, topbarRouters, sidebarRouters, defaultRoutes };
+});
+
+// 动态路由遍历,验证是否具备权限
+export const filterDynamicRoutes = (routes: RouteOption[]) => {
+ const res: RouteOption[] = [];
+ routes.forEach((route) => {
+ if (route.permissions) {
+ if (auth.hasPermiOr(route.permissions)) {
+ res.push(route);
+ }
+ } else if (route.roles) {
+ if (auth.hasRoleOr(route.roles)) {
+ res.push(route);
+ }
+ }
+ });
+ return res;
+};
+
+export const loadView = (view: any) => {
+ let res;
+ for (const path in modules) {
+ const dir = path.split('views/')[1].split('.vue')[0];
+ if (dir === view) {
+ res = () => modules[path]();
+ }
+ }
+ return res;
+};
+
+// 非setup
+export const usePermissionStoreHook = () => {
+ return usePermissionStore(store);
+};
+
+export default usePermissionStore;
diff --git a/src/store/modules/settings.ts b/src/store/modules/settings.ts
new file mode 100644
index 0000000..fcf595c
--- /dev/null
+++ b/src/store/modules/settings.ts
@@ -0,0 +1,54 @@
+import { defineStore } from 'pinia';
+import defaultSettings from '@/settings';
+import { SettingTypeEnum } from '@/enums/SettingTypeEnum';
+import { useDynamicTitle } from '@/utils/dynamicTitle';
+import { Ref } from 'vue';
+
+export const useSettingsStore = defineStore('setting', () => {
+ const storageSetting = JSON.parse(localStorage.getItem('layout-setting') || '{}');
+
+ const prop: { [key: string]: Ref<any> } = {
+ title: ref<string>(''),
+ theme: ref<string>(storageSetting.theme || defaultSettings.theme),
+ sideTheme: ref<string>(storageSetting.sideTheme || defaultSettings.sideTheme),
+ showSettings: ref<boolean>(storageSetting.showSettings || defaultSettings.showSettings),
+ topNav: ref<boolean>(storageSetting.topNav === undefined ? defaultSettings.topNav : storageSetting.topNav),
+ tagsView: ref<boolean>(storageSetting.tagsView === undefined ? defaultSettings.tagsView : storageSetting.tagsView),
+ fixedHeader: ref<boolean>(storageSetting.fixedHeader === undefined ? defaultSettings.fixedHeader : storageSetting.fixedHeader),
+ sidebarLogo: ref<boolean>(storageSetting.sidebarLogo === undefined ? defaultSettings.sidebarLogo : storageSetting.sidebarLogo),
+ dynamicTitle: ref<boolean>(storageSetting.dynamicTitle === undefined ? defaultSettings.dynamicTitle : storageSetting.dynamicTitle),
+ animationEnable: ref<boolean>(storageSetting.animationEnable === undefined ? defaultSettings.animationEnable : storageSetting.animationEnable),
+ dark: ref<boolean>(storageSetting.dark || defaultSettings.dark)
+ };
+
+ const { title, theme, sideTheme, showSettings, topNav, tagsView, fixedHeader, sidebarLogo, dynamicTitle, animationEnable, dark } = prop;
+
+ // actions
+ const changeSetting = (param: { key: SettingTypeEnum; value: any }) => {
+ const { key, value } = param;
+ if (key in prop) {
+ prop[key].value = value;
+ }
+ };
+ const setTitle = (value: string) => {
+ title.value = value;
+ useDynamicTitle();
+ };
+ return {
+ title,
+ theme,
+ sideTheme,
+ showSettings,
+ topNav,
+ tagsView,
+ fixedHeader,
+ sidebarLogo,
+ dynamicTitle,
+ animationEnable,
+ dark,
+ changeSetting,
+ setTitle
+ };
+});
+
+export default useSettingsStore;
diff --git a/src/store/modules/tagsView.ts b/src/store/modules/tagsView.ts
new file mode 100644
index 0000000..1c1c9a2
--- /dev/null
+++ b/src/store/modules/tagsView.ts
@@ -0,0 +1,209 @@
+import { TagView, RouteRecordNormalized } from 'vue-router';
+
+export const useTagsViewStore = defineStore('tagsView', () => {
+ const visitedViews = ref<TagView[]>([]);
+ const cachedViews = ref<string[]>([]);
+ const iframeViews = ref<TagView[]>([]);
+
+ const addView = (view: TagView) => {
+ addVisitedView(view);
+ addCachedView(view);
+ };
+
+ const addIframeView = (view: TagView): void => {
+ if (iframeViews.value.some((v) => v.path === view.path)) return;
+ iframeViews.value.push(
+ Object.assign({}, view, {
+ title: view.meta?.title || 'no-name'
+ })
+ );
+ };
+ const delIframeView = (view: TagView): Promise<TagView[]> => {
+ return new Promise((resolve) => {
+ iframeViews.value = iframeViews.value.filter((item) => item.path !== view.path);
+ resolve([...iframeViews.value]);
+ });
+ };
+ const addVisitedView = (view: TagView): void => {
+ if (visitedViews.value.some((v) => v.path === view.path)) return;
+ visitedViews.value.push(
+ Object.assign({}, view, {
+ title: view.meta?.title || 'no-name'
+ })
+ );
+ };
+ const delView = (view: TagView): Promise<{ visitedViews: TagView[]; cachedViews: string[] }> => {
+ return new Promise((resolve) => {
+ delVisitedView(view);
+ if (!isDynamicRoute(view)) {
+ delCachedView(view);
+ }
+ resolve({
+ visitedViews: [...visitedViews.value],
+ cachedViews: [...cachedViews.value]
+ });
+ });
+ };
+
+ const delVisitedView = (view: TagView): Promise<TagView[]> => {
+ return new Promise((resolve) => {
+ for (const [i, v] of visitedViews.value.entries()) {
+ if (v.path === view.path) {
+ visitedViews.value.splice(i, 1);
+ break;
+ }
+ }
+ resolve([...visitedViews.value]);
+ });
+ };
+ const delCachedView = (view?: TagView): Promise<string[]> => {
+ let viewName = '';
+ if (view) {
+ viewName = view.name as string;
+ }
+ return new Promise((resolve) => {
+ const index = cachedViews.value.indexOf(viewName);
+ index > -1 && cachedViews.value.splice(index, 1);
+ resolve([...cachedViews.value]);
+ });
+ };
+ const delOthersViews = (view: TagView): Promise<{ visitedViews: TagView[]; cachedViews: string[] }> => {
+ return new Promise((resolve) => {
+ delOthersVisitedViews(view);
+ delOthersCachedViews(view);
+ resolve({
+ visitedViews: [...visitedViews.value],
+ cachedViews: [...cachedViews.value]
+ });
+ });
+ };
+
+ const delOthersVisitedViews = (view: TagView): Promise<TagView[]> => {
+ return new Promise((resolve) => {
+ visitedViews.value = visitedViews.value.filter((v) => {
+ return v.meta?.affix || v.path === view.path;
+ });
+ resolve([...visitedViews.value]);
+ });
+ };
+ const delOthersCachedViews = (view: TagView): Promise<string[]> => {
+ const viewName = view.name as string;
+ return new Promise((resolve) => {
+ const index = cachedViews.value.indexOf(viewName);
+ if (index > -1) {
+ cachedViews.value = cachedViews.value.slice(index, index + 1);
+ } else {
+ cachedViews.value = [];
+ }
+ resolve([...cachedViews.value]);
+ });
+ };
+
+ const delAllViews = (): Promise<{ visitedViews: TagView[]; cachedViews: string[] }> => {
+ return new Promise((resolve) => {
+ delAllVisitedViews();
+ delAllCachedViews();
+ resolve({
+ visitedViews: [...visitedViews.value],
+ cachedViews: [...cachedViews.value]
+ });
+ });
+ };
+ const delAllVisitedViews = (): Promise<TagView[]> => {
+ return new Promise((resolve) => {
+ visitedViews.value = visitedViews.value.filter((tag) => tag.meta?.affix);
+ resolve([...visitedViews.value]);
+ });
+ };
+
+ const delAllCachedViews = (): Promise<string[]> => {
+ return new Promise((resolve) => {
+ cachedViews.value = [];
+ resolve([...cachedViews.value]);
+ });
+ };
+
+ const updateVisitedView = (view: TagView): void => {
+ for (let v of visitedViews.value) {
+ if (v.path === view.path) {
+ v = Object.assign(v, view);
+ break;
+ }
+ }
+ };
+ const delRightTags = (view: TagView): Promise<TagView[]> => {
+ return new Promise((resolve) => {
+ const index = visitedViews.value.findIndex((v) => v.path === view.path);
+ if (index === -1) {
+ return;
+ }
+ visitedViews.value = visitedViews.value.filter((item, idx) => {
+ if (idx <= index || (item.meta && item.meta.affix)) {
+ return true;
+ }
+ const i = cachedViews.value.indexOf(item.name as string);
+ if (i > -1) {
+ cachedViews.value.splice(i, 1);
+ }
+ return false;
+ });
+ resolve([...visitedViews.value]);
+ });
+ };
+ const delLeftTags = (view: TagView): Promise<TagView[]> => {
+ return new Promise((resolve) => {
+ const index = visitedViews.value.findIndex((v) => v.path === view.path);
+ if (index === -1) {
+ return;
+ }
+ visitedViews.value = visitedViews.value.filter((item, idx) => {
+ if (idx >= index || (item.meta && item.meta.affix)) {
+ return true;
+ }
+ const i = cachedViews.value.indexOf(item.name as string);
+ if (i > -1) {
+ cachedViews.value.splice(i, 1);
+ }
+ return false;
+ });
+ resolve([...visitedViews.value]);
+ });
+ };
+
+ const addCachedView = (view: TagView): void => {
+ const viewName = view.name as string;
+ if (!viewName) return;
+ if (cachedViews.value.includes(viewName)) return;
+ if (!view.meta?.noCache) {
+ cachedViews.value.push(viewName);
+ }
+ };
+
+ const isDynamicRoute = (view: any): boolean => {
+ // 检查匹配的路由记录中是否有动态段
+ return view.matched.some((m: RouteRecordNormalized) => m.path.includes(':'));
+ };
+
+ return {
+ visitedViews,
+ cachedViews,
+ iframeViews,
+ addVisitedView,
+ addCachedView,
+ delVisitedView,
+ delCachedView,
+ updateVisitedView,
+ addView,
+ delView,
+ delAllViews,
+ delAllVisitedViews,
+ delAllCachedViews,
+ delOthersViews,
+ delRightTags,
+ delLeftTags,
+ addIframeView,
+ delIframeView
+ };
+});
+
+export default useTagsViewStore;
diff --git a/src/store/modules/user.ts b/src/store/modules/user.ts
new file mode 100644
index 0000000..7c02de0
--- /dev/null
+++ b/src/store/modules/user.ts
@@ -0,0 +1,88 @@
+import { to } from 'await-to-js';
+import defAva from '@/assets/images/profile.jpg';
+import store from '@/store';
+import { getToken, removeToken, setToken } from '@/utils/auth';
+import { login as loginApi, logout as logoutApi, getInfo as getUserInfo } from '@/api/login';
+import { LoginData } from '@/api/types';
+
+export const useUserStore = defineStore('user', () => {
+ const token = ref(getToken());
+ const name = ref('');
+ const nickname = ref('');
+ const userId = ref<string | number>('');
+ const avatar = ref('');
+ const roles = ref<Array<string>>([]); // 用户角色编码集合 → 判断路由权限
+ const permissions = ref<Array<string>>([]); // 用户权限编码集合 → 判断按钮权限
+
+ /**
+ * 登录
+ * @param userInfo
+ * @returns
+ */
+ const login = async (userInfo: LoginData): Promise<void> => {
+ const [err, res] = await to(loginApi(userInfo));
+ if (res) {
+ const data = res.data;
+ setToken(data.access_token);
+ token.value = data.access_token;
+ return Promise.resolve();
+ }
+ return Promise.reject(err);
+ };
+
+ // 获取用户信息
+ const getInfo = async (): Promise<void> => {
+ const [err, res] = await to(getUserInfo());
+ if (res) {
+ const data = res.data;
+ const user = data.user;
+ const profile = user.avatar == '' || user.avatar == null ? defAva : user.avatar;
+
+ if (data.roles && data.roles.length > 0) {
+ // 验证返回的roles是否是一个非空数组
+ roles.value = data.roles;
+ permissions.value = data.permissions;
+ } else {
+ roles.value = ['ROLE_DEFAULT'];
+ }
+ name.value = user.userName;
+ nickname.value = user.nickName;
+ avatar.value = profile;
+ userId.value = user.userId;
+ return Promise.resolve();
+ }
+ return Promise.reject(err);
+ };
+
+ // 注销
+ const logout = async (): Promise<void> => {
+ await logoutApi();
+ token.value = '';
+ roles.value = [];
+ permissions.value = [];
+ removeToken();
+ };
+
+ const setAvatar = (value: string) => {
+ avatar.value = value;
+ };
+
+ return {
+ userId,
+ token,
+ nickname,
+ avatar,
+ roles,
+ permissions,
+ login,
+ getInfo,
+ logout,
+ setAvatar
+ };
+});
+
+export default useUserStore;
+// 非setup
+export function useUserStoreHook() {
+ return useUserStore(store);
+}
diff --git a/src/types/axios.d.ts b/src/types/axios.d.ts
new file mode 100644
index 0000000..36a9f8f
--- /dev/null
+++ b/src/types/axios.d.ts
@@ -0,0 +1,10 @@
+import axios from 'axios';
+
+declare module 'axios' {
+ export interface AxiosResponse<T = any> {
+ code: number;
+ msg: string;
+ rows: T;
+ total: number;
+ }
+}
diff --git a/src/types/element.d.ts b/src/types/element.d.ts
new file mode 100644
index 0000000..ba7598d
--- /dev/null
+++ b/src/types/element.d.ts
@@ -0,0 +1,34 @@
+import type * as ep from 'element-plus';
+declare global {
+ declare type ElTagType = '' | 'success' | 'warning' | 'info' | 'danger' | 'default' | 'primary';
+ declare type ElFormInstance = InstanceType<typeof ep.ElForm>;
+ declare type ElTableInstance = InstanceType<typeof ep.ElTable>;
+ declare type ElTreeInstance = InstanceType<typeof ep.ElTree>;
+ declare type ElTreeSelectInstance = InstanceType<typeof ep.ElTreeSelect>;
+ declare type ElSelectInstance = InstanceType<typeof ep.ElSelect>;
+ declare type ElUploadInstance = InstanceType<typeof ep.ElUpload>;
+ declare type ElCardInstance = InstanceType<typeof ep.ElCard>;
+ declare type ElDialogInstance = InstanceType<typeof ep.ElDialog>;
+ declare type ElInputInstance = InstanceType<typeof ep.ElInput>;
+ declare type ElInputNumberInstance = InstanceType<typeof ep.ElInputNumber>;
+ declare type ElRadioInstance = InstanceType<typeof ep.ElRadio>;
+ declare type ElRadioGroupInstance = InstanceType<typeof ep.ElRadioGroup>;
+ declare type ElRadioButtonInstance = InstanceType<typeof ep.ElRadioButton>;
+ declare type ElCheckboxInstance = InstanceType<typeof ep.ElCheckbox>;
+ declare type ElCheckboxGroupInstance = InstanceType<typeof ep.ElCheckboxGroup>;
+ declare type ElSwitchInstance = InstanceType<typeof ep.ElSwitch>;
+ declare type ElDatePickerInstance = InstanceType<typeof ep.ElDatePicker>;
+ declare type ElTimePickerInstance = InstanceType<typeof ep.ElTimePicker>;
+ declare type ElTimeSelectInstance = InstanceType<typeof ep.ElTimeSelect>;
+ declare type ElCascaderInstance = InstanceType<typeof ep.ElCascader>;
+ declare type ElColorPickerInstance = InstanceType<typeof ep.ElColorPicker>;
+ declare type ElRateInstance = InstanceType<typeof ep.ElRate>;
+ declare type ElSliderInstance = InstanceType<typeof ep.ElSlider>;
+ declare type ElScrollbarInstance = InstanceType<typeof ep.ElScrollbar>;
+
+ declare type TransferKey = ep.TransferKey;
+ declare type CheckboxValueType = ep.CheckboxValueType;
+ declare type ElFormRules = ep.FormRules;
+ declare type DateModelType = ep.DateModelType;
+ declare type UploadFile = typeof ep.UploadFile;
+}
diff --git a/src/types/env.d.ts b/src/types/env.d.ts
new file mode 100644
index 0000000..9560307
--- /dev/null
+++ b/src/types/env.d.ts
@@ -0,0 +1,78 @@
+declare module '*.vue' {
+ import { DefineComponent } from 'vue';
+ const component: DefineComponent<{}, {}, any>;
+ export default component;
+}
+declare module '*.avif' {
+ const src: string;
+ export default src;
+}
+
+declare module '*.bmp' {
+ const src: string;
+ export default src;
+}
+
+declare module '*.gif' {
+ const src: string;
+ export default src;
+}
+
+declare module '*.jpg' {
+ const src: string;
+ export default src;
+}
+
+declare module '*.jpeg' {
+ const src: string;
+ export default src;
+}
+
+declare module '*.png' {
+ const src: string;
+ export default src;
+}
+
+declare module '*.webp' {
+ const src: string;
+ export default src;
+}
+
+declare module '*.svg' {
+ const src: string;
+ export default src;
+}
+
+declare module '*.module.css' {
+ const classes: { readonly [key: string]: string };
+ export default classes;
+}
+
+declare module '*.module.scss' {
+ const classes: { readonly [key: string]: string };
+ export default classes;
+}
+
+declare module '*.module.sass' {
+ const classes: { readonly [key: string]: string };
+ export default classes;
+}
+// 环境变量
+interface ImportMetaEnv {
+ VITE_APP_TITLE: string;
+ VITE_APP_PORT: number;
+ VITE_APP_BASE_API: string;
+ VITE_APP_BASE_URL: string;
+ VITE_APP_CONTEXT_PATH: string;
+ VITE_APP_MONITRO_ADMIN: string;
+ VITE_APP_POWERJOB_ADMIN: string;
+ VITE_APP_ENV: string;
+ VITE_APP_RSA_PUBLIC_KEY: string;
+ VITE_APP_RSA_PRIVATE_KEY: string;
+ VITE_APP_CLIENT_ID: string;
+ VITE_APP_WEBSOCKET: string;
+}
+interface ImportMeta {
+ readonly env: ImportMetaEnv;
+ // readonly glob: any;
+}
diff --git a/src/types/global.d.ts b/src/types/global.d.ts
new file mode 100644
index 0000000..32081bf
--- /dev/null
+++ b/src/types/global.d.ts
@@ -0,0 +1,91 @@
+import type { ComponentInternalInstance as ComponentInstance, PropType as VuePropType } from 'vue';
+
+declare global {
+ /** vue Instance */
+ declare type ComponentInternalInstance = ComponentInstance;
+ /**vue */
+ declare type PropType<T> = VuePropType<T>;
+
+ /**
+ * 界面字段隐藏属性
+ */
+ declare interface FieldOption {
+ key: number;
+ label: string;
+ visible: boolean;
+ children?: Array<FieldOption>;
+ }
+
+ /**
+ * 弹窗属性
+ */
+ declare interface DialogOption {
+ /**
+ * 弹窗标题
+ */
+ title?: string;
+ /**
+ * 是否显示
+ */
+ visible: boolean;
+ }
+
+ declare interface UploadOption {
+ /** 设置上传的请求头部 */
+ headers: { [key: string]: any };
+
+ /** 上传的地址 */
+ url: string;
+ }
+
+ /**
+ * 导入属性
+ */
+ declare interface ImportOption extends UploadOption {
+ /** 是否显示弹出层 */
+ open: boolean;
+ /** 弹出层标题 */
+ title: string;
+ /** 是否禁用上传 */
+ isUploading: boolean;
+
+ /** 其他参数 */
+ [key: string]: any;
+ }
+ /**
+ * 字典数据 数据配置
+ */
+ declare interface DictDataOption {
+ label: string;
+ value: string;
+ elTagType?: ElTagType;
+ elTagClass?: string;
+ }
+
+ declare interface BaseEntity {
+ createBy?: any;
+ createDept?: any;
+ createTime?: string;
+ updateBy?: any;
+ updateTime?: any;
+ }
+
+ /**
+ * 分页数据
+ * T : 表单数据
+ * D : 查询参数
+ */
+ declare interface PageData<T, D> {
+ form: T;
+ queryParams: D;
+ rules: ElFormRules;
+ }
+ /**
+ * 分页查询参数
+ */
+ declare interface PageQuery {
+ pageNum: number;
+ pageSize: number;
+ }
+}
+export {};
diff --git a/src/types/module.d.ts b/src/types/module.d.ts
new file mode 100644
index 0000000..987c349
--- /dev/null
+++ b/src/types/module.d.ts
@@ -0,0 +1,32 @@
+import type modal from '@/plugins/modal';
+import type tab from '@/plugins/tab';
+import type download from '@/plugins/download';
+import type auth from '@/plugins/auth';
+import type cache from '@/plugins/cache';
+import type animate from '@/animate';
+import type { useDict } from '@/utils/dict';
+import type { addDateRange, handleTree, selectDictLabel, selectDictLabels, parseTime } from '@/utils/ruoyi';
+import type { getConfigKey, updateConfigByKey } from '@/api/system/config';
+import type { download as rd } from '@/utils/request';
+
+declare module '@vue/runtime-core' {
+ interface ComponentCustomProperties {
+ // 全局方法声明
+ $modal: typeof modal;
+ $tab: typeof tab;
+ $download: typeof download;
+ $auth: typeof auth;
+ $cache: typeof cache;
+ animate: typeof animate;
+
+ useDict: typeof useDict;
+ addDateRange: typeof addDateRange;
+ download: typeof rd;
+ handleTree: typeof handleTree;
+ getConfigKey: typeof getConfigKey;
+ updateConfigByKey: typeof updateConfigByKey;
+ selectDictLabel: typeof selectDictLabel;
+ selectDictLabels: typeof selectDictLabels;
+ parseTime: typeof parseTime;
+ }
+}
diff --git a/src/types/router.d.ts b/src/types/router.d.ts
new file mode 100644
index 0000000..9731fa2
--- /dev/null
+++ b/src/types/router.d.ts
@@ -0,0 +1,36 @@
+import { RouteRecordRaw } from 'vue-router';
+
+declare module 'vue-router' {
+ declare type RouteOption = {
+ hidden?: boolean;
+ permissions?: string[];
+ roles?: string[];
+ component?: any;
+ children?: RouteOption[];
+ alwaysShow?: boolean;
+ parentPath?: string;
+ meta?: {
+ title: string;
+ icon: string;
+ };
+ query?: string;
+ } & RouteRecordRaw;
+
+ declare interface _RouteLocationBase {
+ children?: RouteOption[];
+ }
+
+ declare interface RouteLocationOptions {
+ fullPath?: string;
+ }
+
+ declare interface TagView extends Partial<_RouteLocationBase> {
+ title?: string;
+ meta?: {
+ link?: string;
+ title?: string;
+ affix?: boolean;
+ noCache?: boolean;
+ };
+ }
+}
diff --git a/src/types/setting.d.ts b/src/types/setting.d.ts
new file mode 100644
index 0000000..1b5f9fd
--- /dev/null
+++ b/src/types/setting.d.ts
@@ -0,0 +1,70 @@
+declare type DefaultSettings = {
+ /**
+ * 网页标题
+ */
+ title?: string;
+
+ /**
+ * 侧边栏主题 theme-dark | theme-light
+ */
+ sideTheme?: string;
+
+ /**
+ * 是否显示系统布局设置
+ */
+ showSettings?: boolean;
+
+ /**
+ * 是否显示顶部导航
+ */
+ topNav?: boolean;
+
+ /**
+ * 是否显示多标签导航
+ */
+ tagsView?: boolean;
+ /**
+ * 是否固定头部
+ */
+ fixedHeader?: boolean;
+ /**
+ * 是否显示侧边栏Logo
+ */
+ sidebarLogo?: boolean;
+ /**
+ * 导航栏布局
+ */
+ layout?: string;
+ /**
+ * 主题模式
+ */
+ theme?: string;
+
+ /**
+ * 布局大小
+ */
+ size?: string;
+
+ /**
+ * 语言
+ */
+ language?: string;
+
+ /**
+ * 是否显示动态标题
+ */
+ dynamicTitle?: boolean;
+ /**
+ * 是否启用动画效果
+ */
+ animationEnable?: boolean;
+ /**
+ * 是否启用暗黑模式
+ *
+ * true:暗黑模式
+ * false: 明亮模式
+ */
+ dark?: boolean;
+
+ errorLog?: string;
+};
diff --git a/src/types/vform3-builds.d.ts b/src/types/vform3-builds.d.ts
new file mode 100644
index 0000000..82573de
--- /dev/null
+++ b/src/types/vform3-builds.d.ts
@@ -0,0 +1,4 @@
+declare module 'vform3-builds' {
+ const content: any;
+ export = content;
+}
diff --git a/src/utils/auth.ts b/src/utils/auth.ts
new file mode 100644
index 0000000..db50ac9
--- /dev/null
+++ b/src/utils/auth.ts
@@ -0,0 +1,9 @@
+const TokenKey = 'Admin-Token';
+
+const tokenStorage = useStorage<null | string>(TokenKey, null);
+
+export const getToken = () => tokenStorage.value;
+
+export const setToken = (access_token: string) => (tokenStorage.value = access_token);
+
+export const removeToken = () => (tokenStorage.value = null);
diff --git a/src/utils/crypto.ts b/src/utils/crypto.ts
new file mode 100644
index 0000000..8217146
--- /dev/null
+++ b/src/utils/crypto.ts
@@ -0,0 +1,66 @@
+import CryptoJS from 'crypto-js';
+
+/**
+ * 随机生成32位的字符串
+ * @returns {string}
+ */
+const generateRandomString = () => {
+ const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
+ let result = '';
+ const charactersLength = characters.length;
+ for (let i = 0; i < 32; i++) {
+ result += characters.charAt(Math.floor(Math.random() * charactersLength));
+ }
+ return result;
+};
+
+/**
+ * 随机生成aes 密钥
+ * @returns {string}
+ */
+export const generateAesKey = () => {
+ return CryptoJS.enc.Utf8.parse(generateRandomString());
+};
+
+/**
+ * 加密base64
+ * @returns {string}
+ */
+export const encryptBase64 = (str: CryptoJS.lib.WordArray) => {
+ return CryptoJS.enc.Base64.stringify(str);
+};
+
+/**
+ * 解密base64
+ */
+export const decryptBase64 = (str: string) => {
+ return CryptoJS.enc.Base64.parse(str);
+};
+
+/**
+ * 使用密钥对数据进行加密
+ * @param message
+ * @param aesKey
+ * @returns {string}
+ */
+export const encryptWithAes = (message: string, aesKey: CryptoJS.lib.WordArray) => {
+ const encrypted = CryptoJS.AES.encrypt(message, aesKey, {
+ mode: CryptoJS.mode.ECB,
+ padding: CryptoJS.pad.Pkcs7
+ });
+ return encrypted.toString();
+};
+
+/**
+ * 使用密钥对数据进行解密
+ * @param message
+ * @param aesKey
+ * @returns {string}
+ */
+export const decryptWithAes = (message: string, aesKey: CryptoJS.lib.WordArray) => {
+ const decrypted = CryptoJS.AES.decrypt(message, aesKey, {
+ mode: CryptoJS.mode.ECB,
+ padding: CryptoJS.pad.Pkcs7
+ });
+ return decrypted.toString(CryptoJS.enc.Utf8);
+};
diff --git a/src/utils/dict.ts b/src/utils/dict.ts
new file mode 100644
index 0000000..0575072
--- /dev/null
+++ b/src/utils/dict.ts
@@ -0,0 +1,27 @@
+import { getDicts } from '@/api/system/dict/data';
+import { useDictStore } from '@/store/modules/dict';
+/**
+ * 获取字典数据
+ */
+export const useDict = (...args: string[]): { [key: string]: DictDataOption[] } => {
+ const res = ref<{
+ [key: string]: DictDataOption[];
+ }>({});
+ return (() => {
+ args.forEach(async (dictType) => {
+ res.value[dictType] = [];
+ const dicts = useDictStore().getDict(dictType);
+ if (dicts) {
+ res.value[dictType] = dicts;
+ } else {
+ await getDicts(dictType).then((resp) => {
+ res.value[dictType] = resp.data.map(
+ (p): DictDataOption => ({ label: p.dictLabel, value: p.dictValue, elTagType: p.listClass, elTagClass: p.cssClass })
+ );
+ useDictStore().setDict(dictType, res.value[dictType]);
+ });
+ }
+ });
+ return res.value;
+ })();
+};
diff --git a/src/utils/dynamicTitle.ts b/src/utils/dynamicTitle.ts
new file mode 100644
index 0000000..8e23ef8
--- /dev/null
+++ b/src/utils/dynamicTitle.ts
@@ -0,0 +1,14 @@
+import defaultSettings from '@/settings';
+import { useSettingsStore } from '@/store/modules/settings';
+
+/**
+ * 动态修改标题
+ */
+export const useDynamicTitle = () => {
+ const settingsStore = useSettingsStore();
+ if (settingsStore.dynamicTitle) {
+ document.title = settingsStore.title + ' - ' + import.meta.env.VITE_APP_TITLE;
+ } else {
+ document.title = defaultSettings.title as string;
+ }
+};
diff --git a/src/utils/errorCode.ts b/src/utils/errorCode.ts
new file mode 100644
index 0000000..d85914e
--- /dev/null
+++ b/src/utils/errorCode.ts
@@ -0,0 +1,7 @@
+export const errorCode: any = {
+ '401': '认证失败,无法访问系统资源',
+ '403': '当前操作没有权限',
+ '404': '访问资源不存在',
+ default: '系统未知错误,请反馈给管理员'
+};
+export default errorCode;
diff --git a/src/utils/i18n.ts b/src/utils/i18n.ts
new file mode 100644
index 0000000..ab77af0
--- /dev/null
+++ b/src/utils/i18n.ts
@@ -0,0 +1,16 @@
+// translate router.meta.title, be used in breadcrumb sidebar tagsview
+import i18n from '@/lang/index';
+
+/**
+ * 获取国际化路由,如果不存在则原生返回
+ * @param title 路由名称
+ * @returns {string}
+ */
+export const translateRouteTitle = (title: string): string => {
+ const hasKey = i18n.global.te('route.' + title);
+ if (hasKey) {
+ const translatedTitle = i18n.global.t('route.' + title);
+ return translatedTitle;
+ }
+ return title;
+};
diff --git a/src/utils/index.ts b/src/utils/index.ts
new file mode 100644
index 0000000..f474462
--- /dev/null
+++ b/src/utils/index.ts
@@ -0,0 +1,318 @@
+import { parseTime } from '@/utils/ruoyi';
+
+/**
+ * 表格时间格式化
+ */
+export const formatDate = (cellValue: string) => {
+ if (cellValue == null || cellValue == '') return '';
+ const date = new Date(cellValue);
+ const year = date.getFullYear();
+ const month = date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1;
+ const day = date.getDate() < 10 ? '0' + date.getDate() : date.getDate();
+ const hours = date.getHours() < 10 ? '0' + date.getHours() : date.getHours();
+ const minutes = date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes();
+ const seconds = date.getSeconds() < 10 ? '0' + date.getSeconds() : date.getSeconds();
+ return year + '-' + month + '-' + day + ' ' + hours + ':' + minutes + ':' + seconds;
+};
+
+/**
+ * @param {number} time
+ * @param {string} option
+ * @returns {string}
+ */
+export const formatTime = (time: string, option: string) => {
+ let t: number;
+ if (('' + time).length === 10) {
+ t = parseInt(time) * 1000;
+ } else {
+ t = +time;
+ }
+ const d: any = new Date(t);
+ const now = Date.now();
+
+ const diff = (now - d) / 1000;
+
+ if (diff < 30) {
+ return '刚刚';
+ } else if (diff < 3600) {
+ // less 1 hour
+ return Math.ceil(diff / 60) + '分钟前';
+ } else if (diff < 3600 * 24) {
+ return Math.ceil(diff / 3600) + '小时前';
+ } else if (diff < 3600 * 24 * 2) {
+ return '1天前';
+ }
+ if (option) {
+ return parseTime(t, option);
+ } else {
+ return d.getMonth() + 1 + '月' + d.getDate() + '日' + d.getHours() + '时' + d.getMinutes() + '分';
+ }
+};
+
+/**
+ * @param {string} url
+ * @returns {Object}
+ */
+export const getQueryObject = (url: string) => {
+ url = url == null ? window.location.href : url;
+ const search = url.substring(url.lastIndexOf('?') + 1);
+ const obj: { [key: string]: string } = {};
+ const reg = /([^?&=]+)=([^?&=]*)/g;
+ search.replace(reg, (rs, $1, $2) => {
+ const name = decodeURIComponent($1);
+ let val = decodeURIComponent($2);
+ val = String(val);
+ obj[name] = val;
+ return rs;
+ });
+ return obj;
+};
+
+/**
+ * @param {string} input value
+ * @returns {number} output value
+ */
+export const byteLength = (str: string) => {
+ // returns the byte length of an utf8 string
+ let s = str.length;
+ for (let i = str.length - 1; i >= 0; i--) {
+ const code = str.charCodeAt(i);
+ if (code > 0x7f && code <= 0x7ff) s++;
+ else if (code > 0x7ff && code <= 0xffff) s += 2;
+ if (code >= 0xdc00 && code <= 0xdfff) i--;
+ }
+ return s;
+};
+
+/**
+ * @param {Array} actual
+ * @returns {Array}
+ */
+export const cleanArray = (actual: Array<any>) => {
+ const newArray = [];
+ for (let i = 0; i < actual.length; i++) {
+ if (actual[i]) {
+ newArray.push(actual[i]);
+ }
+ }
+ return newArray;
+};
+
+/**
+ * @param {Object} json
+ * @returns {Array}
+ */
+export const param = (json: any) => {
+ if (!json) return '';
+ return cleanArray(
+ Object.keys(json).map((key) => {
+ if (json[key] === undefined) return '';
+ return encodeURIComponent(key) + '=' + encodeURIComponent(json[key]);
+ })
+ ).join('&');
+};
+
+/**
+ * @param {string} url
+ * @returns {Object}
+ */
+export const param2Obj = (url: string) => {
+ const search = decodeURIComponent(url.split('?')[1]).replace(/\+/g, ' ');
+ if (!search) {
+ return {};
+ }
+ const obj: any = {};
+ const searchArr = search.split('&');
+ searchArr.forEach((v) => {
+ const index = v.indexOf('=');
+ if (index !== -1) {
+ const name = v.substring(0, index);
+ const val = v.substring(index + 1, v.length);
+ obj[name] = val;
+ }
+ });
+ return obj;
+};
+
+/**
+ * @param {string} val
+ * @returns {string}
+ */
+export const html2Text = (val: string) => {
+ const div = document.createElement('div');
+ div.innerHTML = val;
+ return div.textContent || div.innerText;
+};
+
+/**
+ * Merges two objects, giving the last one precedence
+ * @param {Object} target
+ * @param {(Object|Array)} source
+ * @returns {Object}
+ */
+export const objectMerge = (target: any, source: any | any[]) => {
+ if (typeof target !== 'object') {
+ target = {};
+ }
+ if (Array.isArray(source)) {
+ return source.slice();
+ }
+ Object.keys(source).forEach((property) => {
+ const sourceProperty = source[property];
+ if (typeof sourceProperty === 'object') {
+ target[property] = objectMerge(target[property], sourceProperty);
+ } else {
+ target[property] = sourceProperty;
+ }
+ });
+ return target;
+};
+
+/**
+ * @param {HTMLElement} element
+ * @param {string} className
+ */
+export const toggleClass = (element: HTMLElement, className: string) => {
+ if (!element || !className) {
+ return;
+ }
+ let classString = element.className;
+ const nameIndex = classString.indexOf(className);
+ if (nameIndex === -1) {
+ classString += '' + className;
+ } else {
+ classString = classString.substring(0, nameIndex) + classString.substring(nameIndex + className.length);
+ }
+ element.className = classString;
+};
+
+/**
+ * @param {string} type
+ * @returns {Date}
+ */
+export const getTime = (type: string) => {
+ if (type === 'start') {
+ return new Date().getTime() - 3600 * 1000 * 24 * 90;
+ } else {
+ return new Date(new Date().toDateString());
+ }
+};
+
+/**
+ * @param {Function} func
+ * @param {number} wait
+ * @param {boolean} immediate
+ * @return {*}
+ */
+export const debounce = (func: any, wait: number, immediate: boolean) => {
+ let timeout: any, args: any, context: any, timestamp: any, result: any;
+
+ const later = function () {
+ // 据上一次触发时间间隔
+ const last = +new Date() - timestamp;
+
+ // 上次被包装函数被调用时间间隔 last 小于设定时间间隔 wait
+ if (last < wait && last > 0) {
+ timeout = setTimeout(later, wait - last);
+ } else {
+ timeout = null;
+ // 如果设定为immediate===true,因为开始边界已经调用过了此处无需调用
+ if (!immediate) {
+ result = func.apply(context, args);
+ if (!timeout) context = args = null;
+ }
+ }
+ };
+
+ return (...args: any) => {
+ context = this;
+ timestamp = +new Date();
+ const callNow = immediate && !timeout;
+ // 如果延时不存在,重新设定延时
+ if (!timeout) timeout = setTimeout(later, wait);
+ if (callNow) {
+ result = func.apply(context, args);
+ context = args = null;
+ }
+ return result;
+ };
+};
+
+/**
+ * This is just a simple version of deep copy
+ * Has a lot of edge cases bug
+ * If you want to use a perfect deep copy, use lodash's _.cloneDeep
+ * @param {Object} source
+ * @returns {Object}
+ */
+export const deepClone = (source: any) => {
+ if (!source && typeof source !== 'object') {
+ throw new Error('error arguments', 'deepClone' as any);
+ }
+ const targetObj: any = source.constructor === Array ? [] : {};
+ Object.keys(source).forEach((keys) => {
+ if (source[keys] && typeof source[keys] === 'object') {
+ targetObj[keys] = deepClone(source[keys]);
+ } else {
+ targetObj[keys] = source[keys];
+ }
+ });
+ return targetObj;
+};
+
+/**
+ * @param {Array} arr
+ * @returns {Array}
+ */
+export const uniqueArr = (arr: any) => {
+ return Array.from(new Set(arr));
+};
+
+/**
+ * @returns {string}
+ */
+export const createUniqueString = (): string => {
+ const timestamp = +new Date() + '';
+ const num = (1 + Math.random()) * 65536;
+ const randomNum = parseInt(num + '');
+ return (+(randomNum + timestamp)).toString(32);
+};
+
+/**
+ * Check if an element has a class
+ * @param ele
+ * @param {string} cls
+ * @returns {boolean}
+ */
+export const hasClass = (ele: HTMLElement, cls: string): boolean => {
+ return !!ele.className.match(new RegExp('(\\s|^)' + cls + '(\\s|$)'));
+};
+
+/**
+ * Add class to element
+ * @param ele
+ * @param {string} cls
+ */
+export const addClass = (ele: HTMLElement, cls: string) => {
+ if (!hasClass(ele, cls)) ele.className += ' ' + cls;
+};
+
+/**
+ * Remove class from element
+ * @param ele
+ * @param {string} cls
+ */
+export const removeClass = (ele: HTMLElement, cls: string) => {
+ if (hasClass(ele, cls)) {
+ const reg = new RegExp('(\\s|^)' + cls + '(\\s|$)');
+ ele.className = ele.className.replace(reg, ' ');
+ }
+};
+
+/**
+ * @param {string} path
+ * @returns {Boolean}
+ */
+export const isExternal = (path: string) => {
+ return /^(https?:|http?:|mailto:|tel:)/.test(path);
+};
diff --git a/src/utils/jsencrypt.ts b/src/utils/jsencrypt.ts
new file mode 100644
index 0000000..42de5a0
--- /dev/null
+++ b/src/utils/jsencrypt.ts
@@ -0,0 +1,21 @@
+import JSEncrypt from 'jsencrypt';
+// 密钥对生成 http://web.chacuo.net/netrsakeypair
+
+const publicKey = import.meta.env.VITE_APP_RSA_PUBLIC_KEY;
+
+// 前端不建议存放私钥 不建议解密数据 因为都是透明的意义不大
+const privateKey = import.meta.env.VITE_APP_RSA_PRIVATE_KEY;
+
+// 加密
+export const encrypt = (txt: string) => {
+ const encryptor = new JSEncrypt();
+ encryptor.setPublicKey(publicKey); // 设置公钥
+ return encryptor.encrypt(txt); // 对数据进行加密
+};
+
+// 解密
+export const decrypt = (txt: string) => {
+ const encryptor = new JSEncrypt();
+ encryptor.setPrivateKey(privateKey); // 设置私钥
+ return encryptor.decrypt(txt); // 对数据进行解密
+};
diff --git a/src/utils/permission.ts b/src/utils/permission.ts
new file mode 100644
index 0000000..eb3838a
--- /dev/null
+++ b/src/utils/permission.ts
@@ -0,0 +1,51 @@
+import useUserStore from '@/store/modules/user';
+
+/**
+ * 字符权限校验
+ * @param {Array} value 校验值
+ * @returns {Boolean}
+ */
+export const checkPermi = (value: any) => {
+ if (value && value instanceof Array && value.length > 0) {
+ const permissions = useUserStore().permissions;
+ const permissionDatas = value;
+ const all_permission = '*:*:*';
+
+ const hasPermission = permissions.some((permission) => {
+ return all_permission === permission || permissionDatas.includes(permission);
+ });
+
+ if (!hasPermission) {
+ return false;
+ }
+ return true;
+ } else {
+ console.error(`need roles! Like checkPermi="['system:user:add','system:user:edit']"`);
+ return false;
+ }
+};
+
+/**
+ * 角色权限校验
+ * @param {Array} value 校验值
+ * @returns {Boolean}
+ */
+export const checkRole = (value: any): boolean => {
+ if (value && value instanceof Array && value.length > 0) {
+ const roles = useUserStore().roles;
+ const permissionRoles = value;
+ const super_admin = 'admin';
+
+ const hasRole = roles.some((role) => {
+ return super_admin === role || permissionRoles.includes(role);
+ });
+
+ if (!hasRole) {
+ return false;
+ }
+ return true;
+ } else {
+ console.error(`need roles! Like checkRole="['admin','editor']"`);
+ return false;
+ }
+};
diff --git a/src/utils/propTypes.ts b/src/utils/propTypes.ts
new file mode 100644
index 0000000..1847040
--- /dev/null
+++ b/src/utils/propTypes.ts
@@ -0,0 +1,25 @@
+import { CSSProperties } from 'vue';
+import VueTypes, { createTypes, toValidableType, VueTypeValidableDef, VueTypesInterface } from 'vue-types';
+
+type PropTypes = VueTypesInterface & {
+ readonly style: VueTypeValidableDef<CSSProperties>;
+};
+
+const propTypes = createTypes({
+ func: undefined,
+ bool: undefined,
+ string: undefined,
+ number: undefined,
+ object: undefined,
+ integer: undefined
+}) as PropTypes;
+
+export default class ProjectTypes extends VueTypes {
+ static get style() {
+ return toValidableType('style', {
+ type: [String, Object],
+ default: undefined
+ });
+ }
+}
+export { propTypes };
diff --git a/src/utils/request.ts b/src/utils/request.ts
new file mode 100644
index 0000000..a183ca6
--- /dev/null
+++ b/src/utils/request.ts
@@ -0,0 +1,198 @@
+import axios, { AxiosResponse, InternalAxiosRequestConfig } from 'axios';
+import { useUserStore } from '@/store/modules/user';
+import { getToken } from '@/utils/auth';
+import { tansParams, blobValidate } from '@/utils/ruoyi';
+import cache from '@/plugins/cache';
+import { HttpStatus } from '@/enums/RespEnum';
+import { errorCode } from '@/utils/errorCode';
+import { LoadingInstance } from 'element-plus/es/components/loading/src/loading';
+import FileSaver from 'file-saver';
+import { getLanguage } from '@/lang';
+import { encryptBase64, encryptWithAes, generateAesKey, decryptWithAes, decryptBase64 } from '@/utils/crypto';
+import { encrypt, decrypt } from '@/utils/jsencrypt';
+
+const encryptHeader = 'encrypt-key';
+let downloadLoadingInstance: LoadingInstance;
+// 是否显示重新登录
+export const isRelogin = { show: false };
+export const globalHeaders = () => {
+ return {
+ Authorization: 'Bearer ' + getToken(),
+ clientid: import.meta.env.VITE_APP_CLIENT_ID
+ };
+};
+
+axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8';
+axios.defaults.headers['clientid'] = import.meta.env.VITE_APP_CLIENT_ID;
+// 创建 axios 实例
+const service = axios.create({
+ baseURL: import.meta.env.VITE_APP_BASE_API,
+ timeout: 50000
+});
+
+// 请求拦截器
+service.interceptors.request.use(
+ (config: InternalAxiosRequestConfig) => {
+ // 对应国际化资源文件后缀
+ config.headers['Content-Language'] = getLanguage();
+
+ const isToken = (config.headers || {}).isToken === false;
+ // 是否需要防止数据重复提交
+ const isRepeatSubmit = (config.headers || {}).repeatSubmit === false;
+ // 是否需要加密
+ const isEncrypt = (config.headers || {}).isEncrypt === 'true';
+ if (getToken() && !isToken) {
+ config.headers['Authorization'] = 'Bearer ' + getToken(); // 让每个请求携带自定义token 请根据实际情况自行修改
+ }
+ // get请求映射params参数
+ if (config.method === 'get' && config.params) {
+ let url = config.url + '?' + tansParams(config.params);
+ url = url.slice(0, -1);
+ config.params = {};
+ config.url = url;
+ }
+
+ if (!isRepeatSubmit && (config.method === 'post' || config.method === 'put')) {
+ const requestObj = {
+ url: config.url,
+ data: typeof config.data === 'object' ? JSON.stringify(config.data) : config.data,
+ time: new Date().getTime()
+ };
+ const sessionObj = cache.session.getJSON('sessionObj');
+ if (sessionObj === undefined || sessionObj === null || sessionObj === '') {
+ cache.session.setJSON('sessionObj', requestObj);
+ } else {
+ const s_url = sessionObj.url; // 请求地址
+ const s_data = sessionObj.data; // 请求数据
+ const s_time = sessionObj.time; // 请求时间
+ const interval = 500; // 间隔时间(ms),小于此时间视为重复提交
+ if (s_data === requestObj.data && requestObj.time - s_time < interval && s_url === requestObj.url) {
+ const message = '数据正在处理,请勿重复提交';
+ console.warn(`[${s_url}]: ` + message);
+ return Promise.reject(new Error(message));
+ } else {
+ cache.session.setJSON('sessionObj', requestObj);
+ }
+ }
+ }
+ // 当开启参数加密
+ if (isEncrypt && (config.method === 'post' || config.method === 'put')) {
+ // 生成一个 AES 密钥
+ const aesKey = generateAesKey();
+ config.headers[encryptHeader] = encrypt(encryptBase64(aesKey));
+ config.data = typeof config.data === 'object' ? encryptWithAes(JSON.stringify(config.data), aesKey) : encryptWithAes(config.data, aesKey);
+ }
+ // FormData数据去请求头Content-Type
+ if (config.data instanceof FormData) {
+ delete config.headers['Content-Type'];
+ }
+ return config;
+ },
+ (error: any) => {
+ console.log(error);
+ return Promise.reject(error);
+ }
+);
+
+// 响应拦截器
+service.interceptors.response.use(
+ (res: AxiosResponse) => {
+ // 加密后的 AES 秘钥
+ const keyStr = res.headers[encryptHeader];
+ // 加密
+ if (keyStr != null && keyStr != '') {
+ const data = res.data;
+ // 请求体 AES 解密
+ const base64Str = decrypt(keyStr);
+ // base64 解码 得到请求头的 AES 秘钥
+ const aesKey = decryptBase64(base64Str.toString());
+ // aesKey 解码 data
+ const decryptData = decryptWithAes(data, aesKey);
+ // 将结果 (得到的是 JSON 字符串) 转为 JSON
+ res.data = JSON.parse(decryptData);
+ }
+ // 未设置状态码则默认成功状态
+ const code = res.data.code || HttpStatus.SUCCESS;
+ // 获取错误信息
+ const msg = errorCode[code] || res.data.msg || errorCode['default'];
+ // 二进制数据则直接返回
+ if (res.request.responseType === 'blob' || res.request.responseType === 'arraybuffer') {
+ return res.data;
+ }
+ if (code === 401) {
+ // prettier-ignore
+ if (!isRelogin.show) {
+ isRelogin.show = true;
+ ElMessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', {
+ confirmButtonText: '重新登录',
+ cancelButtonText: '取消',
+ type: 'warning'
+ }).then(() => {
+ isRelogin.show = false;
+ useUserStore().logout().then(() => {
+ location.href = import.meta.env.VITE_APP_CONTEXT_PATH + 'index';
+ });
+ }).catch(() => {
+ isRelogin.show = false;
+ });
+ }
+ return Promise.reject('无效的会话,或者会话已过期,请重新登录。');
+ } else if (code === HttpStatus.SERVER_ERROR) {
+ console.log(msg);
+ ElMessage({ message: msg, type: 'error' });
+ return Promise.reject(new Error(msg));
+ } else if (code === HttpStatus.WARN) {
+ ElMessage({ message: msg, type: 'warning' });
+ return Promise.reject(new Error(msg));
+ } else if (code !== HttpStatus.SUCCESS) {
+ ElNotification.error({ title: msg });
+ return Promise.reject('error');
+ } else {
+ return Promise.resolve(res.data);
+ }
+ },
+ (error: any) => {
+ let { message } = error;
+ if (message == 'Network Error') {
+ message = '后端接口连接异常';
+ } else if (message.includes('timeout')) {
+ message = '系统接口请求超时';
+ } else if (message.includes('Request failed with status code')) {
+ message = '系统接口' + message.substr(message.length - 3) + '异常';
+ }
+ ElMessage({ message: message, type: 'error', duration: 5 * 1000 });
+ return Promise.reject(error);
+ }
+);
+// 通用下载方法
+export function download(url: string, params: any, fileName: string) {
+ downloadLoadingInstance = ElLoading.service({ text: '正在下载数据,请稍候', background: 'rgba(0, 0, 0, 0.7)' });
+ // prettier-ignore
+ return service.post(url, params, {
+ transformRequest: [
+ (params: any) => {
+ return tansParams(params);
+ }
+ ],
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
+ responseType: 'blob'
+ }).then(async (resp: any) => {
+ const isLogin = blobValidate(resp);
+ if (isLogin) {
+ const blob = new Blob([resp]);
+ FileSaver.saveAs(blob, fileName);
+ } else {
+ const resText = await resp.data.text();
+ const rspObj = JSON.parse(resText);
+ const errMsg = errorCode[rspObj.code] || rspObj.msg || errorCode['default'];
+ ElMessage.error(errMsg);
+ }
+ downloadLoadingInstance.close();
+ }).catch((r: any) => {
+ console.error(r);
+ ElMessage.error('下载文件出现错误,请联系管理员!');
+ downloadLoadingInstance.close();
+ });
+}
+// 导出 axios 实例
+export default service;
diff --git a/src/utils/ruoyi.ts b/src/utils/ruoyi.ts
new file mode 100644
index 0000000..a116b52
--- /dev/null
+++ b/src/utils/ruoyi.ts
@@ -0,0 +1,247 @@
+// 日期格式化
+export function parseTime(time: any, pattern?: string) {
+ if (arguments.length === 0 || !time) {
+ return null;
+ }
+ const format = pattern || '{y}-{m}-{d} {h}:{i}:{s}';
+ let date;
+ if (typeof time === 'object') {
+ date = time;
+ } else {
+ if (typeof time === 'string' && /^[0-9]+$/.test(time)) {
+ time = parseInt(time);
+ } else if (typeof time === 'string') {
+ time = time
+ .replace(new RegExp(/-/gm), '/')
+ .replace('T', ' ')
+ .replace(new RegExp(/\.[\d]{3}/gm), '');
+ }
+ if (typeof time === 'number' && time.toString().length === 10) {
+ time = time * 1000;
+ }
+ date = new Date(time);
+ }
+ const formatObj: { [key: string]: any } = {
+ y: date.getFullYear(),
+ m: date.getMonth() + 1,
+ d: date.getDate(),
+ h: date.getHours(),
+ i: date.getMinutes(),
+ s: date.getSeconds(),
+ a: date.getDay()
+ };
+ return format.replace(/{(y|m|d|h|i|s|a)+}/g, (result: string, key: string) => {
+ let value = formatObj[key];
+ // Note: getDay() returns 0 on Sunday
+ if (key === 'a') {
+ return ['日', '一', '二', '三', '四', '五', '六'][value];
+ }
+ if (result.length > 0 && value < 10) {
+ value = '0' + value;
+ }
+ return value || 0;
+ });
+}
+
+/**
+ * 添加日期范围
+ * @param params
+ * @param dateRange
+ * @param propName
+ */
+export const addDateRange = (params: any, dateRange: any[], propName?: string) => {
+ const search = params;
+ search.params = typeof search.params === 'object' && search.params !== null && !Array.isArray(search.params) ? search.params : {};
+ dateRange = Array.isArray(dateRange) ? dateRange : [];
+ if (typeof propName === 'undefined') {
+ search.params['beginTime'] = dateRange[0];
+ search.params['endTime'] = dateRange[1];
+ } else {
+ search.params['begin' + propName] = dateRange[0];
+ search.params['end' + propName] = dateRange[1];
+ }
+ return search;
+};
+
+// 回显数据字典
+export const selectDictLabel = (datas: any, value: number | string) => {
+ if (value === undefined) {
+ return '';
+ }
+ const actions = [];
+ Object.keys(datas).some((key) => {
+ if (datas[key].value == '' + value) {
+ actions.push(datas[key].label);
+ return true;
+ }
+ });
+ if (actions.length === 0) {
+ actions.push(value);
+ }
+ return actions.join('');
+};
+
+// 回显数据字典(字符串数组)
+export const selectDictLabels = (datas: any, value: any, separator: any) => {
+ if (value === undefined || value.length === 0) {
+ return '';
+ }
+ if (Array.isArray(value)) {
+ value = value.join(',');
+ }
+ const actions: any[] = [];
+ const currentSeparator = undefined === separator ? ',' : separator;
+ const temp = value.split(currentSeparator);
+ Object.keys(value.split(currentSeparator)).some((val) => {
+ let match = false;
+ Object.keys(datas).some((key) => {
+ if (datas[key].value == '' + temp[val]) {
+ actions.push(datas[key].label + currentSeparator);
+ match = true;
+ }
+ });
+ if (!match) {
+ actions.push(temp[val] + currentSeparator);
+ }
+ });
+ return actions.join('').substring(0, actions.join('').length - 1);
+};
+
+// 字符串格式化(%s )
+export function sprintf(str: string) {
+ if (arguments.length !== 0) {
+ let flag = true,
+ i = 1;
+ str = str.replace(/%s/g, function () {
+ const arg = arguments[i++];
+ if (typeof arg === 'undefined') {
+ flag = false;
+ return '';
+ }
+ return arg;
+ });
+ return flag ? str : '';
+ }
+}
+
+// 转换字符串,undefined,null等转化为""
+export const parseStrEmpty = (str: any) => {
+ if (!str || str == 'undefined' || str == 'null') {
+ return '';
+ }
+ return str;
+};
+
+// 数据合并
+export const mergeRecursive = (source: any, target: any) => {
+ for (const p in target) {
+ try {
+ if (target[p].constructor == Object) {
+ source[p] = mergeRecursive(source[p], target[p]);
+ } else {
+ source[p] = target[p];
+ }
+ } catch (e) {
+ source[p] = target[p];
+ }
+ }
+ return source;
+};
+
+/**
+ * 构造树型结构数据
+ * @param {*} data 数据源
+ * @param {*} id id字段 默认 'id'
+ * @param {*} parentId 父节点字段 默认 'parentId'
+ * @param {*} children 孩子节点字段 默认 'children'
+ */
+export const handleTree = <T>(data: any[], id?: string, parentId?: string, children?: string): T[] => {
+ const config: {
+ id: string;
+ parentId: string;
+ childrenList: string;
+ } = {
+ id: id || 'id',
+ parentId: parentId || 'parentId',
+ childrenList: children || 'children'
+ };
+
+ const childrenListMap: any = {};
+ const nodeIds: any = {};
+ const tree: T[] = [];
+
+ for (const d of data) {
+ const parentId = d[config.parentId];
+ if (childrenListMap[parentId] == null) {
+ childrenListMap[parentId] = [];
+ }
+ nodeIds[d[config.id]] = d;
+ childrenListMap[parentId].push(d);
+ }
+
+ for (const d of data) {
+ const parentId = d[config.parentId];
+ if (nodeIds[parentId] == null) {
+ tree.push(d);
+ }
+ }
+ const adaptToChildrenList = (o: any) => {
+ if (childrenListMap[o[config.id]] !== null) {
+ o[config.childrenList] = childrenListMap[o[config.id]];
+ }
+ if (o[config.childrenList]) {
+ for (const c of o[config.childrenList]) {
+ adaptToChildrenList(c);
+ }
+ }
+ };
+
+ for (const t of tree) {
+ adaptToChildrenList(t);
+ }
+
+ return tree;
+};
+
+/**
+ * 参数处理
+ * @param {*} params 参数
+ */
+export const tansParams = (params: any) => {
+ let result = '';
+ for (const propName of Object.keys(params)) {
+ const value = params[propName];
+ const part = encodeURIComponent(propName) + '=';
+ if (value !== null && value !== '' && typeof value !== 'undefined') {
+ if (typeof value === 'object') {
+ for (const key of Object.keys(value)) {
+ if (value[key] !== null && value[key] !== '' && typeof value[key] !== 'undefined') {
+ const params = propName + '[' + key + ']';
+ const subPart = encodeURIComponent(params) + '=';
+ result += subPart + encodeURIComponent(value[key]) + '&';
+ }
+ }
+ } else {
+ result += part + encodeURIComponent(value) + '&';
+ }
+ }
+ }
+ return result;
+};
+
+// 返回项目路径
+export const getNormalPath = (p: string): string => {
+ if (p.length === 0 || !p || p === 'undefined') {
+ return p;
+ }
+ const res = p.replace('//', '/');
+ if (res[res.length - 1] === '/') {
+ return res.slice(0, res.length - 1);
+ }
+ return res;
+};
+
+// 验证是否为blob格式
+export const blobValidate = (data: any) => {
+ return data.type !== 'application/json';
+};
diff --git a/src/utils/scroll-to.ts b/src/utils/scroll-to.ts
new file mode 100644
index 0000000..c2fa379
--- /dev/null
+++ b/src/utils/scroll-to.ts
@@ -0,0 +1,65 @@
+const easeInOutQuad = (t: number, b: number, c: number, d: number) => {
+ t /= d / 2;
+ if (t < 1) {
+ return (c / 2) * t * t + b;
+ }
+ t--;
+ return (-c / 2) * (t * (t - 2) - 1) + b;
+};
+
+// requestAnimationFrame for Smart Animating http://goo.gl/sx5sts
+const requestAnimFrame = (function () {
+ return (
+ window.requestAnimationFrame ||
+ (window as any).webkitRequestAnimationFrame ||
+ (window as any).mozRequestAnimationFrame ||
+ function (callback) {
+ window.setTimeout(callback, 1000 / 60);
+ }
+ );
+})();
+
+/**
+ * Because it's so fucking difficult to detect the scrolling element, just move them all
+ * @param {number} amount
+ */
+const move = (amount: number) => {
+ document.documentElement.scrollTop = amount;
+ (document.body.parentNode as HTMLElement).scrollTop = amount;
+ document.body.scrollTop = amount;
+};
+
+const position = () => {
+ return document.documentElement.scrollTop || (document.body.parentNode as HTMLElement).scrollTop || document.body.scrollTop;
+};
+
+/**
+ * @param {number} to
+ * @param {number} duration
+ * @param {Function} callback
+ */
+export const scrollTo = (to: number, duration: number, callback?: any) => {
+ const start = position();
+ const change = to - start;
+ const increment = 20;
+ let currentTime = 0;
+ duration = typeof duration === 'undefined' ? 500 : duration;
+ const animateScroll = function () {
+ // increment the time
+ currentTime += increment;
+ // find the value with the quadratic in-out easing function
+ const val = easeInOutQuad(currentTime, start, change, duration);
+ // move the document.body
+ move(val);
+ // do the animation unless its over
+ if (currentTime < duration) {
+ requestAnimFrame(animateScroll);
+ } else {
+ if (callback && typeof callback === 'function') {
+ // the animation is done so lets callback
+ callback();
+ }
+ }
+ };
+ animateScroll();
+};
diff --git a/src/utils/theme.ts b/src/utils/theme.ts
new file mode 100644
index 0000000..3936248
--- /dev/null
+++ b/src/utils/theme.ts
@@ -0,0 +1,52 @@
+// 处理主题样式
+export const handleThemeStyle = (theme: string) => {
+ document.documentElement.style.setProperty('--el-color-primary', theme);
+ for (let i = 1; i <= 9; i++) {
+ document.documentElement.style.setProperty(`--el-color-primary-light-${i}`, `${getLightColor(theme, i / 10)}`);
+ }
+ for (let i = 1; i <= 9; i++) {
+ document.documentElement.style.setProperty(`--el-color-primary-dark-${i}`, `${getDarkColor(theme, i / 10)}`);
+ }
+};
+
+// hex颜色转rgb颜色
+export const hexToRgb = (str: string): string[] => {
+ str = str.replace('#', '');
+ const hexs = str.match(/../g);
+ for (let i = 0; i < 3; i++) {
+ if (hexs) {
+ hexs[i] = String(parseInt(hexs[i], 16));
+ }
+ }
+ return hexs ? hexs : [];
+};
+
+// rgb颜色转Hex颜色
+export const rgbToHex = (r: string, g: string, b: string) => {
+ const hexs = [Number(r).toString(16), Number(g).toString(16), Number(b).toString(16)];
+ for (let i = 0; i < 3; i++) {
+ if (hexs[i].length == 1) {
+ hexs[i] = `0${hexs[i]}`;
+ }
+ }
+ return `#${hexs.join('')}`;
+};
+
+// 变浅颜色值
+export const getLightColor = (color: string, level: number) => {
+ const rgb = hexToRgb(color);
+ for (let i = 0; i < 3; i++) {
+ const s = (255 - Number(rgb[i])) * level + Number(rgb[i]);
+ rgb[i] = String(Math.floor(s));
+ }
+ return rgbToHex(rgb[0], rgb[1], rgb[2]);
+};
+
+// 变深颜色值
+export const getDarkColor = (color: string, level: number) => {
+ const rgb = hexToRgb(color);
+ for (let i = 0; i < 3; i++) {
+ rgb[i] = String(Math.floor(Number(rgb[i]) * (1 - level)));
+ }
+ return rgbToHex(rgb[0], rgb[1], rgb[2]);
+};
diff --git a/src/utils/validate.ts b/src/utils/validate.ts
new file mode 100644
index 0000000..4d57894
--- /dev/null
+++ b/src/utils/validate.ts
@@ -0,0 +1,92 @@
+/**
+ * 判断url是否是http或https
+ * @returns {Boolean}
+ * @param url
+ */
+export const isHttp = (url: string): boolean => {
+ return url.indexOf('http://') !== -1 || url.indexOf('https://') !== -1;
+};
+
+/**
+ * 判断path是否为外链
+ * @param {string} path
+ * @returns {Boolean}
+ */
+export const isExternal = (path: string) => {
+ return /^(https?:|mailto:|tel:)/.test(path);
+};
+
+/**
+ * @param {string} str
+ * @returns {Boolean}
+ */
+export const validUsername = (str: string) => {
+ const valid_map = ['admin', 'editor'];
+ return valid_map.indexOf(str.trim()) >= 0;
+};
+
+/**
+ * @param {string} url
+ * @returns {Boolean}
+ */
+export const validURL = (url: string) => {
+ const reg =
+ /^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/;
+ return reg.test(url);
+};
+
+/**
+ * @param {string} str
+ * @returns {Boolean}
+ */
+export const validLowerCase = (str: string) => {
+ const reg = /^[a-z]+$/;
+ return reg.test(str);
+};
+
+/**
+ * @param {string} str
+ * @returns {Boolean}
+ */
+export const validUpperCase = (str: string) => {
+ const reg = /^[A-Z]+$/;
+ return reg.test(str);
+};
+
+/**
+ * @param {string} str
+ * @returns {Boolean}
+ */
+export const validAlphabets = (str: string) => {
+ const reg = /^[A-Za-z]+$/;
+ return reg.test(str);
+};
+
+/**
+ * @param {string} email
+ * @returns {Boolean}
+ */
+export const validEmail = (email: string) => {
+ const reg =
+ /^(([^<>()\]\\.,;:\s@"]+(\.[^<>()\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
+ return reg.test(email);
+};
+
+/**
+ * @param {string} str
+ * @returns {Boolean}
+ */
+export const isString = (str: any) => {
+ return typeof str === 'string' || str instanceof String;
+};
+
+/**
+ * @param {Array} arg
+ * @returns {Boolean}
+ */
+export const isArray = (arg: string | string[]) => {
+ if (typeof Array.isArray === 'undefined') {
+ return Object.prototype.toString.call(arg) === '[object Array]';
+ }
+ return Array.isArray(arg);
+};
diff --git a/src/utils/websocket.ts b/src/utils/websocket.ts
new file mode 100644
index 0000000..a4529f9
--- /dev/null
+++ b/src/utils/websocket.ts
@@ -0,0 +1,141 @@
+/**
+ * @module initWebSocket 初始化
+ * @module websocketonopen 连接成功
+ * @module websocketonerror 连接失败
+ * @module websocketclose 断开连接
+ * @module resetHeart 重置心跳
+ * @module sendSocketHeart 心跳发送
+ * @module reconnect 重连
+ * @module sendMsg 发送数据
+ * @module websocketonmessage 接收数据
+ * @module test 测试收到消息传递
+ * @description socket 通信
+ * @param {any} url socket地址
+ * @param {any} websocket websocket 实例
+ * @param {any} heartTime 心跳定时器实例
+ * @param {number} socketHeart 心跳次数
+ * @param {number} HeartTimeOut 心跳超时时间
+ * @param {number} socketError 错误次数
+ */
+
+import { getToken } from '@/utils/auth';
+import useNoticeStore from '@/store/modules/notice';
+import { ElNotification } from "element-plus";
+
+const { addNotice } = useNoticeStore();
+
+let socketUrl: any = ''; // socket地址
+let websocket: any = null; // websocket 实例
+let heartTime: any = null; // 心跳定时器实例
+let socketHeart = 0 as number; // 心跳次数
+const HeartTimeOut = 10000; // 心跳超时时间 10000 = 10s
+let socketError = 0 as number; // 错误次数
+
+// 初始化socket
+export const initWebSocket = (url: any) => {
+ if (import.meta.env.VITE_APP_WEBSOCKET === 'false') {
+ return;
+ }
+ socketUrl = url;
+ // 初始化 websocket
+ websocket = new WebSocket(url + '?Authorization=Bearer ' + getToken() + '&clientid=' + import.meta.env.VITE_APP_CLIENT_ID);
+ websocketonopen();
+ websocketonmessage();
+ websocketonerror();
+ websocketclose();
+ sendSocketHeart();
+ return websocket;
+};
+
+// socket 连接成功
+export const websocketonopen = () => {
+ websocket.onopen = function () {
+ console.log('连接 websocket 成功');
+ resetHeart();
+ };
+};
+
+// socket 连接失败
+export const websocketonerror = () => {
+ websocket.onerror = function (e: any) {
+ console.log('连接 websocket 失败', e);
+ };
+};
+
+// socket 断开链接
+export const websocketclose = () => {
+ websocket.onclose = function (e: any) {
+ console.log('断开连接', e);
+ };
+};
+
+// socket 重置心跳
+export const resetHeart = () => {
+ socketHeart = 0;
+ socketError = 0;
+ clearInterval(heartTime);
+ sendSocketHeart();
+};
+
+// socket心跳发送
+export const sendSocketHeart = () => {
+ heartTime = setInterval(() => {
+ // 如果连接正常则发送心跳
+ if (websocket.readyState == 1) {
+ // if (socketHeart <= 30) {
+ websocket.send(
+ JSON.stringify({
+ type: 'ping'
+ })
+ );
+ socketHeart = socketHeart + 1;
+ } else {
+ // 重连
+ reconnect();
+ }
+ }, HeartTimeOut);
+};
+
+// socket重连
+export const reconnect = () => {
+ if (socketError <= 2) {
+ clearInterval(heartTime);
+ initWebSocket(socketUrl);
+ socketError = socketError + 1;
+ // eslint-disable-next-line prettier/prettier
+ console.log('socket重连', socketError);
+ } else {
+ // eslint-disable-next-line prettier/prettier
+ console.log('重试次数已用完');
+ clearInterval(heartTime);
+ }
+};
+
+// socket 发送数据
+export const sendMsg = (data: any) => {
+ websocket.send(data);
+};
+
+// socket 接收数据
+export const websocketonmessage = () => {
+ websocket.onmessage = function (e: any) {
+ if (e.data.indexOf('heartbeat') > 0) {
+ resetHeart();
+ }
+ if (e.data.indexOf('ping') > 0) {
+ return;
+ }
+ addNotice({
+ message: e.data,
+ read: false,
+ time: new Date().toLocaleString()
+ });
+ ElNotification({
+ title: '消息',
+ message: e.data,
+ type: 'success',
+ duration: 3000
+ })
+ return e.data;
+ };
+};
diff --git a/src/views/basis/sysParam/config/index.vue b/src/views/basis/sysParam/config/index.vue
new file mode 100644
index 0000000..7e2e161
--- /dev/null
+++ b/src/views/basis/sysParam/config/index.vue
@@ -0,0 +1,257 @@
+<template>
+ <div class="p-2">
+ <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
+ <div class="mb-[10px]" v-show="showSearch">
+ <el-card shadow="hover">
+ <el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px">
+ <el-form-item label="参数名称" prop="configName">
+ <el-input v-model="queryParams.configName" placeholder="请输入参数名称" clearable style="width: 240px" @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item label="参数键名" prop="configKey">
+ <el-input v-model="queryParams.configKey" placeholder="请输入参数键名" clearable style="width: 240px" @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item label="系统内置" prop="configType">
+ <el-select v-model="queryParams.configType" placeholder="系统内置" clearable>
+ <el-option v-for="dict in sys_yes_no" :key="dict.value" :label="dict.label" :value="dict.value" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="创建时间" style="width: 308px;">
+ <el-date-picker
+ v-model="dateRange"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ range-separator="-"
+ start-placeholder="开始日期"
+ end-placeholder="结束日期"
+ :default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
+ ></el-date-picker>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+ <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+ </el-form-item>
+ </el-form>
+ </el-card>
+ </div>
+ </transition>
+ <el-card shadow="hover">
+ <template #header>
+ <el-row :gutter="10" class="mb8">
+ <el-col :span="1.5">
+ <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['system:config:add']">新增</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['system:config:edit']">
+ 修改
+ </el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['system:config:remove']">
+ 删除
+ </el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['system:config:export']">导出</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button type="danger" plain icon="Refresh" @click="handleRefreshCache" v-hasPermi="['system:config:remove']">刷新缓存</el-button>
+ </el-col>
+ <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
+ </el-row>
+ </template>
+
+ <el-table v-loading="loading" :data="configList" @selection-change="handleSelectionChange">
+ <el-table-column type="selection" width="55" align="center" />
+ <el-table-column label="参数主键" align="center" prop="configId" v-if="false" />
+ <el-table-column label="参数名称" align="center" prop="configName" :show-overflow-tooltip="true" />
+ <el-table-column label="参数键名" align="center" prop="configKey" :show-overflow-tooltip="true" />
+ <el-table-column label="参数键值" align="center" prop="configValue" :show-overflow-tooltip="true" />
+ <el-table-column label="系统内置" align="center" prop="configType">
+ <template #default="scope">
+ <dict-tag :options="sys_yes_no" :value="scope.row.configType" />
+ </template>
+ </el-table-column>
+ <el-table-column label="备注" align="center" prop="remark" :show-overflow-tooltip="true" />
+ <el-table-column label="创建时间" align="center" prop="createTime" width="180">
+ <template #default="scope">
+ <span>{{ parseTime(scope.row.createTime) }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="操作" align="center" width="150" class-name="small-padding fixed-width">
+ <template #default="scope">
+ <el-tooltip content="修改" placement="top">
+ <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:config:edit']"></el-button>
+ </el-tooltip>
+ <el-tooltip content="删除" placement="top">
+ <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:config:remove']"></el-button>
+ </el-tooltip>
+ </template>
+ </el-table-column>
+ </el-table>
+ <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
+ </el-card>
+
+ <!-- 添加或修改参数配置对话框 -->
+ <el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
+ <el-form ref="configFormRef" :model="form" :rules="rules" label-width="80px">
+ <el-form-item label="参数名称" prop="configName">
+ <el-input v-model="form.configName" placeholder="请输入参数名称" />
+ </el-form-item>
+ <el-form-item label="参数键名" prop="configKey">
+ <el-input v-model="form.configKey" placeholder="请输入参数键名" />
+ </el-form-item>
+ <el-form-item label="参数键值" prop="configValue">
+ <el-input v-model="form.configValue" placeholder="请输入参数键值" />
+ </el-form-item>
+ <el-form-item label="系统内置" prop="configType">
+ <el-radio-group v-model="form.configType">
+ <el-radio v-for="dict in sys_yes_no" :key="dict.value" :label="dict.value">{{ dict.label }}</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="备注" prop="remark">
+ <el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitForm">确 定</el-button>
+ <el-button @click="cancel">取 消</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup name="Config" lang="ts">
+import { listConfig, getConfig, delConfig, addConfig, updateConfig, refreshCache } from "@/api/system/config";
+import { ConfigForm, ConfigQuery, ConfigVO } from "@/api/system/config/types";
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const { sys_yes_no } = toRefs<any>(proxy?.useDict("sys_yes_no"));
+
+const configList = ref<ConfigVO[]>([]);
+const loading = ref(true);
+const showSearch = ref(true);
+const ids = ref<Array<number | string>>([]);
+const single = ref(true);
+const multiple = ref(true);
+const total = ref(0);
+const dateRange = ref<[DateModelType, DateModelType]>(['', '']);
+
+const queryFormRef = ref<ElFormInstance>();
+const configFormRef = ref<ElFormInstance>();
+const dialog = reactive<DialogOption>({
+ visible: false,
+ title: ''
+});
+const initFormData: ConfigForm = {
+ configId: undefined,
+ configName: '',
+ configKey: '',
+ configValue: '',
+ configType: "Y",
+ remark: ''
+}
+const data = reactive<PageData<ConfigForm, ConfigQuery>>({
+ form: { ...initFormData },
+ queryParams: {
+ pageNum: 1,
+ pageSize: 10,
+ configName: '',
+ configKey: '',
+ configType: '',
+ },
+ rules: {
+ configName: [{ required: true, message: "参数名称不能为空", trigger: "blur" }],
+ configKey: [{ required: true, message: "参数键名不能为空", trigger: "blur" }],
+ configValue: [{ required: true, message: "参数键值不能为空", trigger: "blur" }]
+ }
+});
+
+const { queryParams, form, rules } = toRefs(data);
+
+/** 查询参数列表 */
+const getList = async () => {
+ loading.value = true;
+ const res = await listConfig(proxy?.addDateRange(queryParams.value, dateRange.value));
+ configList.value = res.rows;
+ total.value = res.total;
+ loading.value = false;
+}
+/** 取消按钮 */
+const cancel = () => {
+ reset();
+ dialog.visible = false;
+}
+/** 表单重置 */
+const reset = () => {
+ form.value = { ...initFormData };
+ configFormRef.value?.resetFields();
+}
+/** 搜索按钮操作 */
+const handleQuery = () => {
+ queryParams.value.pageNum = 1;
+ getList();
+}
+/** 重置按钮操作 */
+const resetQuery = () => {
+ dateRange.value = ['', ''];
+ queryFormRef.value?.resetFields();
+ handleQuery();
+}
+/** 多选框选中数据 */
+const handleSelectionChange = (selection: ConfigVO[]) => {
+ ids.value = selection.map(item => item.configId);
+ single.value = selection.length != 1;
+ multiple.value = !selection.length;
+}
+/** 新增按钮操作 */
+const handleAdd = () => {
+ reset();
+ dialog.visible = true;
+ dialog.title = "添加参数";
+}
+/** 修改按钮操作 */
+const handleUpdate = async (row?: ConfigVO) => {
+ reset();
+ const configId = row?.configId || ids.value[0];
+ const res = await getConfig(configId);
+ Object.assign(form.value, res.data);
+ dialog.visible = true;
+ dialog.title = "修改参数";
+}
+/** 提交按钮 */
+const submitForm = () => {
+ configFormRef.value?.validate(async (valid: boolean) => {
+ if (valid) {
+ form.value.configId ? await updateConfig(form.value) : await addConfig(form.value);
+ proxy?.$modal.msgSuccess("操作成功");
+ dialog.visible = false;
+ await getList();
+ }
+ });
+}
+/** 删除按钮操作 */
+const handleDelete = async (row?: ConfigVO) => {
+ const configIds = row?.configId || ids.value;
+ await proxy?.$modal.confirm('是否确认删除参数编号为"' + configIds + '"的数据项?');
+ await delConfig(configIds);
+ await getList();
+ proxy?.$modal.msgSuccess("删除成功");
+}
+/** 导出按钮操作 */
+const handleExport = () => {
+ proxy?.download("system/config/export", {
+ ...queryParams.value
+ }, `config_${new Date().getTime()}.xlsx`);
+}
+/** 刷新缓存按钮操作 */
+const handleRefreshCache = async () => {
+ await refreshCache();
+ proxy?.$modal.msgSuccess("刷新缓存成功");
+}
+
+onMounted(() => {
+ getList();
+})
+</script>
diff --git a/src/views/demo/demo/index.vue b/src/views/demo/demo/index.vue
new file mode 100644
index 0000000..a7f3bd7
--- /dev/null
+++ b/src/views/demo/demo/index.vue
@@ -0,0 +1,262 @@
+<template>
+ <div class="p-2">
+ <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
+ <div class="mb-[10px]" v-show="showSearch">
+ <el-card shadow="hover">
+ <el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px">
+ <el-form-item label="部门id" prop="deptId">
+ <el-input v-model="queryParams.deptId" placeholder="请输入部门id" clearable style="width: 240px" @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item label="用户id" prop="userId">
+ <el-input v-model="queryParams.userId" placeholder="请输入用户id" clearable style="width: 240px" @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item label="排序号" prop="orderNum">
+ <el-input v-model="queryParams.orderNum" placeholder="请输入排序号" clearable style="width: 240px" @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item label="key键" prop="testKey">
+ <el-input v-model="queryParams.testKey" placeholder="请输入key键" clearable style="width: 240px" @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item label="值" prop="value">
+ <el-input v-model="queryParams.value" placeholder="请输入值" clearable style="width: 240px" @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+ <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+ </el-form-item>
+ </el-form>
+ </el-card>
+ </div>
+ </transition>
+
+ <el-card shadow="hover">
+ <template #header>
+ <el-row :gutter="10" class="mb8">
+ <el-col :span="1.5">
+ <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['demo:demo:add']">新增</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['demo:demo:edit']">修改</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['demo:demo:remove']"
+ >删除</el-button
+ >
+ </el-col>
+ <el-col :span="1.5">
+ <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['demo:demo:export']">导出</el-button>
+ </el-col>
+ <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
+ </el-row>
+ </template>
+
+ <el-table v-loading="loading" :data="demoList" @selection-change="handleSelectionChange">
+ <el-table-column type="selection" width="55" align="center" />
+ <el-table-column label="主键" align="center" prop="id" v-if="true" />
+ <el-table-column label="部门id" align="center" prop="deptId" />
+ <el-table-column label="用户id" align="center" prop="userId" />
+ <el-table-column label="排序号" align="center" prop="orderNum" />
+ <el-table-column label="key键" align="center" prop="testKey" />
+ <el-table-column label="值" align="center" prop="value" />
+ <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+ <template #default="scope">
+ <el-tooltip content="修改" placement="top">
+ <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['demo:demo:edit']"></el-button>
+ </el-tooltip>
+ <el-tooltip content="删除" placement="top">
+ <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['demo:demo:remove']"></el-button>
+ </el-tooltip>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
+ </el-card>
+ <!-- 添加或修改测试单对话框 -->
+ <el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
+ <el-form ref="demoFormRef" :model="form" :rules="rules" label-width="80px">
+ <el-form-item label="部门id" prop="deptId">
+ <el-input v-model="form.deptId" placeholder="请输入部门id" />
+ </el-form-item>
+ <el-form-item label="用户id" prop="userId">
+ <el-input v-model="form.userId" placeholder="请输入用户id" />
+ </el-form-item>
+ <el-form-item label="排序号" prop="orderNum">
+ <el-input v-model="form.orderNum" placeholder="请输入排序号" />
+ </el-form-item>
+ <el-form-item label="key键" prop="testKey">
+ <el-input v-model="form.testKey" placeholder="请输入key键" />
+ </el-form-item>
+ <el-form-item label="值" prop="value">
+ <el-input v-model="form.value" placeholder="请输入值" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
+ <el-button @click="cancel">取 消</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup name="Demo" lang="ts">
+import { listDemo, getDemo, delDemo, addDemo, updateDemo } from '@/api/demo/demo';
+import { DemoVO, DemoQuery, DemoForm } from '@/api/demo/demo/types';
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+
+const demoList = ref<DemoVO[]>([]);
+const buttonLoading = ref(false);
+const loading = ref(true);
+const showSearch = ref(true);
+const ids = ref<Array<string | number>>([]);
+const single = ref(true);
+const multiple = ref(true);
+const total = ref(0);
+
+const queryFormRef = ref<ElFormInstance>();
+const demoFormRef = ref<ElFormInstance>();
+
+const dialog = reactive<DialogOption>({
+ visible: false,
+ title: ''
+});
+
+const initFormData: DemoForm = {
+ id: undefined,
+ deptId: undefined,
+ userId: undefined,
+ orderNum: undefined,
+ testKey: undefined,
+ value: undefined,
+}
+const data = reactive<PageData<DemoForm, DemoQuery>>({
+ form: { ...initFormData },
+ queryParams: {
+ pageNum: 1,
+ pageSize: 10,
+ deptId: undefined,
+ userId: undefined,
+ orderNum: undefined,
+ testKey: undefined,
+ value: undefined,
+ },
+ rules: {
+ id: [
+ { required: true, message: "主键不能为空", trigger: "blur" }
+ ],
+ deptId: [
+ { required: true, message: "部门id不能为空", trigger: "blur" }
+ ],
+ userId: [
+ { required: true, message: "用户id不能为空", trigger: "blur" }
+ ],
+ orderNum: [
+ { required: true, message: "排序号不能为空", trigger: "blur" }
+ ],
+ testKey: [
+ { required: true, message: "key键不能为空", trigger: "blur" }
+ ],
+ value: [
+ { required: true, message: "值不能为空", trigger: "blur" }
+ ],
+ }
+});
+
+const { queryParams, form, rules } = toRefs(data);
+
+/** 查询测试单列表 */
+const getList = async () => {
+ loading.value = true;
+ const res = await listDemo(queryParams.value);
+ demoList.value = res.rows;
+ total.value = res.total;
+ loading.value = false;
+}
+
+/** 取消按钮 */
+const cancel = () => {
+ reset();
+ dialog.visible = false;
+}
+
+/** 表单重置 */
+const reset = () => {
+ form.value = { ...initFormData };
+ demoFormRef.value?.resetFields();
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+ queryParams.value.pageNum = 1;
+ getList();
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+ queryFormRef.value?.resetFields();
+ handleQuery();
+}
+
+/** 多选框选中数据 */
+const handleSelectionChange = (selection: DemoVO[]) => {
+ ids.value = selection.map(item => item.id);
+ single.value = selection.length != 1;
+ multiple.value = !selection.length;
+}
+
+/** 新增按钮操作 */
+const handleAdd = () => {
+ reset();
+ dialog.visible = true;
+ dialog.title = "添加测试单";
+}
+
+/** 修改按钮操作 */
+const handleUpdate = async (row?: DemoVO) => {
+ reset();
+ const _id = row?.id || ids.value[0]
+ const res = await getDemo(_id);
+ Object.assign(form.value, res.data);
+ dialog.visible = true;
+ dialog.title = "修改测试单";
+}
+
+/** 提交按钮 */
+const submitForm = () => {
+ demoFormRef.value?.validate(async (valid: boolean) => {
+ if (valid) {
+ buttonLoading.value = true;
+ if (form.value.id) {
+ await updateDemo(form.value).finally(() => buttonLoading.value = false);
+ } else {
+ await addDemo(form.value).finally(() => buttonLoading.value = false);
+ }
+ proxy?.$modal.msgSuccess("修改成功");
+ dialog.visible = false;
+ await getList();
+ }
+ });
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (row?: DemoVO) => {
+ const _ids = row?.id || ids.value;
+ await proxy?.$modal.confirm('是否确认删除测试单编号为"' + _ids + '"的数据项?').finally(() => loading.value = false);
+ await delDemo(_ids);
+ proxy?.$modal.msgSuccess("删除成功");
+ await getList();
+}
+
+/** 导出按钮操作 */
+const handleExport = () => {
+ proxy?.download('demo/demo/export', {
+ ...queryParams.value
+ }, `demo_${new Date().getTime()}.xlsx`)
+}
+
+onMounted(() => {
+ getList();
+});
+</script>
diff --git a/src/views/demo/tree/index.vue b/src/views/demo/tree/index.vue
new file mode 100644
index 0000000..d79f817
--- /dev/null
+++ b/src/views/demo/tree/index.vue
@@ -0,0 +1,271 @@
+<template>
+ <div class="p-2">
+ <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
+ <div class="mb-[10px]" v-show="showSearch">
+ <el-card shadow="hover">
+ <el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px">
+ <el-form-item label="树节点名" prop="treeName">
+ <el-input v-model="queryParams.treeName" placeholder="请输入树节点名" clearable style="width: 240px" @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+ <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+ </el-form-item>
+ </el-form>
+ </el-card>
+ </div>
+ </transition>
+
+ <el-card shadow="hover">
+ <template #header>
+ <el-row :gutter="10" class="mb8">
+ <el-col :span="1.5">
+ <el-button type="primary" plain icon="Plus" @click="handleAdd()" v-hasPermi="['demo:tree:add']">新增</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button type="info" plain icon="Sort" @click="handleToggleExpandAll">展开/折叠</el-button>
+ </el-col>
+ <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
+ </el-row>
+ </template>
+ <el-table
+ v-loading="loading"
+ :data="treeList"
+ row-key="id"
+ :default-expand-all="isExpandAll"
+ :tree-props="{children: 'children', hasChildren: 'hasChildren'}"
+ ref="treeTableRef"
+ >
+ <el-table-column label="父id" align="center" prop="parentId" />
+ <el-table-column label="部门id" align="center" prop="deptId" />
+ <el-table-column label="用户id" align="center" prop="userId" />
+ <el-table-column label="树节点名" align="center" prop="treeName" />
+ <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+ <template #default="scope">
+ <el-tooltip content="修改" placement="top">
+ <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['demo:tree:edit']" />
+ </el-tooltip>
+ <el-tooltip content="新增" placement="top">
+ <el-button link type="primary" icon="Plus" @click="handleAdd(scope.row)" v-hasPermi="['demo:tree:add']" />
+ </el-tooltip>
+ <el-tooltip content="删除" placement="top">
+ <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['demo:tree:remove']" />
+ </el-tooltip>
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-card>
+ <!-- 添加或修改测试树对话框 -->
+ <el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
+ <el-form ref="treeFormRef" :model="form" :rules="rules" label-width="80px">
+ <el-form-item label="父id" prop="parentId">
+ <el-tree-select
+ v-model="form.parentId"
+ :data="treeOptions"
+ :props="{ value: 'id', label: 'treeName', children: 'children' }"
+ value-key="id"
+ placeholder="请选择父id"
+ check-strictly
+ />
+ </el-form-item>
+ <el-form-item label="部门id" prop="deptId">
+ <el-input v-model="form.deptId" placeholder="请输入部门id" />
+ </el-form-item>
+ <el-form-item label="用户id" prop="userId">
+ <el-input v-model="form.userId" placeholder="请输入用户id" />
+ </el-form-item>
+ <el-form-item label="值" prop="treeName">
+ <el-input v-model="form.treeName" placeholder="请输入值" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
+ <el-button @click="cancel">取 消</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup name="Tree" lang="ts">
+import { listTree, getTree, delTree, addTree, updateTree } from "@/api/demo/tree";
+import { TreeVO, TreeQuery, TreeForm } from '@/api/demo/tree/types';
+
+
+type TreeOption = {
+ id: number;
+ treeName: string;
+ children?: TreeOption[];
+}
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;;
+
+
+const treeList = ref<TreeVO[]>([]);
+const treeOptions = ref<TreeOption[]>([]);
+const buttonLoading = ref(false);
+const showSearch = ref(true);
+const isExpandAll = ref(true);
+const loading = ref(false);
+
+const queryFormRef = ref<ElFormInstance>();
+const treeFormRef = ref<ElFormInstance>();
+const treeTableRef = ref<ElTableInstance>()
+
+const dialog = reactive<DialogOption>({
+ visible: false,
+ title: ''
+});
+
+
+const initFormData: TreeForm = {
+ id: undefined,
+ parentId: undefined,
+ deptId: undefined,
+ userId: undefined,
+ treeName: undefined,
+}
+
+const data = reactive<PageData<TreeForm, TreeQuery>>({
+ form: {...initFormData},
+ queryParams: {
+ parentId: undefined,
+ deptId: undefined,
+ userId: undefined,
+ treeName: undefined,
+ },
+ rules: {
+ id: [
+ { required: true, message: "主键不能为空", trigger: "blur" }
+ ],
+ parentId: [
+ { required: true, message: "父id不能为空", trigger: "blur" }
+ ],
+ deptId: [
+ { required: true, message: "部门id不能为空", trigger: "blur" }
+ ],
+ userId: [
+ { required: true, message: "用户id不能为空", trigger: "blur" }
+ ],
+ treeName: [
+ { required: true, message: "值不能为空", trigger: "blur" }
+ ],
+ }
+});
+
+const { queryParams, form, rules } = toRefs(data);
+
+/** 查询测试树列表 */
+const getList = async () => {
+ loading.value = true;
+ const res = await listTree(queryParams.value);
+ const data = proxy?.handleTree<TreeVO>(res.data, "id", "parentId");
+ if (data) {
+ treeList.value = data;
+ loading.value = false;
+ }
+}
+
+/** 查询测试树下拉树结构 */
+const getTreeselect = async () => {
+ const res = await listTree();
+ treeOptions.value = [];
+ const data: TreeOption = { id: 0, treeName: '顶级节点', children: [] };
+ data.children = proxy?.handleTree<TreeOption>(res.data, "id", "parentId");
+ treeOptions.value.push(data);
+}
+
+// 取消按钮
+const cancel = () => {
+ reset();
+ dialog.visible = false;
+}
+
+// 表单重置
+const reset = () => {
+ form.value = {...initFormData}
+ treeFormRef.value?.resetFields();
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+ getList();
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+ queryFormRef.value?.resetFields();
+ handleQuery();
+}
+
+/** 新增按钮操作 */
+const handleAdd = (row?: TreeVO) => {
+ reset();
+ getTreeselect();
+ if (row && row.id) {
+ form.value.parentId = row.id;
+ } else {
+ form.value.parentId = 0;
+ }
+ dialog.visible = true;
+ dialog.title = "添加测试树";
+}
+
+/** 展开/折叠操作 */
+const handleToggleExpandAll = () => {
+ isExpandAll.value = !isExpandAll.value;
+ toggleExpandAll(treeList.value, isExpandAll.value)
+}
+
+/** 展开/折叠操作 */
+const toggleExpandAll = (data: TreeVO[], status: boolean) => {
+ data.forEach((item) => {
+ treeTableRef.value?.toggleRowExpansion(item, status)
+ if (item.children && item.children.length > 0) toggleExpandAll(item.children, status)
+ })
+}
+
+/** 修改按钮操作 */
+const handleUpdate = async (row: TreeVO) => {
+ reset();
+ await getTreeselect();
+ if (row) {
+ form.value.parentId = row.id;
+ }
+ const res = await getTree(row.id);
+ Object.assign(form.value, res.data);
+ dialog.visible = true;
+ dialog.title = "修改测试树";
+}
+
+/** 提交按钮 */
+const submitForm = () => {
+ treeFormRef.value?.validate(async (valid: boolean) => {
+ if (valid) {
+ buttonLoading.value = true;
+ if (form.value.id) {
+ await updateTree(form.value).finally(() => buttonLoading.value = false);
+ } else {
+ await addTree(form.value).finally(() => buttonLoading.value = false);
+ }
+ proxy?.$modal.msgSuccess("操作成功");
+ dialog.visible = false;
+ await getList();
+ }
+ });
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (row: TreeVO) => {
+ await proxy?.$modal.confirm('是否确认删除测试树编号为"' + row.id + '"的数据项?');
+ loading.value = true;
+ await delTree(row.id).finally(() => loading.value = false);
+ await getList();
+ proxy?.$modal.msgSuccess("删除成功");
+}
+
+onMounted(() => {
+ getList();
+});
+</script>
diff --git a/src/views/error/401.vue b/src/views/error/401.vue
new file mode 100644
index 0000000..1d9c76e
--- /dev/null
+++ b/src/views/error/401.vue
@@ -0,0 +1,76 @@
+<template>
+ <div class="errPage-container">
+ <el-button icon="arrow-left" class="pan-back-btn" @click="back"> 返回 </el-button>
+ <el-row>
+ <el-col :span="12">
+ <h1 class="text-jumbo text-ginormous">401错误!</h1>
+ <h2>您没有访问权限!</h2>
+ <h6>对不起,您没有访问权限,请不要进行非法操作!您可以返回主页面</h6>
+ <ul class="list-unstyled">
+ <li class="link-type">
+ <router-link to="/"> 回首页 </router-link>
+ </li>
+ </ul>
+ </el-col>
+ <el-col :span="12">
+ <img :src="errGif" width="313" height="428" alt="Girl has dropped her ice cream." />
+ </el-col>
+ </el-row>
+ </div>
+</template>
+
+<script setup lang="ts">
+import errImage from '@/assets/401_images/401.gif';
+
+let { proxy } = getCurrentInstance() as ComponentInternalInstance;
+
+const errGif = ref(errImage + "?" + +new Date());
+
+function back() {
+ if (proxy?.$route.query.noGoBack) {
+ proxy.$router.push({ path: "/" });
+ } else {
+ proxy?.$router.go(-1);
+ }
+}
+</script>
+
+<style lang="scss" scoped>
+.errPage-container {
+ width: 800px;
+ max-width: 100%;
+ margin: 100px auto;
+ .pan-back-btn {
+ background: #008489;
+ color: #fff;
+ border: none !important;
+ }
+ .pan-gif {
+ margin: 0 auto;
+ display: block;
+ }
+ .pan-img {
+ display: block;
+ margin: 0 auto;
+ width: 100%;
+ }
+ .text-jumbo {
+ font-size: 60px;
+ font-weight: 700;
+ color: #484848;
+ }
+ .list-unstyled {
+ font-size: 14px;
+ li {
+ padding-bottom: 5px;
+ }
+ a {
+ color: #008489;
+ text-decoration: none;
+ &:hover {
+ text-decoration: underline;
+ }
+ }
+ }
+}
+</style>
diff --git a/src/views/error/404.vue b/src/views/error/404.vue
new file mode 100644
index 0000000..94ed936
--- /dev/null
+++ b/src/views/error/404.vue
@@ -0,0 +1,223 @@
+<template>
+ <div class="wscn-http404-container">
+ <div class="wscn-http404">
+ <div class="pic-404">
+ <img class="pic-404__parent" src="@/assets/404_images/404.png" alt="404" />
+ <img class="pic-404__child left" src="@/assets/404_images/404_cloud.png" alt="404" />
+ <img class="pic-404__child mid" src="@/assets/404_images/404_cloud.png" alt="404" />
+ <img class="pic-404__child right" src="@/assets/404_images/404_cloud.png" alt="404" />
+ </div>
+ <div class="bullshit">
+ <div class="bullshit__oops">404错误!</div>
+ <div class="bullshit__headline">
+ {{ message }}
+ </div>
+ <div class="bullshit__info">
+ 对不起,您正在寻找的页面不存在。尝试检查URL的错误,然后按浏览器上的刷新按钮或尝试在我们的应用程序中找到其他内容。
+ </div>
+ <router-link to="/index" class="bullshit__return-home"> 返回首页 </router-link>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script setup lang="ts">
+let message = computed(() => {
+ return '找不到网页!'
+})
+</script>
+
+<style lang="scss" scoped>
+.wscn-http404-container{
+ transform: translate(-50%,-50%);
+ position: absolute;
+ top: 40%;
+ left: 50%;
+}
+.wscn-http404 {
+ position: relative;
+ width: 1200px;
+ padding: 0 50px;
+ overflow: hidden;
+ .pic-404 {
+ position: relative;
+ float: left;
+ width: 600px;
+ overflow: hidden;
+ &__parent {
+ width: 100%;
+ }
+ &__child {
+ position: absolute;
+ &.left {
+ width: 80px;
+ top: 17px;
+ left: 220px;
+ opacity: 0;
+ animation-name: cloudLeft;
+ animation-duration: 2s;
+ animation-timing-function: linear;
+ animation-fill-mode: forwards;
+ animation-delay: 1s;
+ }
+ &.mid {
+ width: 46px;
+ top: 10px;
+ left: 420px;
+ opacity: 0;
+ animation-name: cloudMid;
+ animation-duration: 2s;
+ animation-timing-function: linear;
+ animation-fill-mode: forwards;
+ animation-delay: 1.2s;
+ }
+ &.right {
+ width: 62px;
+ top: 100px;
+ left: 500px;
+ opacity: 0;
+ animation-name: cloudRight;
+ animation-duration: 2s;
+ animation-timing-function: linear;
+ animation-fill-mode: forwards;
+ animation-delay: 1s;
+ }
+ @keyframes cloudLeft {
+ 0% {
+ top: 17px;
+ left: 220px;
+ opacity: 0;
+ }
+ 20% {
+ top: 33px;
+ left: 188px;
+ opacity: 1;
+ }
+ 80% {
+ top: 81px;
+ left: 92px;
+ opacity: 1;
+ }
+ 100% {
+ top: 97px;
+ left: 60px;
+ opacity: 0;
+ }
+ }
+ @keyframes cloudMid {
+ 0% {
+ top: 10px;
+ left: 420px;
+ opacity: 0;
+ }
+ 20% {
+ top: 40px;
+ left: 360px;
+ opacity: 1;
+ }
+ 70% {
+ top: 130px;
+ left: 180px;
+ opacity: 1;
+ }
+ 100% {
+ top: 160px;
+ left: 120px;
+ opacity: 0;
+ }
+ }
+ @keyframes cloudRight {
+ 0% {
+ top: 100px;
+ left: 500px;
+ opacity: 0;
+ }
+ 20% {
+ top: 120px;
+ left: 460px;
+ opacity: 1;
+ }
+ 80% {
+ top: 180px;
+ left: 340px;
+ opacity: 1;
+ }
+ 100% {
+ top: 200px;
+ left: 300px;
+ opacity: 0;
+ }
+ }
+ }
+ }
+ .bullshit {
+ position: relative;
+ float: left;
+ width: 300px;
+ padding: 30px 0;
+ overflow: hidden;
+ &__oops {
+ font-size: 32px;
+ font-weight: bold;
+ line-height: 40px;
+ color: #1482f0;
+ opacity: 0;
+ margin-bottom: 20px;
+ animation-name: slideUp;
+ animation-duration: 0.5s;
+ animation-fill-mode: forwards;
+ }
+ &__headline {
+ font-size: 20px;
+ line-height: 24px;
+ color: #222;
+ font-weight: bold;
+ opacity: 0;
+ margin-bottom: 10px;
+ animation-name: slideUp;
+ animation-duration: 0.5s;
+ animation-delay: 0.1s;
+ animation-fill-mode: forwards;
+ }
+ &__info {
+ font-size: 13px;
+ line-height: 21px;
+ color: grey;
+ opacity: 0;
+ margin-bottom: 30px;
+ animation-name: slideUp;
+ animation-duration: 0.5s;
+ animation-delay: 0.2s;
+ animation-fill-mode: forwards;
+ }
+ &__return-home {
+ display: block;
+ float: left;
+ width: 110px;
+ height: 36px;
+ background: #1482f0;
+ border-radius: 100px;
+ text-align: center;
+ color: #ffffff;
+ opacity: 0;
+ font-size: 14px;
+ line-height: 36px;
+ cursor: pointer;
+ animation-name: slideUp;
+ animation-duration: 0.5s;
+ animation-delay: 0.3s;
+ animation-fill-mode: forwards;
+ }
+ @keyframes slideUp {
+ 0% {
+ transform: translateY(60px);
+ opacity: 0;
+ }
+ 100% {
+ transform: translateY(0);
+ opacity: 1;
+ }
+ }
+ }
+}
+</style>
diff --git a/src/views/index.vue b/src/views/index.vue
new file mode 100644
index 0000000..12d764b
--- /dev/null
+++ b/src/views/index.vue
@@ -0,0 +1,173 @@
+<template>
+ <div class="app-container home">
+ <el-row :gutter="20">
+ <el-col :sm="24" :lg="12" style="padding-left: 20px">
+ <h2>RuoYi-Vue-Plus多租户管理系统</h2>
+ <p>
+ RuoYi-Vue-Plus 是基于 RuoYi-Vue 针对 分布式集群 场景升级(不兼容原框架)
+ <br />
+ * 前端开发框架 Vue3、TS、Element Plus<br />
+ * 后端开发框架 Spring Boot<br />
+ * 容器框架 Undertow 基于 Netty 的高性能容器<br />
+ * 权限认证框架 Sa-Token 支持多终端认证系统<br />
+ * 关系数据库 MySQL 适配 8.X 最低 5.7<br />
+ * 缓存数据库 Redis 适配 6.X 最低 4.X<br />
+ * 数据库框架 Mybatis-Plus 快速 CRUD 增加开发效率<br />
+ * 数据库框架 p6spy 更强劲的 SQL 分析<br />
+ * 多数据源框架 dynamic-datasource 支持主从与多种类数据库异构<br />
+ * 序列化框架 Jackson 统一使用 jackson 高效可靠<br />
+ * Redis客户端 Redisson 性能强劲、API丰富<br />
+ * 分布式限流 Redisson 全局、请求IP、集群ID 多种限流<br />
+ * 分布式锁 Lock4j 注解锁、工具锁 多种多样<br />
+ * 分布式幂等 Lock4j 基于分布式锁实现<br />
+ * 分布式链路追踪 SkyWalking 支持链路追踪、网格分析、度量聚合、可视化<br />
+ * 分布式任务调度 PowerJob 高性能 高可靠 易扩展<br />
+ * 文件存储 Minio 本地存储<br />
+ * 文件存储 七牛、阿里、腾讯 云存储<br />
+ * 监控框架 SpringBoot-Admin 全方位服务监控<br />
+ * 校验框架 Validation 增强接口安全性 严谨性<br />
+ * Excel框架 Alibaba EasyExcel 性能优异 扩展性强<br />
+ * 文档框架 SpringDoc、javadoc 无注解零入侵基于java注释<br />
+ * 工具类框架 Hutool、Lombok 减少代码冗余 增加安全性<br />
+ * 代码生成器 适配MP、SpringDoc规范化代码 一键生成前后端代码<br />
+ * 部署方式 Docker 容器编排 一键部署业务集群<br />
+ * 国际化 SpringMessage Spring标准国际化方案<br />
+ </p>
+ <p><b>当前版本:</b> <span>v5.1.2</span></p>
+ <p>
+ <el-tag type="danger">¥免费开源</el-tag>
+ </p>
+ <p>
+ <el-button type="primary" icon="Cloudy" plain @click="goTarget('https://gitee.com/dromara/RuoYi-Vue-Plus')">访问码云</el-button>
+ <el-button type="primary" icon="Cloudy" plain @click="goTarget('https://github.com/dromara/RuoYi-Vue-Plus')">访问GitHub</el-button>
+ <el-button type="primary" icon="Cloudy" plain @click="goTarget('https://plus-doc.dromara.org/#/ruoyi-vue-plus/changlog')"
+ >更新日志</el-button
+ >
+ </p>
+ </el-col>
+
+ <el-col :sm="24" :lg="12" style="padding-left: 20px">
+ <h2>RuoYi-Cloud-Plus多租户微服务管理系统</h2>
+ <p>
+ RuoYi-Cloud-Plus 微服务通用权限管理系统 重写 RuoYi-Cloud 全方位升级(不兼容原框架)
+ <br />
+ * 前端开发框架 Vue3、TS、Element UI<br />
+ * 后端开发框架 Spring Boot<br />
+ * 微服务开发框架 Spring Cloud、Spring Cloud Alibaba<br />
+ * 容器框架 Undertow 基于 XNIO 的高性能容器<br />
+ * 权限认证框架 Sa-Token、Jwt 支持多终端认证系统<br />
+ * 关系数据库 MySQL 适配 8.X 最低 5.7<br />
+ * 关系数据库 Oracle 适配 11g 12c<br />
+ * 关系数据库 PostgreSQL 适配 13 14<br />
+ * 关系数据库 SQLServer 适配 2017 2019<br />
+ * 缓存数据库 Redis 适配 6.X 最低 5.X<br />
+ * 分布式注册中心 Alibaba Nacos 采用2.X 基于GRPC通信高性能<br />
+ * 分布式配置中心 Alibaba Nacos 采用2.X 基于GRPC通信高性能<br />
+ * 服务网关 Spring Cloud Gateway 响应式高性能网关<br />
+ * 负载均衡 Spring Cloud Loadbalancer 负载均衡处理<br />
+ * RPC远程调用 Apache Dubbo 原生态使用体验、高性能<br />
+ * 分布式限流熔断 Alibaba Sentinel 无侵入、高扩展<br />
+ * 分布式事务 Alibaba Seata 无侵入、高扩展 支持 四种模式<br />
+ * 分布式消息队列 Spring Cloud Stream 门面框架兼容各种MQ集成<br />
+ * 分布式消息队列 Apache Kafka 高性能高速度<br />
+ * 分布式消息队列 Apache RocketMQ 高可用功能多样<br />
+ * 分布式消息队列 RabbitMQ 支持各种扩展插件功能多样性<br />
+ * 分布式搜索引擎 ElasticSearch 业界知名<br />
+ * 分布式链路追踪 Apache SkyWalking 链路追踪、网格分析、度量聚合、可视化<br />
+ * 分布式日志中心 ELK 业界成熟解决方案<br />
+ * 分布式监控 Prometheus、Grafana 全方位性能监控<br />
+ * 其余与 Vue 版本一致<br />
+ </p>
+ <p><b>当前版本:</b> <span>v2.1.2</span></p>
+ <p>
+ <el-tag type="danger">¥免费开源</el-tag>
+ </p>
+ <p>
+ <el-button type="primary" icon="Cloudy" plain @click="goTarget('https://gitee.com/dromara/RuoYi-Cloud-Plus')">访问码云</el-button>
+ <el-button type="primary" icon="Cloudy" plain @click="goTarget('https://github.com/dromara/RuoYi-Cloud-Plus')">访问GitHub</el-button>
+ <el-button type="primary" icon="Cloudy" plain @click="goTarget('https://plus-doc.dromara.org/#/ruoyi-cloud-plus/changlog')"
+ >更新日志</el-button
+ >
+ </p>
+ </el-col>
+ </el-row>
+ <el-divider />
+ </div>
+</template>
+
+<script setup name="Index" lang="ts">
+import { initWebSocket } from '@/utils/websocket';
+
+onMounted(() => {
+ let protocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://'
+ initWebSocket(protocol + window.location.host + import.meta.env.VITE_APP_BASE_API + "/resource/websocket");
+});
+
+const goTarget = (url:string) => {
+ window.open(url, '__blank')
+}
+</script>
+
+<style scoped lang="scss">
+.home {
+ blockquote {
+ padding: 10px 20px;
+ margin: 0 0 20px;
+ font-size: 17.5px;
+ border-left: 5px solid #eee;
+ }
+ hr {
+ margin-top: 20px;
+ margin-bottom: 20px;
+ border: 0;
+ border-top: 1px solid #eee;
+ }
+ .col-item {
+ margin-bottom: 20px;
+ }
+
+ ul {
+ padding: 0;
+ margin: 0;
+ }
+
+ font-family: "open sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
+ font-size: 13px;
+ color: #676a6c;
+ overflow-x: hidden;
+
+ ul {
+ list-style-type: none;
+ }
+
+ h4 {
+ margin-top: 0px;
+ }
+
+ h2 {
+ margin-top: 10px;
+ font-size: 26px;
+ font-weight: 100;
+ }
+
+ p {
+ margin-top: 10px;
+
+ b {
+ font-weight: 700;
+ }
+ }
+
+ .update-log {
+ ol {
+ display: block;
+ list-style-type: decimal;
+ margin-block-start: 1em;
+ margin-block-end: 1em;
+ margin-inline-start: 0;
+ margin-inline-end: 0;
+ padding-inline-start: 40px;
+ }
+ }
+}
+</style>
diff --git a/src/views/login.vue b/src/views/login.vue
new file mode 100644
index 0000000..33e9abc
--- /dev/null
+++ b/src/views/login.vue
@@ -0,0 +1,283 @@
+<template>
+ <div class="login">
+ <el-form ref="loginRef" :model="loginForm" :rules="loginRules" class="login-form">
+ <h3 class="title">通用多租户管理系统</h3>
+ <el-form-item prop="tenantId" v-if="tenantEnabled">
+ <el-select v-model="loginForm.tenantId" filterable placeholder="请选择/输入公司名称" style="width: 100%">
+ <el-option v-for="item in tenantList" :key="item.tenantId" :label="item.companyName" :value="item.tenantId"></el-option>
+ <template #prefix><svg-icon icon-class="company" class="el-input__icon input-icon" /></template>
+ </el-select>
+ </el-form-item>
+ <el-form-item prop="username">
+ <el-input v-model="loginForm.username" type="text" size="large" auto-complete="off" placeholder="账号">
+ <template #prefix><svg-icon icon-class="user" class="el-input__icon input-icon" /></template>
+ </el-input>
+ </el-form-item>
+ <el-form-item prop="password">
+ <el-input v-model="loginForm.password" type="password" size="large" auto-complete="off" placeholder="密码" @keyup.enter="handleLogin">
+ <template #prefix><svg-icon icon-class="password" class="el-input__icon input-icon" /></template>
+ </el-input>
+ </el-form-item>
+ <el-form-item prop="code" v-if="captchaEnabled">
+ <el-input v-model="loginForm.code" size="large" auto-complete="off" placeholder="验证码" style="width: 63%" @keyup.enter="handleLogin">
+ <template #prefix><svg-icon icon-class="validCode" class="el-input__icon input-icon" /></template>
+ </el-input>
+ <div class="login-code">
+ <img :src="codeUrl" @click="getCode" class="login-code-img" />
+ </div>
+ </el-form-item>
+ <el-checkbox v-model="loginForm.rememberMe" style="margin:0px 0px 25px 0px;">记住密码</el-checkbox>
+ <el-form-item style="float: right;">
+ <el-button circle title="微信登录" @click="doSocialLogin('wechat')">
+ <svg-icon icon-class="wechat" />
+ </el-button>
+ <el-button circle title="MaxKey登录" @click="doSocialLogin('maxkey')">
+ <svg-icon icon-class="maxkey" />
+ </el-button>
+ <el-button circle title="Gitee登录" @click="doSocialLogin('gitee')">
+ <svg-icon icon-class="gitee" />
+ </el-button>
+ <el-button circle title="Github登录" @click="doSocialLogin('github')">
+ <svg-icon icon-class="github" />
+ </el-button>
+ </el-form-item>
+ <el-form-item style="width:100%;">
+ <el-button :loading="loading" size="large" type="primary" style="width:100%;" @click.prevent="handleLogin">
+ <span v-if="!loading">登 录</span>
+ <span v-else>登 录 中...</span>
+ </el-button>
+ <div style="float: right;" v-if="register">
+ <router-link class="link-type" :to="'/register'">立即注册</router-link>
+ </div>
+ </el-form-item>
+ </el-form>
+ <!-- 底部 -->
+ <div class="el-login-footer">
+ <span>Copyright © 2018-2023 疯狂的狮子Li All Rights Reserved.</span>
+ </div>
+ </div>
+</template>
+
+<script setup lang="ts">
+import { getCodeImg, getTenantList } from '@/api/login';
+import { authBinding } from '@/api/system/social/auth';
+import { useUserStore } from '@/store/modules/user';
+import { LoginData, TenantVO } from '@/api/types';
+import { to } from 'await-to-js';
+import { HttpStatus } from "@/enums/RespEnum";
+
+const userStore = useUserStore();
+const router = useRouter();
+
+const loginForm = ref<LoginData>({
+ tenantId: '000000',
+ username: 'admin',
+ password: 'admin123',
+ rememberMe: false,
+ code: '',
+ uuid: ''
+} as LoginData);
+
+const loginRules: ElFormRules = {
+ tenantId: [{ required: true, trigger: "blur", message: "请输入您的租户编号" }],
+ username: [{ required: true, trigger: 'blur', message: '请输入您的账号' }],
+ password: [{ required: true, trigger: 'blur', message: '请输入您的密码' }],
+ code: [{ required: true, trigger: 'change', message: '请输入验证码' }]
+};
+
+const codeUrl = ref('');
+const loading = ref(false);
+// 验证码开关
+const captchaEnabled = ref(true);
+// 租户开关
+const tenantEnabled = ref(true);
+
+
+// 注册开关
+const register = ref(false);
+const redirect = ref(undefined);
+const loginRef = ref<ElFormInstance>();
+// 租户列表
+const tenantList = ref<TenantVO[]>([]);
+
+watch(() => router.currentRoute.value, (newRoute: any) => {
+ redirect.value = newRoute.query && newRoute.query.redirect;
+}, { immediate: true });
+
+const handleLogin = () => {
+ loginRef.value?.validate(async (valid: boolean, fields: any) => {
+ if (valid) {
+ loading.value = true;
+ // 勾选了需要记住密码设置在 localStorage 中设置记住用户名和密码
+ if (loginForm.value.rememberMe) {
+ localStorage.setItem("tenantId", String(loginForm.value.tenantId));
+ localStorage.setItem('username', String(loginForm.value.username));
+ localStorage.setItem('password', String(loginForm.value.password));
+ localStorage.setItem('rememberMe', String(loginForm.value.rememberMe));
+ } else {
+ // 否则移除
+ localStorage.removeItem("tenantId");
+ localStorage.removeItem('username');
+ localStorage.removeItem('password');
+ localStorage.removeItem('rememberMe');
+ }
+ // 调用action的登录方法
+ const [err] = await to(userStore.login(loginForm.value));
+ if (!err) {
+ await router.push({ path: redirect.value || '/' });
+ loading.value = false;
+ } else {
+ loading.value = false;
+ // 重新获取验证码
+ if (captchaEnabled.value) {
+ await getCode();
+ }
+ }
+ } else {
+ console.log('error submit!', fields);
+ }
+ });
+};
+
+/**
+ * 获取验证码
+ */
+const getCode = async () => {
+ const res = await getCodeImg();
+ const { data } = res;
+ captchaEnabled.value = data.captchaEnabled === undefined ? true : data.captchaEnabled;
+ if (captchaEnabled.value) {
+ codeUrl.value = 'data:image/gif;base64,' + data.img;
+ loginForm.value.uuid = data.uuid;
+ }
+};
+
+const getLoginData = () => {
+ const tenantId = localStorage.getItem("tenantId");
+ const username = localStorage.getItem('username');
+ const password = localStorage.getItem('password');
+ const rememberMe = localStorage.getItem('rememberMe');
+ loginForm.value = {
+ tenantId: tenantId === null ? String(loginForm.value.tenantId) : tenantId,
+ username: username === null ? String(loginForm.value.username) : username,
+ password: password === null ? String(loginForm.value.password) : String(password),
+ rememberMe: rememberMe === null ? false : Boolean(rememberMe)
+ } as LoginData;
+}
+
+
+/**
+ * 获取租户列表
+ */
+const initTenantList = async () => {
+ const { data } = await getTenantList();
+ tenantEnabled.value = data.tenantEnabled === undefined ? true : data.tenantEnabled;
+ if (tenantEnabled.value) {
+ tenantList.value = data.voList;
+ if (tenantList.value != null && tenantList.value.length !== 0) {
+ loginForm.value.tenantId = tenantList.value[0].tenantId;
+ }
+ }
+}
+
+//检测租户选择框的变化
+watch(() => loginForm.value.tenantId, () => {
+ localStorage.setItem("tenantId", String(loginForm.value.tenantId))
+});
+
+/**
+ * 第三方登录
+ * @param type
+ */
+const doSocialLogin = (type: string) => {
+ authBinding(type).then((res: any) => {
+ if (res.code === HttpStatus.SUCCESS) {
+ // 获取授权地址跳转
+ window.location.href = res.data;
+ } else {
+ ElMessage.error(res.msg);
+ }
+ });
+};
+
+
+
+onMounted(() => {
+ getCode();
+ initTenantList();
+ getLoginData();
+});
+</script>
+
+<style lang="scss" scoped>
+.login {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 100%;
+ background-image: url("../assets/images/login-background.jpg");
+ background-size: cover;
+}
+
+.title {
+ margin: 0px auto 30px auto;
+ text-align: center;
+ color: #707070;
+}
+
+.login-form {
+ border-radius: 6px;
+ background: #ffffff;
+ width: 400px;
+ padding: 25px 25px 5px 25px;
+
+ .el-input {
+ height: 40px;
+
+ input {
+ height: 40px;
+ }
+ }
+
+ .input-icon {
+ height: 39px;
+ width: 14px;
+ margin-left: 0px;
+ }
+}
+
+.login-tip {
+ font-size: 13px;
+ text-align: center;
+ color: #bfbfbf;
+}
+
+.login-code {
+ width: 33%;
+ height: 40px;
+ float: right;
+
+ img {
+ cursor: pointer;
+ vertical-align: middle;
+ }
+}
+
+.el-login-footer {
+ height: 40px;
+ line-height: 40px;
+ position: fixed;
+ bottom: 0;
+ width: 100%;
+ text-align: center;
+ color: #fff;
+ font-family: Arial, serif;
+ font-size: 12px;
+ letter-spacing: 1px;
+}
+
+.login-code-img {
+ height: 40px;
+ padding-left: 12px;
+}
+</style>
diff --git a/src/views/monitor/admin/index.vue b/src/views/monitor/admin/index.vue
new file mode 100644
index 0000000..04c63b5
--- /dev/null
+++ b/src/views/monitor/admin/index.vue
@@ -0,0 +1,9 @@
+<template>
+ <div>
+ <i-frame v-model:src="url"></i-frame>
+ </div>
+</template>
+
+<script setup lang="ts">
+const url = ref(import.meta.env.VITE_APP_MONITRO_ADMIN);
+</script>
diff --git a/src/views/monitor/cache/index.vue b/src/views/monitor/cache/index.vue
new file mode 100644
index 0000000..8719c94
--- /dev/null
+++ b/src/views/monitor/cache/index.vue
@@ -0,0 +1,191 @@
+<template>
+ <div class="p-2">
+ <el-row>
+ <el-col :span="24" class="card-box">
+ <el-card shadow="hover">
+ <template #header>
+ <Monitor style="width: 1em; height: 1em; vertical-align: middle;" />
+ <span style="vertical-align: middle;">基本信息</span>
+ </template>
+
+ <div class="el-table el-table--enable-row-hover el-table--medium">
+ <table style="width: 100%">
+ <tbody>
+ <tr>
+ <td class="el-table__cell is-leaf">
+ <div class="cell">Redis版本</div>
+ </td>
+ <td class="el-table__cell is-leaf">
+ <div class="cell" v-if="cache.info">{{ cache.info.redis_version }}</div>
+ </td>
+ <td class="el-table__cell is-leaf">
+ <div class="cell">运行模式</div>
+ </td>
+ <td class="el-table__cell is-leaf">
+ <div class="cell" v-if="cache.info">{{ cache.info.redis_mode === "standalone" ? "单机" : "集群" }}</div>
+ </td>
+ <td class="el-table__cell is-leaf">
+ <div class="cell">端口</div>
+ </td>
+ <td class="el-table__cell is-leaf">
+ <div class="cell" v-if="cache.info">{{ cache.info.tcp_port }}</div>
+ </td>
+ <td class="el-table__cell is-leaf">
+ <div class="cell">客户端数</div>
+ </td>
+ <td class="el-table__cell is-leaf">
+ <div class="cell" v-if="cache.info">{{ cache.info.connected_clients }}</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="el-table__cell is-leaf">
+ <div class="cell">运行时间(天)</div>
+ </td>
+ <td class="el-table__cell is-leaf">
+ <div class="cell" v-if="cache.info">{{ cache.info.uptime_in_days }}</div>
+ </td>
+ <td class="el-table__cell is-leaf">
+ <div class="cell">使用内存</div>
+ </td>
+ <td class="el-table__cell is-leaf">
+ <div class="cell" v-if="cache.info">{{ cache.info.used_memory_human }}</div>
+ </td>
+ <td class="el-table__cell is-leaf">
+ <div class="cell">使用CPU</div>
+ </td>
+ <td class="el-table__cell is-leaf">
+ <div class="cell" v-if="cache.info">{{ parseFloat(cache.info.used_cpu_user_children).toFixed(2) }}</div>
+ </td>
+ <td class="el-table__cell is-leaf">
+ <div class="cell">内存配置</div>
+ </td>
+ <td class="el-table__cell is-leaf">
+ <div class="cell" v-if="cache.info">{{ cache.info.maxmemory_human }}</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="el-table__cell is-leaf">
+ <div class="cell">AOF是否开启</div>
+ </td>
+ <td class="el-table__cell is-leaf">
+ <div class="cell" v-if="cache.info">{{ cache.info.aof_enabled === "0" ? "否" : "是" }}</div>
+ </td>
+ <td class="el-table__cell is-leaf">
+ <div class="cell">RDB是否成功</div>
+ </td>
+ <td class="el-table__cell is-leaf">
+ <div class="cell" v-if="cache.info">{{ cache.info.rdb_last_bgsave_status }}</div>
+ </td>
+ <td class="el-table__cell is-leaf">
+ <div class="cell">Key数量</div>
+ </td>
+ <td class="el-table__cell is-leaf">
+ <div class="cell" v-if="cache.dbSize">{{ cache.dbSize }}</div>
+ </td>
+ <td class="el-table__cell is-leaf">
+ <div class="cell">网络入口/出口</div>
+ </td>
+ <td class="el-table__cell is-leaf">
+ <div class="cell" v-if="cache.info">
+ {{ cache.info.instantaneous_input_kbps }}kps/{{ cache.info.instantaneous_output_kbps }}kps
+ </div>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </el-card>
+ </el-col>
+
+ <el-col :span="12" class="card-box">
+ <el-card shadow="hover">
+ <template #header>
+ <PieChart style="width: 1em; height: 1em; vertical-align: middle;" />
+ <span style="vertical-align: middle;">命令统计</span>
+ </template>
+ <div class="el-table el-table--enable-row-hover el-table--medium">
+ <div ref="commandstats" style="height: 420px" />
+ </div>
+ </el-card>
+ </el-col>
+
+ <el-col :span="12" class="card-box">
+ <el-card shadow="hover">
+ <template #header>
+ <Odometer style="width: 1em; height: 1em; vertical-align: middle;" /> <span style="vertical-align: middle;">内存信息</span>
+ </template>
+ <div class="el-table el-table--enable-row-hover el-table--medium">
+ <div ref="usedmemory" style="height: 420px" />
+ </div>
+ </el-card>
+ </el-col>
+ </el-row>
+ </div>
+</template>
+
+<script setup name="Cache" lang="ts">
+import { getCache } from '@/api/monitor/cache';
+import * as echarts from 'echarts';
+
+const cache = ref<any>({});
+const commandstats = ref();
+const usedmemory = ref();
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+
+const getList = async () => {
+ proxy?.$modal.loading("正在加载缓存监控数据,请稍候!");
+ const res = await getCache();
+ proxy?.$modal.closeLoading();
+ cache.value = res.data;
+ const commandstatsIntance = echarts.init(commandstats.value, "macarons");
+ commandstatsIntance.setOption({
+ tooltip: {
+ trigger: "item",
+ formatter: "{a} <br/>{b} : {c} ({d}%)"
+ },
+ series: [
+ {
+ name: "命令",
+ type: "pie",
+ roseType: "radius",
+ radius: [15, 95],
+ center: ["50%", "38%"],
+ data: res.data.commandStats,
+ animationEasing: "cubicInOut",
+ animationDuration: 1000
+ }
+ ]
+ });
+ const usedmemoryInstance = echarts.init(usedmemory.value, "macarons");
+ usedmemoryInstance.setOption({
+ tooltip: {
+ formatter: "{b} <br/>{a} : " + cache.value.info.used_memory_human
+ },
+ series: [
+ {
+ name: "峰值",
+ type: "gauge",
+ min: 0,
+ max: 1000,
+ detail: {
+ formatter: cache.value.info.used_memory_human
+ },
+ data: [
+ {
+ value: parseFloat(cache.value.info.used_memory_human),
+ name: "内存消耗"
+ }
+ ]
+ }
+ ]
+ })
+ window.addEventListener("resize",()=>{
+ commandstatsIntance.resize()
+ usedmemoryInstance.resize()
+ });
+}
+
+onMounted(() => {
+ getList();
+})
+</script>
diff --git a/src/views/monitor/logininfor/index.vue b/src/views/monitor/logininfor/index.vue
new file mode 100644
index 0000000..067790e
--- /dev/null
+++ b/src/views/monitor/logininfor/index.vue
@@ -0,0 +1,204 @@
+<template>
+ <div class="p-2">
+ <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
+ <div class="mb-[10px]" v-show="showSearch">
+ <el-card shadow="hover">
+ <el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px">
+ <el-form-item label="登录地址" prop="ipaddr">
+ <el-input v-model="queryParams.ipaddr" placeholder="请输入登录地址" clearable style="width: 240px;" @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item label="用户名称" prop="userName">
+ <el-input v-model="queryParams.userName" placeholder="请输入用户名称" clearable style="width: 240px;" @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item label="状态" prop="status">
+ <el-select v-model="queryParams.status" placeholder="登录状态" clearable style="width: 240px">
+ <el-option v-for="dict in sys_common_status" :key="dict.value" :label="dict.label" :value="dict.value" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="登录时间" style="width: 308px">
+ <el-date-picker
+ v-model="dateRange"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ range-separator="-"
+ start-placeholder="开始日期"
+ end-placeholder="结束日期"
+ :default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
+ ></el-date-picker>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+ <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+ </el-form-item>
+ </el-form>
+ </el-card>
+ </div>
+ </transition>
+
+ <el-card shadow="hover">
+ <template #header>
+ <el-row :gutter="10" class="mb8">
+ <el-col :span="1.5">
+ <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['monitor:logininfor:remove']">
+ 删除
+ </el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button type="danger" plain icon="Delete" @click="handleClean" v-hasPermi="['monitor:logininfor:remove']">清空</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button type="primary" plain icon="Unlock" :disabled="single" @click="handleUnlock" v-hasPermi="['monitor:logininfor:unlock']">
+ 解锁
+ </el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['monitor:logininfor:export']">导出</el-button>
+ </el-col>
+ <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
+ </el-row>
+ </template>
+
+ <el-table
+ ref="loginInfoTableRef"
+ v-loading="loading"
+ :data="loginInfoList"
+ @selection-change="handleSelectionChange"
+ :default-sort="defaultSort"
+ @sort-change="handleSortChange"
+ >
+ <el-table-column type="selection" width="55" align="center" />
+ <el-table-column label="访问编号" align="center" prop="infoId" />
+ <el-table-column
+ label="用户名称"
+ align="center"
+ prop="userName"
+ :show-overflow-tooltip="true"
+ sortable="custom"
+ :sort-orders="['descending', 'ascending']"
+ />
+ <el-table-column label="客户端" align="center" prop="clientKey" :show-overflow-tooltip="true" />
+ <el-table-column label="设备类型" align="center">
+ <template #default="scope">
+ <dict-tag :options="sys_device_type" :value="scope.row.deviceType" />
+ </template>
+ </el-table-column>
+ <el-table-column label="地址" align="center" prop="ipaddr" :show-overflow-tooltip="true" />
+ <el-table-column label="登录地点" align="center" prop="loginLocation" :show-overflow-tooltip="true" />
+ <el-table-column label="操作系统" align="center" prop="os" :show-overflow-tooltip="true" />
+ <el-table-column label="浏览器" align="center" prop="browser" :show-overflow-tooltip="true" />
+ <el-table-column label="登录状态" align="center" prop="status">
+ <template #default="scope">
+ <dict-tag :options="sys_common_status" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column label="描述" align="center" prop="msg" :show-overflow-tooltip="true" />
+ <el-table-column label="访问时间" align="center" prop="loginTime" sortable="custom" :sort-orders="['descending', 'ascending']" width="180">
+ <template #default="scope">
+ <span>{{ parseTime(scope.row.loginTime) }}</span>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
+ </el-card>
+ </div>
+</template>
+
+<script setup name="Logininfor" lang="ts">
+import { list, delLoginInfo, cleanLoginInfo, unlockLoginInfo } from "@/api/monitor/loginInfo";
+import { LoginInfoQuery, LoginInfoVO } from "@/api/monitor/loginInfo/types";
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const { sys_device_type } = toRefs<any>(proxy?.useDict("sys_device_type"));
+const { sys_common_status } = toRefs<any>(proxy?.useDict("sys_common_status"));
+
+const loginInfoList = ref<LoginInfoVO[]>([]);
+const loading = ref(true);
+const showSearch = ref(true);
+const ids = ref<Array<number | string>>([]);
+const single = ref(true);
+const multiple = ref(true);
+const selectName = ref<Array<string>>([]);
+const total = ref(0);
+const dateRange = ref<[DateModelType,DateModelType]>(['', '']);
+const defaultSort = ref<any>({ prop: "loginTime", order: "descending" });
+
+const queryFormRef = ref<ElFormInstance>();
+const loginInfoTableRef = ref<ElTableInstance>();
+// 查询参数
+const queryParams = ref<LoginInfoQuery>({
+ pageNum: 1,
+ pageSize: 10,
+ ipaddr: '',
+ userName: '',
+ status: '',
+ orderByColumn: defaultSort.value.prop,
+ isAsc: defaultSort.value.order
+});
+
+/** 查询登录日志列表 */
+const getList = async () => {
+ loading.value = true;
+ const res = await list(proxy?.addDateRange(queryParams.value, dateRange.value));
+ loginInfoList.value = res.rows;
+ total.value = res.total;
+ loading.value = false;
+}
+/** 搜索按钮操作 */
+const handleQuery = () => {
+ queryParams.value.pageNum = 1;
+ getList();
+}
+/** 重置按钮操作 */
+const resetQuery = () => {
+ dateRange.value = ['', ''];
+ queryFormRef.value?.resetFields();
+ queryParams.value.pageNum = 1;
+ loginInfoTableRef.value?.sort(defaultSort.value.prop, defaultSort.value.order);
+}
+/** 多选框选中数据 */
+const handleSelectionChange = (selection: LoginInfoVO[]) => {
+ ids.value = selection.map(item => item.infoId);
+ multiple.value = !selection.length;
+ single.value = selection.length != 1;
+ selectName.value = selection.map(item => item.userName);
+}
+/** 排序触发事件 */
+const handleSortChange = (column: any) => {
+ queryParams.value.orderByColumn = column.prop;
+ queryParams.value.isAsc = column.order;
+ getList();
+}
+/** 删除按钮操作 */
+const handleDelete = async (row?: LoginInfoVO) => {
+ const infoIds = row?.infoId || ids.value;
+ await proxy?.$modal.confirm('是否确认删除访问编号为"' + infoIds + '"的数据项?');
+ await delLoginInfo(infoIds);
+ await getList();
+ proxy?.$modal.msgSuccess("删除成功");
+}
+/** 清空按钮操作 */
+const handleClean = async () => {
+ await proxy?.$modal.confirm("是否确认清空所有登录日志数据项?");
+ await cleanLoginInfo();
+ await getList();
+ proxy?.$modal.msgSuccess("清空成功");
+}
+/** 解锁按钮操作 */
+const handleUnlock = async () => {
+ const username = selectName.value;
+ await proxy?.$modal.confirm('是否确认解锁用户"' + username + '"数据项?');
+ await unlockLoginInfo(username);
+ proxy?.$modal.msgSuccess("用户" + username + "解锁成功");
+}
+/** 导出按钮操作 */
+const handleExport = () => {
+ proxy?.download("monitor/logininfor/export", {
+ ...queryParams.value,
+ }, `config_${new Date().getTime()}.xlsx`);
+}
+
+onMounted(() => {
+ getList();
+})
+</script>
diff --git a/src/views/monitor/online/index.vue b/src/views/monitor/online/index.vue
new file mode 100644
index 0000000..d105cd7
--- /dev/null
+++ b/src/views/monitor/online/index.vue
@@ -0,0 +1,116 @@
+<template>
+ <div class="p-2">
+ <div class="mb-[10px]">
+ <el-card shadow="hover">
+ <el-form :model="queryParams" ref="queryFormRef" :inline="true">
+ <el-form-item label="登录地址" prop="ipaddr">
+ <el-input v-model="queryParams.ipaddr" placeholder="请输入登录地址" clearable style="width: 200px" @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item label="用户名称" prop="userName">
+ <el-input v-model="queryParams.userName" placeholder="请输入用户名称" clearable style="width: 200px" @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+ <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+ </el-form-item>
+ </el-form>
+ </el-card>
+ </div>
+ <el-card shadow="hover">
+ <el-table
+ v-loading="loading"
+ :data="onlineList.slice((queryParams.pageNum - 1) * queryParams.pageSize, queryParams.pageNum * queryParams.pageSize)"
+ style="width: 100%;"
+ >
+ <el-table-column label="序号" width="50" type="index" align="center">
+ <template #default="scope">
+ <span>{{ (queryParams.pageNum - 1) * queryParams.pageSize + scope.$index + 1 }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="会话编号" align="center" prop="tokenId" :show-overflow-tooltip="true" />
+ <el-table-column label="登录名称" align="center" prop="userName" :show-overflow-tooltip="true" />
+ <el-table-column label="客户端" align="center" prop="clientKey" :show-overflow-tooltip="true" />
+ <el-table-column label="设备类型" align="center">
+ <template #default="scope">
+ <dict-tag :options="sys_device_type" :value="scope.row.deviceType" />
+ </template>
+ </el-table-column>
+ <el-table-column label="所属部门" align="center" prop="deptName" :show-overflow-tooltip="true" />
+ <el-table-column label="主机" align="center" prop="ipaddr" :show-overflow-tooltip="true" />
+ <el-table-column label="登录地点" align="center" prop="loginLocation" :show-overflow-tooltip="true" />
+ <el-table-column label="操作系统" align="center" prop="os" :show-overflow-tooltip="true" />
+ <el-table-column label="浏览器" align="center" prop="browser" :show-overflow-tooltip="true" />
+ <el-table-column label="登录时间" align="center" prop="loginTime" width="180">
+ <template #default="scope">
+ <span>{{ parseTime(scope.row.loginTime) }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+ <template #default="scope">
+ <el-tooltip content="强退" placement="top">
+ <el-button link type="primary" icon="Delete" @click="handleForceLogout(scope.row)" v-hasPermi="['monitor:online:forceLogout']">
+ </el-button>
+ </el-tooltip>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" />
+ </el-card>
+ </div>
+</template>
+
+<script setup name="Online" lang="ts">
+import { forceLogout, list as initData } from "@/api/monitor/online";
+import { OnlineQuery, OnlineVO } from "@/api/monitor/online/types";
+import api from "@/api/system/user";
+import {to} from "await-to-js";
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const { sys_device_type } = toRefs<any>(proxy?.useDict("sys_device_type"));
+
+const onlineList = ref<OnlineVO[]>([]);
+const loading = ref(true);
+const total = ref(0);
+
+const queryFormRef = ref<ElFormInstance>();
+
+const queryParams = ref<OnlineQuery>({
+ pageNum: 1,
+ pageSize: 10,
+ ipaddr: '',
+ userName: ''
+});
+
+/** 查询登录日志列表 */
+const getList = async () => {
+ loading.value = true;
+ const res = await initData(queryParams.value);
+ onlineList.value = res.rows;
+ total.value = res.total;
+ loading.value = false;
+}
+/** 搜索按钮操作 */
+const handleQuery = () => {
+ queryParams.value.pageNum = 1;
+ getList();
+}
+/** 重置按钮操作 */
+const resetQuery = () => {
+ queryFormRef.value?.resetFields();
+ handleQuery();
+}
+/** 强退按钮操作 */
+const handleForceLogout = async (row: OnlineVO) => {
+ const [err] = await to(proxy?.$modal.confirm('是否确认强退名称为"' + row.userName + '"的用户?') as any);
+ if (!err) {
+ await forceLogout(row.tokenId);
+ await getList();
+ proxy?.$modal.msgSuccess("删除成功");
+ }
+}
+
+onMounted(() => {
+ getList();
+})
+</script>
diff --git a/src/views/monitor/operlog/index.vue b/src/views/monitor/operlog/index.vue
new file mode 100644
index 0000000..ea68c24
--- /dev/null
+++ b/src/views/monitor/operlog/index.vue
@@ -0,0 +1,302 @@
+<template>
+ <div class="p-2">
+ <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
+ <div class="mb-[10px]">
+ <el-card shadow="hover">
+ <el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px">
+ <el-form-item label="操作地址" prop="operIp">
+ <el-input v-model="queryParams.operIp" placeholder="请输入操作地址" clearable style="width: 240px;" @keyup.enter="handleQuery"/>
+ </el-form-item>
+ <el-form-item label="系统模块" prop="title">
+ <el-input v-model="queryParams.title" placeholder="请输入系统模块" clearable style="width: 240px;" @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item label="操作人员" prop="operName">
+ <el-input v-model="queryParams.operName" placeholder="请输入操作人员" clearable style="width: 240px;" @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item label="类型" prop="businessType">
+ <el-select v-model="queryParams.businessType" placeholder="操作类型" clearable style="width: 240px">
+ <el-option v-for="dict in sys_oper_type" :key="dict.value" :label="dict.label" :value="dict.value" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="状态" prop="status">
+ <el-select v-model="queryParams.status" placeholder="操作状态" clearable style="width: 240px">
+ <el-option v-for="dict in sys_common_status" :key="dict.value" :label="dict.label" :value="dict.value" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="操作时间" style="width: 308px">
+ <el-date-picker
+ v-model="dateRange"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ range-separator="-"
+ start-placeholder="开始日期"
+ end-placeholder="结束日期"
+ :default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
+ ></el-date-picker>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+ <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+ </el-form-item>
+ </el-form>
+ </el-card>
+ </div>
+ </transition>
+
+ <el-card shadow="hover">
+ <template #header>
+ <el-row :gutter="10" class="mb8">
+ <el-col :span="1.5">
+ <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['monitor:operlog:remove']">
+ 删除
+ </el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button type="danger" plain icon="WarnTriangleFilled" @click="handleClean" v-hasPermi="['monitor:operlog:remove']">清空</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['monitor:operlog:export']">导出</el-button>
+ </el-col>
+ <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
+ </el-row>
+ </template>
+
+ <el-table
+ ref="operLogTableRef"
+ v-loading="loading"
+ :data="operlogList"
+ @selection-change="handleSelectionChange"
+ :default-sort="defaultSort"
+ @sort-change="handleSortChange"
+ >
+ <el-table-column type="selection" width="50" align="center" />
+ <el-table-column label="日志编号" align="center" prop="operId" />
+ <el-table-column label="系统模块" align="center" prop="title" :show-overflow-tooltip="true" />
+ <el-table-column label="操作类型" align="center" prop="businessType">
+ <template #default="scope">
+ <dict-tag :options="sys_oper_type" :value="scope.row.businessType" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="操作人员"
+ align="center"
+ width="110"
+ prop="operName"
+ :show-overflow-tooltip="true"
+ sortable="custom"
+ :sort-orders="['descending', 'ascending']"
+ />
+ <el-table-column label="部门" align="center" prop="deptName" width="130" :show-overflow-tooltip="true" />
+ <el-table-column label="操作地址" align="center" prop="operIp" width="130" :show-overflow-tooltip="true" />
+ <el-table-column label="操作状态" align="center" prop="status">
+ <template #default="scope">
+ <dict-tag :options="sys_common_status" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column label="操作日期" align="center" prop="operTime" width="180" sortable="custom" :sort-orders="['descending', 'ascending']">
+ <template #default="scope">
+ <span>{{ parseTime(scope.row.operTime) }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="消耗时间"
+ align="center"
+ prop="costTime"
+ width="110"
+ :show-overflow-tooltip="true"
+ sortable="custom"
+ :sort-orders="['descending', 'ascending']"
+ >
+ <template #default="scope">
+ <span>{{ scope.row.costTime }}毫秒</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="操作" fixed="right" align="center" class-name="small-padding fixed-width">
+ <template #default="scope">
+ <el-tooltip content="详细" placement="top">
+ <el-button link type="primary" icon="View" @click="handleView(scope.row)" v-hasPermi="['monitor:operlog:query']"> </el-button>
+ </el-tooltip>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
+ </el-card>
+ <!-- 操作日志详细 -->
+ <el-dialog title="操作日志详细" v-model="dialog.visible" width="700px" append-to-body>
+ <el-form :model="form" label-width="100px">
+ <el-row>
+ <el-col :span="24">
+ <el-form-item label="登录信息:">{{ form.operName }} / {{form.deptName}} / {{ form.operIp }} / {{ form.operLocation }}</el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="请求信息:">{{ form.requestMethod }} {{ form.operUrl }}</el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="操作模块:">{{ form.title }} / {{ typeFormat(form) }}</el-form-item>
+ </el-col>
+ <el-col :span="24">
+ <el-form-item label="操作方法:">{{ form.method }}</el-form-item>
+ </el-col>
+ <el-col :span="24">
+ <el-form-item label="请求参数:">{{ form.operParam }}</el-form-item>
+ </el-col>
+ <el-col :span="24">
+ <el-form-item label="返回参数:">{{ form.jsonResult }}</el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="操作状态:">
+ <div v-if="form.status === 0">正常</div>
+ <div v-else-if="form.status === 1">失败</div>
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="消耗时间:">{{ form.costTime }}毫秒</el-form-item>
+ </el-col>
+ <el-col :span="10">
+ <el-form-item label="操作时间:">{{ parseTime(form.operTime) }}</el-form-item>
+ </el-col>
+ <el-col :span="24">
+ <el-form-item label="异常信息:" v-if="form.status === 1">{{ form.errorMsg }}</el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button @click="dialog.visible = false">关 闭</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup name="Operlog" lang="ts">
+import { list, delOperlog, cleanOperlog } from '@/api/monitor/operlog';
+import { OperLogForm, OperLogQuery, OperLogVO } from '@/api/monitor/operlog/types';
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const { sys_oper_type, sys_common_status } = toRefs<any>(proxy?.useDict("sys_oper_type", "sys_common_status"));
+
+const operlogList = ref<OperLogVO[]>([]);
+const loading = ref(true);
+const showSearch = ref(true);
+const ids = ref<Array<number | string>>([]);
+const multiple = ref(true);
+const total = ref(0);
+const dateRange = ref<[DateModelType, DateModelType]>(['', '']);
+const defaultSort = ref<any>({ prop: "operTime", order: "descending" });
+
+const operLogTableRef = ref<ElTableInstance>();
+const queryFormRef = ref<ElFormInstance>();
+
+const dialog = reactive<DialogOption>({
+ visible: false,
+ title: ''
+});
+
+
+const data = reactive<PageData<OperLogForm, OperLogQuery>>({
+ form: {
+ operId: undefined,
+ tenantId: undefined,
+ title: '',
+ businessType: 0,
+ businessTypes: undefined,
+ method: '',
+ requestMethod: '',
+ operatorType: 0,
+ operName: '',
+ deptName: '',
+ operUrl: '',
+ operIp: '',
+ operLocation: '',
+ operParam: '',
+ jsonResult: '',
+ status: 0,
+ errorMsg: '',
+ operTime: '',
+ costTime: 0
+ },
+ queryParams: {
+ pageNum: 1,
+ pageSize: 10,
+ operIp: '',
+ title: '',
+ operName: '',
+ businessType: '',
+ status: '',
+ orderByColumn: defaultSort.value.prop,
+ isAsc: defaultSort.value.order
+ },
+ rules: {}
+});
+
+const { queryParams, form } = toRefs(data);
+
+/** 查询登录日志 */
+const getList = async () => {
+ loading.value = true;
+ const res = await list(proxy?.addDateRange(queryParams.value, dateRange.value));
+ operlogList.value = res.rows;
+ total.value = res.total;
+ loading.value = false;
+}
+/** 操作日志类型字典翻译 */
+const typeFormat = (row: OperLogForm) => {
+ return proxy?.selectDictLabel(sys_oper_type.value, row.businessType);
+}
+/** 搜索按钮操作 */
+const handleQuery = () => {
+ queryParams.value.pageNum = 1;
+ getList();
+}
+/** 重置按钮操作 */
+const resetQuery = () => {
+ dateRange.value = ['', ''];
+ queryFormRef.value?.resetFields();
+ queryParams.value.pageNum = 1;
+ operLogTableRef.value?.sort(defaultSort.value.prop, defaultSort.value.order);
+}
+/** 多选框选中数据 */
+const handleSelectionChange = (selection: OperLogVO[]) => {
+ ids.value = selection.map(item => item.operId);
+ multiple.value = !selection.length;
+}
+/** 排序触发事件 */
+const handleSortChange = (column: any) => {
+ queryParams.value.orderByColumn = column.prop;
+ queryParams.value.isAsc = column.order;
+ getList();
+}
+/** 详细按钮操作 */
+const handleView = (row: OperLogVO) => {
+ dialog.visible = true;
+ form.value = row;
+}
+/** 删除按钮操作 */
+const handleDelete = async (row?: OperLogVO) => {
+ const operIds = row?.operId || ids.value;
+ await proxy?.$modal.confirm('是否确认删除日志编号为"' + operIds + '"的数据项?');
+ await delOperlog(operIds);
+ await getList();
+ proxy?.$modal.msgSuccess("删除成功");
+}
+
+/** 清空按钮操作 */
+const handleClean = async () => {
+ await proxy?.$modal.confirm("是否确认清空所有操作日志数据项?");
+ await cleanOperlog();
+ await getList();
+ proxy?.$modal.msgSuccess("清空成功");
+}
+
+/** 导出按钮操作 */
+const handleExport = () => {
+ proxy?.download("monitor/operlog/export", {
+ ...queryParams.value,
+ }, `config_${new Date().getTime()}.xlsx`);
+}
+onMounted(() => {
+ getList();
+})
+</script>
diff --git a/src/views/monitor/powerjob/index.vue b/src/views/monitor/powerjob/index.vue
new file mode 100644
index 0000000..0319e97
--- /dev/null
+++ b/src/views/monitor/powerjob/index.vue
@@ -0,0 +1,9 @@
+<template>
+ <div>
+ <i-frame v-model:src="url"></i-frame>
+ </div>
+</template>
+
+<script setup lang="ts">
+const url = ref(import.meta.env.VITE_APP_POWERJOB_ADMIN);
+</script>
diff --git a/src/views/redirect/index.vue b/src/views/redirect/index.vue
new file mode 100644
index 0000000..15401f8
--- /dev/null
+++ b/src/views/redirect/index.vue
@@ -0,0 +1,14 @@
+<template>
+ <div></div>
+</template>
+
+<script setup>
+import { useRoute, useRouter } from 'vue-router'
+
+const route = useRoute();
+const router = useRouter();
+const { params, query } = route
+const { path } = params
+
+router.replace({ path: '/' + path, query })
+</script>
diff --git a/src/views/register.vue b/src/views/register.vue
new file mode 100644
index 0000000..ad78cf1
--- /dev/null
+++ b/src/views/register.vue
@@ -0,0 +1,232 @@
+<template>
+ <div class="register">
+ <el-form ref="registerRef" :model="registerForm" :rules="registerRules" class="register-form">
+ <h3 class="title">RuoYi-Vue-Plus多租户管理系统</h3>
+ <el-form-item prop="tenantId" v-if="tenantEnabled">
+ <el-select v-model="registerForm.tenantId" filterable placeholder="请选择/输入公司名称" style="width: 100%">
+ <el-option v-for="item in tenantList" :key="item.tenantId" :label="item.companyName" :value="item.tenantId"> </el-option>
+ <template #prefix><svg-icon icon-class="company" class="el-input__icon input-icon" /></template>
+ </el-select>
+ </el-form-item>
+ <el-form-item prop="username">
+ <el-input v-model="registerForm.username" type="text" size="large" auto-complete="off" placeholder="账号">
+ <template #prefix><svg-icon icon-class="user" class="el-input__icon input-icon" /></template>
+ </el-input>
+ </el-form-item>
+ <el-form-item prop="password">
+ <el-input v-model="registerForm.password" type="password" size="large" auto-complete="off" placeholder="密码" @keyup.enter="handleRegister">
+ <template #prefix><svg-icon icon-class="password" class="el-input__icon input-icon" /></template>
+ </el-input>
+ </el-form-item>
+ <el-form-item prop="confirmPassword">
+ <el-input
+ v-model="registerForm.confirmPassword"
+ type="password"
+ size="large"
+ auto-complete="off"
+ placeholder="确认密码"
+ @keyup.enter="handleRegister"
+ >
+ <template #prefix><svg-icon icon-class="password" class="el-input__icon input-icon" /></template>
+ </el-input>
+ </el-form-item>
+ <el-form-item prop="code" v-if="captchaEnabled">
+ <el-input size="large" v-model="registerForm.code" auto-complete="off" placeholder="验证码" style="width: 63%" @keyup.enter="handleRegister">
+ <template #prefix><svg-icon icon-class="validCode" class="el-input__icon input-icon" /></template>
+ </el-input>
+ <div class="register-code">
+ <img :src="codeUrl" @click="getCode" class="register-code-img" />
+ </div>
+ </el-form-item>
+ <el-form-item style="width:100%;">
+ <el-button :loading="loading" size="large" type="primary" style="width:100%;" @click.prevent="handleRegister">
+ <span v-if="!loading">注 册</span>
+ <span v-else>注 册 中...</span>
+ </el-button>
+ <div style="float: right;">
+ <router-link class="link-type" :to="'/login'">使用已有账户登录</router-link>
+ </div>
+ </el-form-item>
+ </el-form>
+ <!-- 底部 -->
+ <div class="el-register-footer">
+ <span>Copyright © 2018-2023 疯狂的狮子Li All Rights Reserved.</span>
+ </div>
+ </div>
+</template>
+
+<script setup lang="ts">
+import { getCodeImg, register, getTenantList } from '@/api/login';
+import { RegisterForm, TenantVO } from '@/api/types';
+import { to } from 'await-to-js';
+
+const router = useRouter();
+
+const registerForm = ref<RegisterForm>({
+ tenantId: "",
+ username: "",
+ password: "",
+ confirmPassword: "",
+ code: "",
+ uuid: "",
+ userType: "sys_user"
+});
+
+// 租户开关
+const tenantEnabled = ref(true);
+
+
+const equalToPassword = (rule: any, value: string, callback: any) => {
+ if (registerForm.value.password !== value) {
+ callback(new Error("两次输入的密码不一致"));
+ } else {
+ callback();
+ }
+};
+
+const registerRules: ElFormRules = {
+ tenantId: [
+ { required: true, trigger: "blur", message: "请输入您的租户编号" }
+ ],
+ username: [
+ { required: true, trigger: "blur", message: "请输入您的账号" },
+ { min: 2, max: 20, message: "用户账号长度必须介于 2 和 20 之间", trigger: "blur" }
+ ],
+ password: [
+ { required: true, trigger: "blur", message: "请输入您的密码" },
+ { min: 5, max: 20, message: "用户密码长度必须介于 5 和 20 之间", trigger: "blur" }
+ ],
+ confirmPassword: [
+ { required: true, trigger: "blur", message: "请再次输入您的密码" },
+ { required: true, validator: equalToPassword, trigger: "blur" }
+ ],
+ code: [{ required: true, trigger: "change", message: "请输入验证码" }]
+};
+const codeUrl = ref("");
+const loading = ref(false);
+const captchaEnabled = ref(true);
+const registerRef = ref<ElFormInstance>();
+// 租户列表
+const tenantList = ref<TenantVO[]>([]);
+
+const handleRegister = () => {
+ registerRef.value?.validate(async (valid: boolean) => {
+ if (valid) {
+ loading.value = true;
+ const [err] = await to(register(registerForm.value));
+ if (!err) {
+ const username = registerForm.value.username;
+ await ElMessageBox.alert("<font color='red'>恭喜你,您的账号 " + username + " 注册成功!</font>", "系统提示", {
+ dangerouslyUseHTMLString: true,
+ type: "success",
+ });
+ await router.push("/login");
+ } else {
+ loading.value = false;
+ if (captchaEnabled) {
+ getCode();
+ }
+ }
+ }
+ });
+}
+
+const getCode = async () => {
+ const res = await getCodeImg();
+ const { data } = res;
+ captchaEnabled.value = data.captchaEnabled === undefined ? true : data.captchaEnabled;
+ if (captchaEnabled.value) {
+ codeUrl.value = 'data:image/gif;base64,' + data.img;
+ registerForm.value.uuid = data.uuid;
+ }
+};
+
+const initTenantList = async () => {
+ const { data } = await getTenantList();
+ tenantEnabled.value = data.tenantEnabled === undefined ? true : data.tenantEnabled;
+ if (tenantEnabled.value) {
+ tenantList.value = data.voList;
+ if (tenantList.value != null && tenantList.value.length !== 0) {
+ registerForm.value.tenantId = tenantList.value[0].tenantId;
+ }
+ }
+}
+
+onMounted(() => {
+ getCode();
+ initTenantList();
+})
+</script>
+
+<style lang="scss" scoped>
+.register {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 100%;
+ background-image: url("../assets/images/login-background.jpg");
+ background-size: cover;
+}
+
+.title {
+ margin: 0 auto 30px auto;
+ text-align: center;
+ color: #707070;
+}
+
+.register-form {
+ border-radius: 6px;
+ background: #ffffff;
+ width: 400px;
+ padding: 25px 25px 5px 25px;
+
+ .el-input {
+ height: 40px;
+
+ input {
+ height: 40px;
+ }
+ }
+
+ .input-icon {
+ height: 39px;
+ width: 14px;
+ margin-left: 0;
+ }
+}
+
+.register-tip {
+ font-size: 13px;
+ text-align: center;
+ color: #bfbfbf;
+}
+
+.register-code {
+ width: 33%;
+ height: 40px;
+ float: right;
+
+ img {
+ cursor: pointer;
+ vertical-align: middle;
+ }
+}
+
+.el-register-footer {
+ height: 40px;
+ line-height: 40px;
+ position: fixed;
+ bottom: 0;
+ width: 100%;
+ text-align: center;
+ color: #fff;
+ font-family: Arial, serif;
+ font-size: 12px;
+ letter-spacing: 1px;
+}
+
+.register-code-img {
+ height: 40px;
+ padding-left: 12px;
+}
+</style>
diff --git a/src/views/system/client/index.vue b/src/views/system/client/index.vue
new file mode 100644
index 0000000..b39b9e5
--- /dev/null
+++ b/src/views/system/client/index.vue
@@ -0,0 +1,324 @@
+<template>
+ <div class="p-2">
+ <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
+ <div class="search" v-show="showSearch">
+ <el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="100px">
+ <el-form-item label="客户端key" prop="clientKey">
+ <el-input v-model="queryParams.clientKey" placeholder="请输入客户端key" clearable style="width: 240px" @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item label="客户端秘钥" prop="clientSecret">
+ <el-input v-model="queryParams.clientSecret" placeholder="请输入客户端秘钥" clearable style="width: 240px" @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item label="状态" prop="status">
+ <el-select v-model="queryParams.status" placeholder="状态" clearable>
+ <el-option v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.label" :value="dict.value" />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+ <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+ </el-form-item>
+ </el-form>
+ </div>
+ </transition>
+
+ <el-card shadow="never">
+ <template #header>
+ <el-row :gutter="10" class="mb8">
+ <el-col :span="1.5">
+ <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['system:client:add']">新增</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['system:client:edit']">
+ 修改
+ </el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['system:client:remove']">
+ 删除
+ </el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['system:client:export']">导出</el-button>
+ </el-col>
+ <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
+ </el-row>
+ </template>
+
+ <el-table v-loading="loading" :data="clientList" @selection-change="handleSelectionChange">
+ <el-table-column type="selection" width="55" align="center" />
+ <el-table-column label="id" align="center" prop="id" v-if="true" />
+ <el-table-column label="客户端id" align="center" prop="clientId" />
+ <el-table-column label="客户端key" align="center" prop="clientKey" />
+ <el-table-column label="客户端秘钥" align="center" prop="clientSecret" />
+ <el-table-column label="授权类型" align="center">
+ <template #default="scope">
+ <dict-tag :options="sys_grant_type" :value="scope.row.grantTypeList" />
+ </template>
+ </el-table-column>
+ <el-table-column label="设备类型" align="center">
+ <template #default="scope">
+ <dict-tag :options="sys_device_type" :value="scope.row.deviceType" />
+ </template>
+ </el-table-column>
+ <el-table-column label="Token活跃超时时间" align="center" prop="activeTimeout" />
+ <el-table-column label="Token固定超时时间" align="center" prop="timeout" />
+ <el-table-column label="状态" align="center" key="status">
+ <template #default="scope">
+ <el-switch v-model="scope.row.status" active-value="0" inactive-value="1" @change="handleStatusChange(scope.row)"></el-switch>
+ </template>
+ </el-table-column>
+ <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+ <template #default="scope">
+ <el-tooltip content="修改" placement="top">
+ <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:client:edit']"></el-button>
+ </el-tooltip>
+ <el-tooltip content="删除" placement="top">
+ <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:client:remove']"></el-button>
+ </el-tooltip>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <pagination v-show="total>0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
+ </el-card>
+ <!-- 添加或修改客户端管理对话框 -->
+ <el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
+ <el-form ref="clientFormRef" :model="form" :rules="rules" label-width="100px">
+ <el-form-item label="客户端key" prop="clientKey">
+ <el-input v-model="form.clientKey" :disabled="form.id != null" placeholder="请输入客户端key" />
+ </el-form-item>
+ <el-form-item label="客户端秘钥" prop="clientSecret">
+ <el-input v-model="form.clientSecret" :disabled="form.id != null" placeholder="请输入客户端秘钥" />
+ </el-form-item>
+ <el-form-item label="授权类型" prop="grantTypeList">
+ <el-select v-model="form.grantTypeList" multiple placeholder="请输入授权类型">
+ <el-option v-for="dict in sys_grant_type" :key="dict.value" :label="dict.label" :value="dict.value"></el-option>
+ </el-select>
+ </el-form-item>
+ <el-form-item label="设备类型" prop="deviceType">
+ <el-select v-model="form.deviceType" placeholder="请输入设备类型">
+ <el-option v-for="dict in sys_device_type" :key="dict.value" :label="dict.label" :value="dict.value"></el-option>
+ </el-select>
+ </el-form-item>
+ <el-form-item prop="activeTimeout" label-width="auto">
+ <template #label>
+ <span>
+ <el-tooltip content="指定时间无操作则过期(单位:秒),默认30分钟(1800秒)" placement="top">
+ <el-icon><question-filled /></el-icon>
+ </el-tooltip>
+ Token活跃超时时间
+ </span>
+ </template>
+ <el-input v-model="form.activeTimeout" placeholder="请输入Token活跃超时时间" />
+ </el-form-item>
+ <el-form-item prop="timeout" label-width="auto">
+ <template #label>
+ <span>
+ <el-tooltip content="指定时间必定过期(单位:秒),默认七天(604800秒)" placement="top">
+ <el-icon><question-filled /></el-icon>
+ </el-tooltip>
+ Token固定超时时间
+ </span>
+ </template>
+ <el-input v-model="form.timeout" placeholder="请输入Token固定超时时间" />
+ </el-form-item>
+ <el-form-item label="状态">
+ <el-radio-group v-model="form.status">
+ <el-radio v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.value">
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
+ <el-button @click="cancel">取 消</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup name="Client" lang="ts">
+import { listClient, getClient, delClient, addClient, updateClient, changeStatus } from '@/api/system/client';
+import { ClientVO, ClientQuery, ClientForm } from '@/api/system/client/types';
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const { sys_normal_disable } = toRefs<any>(proxy?.useDict("sys_normal_disable"));
+const { sys_grant_type } = toRefs<any>(proxy?.useDict("sys_grant_type"));
+const { sys_device_type } = toRefs<any>(proxy?.useDict("sys_device_type"));
+
+const clientList = ref<ClientVO[]>([]);
+const buttonLoading = ref(false);
+const loading = ref(true);
+const showSearch = ref(true);
+const ids = ref<Array<string | number>>([]);
+const single = ref(true);
+const multiple = ref(true);
+const total = ref(0);
+
+const queryFormRef = ref<ElFormInstance>();
+const clientFormRef = ref<ElFormInstance>();
+
+const dialog = reactive<DialogOption>({
+ visible: false,
+ title: ''
+});
+
+const initFormData: ClientForm = {
+ id: undefined,
+ clientId: undefined,
+ clientKey: undefined,
+ clientSecret: undefined,
+ grantTypeList: undefined,
+ deviceType: undefined,
+ activeTimeout: undefined,
+ timeout: undefined,
+ status: undefined,
+}
+const data = reactive<PageData<ClientForm, ClientQuery>>({
+ form: {...initFormData},
+ queryParams: {
+ pageNum: 1,
+ pageSize: 10,
+ clientId: undefined,
+ clientKey: undefined,
+ clientSecret: undefined,
+ grantType: undefined,
+ deviceType: undefined,
+ activeTimeout: undefined,
+ timeout: undefined,
+ status: undefined,
+ },
+ rules: {
+ id: [
+ { required: true, message: "id不能为空", trigger: "blur" }
+ ],
+ clientId: [
+ { required: true, message: "客户端id不能为空", trigger: "blur" }
+ ],
+ clientKey: [
+ { required: true, message: "客户端key不能为空", trigger: "blur" }
+ ],
+ clientSecret: [
+ { required: true, message: "客户端秘钥不能为空", trigger: "blur" }
+ ],
+ grantTypeList: [
+ { required: true, message: "授权类型不能为空", trigger: "change" }
+ ],
+ deviceType: [
+ { required: true, message: "设备类型不能为空", trigger: "change" }
+ ],
+ }
+});
+
+const { queryParams, form, rules } = toRefs(data);
+
+/** 查询客户端管理列表 */
+const getList = async () => {
+ loading.value = true;
+ const res = await listClient(queryParams.value);
+ clientList.value = res.rows;
+ total.value = res.total;
+ loading.value = false;
+}
+
+/** 取消按钮 */
+const cancel = () => {
+ reset();
+ dialog.visible = false;
+}
+
+/** 表单重置 */
+const reset = () => {
+ form.value = {...initFormData};
+ clientFormRef.value?.resetFields();
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+ queryParams.value.pageNum = 1;
+ getList();
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+ queryFormRef.value?.resetFields();
+ handleQuery();
+}
+
+/** 多选框选中数据 */
+const handleSelectionChange = (selection: ClientVO[]) => {
+ ids.value = selection.map(item => item.id);
+ single.value = selection.length != 1;
+ multiple.value = !selection.length;
+}
+
+/** 新增按钮操作 */
+const handleAdd = () => {
+ reset();
+ dialog.visible = true;
+ dialog.title = "添加客户端管理";
+}
+
+/** 修改按钮操作 */
+const handleUpdate = async (row?: ClientVO) => {
+ reset();
+ const _id = row?.id || ids.value[0]
+ const res = await getClient(_id);
+ Object.assign(form.value, res.data);
+ dialog.visible = true;
+ dialog.title = "修改客户端管理";
+}
+
+/** 提交按钮 */
+const submitForm = () => {
+ clientFormRef.value?.validate(async (valid: boolean) => {
+ if (valid) {
+ buttonLoading.value = true;
+ if (form.value.id) {
+ await updateClient(form.value).finally(() => buttonLoading.value = false);
+ } else {
+ await addClient(form.value).finally(() => buttonLoading.value = false);
+ }
+ proxy?.$modal.msgSuccess("修改成功");
+ dialog.visible = false;
+ await getList();
+ }
+ });
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (row?: ClientVO) => {
+ const _ids = row?.id || ids.value;
+ await proxy?.$modal.confirm('是否确认删除客户端管理编号为"' + _ids + '"的数据项?').finally(() => loading.value = false);
+ await delClient(_ids);
+ proxy?.$modal.msgSuccess("删除成功");
+ await getList();
+}
+
+/** 导出按钮操作 */
+const handleExport = () => {
+ proxy?.download('system/client/export', {
+ ...queryParams.value
+ }, `client_${new Date().getTime()}.xlsx`)
+}
+
+/** 状态修改 */
+const handleStatusChange = async (row: ClientVO) => {
+ let text = row.status === "0" ? "启用" : "停用"
+ try {
+ await proxy?.$modal.confirm('确认要"' + text + '"吗?');
+ await changeStatus(row.id, row.status);
+ proxy?.$modal.msgSuccess(text + "成功");
+ } catch (err) {
+ row.status = row.status === "0" ? "1" : "0";
+ }
+}
+
+onMounted(() => {
+ getList();
+});
+</script>
diff --git a/src/views/system/dept/index.vue b/src/views/system/dept/index.vue
new file mode 100644
index 0000000..9797125
--- /dev/null
+++ b/src/views/system/dept/index.vue
@@ -0,0 +1,309 @@
+<template>
+ <div class="p-2">
+ <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
+ <div class="mb-[10px]" v-show="showSearch">
+ <el-card shadow="hover">
+ <el-form ref="queryFormRef" :model="queryParams" :inline="true" label-width="68px">
+ <el-form-item label="部门名称" prop="deptName">
+ <el-input v-model="queryParams.deptName" placeholder="请输入部门名称" clearable style="width: 240px" @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item label="状态" prop="status">
+ <el-select v-model="queryParams.status" placeholder="部门状态" clearable>
+ <el-option v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.label" :value="dict.value" />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+ <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+ </el-form-item>
+ </el-form>
+ </el-card>
+ </div>
+ </transition>
+
+ <el-card shadow="hover">
+ <template #header>
+ <el-row :gutter="10">
+ <el-col :span="1.5">
+ <el-button type="primary" plain icon="Plus" @click="handleAdd()" v-hasPermi="['system:dept:add']">新增 </el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button type="info" plain icon="Sort" @click="handleToggleExpandAll">展开/折叠</el-button>
+ </el-col>
+ <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
+ </el-row>
+ </template>
+
+ <el-table
+ v-loading="loading"
+ :data="deptList"
+ row-key="deptId"
+ :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
+ ref="deptTableRef"
+ :default-expand-all="isExpandAll"
+ >
+ <el-table-column prop="deptName" label="部门名称" width="260"></el-table-column>
+ <el-table-column prop="orderNum" align="center" label="排序" width="200"></el-table-column>
+ <el-table-column prop="status" align="center" label="状态" width="100">
+ <template #default="scope">
+ <dict-tag :options="sys_normal_disable" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column label="创建时间" align="center" prop="createTime" width="200">
+ <template #default="scope">
+ <span>{{ parseTime(scope.row.createTime) }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column fixed="right" align="center" label="操作">
+ <template #default="scope">
+ <el-tooltip content="修改" placement="top">
+ <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:dept:edit']" />
+ </el-tooltip>
+ <el-tooltip content="新增" placement="top">
+ <el-button link type="primary" icon="Plus" @click="handleAdd(scope.row)" v-hasPermi="['system:dept:add']" />
+ </el-tooltip>
+ <el-tooltip content="删除" placement="top">
+ <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:dept:remove']" />
+ </el-tooltip>
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-card>
+
+ <el-dialog :title="dialog.title" v-model="dialog.visible" destroy-on-close append-to-body width="600px">
+ <el-form ref="deptFormRef" :model="form" :rules="rules" label-width="80px">
+ <el-row>
+ <el-col :span="24" v-if="form.parentId !== 0">
+ <el-form-item label="上级部门" prop="parentId">
+ <el-tree-select
+ v-model="form.parentId"
+ :data="deptOptions"
+ :props="{ value: 'deptId', label: 'deptName', children: 'children' }"
+ value-key="deptId"
+ placeholder="选择上级部门"
+ check-strictly
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="部门名称" prop="deptName">
+ <el-input v-model="form.deptName" placeholder="请输入部门名称" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="显示排序" prop="orderNum">
+ <el-input-number v-model="form.orderNum" controls-position="right" :min="0" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="负责人" prop="leader">
+ <el-select v-model="form.leader" placeholder="请选择负责人">
+ <el-option v-for="item in deptUserList" :key="item.userId" :label="item.userName" :value="item.userId" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="联系电话" prop="phone">
+ <el-input v-model="form.phone" placeholder="请输入联系电话" maxlength="11" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="邮箱" prop="email">
+ <el-input v-model="form.email" placeholder="请输入邮箱" maxlength="50" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="部门状态">
+ <el-radio-group v-model="form.status">
+ <el-radio v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.value">{{ dict.label
+ }}</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitForm">确 定</el-button>
+ <el-button @click="cancel">取 消</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup name="Dept" lang="ts">
+import { listDept, getDept, delDept, addDept, updateDept, listDeptExcludeChild } from "@/api/system/dept"
+import { DeptForm, DeptQuery, DeptVO } from "@/api/system/dept/types";
+import {UserVO} from "@/api/system/user/types";
+import {listUserByDeptId} from "@/api/system/user";
+
+interface DeptOptionsType {
+ deptId: number | string;
+ deptName: string;
+ children: DeptOptionsType[];
+
+}
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance
+const { sys_normal_disable } = toRefs<any>(proxy?.useDict("sys_normal_disable"));
+
+const deptList = ref<DeptVO[]>([])
+const loading = ref(true)
+const showSearch = ref(true)
+const deptOptions = ref<DeptOptionsType[]>([])
+const isExpandAll = ref(true)
+const deptUserList = ref<UserVO[]>([]);
+
+const dialog = reactive<DialogOption>({
+ visible: false,
+ title: ''
+});
+
+const deptTableRef = ref<ElTableInstance>();
+const queryFormRef = ref<ElFormInstance>();
+const deptFormRef = ref<ElFormInstance>();
+
+const initFormData: DeptForm = {
+ deptId: undefined,
+ parentId: undefined,
+ deptName: undefined,
+ orderNum: 0,
+ leader: undefined,
+ phone: undefined,
+ email: undefined,
+ status: "0"
+}
+const data = reactive<PageData<DeptForm, DeptQuery>>({
+ form: { ...initFormData },
+ queryParams: {
+ pageNum: 1,
+ pageSize: 10,
+ deptName: undefined,
+ status: undefined
+ },
+ rules: {
+ parentId: [{ required: true, message: "上级部门不能为空", trigger: "blur" }],
+ deptName: [{ required: true, message: "部门名称不能为空", trigger: "blur" }],
+ orderNum: [{ required: true, message: "显示排序不能为空", trigger: "blur" }],
+ email: [{ type: "email", message: "请输入正确的邮箱地址", trigger: ["blur", "change"] }],
+ phone: [{ pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, message: "请输入正确的手机号码", trigger: "blur" }]
+ },
+})
+
+const { queryParams, form, rules } = toRefs<PageData<DeptForm, DeptQuery>>(data)
+
+/** 查询菜单列表 */
+const getList = async () => {
+ loading.value = true;
+ const res = await listDept(queryParams.value);
+ const data = proxy?.handleTree<DeptVO>(res.data, "deptId")
+ if (data) {
+ deptList.value = data
+ }
+ loading.value = false
+}
+
+/** 查询当前部门的所有用户 */
+async function getDeptAllUser(deptId: any) {
+ if (deptId !== null && deptId !== "" && deptId !== undefined) {
+ const res = await listUserByDeptId(deptId);
+ deptUserList.value = res.data;
+ }
+}
+
+/** 取消按钮 */
+const cancel = () => {
+ reset()
+ dialog.visible = false
+}
+/** 表单重置 */
+const reset = () => {
+ form.value = { ...initFormData };
+ deptFormRef.value?.resetFields();
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+ getList();
+}
+/** 重置按钮操作 */
+const resetQuery = () => {
+ queryFormRef.value?.resetFields();
+ handleQuery()
+}
+
+/** 展开/折叠操作 */
+const handleToggleExpandAll = () => {
+ isExpandAll.value = !isExpandAll.value;
+ toggleExpandAll(deptList.value, isExpandAll.value)
+}
+/** 展开/折叠所有 */
+const toggleExpandAll = (data: DeptVO[], status: boolean) => {
+ data.forEach((item) => {
+ deptTableRef.value?.toggleRowExpansion(item, status)
+ if (item.children && item.children.length > 0) toggleExpandAll(item.children, status)
+ })
+}
+
+/** 新增按钮操作 */
+const handleAdd = async (row?: DeptVO) => {
+ reset();
+ const res = await listDept();
+ const data = proxy?.handleTree<DeptOptionsType>(res.data, "deptId");
+ if (data) {
+ deptOptions.value = data
+ if (row && row.deptId) {
+ form.value.parentId = row?.deptId;
+ }
+ dialog.visible = true;
+ dialog.title = "添加部门";
+ }
+}
+
+/** 修改按钮操作 */
+const handleUpdate = async (row: DeptVO) => {
+ reset();
+ //查询当前部门所有用户
+ getDeptAllUser(row.deptId);
+ const res = await getDept(row.deptId);
+ form.value = res.data
+ const response = await listDeptExcludeChild(row.deptId);
+ const data = proxy?.handleTree<DeptOptionsType>(response.data, "deptId")
+ if (data) {
+ deptOptions.value = data;
+ if (data.length === 0) {
+ const noResultsOptions: DeptOptionsType = {
+ deptId: res.data.parentId,
+ deptName: res.data.parentName,
+ children: []
+ };
+ deptOptions.value.push(noResultsOptions);
+ }
+ }
+ dialog.visible = true;
+ dialog.title = "修改部门";
+}
+/** 提交按钮 */
+const submitForm = () => {
+ deptFormRef.value?.validate(async (valid: boolean) => {
+ if (valid) {
+ form.value.deptId ? await updateDept(form.value) : await addDept(form.value);
+ proxy?.$modal.msgSuccess("操作成功");
+ dialog.visible = false;
+ await getList();
+ }
+ })
+}
+/** 删除按钮操作 */
+const handleDelete = async (row: DeptVO) => {
+ await proxy?.$modal.confirm('是否确认删除名称为"' + row.deptName + '"的数据项?');
+ await delDept(row.deptId);
+ await getList();
+ proxy?.$modal.msgSuccess("删除成功");
+}
+
+onMounted(() => {
+ getList();
+});
+</script>
diff --git a/src/views/system/dict/data.vue b/src/views/system/dict/data.vue
new file mode 100644
index 0000000..c2ef550
--- /dev/null
+++ b/src/views/system/dict/data.vue
@@ -0,0 +1,289 @@
+<template>
+ <div class="p-2">
+ <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
+ <div class="mb-[10px]" v-show="showSearch">
+ <el-card shadow="hover">
+ <el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px">
+ <el-form-item label="字典名称" prop="dictType">
+ <el-select v-model="queryParams.dictType" style="width: 200px">
+ <el-option v-for="item in typeOptions" :key="item.dictId" :label="item.dictName" :value="item.dictType" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="字典标签" prop="dictLabel">
+ <el-input v-model="queryParams.dictLabel" placeholder="请输入字典标签" clearable style="width: 200px" @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+ <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+ </el-form-item>
+ </el-form>
+ </el-card>
+ </div>
+ </transition>
+ <el-card shadow="hover">
+ <template #header>
+ <el-row :gutter="10" class="mb8">
+ <el-col :span="1.5">
+ <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['system:dict:add']">新增</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['system:dict:edit']">修改</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['system:dict:remove']">
+ 删除
+ </el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['system:dict:export']">导出</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button type="warning" plain icon="Close" @click="handleClose">关闭</el-button>
+ </el-col>
+ <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
+ </el-row>
+ </template>
+
+ <el-table v-loading="loading" :data="dataList" @selection-change="handleSelectionChange">
+ <el-table-column type="selection" width="55" align="center" />
+ <el-table-column label="字典编码" align="center" prop="dictCode" v-if="false" />
+ <el-table-column label="字典标签" align="center" prop="dictLabel">
+ <template #default="scope">
+ <span v-if="(scope.row.listClass === '' || scope.row.listClass === 'default') && (scope.row.cssClass === '' || scope.row.cssClass == null)">{{ scope.row.dictLabel }}</span>
+ <el-tag v-else :type="(scope.row.listClass === 'primary' || scope.row.listClass === 'default') ? '' : scope.row.listClass" :class="scope.row.cssClass">{{ scope.row.dictLabel }}</el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="字典键值" align="center" prop="dictValue" />
+ <el-table-column label="字典排序" align="center" prop="dictSort" />
+ <el-table-column label="备注" align="center" prop="remark" :show-overflow-tooltip="true" />
+ <el-table-column label="创建时间" align="center" prop="createTime" width="180">
+ <template #default="scope">
+ <span>{{ parseTime(scope.row.createTime) }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="操作" align="center" width="160" class-name="small-padding fixed-width">
+ <template #default="scope">
+ <el-tooltip content="修改" placement="top">
+ <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:dict:edit']"></el-button>
+ </el-tooltip>
+ <el-tooltip content="删除" placement="top">
+ <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:dict:remove']"></el-button>
+ </el-tooltip>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
+ </el-card>
+ <!-- 添加或修改参数配置对话框 -->
+ <el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
+ <el-form ref="dataFormRef" :model="form" :rules="rules" label-width="80px">
+ <el-form-item label="字典类型">
+ <el-input v-model="form.dictType" :disabled="true" />
+ </el-form-item>
+ <el-form-item label="数据标签" prop="dictLabel">
+ <el-input v-model="form.dictLabel" placeholder="请输入数据标签" />
+ </el-form-item>
+ <el-form-item label="数据键值" prop="dictValue">
+ <el-input v-model="form.dictValue" placeholder="请输入数据键值" />
+ </el-form-item>
+ <el-form-item label="样式属性" prop="cssClass">
+ <el-input v-model="form.cssClass" placeholder="请输入样式属性" />
+ </el-form-item>
+ <el-form-item label="显示排序" prop="dictSort">
+ <el-input-number v-model="form.dictSort" controls-position="right" :min="0" />
+ </el-form-item>
+ <el-form-item label="回显样式" prop="listClass">
+ <el-select v-model="form.listClass">
+ <el-option
+ v-for="item in listClassOptions"
+ :key="item.value"
+ :label="item.label + '(' + item.value + ')'"
+ :value="item.value"
+ ></el-option>
+ </el-select>
+ </el-form-item>
+ <el-form-item label="备注" prop="remark">
+ <el-input v-model="form.remark" type="textarea" placeholder="请输入内容"></el-input>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitForm">确 定</el-button>
+ <el-button @click="cancel">取 消</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup name="Data" lang="ts">
+import useDictStore from '@/store/modules/dict'
+import { optionselect as getDictOptionselect, getType } from "@/api/system/dict/type";
+import { listData, getData, delData, addData, updateData } from "@/api/system/dict/data";
+import { DictTypeVO } from '@/api/system/dict/type/types';
+import { DictDataForm, DictDataQuery, DictDataVO } from "@/api/system/dict/data/types";
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance
+const route = useRoute();
+
+const dataList = ref<DictDataVO[]>([]);
+const loading = ref(true);
+const showSearch = ref(true);
+const ids = ref<Array<string | number>>([]);
+const single = ref(true);
+const multiple = ref(true);
+const total = ref(0);
+const defaultDictType = ref("");
+const typeOptions = ref<DictTypeVO[]>([]);
+
+const dataFormRef = ref<ElFormInstance>();
+const queryFormRef = ref<ElFormInstance>();
+
+
+const dialog = reactive<DialogOption>({
+ visible: false,
+ title: ''
+});
+
+// 数据标签回显样式
+const listClassOptions = ref<Array<{ value: string, label: string }>>([
+ { value: "default", label: "默认" },
+ { value: "primary", label: "主要" },
+ { value: "success", label: "成功" },
+ { value: "info", label: "信息" },
+ { value: "warning", label: "警告" },
+ { value: "danger", label: "危险" }
+]);
+
+const initFormData: DictDataForm = {
+ dictCode: undefined,
+ dictLabel: '',
+ dictValue: '',
+ cssClass: '',
+ listClass: "default",
+ dictSort: 0,
+ remark: ''
+}
+const data = reactive<PageData<DictDataForm, DictDataQuery>>({
+ form: { ...initFormData },
+ queryParams: {
+ pageNum: 1,
+ pageSize: 10,
+ dictName: '',
+ dictType: '',
+ dictLabel: ''
+ },
+ rules: {
+ dictLabel: [{ required: true, message: "数据标签不能为空", trigger: "blur" }],
+ dictValue: [{ required: true, message: "数据键值不能为空", trigger: "blur" }],
+ dictSort: [{ required: true, message: "数据顺序不能为空", trigger: "blur" }]
+ }
+});
+
+const { queryParams, form, rules } = toRefs(data);
+
+/** 查询字典类型详细 */
+const getTypes = async (dictId: string | number) => {
+ const { data } = await getType(dictId);
+ queryParams.value.dictType = data.dictType;
+ defaultDictType.value = data.dictType;
+ getList();
+}
+
+/** 查询字典类型列表 */
+const getTypeList = async () => {
+ const res = await getDictOptionselect()
+ typeOptions.value = res.data;
+}
+/** 查询字典数据列表 */
+const getList = async () => {
+ loading.value = true;
+ const res = await listData(queryParams.value);
+ dataList.value = res.rows;
+ total.value = res.total;
+ loading.value = false;
+}
+/** 取消按钮 */
+const cancel = () => {
+ dialog.visible = false;
+ reset();
+}
+/** 表单重置 */
+const reset = () => {
+ form.value = { ...initFormData };
+ dataFormRef.value?.resetFields();
+}
+/** 搜索按钮操作 */
+const handleQuery = () => {
+ queryParams.value.pageNum = 1;
+ getList();
+}
+/** 返回按钮操作 */
+const handleClose = () => {
+ const obj = { path: "/system/dict" };
+ proxy?.$tab.closeOpenPage(obj);
+}
+/** 重置按钮操作 */
+const resetQuery = () => {
+ queryFormRef.value?.resetFields();
+ queryParams.value.dictType = defaultDictType.value;
+ handleQuery();
+}
+/** 新增按钮操作 */
+const handleAdd = () => {
+ reset();
+ form.value.dictType = queryParams.value.dictType;
+ dialog.visible = true;
+ dialog.title = "添加字典数据";
+}
+/** 多选框选中数据 */
+const handleSelectionChange = (selection: DictDataVO[]) => {
+ ids.value = selection.map(item => item.dictCode);
+ single.value = selection.length != 1;
+ multiple.value = !selection.length;
+}
+/** 修改按钮操作 */
+const handleUpdate = async (row?: DictDataVO) => {
+ reset();
+ const dictCode = row?.dictCode || ids.value[0];
+ const res = await getData(dictCode);
+ Object.assign(form.value, res.data);
+ dialog.visible = true;
+ dialog.title = "修改字典数据";
+}
+/** 提交按钮 */
+const submitForm = () => {
+ dataFormRef.value?.validate(async (valid: boolean) => {
+ if (valid) {
+ form.value.dictCode ? await updateData(form.value) : await addData(form.value);
+ useDictStore().removeDict(queryParams.value.dictType);
+ proxy?.$modal.msgSuccess("操作成功");
+ dialog.visible = false;
+ await getList();
+
+ }
+ });
+}
+/** 删除按钮操作 */
+const handleDelete = async (row?: DictDataVO) => {
+ const dictCodes = row?.dictCode || ids.value;
+ await proxy?.$modal.confirm('是否确认删除字典编码为"' + dictCodes + '"的数据项?');
+ await delData(dictCodes);
+ await getList();
+ proxy?.$modal.msgSuccess("删除成功");
+ useDictStore().removeDict(queryParams.value.dictType);
+
+}
+/** 导出按钮操作 */
+const handleExport = () => {
+ proxy?.download("system/dict/data/export", {
+ ...queryParams.value
+ }, `dict_data_${new Date().getTime()}.xlsx`);
+}
+
+onMounted(() => {
+ getTypes(route.params && route.params.dictId as string);
+ getTypeList();
+})
+</script>
diff --git a/src/views/system/dict/index.vue b/src/views/system/dict/index.vue
new file mode 100644
index 0000000..f3b0051
--- /dev/null
+++ b/src/views/system/dict/index.vue
@@ -0,0 +1,243 @@
+<template>
+ <div class="p-2">
+ <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
+ <div class="mb-[10px]" v-show="showSearch">
+ <el-card shadow="hover">
+ <el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px">
+ <el-form-item label="字典名称" prop="dictName">
+ <el-input v-model="queryParams.dictName" placeholder="请输入字典名称" clearable style="width: 240px" @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item label="字典类型" prop="dictType">
+ <el-input v-model="queryParams.dictType" placeholder="请输入字典类型" clearable style="width: 240px" @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item label="创建时间" style="width: 308px">
+ <el-date-picker
+ v-model="dateRange"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ range-separator="-"
+ start-placeholder="开始日期"
+ end-placeholder="结束日期"
+ :default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
+ ></el-date-picker>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+ <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+ </el-form-item>
+ </el-form>
+ </el-card>
+ </div>
+ </transition>
+ <el-card shadow="hover">
+ <template #header>
+ <el-row :gutter="10" class="mb8">
+ <el-col :span="1.5">
+ <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['system:dict:add']">新增</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['system:dict:edit']">修改</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['system:dict:remove']">
+ 删除
+ </el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['system:dict:export']">导出</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button type="danger" plain icon="Refresh" @click="handleRefreshCache" v-hasPermi="['system:dict:remove']">刷新缓存</el-button>
+ </el-col>
+ <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
+ </el-row>
+ </template>
+
+ <el-table v-loading="loading" :data="typeList" @selection-change="handleSelectionChange">
+ <el-table-column type="selection" width="55" align="center" />
+ <el-table-column label="字典编号" align="center" prop="dictId" v-if="false" />
+ <el-table-column label="字典名称" align="center" prop="dictName" :show-overflow-tooltip="true" />
+ <el-table-column label="字典类型" align="center" :show-overflow-tooltip="true">
+ <template #default="scope">
+ <router-link :to="'/system/dict-data/index/' + scope.row.dictId" class="link-type">
+ <span>{{ scope.row.dictType }}</span>
+ </router-link>
+ </template>
+ </el-table-column>
+ <el-table-column label="备注" align="center" prop="remark" :show-overflow-tooltip="true" />
+ <el-table-column label="创建时间" align="center" prop="createTime" width="180">
+ <template #default="scope">
+ <span>{{ parseTime(scope.row.createTime) }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="操作" align="center" width="160" class-name="small-padding fixed-width">
+ <template #default="scope">
+ <el-tooltip content="修改" placement="top">
+ <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:dict:edit']"></el-button>
+ </el-tooltip>
+ <el-tooltip content="删除" placement="top">
+ <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:dict:remove']"></el-button>
+ </el-tooltip>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
+ </el-card>
+ <!-- 添加或修改参数配置对话框 -->
+ <el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
+ <el-form ref="dictFormRef" :model="form" :rules="rules" label-width="80px">
+ <el-form-item label="字典名称" prop="dictName">
+ <el-input v-model="form.dictName" placeholder="请输入字典名称" />
+ </el-form-item>
+ <el-form-item label="字典类型" prop="dictType">
+ <el-input v-model="form.dictType" placeholder="请输入字典类型" />
+ </el-form-item>
+ <el-form-item label="备注" prop="remark">
+ <el-input v-model="form.remark" type="textarea" placeholder="请输入内容"></el-input>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitForm">确 定</el-button>
+ <el-button @click="cancel">取 消</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup name="Dict" lang="ts">
+import useDictStore from '@/store/modules/dict'
+import { listType, getType, delType, addType, updateType, refreshCache } from "@/api/system/dict/type";
+import { DictTypeForm, DictTypeQuery, DictTypeVO } from "@/api/system/dict/type/types";
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+
+const typeList = ref<DictTypeVO[]>([]);
+const loading = ref(true);
+const showSearch = ref(true);
+const ids = ref<Array<number | string>>([]);
+const single = ref(true);
+const multiple = ref(true);
+const total = ref(0);
+const dateRange = ref<[DateModelType, DateModelType]>(['', '']);
+
+const dictFormRef = ref<ElFormInstance>();
+const queryFormRef = ref<ElFormInstance>();
+
+
+const dialog = reactive<DialogOption>({
+ visible: false,
+ title: ''
+});
+
+const initFormData: DictTypeForm = {
+ dictId: undefined,
+ dictName: '',
+ dictType: '',
+ remark: ''
+}
+const data = reactive<PageData<DictTypeForm, DictTypeQuery>>({
+ form: { ...initFormData },
+ queryParams: {
+ pageNum: 1,
+ pageSize: 10,
+ dictName: '',
+ dictType: ''
+ },
+ rules: {
+ dictName: [{ required: true, message: "字典名称不能为空", trigger: "blur" }],
+ dictType: [{ required: true, message: "字典类型不能为空", trigger: "blur" }]
+ },
+});
+
+const { queryParams, form, rules } = toRefs(data);
+
+/** 查询字典类型列表 */
+const getList = () => {
+ loading.value = true;
+ listType(proxy?.addDateRange(queryParams.value, dateRange.value)).then(res => {
+ typeList.value = res.rows;
+ total.value = res.total;
+ loading.value = false;
+ });
+}
+/** 取消按钮 */
+const cancel = () => {
+ reset();
+ dialog.visible = false;
+}
+/** 表单重置 */
+const reset = () => {
+ form.value = { ...initFormData };
+ dictFormRef.value?.resetFields();
+}
+/** 搜索按钮操作 */
+const handleQuery = () => {
+ queryParams.value.pageNum = 1;
+ getList();
+}
+/** 重置按钮操作 */
+const resetQuery = () => {
+ dateRange.value = ['', ''];
+ queryFormRef.value?.resetFields();
+ handleQuery();
+}
+/** 新增按钮操作 */
+const handleAdd = () => {
+ reset();
+ dialog.visible = true;
+ dialog.title = "添加字典类型";
+}
+/** 多选框选中数据 */
+const handleSelectionChange = (selection: DictTypeVO[]) => {
+ ids.value = selection.map(item => item.dictId);
+ single.value = selection.length != 1;
+ multiple.value = !selection.length;
+}
+/** 修改按钮操作 */
+const handleUpdate = async (row?: DictTypeVO) => {
+ reset();
+ const dictId = row?.dictId || ids.value[0];
+ const res = await getType(dictId);
+ Object.assign(form.value, res.data);
+ dialog.visible = true;
+ dialog.title = "修改字典类型";
+}
+/** 提交按钮 */
+const submitForm = () => {
+ dictFormRef.value?.validate(async (valid: boolean) => {
+ if (valid) {
+ form.value.dictId ? await updateType(form.value) : await addType(form.value);
+ proxy?.$modal.msgSuccess("操作成功");
+ dialog.visible = false;
+ getList();
+ }
+ });
+}
+/** 删除按钮操作 */
+const handleDelete = async (row?: DictTypeVO) => {
+ const dictIds = row?.dictId || ids.value;
+ await proxy?.$modal.confirm('是否确认删除字典编号为"' + dictIds + '"的数据项?');
+ await delType(dictIds);
+ getList();
+ proxy?.$modal.msgSuccess("删除成功");
+}
+/** 导出按钮操作 */
+const handleExport = () => {
+ proxy?.download("system/dict/type/export", {
+ ...queryParams.value
+ }, `dict_${new Date().getTime()}.xlsx`);
+}
+/** 刷新缓存按钮操作 */
+const handleRefreshCache = async () => {
+ await refreshCache();
+ proxy?.$modal.msgSuccess("刷新成功");
+ useDictStore().cleanDict();
+}
+
+onMounted(() => {
+ getList();
+})
+</script>
diff --git a/src/views/system/goodsIn/index.vue b/src/views/system/goodsIn/index.vue
new file mode 100644
index 0000000..a2d7bd0
--- /dev/null
+++ b/src/views/system/goodsIn/index.vue
@@ -0,0 +1,323 @@
+<template>
+ <div class="p-2">
+ <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
+ <div class="search" v-show="showSearch">
+ <el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px">
+ <el-form-item label="柜号" prop="cabinetCode">
+ <el-input v-model="queryParams.cabinetCode" placeholder="请输入柜号" clearable style="width: 240px" @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item label="门号" prop="doorCode">
+ <el-input v-model="queryParams.doorCode" placeholder="请输入门号" clearable style="width: 240px" @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item label="物品名称" prop="goodsName">
+ <el-input v-model="queryParams.goodsName" placeholder="请输入物品名称" clearable style="width: 240px" @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item label="物品编码" prop="goodsCode">
+ <el-input v-model="queryParams.goodsCode" placeholder="请输入物品编码" clearable style="width: 240px" @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item label="入库数量" prop="inAmount">
+ <el-input v-model="queryParams.inAmount" placeholder="请输入入库数量" clearable style="width: 240px" @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item label="库存数量" prop="repAmount">
+ <el-input v-model="queryParams.repAmount" placeholder="请输入库存数量" clearable style="width: 240px" @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item label="入库时间" prop="inTime">
+ <el-date-picker clearable
+ v-model="queryParams.inTime"
+ type="date"
+ value-format="YYYY-MM-DD"
+ placeholder="请选择入库时间"
+ />
+ </el-form-item>
+ <el-form-item label="入库人" prop="inOperator">
+ <el-input v-model="queryParams.inOperator" placeholder="请输入入库人" clearable style="width: 240px" @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+ <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+ </el-form-item>
+ </el-form>
+ </div>
+ </transition>
+
+ <el-card shadow="never">
+ <template #header>
+ <el-row :gutter="10" class="mb8">
+ <el-col :span="1.5">
+ <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['system:goodsIn:add']">新增</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['system:goodsIn:edit']">修改</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['system:goodsIn:remove']">删除</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['system:goodsIn:export']">导出</el-button>
+ </el-col>
+ <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
+ </el-row>
+ </template>
+
+ <el-table v-loading="loading" :data="goodsInList" @selection-change="handleSelectionChange">
+ <el-table-column type="selection" width="55" align="center" />
+ <el-table-column label="入库登记id" align="center" prop="inId" v-if="true" />
+ <el-table-column label="柜号" align="center" prop="cabinetCode" />
+ <el-table-column label="门号" align="center" prop="doorCode" />
+ <el-table-column label="物品名称" align="center" prop="goodsName" />
+ <el-table-column label="物品编码" align="center" prop="goodsCode" />
+ <el-table-column label="入库数量" align="center" prop="inAmount" />
+ <el-table-column label="库存数量" align="center" prop="repAmount" />
+ <el-table-column label="入库时间" align="center" prop="inTime" width="180">
+ <template #default="scope">
+ <span>{{ parseTime(scope.row.inTime, '{y}-{m}-{d}') }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="入库人" align="center" prop="inOperator" />
+ <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+ <template #default="scope">
+ <el-tooltip content="修改" placement="top">
+ <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:goodsIn:edit']"></el-button>
+ </el-tooltip>
+ <el-tooltip content="删除" placement="top">
+ <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:goodsIn:remove']"></el-button>
+ </el-tooltip>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <pagination
+ v-show="total>0"
+ :total="total"
+ v-model:page="queryParams.pageNum"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </el-card>
+ <!-- 添加或修改入库登记对话框 -->
+ <el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
+ <el-form ref="goodsInFormRef" :model="form" :rules="goodsInRules" label-width="80px">
+ <el-form-item label="柜号" prop="cabinetCode">
+ <el-input v-model="form.cabinetCode" placeholder="请输入柜号" />
+ </el-form-item>
+ <el-form-item label="门号" prop="doorCode">
+ <el-input v-model="form.doorCode" placeholder="请输入门号" />
+ </el-form-item>
+ <el-form-item label="物品名称" prop="goodsName">
+ <el-input v-model="form.goodsName" placeholder="请输入物品名称" />
+ </el-form-item>
+ <el-form-item label="物品编码" prop="goodsCode">
+ <el-input v-model="form.goodsCode" placeholder="请输入物品编码" />
+ </el-form-item>
+ <el-form-item label="入库数量" prop="inAmount">
+ <el-input v-model="form.inAmount" placeholder="请输入入库数量" />
+ </el-form-item>
+ <el-form-item label="库存数量" prop="repAmount">
+ <el-input v-model="form.repAmount" placeholder="请输入库存数量" />
+ </el-form-item>
+ <el-form-item label="入库时间" prop="inTime">
+ <el-date-picker clearable
+ v-model="form.inTime"
+ type="datetime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ placeholder="请选择入库时间">
+ </el-date-picker>
+ </el-form-item>
+ <el-form-item label="入库人" prop="inOperator">
+ <el-input v-model="form.inOperator" placeholder="请输入入库人" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
+ <el-button @click="cancel">取 消</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup name="GoodsIn" lang="ts">
+import { listGoodsIn, getGoodsIn, delGoodsIn, addGoodsIn, updateGoodsIn } from '@/api/system/goodsIn';
+import { GoodsInVO, GoodsInQuery, GoodsInForm } from '@/api/system/goodsIn/types';
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+
+// 选中的ids
+const ids = ref<Array<string | number>>([]);
+// 单选
+const single = ref(true);
+//多选
+const multiple = ref(true);
+//总纪录数
+const total = ref(0);
+// 显示记录数
+const goodsInList = ref<GoodsInVO[]>([]);
+// 是否显示查询条件
+const showSearch = ref(true);
+// 是否加遮罩层
+const loading = ref(true);
+// 查询窗体 Ref
+const queryFormRef = ref<ElFormInstance>();
+// 数据录入窗体 Ref
+const goodsInFormRef = ref<ElFormInstance>();
+// 对话框 弹出增加、修改
+const dialog = reactive<DialogOption>({
+ visible: false,
+ title: ''
+});
+// 数据录入窗体初始化
+const initFormData: GoodsInForm = {
+ inId: '',
+ cabinetCode: '',
+ doorCode: '',
+ goodsName: '',
+ goodsCode: '',
+ inAmount: 0,
+ repAmount: 0,
+ inTime: '',
+ inOperator: '',
+}
+// 查询参数
+const queryParams = reactive<GoodsInQuery>({
+ pageNum: 1,
+ pageSize: 10,
+ cabinetCode: '',
+ doorCode: '',
+ goodsName: '',
+ goodsCode: '',
+ inAmount: undefined,
+ repAmount: undefined,
+ inTime: '',
+ inOperator: '',
+ params: {}
+})
+// 窗体输入验证规则
+const goodsInRules = {
+ inId: [
+ {required: true, message: "入库登记id不能为空", trigger: "blur"}
+ ],
+ cabinetCode: [
+ {required: true, message: "柜号不能为空", trigger: "blur"}
+ ],
+ doorCode: [
+ {required: true, message: "门号不能为空", trigger: "blur"}
+ ],
+ goodsName: [
+ {required: true, message: "物品名称不能为空", trigger: "blur"}
+ ],
+ goodsCode: [
+ {required: true, message: "物品编码不能为空", trigger: "blur"}
+ ],
+ inAmount: [
+ {required: true, message: "入库数量不能为空", trigger: "blur"}
+ ],
+ repAmount: [
+ {required: true, message: "库存数量不能为空", trigger: "blur"}
+ ],
+ inTime: [
+ {required: true, message: "入库时间不能为空", trigger: "blur"}
+ ],
+ inOperator: [
+ {required: true, message: "入库人不能为空", trigger: "blur"}
+ ]
+}
+// 窗体
+let form = {...initFormData}
+
+const buttonLoading = ref(false)
+
+/** 查询入库登记列表 */
+const getList = async () => {
+ loading.value = true;
+ const res = await listGoodsIn(queryParams);
+ goodsInList.value = res.rows;
+ total.value = res.total;
+ loading.value = false;
+}
+
+/** 取消按钮 */
+const cancel = () => {
+ reset();
+ dialog.visible = false;
+}
+
+/** 表单重置 */
+const reset = () => {
+ form= {...initFormData};
+ goodsInFormRef.value?.resetFields();
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+ queryParams.pageNum = 1;
+ getList();
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+ queryFormRef.value?.resetFields();
+ handleQuery();
+}
+
+/** 多选框选中数据 */
+const handleSelectionChange = (selection: GoodsInVO[]) => {
+ ids.value = selection.map(item => item.inId);
+ single.value = selection.length != 1;
+ multiple.value = !selection.length;
+}
+
+/** 新增按钮操作 */
+const handleAdd = () => {
+ reset();
+ dialog.visible = true;
+ dialog.title = "添加入库登记";
+}
+
+/** 修改按钮操作 */
+const handleUpdate = async (row?: GoodsInVO) => {
+ reset();
+ const _inId = row?.inId || ids.value[0]
+ const res = await getGoodsIn(_inId);
+ Object.assign(form, res.data);
+ dialog.visible = true;
+ dialog.title = "修改入库登记";
+}
+
+/** 提交按钮 */
+const submitForm = () => {
+ goodsInFormRef.value?.validate(async (valid: boolean) => {
+ if (valid) {
+ buttonLoading.value = true;
+ if (form.inId) {
+ await updateGoodsIn(form).finally(() => buttonLoading.value = false);
+ } else {
+ await addGoodsIn(form).finally(() => buttonLoading.value = false);
+ }
+ proxy?.$modal.msgSuccess("修改成功");
+ dialog.visible = false;
+ await getList();
+ }
+ });
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (row?: GoodsInVO) => {
+ const _inIds = row?.inId || ids.value;
+ await proxy?.$modal.confirm('是否确认删除入库登记编号为"' + _inIds + '"的数据项?').finally(() => loading.value = false);
+ await delGoodsIn(_inIds);
+ proxy?.$modal.msgSuccess("删除成功");
+ await getList();
+}
+
+/** 导出按钮操作 */
+const handleExport = () => {
+ proxy?.download('system/goodsIn/export', {
+ ...queryParams
+ }, `goodsIn_${new Date().getTime()}.xlsx`)
+}
+
+onMounted(() => {
+ getList();
+});
+</script>
diff --git a/src/views/system/menu/index.vue b/src/views/system/menu/index.vue
new file mode 100644
index 0000000..06cd928
--- /dev/null
+++ b/src/views/system/menu/index.vue
@@ -0,0 +1,410 @@
+<template>
+ <div class="p-2">
+ <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
+ <div class="mb-[10px]" v-show="showSearch">
+ <el-card shadow="hover">
+ <el-form ref="queryFormRef" :model="queryParams" :inline="true" label-width="68px">
+ <el-form-item label="菜单名称" prop="menuName">
+ <el-input v-model="queryParams.menuName" placeholder="请输入菜单名称" clearable style="width: 240px" @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item label="状态" prop="status">
+ <el-select v-model="queryParams.status" placeholder="菜单状态" clearable>
+ <el-option v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.label" :value="dict.value" />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+ <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+ </el-form-item>
+ </el-form>
+ </el-card>
+ </div>
+ </transition>
+
+ <el-card shadow="hover">
+ <template #header>
+ <el-row :gutter="10">
+ <el-col :span="1.5">
+ <el-button type="primary" plain icon="Plus" @click="handleAdd()" v-hasPermi="['system:menu:add']">新增 </el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button type="info" plain icon="Sort" @click="handleToggleExpandAll">展开/折叠</el-button>
+ </el-col>
+ <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
+ </el-row>
+ </template>
+
+ <el-table
+ v-loading="loading"
+ :data="menuList"
+ row-key="menuId"
+ :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
+ ref="menuTableRef"
+ :default-expand-all="isExpandAll"
+ >
+ <el-table-column prop="menuName" label="菜单名称" :show-overflow-tooltip="true" width="160"></el-table-column>
+ <el-table-column prop="icon" label="图标" align="center" width="100">
+ <template #default="scope">
+ <svg-icon :icon-class="scope.row.icon" />
+ </template>
+ </el-table-column>
+ <el-table-column prop="orderNum" label="排序" width="60"></el-table-column>
+ <el-table-column prop="perms" label="权限标识" :show-overflow-tooltip="true"></el-table-column>
+ <el-table-column prop="component" label="组件路径" :show-overflow-tooltip="true"></el-table-column>
+ <el-table-column prop="status" label="状态" width="80">
+ <template #default="scope">
+ <dict-tag :options="sys_normal_disable" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column label="创建时间" align="center" prop="createTime">
+ <template #default="scope">
+ <span>{{ scope.row.createTime }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column fixed="right" label="操作" width="180">
+ <template #default="scope">
+ <el-tooltip content="修改" placement="top">
+ <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:menu:edit']" />
+ </el-tooltip>
+ <el-tooltip content="新增" placement="top">
+ <el-button link type="primary" icon="Plus" @click="handleAdd(scope.row)" v-hasPermi="['system:menu:add']" />
+ </el-tooltip>
+ <el-tooltip content="删除" placement="top">
+ <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:menu:remove']" />
+ </el-tooltip>
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-card>
+
+ <el-dialog :title="dialog.title" v-model="dialog.visible" destroy-on-close append-to-bod width="750px">
+ <el-form ref="menuFormRef" :model="form" :rules="rules" label-width="100px">
+ <el-row>
+ <el-col :span="24">
+ <el-form-item label="上级菜单">
+ <el-tree-select
+ v-model="form.parentId"
+ :data="menuOptions"
+ :props="{ value: 'menuId', label: 'menuName', children: 'children' }"
+ value-key="menuId"
+ placeholder="选择上级菜单"
+ check-strictly
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="24">
+ <el-form-item label="菜单类型" prop="menuType">
+ <el-radio-group v-model="form.menuType">
+ <el-radio label="M">目录</el-radio>
+ <el-radio label="C">菜单</el-radio>
+ <el-radio label="F">按钮</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ </el-col>
+ <el-col :span="24" v-if="form.menuType !== 'F'">
+ <el-form-item label="菜单图标" prop="icon">
+ <!-- 图标选择器 -->
+ <icon-select v-model="form.icon" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="菜单名称" prop="menuName">
+ <el-input v-model="form.menuName" placeholder="请输入菜单名称" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="显示排序" prop="orderNum">
+ <el-input-number v-model="form.orderNum" controls-position="right" :min="0" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12" v-if="form.menuType !== 'F'">
+ <el-form-item>
+ <template #label>
+ <span>
+ <el-tooltip content="选择是外链则路由地址需要以`http(s)://`开头" placement="top">
+ <el-icon>
+ <question-filled />
+ </el-icon> </el-tooltip
+ >是否外链
+ </span>
+ </template>
+ <el-radio-group v-model="form.isFrame">
+ <el-radio label="0">是</el-radio>
+ <el-radio label="1">否</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12" v-if="form.menuType !== 'F'">
+ <el-form-item prop="path">
+ <template #label>
+ <span>
+ <el-tooltip content="访问的路由地址,如:`user`,如外网地址需内链访问则以`http(s)://`开头" placement="top">
+ <el-icon>
+ <question-filled />
+ </el-icon>
+ </el-tooltip>
+ 路由地址
+ </span>
+ </template>
+ <el-input v-model="form.path" placeholder="请输入路由地址" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12" v-if="form.menuType === 'C'">
+ <el-form-item prop="component">
+ <template #label>
+ <span>
+ <el-tooltip content="访问的组件路径,如:`system/user/index`,默认在`views`目录下" placement="top">
+ <el-icon>
+ <question-filled />
+ </el-icon>
+ </el-tooltip>
+ 组件路径
+ </span>
+ </template>
+ <el-input v-model="form.component" placeholder="请输入组件路径" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12" v-if="form.menuType !== 'M'">
+ <el-form-item>
+ <el-input v-model="form.perms" placeholder="请输入权限标识" maxlength="100" />
+ <template #label>
+ <span>
+ <el-tooltip content="控制器中定义的权限字符,如:@SaCheckPermission('system:user:list')" placement="top">
+ <el-icon>
+ <question-filled />
+ </el-icon>
+ </el-tooltip>
+ 权限字符
+ </span>
+ </template>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12" v-if="form.menuType === 'C'">
+ <el-form-item>
+ <el-input v-model="form.queryParam" placeholder="请输入路由参数" maxlength="255" />
+ <template #label>
+ <span>
+ <el-tooltip content='访问路由的默认传递参数,如:`{"id": 1, "name": "ry"}`' placement="top">
+ <el-icon>
+ <question-filled />
+ </el-icon>
+ </el-tooltip>
+ 路由参数
+ </span>
+ </template>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12" v-if="form.menuType === 'C'">
+ <el-form-item>
+ <template #label>
+ <span>
+ <el-tooltip content="选择是则会被`keep-alive`缓存,需要匹配组件的`name`和地址保持一致" placement="top">
+ <el-icon>
+ <question-filled />
+ </el-icon>
+ </el-tooltip>
+ 是否缓存
+ </span>
+ </template>
+ <el-radio-group v-model="form.isCache">
+ <el-radio label="0">缓存</el-radio>
+ <el-radio label="1">不缓存</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12" v-if="form.menuType !== 'F'">
+ <el-form-item>
+ <template #label>
+ <span>
+ <el-tooltip content="选择隐藏则路由将不会出现在侧边栏,但仍然可以访问" placement="top">
+ <el-icon>
+ <question-filled />
+ </el-icon>
+ </el-tooltip>
+ 显示状态
+ </span>
+ </template>
+ <el-radio-group v-model="form.visible">
+ <el-radio v-for="dict in sys_show_hide" :key="dict.value" :label="dict.value">{{ dict.label }} </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item>
+ <template #label>
+ <span>
+ <el-tooltip content="选择停用则路由将不会出现在侧边栏,也不能被访问" placement="top">
+ <el-icon>
+ <question-filled />
+ </el-icon>
+ </el-tooltip>
+ 菜单状态
+ </span>
+ </template>
+ <el-radio-group v-model="form.status">
+ <el-radio v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.value">
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitForm">确 定</el-button>
+ <el-button @click="cancel">取 消</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup name="Menu" lang="ts">
+import { addMenu, delMenu, getMenu, listMenu, updateMenu } from '@/api/system/menu';
+import { MenuForm, MenuQuery, MenuVO } from '@/api/system/menu/types';
+import { MenuTypeEnum } from '@/enums/MenuTypeEnum';
+
+interface MenuOptionsType {
+ menuId: number;
+ menuName: string;
+ children: MenuOptionsType[] | undefined;
+}
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance
+const { sys_show_hide, sys_normal_disable } = toRefs<any>(proxy?.useDict("sys_show_hide", "sys_normal_disable"));
+
+const menuList = ref<MenuVO[]>([])
+const loading = ref(true)
+const showSearch = ref(true)
+const menuOptions = ref<MenuOptionsType[]>([])
+const isExpandAll = ref(false)
+
+const dialog = reactive<DialogOption>({
+ visible: false,
+ title: ''
+});
+
+const queryFormRef = ref<ElFormInstance>();
+const menuFormRef = ref<ElFormInstance>();
+const initFormData = {
+ path: '',
+ menuId: undefined,
+ parentId: 0,
+ menuName: '',
+ icon: '',
+ menuType: MenuTypeEnum.M,
+ orderNum: 1,
+ isFrame: "1",
+ isCache: "0",
+ visible: "0",
+ status: "0"
+}
+const data = reactive<PageData<MenuForm, MenuQuery>>({
+ form: { ...initFormData },
+ queryParams: {
+ menuName: undefined,
+ status: undefined
+ },
+ rules: {
+ menuName: [{ required: true, message: "菜单名称不能为空", trigger: "blur" }],
+ orderNum: [{ required: true, message: "菜单顺序不能为空", trigger: "blur" }],
+ path: [{ required: true, message: "路由地址不能为空", trigger: "blur" }]
+ },
+})
+
+const menuTableRef = ref<ElTableInstance>();
+
+const { queryParams, form, rules } = toRefs<PageData<MenuForm, MenuQuery>>(data)
+/** 查询菜单列表 */
+const getList = async () => {
+ loading.value = true
+ const res = await listMenu(queryParams.value);
+ const data = proxy?.handleTree<MenuVO>(res.data, "menuId")
+ if (data) {
+ menuList.value = data
+ }
+ loading.value = false
+}
+/** 查询菜单下拉树结构 */
+const getTreeselect = async () => {
+ menuOptions.value = []
+ const response = await listMenu();
+ const menu: MenuOptionsType = { menuId: 0, menuName: "主类目", children: [] }
+ menu.children = proxy?.handleTree<MenuOptionsType>(response.data, "menuId")
+ menuOptions.value.push(menu)
+}
+/** 取消按钮 */
+const cancel = () => {
+ reset()
+ dialog.visible = false
+}
+/** 表单重置 */
+const reset = () => {
+ form.value = { ...initFormData };
+ menuFormRef.value?.resetFields();
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+ getList();
+}
+/** 重置按钮操作 */
+const resetQuery = () => {
+ queryFormRef.value?.resetFields();
+ handleQuery();
+}
+/** 新增按钮操作 */
+const handleAdd = (row?: MenuVO) => {
+ reset();
+ getTreeselect();
+ row && row.menuId ? form.value.parentId = row.menuId : form.value.parentId = 0;
+ dialog.visible = true;
+ dialog.title = "添加菜单";
+}
+/** 展开/折叠操作 */
+const handleToggleExpandAll = () => {
+ isExpandAll.value = !isExpandAll.value;
+ toggleExpandAll(menuList.value, isExpandAll.value)
+}
+/** 展开/折叠所有 */
+const toggleExpandAll = (data: MenuVO[], status: boolean) => {
+ data.forEach((item: MenuVO) => {
+ menuTableRef.value?.toggleRowExpansion(item, status)
+ if (item.children && item.children.length > 0) toggleExpandAll(item.children, status)
+ })
+}
+/** 修改按钮操作 */
+const handleUpdate = async (row: MenuVO) => {
+ reset();
+ await getTreeselect();
+ if (row.menuId) {
+ const { data } = await getMenu(row.menuId);
+ form.value = data;
+ }
+ dialog.visible = true;
+ dialog.title = "修改菜单";
+}
+/** 提交按钮 */
+const submitForm = () => {
+ menuFormRef.value?.validate(async (valid: boolean) => {
+ if (valid) {
+ form.value.menuId ? await updateMenu(form.value) : await addMenu(form.value);
+ proxy?.$modal.msgSuccess("操作成功");
+ dialog.visible = false;
+ await getList();
+ }
+ })
+}
+/** 删除按钮操作 */
+const handleDelete = async (row: MenuVO) => {
+ await proxy?.$modal.confirm('是否确认删除名称为"' + row.menuName + '"的数据项?');
+ await delMenu(row.menuId);
+ await getList();
+ proxy?.$modal.msgSuccess("删除成功");
+}
+
+onMounted(() => {
+ getList();
+});
+</script>
diff --git a/src/views/system/notice/index.vue b/src/views/system/notice/index.vue
new file mode 100644
index 0000000..0c41ce1
--- /dev/null
+++ b/src/views/system/notice/index.vue
@@ -0,0 +1,245 @@
+<template>
+ <div class="p-2">
+ <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
+ <div class="mb-[10px]" v-show="showSearch">
+ <el-card shadow="hover">
+ <el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px">
+ <el-form-item label="公告标题" prop="noticeTitle">
+ <el-input v-model="queryParams.noticeTitle" placeholder="请输入公告标题" clearable style="width: 200px" @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item label="操作人员" prop="createByName">
+ <el-input v-model="queryParams.createByName" placeholder="请输入操作人员" clearable style="width: 200px" @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item label="类型" prop="noticeType">
+ <el-select v-model="queryParams.noticeType" placeholder="公告类型" clearable style="width: 200px">
+ <el-option v-for="dict in sys_notice_type" :key="dict.value" :label="dict.label" :value="dict.value" />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+ <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+ </el-form-item>
+ </el-form>
+ </el-card>
+ </div>
+ </transition>
+
+ <el-card shadow="hover">
+ <template #header>
+ <el-row :gutter="10" class="mb8">
+ <el-col :span="1.5">
+ <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['system:notice:add']">新增</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['system:notice:edit']"
+ >修改</el-button
+ >
+ </el-col>
+ <el-col :span="1.5">
+ <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['system:notice:remove']">
+ 删除
+ </el-button>
+ </el-col>
+ <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
+ </el-row>
+ </template>
+
+ <el-table v-loading="loading" :data="noticeList" @selection-change="handleSelectionChange">
+ <el-table-column type="selection" width="55" align="center" />
+ <el-table-column label="序号" align="center" prop="noticeId" width="100" v-if="false" />
+ <el-table-column label="公告标题" align="center" prop="noticeTitle" :show-overflow-tooltip="true" />
+ <el-table-column label="公告类型" align="center" prop="noticeType" width="100">
+ <template #default="scope">
+ <dict-tag :options="sys_notice_type" :value="scope.row.noticeType" />
+ </template>
+ </el-table-column>
+ <el-table-column label="状态" align="center" prop="status" width="100">
+ <template #default="scope">
+ <dict-tag :options="sys_notice_status" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column label="创建者" align="center" prop="createByName" width="100" />
+ <el-table-column label="创建时间" align="center" prop="createTime" width="100">
+ <template #default="scope">
+ <span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d}') }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+ <template #default="scope">
+ <el-tooltip content="修改" placement="top">
+ <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:notice:edit']"></el-button>
+ </el-tooltip>
+ <el-tooltip content="删除" placement="top">
+ <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:notice:remove']"></el-button>
+ </el-tooltip>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
+ </el-card>
+ <!-- 添加或修改公告对话框 -->
+ <el-dialog :title="dialog.title" v-model="dialog.visible" width="780px" append-to-body>
+ <el-form ref="noticeFormRef" :model="form" :rules="rules" label-width="80px">
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="公告标题" prop="noticeTitle">
+ <el-input v-model="form.noticeTitle" placeholder="请输入公告标题" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="公告类型" prop="noticeType">
+ <el-select v-model="form.noticeType" placeholder="请选择">
+ <el-option v-for="dict in sys_notice_type" :key="dict.value" :label="dict.label" :value="dict.value"></el-option>
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="24">
+ <el-form-item label="状态">
+ <el-radio-group v-model="form.status">
+ <el-radio v-for="dict in sys_notice_status" :key="dict.value" :label="dict.value">{{ dict.label
+ }}</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ </el-col>
+ <el-col :span="24">
+ <el-form-item label="内容">
+ <editor v-model="form.noticeContent" :min-height="192" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitForm">确 定</el-button>
+ <el-button @click="cancel">取 消</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup name="Notice" lang="ts">
+import { listNotice, getNotice, delNotice, addNotice, updateNotice } from "@/api/system/notice";
+import { NoticeForm, NoticeQuery, NoticeVO } from "@/api/system/notice/types";
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const { sys_notice_status, sys_notice_type } = toRefs<any>(proxy?.useDict("sys_notice_status", "sys_notice_type"));
+
+const noticeList = ref<NoticeVO[]>([]);
+const loading = ref(true);
+const showSearch = ref(true);
+const ids = ref<Array<string | number>>([]);
+const single = ref(true);
+const multiple = ref(true);
+const total = ref(0);
+
+const queryFormRef = ref<ElFormInstance>();
+const noticeFormRef = ref<ElFormInstance>();
+
+
+const dialog = reactive<DialogOption>({
+ visible: false,
+ title: ''
+});
+
+const initFormData: NoticeForm = {
+ noticeId: undefined,
+ noticeTitle: '',
+ noticeType: '',
+ noticeContent: '',
+ status: "0",
+ remark: '',
+ createByName: ''
+}
+const data = reactive<PageData<NoticeForm, NoticeQuery>>({
+ form: { ...initFormData },
+ queryParams: {
+ pageNum: 1,
+ pageSize: 10,
+ noticeTitle: '',
+ createByName: '',
+ status: '',
+ noticeType: ''
+ },
+ rules: {
+ noticeTitle: [{ required: true, message: "公告标题不能为空", trigger: "blur" }],
+ noticeType: [{ required: true, message: "公告类型不能为空", trigger: "change" }]
+ },
+});
+
+const { queryParams, form, rules } = toRefs(data);
+
+/** 查询公告列表 */
+const getList = async () => {
+ loading.value = true;
+ const res = await listNotice(queryParams.value);
+ noticeList.value = res.rows;
+ total.value = res.total;
+ loading.value = false;
+}
+/** 取消按钮 */
+const cancel = () => {
+ reset();
+ dialog.visible = false;
+}
+/** 表单重置 */
+const reset = () => {
+ form.value = { ...initFormData };
+ noticeFormRef.value?.resetFields();
+}
+/** 搜索按钮操作 */
+const handleQuery = () => {
+ queryParams.value.pageNum = 1;
+ getList();
+}
+/** 重置按钮操作 */
+const resetQuery = () => {
+ queryFormRef.value?.resetFields();
+ handleQuery();
+}
+/** 多选框选中数据 */
+const handleSelectionChange = (selection: NoticeVO[]) => {
+ ids.value = selection.map(item => item.noticeId);
+ single.value = selection.length != 1;
+ multiple.value = !selection.length;
+}
+/** 新增按钮操作 */
+const handleAdd = () => {
+ reset();
+ dialog.visible = true;
+ dialog.title = "添加公告";
+}
+/**修改按钮操作 */
+const handleUpdate = async (row?: NoticeVO) => {
+ reset();
+ const noticeId = row?.noticeId || ids.value[0];
+ const { data } = await getNotice(noticeId);
+ Object.assign(form.value, data);
+ dialog.visible = true;
+ dialog.title = "修改公告";
+}
+/** 提交按钮 */
+const submitForm = () => {
+ noticeFormRef.value?.validate(async (valid: boolean) => {
+ if (valid) {
+ form.value.noticeId ? await updateNotice(form.value) : await addNotice(form.value);
+ proxy?.$modal.msgSuccess("修改成功");
+ dialog.visible = false;
+ await getList();
+ }
+ });
+}
+/** 删除按钮操作 */
+const handleDelete = async (row?: NoticeVO) => {
+ const noticeIds = row?.noticeId || ids.value
+ await proxy?.$modal.confirm('是否确认删除公告编号为"' + noticeIds + '"的数据项?');
+ await delNotice(noticeIds);
+ await getList();
+ proxy?.$modal.msgSuccess("删除成功");
+}
+
+onMounted(() => {
+ getList();
+})
+</script>
diff --git a/src/views/system/oss/config.vue b/src/views/system/oss/config.vue
new file mode 100644
index 0000000..876347a
--- /dev/null
+++ b/src/views/system/oss/config.vue
@@ -0,0 +1,341 @@
+<template>
+ <div class="p-2">
+ <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
+ <div class="mb-[10px]" v-show="showSearch">
+ <el-card shadow="hover">
+ <el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px">
+ <el-form-item label="配置key" prop="configKey">
+ <el-input v-model="queryParams.configKey" placeholder="配置key" clearable style="width: 200px" @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item label="桶名称" prop="bucketName">
+ <el-input v-model="queryParams.bucketName" placeholder="请输入桶名称" clearable style="width: 200px" @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item label="是否默认" prop="status">
+ <el-select v-model="queryParams.status" placeholder="请选择状态" clearable style="width: 200px">
+ <el-option key="0" label="是" value="0" />
+ <el-option key="1" label="否" value="1" />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" icon="search" @click="handleQuery">搜索</el-button>
+ <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+ </el-form-item>
+ </el-form>
+ </el-card>
+ </div>
+ </transition>
+
+ <el-card shadow="hover">
+ <template #header>
+ <el-row :gutter="10" class="mb8">
+ <el-col :span="1.5">
+ <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['system:ossConfig:add']">新增</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['system:ossConfig:edit']">修改</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['system:ossConfig:remove']">
+ 删除
+ </el-button>
+ </el-col>
+ <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
+ </el-row>
+ </template>
+
+ <el-table v-loading="loading" :data="ossConfigList" @selection-change="handleSelectionChange">
+ <el-table-column type="selection" width="55" align="center" />
+ <el-table-column label="主建" align="center" prop="ossConfigId" v-if="columns[0].visible" />
+ <el-table-column label="配置key" align="center" prop="configKey" v-if="columns[1].visible" />
+ <el-table-column label="访问站点" align="center" prop="endpoint" v-if="columns[2].visible" width="200" />
+ <el-table-column label="自定义域名" align="center" prop="domain" v-if="columns[3].visible" width="200" />
+ <el-table-column label="桶名称" align="center" prop="bucketName" v-if="columns[4].visible" />
+ <el-table-column label="前缀" align="center" prop="prefix" v-if="columns[5].visible" />
+ <el-table-column label="域" align="center" prop="region" v-if="columns[6].visible" />
+ <el-table-column label="桶权限类型" align="center" prop="accessPolicy" v-if="columns[7].visible">
+ <template #default="scope">
+ <el-tag type="warning" v-if="scope.row.accessPolicy === '0'">private</el-tag>
+ <el-tag type="success" v-if="scope.row.accessPolicy === '1'">public</el-tag>
+ <el-tag type="info" v-if="scope.row.accessPolicy === '2'">custom</el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="是否默认" align="center" prop="status" v-if="columns[8].visible">
+ <template #default="scope">
+ <el-switch v-model="scope.row.status" active-value="0" inactive-value="1" @change="handleStatusChange(scope.row)"></el-switch>
+ </template>
+ </el-table-column>
+ <el-table-column label="操作" fixed="right" align="center" width="150" class-name="small-padding">
+ <template #default="scope">
+ <el-tooltip content="修改" placement="top">
+ <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:ossConfig:edit']"></el-button>
+ </el-tooltip>
+ <el-tooltip content="删除" placement="top">
+ <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:ossConfig:remove']"></el-button>
+ </el-tooltip>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
+ </el-card>
+ <!-- 添加或修改对象存储配置对话框 -->
+ <el-dialog :title="dialog.title" v-model="dialog.visible" width="800px" append-to-body>
+ <el-form ref="ossConfigFormRef" :model="form" :rules="rules" label-width="120px">
+ <el-form-item label="配置key" prop="configKey">
+ <el-input v-model="form.configKey" placeholder="请输入配置key" />
+ </el-form-item>
+ <el-form-item label="访问站点" prop="endpoint">
+ <el-input v-model="form.endpoint" placeholder="请输入访问站点" />
+ </el-form-item>
+ <el-form-item label="自定义域名" prop="domain">
+ <el-input v-model="form.domain" placeholder="请输入自定义域名" />
+ </el-form-item>
+ <el-form-item label="accessKey" prop="accessKey">
+ <el-input v-model="form.accessKey" placeholder="请输入accessKey" />
+ </el-form-item>
+ <el-form-item label="secretKey" prop="secretKey">
+ <el-input v-model="form.secretKey" placeholder="请输入秘钥" show-password />
+ </el-form-item>
+ <el-form-item label="桶名称" prop="bucketName">
+ <el-input v-model="form.bucketName" placeholder="请输入桶名称" />
+ </el-form-item>
+ <el-form-item label="前缀" prop="prefix">
+ <el-input v-model="form.prefix" placeholder="请输入前缀" />
+ </el-form-item>
+ <el-form-item label="是否HTTPS">
+ <el-radio-group v-model="form.isHttps">
+ <el-radio v-for="dict in sys_yes_no" :key="dict.value" :label="dict.value">{{ dict.label }}</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="桶权限类型">
+ <el-radio-group v-model="form.accessPolicy">
+ <el-radio label="0">private</el-radio>
+ <el-radio label="1">public</el-radio>
+ <el-radio label="2">custom</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="域" prop="region">
+ <el-input v-model="form.region" placeholder="请输入域" />
+ </el-form-item>
+ <el-form-item label="备注" prop="remark">
+ <el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
+ <el-button @click="cancel">取 消</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup name="OssConfig" lang="ts">
+import {
+ listOssConfig,
+ getOssConfig,
+ delOssConfig,
+ addOssConfig,
+ updateOssConfig,
+ changeOssConfigStatus
+} from "@/api/system/ossConfig";
+import { OssConfigForm, OssConfigQuery, OssConfigVO } from "@/api/system/ossConfig/types";
+
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance
+const { sys_yes_no } = toRefs<any>(proxy?.useDict("sys_yes_no"));
+
+const ossConfigList = ref<OssConfigVO[]>([]);
+const buttonLoading = ref(false);
+const loading = ref(true);
+const showSearch = ref(true);
+const ids = ref<Array<number | string>>([]);
+const single = ref(true);
+const multiple = ref(true);
+const total = ref(0);
+
+const queryFormRef = ref<ElFormInstance>();
+const ossConfigFormRef = ref<ElFormInstance>();
+
+const dialog = reactive<DialogOption>({
+ visible: false,
+ title: ''
+});
+
+// 列显隐信息
+const columns = ref<FieldOption[]>([
+ { key: 0, label: `主建`, visible: true },
+ { key: 1, label: `配置key`, visible: false },
+ { key: 2, label: `访问站点`, visible: true },
+ { key: 3, label: `自定义域名`, visible: true },
+ { key: 4, label: `桶名称`, visible: true },
+ { key: 5, label: `前缀`, visible: true },
+ { key: 6, label: `域`, visible: true },
+ { key: 7, label: `桶权限类型`, visible: true },
+ { key: 8, label: `状态`, visible: true }
+]);
+
+
+const initFormData: OssConfigForm = {
+ ossConfigId: undefined,
+ configKey: '',
+ accessKey: '',
+ secretKey: '',
+ bucketName: '',
+ prefix: '',
+ endpoint: '',
+ domain: '',
+ isHttps: "N",
+ accessPolicy: "1",
+ region: '',
+ status: "1",
+ remark: '',
+}
+const data = reactive<PageData<OssConfigForm, OssConfigQuery>>({
+ form: { ...initFormData },
+ // 查询参数
+ queryParams: {
+ pageNum: 1,
+ pageSize: 10,
+ configKey: '',
+ bucketName: '',
+ status: '',
+ },
+ rules: {
+ configKey: [{ required: true, message: "configKey不能为空", trigger: "blur" },],
+ accessKey: [
+ { required: true, message: "accessKey不能为空", trigger: "blur" },
+ {
+ min: 2,
+ max: 200,
+ message: "accessKey长度必须介于 2 和 100 之间",
+ trigger: "blur",
+ },
+ ],
+ secretKey: [
+ { required: true, message: "secretKey不能为空", trigger: "blur" },
+ {
+ min: 2,
+ max: 100,
+ message: "secretKey长度必须介于 2 和 100 之间",
+ trigger: "blur",
+ },
+ ],
+ bucketName: [
+ { required: true, message: "bucketName不能为空", trigger: "blur" },
+ {
+ min: 2,
+ max: 100,
+ message: "bucketName长度必须介于 2 和 100 之间",
+ trigger: "blur",
+ },
+ ],
+ endpoint: [
+ { required: true, message: "endpoint不能为空", trigger: "blur" },
+ {
+ min: 2,
+ max: 100,
+ message: "endpoint名称长度必须介于 2 和 100 之间",
+ trigger: "blur",
+ },
+ ],
+ accessPolicy: [{ required: true, message: "accessPolicy不能为空", trigger: "blur" }]
+ }
+});
+
+const { queryParams, form, rules } = toRefs(data);
+
+/** 查询对象存储配置列表 */
+const getList = async () => {
+ loading.value = true;
+ const res = await listOssConfig(queryParams.value);
+ ossConfigList.value = res.rows;
+ total.value = res.total;
+ loading.value = false;
+}
+/** 取消按钮 */
+const cancel = () => {
+ dialog.visible = false;
+ reset();
+}
+/** 表单重置 */
+const reset = () => {
+ form.value = { ...initFormData };
+ ossConfigFormRef.value?.resetFields();
+}
+/** 搜索按钮操作 */
+const handleQuery = () => {
+ queryParams.value.pageNum = 1;
+ getList();
+}
+/** 重置按钮操作 */
+const resetQuery = () => {
+ queryFormRef.value?.resetFields();
+ handleQuery();
+}
+/** 选择条数 */
+const handleSelectionChange = (selection: OssConfigVO[]) => {
+ ids.value = selection.map(item => item.ossConfigId);
+ single.value = selection.length != 1;
+ multiple.value = !selection.length;
+}
+/** 新增按钮操作 */
+const handleAdd = () => {
+ reset();
+ dialog.visible = true;
+ dialog.title = "添加对象存储配置";
+}
+/** 修改按钮操作 */
+const handleUpdate = async (row?: OssConfigVO) => {
+ reset();
+ const ossConfigId = row?.ossConfigId || ids.value[0];
+ const res = await getOssConfig(ossConfigId);
+ Object.assign(form.value, res.data);
+ dialog.visible = true;
+ dialog.title = "修改对象存储配置";
+}
+/** 提交按钮 */
+const submitForm = () => {
+ ossConfigFormRef.value?.validate(async (valid: boolean) => {
+ if (valid) {
+ buttonLoading.value = true;
+ if (form.value.ossConfigId) {
+ await updateOssConfig(form.value).finally(() => buttonLoading.value = false);
+ } else {
+ await addOssConfig(form.value).finally(() => buttonLoading.value = false);
+ }
+ proxy?.$modal.msgSuccess("新增成功");
+ dialog.visible = false;
+ await getList();
+ }
+ });
+}
+/** 状态修改 */
+const handleStatusChange = async (row: OssConfigVO) => {
+ let text = row.status === "0" ? "启用" : "停用";
+ try {
+ await proxy?.$modal.confirm('确认要"' + text + '""' + row.configKey + '"配置吗?');
+ await changeOssConfigStatus(row.ossConfigId, row.status, row.configKey);
+ await getList()
+ proxy?.$modal.msgSuccess(text + "成功");
+ } catch { return } finally {
+ row.status = row.status === "0" ? "1" : "0";
+ }
+
+}
+/** 删除按钮操作 */
+const handleDelete = async (row?: OssConfigVO) => {
+ const ossConfigIds = row?.ossConfigId || ids.value;
+ await proxy?.$modal.confirm('是否确认删除OSS配置编号为"' + ossConfigIds + '"的数据项?');
+ loading.value = true;
+ await delOssConfig(ossConfigIds).finally(() => loading.value = false);
+ await getList();
+ proxy?.$modal.msgSuccess("删除成功");
+
+}
+
+onMounted(() => {
+ getList();
+})
+</script>
diff --git a/src/views/system/oss/index.vue b/src/views/system/oss/index.vue
new file mode 100644
index 0000000..ec11708
--- /dev/null
+++ b/src/views/system/oss/index.vue
@@ -0,0 +1,335 @@
+<template>
+ <div class="p-2">
+ <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
+ <div class="mb-[10px]" v-show="showSearch">
+ <el-card shadow="hover">
+ <el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px">
+ <el-form-item label="文件名" prop="fileName">
+ <el-input v-model="queryParams.fileName" placeholder="请输入文件名" clearable style="width: 200px" @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item label="原名" prop="originalName">
+ <el-input v-model="queryParams.originalName" placeholder="请输入原名" clearable style="width: 200px" @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item label="文件后缀" prop="fileSuffix">
+ <el-input v-model="queryParams.fileSuffix" placeholder="请输入文件后缀" clearable style="width: 200px" @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item label="创建时间">
+ <el-date-picker
+ v-model="dateRangeCreateTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ range-separator="-"
+ start-placeholder="开始日期"
+ end-placeholder="结束日期"
+ :default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
+ ></el-date-picker>
+ </el-form-item>
+ <el-form-item label="服务商" prop="service">
+ <el-input v-model="queryParams.service" placeholder="请输入服务商" clearable style="width: 200px" @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" icon="search" @click="handleQuery">搜索</el-button>
+ <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+ </el-form-item>
+ </el-form>
+ </el-card>
+ </div>
+ </transition>
+
+ <el-card shadow="hover">
+ <template #header>
+ <el-row :gutter="10" class="mb8">
+ <el-col :span="1.5">
+ <el-button type="primary" plain icon="Upload" @click="handleFile" v-hasPermi="['system:oss:upload']">上传文件</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button type="primary" plain icon="Upload" @click="handleImage" v-hasPermi="['system:oss:upload']">上传图片</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['system:oss:remove']">
+ 删除
+ </el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button
+ :type="previewListResource ? 'danger' : 'warning'"
+ plain
+ @click="handlePreviewListResource(!previewListResource)"
+ v-hasPermi="['system:oss:edit']"
+ >预览开关 :
+ {{
+ previewListResource ? "禁用" : "启用" }}</el-button
+ >
+ </el-col>
+ <el-col :span="1.5">
+ <el-button type="info" plain icon="Operation" @click="handleOssConfig" v-hasPermi="['system:oss:list']">配置管理</el-button>
+ </el-col>
+ <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
+ </el-row>
+ </template>
+
+ <el-table
+ v-loading="loading"
+ :data="ossList"
+ @selection-change="handleSelectionChange"
+ :header-cell-class-name="handleHeaderClass"
+ @header-click="handleHeaderCLick"
+ v-if="showTable"
+ >
+ <el-table-column type="selection" width="55" align="center" />
+ <el-table-column label="对象存储主键" align="center" prop="ossId" v-if="false" />
+ <el-table-column label="文件名" align="center" prop="fileName" />
+ <el-table-column label="原名" align="center" prop="originalName" />
+ <el-table-column label="文件后缀" align="center" prop="fileSuffix" />
+ <el-table-column label="文件展示" align="center" prop="url">
+ <template #default="scope">
+ <ImagePreview
+ v-if="previewListResource && checkFileSuffix(scope.row.fileSuffix)"
+ :width="100"
+ :height="100"
+ :src="scope.row.url"
+ :preview-src-list="[scope.row.url]"
+ />
+ <span v-text="scope.row.url" v-if="!checkFileSuffix(scope.row.fileSuffix) || !previewListResource" />
+ </template>
+ </el-table-column>
+ <el-table-column label="创建时间" align="center" prop="createTime" width="180" sortable="custom">
+ <template #default="scope">
+ <span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d}') }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="上传人" align="center" prop="createByName" />
+ <el-table-column label="服务商" align="center" prop="service" sortable="custom" />
+ <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+ <template #default="scope">
+ <el-tooltip content="下载" placement="top">
+ <el-button link type="primary" icon="Download" @click="handleDownload(scope.row)" v-hasPermi="['system:oss:download']"></el-button>
+ </el-tooltip>
+ <el-tooltip content="删除" placement="top">
+ <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:oss:remove']"></el-button>
+ </el-tooltip>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
+ </el-card>
+ <!-- 添加或修改OSS对象存储对话框 -->
+ <el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
+ <el-form ref="ossFormRef" :model="form" :rules="rules" label-width="80px">
+ <el-form-item label="文件名">
+ <fileUpload v-model="form.file" v-if="type === 0" />
+ <imageUpload v-model="form.file" v-if="type === 1" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
+ <el-button @click="cancel">取 消</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup name="Oss" lang="ts">
+import { listOss, delOss } from "@/api/system/oss";
+import ImagePreview from "@/components/ImagePreview/index.vue";
+import { OssForm, OssQuery, OssVO } from "@/api/system/oss/types";
+
+const router = useRouter();
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+
+const ossList = ref<OssVO[]>([]);
+const showTable = ref(true);
+const buttonLoading = ref(false);
+const loading = ref(true);
+const showSearch = ref(true);
+const ids = ref<Array<string | number>>([]);
+const single = ref(true);
+const multiple = ref(true);
+const total = ref(0);
+const type = ref(0);
+const previewListResource = ref(true);
+const dateRangeCreateTime = ref<[DateModelType, DateModelType]>(['', '']);
+
+const dialog = reactive<DialogOption>({
+ visible: false,
+ title: ''
+});
+
+// 默认排序
+const defaultSort = ref({ prop: 'createTime', order: 'ascending' });
+
+const ossFormRef = ref<ElFormInstance>();
+const queryFormRef = ref<ElFormInstance>();
+
+const initFormData = {
+ file: undefined,
+}
+const data = reactive<PageData<OssForm, OssQuery>>({
+ form: { ...initFormData },
+ // 查询参数
+ queryParams: {
+ pageNum: 1,
+ pageSize: 10,
+ fileName: '',
+ originalName: '',
+ fileSuffix: '',
+ createTime: '',
+ service: '',
+ orderByColumn: defaultSort.value.prop,
+ isAsc: defaultSort.value.order
+ },
+ rules: {
+ file: [
+ { required: true, message: "文件不能为空", trigger: "blur" }
+ ]
+ }
+});
+
+const { queryParams, form, rules } = toRefs(data);
+
+/** 查询OSS对象存储列表 */
+const getList = async () => {
+ loading.value = true;
+ const res = await proxy?.getConfigKey("sys.oss.previewListResource");
+ previewListResource.value = res?.data === undefined ? true : res.data === 'true';
+ const response = await listOss(proxy?.addDateRange(queryParams.value, dateRangeCreateTime.value, "CreateTime"));
+ ossList.value = response.rows;
+ total.value = response.total;
+ loading.value = false;
+ showTable.value = true;
+}
+function checkFileSuffix(fileSuffix: string[]) {
+ let arr = ["png", "jpg", "jpeg"];
+ return arr.some(type => {
+ return fileSuffix.indexOf(type) > -1;
+ });
+}
+/** 取消按钮 */
+function cancel() {
+ dialog.visible = false;
+ reset();
+}
+/** 表单重置 */
+function reset() {
+ form.value = { ...initFormData };
+ ossFormRef.value?.resetFields();
+}
+/** 搜索按钮操作 */
+function handleQuery() {
+ queryParams.value.pageNum = 1;
+ getList();
+}
+/** 重置按钮操作 */
+function resetQuery() {
+ showTable.value = false;
+ dateRangeCreateTime.value = ['', ''];
+ queryFormRef.value?.resetFields();
+ queryParams.value.orderByColumn = defaultSort.value.prop;
+ queryParams.value.isAsc = defaultSort.value.order;
+ handleQuery();
+}
+/** 选择条数 */
+function handleSelectionChange(selection: OssVO[]) {
+ ids.value = selection.map(item => item.ossId);
+ single.value = selection.length != 1;
+ multiple.value = !selection.length;
+}
+/** 设置列的排序为我们自定义的排序 */
+const handleHeaderClass = ({ column }: any): any => {
+ column.order = column.multiOrder
+}
+/** 点击表头进行排序 */
+const handleHeaderCLick = (column: any) => {
+ if (column.sortable !== 'custom') {
+ return
+ }
+ switch (column.multiOrder) {
+ case 'descending':
+ column.multiOrder = 'ascending';
+ break;
+ case 'ascending':
+ column.multiOrder = '';
+ break;
+ default:
+ column.multiOrder = 'descending';
+ break;
+ }
+ handleOrderChange(column.property, column.multiOrder)
+}
+const handleOrderChange = (prop: string, order: string) => {
+ let orderByArr = queryParams.value.orderByColumn ? queryParams.value.orderByColumn.split(",") : [];
+ let isAscArr = queryParams.value.isAsc ? queryParams.value.isAsc.split(",") : [];
+ let propIndex = orderByArr.indexOf(prop)
+ if (propIndex !== -1) {
+ if (order) {
+ //排序里已存在 只修改排序
+ isAscArr[propIndex] = order;
+ } else {
+ //如果order为null 则删除排序字段和属性
+ isAscArr.splice(propIndex, 1);//删除排序
+ orderByArr.splice(propIndex, 1);//删除属性
+ }
+ } else {
+ //排序里不存在则新增排序
+ orderByArr.push(prop);
+ isAscArr.push(order);
+ }
+ //合并排序
+ queryParams.value.orderByColumn = orderByArr.join(",");
+ queryParams.value.isAsc = isAscArr.join(",");
+ getList();
+}
+/** 任务日志列表查询 */
+const handleOssConfig = () => {
+ router.push('/system/oss-config/index')
+}
+/** 文件按钮操作 */
+const handleFile = () => {
+ reset();
+ type.value = 0;
+ dialog.visible = true;
+ dialog.title = "上传文件";
+}
+/** 图片按钮操作 */
+const handleImage = () => {
+ reset();
+ type.value = 1;
+ dialog.visible = true;
+ dialog.title = "上传图片";
+}
+/** 提交按钮 */
+const submitForm = () => {
+ dialog.visible = false;
+ getList();
+}
+/** 下载按钮操作 */
+const handleDownload = (row: OssVO) => {
+ proxy?.$download.oss(row.ossId)
+}
+/** 用户状态修改 */
+const handlePreviewListResource = async (preview: boolean) => {
+ let text = preview ? "启用" : "停用";
+ try {
+ await proxy?.$modal.confirm('确认要"' + text + '""预览列表图片"配置吗?');
+ await proxy?.updateConfigByKey("sys.oss.previewListResource", preview);
+ await getList()
+ proxy?.$modal.msgSuccess(text + "成功");
+ } catch { return }
+}
+/** 删除按钮操作 */
+const handleDelete = async (row?: OssVO) => {
+ const ossIds = row?.ossId || ids.value;
+ await proxy?.$modal.confirm('是否确认删除OSS对象存储编号为"' + ossIds + '"的数据项?');
+ loading.value = true;
+ await delOss(ossIds).finally(() => loading.value = false);
+ await getList();
+ proxy?.$modal.msgSuccess("删除成功");
+}
+
+onMounted(() => {
+ getList();
+})
+</script>
diff --git a/src/views/system/post/index.vue b/src/views/system/post/index.vue
new file mode 100644
index 0000000..b1cddd3
--- /dev/null
+++ b/src/views/system/post/index.vue
@@ -0,0 +1,237 @@
+<template>
+ <div class="p-2">
+ <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
+ <div class="mb-[10px]" v-show="showSearch">
+ <el-card shadow="hover">
+ <el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="70">
+ <el-form-item label="岗位编码" prop="postCode">
+ <el-input v-model="queryParams.postCode" placeholder="请输入岗位编码" clearable style="width: 200px" @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item label="岗位名称" prop="postName">
+ <el-input v-model="queryParams.postName" placeholder="请输入岗位名称" clearable style="width: 200px" @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item label="状态" prop="status">
+ <el-select v-model="queryParams.status" placeholder="岗位状态" clearable style="width: 200px">
+ <el-option v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.label" :value="dict.value" />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+ <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+ </el-form-item>
+ </el-form>
+ </el-card>
+ </div>
+ </transition>
+ <el-card shadow="hover">
+ <template #header>
+ <el-row :gutter="10" class="mb8">
+ <el-col :span="1.5">
+ <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['system:post:add']">新增</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['system:post:edit']">修改</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['system:post:remove']">
+ 删除
+ </el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['system:post:export']">导出</el-button>
+ </el-col>
+ <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
+ </el-row>
+ </template>
+
+ <el-table v-loading="loading" :data="postList" @selection-change="handleSelectionChange">
+ <el-table-column type="selection" width="55" align="center" />
+ <el-table-column label="岗位编号" align="center" prop="postId" v-if="false" />
+ <el-table-column label="岗位编码" align="center" prop="postCode" />
+ <el-table-column label="岗位名称" align="center" prop="postName" />
+ <el-table-column label="岗位排序" align="center" prop="postSort" />
+ <el-table-column label="状态" align="center" prop="status">
+ <template #default="scope">
+ <dict-tag :options="sys_normal_disable" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column label="创建时间" align="center" prop="createTime" width="180">
+ <template #default="scope">
+ <span>{{ parseTime(scope.row.createTime) }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="操作" width="180" align="center" class-name="small-padding fixed-width">
+ <template #default="scope">
+ <el-tooltip content="修改" placement="top">
+ <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:post:edit']"></el-button>
+ </el-tooltip>
+ <el-tooltip content="删除" placement="top">
+ <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:post:remove']"></el-button>
+ </el-tooltip>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
+ </el-card>
+
+ <!-- 添加或修改岗位对话框 -->
+ <el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
+ <el-form ref="postFormRef" :model="form" :rules="rules" label-width="80px">
+ <el-form-item label="岗位名称" prop="postName">
+ <el-input v-model="form.postName" placeholder="请输入岗位名称" />
+ </el-form-item>
+ <el-form-item label="岗位编码" prop="postCode">
+ <el-input v-model="form.postCode" placeholder="请输入编码名称" />
+ </el-form-item>
+ <el-form-item label="岗位顺序" prop="postSort">
+ <el-input-number v-model="form.postSort" controls-position="right" :min="0" />
+ </el-form-item>
+ <el-form-item label="岗位状态" prop="status">
+ <el-radio-group v-model="form.status">
+ <el-radio v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.value">{{ dict.label }}</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="备注" prop="remark">
+ <el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitForm">确 定</el-button>
+ <el-button @click="cancel">取 消</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup name="Post" lang="ts">
+import { listPost, addPost, delPost, getPost, updatePost } from "@/api/system/post";
+import { PostForm, PostQuery, PostVO } from "@/api/system/post/types";
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const { sys_normal_disable } = toRefs<any>(proxy?.useDict("sys_normal_disable"));
+
+const postList = ref<PostVO[]>([]);
+const loading = ref(true);
+const showSearch = ref(true);
+const ids = ref<Array<number | string>>([]);
+const single = ref(true);
+const multiple = ref(true);
+const total = ref(0);
+
+const postFormRef = ref<ElFormInstance>();
+const queryFormRef = ref<ElFormInstance>();
+
+const dialog = reactive<DialogOption>({
+ visible: false,
+ title: ''
+});
+
+const initFormData: PostForm = {
+ postId: undefined,
+ postCode: '',
+ postName: '',
+ postSort: 0,
+ status: "0",
+ remark: ''
+}
+
+const data = reactive<PageData<PostForm, PostQuery>>({
+ form: { ...initFormData },
+ queryParams: {
+ pageNum: 1,
+ pageSize: 10,
+ postCode: '',
+ postName: '',
+ status: ''
+ },
+ rules: {
+ postName: [{ required: true, message: "岗位名称不能为空", trigger: "blur" }],
+ postCode: [{ required: true, message: "岗位编码不能为空", trigger: "blur" }],
+ postSort: [{ required: true, message: "岗位顺序不能为空", trigger: "blur" }],
+ }
+});
+
+const { queryParams, form, rules } = toRefs<PageData<PostForm, PostQuery>>(data);
+
+/** 查询岗位列表 */
+const getList = async () => {
+ loading.value = true;
+ const res = await listPost(queryParams.value);
+ postList.value = res.rows;
+ total.value = res.total;
+ loading.value = false;
+}
+/** 取消按钮 */
+const cancel = () => {
+ reset();
+ dialog.visible = false;
+}
+/** 表单重置 */
+const reset = () => {
+ form.value = { ...initFormData };
+ postFormRef.value?.resetFields();
+}
+/** 搜索按钮操作 */
+const handleQuery = () => {
+ queryParams.value.pageNum = 1;
+ getList();
+}
+/** 重置按钮操作 */
+const resetQuery = () => {
+ queryFormRef.value?.resetFields();
+ handleQuery();
+}
+/** 多选框选中数据 */
+const handleSelectionChange = (selection: PostVO[]) => {
+ ids.value = selection.map(item => item.postId);
+ single.value = selection.length != 1;
+ multiple.value = !selection.length;
+}
+/** 新增按钮操作 */
+const handleAdd = () => {
+ reset();
+ dialog.visible = true;
+ dialog.title = "添加岗位";
+}
+/** 修改按钮操作 */
+const handleUpdate = async (row?: PostVO) => {
+ reset();
+ const postId = row?.postId || ids.value[0];
+ const res = await getPost(postId);
+ Object.assign(form.value, res.data);
+ dialog.visible = true;
+ dialog.title = "修改岗位";
+}
+/** 提交按钮 */
+const submitForm = () => {
+ postFormRef.value?.validate(async (valid: boolean) => {
+ if (valid) {
+ form.value.postId ? await updatePost(form.value) : await addPost(form.value);
+ proxy?.$modal.msgSuccess("操作成功");
+ dialog.visible = false;
+ await getList();
+ }
+ });
+}
+/** 删除按钮操作 */
+const handleDelete = async (row?: PostVO) => {
+ const postIds = row?.postId || ids.value;
+ await proxy?.$modal.confirm('是否确认删除岗位编号为"' + postIds + '"的数据项?');
+ await delPost(postIds);
+ await getList();
+ proxy?.$modal.msgSuccess("删除成功");
+}
+/** 导出按钮操作 */
+const handleExport = () => {
+ proxy?.download("system/post/export", {
+ ...queryParams.value
+ }, `post_${new Date().getTime()}.xlsx`);
+}
+
+onMounted(() => {
+ getList();
+});
+</script>
diff --git a/src/views/system/role/authUser.vue b/src/views/system/role/authUser.vue
new file mode 100644
index 0000000..7ad8a5a
--- /dev/null
+++ b/src/views/system/role/authUser.vue
@@ -0,0 +1,150 @@
+<template>
+ <div class="p-2">
+ <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
+ <div class="search" v-show="showSearch">
+ <el-form :model="queryParams" ref="queryFormRef" :inline="true">
+ <el-form-item label="用户名称" prop="userName">
+ <el-input v-model="queryParams.userName" placeholder="请输入用户名称" clearable style="width: 240px" @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item label="手机号码" prop="phonenumber">
+ <el-input v-model="queryParams.phonenumber" placeholder="请输入手机号码" clearable style="width: 240px" @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+ <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+ </el-form-item>
+ </el-form>
+ </div>
+ </transition>
+ <el-card shadow="never">
+ <template #header>
+ <el-row :gutter="10">
+ <el-col :span="1.5">
+ <el-button type="primary" plain icon="Plus" @click="openSelectUser" v-hasPermi="['system:role:add']">添加用户</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button type="danger" plain icon="CircleClose" :disabled="multiple" @click="cancelAuthUserAll" v-hasPermi="['system:role:remove']">
+ 批量取消授权
+ </el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button type="warning" plain icon="Close" @click="handleClose">关闭</el-button>
+ </el-col>
+ <right-toolbar v-model:showSearch="showSearch" @queryTable="getList" :search="true"></right-toolbar>
+ </el-row>
+ </template>
+ <el-table v-loading="loading" :data="userList" @selection-change="handleSelectionChange">
+ <el-table-column type="selection" width="55" align="center" />
+ <el-table-column label="用户名称" prop="userName" :show-overflow-tooltip="true" />
+ <el-table-column label="用户昵称" prop="nickName" :show-overflow-tooltip="true" />
+ <el-table-column label="邮箱" prop="email" :show-overflow-tooltip="true" />
+ <el-table-column label="手机" prop="phonenumber" :show-overflow-tooltip="true" />
+ <el-table-column label="状态" align="center" prop="status">
+ <template #default="scope">
+ <dict-tag :options="sys_normal_disable" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column label="创建时间" align="center" prop="createTime" width="180">
+ <template #default="scope">
+ <span>{{ scope.row.createTime }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+ <template #default="scope">
+ <el-tooltip content="取消授权" placement="top">
+ <el-button link type="primary" icon="CircleClose" @click="cancelAuthUser(scope.row)" v-hasPermi="['system:role:remove']"> </el-button>
+ </el-tooltip>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
+ <select-user ref="selectRef" :roleId="queryParams.roleId" @ok="handleQuery" />
+ </el-card>
+ </div>
+</template>
+
+<script setup name="AuthUser" lang="ts">
+import { allocatedUserList, authUserCancel, authUserCancelAll } from "@/api/system/role";
+import { UserQuery } from "@/api/system/user/types";
+import { UserVO } from "@/api/system/user/types";
+import SelectUser from "./selectUser.vue";
+
+
+const route = useRoute();
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const { sys_normal_disable } = toRefs<any>(proxy?.useDict("sys_normal_disable"));
+
+const userList = ref<UserVO[]>([]);
+const loading = ref(true);
+const showSearch = ref(true);
+const multiple = ref(true);
+const total = ref(0);
+const userIds = ref<Array<string | number>>([]);
+
+const queryFormRef = ref<ElFormInstance>();
+const selectRef = ref<InstanceType<typeof SelectUser>>();
+
+const queryParams = reactive<UserQuery>({
+ pageNum: 1,
+ pageSize: 10,
+ roleId: route.params.roleId as string,
+ userName: undefined,
+ phonenumber: undefined,
+});
+
+/** 查询授权用户列表 */
+const getList = async () => {
+ loading.value = true;
+ const res = await allocatedUserList(queryParams);
+ userList.value = res.rows;
+ total.value = res.total;
+ loading.value = false;
+}
+// 返回按钮
+const handleClose = () => {
+ const obj = { path: "/system/role" };
+ proxy?.$tab.closeOpenPage(obj);
+}
+/** 搜索按钮操作 */
+const handleQuery = () => {
+ queryParams.pageNum = 1;
+ getList();
+}
+/** 重置按钮操作 */
+const resetQuery = () => {
+ queryFormRef.value?.resetFields();
+ handleQuery();
+}
+// 多选框选中数据
+const handleSelectionChange = (selection: UserVO[]) => {
+ userIds.value = selection.map(item => item.userId);
+ multiple.value = !selection.length;
+}
+/** 打开授权用户表弹窗 */
+const openSelectUser = () => {
+ selectRef.value?.show();
+}
+/** 取消授权按钮操作 */
+const cancelAuthUser = async (row: UserVO) => {
+ await proxy?.$modal.confirm('确认要取消该用户"' + row.userName + '"角色吗?');
+ await authUserCancel({ userId: row.userId, roleId: queryParams.roleId });
+ await getList();
+ proxy?.$modal.msgSuccess("取消授权成功");
+}
+/** 批量取消授权按钮操作 */
+const cancelAuthUserAll = async () => {
+ const roleId = queryParams.roleId;
+ const uIds = userIds.value.join(",");
+ await proxy?.$modal.confirm("是否取消选中用户授权数据项?");
+ await authUserCancelAll({ roleId: roleId, userIds: uIds });
+ await getList();
+ proxy?.$modal.msgSuccess("取消授权成功");
+}
+
+onMounted(() => {
+ getList();
+});
+</script>
+
+<style lang="scss" scoped></style>
diff --git a/src/views/system/role/index.vue b/src/views/system/role/index.vue
new file mode 100644
index 0000000..bb6fc2c
--- /dev/null
+++ b/src/views/system/role/index.vue
@@ -0,0 +1,502 @@
+<template>
+ <div class="p-2">
+ <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
+ <div class="mb-[10px]" v-show="showSearch">
+ <el-card shadow="hover">
+ <el-form ref="queryFormRef" :model="queryParams" :inline="true" label-width="68px">
+ <el-form-item label="角色名称" prop="roleName">
+ <el-input v-model="queryParams.roleName" placeholder="请输入角色名称" clearable style="width: 240px" @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item label="权限字符" prop="roleKey">
+ <el-input v-model="queryParams.roleKey" placeholder="请输入权限字符" clearable style="width: 240px" @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item label="状态" prop="status">
+ <el-select v-model="queryParams.status" placeholder="角色状态" clearable style="width: 240px">
+ <el-option v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.label" :value="dict.value" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="创建时间" style="width: 308px">
+ <el-date-picker
+ v-model="dateRange"
+ value-format="YYYY-MM-DD"
+ type="daterange"
+ range-separator="-"
+ start-placeholder="开始日期"
+ end-placeholder="结束日期"
+ :default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
+ ></el-date-picker>
+ </el-form-item>
+
+ <el-form-item>
+ <el-button type="primary" @click="handleQuery" icon="Search">搜索</el-button>
+ <el-button @click="resetQuery" icon="Refresh">重置</el-button>
+ </el-form-item>
+ </el-form>
+ </el-card>
+ </div>
+ </transition>
+
+ <el-card shadow="hover">
+ <template #header>
+ <el-row :gutter="10">
+ <el-col :span="1.5">
+ <el-button type="primary" plain @click="handleAdd()" icon="Plus" v-hasPermi="['system:role:add']">新增</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button type="success" plain @click="handleUpdate()" :disabled="single" icon="Edit" v-hasPermi="['system:role:edit']">修改</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button type="danger" plain :disabled="ids.length === 0" @click="handleDelete()" v-hasPermi="['system:role:delete']">删除</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['system:role:export']">导出</el-button>
+ </el-col>
+ <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
+ </el-row>
+ </template>
+
+ <el-table ref="roleTableRef" v-loading="loading" :data="roleList" @selection-change="handleSelectionChange">
+ <el-table-column type="selection" width="55" align="center" />
+ <el-table-column label="角色编号" prop="roleId" width="120" v-if="false" />
+ <el-table-column label="角色名称" prop="roleName" :show-overflow-tooltip="true" width="150" />
+ <el-table-column label="权限字符" prop="roleKey" :show-overflow-tooltip="true" width="200" />
+ <el-table-column label="显示顺序" prop="roleSort" width="100" />
+ <el-table-column label="状态" align="center" width="100">
+ <template #default="scope">
+ <el-switch v-model="scope.row.status" active-value="0" inactive-value="1" @change="handleStatusChange(scope.row)"></el-switch>
+ </template>
+ </el-table-column>
+ <el-table-column label="创建时间" align="center" prop="createTime">
+ <template #default="scope">
+ <span>{{ parseTime(scope.row.createTime) }}</span>
+ </template>
+ </el-table-column>
+
+ <el-table-column fixed="right" label="操作" width="180">
+ <template #default="scope">
+ <el-tooltip content="修改" placement="top" v-if="scope.row.roleId !== 1">
+ <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:role:edit']"></el-button>
+ </el-tooltip>
+ <el-tooltip content="删除" placement="top" v-if="scope.row.roleId !== 1">
+ <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:role:remove']"></el-button>
+ </el-tooltip>
+ <el-tooltip content="数据权限" placement="top" v-if="scope.row.roleId !== 1">
+ <el-button link type="primary" icon="CircleCheck" @click="handleDataScope(scope.row)" v-hasPermi="['system:role:edit']"></el-button>
+ </el-tooltip>
+ <el-tooltip content="分配用户" placement="top" v-if="scope.row.roleId !== 1">
+ <el-button link type="primary" icon="User" @click="handleAuthUser(scope.row)" v-hasPermi="['system:role:edit']"></el-button>
+ </el-tooltip>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <pagination
+ v-if="total > 0"
+ v-model:total="total"
+ v-model:page="queryParams.pageNum"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </el-card>
+
+ <el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
+ <el-form ref="roleFormRef" :model="form" :rules="rules" label-width="100px">
+ <el-form-item label="角色名称" prop="roleName">
+ <el-input v-model="form.roleName" placeholder="请输入角色名称" />
+ </el-form-item>
+ <el-form-item prop="roleKey">
+ <template #label>
+ <span>
+ <el-tooltip content="控制器中定义的权限字符,如:@SaCheckRole('admin')" placement="top">
+ <el-icon><question-filled /></el-icon>
+ </el-tooltip>
+ 权限字符
+ </span>
+ </template>
+ <el-input v-model="form.roleKey" placeholder="请输入权限字符" />
+ </el-form-item>
+ <el-form-item label="角色顺序" prop="roleSort">
+ <el-input-number v-model="form.roleSort" controls-position="right" :min="0" />
+ </el-form-item>
+ <el-form-item label="状态">
+ <el-radio-group v-model="form.status">
+ <el-radio v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.value">{{
+ dict.label
+ }}</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="菜单权限">
+ <el-checkbox v-model="menuExpand" @change="handleCheckedTreeExpand($event, 'menu')">展开/折叠</el-checkbox>
+ <el-checkbox v-model="menuNodeAll" @change="handleCheckedTreeNodeAll($event, 'menu')">全选/全不选</el-checkbox>
+ <el-checkbox v-model="form.menuCheckStrictly" @change="handleCheckedTreeConnect($event, 'menu')">父子联动</el-checkbox>
+ <el-tree
+ class="tree-border"
+ :data="menuOptions"
+ show-checkbox
+ ref="menuRef"
+ node-key="id"
+ :check-strictly="!form.menuCheckStrictly"
+ empty-text="加载中,请稍候"
+ :props="{ label: 'label', children: 'children' }"
+ ></el-tree>
+ </el-form-item>
+ <el-form-item label="备注">
+ <el-input v-model="form.remark" type="textarea" placeholder="请输入内容"></el-input>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitForm">确 定</el-button>
+ <el-button @click="cancel">取 消</el-button>
+ </div>
+ </template>
+ </el-dialog>
+
+ <!-- 分配角色数据权限对话框 -->
+ <el-dialog :title="dialog.title" v-model="openDataScope" width="500px" append-to-body>
+ <el-form :model="form" label-width="80px" ref="dataScopeRef">
+ <el-form-item label="角色名称">
+ <el-input v-model="form.roleName" :disabled="true" />
+ </el-form-item>
+ <el-form-item label="权限字符">
+ <el-input v-model="form.roleKey" :disabled="true" />
+ </el-form-item>
+ <el-form-item label="权限范围">
+ <el-select v-model="form.dataScope" @change="dataScopeSelectChange">
+ <el-option v-for="item in dataScopeOptions" :key="item.value" :label="item.label" :value="item.value"></el-option>
+ </el-select>
+ </el-form-item>
+ <el-form-item label="数据权限" v-show="form.dataScope === '2'">
+ <el-checkbox v-model="deptExpand" @change="handleCheckedTreeExpand($event, 'dept')">展开/折叠</el-checkbox>
+ <el-checkbox v-model="deptNodeAll" @change="handleCheckedTreeNodeAll($event, 'dept')">全选/全不选</el-checkbox>
+ <el-checkbox v-model="form.deptCheckStrictly" @change="handleCheckedTreeConnect($event, 'dept')">父子联动</el-checkbox>
+ <el-tree
+ class="tree-border"
+ :data="deptOptions"
+ show-checkbox
+ default-expand-all
+ ref="deptRef"
+ node-key="id"
+ :check-strictly="!form.deptCheckStrictly"
+ empty-text="加载中,请稍候"
+ :props="{ label: 'label', children: 'children' }"
+ ></el-tree>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitDataScope">确 定</el-button>
+ <el-button @click="cancelDataScope">取 消</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup name="Role" lang="ts">
+import { addRole, changeRoleStatus, dataScope, delRole, getRole, listRole, updateRole, deptTreeSelect } from "@/api/system/role";
+import { roleMenuTreeselect, treeselect as menuTreeselect } from '@/api/system/menu/index';
+import { RoleVO, RoleForm, RoleQuery, DeptTreeOption } from '@/api/system/role/types';
+import { MenuTreeOption, RoleMenuTree } from '@/api/system/menu/types';
+
+const router = useRouter();
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const { sys_normal_disable } = toRefs<any>(proxy?.useDict('sys_normal_disable'));
+
+const roleList = ref<RoleVO[]>();
+const loading = ref(true)
+const showSearch = ref(true)
+const ids = ref<Array<string | number>>([])
+const single = ref(true)
+const multiple = ref(true)
+const total = ref(0)
+const dateRange = ref<[DateModelType, DateModelType]>(['', ''])
+const menuOptions = ref<MenuTreeOption[]>([])
+const menuExpand = ref(false)
+const menuNodeAll = ref(false)
+const deptExpand = ref(true)
+const deptNodeAll = ref(false)
+const deptOptions = ref<DeptTreeOption[]>([])
+const openDataScope = ref(false)
+
+/** 数据范围选项*/
+const dataScopeOptions = ref([
+ { value: "1", label: "全部数据权限" },
+ { value: "2", label: "自定数据权限" },
+ { value: "3", label: "本部门数据权限" },
+ { value: "4", label: "本部门及以下数据权限" },
+ { value: "5", label: "仅本人数据权限" }
+])
+
+const queryFormRef = ref<ElFormInstance>();
+const roleFormRef = ref<ElFormInstance>();
+const dataScopeRef = ref<ElFormInstance>();
+const menuRef = ref<ElTreeInstance>();
+const deptRef = ref<ElTreeInstance>();
+
+const initForm: RoleForm = {
+ roleId: undefined,
+ roleSort: 1,
+ status: '0',
+ roleName: '',
+ roleKey: '',
+ menuCheckStrictly: true,
+ deptCheckStrictly: true,
+ remark: '',
+ dataScope: '1',
+ menuIds: [],
+ deptIds: [],
+}
+
+const data = reactive<PageData<RoleForm, RoleQuery>>({
+ form: { ...initForm },
+ queryParams: {
+ pageNum: 1,
+ pageSize: 10,
+ roleName: '',
+ roleKey: '',
+ status: '',
+ },
+ rules: {
+ roleName: [{ required: true, message: "角色名称不能为空", trigger: "blur" }],
+ roleKey: [{ required: true, message: "权限字符不能为空", trigger: "blur" }],
+ roleSort: [{ required: true, message: "角色顺序不能为空", trigger: "blur" }]
+ }
+})
+const { form, queryParams, rules } = toRefs(data)
+
+const dialog = reactive<DialogOption>({
+ visible: false,
+ title: ''
+});
+
+/**
+ * 查询角色列表
+ */
+const getList = () => {
+ loading.value = true
+ listRole(proxy?.addDateRange(queryParams.value, dateRange.value)).then(res => {
+ roleList.value = res.rows
+ total.value = res.total
+ loading.value = false
+ })
+}
+
+/**
+ * 搜索按钮操作
+ */
+const handleQuery = () => {
+ queryParams.value.pageNum = 1;
+ getList();
+}
+
+/** 重置 */
+const resetQuery = () => {
+ dateRange.value = ['', '']
+ queryFormRef.value?.resetFields();
+ handleQuery();
+}
+/**删除按钮操作 */
+const handleDelete = async (row?: RoleVO) => {
+ const roleids = row?.roleId || ids.value;
+ await proxy?.$modal.confirm('是否确认删除角色编号为' + roleids + '数据项目');
+ await delRole(roleids);
+ getList();
+ proxy?.$modal.msgSuccess('删除成功');
+}
+
+/** 导出按钮操作 */
+const handleExport = () => {
+ proxy?.download("system/role/export", {
+ ...queryParams.value,
+ }, `role_${new Date().getTime()}.xlsx`)
+}
+/** 多选框选中数据 */
+const handleSelectionChange = (selection: RoleVO[]) => {
+ ids.value = selection.map((item: RoleVO) => item.roleId);
+ single.value = selection.length != 1;
+ multiple.value = !selection.length;
+}
+
+/** 角色状态修改 */
+const handleStatusChange = async (row: RoleVO) => {
+ let text = row.status === "0" ? "启用" : "停用";
+ try {
+ await proxy?.$modal.confirm('确认要"' + text + '""' + row.roleName + '"角色吗?');
+ await changeRoleStatus(row.roleId, row.status);
+ proxy?.$modal.msgSuccess(text + "成功");
+ } catch {
+ row.status = row.status === "0" ? "1" : "0";
+ }
+}
+
+/** 分配用户 */
+const handleAuthUser = (row: RoleVO) => {
+ router.push("/system/role-auth/user/" + row.roleId);
+}
+
+/** 查询菜单树结构 */
+const getMenuTreeselect = async () => {
+ const res = await menuTreeselect();
+ menuOptions.value = res.data;
+}
+/** 所有部门节点数据 */
+const getDeptAllCheckedKeys = (): any => {
+ // 目前被选中的部门节点
+ let checkedKeys = deptRef.value?.getCheckedKeys();
+ // 半选中的部门节点
+ let halfCheckedKeys = deptRef.value?.getHalfCheckedKeys();
+ if (halfCheckedKeys) {
+ checkedKeys?.unshift.apply(checkedKeys, halfCheckedKeys);
+ }
+ return checkedKeys
+}
+/** 重置新增的表单以及其他数据 */
+const reset = () => {
+ menuRef.value?.setCheckedKeys([]);
+ menuExpand.value = false
+ menuNodeAll.value = false
+ deptExpand.value = true
+ deptNodeAll.value = false
+ form.value = { ...initForm };
+ roleFormRef.value?.resetFields();
+
+}
+
+/** 添加角色 */
+const handleAdd = () => {
+ reset();
+ getMenuTreeselect();
+ dialog.visible = true;
+ dialog.title = "添加角色";
+}
+/** 修改角色 */
+const handleUpdate = async (row?: RoleVO) => {
+ reset();
+ const roleId = row?.roleId || ids.value[0]
+ const { data } = await getRole(roleId);
+ Object.assign(form.value, data);
+ form.value.roleSort = Number(form.value.roleSort);
+ const res = await getRoleMenuTreeselect(roleId);
+ dialog.title = "修改角色";
+ dialog.visible = true;
+ res.checkedKeys.forEach((v) => {
+ nextTick(() => {
+ menuRef.value?.setChecked(v, true, false);
+ })
+ })
+
+}
+/** 根据角色ID查询菜单树结构 */
+const getRoleMenuTreeselect = (roleId: string | number) => {
+ return roleMenuTreeselect(roleId).then((res): RoleMenuTree => {
+ menuOptions.value = res.data.menus;
+ return res.data;
+ })
+}
+/** 根据角色ID查询部门树结构 */
+const getRoleDeptTreeSelect = async (roleId: string | number) => {
+ const res = await deptTreeSelect(roleId);
+ deptOptions.value = res.data.depts;
+ return res.data;
+}
+/** 树权限(展开/折叠)*/
+const handleCheckedTreeExpand = (value: boolean, type: string) => {
+ if (type == "menu") {
+ let treeList = menuOptions.value;
+ for (let i = 0; i < treeList.length; i++) {
+ if (menuRef.value) {
+ menuRef.value.store.nodesMap[treeList[i].id].expanded = value;
+ }
+ }
+ } else if (type == "dept") {
+ let treeList = deptOptions.value;
+ for (let i = 0; i < treeList.length; i++) {
+ if (deptRef.value) {
+ deptRef.value.store.nodesMap[treeList[i].id].expanded = value;
+ }
+ }
+ }
+}
+/** 树权限(全选/全不选) */
+const handleCheckedTreeNodeAll = (value: any, type: string) => {
+ if (type == "menu") {
+ menuRef.value?.setCheckedNodes(value ? menuOptions.value as any : []);
+ } else if (type == "dept") {
+ deptRef.value?.setCheckedNodes(value ? deptOptions.value as any : []);
+ }
+}
+/** 树权限(父子联动) */
+const handleCheckedTreeConnect = (value: any, type: string) => {
+ if (type == "menu") {
+ form.value.menuCheckStrictly = value;
+ } else if (type == "dept") {
+ form.value.deptCheckStrictly = value;
+ }
+}
+/** 所有菜单节点数据 */
+const getMenuAllCheckedKeys = (): any => {
+ // 目前被选中的菜单节点
+ let checkedKeys = menuRef.value?.getCheckedKeys();
+ // 半选中的菜单节点
+ let halfCheckedKeys = menuRef.value?.getHalfCheckedKeys();
+ if (halfCheckedKeys) {
+ checkedKeys?.unshift.apply(checkedKeys, halfCheckedKeys);
+ }
+ return checkedKeys;
+}
+/** 提交按钮 */
+const submitForm = () => {
+ roleFormRef.value?.validate(async (valid: boolean) => {
+ if (valid) {
+ form.value.menuIds = getMenuAllCheckedKeys()
+ form.value.roleId ? await updateRole(form.value) : await addRole(form.value);
+ proxy?.$modal.msgSuccess("操作成功")
+ dialog.visible = false
+ getList()
+ }
+ })
+}
+/** 取消按钮 */
+const cancel = () => {
+ reset()
+ dialog.visible = false;
+}
+/** 选择角色权限范围触发 */
+const dataScopeSelectChange = (value: string) => {
+ if (value !== "2") {
+ deptRef.value?.setCheckedKeys([])
+ }
+}
+/** 分配数据权限操作 */
+const handleDataScope = async (row: RoleVO) => {
+ const response = await getRole(row.roleId);
+ Object.assign(form.value, response.data);
+ const res = await getRoleDeptTreeSelect(row.roleId);
+ openDataScope.value = true;
+ dialog.title = "分配数据权限";
+ await nextTick(() => {
+ deptRef.value?.setCheckedKeys(res.checkedKeys);
+ })
+}
+/** 提交按钮(数据权限) */
+const submitDataScope = async () => {
+ if (form.value.roleId) {
+ form.value.deptIds = getDeptAllCheckedKeys();
+ await dataScope(form.value);
+ proxy?.$modal.msgSuccess("修改成功");
+ openDataScope.value = false;
+ getList();
+ }
+}
+/** 取消按钮(数据权限)*/
+const cancelDataScope = () => {
+ dataScopeRef.value?.resetFields();
+ form.value = { ...initForm };
+ openDataScope.value = false;
+}
+
+onMounted(() => {
+ getList();
+});
+</script>
diff --git a/src/views/system/role/selectUser.vue b/src/views/system/role/selectUser.vue
new file mode 100644
index 0000000..e0cd433
--- /dev/null
+++ b/src/views/system/role/selectUser.vue
@@ -0,0 +1,132 @@
+<template>
+ <el-row>
+ <el-dialog title="选择用户" v-model="visible" width="800px" top="5vh" append-to-body>
+ <el-form :model="queryParams" ref="queryFormRef" :inline="true">
+ <el-form-item label="用户名称" prop="userName">
+ <el-input v-model="queryParams.userName" placeholder="请输入用户名称" clearable @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item label="手机号码" prop="phonenumber">
+ <el-input v-model="queryParams.phonenumber" placeholder="请输入手机号码" clearable @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+ <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+ </el-form-item>
+ </el-form>
+ <el-row>
+ <el-table @row-click="clickRow" ref="tableRef" :data="userList" @selection-change="handleSelectionChange" height="260px">
+ <el-table-column type="selection" width="55"></el-table-column>
+ <el-table-column label="用户名称" prop="userName" :show-overflow-tooltip="true" />
+ <el-table-column label="用户昵称" prop="nickName" :show-overflow-tooltip="true" />
+ <el-table-column label="邮箱" prop="email" :show-overflow-tooltip="true" />
+ <el-table-column label="手机" prop="phonenumber" :show-overflow-tooltip="true" />
+ <el-table-column label="状态" align="center" prop="status">
+ <template #default="scope">
+ <dict-tag :options="sys_normal_disable" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column label="创建时间" align="center" prop="createTime" width="180">
+ <template #default="scope">
+ <span>{{ parseTime(scope.row.createTime) }}</span>
+ </template>
+ </el-table-column>
+ </el-table>
+ <pagination v-if="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
+ </el-row>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="handleSelectUser">确 定</el-button>
+ <el-button @click="visible = false">取 消</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </el-row>
+</template>
+
+<script setup name="SelectUser" lang="ts">
+import { authUserSelectAll, unallocatedUserList } from "@/api/system/role";
+import { UserVO } from '@/api/system/user/types';
+import { UserQuery } from '@/api/system/user/types';
+
+
+const props = defineProps({
+ roleId: {
+ type: [Number, String]
+ }
+})
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const { sys_normal_disable } = toRefs<any>(proxy?.useDict('sys_normal_disable'));
+
+const userList = ref<UserVO[]>([]);
+const visible = ref(false);
+const total = ref(0);
+const userIds = ref<Array<string | number>>([]);
+
+const queryParams = reactive<UserQuery>({
+ pageNum: 1,
+ pageSize: 10,
+ roleId: undefined,
+ userName: undefined,
+ phonenumber: undefined
+})
+
+const tableRef = ref<ElTableInstance>();
+const queryFormRef = ref<ElFormInstance>();
+
+const show = () => {
+ queryParams.roleId = props.roleId;
+ getList();
+ visible.value = true;
+}
+
+/**
+ * 选择行
+ */
+const clickRow = (row: any) => {
+ // ele的bug
+ tableRef.value?.toggleRowSelection(row, false);
+}
+/** 多选框选中数据 */
+const handleSelectionChange = (selection: UserVO[]) => {
+ userIds.value = selection.map((item: UserVO) => item.userId);
+}
+
+/** 查询数据 */
+const getList = async () => {
+ const res = await unallocatedUserList(queryParams);
+ userList.value = res.rows;
+ total.value = res.total;
+}
+/** 搜索按钮操作 */
+const handleQuery = () => {
+ queryParams.pageNum = 1;
+ getList();
+}
+/** 重置按钮操作 */
+const resetQuery = () => {
+ queryFormRef.value?.resetFields();
+ getList();
+}
+
+const emit = defineEmits(["ok"]);
+/**选择授权用户操作 */
+const handleSelectUser = async () => {
+ const roleId = queryParams.roleId;
+ const ids = userIds.value.join(',');
+ if (ids == "") {
+ proxy?.$modal.msgError('请选择要分配的用户');
+ return;
+ }
+ await authUserSelectAll({ roleId, userIds: ids });
+ proxy?.$modal.msgSuccess('分配成功');
+ emit('ok');
+ visible.value = false;
+}
+// 暴露
+defineExpose({
+ show,
+});
+</script>
+
+<style scoped></style>
diff --git a/src/views/system/tenant/index.vue b/src/views/system/tenant/index.vue
new file mode 100644
index 0000000..9a7fdf3
--- /dev/null
+++ b/src/views/system/tenant/index.vue
@@ -0,0 +1,347 @@
+<template>
+ <div class="p-2">
+ <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
+ <div class="mb-[10px]" v-show="showSearch">
+ <el-card shadow="hover">
+ <el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px">
+ <el-form-item label="租户编号" prop="tenantId">
+ <el-input v-model="queryParams.tenantId" placeholder="请输入租户编号" clearable style="width: 240px" @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item label="联系人" prop="contactUserName">
+ <el-input v-model="queryParams.contactUserName" placeholder="请输入联系人" clearable style="width: 240px" @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item label="联系电话" prop="contactPhone">
+ <el-input v-model="queryParams.contactPhone" placeholder="请输入联系电话" clearable style="width: 240px" @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item label="企业名称" prop="companyName">
+ <el-input v-model="queryParams.companyName" placeholder="请输入企业名称" clearable style="width: 240px" @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+ <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+ </el-form-item>
+ </el-form>
+ </el-card>
+ </div>
+ </transition>
+
+ <el-card shadow="hover">
+ <template #header>
+ <el-row :gutter="10" class="mb8">
+ <el-col :span="1.5">
+ <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['system:tenant:add']">新增</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['system:tenant:edit']"
+ >修改</el-button
+ >
+ </el-col>
+ <el-col :span="1.5">
+ <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['system:tenant:remove']">
+ 删除
+ </el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['system:tenant:export']">导出</el-button>
+ </el-col>
+ <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
+ </el-row>
+ </template>
+
+ <el-table v-loading="loading" :data="tenantList" @selection-change="handleSelectionChange">
+ <el-table-column type="selection" width="55" align="center" />
+ <el-table-column label="id" align="center" prop="id" v-if="false" />
+ <el-table-column label="租户编号" align="center" prop="tenantId" />
+ <el-table-column label="联系人" align="center" prop="contactUserName" />
+ <el-table-column label="联系电话" align="center" prop="contactPhone" />
+ <el-table-column label="企业名称" align="center" prop="companyName" />
+ <el-table-column label="社会信用代码" align="center" prop="licenseNumber" />
+ <el-table-column label="过期时间" align="center" prop="expireTime" width="180">
+ <template #default="scope">
+ <span>{{ parseTime(scope.row.expireTime, '{y}-{m}-{d}') }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="租户状态" align="center" prop="status">
+ <template #default="scope">
+ <el-switch v-model="scope.row.status" active-value="0" inactive-value="1" @change="handleStatusChange(scope.row)"></el-switch>
+ </template>
+ </el-table-column>
+ <el-table-column width="150" label="操作" align="center" fixed="right" class-name="small-padding fixed-width">
+ <template #default="scope">
+ <el-tooltip content="修改" placement="top">
+ <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:tenant:edit']"></el-button>
+ </el-tooltip>
+ <el-tooltip content="同步套餐" placement="top">
+ <el-button link type="primary" icon="Refresh" @click="handleSyncTenantPackage(scope.row)" v-hasPermi="['system:tenant:edit']">
+ </el-button>
+ </el-tooltip>
+ <el-tooltip content="删除" placement="top">
+ <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:tenant:remove']"></el-button>
+ </el-tooltip>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
+ </el-card>
+ <!-- 添加或修改租户对话框 -->
+ <el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
+ <el-form ref="tenantFormRef" :model="form" :rules="rules" label-width="80px">
+ <el-form-item label="企业名称" prop="companyName">
+ <el-input v-model="form.companyName" placeholder="请输入企业名称" />
+ </el-form-item>
+ <el-form-item label="联系人" prop="contactUserName">
+ <el-input v-model="form.contactUserName" placeholder="请输入联系人" />
+ </el-form-item>
+ <el-form-item label="联系电话" prop="contactPhone">
+ <el-input v-model="form.contactPhone" placeholder="请输入联系电话" />
+ </el-form-item>
+ <el-form-item v-if="!form.id" label="用户名" prop="username">
+ <el-input v-model="form.username" placeholder="请输入系统用户名" maxlength="30" />
+ </el-form-item>
+ <el-form-item v-if="!form.id" label="用户密码" prop="password">
+ <el-input type="password" v-model="form.password" placeholder="请输入系统用户密码" maxlength="20" />
+ </el-form-item>
+ <el-form-item label="租户套餐" prop="packageId">
+ <el-select v-model="form.packageId" :disabled="!!form.tenantId" placeholder="请选择租户套餐" clearable style="width: 100%">
+ <el-option v-for="item in packageList" :key="item.packageId" :label="item.packageName" :value="item.packageId" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="过期时间" prop="expireTime">
+ <el-date-picker clearable v-model="form.expireTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" placeholder="请选择过期时间">
+ </el-date-picker>
+ </el-form-item>
+ <el-form-item label="用户数量" prop="accountCount">
+ <el-input v-model="form.accountCount" placeholder="请输入用户数量" />
+ </el-form-item>
+ <el-form-item label="绑定域名" prop="domain">
+ <el-input v-model="form.domain" placeholder="请输入绑定域名" />
+ </el-form-item>
+ <el-form-item label="企业地址" prop="address">
+ <el-input v-model="form.address" placeholder="请输入企业地址" />
+ </el-form-item>
+ <el-form-item label="企业代码" prop="licenseNumber">
+ <el-input v-model="form.licenseNumber" placeholder="请输入统一社会信用代码" />
+ </el-form-item>
+ <el-form-item label="企业简介" prop="intro">
+ <el-input type="textarea" v-model="form.intro" placeholder="请输入企业简介" />
+ </el-form-item>
+ <el-form-item label="备注" prop="remark">
+ <el-input v-model="form.remark" placeholder="请输入备注" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
+ <el-button @click="cancel">取 消</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup name="Tenant" lang="ts">
+import { listTenant, getTenant, delTenant, addTenant, updateTenant, changeTenantStatus, syncTenantPackage } from '@/api/system/tenant';
+import { selectTenantPackage } from '@/api/system/tenantPackage';
+import { TenantForm, TenantQuery, TenantVO } from '@/api/system/tenant/types';
+import { TenantPkgVO } from '@/api/system/tenantPackage/types';
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+
+const tenantList = ref<TenantVO[]>([]);
+const packageList = ref<TenantPkgVO[]>([]);
+const buttonLoading = ref(false);
+const loading = ref(true);
+const showSearch = ref(true);
+const ids = ref<Array<string | number>>([]);
+const single = ref(true);
+const multiple = ref(true);
+const total = ref(0);
+
+const queryFormRef = ref<ElFormInstance>();
+const tenantFormRef = ref<ElFormInstance>();
+
+const dialog = reactive<DialogOption>({
+ visible: false,
+ title: ''
+});
+
+const initFormData: TenantForm = {
+ id: undefined,
+ tenantId: undefined,
+ contactUserName: '',
+ contactPhone: '',
+ username: '',
+ password: '',
+ companyName: '',
+ licenseNumber: '',
+ domain: '',
+ address: '',
+ intro: '',
+ remark: '',
+ packageId: '',
+ expireTime: '',
+ accountCount: 0,
+ status: '0',
+}
+const data = reactive<PageData<TenantForm, TenantQuery>>({
+ form: { ...initFormData },
+ queryParams: {
+ pageNum: 1,
+ pageSize: 10,
+ tenantId: '',
+ contactUserName: '',
+ contactPhone: '',
+ companyName: ''
+ },
+ rules: {
+ id: [{ required: true, message: "id不能为空", trigger: "blur" }],
+ tenantId: [{ required: true, message: "租户编号不能为空", trigger: "blur" }],
+ contactUserName: [{ required: true, message: "联系人不能为空", trigger: "blur" }],
+ contactPhone: [{ required: true, message: "联系电话不能为空", trigger: "blur" }],
+ companyName: [{ required: true, message: "企业名称不能为空", trigger: "blur" }],
+ username: [
+ { required: true, message: "用户名不能为空", trigger: "blur" },
+ { min: 2, max: 20, message: '用户名称长度必须介于 2 和 20 之间', trigger: 'blur' }
+ ],
+ password: [
+ { required: true, message: "密码不能为空", trigger: "blur" },
+ { min: 5, max: 20, message: '用户密码长度必须介于 5 和 20 之间', trigger: 'blur' }
+ ]
+ }
+});
+
+const { queryParams, form, rules } = toRefs(data);
+
+/** 查询所有租户套餐 */
+const getTenantPackage = async () => {
+ const res = await selectTenantPackage()
+ packageList.value = res.data;
+}
+
+/** 查询租户列表 */
+const getList = async () => {
+ loading.value = true;
+ const res = await listTenant(queryParams.value);
+ tenantList.value = res.rows;
+ total.value = res.total;
+ loading.value = false;
+}
+
+// 租户套餐状态修改
+const handleStatusChange = async (row: TenantVO) => {
+ let text = row.status === "0" ? "启用" : "停用";
+ try {
+ await proxy?.$modal.confirm('确认要"' + text + '""' + row.companyName + '"租户吗?');
+ await changeTenantStatus(row.id, row.tenantId, row.status);
+ proxy?.$modal.msgSuccess(text + "成功");
+ } catch {
+ row.status = row.status === "0" ? "1" : "0";
+ }
+
+
+}
+
+// 取消按钮
+const cancel = () => {
+ reset();
+ dialog.visible = false;
+}
+
+// 表单重置
+const reset = () => {
+ form.value = { ...initFormData };
+ tenantFormRef.value?.resetFields();
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+ queryParams.value.pageNum = 1;
+ getList();
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+ queryFormRef.value?.resetFields();
+ handleQuery();
+}
+
+// 多选框选中数据
+const handleSelectionChange = (selection: TenantVO[]) => {
+ ids.value = selection.map(item => item.id);
+ single.value = selection.length != 1;
+ multiple.value = !selection.length;
+}
+
+/** 新增按钮操作 */
+const handleAdd = () => {
+ reset();
+ getTenantPackage();
+ dialog.visible = true;
+ dialog.title = "添加租户";
+}
+
+/** 修改按钮操作 */
+const handleUpdate = async (row?: TenantVO) => {
+ reset();
+ await getTenantPackage();
+ const _id = row?.id || ids.value[0];
+ const res = await getTenant(_id);
+ Object.assign(form.value, res.data)
+ dialog.visible = true;
+ dialog.title = "修改租户";
+}
+
+/** 提交按钮 */
+const submitForm = () => {
+ tenantFormRef.value?.validate(async (valid: boolean) => {
+ if (valid) {
+ buttonLoading.value = true;
+ if (form.value.id) {
+ await updateTenant(form.value).finally(() => buttonLoading.value = false);
+ } else {
+ await addTenant(form.value).finally(() => buttonLoading.value = false);
+ }
+ proxy?.$modal.msgSuccess("操作成功");
+ dialog.visible = false;
+ await getList();
+ }
+ });
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (row?: TenantVO) => {
+ const _ids = row?.id || ids.value;
+ await proxy?.$modal.confirm('是否确认删除租户编号为"' + _ids + '"的数据项?')
+ loading.value = true;
+ await delTenant(_ids).finally(() => loading.value = false);
+ await getList();
+ proxy?.$modal.msgSuccess("删除成功");
+
+
+}
+
+/** 同步租户套餐按钮操作 */
+const handleSyncTenantPackage = async (row: TenantVO) => {
+ try {
+ await proxy?.$modal.confirm('是否确认同步租户套餐租户编号为"' + row.tenantId + '"的数据项?');
+ loading.value = true;
+ await syncTenantPackage(row.tenantId, row.packageId);
+ await getList();
+ proxy?.$modal.msgSuccess("同步成功");
+ } catch { return } finally {
+ loading.value = false;
+ }
+}
+
+/** 导出按钮操作 */
+const handleExport = () => {
+ proxy?.download('system/tenant/export', {
+ ...queryParams.value
+ }, `tenant_${new Date().getTime()}.xlsx`)
+}
+
+onMounted(() => {
+ getList();
+})
+</script>
diff --git a/src/views/system/tenantPackage/index.vue b/src/views/system/tenantPackage/index.vue
new file mode 100644
index 0000000..5311996
--- /dev/null
+++ b/src/views/system/tenantPackage/index.vue
@@ -0,0 +1,332 @@
+<template>
+ <div class="p-2">
+ <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
+ <div class="mb-[10px]" v-show="showSearch">
+ <el-card shadow="hover">
+ <el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px">
+ <el-form-item label="套餐名称" prop="packageName">
+ <el-input v-model="queryParams.packageName" placeholder="请输入套餐名称" clearable style="width: 240px" @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+ <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+ </el-form-item>
+ </el-form>
+ </el-card>
+ </div>
+ </transition>
+
+ <el-card shadow="hover">
+ <template #header>
+ <el-row :gutter="10" class="mb8">
+ <el-col :span="1.5">
+ <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['system:tenantPackage:add']"> 新增 </el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['system:tenantPackage:edit']">
+ 修改
+ </el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['system:tenantPackage:remove']">
+ 删除
+ </el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['system:tenantPackage:export']">导出 </el-button>
+ </el-col>
+ <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
+ </el-row>
+ </template>
+
+ <el-table v-loading="loading" :data="tenantPackageList" @selection-change="handleSelectionChange">
+ <el-table-column type="selection" width="55" align="center" />
+ <el-table-column label="租户套餐id" align="center" prop="packageId" v-if="false" />
+ <el-table-column label="套餐名称" align="center" prop="packageName" />
+ <el-table-column label="备注" align="center" prop="remark" />
+ <el-table-column label="状态" align="center" prop="status">
+ <template #default="scope">
+ <el-switch v-model="scope.row.status" active-value="0" inactive-value="1" @click="handleStatusChange(scope.row)"></el-switch>
+ </template>
+ </el-table-column>
+ <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+ <template #default="scope">
+ <el-tooltip content="修改" placement="top">
+ <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:tenantPackage:edit']"></el-button>
+ </el-tooltip>
+ <el-tooltip content="删除" placement="top">
+ <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:tenantPackage:remove']"></el-button>
+ </el-tooltip>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
+ </el-card>
+
+ <!-- 添加或修改租户套餐对话框 -->
+ <el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
+ <el-form ref="tenantPackageFormRef" :model="form" :rules="rules" label-width="80px">
+ <el-form-item label="套餐名称" prop="packageName">
+ <el-input v-model="form.packageName" placeholder="请输入套餐名称" />
+ </el-form-item>
+ <el-form-item label="关联菜单">
+ <el-checkbox v-model="menuExpand" @change="handleCheckedTreeExpand($event, 'menu')">展开/折叠</el-checkbox>
+ <el-checkbox v-model="menuNodeAll" @change="handleCheckedTreeNodeAll($event, 'menu')">全选/全不选 </el-checkbox>
+ <el-checkbox v-model="form.menuCheckStrictly" @change="handleCheckedTreeConnect($event, 'menu')">父子联动 </el-checkbox>
+ <el-tree
+ class="tree-border"
+ :data="menuOptions"
+ show-checkbox
+ ref="menuTreeRef"
+ node-key="id"
+ :check-strictly="!form.menuCheckStrictly"
+ empty-text="加载中,请稍候"
+ :props="{ label: 'label', children: 'children' }"
+ ></el-tree>
+ </el-form-item>
+ <el-form-item label="备注" prop="remark">
+ <el-input v-model="form.remark" placeholder="请输入备注" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
+ <el-button @click="cancel">取 消</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup name="TenantPackage" lang="ts">
+import {
+ listTenantPackage,
+ getTenantPackage,
+ delTenantPackage,
+ addTenantPackage,
+ updateTenantPackage,
+ changePackageStatus
+} from "@/api/system/tenantPackage";
+import { treeselect as menuTreeselect, tenantPackageMenuTreeselect } from "@/api/system/menu";
+import { TenantPkgForm, TenantPkgQuery, TenantPkgVO } from "@/api/system/tenantPackage/types";
+import { MenuTreeOption } from "@/api/system/menu/types";
+import to from "await-to-js";
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+
+const tenantPackageList = ref<TenantPkgVO[]>([]);
+const buttonLoading = ref(false);
+const loading = ref(true);
+const showSearch = ref(true);
+const ids = ref<Array<string | number>>([]);
+const single = ref(true);
+const multiple = ref(true);
+const total = ref(0);
+const menuExpand = ref(false);
+const menuNodeAll = ref(false);
+const menuOptions = ref<MenuTreeOption[]>([]);
+
+const menuTreeRef = ref<ElTreeInstance>();
+const queryFormRef = ref<ElFormInstance>();
+const tenantPackageFormRef = ref<ElFormInstance>();
+
+const dialog = reactive<DialogOption>({
+ visible: false,
+ title: ""
+});
+
+
+const initFormData: TenantPkgForm = {
+ packageId: undefined,
+ packageName: "",
+ menuIds: "",
+ remark: "",
+ menuCheckStrictly: true
+};
+const data = reactive<PageData<TenantPkgForm, TenantPkgQuery>>({
+ form: { ...initFormData },
+ queryParams: {
+ pageNum: 1,
+ pageSize: 10,
+ packageName: ""
+ },
+ rules: {
+ packageId: [{ required: true, message: "租户套餐id不能为空", trigger: "blur" }],
+ packageName: [{ required: true, message: "套餐名称不能为空", trigger: "blur" }]
+ }
+});
+
+const { queryParams, form, rules } = toRefs(data);
+
+/** 查询菜单树结构 */
+const getMenuTreeselect = async () => {
+ const { data } = await menuTreeselect();
+ menuOptions.value = data;
+};
+
+// 所有菜单节点数据
+const getMenuAllCheckedKeys = (): any => {
+ // 目前被选中的菜单节点
+ let checkedKeys = menuTreeRef.value?.getCheckedKeys();
+ // 半选中的菜单节点
+ let halfCheckedKeys = menuTreeRef.value?.getHalfCheckedKeys();
+ if (halfCheckedKeys) {
+ checkedKeys?.unshift.apply(checkedKeys, halfCheckedKeys);
+ }
+ return checkedKeys;
+};
+
+/** 根据租户套餐ID查询菜单树结构 */
+const getPackageMenuTreeselect = async (packageId: string | number) => {
+ const res = await tenantPackageMenuTreeselect(packageId);
+ menuOptions.value = res.data.menus;
+ return Promise.resolve(res);
+};
+
+/** 查询租户套餐列表 */
+const getList = async () => {
+ loading.value = true;
+ const res = await listTenantPackage(queryParams.value);
+ tenantPackageList.value = res.rows;
+ total.value = res.total;
+ loading.value = false;
+};
+
+// 租户套餐状态修改
+const handleStatusChange = async (row: TenantPkgVO) => {
+ let text = row.status === "0" ? "启用" : "停用";
+ const [err] = await to(proxy?.$modal.confirm("确认要\"" + text + "\"\"" + row.packageName + "\"套餐吗?") as Promise<any>);
+ if (err) {
+ row.status = row.status === "0" ? "1" : "0";
+ } else {
+ await changePackageStatus(row.packageId, row.status);
+ proxy?.$modal.msgSuccess(text + "成功");
+ }
+};
+
+// 取消按钮
+const cancel = () => {
+ reset();
+ dialog.visible = false;
+};
+
+// 表单重置
+const reset = () => {
+ menuTreeRef.value?.setCheckedKeys([]);
+ menuExpand.value = false;
+ menuNodeAll.value = false;
+ form.value = { ...initFormData };
+ tenantPackageFormRef.value?.resetFields();
+};
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+ queryParams.value.pageNum = 1;
+ getList();
+};
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+ queryFormRef.value?.resetFields();
+ handleQuery();
+};
+
+// 多选框选中数据
+const handleSelectionChange = (selection: TenantPkgVO[]) => {
+ ids.value = selection.map(item => item.packageId);
+ single.value = selection.length != 1;
+ multiple.value = !selection.length;
+};
+
+// 树权限(展开/折叠)
+const handleCheckedTreeExpand = (value: CheckboxValueType, type: string) => {
+ if (type == "menu") {
+ let treeList = menuOptions.value;
+ for (let i = 0; i < treeList.length; i++) {
+ if (menuTreeRef.value) {
+ menuTreeRef.value.store.nodesMap[treeList[i].id].expanded = value as boolean;
+ }
+ }
+ }
+};
+
+// 树权限(全选/全不选)
+const handleCheckedTreeNodeAll = (value: CheckboxValueType, type: string) => {
+ if (type == "menu") {
+ menuTreeRef.value?.setCheckedNodes(value ? menuOptions.value as any : []);
+ }
+};
+
+// 树权限(父子联动)
+const handleCheckedTreeConnect = (value: CheckboxValueType, type: string) => {
+ if (type == "menu") {
+ form.value.menuCheckStrictly = value as boolean;
+ }
+};
+
+/** 新增按钮操作 */
+const handleAdd = () => {
+ reset();
+ getMenuTreeselect();
+ dialog.visible = true;
+ dialog.title = "添加租户套餐";
+};
+
+/** 修改按钮操作 */
+const handleUpdate = async (row?: TenantPkgVO) => {
+ reset();
+ const _packageId = row?.packageId || ids.value[0];
+ const response = await getTenantPackage(_packageId);
+ form.value = response.data;
+ const res = await getPackageMenuTreeselect(_packageId);
+ dialog.visible = true;
+ dialog.title = "修改租户套餐";
+ res.data.checkedKeys.forEach((v) => {
+ nextTick(() => {
+ menuTreeRef.value?.setChecked(v, true, false);
+ });
+ });
+};
+
+/** 提交按钮 */
+const submitForm = () => {
+ tenantPackageFormRef.value?.validate(async (valid: boolean) => {
+ if (valid) {
+ buttonLoading.value = true;
+ form.value.menuIds = getMenuAllCheckedKeys();
+ if (form.value.packageId != null) {
+ await updateTenantPackage(form.value).finally(() => buttonLoading.value = false);
+ } else {
+ await addTenantPackage(form.value).finally(() => buttonLoading.value = false);
+ }
+ proxy?.$modal.msgSuccess("操作成功");
+ dialog.visible = false;
+ await getList();
+ }
+ });
+};
+
+/** 删除按钮操作 */
+const handleDelete = async (row?: TenantPkgVO) => {
+ const _packageIds = row?.packageId || ids.value;
+ await proxy?.$modal.confirm("是否确认删除租户套餐编号为\"" + _packageIds + "\"的数据项?").finally(() => {
+ loading.value = false;
+ });
+ await delTenantPackage(_packageIds);
+ loading.value = true;
+ await getList();
+ proxy?.$modal.msgSuccess("删除成功");
+};
+
+/** 导出按钮操作 */
+const handleExport = () => {
+ proxy?.download("system/tenantPackage/export", {
+ ...queryParams.value
+ }, `tenantPackage_${new Date().getTime()}.xlsx`);
+};
+
+onMounted(() => {
+ getList();
+});
+</script>
diff --git a/src/views/system/user/authRole.vue b/src/views/system/user/authRole.vue
new file mode 100644
index 0000000..e869ba3
--- /dev/null
+++ b/src/views/system/user/authRole.vue
@@ -0,0 +1,127 @@
+<template>
+ <div class="p-2">
+ <div class="panel">
+ <h4 class="panel-title">基本信息</h4>
+ <el-form :model="form" label-width="80px" :inline="true">
+ <el-row :gutter="10">
+ <el-col :span="2.5">
+ <el-form-item label="用户昵称" prop="nickName">
+ <el-input v-model="form.nickName" disabled />
+ </el-form-item>
+ </el-col>
+ <el-col :span="2.5">
+ <el-form-item label="登录账号" prop="userName">
+ <el-input v-model="form.userName" disabled />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ </div>
+ <div class="panel">
+ <h4 class="panel-title">角色信息</h4>
+ <div>
+ <el-table
+ v-loading="loading"
+ :row-key="getRowKey"
+ @row-click="clickRow"
+ ref="tableRef"
+ @selection-change="handleSelectionChange"
+ :data="roles.slice((pageNum - 1) * pageSize, pageNum * pageSize)"
+ >
+ <el-table-column label="序号" width="55" type="index" align="center">
+ <template #default="scope">
+ <span>{{ (pageNum - 1) * pageSize + scope.$index + 1 }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column type="selection" :reserve-selection="true" width="55"></el-table-column>
+ <el-table-column label="角色编号" align="center" prop="roleId" />
+ <el-table-column label="角色名称" align="center" prop="roleName" />
+ <el-table-column label="权限字符" align="center" prop="roleKey" />
+ <el-table-column label="创建时间" align="center" prop="createTime" width="180">
+ <template #default="scope">
+ <span>{{ parseTime(scope.row.createTime) }}</span>
+ </template>
+ </el-table-column>
+ </el-table>
+ <pagination v-show="total > 0" :total="total" v-model:page="pageNum" v-model:limit="pageSize" />
+ <div style="text-align: center;margin-left:-120px;margin-top:30px;">
+ <el-button type="primary" @click="submitForm()">提交</el-button>
+ <el-button @click="close()">返回</el-button>
+ </div>
+ <div></div>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script setup name="AuthRole" lang="ts">
+import { RoleVO } from "@/api/system/role/types";
+import { getAuthRole, updateAuthRole } from "@/api/system/user";
+import { UserForm } from "@/api/system/user/types";
+
+const route = useRoute();
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+
+const loading = ref(true);
+const total = ref(0);
+const pageNum = ref(1);
+const pageSize = ref(10);
+const roleIds = ref<Array<string | number>>([]);
+const roles = ref<RoleVO[]>([]);
+const form = ref<Partial<UserForm>>({
+ nickName: undefined,
+ userName: "",
+ userId: undefined
+});
+
+const tableRef = ref<ElTableInstance>();
+
+/** 单击选中行数据 */
+const clickRow = (row: RoleVO) => {
+ // ele的方法有问题,selected应该为可选参数
+ tableRef.value?.toggleRowSelection(row, false);
+};
+/** 多选框选中数据 */
+const handleSelectionChange = (selection: RoleVO[]) => {
+ roleIds.value = selection.map(item => item.roleId);
+};
+/** 保存选中的数据编号 */
+const getRowKey = (row: RoleVO): string => {
+ return String(row.roleId);
+};
+/** 关闭按钮 */
+const close = () => {
+ const obj = { path: "/system/user" };
+ proxy?.$tab.closeOpenPage(obj);
+};
+/** 提交按钮 */
+const submitForm = async () => {
+ const userId = form.value.userId;
+ const rIds = roleIds.value.join(",");
+ await updateAuthRole({ userId: userId as string, roleIds: rIds });
+ proxy?.$modal.msgSuccess("授权成功");
+ close();
+};
+
+const getList = async () => {
+ const userId = route.params && route.params.userId;
+ if (userId) {
+ loading.value = true;
+ const res = await getAuthRole(userId as string);
+ Object.assign(form.value, res.data.user);
+ Object.assign(roles.value, res.data.roles);
+ total.value = roles.value.length;
+ await nextTick(() => {
+ roles.value.forEach(row => {
+ if (row?.flag) {
+ tableRef.value?.toggleRowSelection(row, true);
+ }
+ });
+ });
+ loading.value = false;
+ }
+};
+onMounted(() => {
+ getList();
+});
+</script>
diff --git a/src/views/system/user/index.vue b/src/views/system/user/index.vue
new file mode 100644
index 0000000..100b5a7
--- /dev/null
+++ b/src/views/system/user/index.vue
@@ -0,0 +1,622 @@
+<template>
+ <div class="p-2">
+ <el-row :gutter="20">
+ <!-- 部门树 -->
+ <el-col :lg="4" :xs="24" style="">
+ <el-card shadow="hover">
+ <el-input v-model="deptName" placeholder="请输入部门名称" prefix-icon="Search" clearable />
+ <el-tree
+ class="mt-2"
+ ref="deptTreeRef"
+ node-key="id"
+ :data="deptOptions"
+ :props="{ label: 'label', children: 'children' }"
+ :expand-on-click-node="false"
+ :filter-node-method="filterNode"
+ highlight-current
+ default-expand-all
+ @node-click="handleNodeClick"
+ />
+ </el-card>
+ </el-col>
+ <el-col :lg="20" :xs="24">
+ <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
+ <div class="mb-[10px]" v-show="showSearch">
+ <el-card shadow="hover">
+ <el-form ref="queryFormRef" :model="queryParams" :inline="true" label-width="68px">
+ <el-form-item label="用户名称" prop="userName">
+ <el-input v-model="queryParams.userName" placeholder="请输入用户名称" clearable style="width: 240px" @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item label="手机号码" prop="phonenumber">
+ <el-input
+ v-model="queryParams.phonenumber"
+ placeholder="请输入手机号码"
+ clearable
+ style="width: 240px"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+
+ <el-form-item label="状态" prop="status">
+ <el-select v-model="queryParams.status" placeholder="用户状态" clearable style="width: 240px">
+ <el-option v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.label" :value="dict.value" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="创建时间" style="width: 308px;">
+ <el-date-picker
+ v-model="dateRange"
+ value-format="YYYY-MM-DD"
+ type="daterange"
+ range-separator="-"
+ start-placeholder="开始日期"
+ end-placeholder="结束日期"
+ ></el-date-picker>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" @click="handleQuery" icon="Search">搜索</el-button>
+ <el-button @click="resetQuery" icon="Refresh">重置</el-button>
+ </el-form-item>
+ </el-form>
+ </el-card>
+ </div>
+ </transition>
+
+ <el-card shadow="hover">
+ <template #header>
+ <el-row :gutter="10">
+ <el-col :span="1.5">
+ <el-button type="primary" plain @click="handleAdd()" v-has-permi="['system:user:add']" icon="Plus">新增</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button type="success" plain @click="handleUpdate()" :disabled="single" v-has-permi="['system:user:add']" icon="Edit">
+ 修改
+ </el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button type="danger" plain @click="handleDelete()" :disabled="multiple" v-has-permi="['system:user:delete']" icon="Delete">
+ 删除
+ </el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-dropdown class="mt-[1px]">
+ <el-button plain type="info">
+ 更多
+ <el-icon class="el-icon--right"><arrow-down /></el-icon
+ ></el-button>
+ <template #dropdown>
+ <el-dropdown-menu>
+ <el-dropdown-item @click="importTemplate" icon="Download">下载模板</el-dropdown-item>
+ <el-dropdown-item @click="handleImport" icon="Top"> 导入数据</el-dropdown-item>
+ <el-dropdown-item @click="handleExport" icon="Download"> 导出数据</el-dropdown-item>
+ </el-dropdown-menu>
+ </template>
+ </el-dropdown>
+ </el-col>
+ <right-toolbar v-model:showSearch="showSearch" @queryTable="getList" :columns="columns" :search="true"></right-toolbar>
+ </el-row>
+ </template>
+
+ <el-table v-loading="loading" :data="userList" @selection-change="handleSelectionChange">
+ <el-table-column type="selection" width="50" align="center" />
+ <el-table-column label="用户编号" align="center" key="userId" prop="userId" v-if="columns[0].visible" />
+ <el-table-column label="用户名称" align="center" key="userName" prop="userName" v-if="columns[1].visible" :show-overflow-tooltip="true" />
+ <el-table-column label="用户昵称" align="center" key="nickName" prop="nickName" v-if="columns[2].visible" :show-overflow-tooltip="true" />
+ <el-table-column
+ label="部门"
+ align="center"
+ key="deptName"
+ prop="dept.deptName"
+ v-if="columns[3].visible"
+ :show-overflow-tooltip="true"
+ />
+ <el-table-column label="手机号码" align="center" key="phonenumber" prop="phonenumber" v-if="columns[4].visible" width="120" />
+ <el-table-column label="状态" align="center" key="status" v-if="columns[5].visible">
+ <template #default="scope">
+ <el-switch v-model="scope.row.status" active-value="0" inactive-value="1" @change="handleStatusChange(scope.row)"></el-switch>
+ </template>
+ </el-table-column>
+
+ <el-table-column label="创建时间" align="center" prop="createTime" v-if="columns[6].visible" width="160">
+ <template #default="scope">
+ <span>{{ scope.row.createTime }}</span>
+ </template>
+ </el-table-column>
+
+ <el-table-column label="操作" fixed="right" width="180" class-name="small-padding fixed-width">
+ <template #default="scope">
+ <el-tooltip content="修改" placement="top" v-if="scope.row.userId !== 1">
+ <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:user:edit']"></el-button>
+ </el-tooltip>
+ <el-tooltip content="删除" placement="top" v-if="scope.row.userId !== 1">
+ <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:user:remove']"></el-button>
+ </el-tooltip>
+
+ <el-tooltip content="重置密码" placement="top" v-if="scope.row.userId !== 1">
+ <el-button link type="primary" icon="Key" @click="handleResetPwd(scope.row)" v-hasPermi="['system:user:resetPwd']"></el-button>
+ </el-tooltip>
+
+ <el-tooltip content="分配角色" placement="top" v-if="scope.row.userId !== 1">
+ <el-button link type="primary" icon="CircleCheck" @click="handleAuthRole(scope.row)" v-hasPermi="['system:user:edit']"></el-button>
+ </el-tooltip>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <pagination
+ v-show="total > 0"
+ :total="total"
+ v-model:page="queryParams.pageNum"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </el-card>
+ </el-col>
+ </el-row>
+
+ <!-- 添加或修改用户配置对话框 -->
+ <el-dialog ref="formDialogRef" :title="dialog.title" v-model="dialog.visible" width="600px" append-to-body @close="closeDialog">
+ <el-form :model="form" :rules="rules" ref="userFormRef" label-width="80px">
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="用户昵称" prop="nickName">
+ <el-input v-model="form.nickName" placeholder="请输入用户昵称" maxlength="30" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="归属部门" prop="deptId">
+ <el-tree-select
+ v-model="form.deptId"
+ :data="deptOptions"
+ :props="{ value: 'id', label: 'label', children: 'children' }"
+ value-key="id"
+ placeholder="请选择归属部门"
+ check-strictly
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="手机号码" prop="phonenumber">
+ <el-input v-model="form.phonenumber" placeholder="请输入手机号码" maxlength="11" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="邮箱" prop="email">
+ <el-input v-model="form.email" placeholder="请输入邮箱" maxlength="50" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="12">
+ <el-form-item v-if="form.userId == undefined" label="用户名称" prop="userName">
+ <el-input v-model="form.userName" placeholder="请输入用户名称" maxlength="30" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item v-if="form.userId == undefined" label="用户密码" prop="password">
+ <el-input v-model="form.password" placeholder="请输入用户密码" type="password" maxlength="20" show-password />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="用户性别">
+ <el-select v-model="form.sex" placeholder="请选择">
+ <el-option v-for="dict in sys_user_sex" :key="dict.value" :label="dict.label" :value="dict.value"></el-option>
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="状态">
+ <el-radio-group v-model="form.status">
+ <el-radio v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.value">{{
+ dict.label }}</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="岗位">
+ <el-select v-model="form.postIds" multiple placeholder="请选择">
+ <el-option
+ v-for="item in postOptions"
+ :key="item.postId"
+ :label="item.postName"
+ :value="item.postId"
+ :disabled="item.status == '1'"
+ ></el-option>
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="角色">
+ <el-select v-model="form.roleIds" multiple placeholder="请选择">
+ <el-option
+ v-for="item in roleOptions"
+ :key="item.roleId"
+ :label="item.roleName"
+ :value="item.roleId"
+ :disabled="item.status == '1'"
+ ></el-option>
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="24">
+ <el-form-item label="备注">
+ <el-input v-model="form.remark" type="textarea" placeholder="请输入内容"></el-input>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitForm">确 定</el-button>
+ <el-button @click="cancel()">取 消</el-button>
+ </div>
+ </template>
+ </el-dialog>
+
+ <!-- 用户导入对话框 -->
+ <el-dialog :title="upload.title" v-model="upload.open" width="400px" append-to-body>
+ <el-upload
+ ref="uploadRef"
+ :limit="1"
+ accept=".xlsx, .xls"
+ :headers="upload.headers"
+ :action="upload.url + '?updateSupport=' + upload.updateSupport"
+ :disabled="upload.isUploading"
+ :on-progress="handleFileUploadProgress"
+ :on-success="handleFileSuccess"
+ :auto-upload="false"
+ drag
+ >
+ <el-icon class="el-icon--upload">
+ <i-ep-upload-filled />
+ </el-icon>
+ <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
+ <template #tip>
+ <div class="text-center el-upload__tip">
+ <div class="el-upload__tip"><el-checkbox v-model="upload.updateSupport" />是否更新已经存在的用户数据</div>
+ <span>仅允许导入xls、xlsx格式文件。</span>
+ <el-link type="primary" :underline="false" style="font-size:12px;vertical-align: baseline;" @click="importTemplate">下载模板</el-link>
+ </div>
+ </template>
+ </el-upload>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitFileForm">确 定</el-button>
+ <el-button @click="upload.open = false">取 消</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup name="User" lang="ts">
+import api from "@/api/system/user"
+import { UserForm, UserQuery, UserVO } from '@/api/system/user/types';
+import { treeselect } from "@/api/system/dept";
+import { DeptVO } from "@/api/system/dept/types";
+import { RoleVO } from "@/api/system/role/types";
+import { PostVO } from "@/api/system/post/types";
+import { to } from "await-to-js";
+import { globalHeaders } from "@/utils/request";
+
+const router = useRouter();
+const { proxy } = getCurrentInstance() as ComponentInternalInstance
+const { sys_normal_disable, sys_user_sex } = toRefs<any>(proxy?.useDict('sys_normal_disable', 'sys_user_sex'));
+
+const userList = ref<UserVO[]>();
+const loading = ref(true);
+const showSearch = ref(true)
+const ids = ref<Array<number | string>>([]);
+const single = ref(true);
+const multiple = ref(true);
+const total = ref(0);
+const dateRange = ref<[DateModelType, DateModelType]>(['', '']);
+const deptName = ref('');
+const deptOptions = ref<DeptVO[]>([]);
+const initPassword = ref<String>('');
+const postOptions = ref<PostVO[]>([]);
+const roleOptions = ref<RoleVO[]>([]);
+/*** 用户导入参数 */
+const upload = reactive<ImportOption>({
+ // 是否显示弹出层(用户导入)
+ open: false,
+ // 弹出层标题(用户导入)
+ title: "",
+ // 是否禁用上传
+ isUploading: false,
+ // 是否更新已经存在的用户数据
+ updateSupport: 0,
+ // 设置上传的请求头部
+ headers: globalHeaders(),
+ // 上传的地址
+ url: import.meta.env.VITE_APP_BASE_API + "/system/user/importData"
+})
+// 列显隐信息
+const columns = ref<FieldOption[]>([
+ { key: 0, label: `用户编号`, visible: false,children: [] },
+ { key: 1, label: `用户名称`, visible: true,children: [] },
+ { key: 2, label: `用户昵称`, visible: true,children: [] },
+ { key: 3, label: `部门`, visible: true,children: [] },
+ { key: 4, label: `手机号码`, visible: true,children: [] },
+ { key: 5, label: `状态`, visible: true,children: [] },
+ { key: 6, label: `创建时间`, visible: true,children: [] }
+])
+
+
+const deptTreeRef = ref<ElTreeInstance>();
+const queryFormRef = ref<ElFormInstance>();
+const userFormRef = ref<ElFormInstance>();
+const uploadRef = ref<ElUploadInstance>();
+const formDialogRef = ref<ElDialogInstance>();
+
+const dialog = reactive<DialogOption>({
+ visible: false,
+ title: ''
+});
+
+const initFormData: UserForm = {
+ userId: undefined,
+ deptId: undefined,
+ userName: '',
+ nickName: undefined,
+ password: '',
+ phonenumber: undefined,
+ email: undefined,
+ sex: undefined,
+ status: "0",
+ remark: '',
+ postIds: [],
+ roleIds: []
+}
+const data = reactive<PageData<UserForm, UserQuery>>({
+ form: { ...initFormData },
+ queryParams: {
+ pageNum: 1,
+ pageSize: 10,
+ userName: '',
+ phonenumber: '',
+ status: '',
+ deptId: ''
+ },
+ rules: {
+ userName: [{ required: true, message: "用户名称不能为空", trigger: "blur" }, { min: 2, max: 20, message: "用户名称长度必须介于 2 和 20 之间", trigger: "blur" }],
+ nickName: [{ required: true, message: "用户昵称不能为空", trigger: "blur" }],
+ password: [{ required: true, message: "用户密码不能为空", trigger: "blur" }, { min: 5, max: 20, message: "用户密码长度必须介于 5 和 20 之间", trigger: "blur" }],
+ email: [{ type: "email", message: "请输入正确的邮箱地址", trigger: ["blur", "change"] }],
+ phonenumber: [{ pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, message: "请输入正确的手机号码", trigger: "blur" }]
+ }
+})
+
+const { queryParams, form, rules } = toRefs<PageData<UserForm, UserQuery>>(data)
+
+/** 通过条件过滤节点 */
+const filterNode = (value: string, data: any) => {
+ if (!value) return true
+ return data.label.indexOf(value) !== -1
+}
+/** 根据名称筛选部门树 */
+watchEffect(
+ () => { deptTreeRef.value?.filter(deptName.value); },
+ {
+ flush: 'post' // watchEffect会在DOM挂载或者更新之前就会触发,此属性控制在DOM元素更新后运行
+ }
+);
+
+/** 查询部门下拉树结构 */
+const getTreeSelect = async () => {
+ const res = await api.deptTreeSelect();
+ deptOptions.value = res.data;
+};
+
+/** 查询用户列表 */
+const getList = async () => {
+ loading.value = true;
+ const res = await api.listUser(proxy?.addDateRange(queryParams.value, dateRange.value));
+ loading.value = false;
+ userList.value = res.rows;
+ total.value = res.total;
+}
+
+/** 节点单击事件 */
+const handleNodeClick = (data: DeptVO) => {
+ queryParams.value.deptId = data.id;
+ handleQuery()
+}
+
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+ queryParams.value.pageNum = 1
+ getList()
+}
+/** 重置按钮操作 */
+const resetQuery = () => {
+ dateRange.value = ['', '']
+ queryFormRef.value?.resetFields();
+ queryParams.value.pageNum = 1;
+ queryParams.value.deptId = undefined;
+ deptTreeRef.value?.setCurrentKey(undefined);
+ handleQuery();
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (row?: UserVO) => {
+ const userIds = row?.userId || ids.value;
+ const [err] = await to(proxy?.$modal.confirm('是否确认删除用户编号为"' + userIds + '"的数据项?') as any);
+ if (!err) {
+ await api.delUser(userIds);
+ await getList();
+ proxy?.$modal.msgSuccess("删除成功");
+ }
+}
+
+/** 用户状态修改 */
+const handleStatusChange = async (row: UserVO) => {
+ let text = row.status === "0" ? "启用" : "停用"
+ try {
+ await proxy?.$modal.confirm('确认要"' + text + '""' + row.userName + '"用户吗?');
+ await api.changeUserStatus(row.userId, row.status);
+ proxy?.$modal.msgSuccess(text + "成功");
+ } catch (err) {
+ row.status = row.status === "0" ? "1" : "0";
+ }
+}
+/** 跳转角色分配 */
+const handleAuthRole = (row: UserVO) => {
+ const userId = row.userId;
+ router.push("/system/user-auth/role/" + userId);
+}
+
+/** 重置密码按钮操作 */
+const handleResetPwd = async (row: UserVO) => {
+ const [err, res] = await to(ElMessageBox.prompt('请输入"' + row.userName + '"的新密码', "提示", {
+ confirmButtonText: "确定",
+ cancelButtonText: "取消",
+ closeOnClickModal: false,
+ inputPattern: /^.{5,20}$/,
+ inputErrorMessage: "用户密码长度必须介于 5 和 20 之间",
+ }))
+ if (!err) {
+ await api.resetUserPwd(row.userId, res.value);
+ proxy?.$modal.msgSuccess("修改成功,新密码是:" + res.value);
+ }
+}
+
+/** 选择条数 */
+const handleSelectionChange = (selection: UserVO[]) => {
+ ids.value = selection.map((item) => item.userId);
+ single.value = selection.length != 1;
+ multiple.value = !selection.length;
+}
+
+/** 导入按钮操作 */
+const handleImport = () => {
+ upload.title = "用户导入";
+ upload.open = true;
+}
+/** 导出按钮操作 */
+const handleExport = () => {
+ proxy?.download("system/user/export", {
+ ...queryParams.value,
+ }, `user_${new Date().getTime()}.xlsx`);
+};
+/** 下载模板操作 */
+const importTemplate = () => {
+ proxy?.download("system/user/importTemplate", {
+ }, `user_template_${new Date().getTime()}.xlsx`);
+}
+
+/**文件上传中处理 */
+const handleFileUploadProgress = () => {
+ upload.isUploading = true;
+}
+/** 文件上传成功处理 */
+const handleFileSuccess = (response: any, file: UploadFile) => {
+ upload.open = false;
+ upload.isUploading = false;
+ uploadRef.value?.handleRemove(file);
+ ElMessageBox.alert("<div style='overflow: auto;overflow-x: hidden;max-height: 70vh;padding: 10px 20px 0;'>" + response.msg + "</div>", "导入结果", { dangerouslyUseHTMLString: true });
+ getList();
+}
+
+/** 提交上传文件 */
+function submitFileForm() {
+ uploadRef.value?.submit();
+}
+
+/** 初始化部门数据 */
+const initTreeData = async () => {
+ // 判断部门的数据是否存在,存在不获取,不存在则获取
+ if (deptOptions.value === undefined) {
+ const { data } = await treeselect();
+ deptOptions.value = data;
+ }
+}
+
+
+/** 重置操作表单 */
+const reset = () => {
+ form.value = { ...initFormData };
+ userFormRef.value?.resetFields();
+}
+/** 取消按钮 */
+const cancel = () => {
+ dialog.visible = false;
+ reset();
+}
+
+/** 新增按钮操作 */
+const handleAdd = async () => {
+ reset();
+ const { data } = await api.getUser();
+ dialog.visible = true;
+ dialog.title = "新增用户";
+ await initTreeData();
+ postOptions.value = data.posts;
+ roleOptions.value = data.roles;
+ form.value.password = initPassword.value.toString();
+}
+/** 修改按钮操作 */
+const handleUpdate = async (row?: UserForm) => {
+ reset();
+ const userId = row?.userId || ids.value[0]
+ const { data } = await api.getUser(userId)
+ dialog.visible = true;
+ dialog.title = "修改用户";
+ await initTreeData();
+ Object.assign(form.value, data.user);
+ postOptions.value = data.posts;
+ roleOptions.value = data.roles;
+ form.value.postIds = data.postIds;
+ form.value.roleIds = data.roleIds;
+ form.value.password = "";
+}
+
+/** 提交按钮 */
+const submitForm = () => {
+ userFormRef.value?.validate(async (valid: boolean) => {
+ if (valid) {
+ form.value.userId ? await api.updateUser(form.value) : await api.addUser(form.value);
+ proxy?.$modal.msgSuccess("操作成功");
+ dialog.visible = false;
+ await getList();
+ }
+ })
+}
+
+
+/**
+ * 关闭用户弹窗
+ */
+const closeDialog = () => {
+ dialog.visible = false;
+ resetForm();
+}
+
+/**
+ * 重置表单
+ */
+const resetForm = () => {
+ userFormRef.value?.resetFields();
+ userFormRef.value?.clearValidate();
+
+ form.value.id = undefined;
+ form.value.status = '1';
+}
+onMounted(() => {
+ getTreeSelect() // 初始化部门数据
+ getList() // 初始化列表数据
+ proxy?.getConfigKey("sys.user.initPassword").then(response => {
+ initPassword.value = response.data;
+ });
+});
+</script>
+
+<style lang="scss" scoped></style>
diff --git a/src/views/system/user/profile/index.vue b/src/views/system/user/profile/index.vue
new file mode 100644
index 0000000..47a6632
--- /dev/null
+++ b/src/views/system/user/profile/index.vue
@@ -0,0 +1,103 @@
+<template>
+ <div class="p-2">
+ <el-row :gutter="20">
+ <el-col :span="6" :xs="24">
+ <el-card class="box-card">
+ <template v-slot:header>
+ <div class="clearfix">
+ <span>个人信息</span>
+ </div>
+ </template>
+ <div>
+ <div class="text-center">
+ <userAvatar/>
+ </div>
+ <ul class="list-group list-group-striped">
+ <li class="list-group-item">
+ <svg-icon icon-class="user" />用户名称
+ <div class="pull-right">{{ state.user.userName }}</div>
+ </li>
+ <li class="list-group-item">
+ <svg-icon icon-class="phone" />手机号码
+ <div class="pull-right">{{ state.user.phonenumber }}</div>
+ </li>
+ <li class="list-group-item">
+ <svg-icon icon-class="email" />用户邮箱
+ <div class="pull-right">{{ state.user.email }}</div>
+ </li>
+ <li class="list-group-item">
+ <svg-icon icon-class="tree" />所属部门
+ <div class="pull-right" v-if="state.user.dept">{{ state.user.dept.deptName }} / {{ state.postGroup }}</div>
+ </li>
+ <li class="list-group-item">
+ <svg-icon icon-class="peoples" />所属角色
+ <div class="pull-right">{{ state.roleGroup }}</div>
+ </li>
+ <li class="list-group-item">
+ <svg-icon icon-class="date" />创建日期
+ <div class="pull-right">{{ state.user.createTime }}</div>
+ </li>
+ </ul>
+ </div>
+ </el-card>
+ </el-col>
+ <el-col :span="18" :xs="24">
+ <el-card>
+ <template v-slot:header>
+ <div class="clearfix">
+ <span>基本资料</span>
+ </div>
+ </template>
+ <el-tabs v-model="activeTab">
+ <el-tab-pane label="基本资料" name="userinfo">
+ <userInfo :user="userForm" />
+ </el-tab-pane>
+ <el-tab-pane label="修改密码" name="resetPwd">
+ <resetPwd />
+ </el-tab-pane>
+ <el-tab-pane label="第三方应用" name="thirdParty">
+ <thirdParty :auths="state.auths" />
+ </el-tab-pane>
+ </el-tabs>
+ </el-card>
+ </el-col>
+ </el-row>
+ </div>
+</template>
+
+<script setup name="Profile" lang="ts">
+import UserAvatar from "./userAvatar.vue";
+import UserInfo from "./userInfo.vue";
+import ResetPwd from "./resetPwd.vue";
+import ThirdParty from "./thirdParty.vue";
+import { getAuthList } from "@/api/system/social/auth";
+import { getUserProfile } from "@/api/system/user";
+
+const activeTab = ref("userinfo");
+const state = ref<Record<string, any>>({
+ user: {},
+ roleGroup: '',
+ postGroup: '',
+ auths: []
+});
+
+const userForm = ref({});
+
+const getUser = async () => {
+ const res = await getUserProfile();
+ state.value.user = res.data.user;
+ userForm.value = { ...res.data.user }
+ state.value.roleGroup = res.data.roleGroup;
+ state.value.postGroup = res.data.postGroup;
+};
+
+const getAuths = async () => {
+ const res = await getAuthList();
+ state.value.auths = res.data;
+};
+
+onMounted(() => {
+ getUser();
+ getAuths();
+})
+</script>
diff --git a/src/views/system/user/profile/resetPwd.vue b/src/views/system/user/profile/resetPwd.vue
new file mode 100644
index 0000000..11b00f6
--- /dev/null
+++ b/src/views/system/user/profile/resetPwd.vue
@@ -0,0 +1,66 @@
+<template>
+ <el-form ref="pwdRef" :model="user" :rules="rules" label-width="80px">
+ <el-form-item label="旧密码" prop="oldPassword">
+ <el-input v-model="user.oldPassword" placeholder="请输入旧密码" type="password" show-password />
+ </el-form-item>
+ <el-form-item label="新密码" prop="newPassword">
+ <el-input v-model="user.newPassword" placeholder="请输入新密码" type="password" show-password />
+ </el-form-item>
+ <el-form-item label="确认密码" prop="confirmPassword">
+ <el-input v-model="user.confirmPassword" placeholder="请确认新密码" type="password" show-password />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" @click="submit">保存</el-button>
+ <el-button type="danger" @click="close">关闭</el-button>
+ </el-form-item>
+ </el-form>
+</template>
+
+<script setup lang="ts">
+import { updateUserPwd } from "@/api/system/user";
+import type { ResetPwdForm } from "@/api/system/user/types";
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const pwdRef = ref<ElFormInstance>();
+const user = ref<ResetPwdForm>({
+ oldPassword: "",
+ newPassword: "",
+ confirmPassword: ""
+});
+
+const equalToPassword = (rule: any, value: string, callback: any) => {
+ if (user.value.newPassword !== value) {
+ callback(new Error("两次输入的密码不一致"));
+ } else {
+ callback();
+ }
+};
+const rules = ref({
+ oldPassword: [{ required: true, message: "旧密码不能为空", trigger: "blur" }],
+ newPassword: [{ required: true, message: "新密码不能为空", trigger: "blur" }, {
+ min: 6,
+ max: 20,
+ message: "长度在 6 到 20 个字符",
+ trigger: "blur"
+ }],
+ confirmPassword: [{ required: true, message: "确认密码不能为空", trigger: "blur" }, {
+ required: true,
+ validator: equalToPassword,
+ trigger: "blur"
+ }]
+});
+
+/** 提交按钮 */
+const submit = () => {
+ pwdRef.value?.validate(async (valid: boolean) => {
+ if (valid) {
+ await updateUserPwd(user.value.oldPassword, user.value.newPassword);
+ proxy?.$modal.msgSuccess("修改成功");
+ }
+ });
+};
+/** 关闭按钮 */
+const close = () => {
+ proxy?.$tab.closePage();
+};
+</script>
diff --git a/src/views/system/user/profile/thirdParty.vue b/src/views/system/user/profile/thirdParty.vue
new file mode 100644
index 0000000..4170894
--- /dev/null
+++ b/src/views/system/user/profile/thirdParty.vue
@@ -0,0 +1,140 @@
+<template>
+ <div>
+ <el-table :data="auths" style="width: 100%; height: 100%; font-size: 10px">
+ <el-table-column label="序号" width="50" type="index"></el-table-column>
+ <el-table-column label="绑定账号平台" width="140" align="center" prop="source" show-overflow-tooltip />
+ <el-table-column label="头像" width="120" align="center" prop="avatar">
+ <template v-slot="scope">
+ <img :src="scope.row.avatar" style="width: 45px; height: 45px" />
+ </template>
+ </el-table-column>
+ <el-table-column label="系统账号" width="180" align="center" prop="userName" :show-overflow-tooltip="true" />
+ <el-table-column label="绑定时间" width="180" align="center" prop="createTime" />
+ <el-table-column label="操作" width="80" align="center" class-name="small-padding fixed-width">
+ <template v-slot="scope">
+ <el-button size="small" type="text" @click="unlockAuth(scope.row)">解绑</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <div id="git-user-binding">
+ <h4 class="provider-desc">你可以绑定以下第三方帐号</h4>
+ <div id="authlist" class="user-bind">
+ <a class="third-app" href="#" @click="authUrl('wechat');" title="使用 微信 账号授权登录">
+ <div class="git-other-login-icon">
+ <svg-icon icon-class="wechat" />
+ </div>
+ <span class="app-name">WeiXin</span>
+ </a>
+ <a class="third-app" href="#" @click="authUrl('maxkey');" title="使用 MaxKey 账号授权登录">
+ <div class="git-other-login-icon">
+ <svg-icon icon-class="maxkey" />
+ </div>
+ <span class="app-name">MaxKey</span>
+ </a>
+ <a class="third-app" href="#" @click="authUrl('gitee');" title="使用 Gitee 账号授权登录">
+ <div class="git-other-login-icon">
+ <svg-icon icon-class="gitee" />
+ </div>
+ <span class="app-name">Gitee</span>
+ </a>
+ <a class="third-app" href="#" @click="authUrl('github');" title="使用 GitHub 账号授权登录">
+ <div class="git-other-login-icon">
+ <svg-icon icon-class="github" />
+ </div>
+ <span class="app-name">Github</span>
+ </a>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script lang="ts" setup>
+import { authUnlock, authBinding } from "@/api/system/social/auth";
+import { PropType } from "vue";
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+
+const props = defineProps({
+ auths: {
+ type: Object as PropType<any>,
+ }
+});
+const auths = computed(() => props.auths);
+
+
+const unlockAuth = (row: any) => {
+ ElMessageBox.confirm('您确定要解除"' + row.source + '"的账号绑定吗?')
+ .then(() => {
+ return authUnlock(row.id);
+ }).then((res: any) => {
+ if (res.code === 200) {
+ proxy?.$modal.msgSuccess("解绑成功");
+ proxy?.$tab.refreshPage();
+ } else {
+ proxy?.$modal.msgError(res.msg);
+ }
+ }).catch(() => { });
+};
+
+const authUrl = (source: string) => {
+ authBinding(source).then((res: any) => {
+ if (res.code === 200) {
+ window.location.href = res.data;
+ } else {
+ proxy?.$modal.msgError(res.msg);
+ }
+ });
+};
+</script>
+
+<style type="text/css">
+.user-bind .third-app {
+ display: -webkit-box;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-box-orient: vertical;
+ -webkit-box-direction: normal;
+ -ms-flex-direction: column;
+ flex-direction: column;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+ min-width: 80px;
+ float: left;
+}
+
+.user-bind {
+ font-size: 1rem;
+ text-align: start;
+ height: 50px;
+ margin-top: 10px;
+}
+
+.git-other-login-icon>img {
+ height: 32px;
+}
+
+a {
+ text-decoration: none;
+ cursor: pointer;
+ color: #005980;
+}
+
+.provider-desc {
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial,
+ "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Liberation Sans",
+ "PingFang SC", "Microsoft YaHei", "Hiragino Sans GB", "Wenquanyi Micro Hei",
+ "WenQuanYi Zen Hei", "ST Heiti", SimHei, SimSun, "WenQuanYi Zen Hei Sharp",
+ sans-serif;
+ font-size: 1.071rem;
+}
+
+td>img {
+ height: 20px;
+ width: 20px;
+ display: inline-block;
+ border-radius: 50%;
+ margin-right: 5px;
+}
+</style>
diff --git a/src/views/system/user/profile/userAvatar.vue b/src/views/system/user/profile/userAvatar.vue
new file mode 100644
index 0000000..ce25c11
--- /dev/null
+++ b/src/views/system/user/profile/userAvatar.vue
@@ -0,0 +1,183 @@
+<template>
+ <div class="user-info-head" @click="editCropper()">
+ <img :src="options.img" title="点击上传头像" class="img-circle img-lg" />
+ <el-dialog :title="title" v-model="open" width="800px" append-to-body @opened="modalOpened" @close="closeDialog">
+ <el-row>
+ <el-col :xs="24" :md="12" :style="{ height: '350px' }">
+ <vue-cropper
+ ref="cropper"
+ :img="options.img"
+ :info="true"
+ :autoCrop="options.autoCrop"
+ :autoCropWidth="options.autoCropWidth"
+ :autoCropHeight="options.autoCropHeight"
+ :fixedBox="options.fixedBox"
+ :outputType="options.outputType"
+ @realTime="realTime"
+ v-if="visible"
+ />
+ </el-col>
+ <el-col :xs="24" :md="12" :style="{ height: '350px' }">
+ <div class="avatar-upload-preview">
+ <img :src="options.previews.url" :style="options.previews.img" />
+ </div>
+ </el-col>
+ </el-row>
+ <br />
+ <el-row>
+ <el-col :lg="2" :md="2">
+ <el-upload action="#" :http-request="requestUpload" :show-file-list="false" :before-upload="beforeUpload">
+ <el-button>
+ 选择
+ <el-icon class="el-icon--right">
+ <Upload />
+ </el-icon>
+ </el-button>
+ </el-upload>
+ </el-col>
+ <el-col :lg="{ span: 1, offset: 2 }" :md="2">
+ <el-button icon="Plus" @click="changeScale(1)"></el-button>
+ </el-col>
+ <el-col :lg="{ span: 1, offset: 1 }" :md="2">
+ <el-button icon="Minus" @click="changeScale(-1)"></el-button>
+ </el-col>
+ <el-col :lg="{ span: 1, offset: 1 }" :md="2">
+ <el-button icon="RefreshLeft" @click="rotateLeft()"></el-button>
+ </el-col>
+ <el-col :lg="{ span: 1, offset: 1 }" :md="2">
+ <el-button icon="RefreshRight" @click="rotateRight()"></el-button>
+ </el-col>
+ <el-col :lg="{ span: 2, offset: 6 }" :md="2">
+ <el-button type="primary" @click="uploadImg()">提 交</el-button>
+ </el-col>
+ </el-row>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup lang="ts">
+import "vue-cropper/dist/index.css";
+import { VueCropper } from "vue-cropper";
+import { uploadAvatar } from "@/api/system/user";
+import useUserStore from "@/store/modules/user";
+
+interface Options {
+ img: string | any; // 裁剪图片的地址
+ autoCrop: boolean; // 是否默认生成截图框
+ autoCropWidth: number; // 默认生成截图框宽度
+ autoCropHeight: number; // 默认生成截图框高度
+ fixedBox: boolean; // 固定截图框大小 不允许改变
+ fileName: string;
+ previews: any; // 预览数据
+ outputType: string;
+ visible: boolean;
+}
+
+
+const userStore = useUserStore();
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+
+const open = ref(false);
+const visible = ref(false);
+const title = ref("修改头像");
+
+const cropper = ref<any>({});
+//图片裁剪数据
+const options = reactive<Options>({
+ img: userStore.avatar,
+ autoCrop: true,
+ autoCropWidth: 200,
+ autoCropHeight: 200,
+ fixedBox: true,
+ outputType: "png",
+ fileName: "",
+ previews: {},
+ visible: false
+});
+
+/** 编辑头像 */
+const editCropper = () => {
+ open.value = true;
+};
+/** 打开弹出层结束时的回调 */
+const modalOpened = () => {
+ visible.value = true;
+};
+/** 覆盖默认上传行为 */
+const requestUpload = (): any => {
+};
+/** 向左旋转 */
+const rotateLeft = () => {
+ cropper.value.rotateLeft();
+};
+/** 向右旋转 */
+const rotateRight = () => {
+ cropper.value.rotateRight();
+};
+/** 图片缩放 */
+const changeScale = (num: number) => {
+ num = num || 1;
+ cropper.value.changeScale(num);
+};
+/** 上传预处理 */
+const beforeUpload = (file: any) => {
+ if (file.type.indexOf("image/") == -1) {
+ proxy?.$modal.msgError("文件格式错误,请上传图片类型,如:JPG,PNG后缀的文件。");
+ } else {
+ const reader = new FileReader();
+ reader.readAsDataURL(file);
+ reader.onload = () => {
+ options.img = reader.result;
+ options.fileName = file.name;
+ };
+ }
+};
+/** 上传图片 */
+const uploadImg = async () => {
+ cropper.value.getCropBlob(async (data: any) => {
+ let formData = new FormData();
+ formData.append("avatarfile", data, options.fileName);
+ const res = await uploadAvatar(formData);
+ open.value = false;
+ options.img = res.data.imgUrl;
+ userStore.setAvatar(options.img as string)
+ proxy?.$modal.msgSuccess("修改成功");
+ visible.value = false;
+ });
+};
+/** 实时预览 */
+const realTime = (data: any) => {
+ options.previews = data;
+};
+/** 关闭窗口 */
+const closeDialog = () => {
+ options.img = userStore.avatar;
+ options.visible = false;
+};
+</script>
+
+<style lang="scss" scoped>
+.user-info-head {
+ position: relative;
+ display: inline-block;
+ height: 120px;
+}
+
+.user-info-head:hover:after {
+ content: "+";
+ position: absolute;
+ left: 0;
+ right: 0;
+ top: 0;
+ bottom: 0;
+ color: #eee;
+ background: rgba(0, 0, 0, 0.5);
+ font-size: 24px;
+ font-style: normal;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ cursor: pointer;
+ line-height: 110px;
+ border-radius: 50%;
+}
+</style>
diff --git a/src/views/system/user/profile/userInfo.vue b/src/views/system/user/profile/userInfo.vue
new file mode 100644
index 0000000..3dd5ff7
--- /dev/null
+++ b/src/views/system/user/profile/userInfo.vue
@@ -0,0 +1,65 @@
+<template>
+ <el-form ref="userRef" :model="userForm" :rules="rules" label-width="80px">
+ <el-form-item label="用户昵称" prop="nickName">
+ <el-input v-model="userForm.nickName" maxlength="30" />
+ </el-form-item>
+ <el-form-item label="手机号码" prop="phonenumber">
+ <el-input v-model="userForm.phonenumber" maxlength="11" />
+ </el-form-item>
+ <el-form-item label="邮箱" prop="email">
+ <el-input v-model="userForm.email" maxlength="50" />
+ </el-form-item>
+ <el-form-item label="性别">
+ <el-radio-group v-model="userForm.sex">
+ <el-radio label="0">男</el-radio>
+ <el-radio label="1">女</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" @click="submit">保存</el-button>
+ <el-button type="danger" @click="close">关闭</el-button>
+ </el-form-item>
+ </el-form>
+</template>
+
+<script setup lang="ts">
+import { updateUserProfile } from "@/api/system/user";
+
+const props = defineProps({
+ user: {
+ type: Object as PropType<any>,
+ required: true
+ }
+});
+const userForm = computed(() => props.user);
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const userRef = ref<ElFormInstance>();
+const rules = ref<ElFormRules>({
+ nickName: [{ required: true, message: "用户昵称不能为空", trigger: "blur" }],
+ email: [{ required: true, message: "邮箱地址不能为空", trigger: "blur" }, {
+ type: "email",
+ message: "请输入正确的邮箱地址",
+ trigger: ["blur", "change"]
+ }],
+ phonenumber: [{
+ required: true,
+ message: "手机号码不能为空",
+ trigger: "blur"
+ }, { pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, message: "请输入正确的手机号码", trigger: "blur" }]
+});
+
+
+/** 提交按钮 */
+const submit = () => {
+ userRef.value?.validate(async (valid: boolean) => {
+ if (valid) {
+ await updateUserProfile(props.user);
+ proxy?.$modal.msgSuccess("修改成功");
+ }
+ });
+};
+/** 关闭按钮 */
+const close = () => {
+ proxy?.$tab.closePage();
+};
+</script>
diff --git a/src/views/tool/build/index.vue b/src/views/tool/build/index.vue
new file mode 100644
index 0000000..ef0c079
--- /dev/null
+++ b/src/views/tool/build/index.vue
@@ -0,0 +1,3 @@
+<template>
+ <div>表单构建(由于此功能的开源组件不支持 VUE3+TS 故暂时无法使用) <svg-icon icon-class="build" /></div>
+</template>
diff --git a/src/views/tool/gen/basicInfoForm.vue b/src/views/tool/gen/basicInfoForm.vue
new file mode 100644
index 0000000..c77d0c9
--- /dev/null
+++ b/src/views/tool/gen/basicInfoForm.vue
@@ -0,0 +1,49 @@
+<template>
+ <el-form ref="basicInfoForm" :model="infoForm" :rules="rules" label-width="150px">
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="表名称" prop="tableName">
+ <el-input placeholder="请输入仓库名称" v-model="infoForm.tableName" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="表描述" prop="tableComment">
+ <el-input placeholder="请输入" v-model="infoForm.tableComment" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="实体类名称" prop="className">
+ <el-input placeholder="请输入" v-model="infoForm.className" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="作者" prop="functionAuthor">
+ <el-input placeholder="请输入" v-model="infoForm.functionAuthor" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="24">
+ <el-form-item label="备注" prop="remark">
+ <el-input type="textarea" :rows="3" v-model="infoForm.remark"></el-input>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+</template>
+
+<script setup lang="ts">
+import { propTypes } from "@/utils/propTypes";
+
+const prop = defineProps({
+ info: propTypes.any.def({})
+});
+
+const infoForm = computed(() => prop.info)
+
+// 表单校验
+const rules = ref({
+ tableName: [{ required: true, message: "请输入表名称", trigger: "blur" }],
+ tableComment: [{ required: true, message: "请输入表描述", trigger: "blur" }],
+ className: [{ required: true, message: "请输入实体类名称", trigger: "blur" }],
+ functionAuthor: [{ required: true, message: "请输入作者", trigger: "blur" }]
+});
+</script>
diff --git a/src/views/tool/gen/editTable.vue b/src/views/tool/gen/editTable.vue
new file mode 100644
index 0000000..fdd1eeb
--- /dev/null
+++ b/src/views/tool/gen/editTable.vue
@@ -0,0 +1,187 @@
+<template>
+ <el-card>
+ <el-tabs v-model="activeName">
+ <el-tab-pane label="基本信息" name="basic">
+ <basic-info-form ref="basicInfo" :info="info" />
+ </el-tab-pane>
+ <el-tab-pane label="字段信息" name="columnInfo">
+ <el-table ref="dragTable" :data="columns" row-key="columnId" :max-height="tableHeight">
+ <el-table-column label="序号" type="index" min-width="5%" />
+ <el-table-column label="字段列名" prop="columnName" min-width="10%" :show-overflow-tooltip="true" />
+ <el-table-column label="字段描述" min-width="10%">
+ <template #default="scope">
+ <el-input v-model="scope.row.columnComment"></el-input>
+ </template>
+ </el-table-column>
+ <el-table-column label="物理类型" prop="columnType" min-width="10%" :show-overflow-tooltip="true" />
+ <el-table-column label="Java类型" min-width="11%">
+ <template #default="scope">
+ <el-select v-model="scope.row.javaType">
+ <el-option label="Long" value="Long" />
+ <el-option label="String" value="String" />
+ <el-option label="Integer" value="Integer" />
+ <el-option label="Double" value="Double" />
+ <el-option label="BigDecimal" value="BigDecimal" />
+ <el-option label="Date" value="Date" />
+ <el-option label="Boolean" value="Boolean" />
+ </el-select>
+ </template>
+ </el-table-column>
+ <el-table-column label="java属性" min-width="10%">
+ <template #default="scope">
+ <el-input v-model="scope.row.javaField"></el-input>
+ </template>
+ </el-table-column>
+
+ <el-table-column label="插入" min-width="5%">
+ <template #default="scope">
+ <el-checkbox true-label="1" false-label="0" v-model="scope.row.isInsert"></el-checkbox>
+ </template>
+ </el-table-column>
+ <el-table-column label="编辑" min-width="5%">
+ <template #default="scope">
+ <el-checkbox true-label="1" false-label="0" v-model="scope.row.isEdit"></el-checkbox>
+ </template>
+ </el-table-column>
+ <el-table-column label="列表" min-width="5%">
+ <template #default="scope">
+ <el-checkbox true-label="1" false-label="0" v-model="scope.row.isList"></el-checkbox>
+ </template>
+ </el-table-column>
+ <el-table-column label="查询" min-width="5%">
+ <template #default="scope">
+ <el-checkbox true-label="1" false-label="0" v-model="scope.row.isQuery"></el-checkbox>
+ </template>
+ </el-table-column>
+ <el-table-column label="查询方式" min-width="10%">
+ <template #default="scope">
+ <el-select v-model="scope.row.queryType">
+ <el-option label="=" value="EQ" />
+ <el-option label="!=" value="NE" />
+ <el-option label=">" value="GT" />
+ <el-option label=">=" value="GE" />
+ <el-option label="<" value="LT" />
+ <el-option label="<=" value="LE" />
+ <el-option label="LIKE" value="LIKE" />
+ <el-option label="BETWEEN" value="BETWEEN" />
+ </el-select>
+ </template>
+ </el-table-column>
+ <el-table-column label="必填" min-width="5%">
+ <template #default="scope">
+ <el-checkbox true-label="1" false-label="0" v-model="scope.row.isRequired"></el-checkbox>
+ </template>
+ </el-table-column>
+ <el-table-column label="显示类型" min-width="12%">
+ <template #default="scope">
+ <el-select v-model="scope.row.htmlType">
+ <el-option label="文本框" value="input" />
+ <el-option label="文本域" value="textarea" />
+ <el-option label="下拉框" value="select" />
+ <el-option label="单选框" value="radio" />
+ <el-option label="复选框" value="checkbox" />
+ <el-option label="日期控件" value="datetime" />
+ <el-option label="图片上传" value="imageUpload" />
+ <el-option label="文件上传" value="fileUpload" />
+ <el-option label="富文本控件" value="editor" />
+ </el-select>
+ </template>
+ </el-table-column>
+ <el-table-column label="字典类型" min-width="12%">
+ <template #default="scope">
+ <el-select v-model="scope.row.dictType" clearable filterable placeholder="请选择">
+ <el-option v-for="dict in dictOptions" :key="dict.dictType" :label="dict.dictName" :value="dict.dictType">
+ <span style="float: left">{{ dict.dictName }}</span>
+ <span style="float: right; color: #8492a6; font-size: 13px">{{ dict.dictType }}</span>
+ </el-option>
+ </el-select>
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-tab-pane>
+ <el-tab-pane label="生成信息" name="genInfo">
+ <gen-info-form ref="genInfo" :info="info" :tables="tables" />
+ </el-tab-pane>
+ </el-tabs>
+ <el-form label-width="100px">
+ <div style="text-align: center;margin-left:-100px;margin-top:10px;">
+ <el-button type="primary" @click="submitForm()">提交</el-button>
+ <el-button @click="close()">返回</el-button>
+ </div>
+ </el-form>
+ </el-card>
+</template>
+
+<script setup name="GenEdit" lang="ts">
+import { getGenTable, updateGenTable } from '@/api/tool/gen';
+import { DbColumnVO, DbTableVO } from '@/api/tool/gen/types';
+import { optionselect as getDictOptionselect } from '@/api/system/dict/type';
+import { DictTypeVO } from '@/api/system/dict/type/types';
+import BasicInfoForm from './basicInfoForm.vue';
+import GenInfoForm from "./genInfoForm.vue";
+
+const route = useRoute();
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+
+const activeName = ref('columnInfo');
+const tableHeight = ref(document.documentElement.scrollHeight - 245 + 'px');
+const tables = ref<DbTableVO[]>([]);
+const columns = ref<DbColumnVO[]>([]);
+const dictOptions = ref<DictTypeVO[]>([]);
+const info = ref<Partial<DbTableVO>>({});
+
+const basicInfo = ref<InstanceType<typeof BasicInfoForm>>();
+const genInfo = ref<InstanceType<typeof GenInfoForm>>();
+
+/** 提交按钮 */
+const submitForm = () => {
+ const basicForm = basicInfo.value?.$refs.basicInfoForm;
+ const genForm = genInfo.value?.$refs.genInfoForm;
+
+ Promise.all([basicForm, genForm].map(getFormPromise)).then(async res => {
+ const validateResult = res.every(item => !!item);
+ if (validateResult) {
+ const genTable: any = Object.assign({}, info.value);
+ genTable.columns = columns.value;
+ genTable.params = {
+ treeCode: info.value?.treeCode,
+ treeName: info.value.treeName,
+ treeParentCode: info.value.treeParentCode,
+ parentMenuId: info.value.parentMenuId
+ };
+ const response = await updateGenTable(genTable);
+ proxy?.$modal.msgSuccess(response.msg);
+ if (response.code === 200) {
+ close();
+ }
+ } else {
+ proxy?.$modal.msgError("表单校验未通过,请重新检查提交内容");
+ }
+ });
+}
+const getFormPromise = (form: any) => {
+ return new Promise(resolve => {
+ form.validate((res: any) => {
+ resolve(res);
+ });
+ });
+}
+const close = () => {
+ const obj = { path: "/tool/gen", query: { t: Date.now(), pageNum: route.query.pageNum } };
+ proxy?.$tab.closeOpenPage(obj);
+}
+
+(async () => {
+ const tableId = route.params && route.params.tableId as string;
+ if (tableId) {
+ // 获取表详细信息
+ const res = await getGenTable(tableId);
+ columns.value = res.data.rows;
+ info.value = res.data.info;
+ tables.value = res.data.tables;
+ /** 查询字典下拉列表 */
+ const response = await getDictOptionselect();
+ dictOptions.value = response.data;
+ }
+})();
+</script>
diff --git a/src/views/tool/gen/genInfoForm.vue b/src/views/tool/gen/genInfoForm.vue
new file mode 100644
index 0000000..94486e2
--- /dev/null
+++ b/src/views/tool/gen/genInfoForm.vue
@@ -0,0 +1,291 @@
+<template>
+ <el-form ref="genInfoForm" :model="infoForm" :rules="rules" label-width="150px">
+ <el-row>
+ <el-col :span="12">
+ <el-form-item prop="tplCategory">
+ <template #label>生成模板</template>
+ <el-select v-model="infoForm.tplCategory" @change="tplSelectChange">
+ <el-option label="单表(增删改查)" value="crud" />
+ <el-option label="树表(增删改查)" value="tree" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+
+ <el-col :span="12">
+ <el-form-item prop="packageName">
+ <template #label>
+ 生成包路径
+ <el-tooltip content="生成在哪个java包下,例如 com.ruoyi.system" placement="top">
+ <el-icon><question-filled /></el-icon>
+ </el-tooltip>
+ </template>
+ <el-input v-model="infoForm.packageName" />
+ </el-form-item>
+ </el-col>
+
+ <el-col :span="12">
+ <el-form-item prop="moduleName">
+ <template #label>
+ 生成模块名
+ <el-tooltip content="可理解为子系统名,例如 system" placement="top">
+ <el-icon><question-filled /></el-icon>
+ </el-tooltip>
+ </template>
+ <el-input v-model="infoForm.moduleName" />
+ </el-form-item>
+ </el-col>
+
+ <el-col :span="12">
+ <el-form-item prop="businessName">
+ <template #label>
+ 生成业务名
+ <el-tooltip content="可理解为功能英文名,例如 user" placement="top">
+ <el-icon><question-filled /></el-icon>
+ </el-tooltip>
+ </template>
+ <el-input v-model="infoForm.businessName" />
+ </el-form-item>
+ </el-col>
+
+ <el-col :span="12">
+ <el-form-item prop="functionName">
+ <template #label>
+ 生成功能名
+ <el-tooltip content="用作类描述,例如 用户" placement="top">
+ <el-icon><question-filled /></el-icon>
+ </el-tooltip>
+ </template>
+ <el-input v-model="infoForm.functionName" />
+ </el-form-item>
+ </el-col>
+
+ <el-col :span="12">
+ <el-form-item>
+ <template #label>
+ 上级菜单
+ <el-tooltip content="分配到指定菜单下,例如 系统管理" placement="top">
+ <el-icon><question-filled /></el-icon>
+ </el-tooltip>
+ </template>
+ <el-tree-select
+ v-model="infoForm.parentMenuId"
+ :data="menuOptions"
+ :props="{ value: 'menuId', label: 'menuName', children: 'children' }"
+ value-key="menuId"
+ node-key="menuId"
+ placeholder="选择上级菜单"
+ check-strictly
+ filterable
+ clearable
+ highlight-current
+ />
+ </el-form-item>
+ </el-col>
+
+ <el-col :span="12">
+ <el-form-item prop="genType">
+ <template #label>
+ 生成代码方式
+ <el-tooltip content="默认为zip压缩包下载,也可以自定义生成路径" placement="top">
+ <el-icon><question-filled /></el-icon>
+ </el-tooltip>
+ </template>
+ <el-radio v-model="infoForm.genType" label="0">zip压缩包</el-radio>
+ <el-radio v-model="infoForm.genType" label="1">自定义路径</el-radio>
+ </el-form-item>
+ </el-col>
+
+ <el-col :span="24" v-if="infoForm.genType == '1'">
+ <el-form-item prop="genPath">
+ <template #label>
+ 自定义路径
+ <el-tooltip content="填写磁盘绝对路径,若不填写,则生成到当前Web项目下" placement="top">
+ <el-icon><question-filled /></el-icon>
+ </el-tooltip>
+ </template>
+ <el-input v-model="infoForm.genPath">
+ <template #append>
+ <el-dropdown>
+ <el-button type="primary">
+ 最近路径快速选择
+ <i class="el-icon-arrow-down el-icon--right"></i>
+ </el-button>
+ <template #dropdown>
+ <el-dropdown-menu>
+ <el-dropdown-item @click="infoForm.genPath = '/'">恢复默认的生成基础路径</el-dropdown-item>
+ </el-dropdown-menu>
+ </template>
+ </el-dropdown>
+ </template>
+ </el-input>
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <template v-if="info.tplCategory == 'tree'">
+ <h4 class="form-header">其他信息</h4>
+ <el-row v-show="info.tplCategory == 'tree'">
+ <el-col :span="12">
+ <el-form-item>
+ <template #label>
+ 树编码字段
+ <el-tooltip content="树显示的编码字段名, 如:dept_id" placement="top">
+ <el-icon><question-filled /></el-icon>
+ </el-tooltip>
+ </template>
+ <el-select v-model="infoForm.treeCode" placeholder="请选择">
+ <el-option
+ v-for="(column, index) in info.columns"
+ :key="index"
+ :label="column.columnName + ':' + column.columnComment"
+ :value="column.columnName"
+ ></el-option>
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item>
+ <template #label>
+ 树父编码字段
+ <el-tooltip content="树显示的父编码字段名, 如:parent_Id" placement="top">
+ <el-icon><question-filled /></el-icon>
+ </el-tooltip>
+ </template>
+ <el-select v-model="infoForm.treeParentCode" placeholder="请选择">
+ <el-option
+ v-for="(column, index) in infoForm.columns"
+ :key="index"
+ :label="column.columnName + ':' + column.columnComment"
+ :value="column.columnName"
+ ></el-option>
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item>
+ <template #label>
+ 树名称字段
+ <el-tooltip content="树节点的显示名称字段名, 如:dept_name" placement="top">
+ <el-icon><question-filled /></el-icon>
+ </el-tooltip>
+ </template>
+ <el-select v-model="infoForm.treeName" placeholder="请选择">
+ <el-option
+ v-for="(column, index) in info.columns"
+ :key="index"
+ :label="column.columnName + ':' + column.columnComment"
+ :value="column.columnName"
+ ></el-option>
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </template>
+
+ <template v-if="info.tplCategory == 'sub'">
+ <h4 class="form-header">关联信息</h4>
+ <el-row>
+ <el-col :span="12">
+ <el-form-item>
+ <template #label>
+ 关联子表的表名
+ <el-tooltip content="关联子表的表名, 如:sys_user" placement="top">
+ <el-icon><question-filled /></el-icon>
+ </el-tooltip>
+ </template>
+ <el-select v-model="infoForm.subTableName" placeholder="请选择" @change="subSelectChange">
+ <el-option v-for="(t, index) in table" :key="index" :label="t.tableName + ':' + t.tableComment" :value="t.tableName"></el-option>
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item>
+ <template #label>
+ 子表关联的外键名
+ <el-tooltip content="子表关联的外键名, 如:user_id" placement="top">
+ <el-icon><question-filled /></el-icon>
+ </el-tooltip>
+ </template>
+ <el-select v-model="infoForm.subTableFkName" placeholder="请选择">
+ <el-option
+ v-for="(column, index) in subColumns"
+ :key="index"
+ :label="column.columnName + ':' + column.columnComment"
+ :value="column.columnName"
+ ></el-option>
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </template>
+ </el-form>
+</template>
+
+<script setup lang="ts">
+import { listMenu } from '@/api/system/menu';
+import { propTypes } from "@/utils/propTypes";
+
+interface MenuOptionsType {
+ menuId: number | string;
+ menuName: string;
+ children: MenuOptionsType[] | undefined;
+}
+
+const subColumns = ref<any>([]);
+const menuOptions = ref<Array<MenuOptionsType>>([]);
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+
+const props = defineProps({
+ info: propTypes.any.def(null),
+ tables: propTypes.any.def(null)
+});
+
+const infoForm = computed(() => props.info);
+
+const table = computed(() => props.tables);
+
+// 表单校验
+const rules = ref({
+ tplCategory: [{ required: true, message: "请选择生成模板", trigger: "blur" }],
+ packageName: [{ required: true, message: "请输入生成包路径", trigger: "blur" }],
+ moduleName: [{ required: true, message: "请输入生成模块名", trigger: "blur" }],
+ businessName: [{ required: true, message: "请输入生成业务名", trigger: "blur" }],
+ functionName: [{ required: true, message: "请输入生成功能名", trigger: "blur" }]
+});
+const subSelectChange = () => {
+ infoForm.value.subTableFkName = "";
+}
+const tplSelectChange = (value: string) => {
+ if (value !== "sub") {
+ infoForm.value.subTableName = "";
+ infoForm.value.subTableFkName = "";
+ }
+}
+const setSubTableColumns = (value: string) => {
+ table.value.forEach((item: any) => {
+ const name = item.tableName;
+ if (value === name) {
+ subColumns.value = item.columns;
+ return;
+ }
+ })
+}
+
+/** 查询菜单下拉树结构 */
+const getMenuTreeselect = async () => {
+ const res = await listMenu();
+ res.data.forEach(m => m.menuId = m.menuId.toString());
+ const data = proxy?.handleTree<MenuOptionsType>(res.data, "menuId");
+ if (data) {
+ menuOptions.value = data
+ }
+}
+
+watch(() => props.info.subTableName, val => {
+ setSubTableColumns(val);
+});
+
+onMounted(() => {
+ getMenuTreeselect();
+})
+</script>
diff --git a/src/views/tool/gen/importTable.vue b/src/views/tool/gen/importTable.vue
new file mode 100644
index 0000000..1ba79ec
--- /dev/null
+++ b/src/views/tool/gen/importTable.vue
@@ -0,0 +1,123 @@
+<template>
+ <!-- 导入表 -->
+ <el-dialog title="导入表" v-model="visible" width="1100px" top="5vh" append-to-body>
+ <el-form :model="queryParams" ref="queryFormRef" :inline="true">
+ <el-form-item label="数据源" prop="dataName">
+ <el-select v-model="queryParams.dataName" filterable placeholder="请选择/输入数据源名称" style="width: 200px">
+ <el-option v-for="item in dataNameList" :key="item" :label="item" :value="item"> </el-option>
+ </el-select>
+ </el-form-item>
+ <el-form-item label="表名称" prop="tableName">
+ <el-input v-model="queryParams.tableName" placeholder="请输入表名称" clearable @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item label="表描述" prop="tableComment">
+ <el-input v-model="queryParams.tableComment" placeholder="请输入表描述" clearable @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+ <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+ </el-form-item>
+ </el-form>
+ <el-row>
+ <el-table @row-click="clickRow" ref="tableRef" :data="dbTableList" @selection-change="handleSelectionChange" height="260px">
+ <el-table-column type="selection" width="55"></el-table-column>
+ <el-table-column prop="tableName" label="表名称" :show-overflow-tooltip="true"></el-table-column>
+ <el-table-column prop="tableComment" label="表描述" :show-overflow-tooltip="true"></el-table-column>
+ <el-table-column prop="createTime" label="创建时间"></el-table-column>
+ <el-table-column prop="updateTime" label="更新时间"></el-table-column>
+ </el-table>
+ <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
+ </el-row>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="handleImportTable">确 定</el-button>
+ <el-button @click="visible = false">取 消</el-button>
+ </div>
+ </template>
+ </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { listDbTable, importTable, getDataNames } from '@/api/tool/gen';
+import { DbTableQuery, DbTableVO } from '@/api/tool/gen/types';
+
+const total = ref(0);
+const visible = ref(false);
+const tables = ref<Array<string>>([]);
+const dbTableList = ref<Array<DbTableVO>>([]);
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+
+const tableRef = ref<ElTableInstance>();
+const queryFormRef = ref<ElFormInstance>();
+
+const queryParams = reactive<DbTableQuery>({
+ pageNum: 1,
+ pageSize: 10,
+ dataName: '',
+ tableName: '',
+ tableComment: ''
+});
+const dataNameList = ref<Array<string>>([]);
+
+const emit = defineEmits(["ok"]);
+
+/** 查询参数列表 */
+const show = (dataName: string) => {
+ getDataNameList();
+ if (dataName) {
+ queryParams.dataName = dataName;
+ } else {
+ queryParams.dataName = 'master';
+ }
+ getList();
+ visible.value = true;
+}
+/** 单击选择行 */
+const clickRow = (row: DbTableVO) => {
+ // ele bug
+ tableRef.value?.toggleRowSelection(row, false);
+}
+/** 多选框选中数据 */
+const handleSelectionChange = (selection: DbTableVO[]) => {
+ tables.value = selection.map(item => item.tableName);
+}
+/** 查询表数据 */
+const getList = async () => {
+ const res = await listDbTable(queryParams);
+ dbTableList.value = res.rows;
+ total.value = res.total;
+}
+/** 搜索按钮操作 */
+const handleQuery = () => {
+ queryParams.pageNum = 1;
+ getList();
+}
+/** 重置按钮操作 */
+const resetQuery = () => {
+ queryFormRef.value?.resetFields();
+ handleQuery();
+}
+/** 导入按钮操作 */
+const handleImportTable = async () => {
+ const tableNames = tables.value.join(",");
+ if (tableNames == "") {
+ proxy?.$modal.msgError("请选择要导入的表");
+ return;
+ }
+ const res = await importTable({ tables: tableNames, dataName: queryParams.dataName });
+ proxy?.$modal.msgSuccess(res.msg);
+ if (res.code === 200) {
+ visible.value = false;
+ emit("ok");
+ }
+}
+/** 查询多数据源名称 */
+const getDataNameList = async () => {
+ const res = await getDataNames()
+ dataNameList.value = res.data;
+}
+
+defineExpose({
+ show,
+});
+</script>
diff --git a/src/views/tool/gen/index.vue b/src/views/tool/gen/index.vue
new file mode 100644
index 0000000..cb684f8
--- /dev/null
+++ b/src/views/tool/gen/index.vue
@@ -0,0 +1,251 @@
+<template>
+ <div class="p-2">
+ <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
+ <div class="mb-[10px]" v-show="showSearch">
+ <el-card shadow="hover">
+ <el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px">
+ <el-form-item label="数据源" prop="dataName">
+ <el-select v-model="queryParams.dataName" filterable clearable placeholder="请选择/输入数据源名称" style="width: 200px">
+ <el-option key="" label="全部" value="" />
+ <el-option v-for="item in dataNameList" :key="item" :label="item" :value="item"> </el-option>
+ </el-select>
+ </el-form-item>
+ <el-form-item label="表名称" prop="tableName">
+ <el-input v-model="queryParams.tableName" placeholder="请输入表名称" clearable style="width: 200px" @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item label="表描述" prop="tableComment">
+ <el-input v-model="queryParams.tableComment" placeholder="请输入表描述" clearable style="width: 200px" @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item label="创建时间" style="width: 308px">
+ <el-date-picker
+ v-model="dateRange"
+ value-format="YYYY-MM-DD"
+ type="daterange"
+ range-separator="-"
+ start-placeholder="开始日期"
+ end-placeholder="结束日期"
+ ></el-date-picker>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+ <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+ </el-form-item>
+ </el-form>
+ </el-card>
+ </div>
+ </transition>
+
+ <el-card shadow="hover">
+ <template #header>
+ <el-row :gutter="10" class="mb8">
+ <el-col :span="1.5">
+ <el-button type="primary" plain icon="Download" @click="handleGenTable()" v-hasPermi="['tool:gen:code']">生成</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button type="info" plain icon="Upload" @click="openImportTable" v-hasPermi="['tool:gen:import']">导入</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button type="success" plain icon="Edit" :disabled="single" @click="handleEditTable()" v-hasPermi="['tool:gen:edit']">修改</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['tool:gen:remove']">
+ 删除
+ </el-button>
+ </el-col>
+ <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
+ </el-row>
+ </template>
+
+ <el-table v-loading="loading" :data="tableList" @selection-change="handleSelectionChange">
+ <el-table-column type="selection" align="center" width="55"></el-table-column>
+ <el-table-column label="序号" type="index" width="50" align="center">
+ <template #default="scope">
+ <span>{{ (queryParams.pageNum - 1) * queryParams.pageSize + scope.$index + 1 }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="数据源" align="center" prop="dataName" :show-overflow-tooltip="true" />
+ <el-table-column label="表名称" align="center" prop="tableName" :show-overflow-tooltip="true" />
+ <el-table-column label="表描述" align="center" prop="tableComment" :show-overflow-tooltip="true" />
+ <el-table-column label="实体" align="center" prop="className" :show-overflow-tooltip="true" />
+ <el-table-column label="创建时间" align="center" prop="createTime" width="160" />
+ <el-table-column label="更新时间" align="center" prop="updateTime" width="160" />
+ <el-table-column label="操作" align="center" width="330" class-name="small-padding fixed-width">
+ <template #default="scope">
+ <el-tooltip content="预览" placement="top">
+ <el-button link type="primary" icon="View" @click="handlePreview(scope.row)" v-hasPermi="['tool:gen:preview']"></el-button>
+ </el-tooltip>
+ <el-tooltip content="编辑" placement="top">
+ <el-button link type="primary" icon="Edit" @click="handleEditTable(scope.row)" v-hasPermi="['tool:gen:edit']"></el-button>
+ </el-tooltip>
+ <el-tooltip content="删除" placement="top">
+ <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['tool:gen:remove']"></el-button>
+ </el-tooltip>
+ <el-tooltip content="同步" placement="top">
+ <el-button link type="primary" icon="Refresh" @click="handleSynchDb(scope.row)" v-hasPermi="['tool:gen:edit']"></el-button>
+ </el-tooltip>
+ <el-tooltip content="生成代码" placement="top">
+ <el-button link type="primary" icon="Download" @click="handleGenTable(scope.row)" v-hasPermi="['tool:gen:code']"></el-button>
+ </el-tooltip>
+ </template>
+ </el-table-column>
+ </el-table>
+ <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
+ </el-card>
+
+ <!-- 预览界面 -->
+ <el-dialog :title="dialog.title" v-model="dialog.visible" width="80%" top="5vh" append-to-body class="scrollbar">
+ <el-tabs v-model="preview.activeName">
+ <el-tab-pane
+ v-for="(value, key) in preview.data"
+ :label="key.substring(key.lastIndexOf('/') + 1, key.indexOf('.vm'))"
+ :name="key.substring(key.lastIndexOf('/') + 1, key.indexOf('.vm'))"
+ :key="value"
+ >
+ <el-link :underline="false" icon="DocumentCopy" v-copyText="value" v-copyText:callback="copyTextSuccess" style="float:right">
+ 复制
+ </el-link>
+ <pre>{{ value }}</pre>
+ </el-tab-pane>
+ </el-tabs>
+ </el-dialog>
+ <import-table ref="importRef" @ok="handleQuery" />
+ </div>
+</template>
+
+<script setup name="Gen" lang="ts">
+import { listTable, previewTable, delTable, genCode, synchDb, getDataNames } from '@/api/tool/gen';
+import { TableQuery, TableVO } from '@/api/tool/gen/types';
+import router from '@/router';
+import ImportTable from './importTable.vue';
+
+const route = useRoute();
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+
+const tableList = ref<TableVO[]>([]);
+const loading = ref(true);
+const showSearch = ref(true);
+const ids = ref<Array<string | number>>([]);
+const single = ref(true);
+const multiple = ref(true);
+const total = ref(0);
+const dateRange = ref<[DateModelType, DateModelType]>(['', '']);
+const uniqueId = ref("");
+const dataNameList = ref<Array<string>>([]);
+
+const queryFormRef = ref<ElFormInstance>();
+const importRef = ref<InstanceType<typeof ImportTable>>();
+
+const queryParams = ref<TableQuery>({
+ pageNum: 1,
+ pageSize: 10,
+ tableName: '',
+ tableComment: '',
+ dataName: ""
+})
+
+const preview = ref<any>({
+ data: {},
+ activeName: 'domain.java'
+})
+const dialog = reactive<DialogOption>({
+ visible: false,
+ title: '代码预览'
+});
+
+onActivated(() => {
+ const time = route.query.t;
+ if (time != null && time != uniqueId.value) {
+ uniqueId.value = time as string;
+ queryParams.value.pageNum = Number(route.query.pageNum);
+ dateRange.value = ['', ''];
+ queryFormRef.value?.resetFields();
+ getList();
+ }
+})
+
+/** 查询多数据源名称 */
+const getDataNameList = async () => {
+ const res = await getDataNames()
+ dataNameList.value = res.data;
+}
+
+/** 查询表集合 */
+const getList = async () => {
+ loading.value = true;
+ const res = await listTable(proxy?.addDateRange(queryParams.value, dateRange.value));
+ tableList.value = res.rows;
+ total.value = res.total;
+ loading.value = false;
+}
+/** 搜索按钮操作 */
+const handleQuery = () => {
+ queryParams.value.pageNum = 1;
+ getList();
+}
+/** 生成代码操作 */
+const handleGenTable = async (row?: TableVO) => {
+ const tbIds = row?.tableId || ids.value;
+ if (tbIds == "") {
+ proxy?.$modal.msgError('请选择要生成的数据');
+ return;
+ }
+ if (row?.genType === "1") {
+ await genCode(row.tableId);
+ proxy?.$modal.msgSuccess('成功生成到自定义路径:' + row.genPath);
+ } else {
+ proxy?.$download.zip('/tool/gen/batchGenCode?tableIdStr=' + tbIds, 'ruoyi.zip');
+ }
+}
+/** 同步数据库操作 */
+const handleSynchDb = async (row: TableVO) => {
+ const tableId = row.tableId;
+ await proxy?.$modal.confirm('确认要强制同步"' + row.tableName + '"表结构吗?');
+ await synchDb(tableId);
+ proxy?.$modal.msgSuccess('同步成功');
+}
+/** 打开导入表弹窗 */
+const openImportTable = () => {
+ importRef.value?.show(queryParams.value.dataName);
+}
+/** 重置按钮操作 */
+const resetQuery = () => {
+ dateRange.value = ['', ''];
+ queryFormRef.value?.resetFields();
+ handleQuery();
+}
+/** 预览按钮 */
+const handlePreview = async (row: TableVO) => {
+ const res = await previewTable(row.tableId);
+ preview.value.data = res.data;
+ dialog.visible = true;
+ preview.value.activeName = 'domain.java';
+}
+/** 复制代码成功 */
+const copyTextSuccess = () => {
+ proxy?.$modal.msgSuccess('复制成功');
+}
+// 多选框选中数据
+const handleSelectionChange = (selection: TableVO[]) => {
+ ids.value = selection.map(item => item.tableId);
+ single.value = selection.length != 1;
+ multiple.value = !selection.length;
+}
+/** 修改按钮操作 */
+const handleEditTable = (row?: TableVO) => {
+ const tableId = row?.tableId || ids.value[0];
+ router.push({ path: '/tool/gen-edit/index/' + tableId, query: { pageNum: queryParams.value.pageNum } });
+}
+/** 删除按钮操作 */
+const handleDelete = async (row?: TableVO) => {
+ const tableIds = row?.tableId || ids.value;
+ await proxy?.$modal.confirm('是否确认删除表编号为"' + tableIds + '"的数据项?');
+ await delTable(tableIds);
+ await getList();
+ proxy?.$modal.msgSuccess('删除成功');
+}
+
+onMounted(() => {
+ getList();
+ getDataNameList();
+})
+</script>
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..3dc32e7
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,26 @@
+{
+ "compilerOptions": {
+ "target": "es2022",
+ "useDefineForClassFields": true,
+ "module": "esnext",
+ "moduleResolution": "node",
+ "strict": true,
+ "jsx": "preserve",
+ "sourceMap": true,
+ "resolveJsonModule": true,
+ "esModuleInterop": true,
+ "lib": ["esnext", "dom"],
+ "baseUrl": ".",
+ "allowJs": true,
+ "paths": {
+ "@/*": ["src/*"]
+ },
+ "types": ["vite/client"],
+ "skipLibCheck": true,
+ // 允许默认导入
+ "allowSyntheticDefaultImports": true,
+ "forceConsistentCasingInFileNames": true
+ },
+ "include": ["src/**/*.ts", "src/**/*.vue", "src/types/**/*.d.ts"],
+ "exclude": ["node_modules", "dist", "**/*.js"]
+}
diff --git a/vite.config.ts b/vite.config.ts
new file mode 100644
index 0000000..8b7163b
--- /dev/null
+++ b/vite.config.ts
@@ -0,0 +1,118 @@
+import { UserConfig, ConfigEnv, loadEnv, defineConfig } from 'vite';
+
+import createPlugins from './vite/plugins';
+
+import path from 'path';
+export default defineConfig(({ mode, command }: ConfigEnv): UserConfig => {
+ const env = loadEnv(mode, process.cwd());
+ return {
+ // 部署生产环境和开发环境下的URL。
+ // 默认情况下,vite 会假设你的应用是被部署在一个域名的根路径上
+ base: env.VITE_APP_CONTEXT_PATH,
+ resolve: {
+ alias: {
+ '~': path.resolve(__dirname, './'),
+ '@': path.resolve(__dirname, './src'),
+ 'vue-i18n': 'vue-i18n/dist/vue-i18n.cjs.js'
+ },
+ extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue']
+ },
+ // https://cn.vitejs.dev/config/#resolve-extensions
+ plugins: createPlugins(env, command === 'build'),
+ server: {
+ host: '0.0.0.0',
+ port: Number(env.VITE_APP_PORT),
+ open: false,
+ proxy: {
+ [env.VITE_APP_BASE_API]: {
+ target: 'http://localhost:8080',
+ changeOrigin: true,
+ ws: true,
+ rewrite: (path) => path.replace(new RegExp('^' + env.VITE_APP_BASE_API), '')
+ }
+ }
+ },
+ css: {
+ preprocessorOptions: {
+ scss: {
+ javascriptEnabled: true
+ }
+ },
+ postcss: {
+ plugins: [
+ {
+ postcssPlugin: 'internal:charset-removal',
+ AtRule: {
+ charset: (atRule) => {
+ if (atRule.name === 'charset') {
+ atRule.remove();
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ // 预编译
+ optimizeDeps: {
+ include: [
+ 'vue',
+ 'vue-router',
+ 'pinia',
+ 'axios',
+ '@vueuse/core',
+ 'path-to-regexp',
+ 'echarts',
+ '@wangeditor/editor',
+ '@wangeditor/editor-for-vue',
+ 'vue-i18n',
+ '@vueup/vue-quill',
+ '@iconify/iconify',
+
+ 'element-plus/es/components/form/style/css',
+ 'element-plus/es/components/form-item/style/css',
+ 'element-plus/es/components/button/style/css',
+ 'element-plus/es/components/input/style/css',
+ 'element-plus/es/components/input-number/style/css',
+ 'element-plus/es/components/switch/style/css',
+ 'element-plus/es/components/upload/style/css',
+ 'element-plus/es/components/menu/style/css',
+ 'element-plus/es/components/col/style/css',
+ 'element-plus/es/components/icon/style/css',
+ 'element-plus/es/components/row/style/css',
+ 'element-plus/es/components/tag/style/css',
+ 'element-plus/es/components/dialog/style/css',
+ 'element-plus/es/components/loading/style/css',
+ 'element-plus/es/components/radio/style/css',
+ 'element-plus/es/components/radio-group/style/css',
+ 'element-plus/es/components/popover/style/css',
+ 'element-plus/es/components/scrollbar/style/css',
+ 'element-plus/es/components/tooltip/style/css',
+ 'element-plus/es/components/dropdown/style/css',
+ 'element-plus/es/components/dropdown-menu/style/css',
+ 'element-plus/es/components/dropdown-item/style/css',
+ 'element-plus/es/components/sub-menu/style/css',
+ 'element-plus/es/components/menu-item/style/css',
+ 'element-plus/es/components/divider/style/css',
+ 'element-plus/es/components/card/style/css',
+ 'element-plus/es/components/link/style/css',
+ 'element-plus/es/components/breadcrumb/style/css',
+ 'element-plus/es/components/breadcrumb-item/style/css',
+ 'element-plus/es/components/table/style/css',
+ 'element-plus/es/components/tree-select/style/css',
+ 'element-plus/es/components/table-column/style/css',
+ 'element-plus/es/components/select/style/css',
+ 'element-plus/es/components/option/style/css',
+ 'element-plus/es/components/pagination/style/css',
+ 'element-plus/es/components/tree/style/css',
+ 'element-plus/es/components/alert/style/css',
+ 'element-plus/es/components/checkbox/style/css',
+ 'element-plus/es/components/date-picker/style/css',
+ 'element-plus/es/components/transfer/style/css',
+ 'element-plus/es/components/tabs/style/css',
+ 'element-plus/es/components/image/style/css',
+ 'element-plus/es/components/tab-pane/style/css'
+ ]
+ }
+ };
+});
diff --git a/vite/plugins/auto-import.ts b/vite/plugins/auto-import.ts
new file mode 100644
index 0000000..072ef61
--- /dev/null
+++ b/vite/plugins/auto-import.ts
@@ -0,0 +1,24 @@
+import AutoImport from 'unplugin-auto-import/vite';
+import { ElementPlusResolver } from 'unplugin-vue-components/resolvers';
+import IconsResolver from 'unplugin-icons/resolver';
+
+export default (path: any) => {
+ return AutoImport({
+ // 自动导入 Vue 相关函数
+ imports: ['vue', 'vue-router', '@vueuse/core', 'pinia'],
+ eslintrc: {
+ enabled: false,
+ filepath: './.eslintrc-auto-import.json',
+ globalsPropValue: true
+ },
+ resolvers: [
+ // 自动导入 Element Plus 相关函数ElMessage, ElMessageBox... (带样式)
+ ElementPlusResolver(),
+ IconsResolver({
+ prefix: 'Icon'
+ })
+ ],
+ vueTemplate: true, // 是否在 vue 模板中自动导入
+ dts: path.resolve(path.resolve(__dirname, '../../src'), 'types', 'auto-imports.d.ts')
+ });
+};
diff --git a/vite/plugins/components.ts b/vite/plugins/components.ts
new file mode 100644
index 0000000..336f5cf
--- /dev/null
+++ b/vite/plugins/components.ts
@@ -0,0 +1,17 @@
+import Components from 'unplugin-vue-components/vite';
+import { ElementPlusResolver } from 'unplugin-vue-components/resolvers';
+import IconsResolver from 'unplugin-icons/resolver';
+
+export default (path: any) => {
+ return Components({
+ resolvers: [
+ // 自动导入 Element Plus 组件
+ ElementPlusResolver(),
+ // 自动注册图标组件
+ IconsResolver({
+ enabledCollections: ['ep']
+ })
+ ],
+ dts: path.resolve(path.resolve(__dirname, '../../src'), 'types', 'components.d.ts')
+ });
+};
diff --git a/vite/plugins/compression.ts b/vite/plugins/compression.ts
new file mode 100644
index 0000000..6a197a1
--- /dev/null
+++ b/vite/plugins/compression.ts
@@ -0,0 +1,27 @@
+import compression from 'vite-plugin-compression';
+
+export default function createCompression(env: any) {
+ const { VITE_BUILD_COMPRESS } = env;
+ const plugin: any[] = [];
+ if (VITE_BUILD_COMPRESS) {
+ const compressList = VITE_BUILD_COMPRESS.split(',');
+ if (compressList.includes('gzip')) {
+ plugin.push(
+ compression({
+ ext: '.gz',
+ deleteOriginFile: false
+ })
+ );
+ }
+ if (compressList.includes('brotli')) {
+ plugin.push(
+ compression({
+ ext: '.br',
+ algorithm: 'brotliCompress',
+ deleteOriginFile: false
+ })
+ );
+ }
+ }
+ return plugin;
+}
diff --git a/vite/plugins/icons.ts b/vite/plugins/icons.ts
new file mode 100644
index 0000000..883f230
--- /dev/null
+++ b/vite/plugins/icons.ts
@@ -0,0 +1,8 @@
+import Icons from 'unplugin-icons/vite';
+
+export default () => {
+ return Icons({
+ // 自动安装图标库
+ autoInstall: true
+ });
+};
diff --git a/vite/plugins/index.ts b/vite/plugins/index.ts
new file mode 100644
index 0000000..a31412d
--- /dev/null
+++ b/vite/plugins/index.ts
@@ -0,0 +1,22 @@
+import vue from '@vitejs/plugin-vue';
+import createUnoCss from './unocss';
+import createAutoImport from './auto-import';
+import createComponents from './components';
+import createIcons from './icons';
+import createSvgIconsPlugin from './svg-icon';
+import createCompression from './compression';
+import createSetupExtend from './setup-extend';
+import path from 'path';
+
+export default (viteEnv: any, isBuild = false): [] => {
+ const vitePlusgins: any = [];
+ vitePlusgins.push(vue());
+ vitePlusgins.push(createUnoCss());
+ vitePlusgins.push(createAutoImport(path));
+ vitePlusgins.push(createComponents(path));
+ vitePlusgins.push(createCompression(viteEnv));
+ vitePlusgins.push(createIcons());
+ vitePlusgins.push(createSvgIconsPlugin(path, isBuild));
+ vitePlusgins.push(createSetupExtend());
+ return vitePlusgins;
+};
diff --git a/vite/plugins/setup-extend.ts b/vite/plugins/setup-extend.ts
new file mode 100644
index 0000000..a1dbb5a
--- /dev/null
+++ b/vite/plugins/setup-extend.ts
@@ -0,0 +1,5 @@
+import setupExtend from 'unplugin-vue-setup-extend-plus/vite'
+
+export default () => {
+ return setupExtend({})
+};
diff --git a/vite/plugins/svg-icon.ts b/vite/plugins/svg-icon.ts
new file mode 100644
index 0000000..70296da
--- /dev/null
+++ b/vite/plugins/svg-icon.ts
@@ -0,0 +1,10 @@
+import { createSvgIconsPlugin } from 'vite-plugin-svg-icons';
+export default (path: any, isBuild: boolean) => {
+ return createSvgIconsPlugin({
+ // 指定需要缓存的图标文件夹
+ iconDirs: [path.resolve(path.resolve(__dirname, '../../src'), 'assets/icons/svg')],
+ // 指定symbolId格式
+ symbolId: 'icon-[dir]-[name]',
+ svgoOptions: isBuild
+ });
+};
diff --git a/vite/plugins/unocss.ts b/vite/plugins/unocss.ts
new file mode 100644
index 0000000..76585c7
--- /dev/null
+++ b/vite/plugins/unocss.ts
@@ -0,0 +1,14 @@
+import UnoCss from 'unocss/vite';
+import { presetUno, presetAttributify, presetIcons } from 'unocss';
+
+export default () => {
+ return UnoCss({
+ presets: [presetUno(), presetAttributify(), presetIcons()],
+ // rules: [['search', {}]],
+ shortcuts: {
+ 'panel-title':
+ 'pb-[5px] font-sans leading-[1.1] font-medium text-base text-[#6379bb] border-b border-b-solid border-[var(--el-border-color-light)] mb-5 mt-0'
+ },
+ hmrTopLevelAwait: false // unocss默认是true,低版本浏览器是不支持的,启动后会报错
+ });
+};
--
Gitblit v1.8.0