commit 0a20099289bd89282855f38283ce078af8c255d5
Author: 汪圳 <13199755+yixiangweb@user.noreply.gitee.com>
Date: Fri Dec 5 10:17:26 2025 +0800
首次提交
diff --git a/.cursor/mcp.json b/.cursor/mcp.json
new file mode 100644
index 0000000..b87707f
--- /dev/null
+++ b/.cursor/mcp.json
@@ -0,0 +1,7 @@
+{
+ "mcpServers": {
+ "vue-mcp": {
+ "url": "http://localhost:3000/__mcp/sse"
+ }
+ }
+}
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..00ee2de
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,15 @@
+# http://editorconfig.org
+root = true
+
+# 表示所有文件适用
+[*]
+charset = utf-8 # 设置文件字符集为 utf-8
+end_of_line = lf # 控制换行类型(lf | cr | crlf)
+indent_style = space # 缩进风格(tab | space)
+indent_size = 2 # 缩进大小
+insert_final_newline = true # 始终在文件末尾插入一个新行
+
+# 表示仅 md 文件适用以下规则
+[*.md]
+max_line_length = off # 关闭最大行长度限制
+trim_trailing_whitespace = false # 关闭末尾空格修剪
diff --git a/.env.development b/.env.development
new file mode 100644
index 0000000..8dbca7f
--- /dev/null
+++ b/.env.development
@@ -0,0 +1,16 @@
+# 应用端口
+VITE_APP_PORT=3000
+# 项目名称
+VITE_APP_TITLE=vue3-element-admin
+# 代理前缀
+VITE_APP_BASE_API=/dev-api
+
+# 接口地址
+VITE_APP_API_URL=https://api.youlai.tech # 线上
+# VITE_APP_API_URL=http://localhost:8989 # 本地
+
+# WebSocket 端点(不配置则关闭),线上 ws://api.youlai.tech/ws ,本地 ws://localhost:8989/ws
+VITE_APP_WS_ENDPOINT=
+
+# 启用 Mock 服务
+VITE_MOCK_DEV_SERVER=false
diff --git a/.env.production b/.env.production
new file mode 100644
index 0000000..cf3bf9a
--- /dev/null
+++ b/.env.production
@@ -0,0 +1,6 @@
+# 代理前缀
+VITE_APP_BASE_API = '/prod-api'
+# 项目名称
+VITE_APP_TITLE=vue3-element-admin
+# WebSocket端点(可选)
+#VITE_APP_WS_ENDPOINT=wss://api.youlai.tech/ws
diff --git a/.eslintrc-auto-import.json b/.eslintrc-auto-import.json
new file mode 100644
index 0000000..0e8546f
--- /dev/null
+++ b/.eslintrc-auto-import.json
@@ -0,0 +1,316 @@
+{
+ "globals": {
+ "Component": true,
+ "ComponentPublicInstance": true,
+ "ComputedRef": true,
+ "EffectScope": true,
+ "ElMessage": true,
+ "ElMessageBox": true,
+ "ElNotification": true,
+ "InjectionKey": true,
+ "PropType": true,
+ "Ref": true,
+ "VNode": 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,
+ "createReusableTemplate": true,
+ "createSharedComposable": true,
+ "createTemplatePromise": 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,
+ "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,
+ "toValue": true,
+ "triggerRef": true,
+ "tryOnBeforeMount": true,
+ "tryOnBeforeUnmount": true,
+ "tryOnMounted": true,
+ "tryOnScopeDispose": true,
+ "tryOnUnmounted": true,
+ "unref": true,
+ "unrefElement": true,
+ "until": true,
+ "useActiveElement": true,
+ "useAnimate": true,
+ "useArrayDifference": true,
+ "useArrayEvery": true,
+ "useArrayFilter": true,
+ "useArrayFind": true,
+ "useArrayFindIndex": true,
+ "useArrayFindLast": true,
+ "useArrayIncludes": 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,
+ "useParentElement": true,
+ "usePerformanceObserver": 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,
+ "watchDeep": true,
+ "watchEffect": true,
+ "watchIgnorable": true,
+ "watchImmediate": true,
+ "watchOnce": true,
+ "watchPausable": true,
+ "watchPostEffect": true,
+ "watchSyncEffect": true,
+ "watchThrottled": true,
+ "watchTriggerable": true,
+ "watchWithFilter": true,
+ "useRoute": true,
+ "useRouter": true,
+ "storeToRefs": true,
+ "whenever": true,
+ "DirectiveBinding": true,
+ "ExtractDefaultPropTypes": true,
+ "ExtractPropTypes": true,
+ "ExtractPublicPropTypes": true,
+ "MaybeRef": true,
+ "MaybeRefOrGetter": true,
+ "WritableComputedRef": true,
+ "acceptHMRUpdate": true,
+ "createPinia": true,
+ "defineStore": true,
+ "getActivePinia": true,
+ "injectLocal": true,
+ "mapActions": true,
+ "mapGetters": true,
+ "mapState": true,
+ "mapStores": true,
+ "mapWritableState": true,
+ "onBeforeRouteLeave": true,
+ "onBeforeRouteUpdate": true,
+ "onWatcherCleanup": true,
+ "provideLocal": true,
+ "setActivePinia": true,
+ "setMapStoreSuffix": true,
+ "useClipboardItems": true,
+ "useI18n": true,
+ "useId": true,
+ "useLink": true,
+ "useModel": true,
+ "useTemplateRef": true
+ }
+}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..a6d77ca
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,20 @@
+node_modules
+.DS_Store
+dist
+dist-ssr
+*.local
+.history
+
+# Editor directories and files
+.idea
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.local
+
+stats.html
+pnpm-lock.yaml
+package-lock.json
+.stylelintcache
+.eslintcache
diff --git a/.prettierignore b/.prettierignore
new file mode 100644
index 0000000..44421a7
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1,12 @@
+dist
+node_modules
+public
+.husky
+.vscode
+.idea
+*.sh
+*.md
+
+src/assets
+stats.html
+pnpm-lock.yaml
diff --git a/.prettierrc.yaml b/.prettierrc.yaml
new file mode 100644
index 0000000..d9cf0c7
--- /dev/null
+++ b/.prettierrc.yaml
@@ -0,0 +1,41 @@
+# 在单参数箭头函数中始终添加括号
+arrowParens: "always"
+# JSX 多行元素的闭合标签另起一行
+bracketSameLine: false
+# 对象字面量中的括号之间添加空格
+bracketSpacing: true
+# 自动格式化嵌入的代码(如 Markdown 和 HTML 内的代码)
+embeddedLanguageFormatting: "auto"
+# 忽略 HTML 空白敏感度,将空白视为非重要内容
+htmlWhitespaceSensitivity: "ignore"
+# 不插入 @prettier 的 pragma 注释
+insertPragma: false
+# 在 JSX 中使用双引号
+jsxSingleQuote: false
+# 每行代码的最大长度限制为 100 字符
+printWidth: 100
+# 在 Markdown 中保留原有的换行格式
+proseWrap: "preserve"
+# 仅在必要时添加对象属性的引号
+quoteProps: "as-needed"
+# 不要求文件开头插入 @prettier 的 pragma 注释
+requirePragma: false
+# 在语句末尾添加分号
+semi: true
+# 使用双引号而不是单引号
+singleQuote: false
+# 缩进使用 2 个空格
+tabWidth: 2
+# 在多行元素的末尾添加逗号(ES5 支持的对象、数组等)
+trailingComma: "es5"
+# 使用空格而不是制表符缩进
+useTabs: false
+# Vue 文件中的 ",
+ "",
+ "",
+ ""
+ ],
+ "description": "Vue3.0"
+ }
+}
diff --git a/.vscode/vue3.2.code-snippets b/.vscode/vue3.2.code-snippets
new file mode 100644
index 0000000..a083940
--- /dev/null
+++ b/.vscode/vue3.2.code-snippets
@@ -0,0 +1,17 @@
+{
+ "Vue3.2+快速生成模板": {
+ "scope": "vue",
+ "prefix": "Vue3.2+",
+ "body": [
+ "",
+ "",
+ "",
+ " ${1:test}
",
+ "",
+ "",
+ "",
+ ""
+ ],
+ "description": "Vue3.2+"
+ }
+}
diff --git a/.vscode/vue3.3.code-snippets b/.vscode/vue3.3.code-snippets
new file mode 100644
index 0000000..705e04f
--- /dev/null
+++ b/.vscode/vue3.3.code-snippets
@@ -0,0 +1,21 @@
+{
+ "Vue3.3+defineOptions快速生成模板": {
+ "scope": "vue",
+ "prefix": "Vue3.3+",
+ "body": [
+ "",
+ "",
+ "",
+ " ${1:test}
",
+ "",
+ "",
+ "",
+ ""
+ ],
+ "description": "Vue3.3+defineOptions快速生成模板"
+ }
+}
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..aef3f6d
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,386 @@
+
+# 2.11.5 (2024/6/18)
+
+## ✨ feat
+
+- 支持后端文件导入([#142](https://github.com/youlaitech/vue3-element-admin/pull/142)) [@cshaptx4869](https://github.com/cshaptx4869)
+
+
+## 🐛 fix
+- vue-dev-tools 插件导致菜单路由切换卡死,暂时关闭 ([28349e](https://github.com/youlaitech/vue3-element-admin/commit/28349efe147afab36531ba148eaac3a448fe6c71)) [@haoxianrui](https://github.com/haoxianrui)
+
+
+
+# 2.11.4 (2024/6/16)
+
+## ✨ feat
+
+- 操作栏增加render配置参数([#138](https://github.com/youlaitech/vue3-element-admin/pull/140)) [@cshaptx4869](https://github.com/cshaptx4869)
+- 左侧工具栏增加type配置参数([#141](https://github.com/youlaitech/vue3-element-admin/pull/141)) [@diamont1001](https://github.com/diamont1001)
+
+## ♻️ refactor
+- 更换权限分配弹窗类型为 drawer 并添加父子联动开关([2d9193](https://github.com/youlaitech/vue3-element-admin/commit/2d9193c47fd224f01f82b9c0b2bbeb5e7cb33584)) [@haoxianrui](https://github.com/haoxianrui)
+
+
+
+# 2.11.3 (2024/6/11)
+
+## ✨ feat
+
+- 支持默认工具栏的导入([#138](https://github.com/youlaitech/vue3-element-admin/pull/138)) [@cshaptx4869](https://github.com/cshaptx4869)
+- 添加CURD导入示例([19e7bb](https://github.com/youlaitech/vue3-element-admin/commit/eab91effd6a01d5a3d9257249c8d06aa252b3bf8)) [@cshaptx4869](https://github.com/cshaptx4869)
+
+## ♻️ refactor
+- 修改导出全量数据选项文本([904fec](https://github.com/youlaitech/vue3-element-admin/commit/904fecad65217650482fcdbb10ffb7f3d27eb9ea)) [@cshaptx4869](https://github.com/cshaptx4869)
+
+## 🐛 fix
+- 菜单列表未适配el-icon导致图标不显示问题修复([e72b68](https://github.com/youlaitech/vue3-element-admin/commit/e72b68337562b5a7ea24ad55bbe00023e1266b40)) [@haoxianrui](https://github.com/haoxianrui)
+
+# 2.11.2 (2024/6/8)
+
+## ✨ feat
+
+- 支持表格远程筛选([#131](https://github.com/youlaitech/vue3-element-admin/pull/131)) [@cshaptx4869](https://github.com/cshaptx4869)
+- 支持标签输入框([#132](https://github.com/youlaitech/vue3-element-admin/pull/132)) [@cshaptx4869](https://github.com/cshaptx4869)
+- 表单项支持tips配置([#133](https://github.com/youlaitech/vue3-element-admin/pull/133)) [@cshaptx4869](https://github.com/cshaptx4869)
+- 前端导出支持全量数据([#134](https://github.com/youlaitech/vue3-element-admin/pull/134)) [@cshaptx4869](https://github.com/cshaptx4869)
+- 支持选中数据导出([#135](https://github.com/youlaitech/vue3-element-admin/pull/135)) [@cshaptx4869](https://github.com/cshaptx4869)
+- 表格默认工具栏的导出、搜索按钮增加权限点控制([883128](https://github.com/youlaitech/vue3-element-admin/commit/8831289b655f2cc086ecdababaa89f8d8a087c42)) [@cshaptx4869](https://github.com/cshaptx4869)
+- 页签title支持动态设置([23876a](https://github.com/youlaitech/vue3-element-admin/commit/23876aa396143bf77cb5c86af8d6023d9ff6555a)) [@haoxianrui](https://github.com/haoxianrui)
+
+## ♻️ refactor
+- 默认工具栏支持自定义([#136](https://github.com/youlaitech/vue3-element-admin/pull/136)) [@cshaptx4869](https://github.com/cshaptx4869)
+- 未配置全量导出接口时选项隐藏([eab91ef](https://github.com/youlaitech/vue3-element-admin/commit/eab91effd6a01d5a3d9257249c8d06aa252b3bf8)) [@cshaptx4869](https://github.com/cshaptx4869)
+
+## 🐛 fix
+- 修复注销登出后redirect跳转路由参数丢失([5626017](https://github.com/youlaitech/vue3-element-admin/commit/562601736731afd20bb1a5140d856f6515720159)) [@haoxianrui](https://github.com/haoxianrui)
+
+# 2.11.1 (2024/6/6)
+
+## ✨ feat
+
+- 增加pagination、request、parseData配置参数([#119](https://github.com/youlaitech/vue3-element-admin/pull/119)) [@cshaptx4869](https://github.com/cshaptx4869)
+- 增加返回顶部功能([#120](https://github.com/youlaitech/vue3-element-admin/pull/120)) [@cshaptx4869](https://github.com/cshaptx4869)
+- 支持前端导出([#126](https://github.com/youlaitech/vue3-element-admin/pull/126)) [@cshaptx4869](https://github.com/cshaptx4869)
+
+## ♻️ refactor
+- 重构布局样式(解决页面抖动问题)([#116](https://github.com/youlaitech/vue3-element-admin/pull/116)) [@cshaptx4869](https://github.com/cshaptx4869)
+- 修改CURD示例编辑弹窗尺寸([#121](https://github.com/youlaitech/vue3-element-admin/pull/121)) [@cshaptx4869](https://github.com/cshaptx4869)
+- 统一注册vue插件([#122](https://github.com/youlaitech/vue3-element-admin/pull/122)) [@cshaptx4869](https://github.com/cshaptx4869)
+- 默认主题跟随系统([#128](https://github.com/youlaitech/vue3-element-admin/pull/128)) [@cshaptx4869](https://github.com/cshaptx4869)
+- 增加"scss.lint.unknownAtRules": "ignore"代码,解决style中使用@apply提示unknow at rules@apply提示问题([Gitee#22](https://gitee.com/youlaiorg/vue3-element-admin/pulls/22)) [@zjsy521](https://gitee.com/zjsy521)
+
+## 🐛 fix
+- 修复左侧布局移动端菜单弹出样式 ([#117](https://github.com/youlaitech/vue3-element-admin/pull/117)) [@cshaptx4869](https://github.com/cshaptx4869)
+
+- 修复编辑后未清空id再新增菜单覆盖的问题([0e78eeb](https://github.com/youlaitech/vue3-element-admin/commit/0e78eeb75008fa8e9732b1b4e7d7a1ea345c7a1b)) [@haoxianrui](https://github.com/haoxianrui)
+- 修复水印层级问题([#123](https://github.com/youlaitech/vue3-element-admin/pull/123)) [@cshaptx4869](https://github.com/cshaptx4869)
+- 修复混合布局样式问题([#124](https://github.com/youlaitech/vue3-element-admin/pull/124)) [@cshaptx4869](https://github.com/cshaptx4869)
+- 修复关闭弹窗时没有clearValidate问题([#125](https://github.com/youlaitech/vue3-element-admin/pull/125)) [@andm31](https://github.com/andm31)
+
+
+
+# 2.11.0 (2024/5/27)
+
+## ✨ feat
+- 菜单添加路由参数设置(author by [haoxianrui](https://github.com/haoxianrui))
+- 增加列表选择组件(author by [cshaptx4869](https://github.com/cshaptx4869))
+- 增加列表选择组件使用示例(author by [cshaptx4869](https://github.com/cshaptx4869))
+- 增加defaultToolbar配置参数(author by [cshaptx4869](https://github.com/cshaptx4869))
+- 表单弹窗支持drawer模式(author by [cshaptx4869](https://github.com/cshaptx4869))
+- 表单项增加computed和watchEffect配置(author by [cshaptx4869](https://github.com/cshaptx4869))
+- 支持switch属性修改(author by [cshaptx4869](https://github.com/cshaptx4869))
+- 表单项增加文本类型支持(author by [cshaptx4869](https://github.com/cshaptx4869))
+- 列表列增加show配置项(author by [cshaptx4869](https://github.com/cshaptx4869))
+- 支持搜索表单显隐控制(author by [cshaptx4869](https://github.com/cshaptx4869))
+- 支持input属性修改(author by [cshaptx4869](https://github.com/cshaptx4869))
+- search配置新增函数能力拓展(author by [xiudaozhe](https://github.com/xiudaozhe))
+- 表格新增列设置控制(author by [haoxianrui](https://github.com/haoxianrui))
+- 搜索添加展开和收缩(author by [haoxianrui](https://github.com/haoxianrui))
+- watch函数增加配置项参数返回(author by [cshaptx4869](https://github.com/cshaptx4869))
+
+## ♻️ refactor
+- 重构图标选择组件(author by [cshaptx4869](https://github.com/cshaptx4869))
+- 重构列表选择组件默认样式 (author by [cshaptx4869](https://github.com/cshaptx4869))
+- 加强对话框表单组件和列表选择组件(author by [cshaptx4869](https://github.com/cshaptx4869))
+- routeMeta增加alwaysShow字段声明(author by [cshaptx4869](https://github.com/cshaptx4869))
+- 分页组件增加溢出滚动效果(author by [cshaptx4869](https://github.com/cshaptx4869))
+- 修正登录表单的Ref类型(author by [cshaptx4869](https://github.com/cshaptx4869))
+- 点击表格刷新按钮不重置页码(author by [cshaptx4869](https://github.com/cshaptx4869))
+- 筛选列超出一定高度滚动(author by [cshaptx4869](https://github.com/cshaptx4869))
+- 优化加强initFn函数,表单项增加initFn函数(author by [cshaptx4869](https://github.com/cshaptx4869))
+- 重构watch、computed、watchEffect调用(author by [cshaptx4869](https://github.com/cshaptx4869))
+- 修改操作成功提示(author by [cshaptx4869](https://github.com/cshaptx4869))
+- PageSearch 改用card作为容器,样式改用unocss写法(author by [cshaptx4869](https://github.com/cshaptx4869))
+- 优化首页 loading 动画效果author by [haoxianrui](https://github.com/haoxianrui))
+
+
+## 🐛 fix
+- 路由是否始终显示不限制只有顶级目录才有的配置,开放至菜单 (author by [haoxianrui](https://github.com/haoxianrui))
+- sockjs-client 报错 global is not defined 导致开发环境无法打开 WebSocket 页面问题修复 (author by [haoxianrui](https://github.com/haoxianrui))
+- 发送用户重启密码功能,最少为6位字符(小于6位登陆时不允许的问题) (author by [dreamnyj](https://gitee.com/dreamnyj))
+- 修复系统设置面板滚动条问题(author by [cshaptx4869](https://github.com/cshaptx4869))
+- 修复表单插槽失效问题(author by [cshaptx4869](https://github.com/cshaptx4869))
+- 修改tagsview刷新丢失query问题(author by [xiudaozhe](https://github.com/xiudaozhe))
+
+## 📦️ build
+- 升级 NPM 包版本至最新 (author by [haoxianrui](https://github.com/haoxianrui))
+
+## ⚙️ ci
+- 规整脚本执行命令(author by [cshaptx4869](https://github.com/cshaptx4869))
+
+
+# 2.10.1 (2024/5/4)
+
+## ♻️ refactor
+- 抽离CURD的使用部分代码为Hooks实现(author by [cshaptx4869](https://github.com/cshaptx4869))
+- 修改CURD导入权限点标识名(author by [cshaptx4869](https://github.com/cshaptx4869))
+- cURD表单字段支持watch监听(author by [cshaptx4869](https://github.com/cshaptx4869))
+- cURD表单input支持number修饰(author by [cshaptx4869](https://github.com/cshaptx4869))
+- cURD表单组件支持checkbox多选框(author by [cshaptx4869](https://github.com/cshaptx4869))
+- 优化axios响应数据TS类型提示(author by [cshaptx4869](https://github.com/cshaptx4869))
+- 修改CURD表单组件自定义类型的attrs传值(author by [cshaptx4869](https://github.com/cshaptx4869))
+- 同步重置密码按钮权限标识重命名(author by [haoxianrui](https://github.com/haoxianrui))
+- 重构API为静态方法实现模块化管理,并将types.ts重命名为model.ts用于存放接口模型定义(author by [haoxianrui](https://github.com/haoxianrui))
+
+
+## 🐛 fix
+- sockjs-client 报错 global is not defined 导致开发环境无法打开 WebSocket 页面问题修复 (author by [haoxianrui](https://github.com/haoxianrui))
+- 主题颜色设置覆盖暗黑模式下el-table行激活的背景色问题修复 (author by [haoxianrui](https://github.com/haoxianrui))
+- 修复因API接口调整而影响的调用页面的问题 (author by [haoxianrui](https://github.com/haoxianrui))
+
+## 📦️ build
+- 升级 NPM 包版本至最新 (author by [haoxianrui](https://github.com/haoxianrui))
+
+
+# 2.10.0 (2024/4/26)
+## ✨ feat
+- 封装增删改查组件(author by [cshaptx4869](https://github.com/cshaptx4869))
+- 集成 vite-plugin-vue-devtools 插件(author by [Tricker39](https://github.com/Tricker39))
+- 增加CURD配置化实现(author by [cshaptx4869](https://github.com/cshaptx4869))
+
+
+# 2.9.3 (2024/04/14)
+## ✨ feat
+- 增加vue文件代码片段(author by [cshaptx4869](https://github.com/cshaptx4869))
+- 菜单 hover 背景色添加值全局SCSS变量进行控制(author by [haoxianrui](https://github.com/haoxianrui))
+
+## ♻️ refactor
+- 加强基础国际化(author by [cshaptx4869](https://github.com/cshaptx4869))
+- 增加语言和布局大小枚举类型(author by [cshaptx4869](https://github.com/cshaptx4869))
+- 增加侧边栏状态枚举类型(author by [cshaptx4869](https://github.com/cshaptx4869))
+- 使用布局枚举替换字面量(author by [haoxianrui](https://github.com/haoxianrui))
+- 控制台使用静态数据循环渲染(author by [april](mailto:april@zen-game.cn))
+- 本地缓存的 token 变量重命名(author by [haoxianrui](https://github.com/haoxianrui))
+- 完善 Vite 环境变量类型声明(author by [haoxianrui](https://github.com/haoxianrui))
+
+## 🐛 fix
+- 修复构建时提示iconComponent.name可能为undefined的报错 (author by [wangji1042](https://github.com/wangji1042))
+- 修复浏览器密码自动填充时可能存在的报错 (author by [cshaptx4869](https://github.com/cshaptx4869))
+- 修复eslint报错(author by [cshaptx4869](https://github.com/cshaptx4869))
+- 移动端下点击左侧菜单节点后关闭侧边栏(author by [haoxianrui](https://github.com/haoxianrui))
+- 添加 size 类型断言修复类型报错(author by [haoxianrui](https://github.com/haoxianrui))
+
+## 📦️ build
+- husky9.x版本适配 (author by [cshaptx4869](https://github.com/cshaptx4869))
+- 升级 npm 包版本至最新(author by [haoxianrui](https://github.com/haoxianrui))
+
+# 2.9.2 (2024/03/05)
+## ✨ feat
+- vscode开发扩展推荐(author by [cshaptx4869](https://github.com/cshaptx4869))
+- 完善基础增删改查Mock接口(author by [haoxianrui](https://github.com/haoxianrui))
+
+## ♻️ refactor
+- 修改login密码框功能实现(author by [cshaptx4869](https://github.com/cshaptx4869))
+- 弱化页面进入动画效果(author by [cshaptx4869](https://github.com/cshaptx4869))
+- 取消推荐TypeScript Vue Plugin (author by [cshaptx4869](https://github.com/cshaptx4869))
+- 网站加载动画替换 (author by [haoxianrui](https://github.com/haoxianrui))
+- 优化主题和主题色监听,避免多个页面重复初始化 (author by [haoxianrui](https://github.com/haoxianrui))
+
+## 🐛 fix
+- AppMain 高度在非固定头部不正确导致出现滚动条问题修复 (author by [haoxianrui](https://github.com/haoxianrui))
+- 修复混合模式开启固定Head时的样式问题 (author by [cshaptx4869](https://github.com/cshaptx4869))
+- 设置面板统一字体大小 (author by [cshaptx4869](https://github.com/cshaptx4869))
+
+## 📦️build
+- 通过env配置控制mock服务 (author by [cshaptx4869](https://github.com/cshaptx4869))
+- 升级依赖包至最新版本 (author by [haoxianrui](https://github.com/haoxianrui))
+- 定义vite全局常量替换项目标题和版本 (author by [cshaptx4869](https://github.com/cshaptx4869))
+
+# 2.9.1 (2024/02/28)
+## ♻️ refactor
+- 项目配置按钮移入navbar(author by [cshaptx4869](https://github.com/cshaptx4869))
+- 优化user数据定义(author by [cshaptx4869](https://github.com/cshaptx4869))
+- 统一设置栏的 SVG 图标风格
+
+## 🐛 fix
+- 规整一些开发依赖(author by [cshaptx4869](https://github.com/cshaptx4869))
+- 修复登录页主题切换问题 (author by [cshaptx4869](https://github.com/cshaptx4869))
+
+## 🚀 pref
+
+- 压缩图片资源 (author by [cshaptx4869](https://github.com/cshaptx4869))
+
+
+# 2.9.0 (2024/02/25)
+
+## ✨ feat
+- 引入 animate.css 动画库
+- 新增水印和配置
+- 动态路由菜单支持 element plus 的图标
+
+## ♻️ refactor
+- Layout 布局重构和相关问题修复
+- sass 使用 @use 替代 @import 引入外部文件指令
+
+## 🐛 fix
+- 修复管理页面部分弹窗无法打开问题
+- 主题颜色设置按钮 hover 等未变化问题修复
+
+
+# 2.8.1 (2024/01/10)
+
+## ✨ feat
+- 替换 Mock 解决方案 vite-plugin-mock 为 vite-plugin-mock-dev-server 适配 Vite5
+
+# 2.8.0 (2023/12/27)
+
+## ⬆️ chore
+- 升级 Vite4 至 Vite5
+
+# 2.7.1 (2023/12/12)
+
+## ♻️ refactor
+- 将打包后的文件进行分类 (author by [ityangzhiwen](https://gitee.com/ityangzhiwen))
+
+# 2.7.0 (2023/11/19)
+
+## ♻️ refactor
+- 代码重构优化
+- 修改自动导入组件类型声明文件路径
+- 完善 typescript 类型
+
+## 🐛 fix
+- 修复管理页面部分弹窗无法打开问题
+
+
+# 2.7.0 (2023/11/19)
+
+## ♻️ refactor
+- 代码重构
+- 修改自动导入组件类型声明文件路径
+- 完善 typescript 类型
+
+## 🐛 fix
+- 修复管理页面部分弹窗无法打开问题
+
+
+# 2.6.3 (2023/10/22)
+
+## ✨ feat
+- 菜单管理新增目录只有一级子路由是否始终显示(alwaysShow)和路由页面是否缓存(keepAlive)的配置
+- 接口文档新增 swagger、knife4j
+- 引入和支持 tsx
+
+## ♻️ refactor
+- 代码瘦身,整理并删除未使用的 svg
+- 控制台样式优化
+
+## 🐛 fix
+- 菜单栏折叠和展开的图标暗黑模式显示问题修复
+
+
+# 2.6.2 (2023/10/11)
+
+## 🐛 fix
+- 主题设置未持久化问题
+- UnoCSS 插件无智能提示
+
+## ♻️ refactor
+- WebSocket 演示样式和代码优化
+- 用户管理代码重构
+
+# 2.6.1 (2023/9/4)
+
+## 🐛 fix
+- 导航顶部模式、混合模式样式在固定 Header 出现的样式问题修复
+- 固定 Header 没有持久化问题修复
+- 字典回显兼容 String 和 Number 类型
+
+# 2.6.0 (2023/8/24)💥💥💥
+
+## ✨ feat
+- 导航顶部模式、混合模式支持(author by [april-tong](https://april-tong.com/))
+- 平台文档(内嵌)(author by [april-tong](https://april-tong.com/))
+
+# 2.5.0 (2023/8/8)
+
+## ✨ feat
+- 新增 Mock(author by [ygcaicn](https://github.com/ygcaicn))
+- 图标 DEMO(author by [ygcaicn](https://github.com/ygcaicn))
+
+## 🐛 fix
+- 字典支持 Number 类型
+
+# 2.4.1 (2023/7/20)
+
+## ✨ feat
+- 整合 vite-plugin-compression 插件打包优化(3.66MB → 1.58MB) (author by [april-tong](https://april-tong.com/))
+- 字典组件封装(author by [haoxr](https://juejin.cn/user/4187394044331261/posts))
+
+## 🐛 fix
+- 分页组件hidden无效
+- 签名无法保存至后端
+- Git 提交 stylelint 校验部分机器报错
+
+# 2.4.0 (2023/6/17)
+
+## ✨ feat
+- 新增组件标签输入框(author by [april-tong](https://april-tong.com/))
+- 新增组件签名(author by [april-tong](https://april-tong.com/))
+- 新增组件表格(author by [april-tong](https://april-tong.com/))
+- Echarts 图表添加下载功能 author by [april-tong](https://april-tong.com/))
+
+## ♻️ refactor
+- 限制包管理器为 pnpm 和 node 版本16+
+- 自定义组件自动导入配置
+- 搜索框样式写法优化
+
+## 🐛 fix
+- 用户导入的部门回显成数字问题修复
+
+## ⬆️ chore
+- element-plus 版本升级 2.3.5 → 2.3.6
+
+# 2.3.1 (2023/5/21)
+
+## 🔄 refactor
+- 组件示例文件名称优化
+
+# 2.2.2 (2023/5/11)
+
+## ✨ feat
+- 组件封装示例添加源码地址
+- 角色、菜单、部门、字段按钮添加权限控制
+
+
+# 2.3.0 (2023/5/12)
+
+## ⬆️ chore
+- vue 版本升级 3.2.45 → 3.3.1 ([CHANGELOG](https://github.com/vuejs/core/blob/main/CHANGELOG.md))
+- vite 版本升级 4.3.1 → 4.3.5
+
+## ♻️ refactor
+- 使用 vue 3.3 版本新特性 `defineOptions` 在 `setup` 定义组件名称,移除重复的 `script` 标签
+
+# 2.2.2 (2023/5/11)
+
+## ✨ feat
+- 用户新增提交添加 `vueUse` 的 `useDebounceFn` 函数实现按钮防抖节流
+
+
+# 2.2.1 (2023/4/25)
+
+## 🐛 fix
+- 图标选择器组件使用 `onClickOutside` 未排除下拉弹出框元素导致无法输入搜索。
+
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..9825cba
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2021-present 有来开源组织
+
+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/README.md b/README.md
new file mode 100644
index 0000000..f5342b7
--- /dev/null
+++ b/README.md
@@ -0,0 +1,11 @@
+# Vue 3 + Typescript + Vite
+
+This template should help get you started developing with Vue 3 and Typescript in Vite. The template uses Vue 3 `
+
+
+=======
+
+
+
+
+
+
+ Vite App
+
+
+
+
+
+>>>>>>> 232db255 ('首次提交')
+
diff --git a/logo.png b/logo.png
new file mode 100644
index 0000000..be095c0
Binary files /dev/null and b/logo.png differ
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..438eb2a
--- /dev/null
+++ b/package.json
@@ -0,0 +1,151 @@
+{
+<<<<<<< HEAD
+ "name": "jurs-zun",
+ "description": "Vue3 + Vite + TypeScript + Element-Plus 的后台管理模板,vue-element-admin 的 Vue3 版本",
+ "version": "3.4.2",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vue-tsc --noEmit & vite build",
+ "preview": "vite preview",
+ "build-only": "vite build",
+ "type-check": "vue-tsc --noEmit",
+ "lint:eslint": "eslint --cache \"src/**/*.{vue,ts,js}\" --fix",
+ "lint:prettier": "prettier --write \"**/*.{js,cjs,ts,json,css,scss,vue,html,md}\"",
+ "lint:stylelint": "stylelint --cache \"**/*.{css,scss,vue}\" --fix",
+ "lint:lint-staged": "lint-staged",
+ "lint": "npm run lint:eslint && npm run lint:prettier && npm run lint:stylelint",
+ "preinstall": "npx only-allow pnpm",
+ "prepare": "husky",
+ "commit": "git-cz"
+ },
+ "config": {
+ "commitizen": {
+ "path": "node_modules/cz-git"
+ }
+ },
+ "lint-staged": {
+ "*.{js,ts}": [
+ "eslint --fix",
+ "prettier --write"
+ ],
+ "*.{cjs,json}": [
+ "prettier --write"
+ ],
+ "*.{vue,html}": [
+ "eslint --fix",
+ "prettier --write",
+ "stylelint --fix"
+ ],
+ "*.{scss,css}": [
+ "stylelint --fix",
+ "prettier --write"
+ ],
+ "*.md": [
+ "prettier --write"
+ ]
+ },
+ "dependencies": {
+ "@element-plus/icons-vue": "^2.3.2",
+ "@stomp/stompjs": "^7.2.1",
+ "@vueuse/core": "^12.8.2",
+ "@wangeditor-next/editor": "^5.6.47",
+ "@wangeditor-next/editor-for-vue": "^5.1.14",
+ "animate.css": "^4.1.1",
+ "axios": "^1.13.2",
+ "codemirror": "^5.65.20",
+ "codemirror-editor-vue3": "^2.8.0",
+ "default-passive-events": "^2.0.0",
+ "echarts": "^6.0.0",
+ "element-plus": "^2.11.8",
+ "exceljs": "^4.4.0",
+ "lodash-es": "^4.17.21",
+ "nprogress": "^0.2.0",
+ "path-browserify": "^1.0.1",
+ "path-to-regexp": "^8.3.0",
+ "pinia": "^3.0.4",
+ "qs": "^6.14.0",
+ "sortablejs": "^1.15.6",
+ "vue": "^3.5.24",
+ "vue-draggable-plus": "^0.6.0",
+ "vue-i18n": "^11.1.12",
+ "vue-router": "^4.6.3",
+ "vxe-table": "~4.6.25"
+ },
+ "devDependencies": {
+ "@commitlint/cli": "^19.8.1",
+ "@commitlint/config-conventional": "^19.8.1",
+ "@eslint/js": "^9.39.1",
+ "@iconify/utils": "^2.3.0",
+ "@types/codemirror": "^5.60.17",
+ "@types/lodash-es": "^4.17.12",
+ "@types/node": "^24.10.1",
+ "@types/nprogress": "^0.2.3",
+ "@types/path-browserify": "^1.0.3",
+ "@types/qs": "^6.14.0",
+ "@types/sortablejs": "^1.15.9",
+ "@typescript-eslint/eslint-plugin": "^8.46.4",
+ "@typescript-eslint/parser": "^8.46.4",
+ "@vitejs/plugin-vue": "^6.0.1",
+ "autoprefixer": "^10.4.22",
+ "commitizen": "^4.3.1",
+ "cz-git": "^1.12.0",
+ "eslint": "^9.39.1",
+ "eslint-config-prettier": "^10.1.8",
+ "eslint-plugin-prettier": "^5.5.4",
+ "eslint-plugin-vue": "^10.5.1",
+ "globals": "^15.15.0",
+ "husky": "^9.1.7",
+ "lint-staged": "^15.5.2",
+ "postcss": "^8.5.6",
+ "postcss-html": "^1.8.0",
+ "postcss-scss": "^4.0.9",
+ "prettier": "^3.6.2",
+ "sass": "^1.94.0",
+ "stylelint": "^16.25.0",
+ "stylelint-config-html": "^1.1.0",
+ "stylelint-config-recess-order": "^6.1.0",
+ "stylelint-config-recommended": "^15.0.0",
+ "stylelint-config-recommended-scss": "^14.1.0",
+ "stylelint-config-recommended-vue": "^1.6.1",
+ "stylelint-prettier": "^5.0.3",
+ "terser": "^5.44.1",
+ "typescript": "^5.9.3",
+ "typescript-eslint": "^8.46.4",
+ "unocss": "^66.5.6",
+ "unplugin-auto-import": "^19.3.0",
+ "unplugin-vue-components": "^28.8.0",
+ "vite": "^7.2.2",
+ "vite-plugin-mock-dev-server": "^2.0.2",
+ "vue-eslint-parser": "^10.2.0",
+ "vue-tsc": "^2.2.12"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "repository": "https://gitee.com/youlaiorg/vue3-element-admin.git",
+ "author": "有来开源组织",
+ "license": "MIT"
+=======
+ "name": "vue3-element-admin",
+ "version": "0.0.0",
+ "scripts": {
+ "dev": "vite",
+ "build": "vue-tsc --noEmit && vite build",
+ "serve": "vite preview"
+ },
+ "dependencies": {
+ "element-plus": "^1.2.0-beta.3",
+ "vue": "^3.2.16",
+ "vue-router": "^4.0.12",
+ "vuex": "^4.0.2"
+ },
+ "devDependencies": {
+ "@vitejs/plugin-vue": "^1.9.3",
+ "typescript": "^4.4.3",
+ "vite": "^2.6.4",
+ "vue-tsc": "^0.3.0"
+ }
+>>>>>>> 232db255 ('首次提交')
+}
diff --git a/public/favicon.ico b/public/favicon.ico
new file mode 100644
index 0000000..df36fcf
Binary files /dev/null and b/public/favicon.ico differ
diff --git a/src/App.vue b/src/App.vue
new file mode 100644
index 0000000..9ce4a77
--- /dev/null
+++ b/src/App.vue
@@ -0,0 +1,41 @@
+<<<<<<< HEAD
+
+
+
+
+
+
+
+
+=======
+
+
+
+
+
+
+
+
+>>>>>>> 232db255 ('首次提交')
diff --git a/src/api/ai/index.ts b/src/api/ai/index.ts
new file mode 100644
index 0000000..faf208d
--- /dev/null
+++ b/src/api/ai/index.ts
@@ -0,0 +1,191 @@
+import request from "@/utils/request";
+
+/**
+ * AI 命令请求参数
+ */
+export interface AiCommandRequest {
+ /** 用户输入的自然语言命令 */
+ command: string;
+ /** 当前页面路由(用于上下文) */
+ currentRoute?: string;
+ /** 当前激活的组件名称 */
+ currentComponent?: string;
+ /** 额外上下文信息 */
+ context?: Record;
+}
+
+/**
+ * 函数调用参数
+ */
+export interface FunctionCall {
+ /** 函数名称 */
+ name: string;
+ /** 函数描述 */
+ description?: string;
+ /** 参数对象 */
+ arguments: Record;
+}
+
+/**
+ * AI 命令解析响应
+ */
+export interface AiCommandResponse {
+ /** 解析日志ID(用于关联执行记录) */
+ parseLogId?: string;
+ /** 是否成功解析 */
+ success: boolean;
+ /** 解析后的函数调用列表 */
+ functionCalls: FunctionCall[];
+ /** AI 的理解和说明 */
+ explanation?: string;
+ /** 置信度 (0-1) */
+ confidence?: number;
+ /** 错误信息 */
+ error?: string;
+ /** 原始 LLM 响应(用于调试) */
+ rawResponse?: string;
+}
+
+/**
+ * AI 命令执行请求
+ */
+export interface AiExecuteRequest {
+ /** 关联的解析日志ID */
+ parseLogId?: string;
+ /** 原始命令(用于审计) */
+ originalCommand?: string;
+ /** 要执行的函数调用 */
+ functionCall: FunctionCall;
+ /** 确认模式:auto=自动执行, manual=需要用户确认 */
+ confirmMode?: "auto" | "manual";
+ /** 用户确认标志 */
+ userConfirmed?: boolean;
+ /** 幂等性令牌(防止重复执行) */
+ idempotencyKey?: string;
+ /** 当前页面路由 */
+ currentRoute?: string;
+}
+
+/**
+ * AI 命令执行响应
+ */
+export interface AiExecuteResponse {
+ /** 是否执行成功 */
+ success: boolean;
+ /** 执行结果数据 */
+ data?: any;
+ /** 执行结果说明 */
+ message?: string;
+ /** 影响的记录数 */
+ affectedRows?: number;
+ /** 错误信息 */
+ error?: string;
+ /** 记录ID(用于追踪) */
+ recordId?: string;
+ /** 需要用户确认 */
+ requiresConfirmation?: boolean;
+ /** 确认提示信息 */
+ confirmationPrompt?: string;
+}
+
+export interface AiCommandRecordPageQuery extends PageQuery {
+ keywords?: string;
+ executeStatus?: string;
+ parseSuccess?: boolean;
+ userId?: number;
+ isDangerous?: boolean;
+ provider?: string;
+ model?: string;
+ functionName?: string;
+ createTime?: [string, string];
+}
+
+export interface AiCommandRecordVO {
+ id: string;
+ userId: number;
+ username: string;
+ originalCommand: string;
+ provider?: string;
+ model?: string;
+ parseSuccess?: boolean;
+ functionCalls?: string;
+ explanation?: string;
+ confidence?: number;
+ parseErrorMessage?: string;
+ inputTokens?: number;
+ outputTokens?: number;
+ totalTokens?: number;
+ parseTime?: number;
+ functionName?: string;
+ functionArguments?: string;
+ executeStatus?: string;
+ executeResult?: string;
+ executeErrorMessage?: string;
+ affectedRows?: number;
+ isDangerous?: boolean;
+ requiresConfirmation?: boolean;
+ userConfirmed?: boolean;
+ executionTime?: number;
+ ipAddress?: string;
+ userAgent?: string;
+ currentRoute?: string;
+ createTime?: string;
+ updateTime?: string;
+ remark?: string;
+}
+
+/**
+ * AI 命令 API
+ */
+class AiCommandApi {
+ /**
+ * 解析自然语言命令
+ *
+ * @param data 命令请求参数
+ * @returns 解析结果
+ */
+ static parseCommand(data: AiCommandRequest): Promise {
+ return request({
+ url: "/api/v1/ai/command/parse",
+ method: "post",
+ data,
+ });
+ }
+
+ /**
+ * 执行已解析的命令
+ *
+ * @param data 执行请求参数
+ * @returns 执行结果数据(成功时返回,失败时抛出异常)
+ */
+ static executeCommand(data: AiExecuteRequest): Promise {
+ return request({
+ url: "/api/v1/ai/command/execute",
+ method: "post",
+ data,
+ });
+ }
+
+ /**
+ * 获取命令记录分页列表
+ */
+ static getCommandRecordPage(queryParams: AiCommandRecordPageQuery) {
+ return request>({
+ url: "/api/v1/ai/command/records",
+ method: "get",
+ params: queryParams,
+ });
+ }
+
+ /**
+ * 撤销命令执行(如果支持)
+ */
+ static rollbackCommand(recordId: string) {
+ return request({
+ url: `/api/v1/ai/command/rollback/${recordId}`,
+ method: "post",
+ });
+ }
+}
+
+export default AiCommandApi;
diff --git a/src/api/auth-api.ts b/src/api/auth-api.ts
new file mode 100644
index 0000000..772e9ef
--- /dev/null
+++ b/src/api/auth-api.ts
@@ -0,0 +1,86 @@
+import request from "@/utils/request";
+
+const AUTH_BASE_URL = "/api/v1/auth";
+
+const AuthAPI = {
+ /** 登录接口*/
+ login(data: LoginFormData) {
+ const formData = new FormData();
+ formData.append("username", data.username);
+ formData.append("password", data.password);
+ formData.append("captchaKey", data.captchaKey);
+ formData.append("captchaCode", data.captchaCode);
+ return request({
+ url: `${AUTH_BASE_URL}/login`,
+ method: "post",
+ data: formData,
+ headers: {
+ "Content-Type": "multipart/form-data",
+ },
+ });
+ },
+
+ /** 刷新 token 接口*/
+ refreshToken(refreshToken: string) {
+ return request({
+ url: `${AUTH_BASE_URL}/refresh-token`,
+ method: "post",
+ params: { refreshToken },
+ headers: {
+ Authorization: "no-auth",
+ },
+ });
+ },
+
+ /** 退出登录接口 */
+ logout() {
+ return request({
+ url: `${AUTH_BASE_URL}/logout`,
+ method: "delete",
+ });
+ },
+
+ /** 获取验证码接口*/
+ getCaptcha() {
+ return request({
+ url: `${AUTH_BASE_URL}/captcha`,
+ method: "get",
+ });
+ },
+};
+
+export default AuthAPI;
+
+/** 登录表单数据 */
+export interface LoginFormData {
+ /** 用户名 */
+ username: string;
+ /** 密码 */
+ password: string;
+ /** 验证码缓存key */
+ captchaKey: string;
+ /** 验证码 */
+ captchaCode: string;
+ /** 记住我 */
+ rememberMe: boolean;
+}
+
+/** 登录响应 */
+export interface LoginResult {
+ /** 访问令牌 */
+ accessToken: string;
+ /** 刷新令牌 */
+ refreshToken: string;
+ /** 令牌类型 */
+ tokenType: string;
+ /** 过期时间(秒) */
+ expiresIn: number;
+}
+
+/** 验证码信息 */
+export interface CaptchaInfo {
+ /** 验证码缓存key */
+ captchaKey: string;
+ /** 验证码图片Base64字符串 */
+ captchaBase64: string;
+}
diff --git a/src/api/codegen-api.ts b/src/api/codegen-api.ts
new file mode 100644
index 0000000..e9863d3
--- /dev/null
+++ b/src/api/codegen-api.ts
@@ -0,0 +1,199 @@
+import request from "@/utils/request";
+
+const GENERATOR_BASE_URL = "/api/v1/codegen";
+
+const GeneratorAPI = {
+ /** 获取数据表分页列表 */
+ getTablePage(params: TablePageQuery) {
+ return request>({
+ url: `${GENERATOR_BASE_URL}/table/page`,
+ method: "get",
+ params,
+ });
+ },
+
+ /** 获取代码生成配置 */
+ getGenConfig(tableName: string) {
+ return request({
+ url: `${GENERATOR_BASE_URL}/${tableName}/config`,
+ method: "get",
+ });
+ },
+
+ /** 获取代码生成配置 */
+ saveGenConfig(tableName: string, data: GenConfigForm) {
+ return request({
+ url: `${GENERATOR_BASE_URL}/${tableName}/config`,
+ method: "post",
+ data,
+ });
+ },
+
+ /** 获取代码生成预览数据 */
+ getPreviewData(tableName: string, pageType?: "classic" | "curd") {
+ return request({
+ url: `${GENERATOR_BASE_URL}/${tableName}/preview`,
+ method: "get",
+ params: pageType ? { pageType } : undefined,
+ });
+ },
+
+ /** 重置代码生成配置 */
+ resetGenConfig(tableName: string) {
+ return request({
+ url: `${GENERATOR_BASE_URL}/${tableName}/config`,
+ method: "delete",
+ });
+ },
+
+ /**
+ * 下载 ZIP 文件
+ * @param url
+ * @param fileName
+ */
+ download(tableName: string, pageType?: "classic" | "curd") {
+ return request({
+ url: `${GENERATOR_BASE_URL}/${tableName}/download`,
+ method: "get",
+ params: pageType ? { pageType } : undefined,
+ responseType: "blob",
+ }).then((response) => {
+ const fileName = decodeURI(
+ response.headers["content-disposition"].split(";")[1].split("=")[1]
+ );
+
+ const blob = new Blob([response.data], { type: "application/zip" });
+ const a = document.createElement("a");
+ const url = window.URL.createObjectURL(blob);
+ a.href = url;
+ a.download = fileName;
+ a.click();
+ window.URL.revokeObjectURL(url);
+ });
+ },
+};
+
+export default GeneratorAPI;
+
+/** 代码生成预览对象 */
+export interface GeneratorPreviewVO {
+ /** 文件生成路径 */
+ path: string;
+ /** 文件名称 */
+ fileName: string;
+ /** 文件内容 */
+ content: string;
+}
+
+/** 数据表分页查询参数 */
+export interface TablePageQuery extends PageQuery {
+ /** 关键字(表名) */
+ keywords?: string;
+}
+
+/** 数据表分页对象 */
+export interface TablePageVO {
+ /** 表名称 */
+ tableName: string;
+
+ /** 表描述 */
+ tableComment: string;
+
+ /** 存储引擎 */
+ engine: string;
+
+ /** 字符集排序规则 */
+ tableCollation: string;
+
+ /** 创建时间 */
+ createTime: string;
+}
+
+/** 代码生成配置表单 */
+export interface GenConfigForm {
+ /** 主键 */
+ id?: string;
+
+ /** 表名 */
+ tableName?: string;
+
+ /** 业务名 */
+ businessName?: string;
+
+ /** 模块名 */
+ moduleName?: string;
+
+ /** 包名 */
+ packageName?: string;
+
+ /** 实体名 */
+ entityName?: string;
+
+ /** 作者 */
+ author?: string;
+
+ /** 上级菜单 */
+ parentMenuId?: string;
+
+ /** 后端应用名 */
+ backendAppName?: string;
+ /** 前端应用名 */
+ frontendAppName?: string;
+
+ /** 字段配置列表 */
+ fieldConfigs?: FieldConfig[];
+
+ /** 页面类型 classic|curd */
+ pageType?: "classic" | "curd";
+
+ /** 要移除的表前缀,如 sys_ */
+ removeTablePrefix?: string;
+}
+
+/** 字段配置 */
+export interface FieldConfig {
+ /** 主键 */
+ id?: string;
+
+ /** 列名 */
+ columnName?: string;
+
+ /** 列类型 */
+ columnType?: string;
+
+ /** 字段名 */
+ fieldName?: string;
+
+ /** 字段类型 */
+ fieldType?: string;
+
+ /** 字段描述 */
+ fieldComment?: string;
+
+ /** 是否在列表显示 */
+ isShowInList?: number;
+
+ /** 是否在表单显示 */
+ isShowInForm?: number;
+
+ /** 是否在查询条件显示 */
+ isShowInQuery?: number;
+
+ /** 是否必填 */
+ isRequired?: number;
+
+ /** 表单类型 */
+ formType?: number;
+
+ /** 查询类型 */
+ queryType?: number;
+
+ /** 字段长度 */
+ maxLength?: number;
+
+ /** 字段排序 */
+ fieldSort?: number;
+
+ /** 字典类型 */
+ dictType?: string;
+}
diff --git a/src/api/file-api.ts b/src/api/file-api.ts
new file mode 100644
index 0000000..b899c44
--- /dev/null
+++ b/src/api/file-api.ts
@@ -0,0 +1,64 @@
+import request from "@/utils/request";
+
+const FileAPI = {
+ /** 上传文件 (传入 FormData,上传进度回调) */
+ upload(formData: FormData, onProgress?: (percent: number) => void) {
+ return request({
+ url: "/api/v1/files",
+ method: "post",
+ data: formData,
+ headers: { "Content-Type": "multipart/form-data" },
+ onUploadProgress: (progressEvent) => {
+ if (progressEvent.total) {
+ const percent = Math.round((progressEvent.loaded * 100) / progressEvent.total);
+ onProgress?.(percent);
+ }
+ },
+ });
+ },
+
+ /** 上传文件(传入 File) */
+ uploadFile(file: File) {
+ const formData = new FormData();
+ formData.append("file", file);
+ return request({
+ url: "/api/v1/files",
+ method: "post",
+ data: formData,
+ headers: { "Content-Type": "multipart/form-data" },
+ });
+ },
+
+ /** 删除文件 */
+ delete(filePath?: string) {
+ return request({
+ url: "/api/v1/files",
+ method: "delete",
+ params: { filePath },
+ });
+ },
+
+ /** 下载文件 */
+ download(url: string, fileName?: string) {
+ return request({
+ url,
+ method: "get",
+ responseType: "blob",
+ }).then((res) => {
+ const blob = new Blob([res.data]);
+ const a = document.createElement("a");
+ const urlObject = window.URL.createObjectURL(blob);
+ a.href = urlObject;
+ a.download = fileName || "下载文件";
+ a.click();
+ window.URL.revokeObjectURL(urlObject);
+ });
+ },
+};
+
+export default FileAPI;
+
+export interface FileInfo {
+ name: string;
+ url: string;
+}
diff --git a/src/api/system/config-api.ts b/src/api/system/config-api.ts
new file mode 100644
index 0000000..3943769
--- /dev/null
+++ b/src/api/system/config-api.ts
@@ -0,0 +1,70 @@
+import request from "@/utils/request";
+
+const CONFIG_BASE_URL = "/api/v1/config";
+
+const ConfigAPI = {
+ /** 获取配置分页数据 */
+ getPage(queryParams?: ConfigPageQuery) {
+ return request>({
+ url: `${CONFIG_BASE_URL}/page`,
+ method: "get",
+ params: queryParams,
+ });
+ },
+ /** 获取配置表单数据 */
+ getFormData(id: string) {
+ return request({
+ url: `${CONFIG_BASE_URL}/${id}/form`,
+ method: "get",
+ });
+ },
+ /** 新增配置 */
+ create(data: ConfigForm) {
+ return request({ url: `${CONFIG_BASE_URL}`, method: "post", data });
+ },
+ /** 修改配置 */
+ update(id: string, data: ConfigForm) {
+ return request({ url: `${CONFIG_BASE_URL}/${id}`, method: "put", data });
+ },
+ /** 删除配置 */
+ deleteById(id: string) {
+ return request({ url: `${CONFIG_BASE_URL}/${id}`, method: "delete" });
+ },
+ /** 刷新配置缓存 */
+ refreshCache() {
+ return request({ url: `${CONFIG_BASE_URL}/refresh`, method: "PUT" });
+ },
+};
+
+export default ConfigAPI;
+
+export interface ConfigPageQuery extends PageQuery {
+ /** 搜索关键字 */
+ keywords?: string;
+}
+
+export interface ConfigForm {
+ /** 主键 */
+ id?: string;
+ /** 配置名称 */
+ configName?: string;
+ /** 配置键 */
+ configKey?: string;
+ /** 配置值 */
+ configValue?: string;
+ /** 描述、备注 */
+ remark?: string;
+}
+
+export interface ConfigPageVO {
+ /** 主键 */
+ id?: string;
+ /** 配置名称 */
+ configName?: string;
+ /** 配置键 */
+ configKey?: string;
+ /** 配置值 */
+ configValue?: string;
+ /** 描述、备注 */
+ remark?: string;
+}
diff --git a/src/api/system/dept-api.ts b/src/api/system/dept-api.ts
new file mode 100644
index 0000000..fcd6608
--- /dev/null
+++ b/src/api/system/dept-api.ts
@@ -0,0 +1,75 @@
+import request from "@/utils/request";
+
+const DEPT_BASE_URL = "/api/v1/dept";
+
+const DeptAPI = {
+ /** 获取部门树形列表 */
+ getList(queryParams?: DeptQuery) {
+ return request({ url: `${DEPT_BASE_URL}`, method: "get", params: queryParams });
+ },
+ /** 获取部门下拉数据源 */
+ getOptions() {
+ return request({ url: `${DEPT_BASE_URL}/options`, method: "get" });
+ },
+ /** 获取部门表单数据 */
+ getFormData(id: string) {
+ return request({ url: `${DEPT_BASE_URL}/${id}/form`, method: "get" });
+ },
+ /** 新增部门 */
+ create(data: DeptForm) {
+ return request({ url: `${DEPT_BASE_URL}`, method: "post", data });
+ },
+ /** 修改部门 */
+ update(id: string, data: DeptForm) {
+ return request({ url: `${DEPT_BASE_URL}/${id}`, method: "put", data });
+ },
+ /** 批量删除部门,多个以英文逗号(,)分割 */
+ deleteByIds(ids: string) {
+ return request({ url: `${DEPT_BASE_URL}/${ids}`, method: "delete" });
+ },
+};
+
+export default DeptAPI;
+
+export interface DeptQuery {
+ /** 搜索关键字 */
+ keywords?: string;
+ /** 状态 */
+ status?: number;
+}
+
+export interface DeptVO {
+ /** 子部门 */
+ children?: DeptVO[];
+ /** 创建时间 */
+ createTime?: Date;
+ /** 部门ID */
+ id?: string;
+ /** 部门名称 */
+ name?: string;
+ /** 部门编号 */
+ code?: string;
+ /** 父部门ID */
+ parentid?: string;
+ /** 排序 */
+ sort?: number;
+ /** 状态(1:启用;0:禁用) */
+ status?: number;
+ /** 修改时间 */
+ updateTime?: Date;
+}
+
+export interface DeptForm {
+ /** 部门ID(新增不填) */
+ id?: string;
+ /** 部门名称 */
+ name?: string;
+ /** 部门编号 */
+ code?: string;
+ /** 父部门ID */
+ parentId: string;
+ /** 排序 */
+ sort?: number;
+ /** 状态(1:启用;0:禁用) */
+ status?: number;
+}
diff --git a/src/api/system/dict-api.ts b/src/api/system/dict-api.ts
new file mode 100644
index 0000000..3976b8d
--- /dev/null
+++ b/src/api/system/dict-api.ts
@@ -0,0 +1,145 @@
+import request from "@/utils/request";
+
+const DICT_BASE_URL = "/api/v1/dicts";
+
+const DictAPI = {
+ /** 字典分页列表 */
+ getPage(queryParams: DictPageQuery) {
+ return request>({
+ url: `${DICT_BASE_URL}/page`,
+ method: "get",
+ params: queryParams,
+ });
+ },
+ /** 字典列表 */
+ getList() {
+ return request({ url: `${DICT_BASE_URL}`, method: "get" });
+ },
+ /** 字典表单数据 */
+ getFormData(id: string) {
+ return request({ url: `${DICT_BASE_URL}/${id}/form`, method: "get" });
+ },
+ /** 新增字典 */
+ create(data: DictForm) {
+ return request({ url: `${DICT_BASE_URL}`, method: "post", data });
+ },
+ /** 修改字典 */
+ update(id: string, data: DictForm) {
+ return request({ url: `${DICT_BASE_URL}/${id}`, method: "put", data });
+ },
+ /** 删除字典 */
+ deleteByIds(ids: string) {
+ return request({ url: `${DICT_BASE_URL}/${ids}`, method: "delete" });
+ },
+
+ /** 获取字典项分页列表 */
+ getDictItemPage(dictCode: string, queryParams: DictItemPageQuery) {
+ return request>({
+ url: `${DICT_BASE_URL}/${dictCode}/items/page`,
+ method: "get",
+ params: queryParams,
+ });
+ },
+ /** 获取字典项列表 */
+ getDictItems(dictCode: string) {
+ return request({
+ url: `${DICT_BASE_URL}/${dictCode}/items`,
+ method: "get",
+ });
+ },
+ /** 新增字典项 */
+ createDictItem(dictCode: string, data: DictItemForm) {
+ return request({ url: `${DICT_BASE_URL}/${dictCode}/items`, method: "post", data });
+ },
+ /** 获取字典项表单数据 */
+ getDictItemFormData(dictCode: string, id: string) {
+ return request({
+ url: `${DICT_BASE_URL}/${dictCode}/items/${id}/form`,
+ method: "get",
+ });
+ },
+ /** 修改字典项 */
+ updateDictItem(dictCode: string, id: string, data: DictItemForm) {
+ return request({ url: `${DICT_BASE_URL}/${dictCode}/items/${id}`, method: "put", data });
+ },
+ /** 删除字典项 */
+ deleteDictItems(dictCode: string, ids: string) {
+ return request({ url: `${DICT_BASE_URL}/${dictCode}/items/${ids}`, method: "delete" });
+ },
+};
+
+export default DictAPI;
+
+export interface DictPageQuery extends PageQuery {
+ /** 搜索关键字 */
+ keywords?: string;
+ /** 状态(1:启用;0:禁用) */
+ status?: number;
+}
+export interface DictPageVO {
+ /** 字典ID */
+ id: string;
+ /** 字典名称 */
+ name: string;
+ /** 字典编码 */
+ dictCode: string;
+ /** 状态(1:启用;0:禁用) */
+ status: number;
+}
+export interface DictForm {
+ /** 字典ID(新增不填) */
+ id?: string;
+ /** 字典名称 */
+ name?: string;
+ /** 字典编码 */
+ dictCode?: string;
+ /** 状态(1:启用;0:禁用) */
+ status?: number;
+ /** 备注 */
+ remark?: string;
+}
+export interface DictItemPageQuery extends PageQuery {
+ /** 搜索关键字 */
+ keywords?: string;
+ /** 字典编码 */
+ dictCode?: string;
+}
+export interface DictItemPageVO {
+ /** 字典项ID */
+ id: string;
+ /** 字典编码 */
+ dictCode: string;
+ /** 字典项值 */
+ value: string;
+ /** 字典项标签 */
+ label: string;
+ /** 状态(1:启用;0:禁用) */
+ status: number;
+ /** 排序 */
+ sort?: number;
+}
+export interface DictItemForm {
+ /** 字典项ID(新增不填) */
+ id?: string;
+ /** 字典编码 */
+ dictCode?: string;
+ /** 字典项值 */
+ value?: string;
+ /** 字典项标签 */
+ label?: string;
+ /** 状态(1:启用;0:禁用) */
+ status?: number;
+ /** 排序 */
+ sort?: number;
+ /** 标签类型 */
+ tagType?: "success" | "warning" | "info" | "primary" | "danger" | "";
+}
+export interface DictItemOption {
+ /** 值 */
+ value: number | string;
+ /** 标签 */
+ label: string;
+ /** 标签类型 */
+ tagType?: "" | "success" | "info" | "warning" | "danger";
+ [key: string]: any;
+}
diff --git a/src/api/system/log-api.ts b/src/api/system/log-api.ts
new file mode 100644
index 0000000..3e98698
--- /dev/null
+++ b/src/api/system/log-api.ts
@@ -0,0 +1,89 @@
+import request from "@/utils/request";
+
+const LOG_BASE_URL = "/api/v1/logs";
+
+const LogAPI = {
+ /** 获取日志分页列表 */
+ getPage(queryParams: LogPageQuery) {
+ return request>({
+ url: `${LOG_BASE_URL}/page`,
+ method: "get",
+ params: queryParams,
+ });
+ },
+ /** 获取访问趋势 */
+ getVisitTrend(queryParams: VisitTrendQuery) {
+ return request({
+ url: `${LOG_BASE_URL}/visit-trend`,
+ method: "get",
+ params: queryParams,
+ });
+ },
+ /** 获取访问统计 */
+ getVisitStats() {
+ return request({ url: `${LOG_BASE_URL}/visit-stats`, method: "get" });
+ },
+};
+
+export default LogAPI;
+
+export interface LogPageQuery extends PageQuery {
+ /** 搜索关键字 */
+ keywords?: string;
+ /** 操作时间 */
+ createTime?: [string, string];
+}
+export interface LogPageVO {
+ /** 主键 */
+ id: string;
+ /** 日志模块 */
+ module: string;
+ /** 日志内容 */
+ content: string;
+ /** 请求路径 */
+ requestUri: string;
+ /** 请求方法 */
+ method: string;
+ /** IP 地址 */
+ ip: string;
+ /** 地区 */
+ region: string;
+ /** 浏览器 */
+ browser: string;
+ /** 终端系统 */
+ os: string;
+ /** 执行时间(毫秒) */
+ executionTime: number;
+ /** 操作人 */
+ operator: string;
+}
+export interface VisitTrendVO {
+ /** 日期列表 */
+ dates: string[];
+ /** 浏览量(PV) */
+ pvList: number[];
+ /** 访客数(UV) */
+ uvList: number[];
+ /** IP数 */
+ ipList: number[];
+}
+export interface VisitTrendQuery {
+ /** 开始日期 */
+ startDate: string;
+ /** 结束日期 */
+ endDate: string;
+}
+export interface VisitStatsVO {
+ /** 今日访客数(UV) */
+ todayUvCount: number;
+ /** 总访客数 */
+ totalUvCount: number;
+ /** 访客数同比增长率(相对于昨天同一时间段的增长率) */
+ uvGrowthRate: number;
+ /** 今日浏览量(PV) */
+ todayPvCount: number;
+ /** 总浏览量 */
+ totalPvCount: number;
+ /** 同比增长率(相对于昨天同一时间段的增长率) */
+ pvGrowthRate: number;
+}
diff --git a/src/api/system/menu-api.ts b/src/api/system/menu-api.ts
new file mode 100644
index 0000000..a568a96
--- /dev/null
+++ b/src/api/system/menu-api.ts
@@ -0,0 +1,135 @@
+import request from "@/utils/request";
+const MENU_BASE_URL = "/api/v1/menus";
+
+const MenuAPI = {
+ /** 获取当前用户的路由列表 */
+ getRoutes() {
+ return request({ url: `${MENU_BASE_URL}/routes`, method: "get" });
+ },
+ /** 获取菜单树形列表 */
+ getList(queryParams: MenuQuery) {
+ return request({ url: `${MENU_BASE_URL}`, method: "get", params: queryParams });
+ },
+ /** 获取菜单下拉数据源 */
+ getOptions(onlyParent?: boolean) {
+ return request({
+ url: `${MENU_BASE_URL}/options`,
+ method: "get",
+ params: { onlyParent },
+ });
+ },
+ /** 获取菜单表单数据 */
+ getFormData(id: string) {
+ return request({ url: `${MENU_BASE_URL}/${id}/form`, method: "get" });
+ },
+ /** 新增菜单 */
+ create(data: MenuForm) {
+ return request({ url: `${MENU_BASE_URL}`, method: "post", data });
+ },
+ /** 修改菜单 */
+ update(id: string, data: MenuForm) {
+ return request({ url: `${MENU_BASE_URL}/${id}`, method: "put", data });
+ },
+ /** 删除菜单 */
+ deleteById(id: string) {
+ return request({ url: `${MENU_BASE_URL}/${id}`, method: "delete" });
+ },
+};
+
+export default MenuAPI;
+
+export interface MenuQuery {
+ /** 搜索关键字 */
+ keywords?: string;
+}
+import type { MenuTypeEnum } from "@/enums/system/menu-enum";
+export interface MenuVO {
+ /** 子菜单 */
+ children?: MenuVO[];
+ /** 组件路径 */
+ component?: string;
+ /** ICON */
+ icon?: string;
+ /** 菜单ID */
+ id?: string;
+ /** 菜单名称 */
+ name?: string;
+ /** 父菜单ID */
+ parentId?: string;
+ /** 按钮权限标识 */
+ perm?: string;
+ /** 跳转路径 */
+ redirect?: string;
+ /** 路由名称 */
+ routeName?: string;
+ /** 路由相对路径 */
+ routePath?: string;
+ /** 菜单排序(数字越小排名越靠前) */
+ sort?: number;
+ /** 菜单类型 */
+ type?: MenuTypeEnum;
+ /** 是否可见(1:显示;0:隐藏) */
+ visible?: number;
+}
+export interface MenuForm {
+ /** 菜单ID */
+ id?: string;
+ /** 父菜单ID */
+ parentId?: string;
+ /** 菜单名称 */
+ name?: string;
+ /** 是否可见(1-是 0-否) */
+ visible: number;
+ /** ICON */
+ icon?: string;
+ /** 排序 */
+ sort?: number;
+ /** 路由名称 */
+ routeName?: string;
+ /** 路由路径 */
+ routePath?: string;
+ /** 组件路径 */
+ component?: string;
+ /** 跳转路由路径 */
+ redirect?: string;
+ /** 菜单类型 */
+ type?: MenuTypeEnum;
+ /** 权限标识 */
+ perm?: string;
+ /** 【菜单】是否开启页面缓存 */
+ keepAlive?: number;
+ /** 【目录】只有一个子路由是否始终显示 */
+ alwaysShow?: number;
+ /** 其他参数 */
+ params?: KeyValue[];
+}
+interface KeyValue {
+ key: string;
+ value: string;
+}
+export interface RouteVO {
+ /** 子路由列表 */
+ children: RouteVO[];
+ /** 组件路径 */
+ component?: string;
+ /** 路由属性 */
+ meta?: Meta;
+ /** 路由名称 */
+ name?: string;
+ /** 路由路径 */
+ path?: string;
+ /** 跳转链接 */
+ redirect?: string;
+}
+export interface Meta {
+ /** 【目录】只有一个子路由是否始终显示 */
+ alwaysShow?: boolean;
+ /** 是否隐藏(true-是 false-否) */
+ hidden?: boolean;
+ /** ICON */
+ icon?: string;
+ /** 【菜单】是否开启页面缓存 */
+ keepAlive?: boolean;
+ /** 路由title */
+ title?: string;
+}
diff --git a/src/api/system/notice-api.ts b/src/api/system/notice-api.ts
new file mode 100644
index 0000000..fbac939
--- /dev/null
+++ b/src/api/system/notice-api.ts
@@ -0,0 +1,121 @@
+import request from "@/utils/request";
+
+const NOTICE_BASE_URL = "/api/v1/notices";
+
+const NoticeAPI = {
+ /** 获取通知公告分页数据 */
+ getPage(queryParams?: NoticePageQuery) {
+ return request>({
+ url: `${NOTICE_BASE_URL}/page`,
+ method: "get",
+ params: queryParams,
+ });
+ },
+ /** 获取通知公告表单数据 */
+ getFormData(id: string) {
+ return request({ url: `${NOTICE_BASE_URL}/${id}/form`, method: "get" });
+ },
+ /** 添加通知公告 */
+ create(data: NoticeForm) {
+ return request({ url: `${NOTICE_BASE_URL}`, method: "post", data });
+ },
+ /** 更新通知公告 */
+ update(id: string, data: NoticeForm) {
+ return request({ url: `${NOTICE_BASE_URL}/${id}`, method: "put", data });
+ },
+ /** 批量删除通知公告,多个以英文逗号(,)分割 */
+ deleteByIds(ids: string) {
+ return request({ url: `${NOTICE_BASE_URL}/${ids}`, method: "delete" });
+ },
+ /** 发布通知 */
+ publish(id: string) {
+ return request({ url: `${NOTICE_BASE_URL}/${id}/publish`, method: "put" });
+ },
+ /** 撤回通知 */
+ revoke(id: string) {
+ return request({ url: `${NOTICE_BASE_URL}/${id}/revoke`, method: "put" });
+ },
+ /** 查看通知 */
+ getDetail(id: string) {
+ return request({ url: `${NOTICE_BASE_URL}/${id}/detail`, method: "get" });
+ },
+ /** 全部已读 */
+ readAll() {
+ return request({ url: `${NOTICE_BASE_URL}/read-all`, method: "put" });
+ },
+ /** 获取我的通知分页列表 */
+ getMyNoticePage(queryParams?: NoticePageQuery) {
+ return request>({
+ url: `${NOTICE_BASE_URL}/my-page`,
+ method: "get",
+ params: queryParams,
+ });
+ },
+};
+
+export default NoticeAPI;
+
+export interface NoticePageQuery extends PageQuery {
+ /** 标题 */
+ title?: string;
+ /** 发布状态(0:草稿;1:已发布;2:已撤回) */
+ publishStatus?: number;
+ /** 是否已读(1:是;0:否) */
+ isRead?: number;
+}
+export interface NoticeForm {
+ /** 通知ID(新增不填) */
+ id?: string;
+ /** 标题 */
+ title?: string;
+ /** 内容 */
+ content?: string;
+ /** 类型 */
+ type?: number;
+ /** 优先级/级别 */
+ level?: string;
+ /** 目标类型 */
+ targetType?: number;
+ /** 目标用户ID(多个以英文逗号(,)分割) */
+ targetUserIds?: string;
+}
+export interface NoticePageVO {
+ /** 通知ID */
+ id: string;
+ /** 标题 */
+ title?: string;
+ /** 内容 */
+ content?: string;
+ /** 类型 */
+ type?: number;
+ /** 发布人ID */
+ publisherId?: bigint;
+ /** 优先级 */
+ priority?: number;
+ /** 目标类型 */
+ targetType?: number;
+ /** 发布状态 */
+ publishStatus?: number;
+ /** 发布时间 */
+ publishTime?: Date;
+ /** 撤回时间 */
+ revokeTime?: Date;
+}
+export interface NoticeDetailVO {
+ /** 通知ID */
+ id?: string;
+ /** 标题 */
+ title?: string;
+ /** 内容 */
+ content?: string;
+ /** 类型 */
+ type?: number;
+ /** 发布人名称 */
+ publisherName?: string;
+ /** 优先级/级别 */
+ level?: string;
+ /** 发布时间 */
+ publishTime?: Date;
+ /** 发布状态 */
+ publishStatus?: number;
+}
diff --git a/src/api/system/role-api.ts b/src/api/system/role-api.ts
new file mode 100644
index 0000000..bf713b9
--- /dev/null
+++ b/src/api/system/role-api.ts
@@ -0,0 +1,79 @@
+import request from "@/utils/request";
+
+const ROLE_BASE_URL = "/api/v1/roles";
+
+const RoleAPI = {
+ /** 获取角色分页数据 */
+ getPage(queryParams?: RolePageQuery) {
+ return request>({
+ url: `${ROLE_BASE_URL}/page`,
+ method: "get",
+ params: queryParams,
+ });
+ },
+ /** 获取角色下拉数据源 */
+ getOptions() {
+ return request({ url: `${ROLE_BASE_URL}/options`, method: "get" });
+ },
+ /** 获取角色的菜单ID集合 */
+ getRoleMenuIds(roleId: string) {
+ return request({ url: `${ROLE_BASE_URL}/${roleId}/menuIds`, method: "get" });
+ },
+ /** 分配菜单权限 */
+ updateRoleMenus(roleId: string, data: number[]) {
+ return request({ url: `${ROLE_BASE_URL}/${roleId}/menus`, method: "put", data });
+ },
+ /** 获取角色表单数据 */
+ getFormData(id: string) {
+ return request({ url: `${ROLE_BASE_URL}/${id}/form`, method: "get" });
+ },
+ /** 新增角色 */
+ create(data: RoleForm) {
+ return request({ url: `${ROLE_BASE_URL}`, method: "post", data });
+ },
+ /** 更新角色 */
+ update(id: string, data: RoleForm) {
+ return request({ url: `${ROLE_BASE_URL}/${id}`, method: "put", data });
+ },
+ /** 批量删除角色,多个以英文逗号(,)分割 */
+ deleteByIds(ids: string) {
+ return request({ url: `${ROLE_BASE_URL}/${ids}`, method: "delete" });
+ },
+};
+
+export default RoleAPI;
+
+export interface RolePageQuery extends PageQuery {
+ /** 搜索关键字 */
+ keywords?: string;
+}
+export interface RolePageVO {
+ /** 角色ID */
+ id?: string;
+ /** 角色编码 */
+ code?: string;
+ /** 角色名称 */
+ name?: string;
+ /** 排序 */
+ sort?: number;
+ /** 角色状态 */
+ status?: number;
+ /** 创建时间 */
+ createTime?: Date;
+ /** 修改时间 */
+ updateTime?: Date;
+}
+export interface RoleForm {
+ /** 角色ID */
+ id?: string;
+ /** 角色编码 */
+ code?: string;
+ /** 数据权限 */
+ dataScope?: number;
+ /** 角色名称 */
+ name?: string;
+ /** 排序 */
+ sort?: number;
+ /** 角色状态(1-正常;0-停用) */
+ status?: number;
+}
diff --git a/src/api/system/user-api.ts b/src/api/system/user-api.ts
new file mode 100644
index 0000000..2a26b34
--- /dev/null
+++ b/src/api/system/user-api.ts
@@ -0,0 +1,384 @@
+import request from "@/utils/request";
+
+const USER_BASE_URL = "/api/v1/users";
+
+const UserAPI = {
+ /**
+ * 获取当前登录用户信息
+ *
+ * @returns 登录用户昵称、头像信息,包括角色和权限
+ */
+ getInfo() {
+ return request({
+ url: `${USER_BASE_URL}/me`,
+ method: "get",
+ });
+ },
+
+ /**
+ * 获取用户分页列表
+ *
+ * @param queryParams 查询参数
+ */
+ getPage(queryParams: UserPageQuery) {
+ return request>({
+ url: `${USER_BASE_URL}/page`,
+ method: "get",
+ params: queryParams,
+ });
+ },
+
+ /**
+ * 获取用户表单详情
+ *
+ * @param userId 用户ID
+ * @returns 用户表单详情
+ */
+ getFormData(userId: string) {
+ return request({
+ url: `${USER_BASE_URL}/${userId}/form`,
+ method: "get",
+ });
+ },
+
+ /**
+ * 添加用户
+ *
+ * @param data 用户表单数据
+ */
+ create(data: UserForm) {
+ return request({
+ url: `${USER_BASE_URL}`,
+ method: "post",
+ data,
+ });
+ },
+
+ /**
+ * 修改用户
+ *
+ * @param id 用户ID
+ * @param data 用户表单数据
+ */
+ update(id: string, data: UserForm) {
+ return request({
+ url: `${USER_BASE_URL}/${id}`,
+ method: "put",
+ data,
+ });
+ },
+
+ /**
+ * 修改用户密码
+ *
+ * @param id 用户ID
+ * @param password 新密码
+ */
+ resetPassword(id: string, password: string) {
+ return request({
+ url: `${USER_BASE_URL}/${id}/password/reset`,
+ method: "put",
+ params: { password },
+ });
+ },
+
+ /**
+ * 批量删除用户,多个以英文逗号(,)分割
+ *
+ * @param ids 用户ID字符串,多个以英文逗号(,)分割
+ */
+ deleteByIds(ids: string) {
+ return request({
+ url: `${USER_BASE_URL}/${ids}`,
+ method: "delete",
+ });
+ },
+
+ /** 下载用户导入模板 */
+ downloadTemplate() {
+ return request({
+ url: `${USER_BASE_URL}/template`,
+ method: "get",
+ responseType: "blob",
+ });
+ },
+
+ /**
+ * 导出用户
+ *
+ * @param queryParams 查询参数
+ */
+ export(queryParams: UserPageQuery) {
+ return request({
+ url: `${USER_BASE_URL}/export`,
+ method: "get",
+ params: queryParams,
+ responseType: "blob",
+ });
+ },
+
+ /**
+ * 导入用户
+ *
+ * @param deptId 部门ID
+ * @param file 导入文件
+ */
+ import(deptId: string, file: File) {
+ const formData = new FormData();
+ formData.append("file", file);
+ return request({
+ url: `${USER_BASE_URL}/import`,
+ method: "post",
+ params: { deptId },
+ data: formData,
+ headers: {
+ "Content-Type": "multipart/form-data",
+ },
+ });
+ },
+
+ /** 获取个人中心用户信息 */
+ getProfile() {
+ return request({
+ url: `${USER_BASE_URL}/profile`,
+ method: "get",
+ });
+ },
+
+ /** 修改个人中心用户信息 */
+ updateProfile(data: UserProfileForm) {
+ return request({
+ url: `${USER_BASE_URL}/profile`,
+ method: "put",
+ data,
+ });
+ },
+
+ /** 修改个人中心用户密码 */
+ changePassword(data: PasswordChangeForm) {
+ return request({
+ url: `${USER_BASE_URL}/password`,
+ method: "put",
+ data,
+ });
+ },
+
+ /** 发送短信验证码(绑定或更换手机号)*/
+ sendMobileCode(mobile: string) {
+ return request({
+ url: `${USER_BASE_URL}/mobile/code`,
+ method: "post",
+ params: { mobile },
+ });
+ },
+
+ /** 绑定或更换手机号 */
+ bindOrChangeMobile(data: MobileUpdateForm) {
+ return request({
+ url: `${USER_BASE_URL}/mobile`,
+ method: "put",
+ data,
+ });
+ },
+
+ /** 发送邮箱验证码(绑定或更换邮箱)*/
+ sendEmailCode(email: string) {
+ return request({
+ url: `${USER_BASE_URL}/email/code`,
+ method: "post",
+ params: { email },
+ });
+ },
+
+ /** 绑定或更换邮箱 */
+ bindOrChangeEmail(data: EmailUpdateForm) {
+ return request({
+ url: `${USER_BASE_URL}/email`,
+ method: "put",
+ data,
+ });
+ },
+
+ /**
+ * 获取用户下拉列表
+ */
+ getOptions() {
+ return request({
+ url: `${USER_BASE_URL}/options`,
+ method: "get",
+ });
+ },
+};
+
+export default UserAPI;
+
+/** 登录用户信息 */
+export interface UserInfo {
+ /** 用户ID */
+ userId?: string;
+
+ /** 用户名 */
+ username?: string;
+
+ /** 昵称 */
+ nickname?: string;
+
+ /** 头像URL */
+ avatar?: string;
+
+ /** 角色 */
+ roles: string[];
+
+ /** 权限 */
+ perms: string[];
+}
+
+/**
+ * 用户分页查询对象
+ */
+export interface UserPageQuery extends PageQuery {
+ /** 搜索关键字 */
+ keywords?: string;
+
+ /** 用户状态 */
+ status?: number;
+
+ /** 部门ID */
+ deptId?: string;
+
+ /** 开始时间 */
+ createTime?: [string, string];
+}
+
+/** 用户分页对象 */
+export interface UserPageVO {
+ /** 用户ID */
+ id: string;
+ /** 用户头像URL */
+ avatar?: string;
+ /** 创建时间 */
+ createTime?: Date;
+ /** 部门名称 */
+ deptName?: string;
+ /** 用户邮箱 */
+ email?: string;
+ /** 性别 */
+ gender?: number;
+ /** 手机号 */
+ mobile?: string;
+ /** 用户昵称 */
+ nickname?: string;
+ /** 角色名称,多个使用英文逗号(,)分割 */
+ roleNames?: string;
+ /** 用户状态(1:启用;0:禁用) */
+ status?: number;
+ /** 用户名 */
+ username?: string;
+}
+
+/** 用户表单类型 */
+export interface UserForm {
+ /** 用户ID */
+ id?: string;
+ /** 用户头像 */
+ avatar?: string;
+ /** 部门ID */
+ deptId?: string;
+ /** 邮箱 */
+ email?: string;
+ /** 性别 */
+ gender?: number;
+ /** 手机号 */
+ mobile?: string;
+ /** 昵称 */
+ nickname?: string;
+ /** 角色ID集合 */
+ roleIds?: number[];
+ /** 用户状态(1:正常;0:禁用) */
+ status?: number;
+ /** 用户名 */
+ username?: string;
+}
+
+/** 个人中心用户信息 */
+export interface UserProfileVO {
+ /** 用户ID */
+ id?: string;
+
+ /** 用户名 */
+ username?: string;
+
+ /** 昵称 */
+ nickname?: string;
+
+ /** 头像URL */
+ avatar?: string;
+
+ /** 性别 */
+ gender?: number;
+
+ /** 手机号 */
+ mobile?: string;
+
+ /** 邮箱 */
+ email?: string;
+
+ /** 部门名称 */
+ deptName?: string;
+
+ /** 角色名称,多个使用英文逗号(,)分割 */
+ roleNames?: string;
+
+ /** 创建时间 */
+ createTime?: Date;
+}
+
+/** 个人中心用户信息表单 */
+export interface UserProfileForm {
+ /** 用户ID */
+ id?: string;
+
+ /** 用户名 */
+ username?: string;
+
+ /** 昵称 */
+ nickname?: string;
+
+ /** 头像URL */
+ avatar?: string;
+
+ /** 性别 */
+ gender?: number;
+
+ /** 手机号 */
+ mobile?: string;
+
+ /** 邮箱 */
+ email?: string;
+}
+
+/** 修改密码表单 */
+export interface PasswordChangeForm {
+ /** 原密码 */
+ oldPassword?: string;
+ /** 新密码 */
+ newPassword?: string;
+ /** 确认新密码 */
+ confirmPassword?: string;
+}
+
+/** 修改手机表单 */
+export interface MobileUpdateForm {
+ /** 手机号 */
+ mobile?: string;
+ /** 验证码 */
+ code?: string;
+}
+
+/** 修改邮箱表单 */
+export interface EmailUpdateForm {
+ /** 邮箱 */
+ email?: string;
+ /** 验证码 */
+ code?: string;
+}
diff --git a/src/assets/icons/ai.svg b/src/assets/icons/ai.svg
new file mode 100644
index 0000000..c3a1c1a
--- /dev/null
+++ b/src/assets/icons/ai.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/api.svg b/src/assets/icons/api.svg
new file mode 100644
index 0000000..0181bdd
--- /dev/null
+++ b/src/assets/icons/api.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/backtop.svg b/src/assets/icons/backtop.svg
new file mode 100644
index 0000000..f8e6aa0
--- /dev/null
+++ b/src/assets/icons/backtop.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/bell.svg b/src/assets/icons/bell.svg
new file mode 100644
index 0000000..262d0ac
--- /dev/null
+++ b/src/assets/icons/bell.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/bilibili.svg b/src/assets/icons/bilibili.svg
new file mode 100644
index 0000000..b86747c
--- /dev/null
+++ b/src/assets/icons/bilibili.svg
@@ -0,0 +1 @@
+
diff --git a/src/assets/icons/browser.svg b/src/assets/icons/browser.svg
new file mode 100644
index 0000000..15c3927
--- /dev/null
+++ b/src/assets/icons/browser.svg
@@ -0,0 +1 @@
+
diff --git a/src/assets/icons/captcha.svg b/src/assets/icons/captcha.svg
new file mode 100644
index 0000000..8b1da30
--- /dev/null
+++ b/src/assets/icons/captcha.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/cascader.svg b/src/assets/icons/cascader.svg
new file mode 100644
index 0000000..57209bf
--- /dev/null
+++ b/src/assets/icons/cascader.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/client.svg b/src/assets/icons/client.svg
new file mode 100644
index 0000000..7373b3d
--- /dev/null
+++ b/src/assets/icons/client.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/close.svg b/src/assets/icons/close.svg
new file mode 100644
index 0000000..e99c978
--- /dev/null
+++ b/src/assets/icons/close.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/close_all.svg b/src/assets/icons/close_all.svg
new file mode 100644
index 0000000..2005198
--- /dev/null
+++ b/src/assets/icons/close_all.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/close_left.svg b/src/assets/icons/close_left.svg
new file mode 100644
index 0000000..fc5cf71
--- /dev/null
+++ b/src/assets/icons/close_left.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/close_other.svg b/src/assets/icons/close_other.svg
new file mode 100644
index 0000000..27ffc32
--- /dev/null
+++ b/src/assets/icons/close_other.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/close_right.svg b/src/assets/icons/close_right.svg
new file mode 100644
index 0000000..b96dc1c
--- /dev/null
+++ b/src/assets/icons/close_right.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/cnblogs.svg b/src/assets/icons/cnblogs.svg
new file mode 100644
index 0000000..4920a4c
--- /dev/null
+++ b/src/assets/icons/cnblogs.svg
@@ -0,0 +1 @@
+
diff --git a/src/assets/icons/code.svg b/src/assets/icons/code.svg
new file mode 100644
index 0000000..d8b546c
--- /dev/null
+++ b/src/assets/icons/code.svg
@@ -0,0 +1 @@
+
diff --git a/src/assets/icons/collapse.svg b/src/assets/icons/collapse.svg
new file mode 100644
index 0000000..1507568
--- /dev/null
+++ b/src/assets/icons/collapse.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/csdn.svg b/src/assets/icons/csdn.svg
new file mode 100644
index 0000000..e16bad0
--- /dev/null
+++ b/src/assets/icons/csdn.svg
@@ -0,0 +1,6 @@
+
diff --git a/src/assets/icons/dict.svg b/src/assets/icons/dict.svg
new file mode 100644
index 0000000..db60220
--- /dev/null
+++ b/src/assets/icons/dict.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/document.svg b/src/assets/icons/document.svg
new file mode 100644
index 0000000..aaa0574
--- /dev/null
+++ b/src/assets/icons/document.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/down.svg b/src/assets/icons/down.svg
new file mode 100644
index 0000000..5fc8b88
--- /dev/null
+++ b/src/assets/icons/down.svg
@@ -0,0 +1 @@
+
diff --git a/src/assets/icons/download.svg b/src/assets/icons/download.svg
new file mode 100644
index 0000000..a8077dc
--- /dev/null
+++ b/src/assets/icons/download.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/enter.svg b/src/assets/icons/enter.svg
new file mode 100644
index 0000000..9e199df
--- /dev/null
+++ b/src/assets/icons/enter.svg
@@ -0,0 +1 @@
+
diff --git a/src/assets/icons/esc.svg b/src/assets/icons/esc.svg
new file mode 100644
index 0000000..2f85dd2
--- /dev/null
+++ b/src/assets/icons/esc.svg
@@ -0,0 +1 @@
+
diff --git a/src/assets/icons/file.svg b/src/assets/icons/file.svg
new file mode 100644
index 0000000..fac9bf0
--- /dev/null
+++ b/src/assets/icons/file.svg
@@ -0,0 +1 @@
+
diff --git a/src/assets/icons/fullscreen-exit.svg b/src/assets/icons/fullscreen-exit.svg
new file mode 100644
index 0000000..2452f2b
--- /dev/null
+++ b/src/assets/icons/fullscreen-exit.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/fullscreen.svg b/src/assets/icons/fullscreen.svg
new file mode 100644
index 0000000..4b6ee11
--- /dev/null
+++ b/src/assets/icons/fullscreen.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/gitcode.svg b/src/assets/icons/gitcode.svg
new file mode 100644
index 0000000..7a02760
--- /dev/null
+++ b/src/assets/icons/gitcode.svg
@@ -0,0 +1 @@
+
diff --git a/src/assets/icons/gitee.svg b/src/assets/icons/gitee.svg
new file mode 100644
index 0000000..c799c2f
--- /dev/null
+++ b/src/assets/icons/gitee.svg
@@ -0,0 +1 @@
+
diff --git a/src/assets/icons/github.svg b/src/assets/icons/github.svg
new file mode 100644
index 0000000..1adfa4e
--- /dev/null
+++ b/src/assets/icons/github.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/homepage.svg b/src/assets/icons/homepage.svg
new file mode 100644
index 0000000..1e1feab
--- /dev/null
+++ b/src/assets/icons/homepage.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/java.svg b/src/assets/icons/java.svg
new file mode 100644
index 0000000..eaa93db
--- /dev/null
+++ b/src/assets/icons/java.svg
@@ -0,0 +1 @@
+
diff --git a/src/assets/icons/juejin.svg b/src/assets/icons/juejin.svg
new file mode 100644
index 0000000..937ace3
--- /dev/null
+++ b/src/assets/icons/juejin.svg
@@ -0,0 +1 @@
+
diff --git a/src/assets/icons/language.svg b/src/assets/icons/language.svg
new file mode 100644
index 0000000..e754062
--- /dev/null
+++ b/src/assets/icons/language.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/menu.svg b/src/assets/icons/menu.svg
new file mode 100644
index 0000000..f5875d3
--- /dev/null
+++ b/src/assets/icons/menu.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/message.svg b/src/assets/icons/message.svg
new file mode 100644
index 0000000..deacdc3
--- /dev/null
+++ b/src/assets/icons/message.svg
@@ -0,0 +1 @@
+
diff --git a/src/assets/icons/monitor.svg b/src/assets/icons/monitor.svg
new file mode 100644
index 0000000..f153b9c
--- /dev/null
+++ b/src/assets/icons/monitor.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/project.svg b/src/assets/icons/project.svg
new file mode 100644
index 0000000..eaf6a12
--- /dev/null
+++ b/src/assets/icons/project.svg
@@ -0,0 +1 @@
+
diff --git a/src/assets/icons/qq.svg b/src/assets/icons/qq.svg
new file mode 100644
index 0000000..a59086b
--- /dev/null
+++ b/src/assets/icons/qq.svg
@@ -0,0 +1 @@
+
diff --git a/src/assets/icons/refresh.svg b/src/assets/icons/refresh.svg
new file mode 100644
index 0000000..e598ed1
--- /dev/null
+++ b/src/assets/icons/refresh.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/role.svg b/src/assets/icons/role.svg
new file mode 100644
index 0000000..5d25278
--- /dev/null
+++ b/src/assets/icons/role.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/search.svg b/src/assets/icons/search.svg
new file mode 100644
index 0000000..2312daf
--- /dev/null
+++ b/src/assets/icons/search.svg
@@ -0,0 +1 @@
+
diff --git a/src/assets/icons/setting.svg b/src/assets/icons/setting.svg
new file mode 100644
index 0000000..fbc4945
--- /dev/null
+++ b/src/assets/icons/setting.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/size.svg b/src/assets/icons/size.svg
new file mode 100644
index 0000000..f92f852
--- /dev/null
+++ b/src/assets/icons/size.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/system.svg b/src/assets/icons/system.svg
new file mode 100644
index 0000000..2e6045b
--- /dev/null
+++ b/src/assets/icons/system.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/table.svg b/src/assets/icons/table.svg
new file mode 100644
index 0000000..1a16abb
--- /dev/null
+++ b/src/assets/icons/table.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/todo.svg b/src/assets/icons/todo.svg
new file mode 100644
index 0000000..f48e667
--- /dev/null
+++ b/src/assets/icons/todo.svg
@@ -0,0 +1 @@
+
diff --git a/src/assets/icons/tree.svg b/src/assets/icons/tree.svg
new file mode 100644
index 0000000..51aea8f
--- /dev/null
+++ b/src/assets/icons/tree.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/typescript.svg b/src/assets/icons/typescript.svg
new file mode 100644
index 0000000..781d6f8
--- /dev/null
+++ b/src/assets/icons/typescript.svg
@@ -0,0 +1 @@
+
diff --git a/src/assets/icons/up.svg b/src/assets/icons/up.svg
new file mode 100644
index 0000000..3b6c535
--- /dev/null
+++ b/src/assets/icons/up.svg
@@ -0,0 +1 @@
+
diff --git a/src/assets/icons/user.svg b/src/assets/icons/user.svg
new file mode 100644
index 0000000..8e693ec
--- /dev/null
+++ b/src/assets/icons/user.svg
@@ -0,0 +1 @@
+
diff --git a/src/assets/icons/visitor.svg b/src/assets/icons/visitor.svg
new file mode 100644
index 0000000..1fd8dbe
--- /dev/null
+++ b/src/assets/icons/visitor.svg
@@ -0,0 +1 @@
+
diff --git a/src/assets/icons/vue.svg b/src/assets/icons/vue.svg
new file mode 100644
index 0000000..456f876
--- /dev/null
+++ b/src/assets/icons/vue.svg
@@ -0,0 +1 @@
+
diff --git a/src/assets/icons/wechat.svg b/src/assets/icons/wechat.svg
new file mode 100644
index 0000000..2fc5803
--- /dev/null
+++ b/src/assets/icons/wechat.svg
@@ -0,0 +1 @@
+
diff --git a/src/assets/icons/xml.svg b/src/assets/icons/xml.svg
new file mode 100644
index 0000000..f041213
--- /dev/null
+++ b/src/assets/icons/xml.svg
@@ -0,0 +1 @@
+
diff --git a/src/assets/images/401.svg b/src/assets/images/401.svg
new file mode 100644
index 0000000..6096d41
--- /dev/null
+++ b/src/assets/images/401.svg
@@ -0,0 +1,398 @@
+
+
+
diff --git a/src/assets/images/404.svg b/src/assets/images/404.svg
new file mode 100644
index 0000000..b9bf23c
--- /dev/null
+++ b/src/assets/images/404.svg
@@ -0,0 +1,340 @@
+
+
+
diff --git a/src/assets/images/login-bg.svg b/src/assets/images/login-bg.svg
new file mode 100644
index 0000000..70f391f
--- /dev/null
+++ b/src/assets/images/login-bg.svg
@@ -0,0 +1,73 @@
+
+
diff --git a/src/assets/images/login-bg1.svg b/src/assets/images/login-bg1.svg
new file mode 100644
index 0000000..a0fbc13
--- /dev/null
+++ b/src/assets/images/login-bg1.svg
@@ -0,0 +1,71 @@
+
diff --git a/src/assets/logo.png b/src/assets/logo.png
new file mode 100644
index 0000000..be095c0
Binary files /dev/null and b/src/assets/logo.png differ
diff --git a/src/assets/user.png b/src/assets/user.png
new file mode 100644
index 0000000..fbe2d32
Binary files /dev/null and b/src/assets/user.png differ
diff --git a/src/components/AiAssistant/index.vue b/src/components/AiAssistant/index.vue
new file mode 100644
index 0000000..5458bf5
--- /dev/null
+++ b/src/components/AiAssistant/index.vue
@@ -0,0 +1,672 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
💡 试试这些命令:
+
+ {{ example }}
+
+
+
+
+
+
+
+
+
+
🎯 将要执行:
+
+
+
+ 跳转到:
+
{{ response.action.pageName }}
+
+ 并搜索:
+ {{ response.action.query }}
+
+
+
+
+ 跳转至:
+
{{ response.action.pageName }}
+
+ 并搜索:
+ {{ response.action.query }}
+
+
+
+ 执行:
+
{{ response.action.functionCall.name }}
+
+
+
+ 执行:
+ {{ response.action.functionName }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/AppLink/index.vue b/src/components/AppLink/index.vue
new file mode 100644
index 0000000..b3b8f17
--- /dev/null
+++ b/src/components/AppLink/index.vue
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
diff --git a/src/components/Breadcrumb/index.vue b/src/components/Breadcrumb/index.vue
new file mode 100644
index 0000000..9d49e1f
--- /dev/null
+++ b/src/components/Breadcrumb/index.vue
@@ -0,0 +1,85 @@
+
+
+
+
+ {{ translateRouteTitle(item.meta.title) }}
+
+
+ {{ translateRouteTitle(item.meta.title) }}
+
+
+
+
+
+
+
+
diff --git a/src/components/CURD/PageContent.vue b/src/components/CURD/PageContent.vue
new file mode 100644
index 0000000..6ca9bb7
--- /dev/null
+++ b/src/components/CURD/PageContent.vue
@@ -0,0 +1,934 @@
+
+
+
+
+
+
+
+
+ {{ btn.text }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ (col.selectList ?? {})[scope.row[col.prop]] }}
+
+
+
+
+
+
+ {{ scope.row[col.prop] }}
+
+
+
+
+
+
+
+ 0 && handleModify(col.prop, scope.row[col.prop], scope.row)
+ "
+ />
+
+
+
+
+
+
+
+
+
+
+
+ {{ `${col.priceFormat ?? "¥"}${scope.row[col.prop]}` }}
+
+
+
+
+ {{ scope.row[col.prop] }}%
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{
+ scope.row[col.prop]
+ ? useDateFormat(scope.row[col.prop], col.dateFormat ?? "YYYY-MM-DD HH:mm:ss")
+ .value
+ : ""
+ }}
+
+
+
+
+
+
+ {{ btn.text }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 确 定
+ 取 消
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 将文件拖到此处,或
+ 点击上传
+
+
+
+ *.xlsx / *.xls
+
+ 下载模板
+
+
+
+
+
+
+
+
+
+
+
+ 确 定
+
+ 取 消
+
+
+
+
+
+
+
+
+
diff --git a/src/components/CURD/PageModal.vue b/src/components/CURD/PageModal.vue
new file mode 100644
index 0000000..3b8195a
--- /dev/null
+++ b/src/components/CURD/PageModal.vue
@@ -0,0 +1,273 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item?.label || "" }}
+
+
+
+ :
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 确 定
+ {{ !formDisable ? "取 消" : "关闭" }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item?.label || "" }}
+
+
+
+ :
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 确 定
+ {{ !formDisable ? "取 消" : "关闭" }}
+
+
+
+
+
+
+
+
+
diff --git a/src/components/CURD/PageSearch.vue b/src/components/CURD/PageSearch.vue
new file mode 100644
index 0000000..1cdb461
--- /dev/null
+++ b/src/components/CURD/PageSearch.vue
@@ -0,0 +1,169 @@
+
+
+
+
+
+
+
+
+
+ {{ item?.label || "" }}
+
+
+
+ :
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+ 重置
+
+
+
+ {{ isExpand ? "收起" : "展开" }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/CURD/types.ts b/src/components/CURD/types.ts
new file mode 100644
index 0000000..a5fe55e
--- /dev/null
+++ b/src/components/CURD/types.ts
@@ -0,0 +1,223 @@
+import type { DialogProps, DrawerProps, FormItemRule, PaginationProps } from "element-plus";
+import type { FormProps, TableProps, ColProps, ButtonProps, CardProps } from "element-plus";
+import type PageContent from "./PageContent.vue";
+import type PageModal from "./PageModal.vue";
+import type PageSearch from "./PageSearch.vue";
+import type { CSSProperties } from "vue";
+
+export type PageSearchInstance = InstanceType;
+export type PageContentInstance = InstanceType;
+export type PageModalInstance = InstanceType;
+
+export type IObject = Record;
+
+type DateComponent = "date-picker" | "time-picker" | "time-select" | "custom-tag" | "input-tag";
+type InputComponent = "input" | "select" | "input-number" | "cascader" | "tree-select";
+type OtherComponent = "text" | "radio" | "checkbox" | "switch" | "icon-select" | "custom";
+export type ISearchComponent = DateComponent | InputComponent | "custom";
+export type IComponentType = DateComponent | InputComponent | OtherComponent;
+
+type ToolbarLeft = "add" | "delete" | "import" | "export";
+type ToolbarRight = "refresh" | "filter" | "imports" | "exports" | "search";
+type ToolbarTable = "edit" | "view" | "delete";
+export type IToolsButton = {
+ name: string; // 按钮名称
+ text?: string; // 按钮文本
+ perm?: Array | string; // 权限标识(可以是完整权限字符串如'sys:user:add'或操作权限如'add')
+ attrs?: Partial & { style?: CSSProperties }; // 按钮属性
+ render?: (row: IObject) => boolean; // 条件渲染
+};
+export type IToolsDefault = ToolbarLeft | ToolbarRight | ToolbarTable | IToolsButton;
+
+export interface IOperateData {
+ name: string;
+ row: IObject;
+ column: IObject;
+ $index: number;
+}
+
+export interface ISearchConfig {
+ // 权限前缀(如sys:user,用于组成权限标识),不提供则不进行权限校验
+ permPrefix?: string;
+ // 标签冒号(默认:false)
+ colon?: boolean;
+ // 表单项(默认:[])
+ formItems?: IFormItems;
+ // 是否开启展开和收缩(默认:true)
+ isExpandable?: boolean;
+ // 默认展示的表单项数量(默认:3)
+ showNumber?: number;
+ // 卡片属性
+ cardAttrs?: Partial & { style?: CSSProperties };
+ // form组件属性
+ form?: IForm;
+ // 自适应网格布局(使用时表单不要添加 style: { width: "200px" })
+ grid?: boolean | "left" | "right";
+}
+
+export interface IContentConfig {
+ // 权限前缀(如sys:user,用于组成权限标识),不提供则不进行权限校验
+ permPrefix?: string;
+ // table组件属性
+ table?: Omit, "data">;
+ // 分页组件位置(默认:left)
+ pagePosition?: "left" | "right";
+ // pagination组件属性
+ pagination?:
+ | boolean
+ | Partial<
+ Omit<
+ PaginationProps,
+ "v-model:page-size" | "v-model:current-page" | "total" | "currentPage"
+ >
+ >;
+ // 列表的网络请求函数(需返回promise)
+ indexAction: (queryParams: T) => Promise;
+ // 默认的分页相关的请求参数
+ request?: {
+ pageName: string;
+ limitName: string;
+ };
+ // 数据格式解析的回调函数
+ parseData?: (res: any) => {
+ total: number;
+ list: IObject[];
+ [key: string]: any;
+ };
+ // 修改属性的网络请求函数(需返回promise)
+ modifyAction?: (data: {
+ [key: string]: any;
+ field: string;
+ value: boolean | string | number;
+ }) => Promise;
+ // 删除的网络请求函数(需返回promise)
+ deleteAction?: (ids: string) => Promise;
+ // 后端导出的网络请求函数(需返回promise)
+ exportAction?: (queryParams: T) => Promise;
+ // 前端全量导出的网络请求函数(需返回promise)
+ exportsAction?: (queryParams: T) => Promise;
+ // 导入模板
+ importTemplate?: string | (() => Promise);
+ // 后端导入的网络请求函数(需返回promise)
+ importAction?: (file: File) => Promise;
+ // 前端导入的网络请求函数(需返回promise)
+ importsAction?: (data: IObject[]) => Promise;
+ // 主键名(默认为id)
+ pk?: string;
+ // 表格工具栏(默认:add,delete,export,也可自定义)
+ toolbar?: Array;
+ // 表格工具栏右侧图标(默认:refresh,filter,imports,exports,search)
+ defaultToolbar?: Array;
+ // table组件列属性(额外的属性templet,operat,slotName)
+ cols: Array<{
+ type?: "default" | "selection" | "index" | "expand";
+ label?: string;
+ prop?: string;
+ width?: string | number;
+ align?: "left" | "center" | "right";
+ columnKey?: string;
+ reserveSelection?: boolean;
+ // 列是否显示
+ show?: boolean;
+ // 模板
+ templet?:
+ | "image"
+ | "list"
+ | "url"
+ | "switch"
+ | "input"
+ | "price"
+ | "percent"
+ | "icon"
+ | "date"
+ | "tool"
+ | "custom";
+ // image模板相关参数
+ imageWidth?: number;
+ imageHeight?: number;
+ // list模板相关参数
+ selectList?: IObject;
+ // switch模板相关参数
+ activeValue?: boolean | string | number;
+ inactiveValue?: boolean | string | number;
+ activeText?: string;
+ inactiveText?: string;
+ // input模板相关参数
+ inputType?: string;
+ // price模板相关参数
+ priceFormat?: string;
+ // date模板相关参数
+ dateFormat?: string;
+ // tool模板相关参数
+ operat?: Array;
+ // filter值拼接符
+ filterJoin?: string;
+ [key: string]: any;
+ // 初始化数据函数
+ initFn?: (item: IObject) => void;
+ }>;
+}
+
+export interface IModalConfig {
+ // 权限前缀(如sys:user,用于组成权限标识),不提供则不进行权限校验
+ permPrefix?: string;
+ // 标签冒号(默认:false)
+ colon?: boolean;
+ // 主键名(主要用于编辑数据,默认为id)
+ pk?: string;
+ // 组件类型(默认:dialog)
+ component?: "dialog" | "drawer";
+ // dialog组件属性
+ dialog?: Partial>;
+ // drawer组件属性
+ drawer?: Partial>;
+ // form组件属性
+ form?: IForm;
+ // 表单项
+ formItems: IFormItems;
+ // 提交之前处理
+ beforeSubmit?: (data: T) => void;
+ // 提交的网络请求函数(需返回promise)
+ formAction?: (data: T) => Promise;
+}
+
+export type IForm = Partial>;
+
+// 表单项
+export type IFormItems = Array<{
+ // 组件类型(如input,select,radio,custom等)
+ type: T;
+ // 标签提示
+ tips?: string | IObject;
+ // 标签文本
+ label: string;
+ // 键名
+ prop: string;
+ // 组件属性
+ attrs?: IObject;
+ // 组件可选项(只适用于select,radio,checkbox组件)
+ options?: Array<{ label: string; value: any; [key: string]: any }> | Ref;
+ // 验证规则
+ rules?: FormItemRule[];
+ // 初始值
+ initialValue?: any;
+ // 插槽名(适用于自定义组件,设置类型为custom)
+ slotName?: string;
+ // 是否隐藏
+ hidden?: boolean;
+ // layout组件Col属性
+ col?: Partial;
+ // 组件事件
+ events?: Record void>;
+ // 初始化数据函数扩展
+ initFn?: (item: IObject) => void;
+}>;
+
+export interface IPageForm {
+ // 主键名(主要用于编辑数据,默认为id)
+ pk?: string;
+ // form组件属性
+ form?: IForm;
+ // 表单项
+ formItems: IFormItems;
+}
diff --git a/src/components/CURD/usePage.ts b/src/components/CURD/usePage.ts
new file mode 100644
index 0000000..18261f2
--- /dev/null
+++ b/src/components/CURD/usePage.ts
@@ -0,0 +1,105 @@
+import { ref } from "vue";
+import type { IObject, PageContentInstance, PageModalInstance, PageSearchInstance } from "./types";
+
+function usePage() {
+ const searchRef = ref();
+ const contentRef = ref();
+ const addModalRef = ref();
+ const editModalRef = ref();
+
+ // 搜索
+ function handleQueryClick(queryParams: IObject) {
+ const filterParams = contentRef.value?.getFilterParams();
+ contentRef.value?.fetchPageData({ ...queryParams, ...filterParams }, true);
+ }
+ // 重置
+ function handleResetClick(queryParams: IObject) {
+ const filterParams = contentRef.value?.getFilterParams();
+ contentRef.value?.fetchPageData({ ...queryParams, ...filterParams }, true);
+ }
+ // 新增
+ function handleAddClick(RefImpl?: Ref) {
+ if (RefImpl) {
+ RefImpl?.value.setModalVisible();
+ RefImpl?.value.handleDisabled(false);
+ } else {
+ addModalRef.value?.setModalVisible();
+ addModalRef.value?.handleDisabled(false);
+ }
+ }
+ // 编辑
+ async function handleEditClick(
+ row: IObject,
+ callback?: (result?: IObject) => IObject,
+ RefImpl?: Ref
+ ) {
+ if (RefImpl) {
+ RefImpl.value?.setModalVisible();
+ RefImpl.value?.handleDisabled(false);
+ const from = await (callback?.(row) ?? Promise.resolve(row));
+ RefImpl.value?.setFormData(from ? from : row);
+ } else {
+ editModalRef.value?.setModalVisible();
+ editModalRef.value?.handleDisabled(false);
+ const from = await (callback?.(row) ?? Promise.resolve(row));
+ editModalRef.value?.setFormData(from ? from : row);
+ }
+ }
+ // 查看
+ async function handleViewClick(
+ row: IObject,
+ callback?: (result?: IObject) => IObject,
+ RefImpl?: Ref
+ ) {
+ if (RefImpl) {
+ RefImpl.value?.setModalVisible();
+ RefImpl.value?.handleDisabled(true);
+ const from = await (callback?.(row) ?? Promise.resolve(row));
+ RefImpl.value?.setFormData(from ? from : row);
+ } else {
+ editModalRef.value?.setModalVisible();
+ editModalRef.value?.handleDisabled(true);
+ const from = await (callback?.(row) ?? Promise.resolve(row));
+ editModalRef.value?.setFormData(from ? from : row);
+ }
+ }
+ // 表单提交
+ function handleSubmitClick() {
+ //根据检索条件刷新列表数据
+ const queryParams = searchRef.value?.getQueryParams();
+ contentRef.value?.fetchPageData(queryParams, true);
+ }
+ // 导出
+ function handleExportClick() {
+ // 根据检索条件导出数据
+ const queryParams = searchRef.value?.getQueryParams();
+ contentRef.value?.exportPageData(queryParams);
+ }
+ // 搜索显隐
+ function handleSearchClick() {
+ searchRef.value?.toggleVisible();
+ }
+ // 涮选数据
+ function handleFilterChange(filterParams: IObject) {
+ const queryParams = searchRef.value?.getQueryParams();
+ contentRef.value?.fetchPageData({ ...queryParams, ...filterParams }, true);
+ }
+
+ return {
+ searchRef,
+ contentRef,
+ addModalRef,
+ editModalRef,
+ handleQueryClick,
+ handleResetClick,
+ handleAddClick,
+ handleEditClick,
+ handleViewClick,
+ handleSubmitClick,
+ handleExportClick,
+ handleSearchClick,
+ handleFilterChange,
+ };
+}
+
+export default usePage;
diff --git a/src/components/CommonWrapper/index.vue b/src/components/CommonWrapper/index.vue
new file mode 100644
index 0000000..9898849
--- /dev/null
+++ b/src/components/CommonWrapper/index.vue
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
diff --git a/src/components/CopyButton/index.vue b/src/components/CopyButton/index.vue
new file mode 100644
index 0000000..5179633
--- /dev/null
+++ b/src/components/CopyButton/index.vue
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/DarkModeSwitch/index.vue b/src/components/DarkModeSwitch/index.vue
new file mode 100644
index 0000000..780c1ab
--- /dev/null
+++ b/src/components/DarkModeSwitch/index.vue
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.label }}
+
+
+
+
+
+
diff --git a/src/components/Dict/DictLabel.vue b/src/components/Dict/DictLabel.vue
new file mode 100644
index 0000000..c660347
--- /dev/null
+++ b/src/components/Dict/DictLabel.vue
@@ -0,0 +1,66 @@
+
+
+ {{ label }}
+
+
+ {{ label }}
+
+
+
diff --git a/src/components/Dict/index.vue b/src/components/Dict/index.vue
new file mode 100644
index 0000000..79d98e4
--- /dev/null
+++ b/src/components/Dict/index.vue
@@ -0,0 +1,123 @@
+
+
+
+
+
+
+
+ {{ option.label }}
+
+
+
+
+
+ {{ option.label }}
+
+
+
+
+
diff --git a/src/components/ECharts/index.vue b/src/components/ECharts/index.vue
new file mode 100644
index 0000000..03b8b98
--- /dev/null
+++ b/src/components/ECharts/index.vue
@@ -0,0 +1,81 @@
+
+
+
+
+
+
+
diff --git a/src/components/Fullscreen/index.vue b/src/components/Fullscreen/index.vue
new file mode 100644
index 0000000..bd888fe
--- /dev/null
+++ b/src/components/Fullscreen/index.vue
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
diff --git a/src/components/GithubCorner/index.vue b/src/components/GithubCorner/index.vue
new file mode 100644
index 0000000..7f91959
--- /dev/null
+++ b/src/components/GithubCorner/index.vue
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
diff --git a/src/components/Hamburger/index.vue b/src/components/Hamburger/index.vue
new file mode 100644
index 0000000..9955af6
--- /dev/null
+++ b/src/components/Hamburger/index.vue
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
diff --git a/src/components/HelloWorld.vue b/src/components/HelloWorld.vue
new file mode 100644
index 0000000..3f21801
--- /dev/null
+++ b/src/components/HelloWorld.vue
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/components/IconSelect/index.vue b/src/components/IconSelect/index.vue
new file mode 100644
index 0000000..b96e93e
--- /dev/null
+++ b/src/components/IconSelect/index.vue
@@ -0,0 +1,207 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/InputTag/index.vue b/src/components/InputTag/index.vue
new file mode 100644
index 0000000..a22b993
--- /dev/null
+++ b/src/components/InputTag/index.vue
@@ -0,0 +1,73 @@
+
+
+
+
+ {{ tag }}
+
+
+
+ {{ config.buttonAttrs.btnText ? config.buttonAttrs.btnText : "+ New Tag" }}
+
+
+
+
+
diff --git a/src/components/LangSelect/index.vue b/src/components/LangSelect/index.vue
new file mode 100644
index 0000000..7fcdb60
--- /dev/null
+++ b/src/components/LangSelect/index.vue
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+ {{ item.label }}
+
+
+
+
+
+
+
diff --git a/src/components/MenuSearch/index.vue b/src/components/MenuSearch/index.vue
new file mode 100644
index 0000000..62b99f1
--- /dev/null
+++ b/src/components/MenuSearch/index.vue
@@ -0,0 +1,523 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 搜索历史
+
+
+
+
+
+ -
+
+
+
+ {{ item.title }}
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+ {{ item.title }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/Notification/index.vue b/src/components/Notification/index.vue
new file mode 100644
index 0000000..8d3baba
--- /dev/null
+++ b/src/components/Notification/index.vue
@@ -0,0 +1,163 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.title }}
+
+
+
+ {{ item.publishTime }}
+
+
+
+
+
+
+ 查看更多
+
+
+
+
+
+ 全部已读
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ noticeDetail.publisherName }}
+
+
+
+ {{ noticeDetail.publishTime }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/OperationColumn/index.vue b/src/components/OperationColumn/index.vue
new file mode 100644
index 0000000..db53222
--- /dev/null
+++ b/src/components/OperationColumn/index.vue
@@ -0,0 +1,91 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/Pagination/index.vue b/src/components/Pagination/index.vue
new file mode 100644
index 0000000..7017d95
--- /dev/null
+++ b/src/components/Pagination/index.vue
@@ -0,0 +1,91 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/components/SizeSelect/index.vue b/src/components/SizeSelect/index.vue
new file mode 100644
index 0000000..8b150eb
--- /dev/null
+++ b/src/components/SizeSelect/index.vue
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+ {{ item.label }}
+
+
+
+
+
+
+
+
diff --git a/src/components/TableSelect/index.vue b/src/components/TableSelect/index.vue
new file mode 100644
index 0000000..c546f75
--- /dev/null
+++ b/src/components/TableSelect/index.vue
@@ -0,0 +1,357 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+ 重置
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ confirmText }}
+
+ 清 空
+ 关 闭
+
+
+
+
+
+
+
+
+
diff --git a/src/components/TextScroll/index.vue b/src/components/TextScroll/index.vue
new file mode 100644
index 0000000..d37e1e7
--- /dev/null
+++ b/src/components/TextScroll/index.vue
@@ -0,0 +1,412 @@
+
+
+
+
+
+
+
+
diff --git a/src/components/Upload/FileUpload.vue b/src/components/Upload/FileUpload.vue
new file mode 100644
index 0000000..6219535
--- /dev/null
+++ b/src/components/Upload/FileUpload.vue
@@ -0,0 +1,276 @@
+
+
+
+
+
+
+ {{ props.uploadBtnText }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/Upload/MultiImageUpload.vue b/src/components/Upload/MultiImageUpload.vue
new file mode 100644
index 0000000..082b694
--- /dev/null
+++ b/src/components/Upload/MultiImageUpload.vue
@@ -0,0 +1,215 @@
+
+
+
+
+
+
+
![]()
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/Upload/SingleImageUpload.vue b/src/components/Upload/SingleImageUpload.vue
new file mode 100644
index 0000000..59a27b4
--- /dev/null
+++ b/src/components/Upload/SingleImageUpload.vue
@@ -0,0 +1,209 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/WangEditor/index.vue b/src/components/WangEditor/index.vue
new file mode 100644
index 0000000..53ca563
--- /dev/null
+++ b/src/components/WangEditor/index.vue
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/composables/auth/useTokenRefresh.ts b/src/composables/auth/useTokenRefresh.ts
new file mode 100644
index 0000000..12e138e
--- /dev/null
+++ b/src/composables/auth/useTokenRefresh.ts
@@ -0,0 +1,91 @@
+import type { InternalAxiosRequestConfig } from "axios";
+import { useUserStoreHook } from "@/store/modules/user-store";
+import { AuthStorage, redirectToLogin } from "@/utils/auth";
+
+/**
+ * 重试请求的回调函数类型
+ */
+type RetryCallback = () => void;
+
+/**
+ * Token刷新组合式函数
+ */
+export function useTokenRefresh() {
+ // Token 刷新相关状态
+ let isRefreshingToken = false;
+ const pendingRequests: RetryCallback[] = [];
+
+ /**
+ * 刷新 Token 并重试请求
+ */
+ async function refreshTokenAndRetry(
+ config: InternalAxiosRequestConfig,
+ httpRequest: any
+ ): Promise {
+ return new Promise((resolve, reject) => {
+ // 封装需要重试的请求
+ const retryRequest = () => {
+ const newToken = AuthStorage.getAccessToken();
+ if (newToken && config.headers) {
+ config.headers.Authorization = `Bearer ${newToken}`;
+ }
+ httpRequest(config).then(resolve).catch(reject);
+ };
+
+ // 将请求加入等待队列
+ pendingRequests.push(retryRequest);
+
+ // 如果没有正在刷新,则开始刷新流程
+ if (!isRefreshingToken) {
+ isRefreshingToken = true;
+
+ useUserStoreHook()
+ .refreshToken()
+ .then(() => {
+ // 刷新成功,重试所有等待的请求
+ pendingRequests.forEach((callback) => {
+ try {
+ callback();
+ } catch (error) {
+ console.error("Retry request error:", error);
+ }
+ });
+ // 清空队列
+ pendingRequests.length = 0;
+ })
+ .catch(async (error) => {
+ console.error("Token refresh failed:", error);
+ // 刷新失败,先 reject 所有等待的请求,再清空队列
+ const failedRequests = [...pendingRequests];
+ pendingRequests.length = 0;
+
+ // 拒绝所有等待的请求
+ failedRequests.forEach(() => {
+ reject(new Error("Token refresh failed"));
+ });
+
+ // 跳转登录页
+ await redirectToLogin("登录状态已失效,请重新登录");
+ })
+ .finally(() => {
+ isRefreshingToken = false;
+ });
+ }
+ });
+ }
+
+ /**
+ * 获取刷新状态(用于外部判断)
+ */
+ function getRefreshStatus() {
+ return {
+ isRefreshing: isRefreshingToken,
+ pendingCount: pendingRequests.length,
+ };
+ }
+
+ return {
+ refreshTokenAndRetry,
+ getRefreshStatus,
+ };
+}
diff --git a/src/composables/index.ts b/src/composables/index.ts
new file mode 100644
index 0000000..4bff69b
--- /dev/null
+++ b/src/composables/index.ts
@@ -0,0 +1,14 @@
+export { useStomp } from "./websocket/useStomp";
+export { useDictSync } from "./websocket/useDictSync";
+export type { DictMessage } from "./websocket/useDictSync";
+export { useOnlineCount } from "./websocket/useOnlineCount";
+export { useTokenRefresh } from "./auth/useTokenRefresh";
+
+export { useLayout } from "./layout/useLayout";
+export { useLayoutMenu } from "./layout/useLayoutMenu";
+export { useDeviceDetection } from "./layout/useDeviceDetection";
+
+export { useAiAction } from "./useAiAction";
+export type { UseAiActionOptions, AiActionHandler } from "./useAiAction";
+
+export { useTableSelection } from "./useTableSelection";
diff --git a/src/composables/layout/useDeviceDetection.ts b/src/composables/layout/useDeviceDetection.ts
new file mode 100644
index 0000000..ccb735c
--- /dev/null
+++ b/src/composables/layout/useDeviceDetection.ts
@@ -0,0 +1,40 @@
+import { watchEffect, computed } from "vue";
+import { useWindowSize } from "@vueuse/core";
+import { useAppStore } from "@/store";
+import { DeviceEnum } from "@/enums/settings/device-enum";
+
+/**
+ * 设备检测和响应式处理
+ * 监听屏幕尺寸变化,自动调整设备类型和侧边栏状态
+ */
+export function useDeviceDetection() {
+ const appStore = useAppStore();
+ const { width } = useWindowSize();
+
+ // 桌面设备断点
+ const DESKTOP_BREAKPOINT = 992;
+
+ // 计算设备类型
+ const isDesktop = computed(() => width.value >= DESKTOP_BREAKPOINT);
+ const isMobile = computed(() => appStore.device === DeviceEnum.MOBILE);
+
+ // 监听屏幕尺寸变化,自动调整设备类型和侧边栏状态
+ watchEffect(() => {
+ const deviceType = isDesktop.value ? DeviceEnum.DESKTOP : DeviceEnum.MOBILE;
+
+ // 更新设备类型
+ appStore.toggleDevice(deviceType);
+
+ // 根据设备类型调整侧边栏状态
+ if (isDesktop.value) {
+ appStore.openSideBar();
+ } else {
+ appStore.closeSideBar();
+ }
+ });
+
+ return {
+ isDesktop,
+ isMobile,
+ };
+}
diff --git a/src/composables/layout/useLayout.ts b/src/composables/layout/useLayout.ts
new file mode 100644
index 0000000..ba2f618
--- /dev/null
+++ b/src/composables/layout/useLayout.ts
@@ -0,0 +1,62 @@
+import { useAppStore, useSettingsStore } from "@/store";
+import { defaultSettings } from "@/settings";
+
+/**
+ * 布局相关的通用逻辑
+ */
+export function useLayout() {
+ const appStore = useAppStore();
+ const settingsStore = useSettingsStore();
+
+ // 计算当前布局模式
+ const currentLayout = computed(() => settingsStore.layout);
+
+ // 侧边栏展开状态
+ const isSidebarOpen = computed(() => appStore.sidebar.opened);
+
+ // 是否显示标签视图
+ const isShowTagsView = computed(() => settingsStore.showTagsView);
+
+ // 是否显示设置面板
+ const isShowSettings = computed(() => defaultSettings.showSettings);
+
+ // 是否显示Logo
+ const isShowLogo = computed(() => settingsStore.showAppLogo);
+
+ // 是否移动设备
+ const isMobile = computed(() => appStore.device === "mobile");
+
+ // 布局CSS类
+ const layoutClass = computed(() => ({
+ hideSidebar: !appStore.sidebar.opened,
+ openSidebar: appStore.sidebar.opened,
+ mobile: appStore.device === "mobile",
+ [`layout-${settingsStore.layout}`]: true,
+ }));
+
+ /**
+ * 处理切换侧边栏的展开/收起状态
+ */
+ function toggleSidebar() {
+ appStore.toggleSidebar();
+ }
+
+ /**
+ * 关闭侧边栏(移动端)
+ */
+ function closeSidebar() {
+ appStore.closeSideBar();
+ }
+
+ return {
+ currentLayout,
+ isSidebarOpen,
+ isShowTagsView,
+ isShowSettings,
+ isShowLogo,
+ isMobile,
+ layoutClass,
+ toggleSidebar,
+ closeSidebar,
+ };
+}
diff --git a/src/composables/layout/useLayoutMenu.ts b/src/composables/layout/useLayoutMenu.ts
new file mode 100644
index 0000000..cebe15f
--- /dev/null
+++ b/src/composables/layout/useLayoutMenu.ts
@@ -0,0 +1,39 @@
+import { useRoute } from "vue-router";
+import { useAppStore, usePermissionStore } from "@/store";
+
+/**
+ * 布局菜单处理逻辑
+ */
+export function useLayoutMenu() {
+ const route = useRoute();
+ const appStore = useAppStore();
+ const permissionStore = usePermissionStore();
+
+ // 顶部菜单激活路径
+ const activeTopMenuPath = computed(() => appStore.activeTopMenuPath);
+
+ // 常规路由(左侧菜单或顶部菜单)
+ const routes = computed(() => permissionStore.routes);
+
+ // 混合布局左侧菜单路由
+ const sideMenuRoutes = computed(() => permissionStore.mixLayoutSideMenus);
+
+ // 当前激活的菜单
+ const activeMenu = computed(() => {
+ const { meta, path } = route;
+
+ // 如果设置了activeMenu,则使用
+ if (meta?.activeMenu) {
+ return meta.activeMenu;
+ }
+
+ return path;
+ });
+
+ return {
+ routes,
+ sideMenuRoutes,
+ activeMenu,
+ activeTopMenuPath,
+ };
+}
diff --git a/src/composables/useAiAction.ts b/src/composables/useAiAction.ts
new file mode 100644
index 0000000..c338305
--- /dev/null
+++ b/src/composables/useAiAction.ts
@@ -0,0 +1,270 @@
+import { useRoute } from "vue-router";
+import { ElMessage, ElMessageBox } from "element-plus";
+import { onMounted, onBeforeUnmount, nextTick } from "vue";
+import AiCommandApi from "@/api/ai";
+
+/**
+ * AI 操作处理器(简化版)
+ *
+ * 可以是简单函数,也可以是配置对象
+ */
+export type AiActionHandler =
+ | ((args: T) => Promise | void)
+ | {
+ /** 执行函数 */
+ execute: (args: T) => Promise | void;
+ /** 是否需要确认(默认 true) */
+ needConfirm?: boolean;
+ /** 确认消息(支持函数或字符串) */
+ confirmMessage?: string | ((args: T) => string);
+ /** 成功消息(支持函数或字符串) */
+ successMessage?: string | ((args: T) => string);
+ /** 是否调用后端 API(默认 false,如果为 true 则自动调用 executeCommand) */
+ callBackendApi?: boolean;
+ };
+
+/**
+ * AI 操作配置
+ */
+export interface UseAiActionOptions {
+ /** 操作映射表:函数名 -> 处理器 */
+ actionHandlers?: Record;
+ /** 数据刷新函数(操作完成后调用) */
+ onRefresh?: () => Promise | void;
+ /** 自动搜索处理函数 */
+ onAutoSearch?: (keywords: string) => void;
+ /** 当前路由路径(用于执行命令时传递) */
+ currentRoute?: string;
+}
+
+/**
+ * AI 操作 Composable
+ *
+ * 统一处理 AI 助手传递的操作,支持:
+ * - 自动搜索(通过 keywords + autoSearch 参数)
+ * - 执行 AI 操作(通过 aiAction 参数)
+ * - 配置化的操作处理器
+ */
+export function useAiAction(options: UseAiActionOptions = {}) {
+ const route = useRoute();
+ const { actionHandlers = {}, onRefresh, onAutoSearch, currentRoute = route.path } = options;
+
+ // 用于跟踪是否已卸载,防止在卸载后执行回调
+ let isUnmounted = false;
+
+ /**
+ * 执行 AI 操作(统一处理确认、执行、反馈流程)
+ */
+ async function executeAiAction(action: any) {
+ if (isUnmounted) return;
+
+ // 兼容两种入参:{ functionName, arguments } 或 { functionCall: { name, arguments } }
+ const fnCall = action.functionCall ?? {
+ name: action.functionName,
+ arguments: action.arguments,
+ };
+
+ if (!fnCall?.name) {
+ ElMessage.warning("未识别的 AI 操作");
+ return;
+ }
+
+ // 查找对应的处理器
+ const handler = actionHandlers[fnCall.name];
+ if (!handler) {
+ ElMessage.warning(`暂不支持操作: ${fnCall.name}`);
+ return;
+ }
+
+ try {
+ // 判断处理器类型(函数 or 配置对象)
+ const isSimpleFunction = typeof handler === "function";
+
+ if (isSimpleFunction) {
+ // 简单函数形式:直接执行
+ await handler(fnCall.arguments);
+ } else {
+ // 配置对象形式:统一处理确认、执行、反馈
+ const config = handler;
+
+ // 1. 确认阶段(默认需要确认)
+ if (config.needConfirm !== false) {
+ const confirmMsg =
+ typeof config.confirmMessage === "function"
+ ? config.confirmMessage(fnCall.arguments)
+ : config.confirmMessage || "确认执行此操作吗?";
+
+ await ElMessageBox.confirm(confirmMsg, "AI 助手操作确认", {
+ confirmButtonText: "确认执行",
+ cancelButtonText: "取消",
+ type: "warning",
+ dangerouslyUseHTMLString: true,
+ });
+ }
+
+ // 2. 执行阶段
+ if (config.callBackendApi) {
+ // 自动调用后端 API
+ await AiCommandApi.executeCommand({
+ originalCommand: action.originalCommand || "",
+ confirmMode: "manual",
+ userConfirmed: true,
+ currentRoute,
+ functionCall: {
+ name: fnCall.name,
+ arguments: fnCall.arguments,
+ },
+ });
+ } else {
+ // 执行自定义函数
+ await config.execute(fnCall.arguments);
+ }
+
+ // 3. 成功反馈
+ const successMsg =
+ typeof config.successMessage === "function"
+ ? config.successMessage(fnCall.arguments)
+ : config.successMessage || "操作执行成功";
+ ElMessage.success(successMsg);
+ }
+
+ // 4. 刷新数据
+ if (onRefresh) {
+ await onRefresh();
+ }
+ } catch (error: any) {
+ // 处理取消操作
+ if (error === "cancel") {
+ ElMessage.info("已取消操作");
+ return;
+ }
+
+ console.error("AI 操作执行失败:", error);
+ ElMessage.error(error.message || "操作执行失败");
+ }
+ }
+
+ /**
+ * 执行后端命令(通用方法)
+ */
+ async function executeCommand(
+ functionName: string,
+ args: any,
+ options: {
+ originalCommand?: string;
+ confirmMode?: "auto" | "manual";
+ needConfirm?: boolean;
+ confirmMessage?: string;
+ } = {}
+ ) {
+ const {
+ originalCommand = "",
+ confirmMode = "manual",
+ needConfirm = false,
+ confirmMessage,
+ } = options;
+
+ // 如果需要确认,先显示确认对话框
+ if (needConfirm && confirmMessage) {
+ try {
+ await ElMessageBox.confirm(confirmMessage, "AI 助手操作确认", {
+ confirmButtonText: "确认执行",
+ cancelButtonText: "取消",
+ type: "warning",
+ dangerouslyUseHTMLString: true,
+ });
+ } catch {
+ ElMessage.info("已取消操作");
+ return;
+ }
+ }
+
+ try {
+ await AiCommandApi.executeCommand({
+ originalCommand,
+ confirmMode,
+ userConfirmed: true,
+ currentRoute,
+ functionCall: {
+ name: functionName,
+ arguments: args,
+ },
+ });
+
+ ElMessage.success("操作执行成功");
+ } catch (error: any) {
+ if (error !== "cancel") {
+ throw error;
+ }
+ }
+ }
+
+ /**
+ * 处理自动搜索
+ */
+ function handleAutoSearch(keywords: string) {
+ if (onAutoSearch) {
+ onAutoSearch(keywords);
+ } else {
+ ElMessage.info(`AI 助手已为您自动搜索:${keywords}`);
+ }
+ }
+
+ /**
+ * 初始化:处理 URL 参数中的 AI 操作
+ *
+ * 注意:此方法只处理 AI 相关参数,不负责页面数据的初始加载
+ * 页面数据加载应由组件的 onMounted 钩子自行处理
+ */
+ async function init() {
+ if (isUnmounted) return;
+
+ // 检查是否有 AI 助手传递的参数
+ const keywords = route.query.keywords as string;
+ const autoSearch = route.query.autoSearch as string;
+ const aiActionParam = route.query.aiAction as string;
+
+ // 如果没有任何 AI 参数,直接返回
+ if (!keywords && !autoSearch && !aiActionParam) {
+ return;
+ }
+
+ // 在 nextTick 中执行,确保页面数据已加载
+ nextTick(async () => {
+ if (isUnmounted) return;
+
+ // 1. 处理自动搜索
+ if (autoSearch === "true" && keywords) {
+ handleAutoSearch(keywords);
+ }
+
+ // 2. 处理 AI 操作
+ if (aiActionParam) {
+ try {
+ const aiAction = JSON.parse(decodeURIComponent(aiActionParam));
+ await executeAiAction(aiAction);
+ } catch (error) {
+ console.error("解析 AI 操作失败:", error);
+ ElMessage.error("AI 操作参数解析失败");
+ }
+ }
+ });
+ }
+
+ // 组件挂载时自动初始化
+ onMounted(() => {
+ init();
+ });
+
+ // 组件卸载时清理
+ onBeforeUnmount(() => {
+ isUnmounted = true;
+ });
+
+ return {
+ executeAiAction,
+ executeCommand,
+ handleAutoSearch,
+ init,
+ };
+}
diff --git a/src/composables/useTableSelection.ts b/src/composables/useTableSelection.ts
new file mode 100644
index 0000000..550a15c
--- /dev/null
+++ b/src/composables/useTableSelection.ts
@@ -0,0 +1,63 @@
+import { computed, ref } from "vue";
+
+/**
+ * 表格行选择 Composable
+ *
+ * @description 提供统一的表格行选择逻辑,包括选中ID管理和清空选择
+ * @template T 数据项类型,必须包含 id 属性
+ * @returns 返回选中的ID列表、选择变化处理函数、清空选择函数
+ *
+ * @example
+ * ```typescript
+ * const { selectedIds, handleSelectionChange, clearSelection } = useTableSelection();
+ * ```
+ */
+export function useTableSelection() {
+ /**
+ * 选中的数据项ID列表
+ */
+ const selectedIds = ref<(string | number)[]>([]);
+
+ /**
+ * 表格选中项变化处理
+ * @param selection 选中的行数据列表
+ */
+ function handleSelectionChange(selection: T[]): void {
+ selectedIds.value = selection.map((item) => item.id);
+ }
+
+ /**
+ * 清空选择
+ */
+ function clearSelection(): void {
+ selectedIds.value = [];
+ }
+
+ /**
+ * 检查指定ID是否被选中
+ * @param id 要检查的ID
+ * @returns 是否被选中
+ */
+ function isSelected(id: string | number): boolean {
+ return selectedIds.value.includes(id);
+ }
+
+ /**
+ * 获取选中的数量
+ */
+ const selectedCount = computed(() => selectedIds.value.length);
+
+ /**
+ * 是否有选中项
+ */
+ const hasSelection = computed(() => selectedIds.value.length > 0);
+
+ return {
+ selectedIds,
+ selectedCount,
+ hasSelection,
+ handleSelectionChange,
+ clearSelection,
+ isSelected,
+ };
+}
diff --git a/src/composables/websocket/useDictSync.ts b/src/composables/websocket/useDictSync.ts
new file mode 100644
index 0000000..f465ac0
--- /dev/null
+++ b/src/composables/websocket/useDictSync.ts
@@ -0,0 +1,205 @@
+import { useDictStoreHook } from "@/store/modules/dict-store";
+import { useStomp } from "./useStomp";
+import type { IMessage } from "@stomp/stompjs";
+
+/**
+ * 字典变更消息结构
+ */
+export interface DictChangeMessage {
+ /** 字典编码 */
+ dictCode: string;
+ /** 时间戳 */
+ timestamp: number;
+}
+
+/**
+ * 字典消息别名(向后兼容)
+ */
+export type DictMessage = DictChangeMessage;
+
+/**
+ * 字典变更事件回调函数类型
+ */
+export type DictChangeCallback = (message: DictChangeMessage) => void;
+
+/**
+ * 全局单例实例
+ */
+let singletonInstance: ReturnType | null = null;
+
+/**
+ * 创建字典同步组合式函数(内部工厂函数)
+ */
+function createDictSyncComposable() {
+ const dictStore = useDictStoreHook();
+
+ // 使用优化后的 useStomp
+ const stomp = useStomp({
+ reconnectDelay: 20000,
+ connectionTimeout: 15000,
+ useExponentialBackoff: false,
+ maxReconnectAttempts: 3,
+ autoRestoreSubscriptions: true, // 自动恢复订阅
+ debug: false,
+ });
+
+ // 字典主题地址
+ const DICT_TOPIC = "/topic/dict";
+
+ // 消息回调函数列表
+ const messageCallbacks = ref([]);
+
+ // 订阅 ID(用于取消订阅)
+ let subscriptionId: string | null = null;
+
+ /**
+ * 处理字典变更事件
+ */
+ const handleDictChangeMessage = (message: IMessage) => {
+ if (!message.body) {
+ return;
+ }
+
+ try {
+ const data = JSON.parse(message.body) as DictChangeMessage;
+ const { dictCode } = data;
+
+ if (!dictCode) {
+ console.warn("[DictSync] 收到无效的字典变更消息:缺少 dictCode");
+ return;
+ }
+
+ console.log(`[DictSync] 字典 "${dictCode}" 已更新,清除本地缓存`);
+
+ // 清除缓存,等待按需加载
+ dictStore.removeDictItem(dictCode);
+
+ // 执行所有注册的回调函数
+ messageCallbacks.value.forEach((callback) => {
+ try {
+ callback(data);
+ } catch (error) {
+ console.error("[DictSync] 回调函数执行失败:", error);
+ }
+ });
+ } catch (error) {
+ console.error("[DictSync] 解析字典变更消息失败:", error);
+ }
+ };
+
+ /**
+ * 初始化 WebSocket 连接并订阅字典主题
+ */
+ const initialize = () => {
+ // 检查是否配置了 WebSocket 端点
+ const wsEndpoint = import.meta.env.VITE_APP_WS_ENDPOINT;
+ if (!wsEndpoint) {
+ console.log("[DictSync] 未配置 WebSocket 端点,跳过字典同步功能");
+ return;
+ }
+
+ console.log("[DictSync] 初始化字典同步服务...");
+
+ // 建立 WebSocket 连接
+ stomp.connect();
+
+ // 订阅字典主题(useStomp 会自动处理重连后的订阅恢复)
+ subscriptionId = stomp.subscribe(DICT_TOPIC, handleDictChangeMessage);
+
+ if (subscriptionId) {
+ console.log(`[DictSync] 已订阅字典主题: ${DICT_TOPIC}`);
+ } else {
+ console.log(`[DictSync] 暂存字典主题订阅,等待连接建立后自动订阅`);
+ }
+ };
+
+ /**
+ * 关闭 WebSocket 连接并清理资源
+ */
+ const cleanup = () => {
+ console.log("[DictSync] 清理字典同步服务...");
+
+ // 取消订阅(如果有的话)
+ if (subscriptionId) {
+ stomp.unsubscribe(subscriptionId);
+ subscriptionId = null;
+ }
+
+ // 也可以通过主题地址取消订阅
+ stomp.unsubscribeDestination(DICT_TOPIC);
+
+ // 断开连接
+ stomp.disconnect();
+
+ // 清空回调列表
+ messageCallbacks.value = [];
+ };
+
+ /**
+ * 注册字典变更回调函数
+ *
+ * @param callback 回调函数
+ * @returns 返回一个取消注册的函数
+ */
+ const onDictChange = (callback: DictChangeCallback) => {
+ messageCallbacks.value.push(callback);
+
+ // 返回取消注册的函数
+ return () => {
+ const index = messageCallbacks.value.indexOf(callback);
+ if (index !== -1) {
+ messageCallbacks.value.splice(index, 1);
+ }
+ };
+ };
+
+ return {
+ // 状态
+ isConnected: stomp.isConnected,
+ connectionState: stomp.connectionState,
+
+ // 方法
+ initialize,
+ cleanup,
+ onDictChange,
+
+ // 别名方法(向后兼容)
+ initWebSocket: initialize,
+ closeWebSocket: cleanup,
+ onDictMessage: onDictChange,
+
+ // 用于测试和调试
+ handleDictChangeMessage,
+ };
+}
+
+/**
+ * 字典同步组合式函数(单例模式)
+ *
+ * 用于监听后端字典变更并自动同步到前端缓存
+ *
+ * @example
+ * ```ts
+ * const dictSync = useDictSync();
+ *
+ * // 初始化(在应用启动时调用)
+ * dictSync.initialize();
+ *
+ * // 注册回调
+ * const unsubscribe = dictSync.onDictChange((message) => {
+ * console.log('字典已更新:', message.dictCode);
+ * });
+ *
+ * // 取消注册
+ * unsubscribe();
+ *
+ * // 清理(在应用退出时调用)
+ * dictSync.cleanup();
+ * ```
+ */
+export function useDictSync() {
+ if (!singletonInstance) {
+ singletonInstance = createDictSyncComposable();
+ }
+ return singletonInstance;
+}
diff --git a/src/composables/websocket/useOnlineCount.ts b/src/composables/websocket/useOnlineCount.ts
new file mode 100644
index 0000000..a9b5fe7
--- /dev/null
+++ b/src/composables/websocket/useOnlineCount.ts
@@ -0,0 +1,217 @@
+import { ref, watch, onMounted, onUnmounted, getCurrentInstance } from "vue";
+import { useStomp } from "./useStomp";
+import { registerWebSocketInstance } from "@/plugins/websocket";
+import { AuthStorage } from "@/utils/auth";
+
+/**
+ * 在线用户数量消息结构
+ */
+interface OnlineCountMessage {
+ count?: number;
+ timestamp?: number;
+}
+
+/**
+ * 全局单例实例
+ */
+let globalInstance: ReturnType | null = null;
+
+/**
+ * 创建在线用户计数组合式函数(内部工厂函数)
+ */
+function createOnlineCountComposable() {
+ // ==================== 状态管理 ====================
+ const onlineUserCount = ref(0);
+ const lastUpdateTime = ref(0);
+
+ // ==================== WebSocket 客户端 ====================
+ const stomp = useStomp({
+ reconnectDelay: 15000,
+ maxReconnectAttempts: 3,
+ connectionTimeout: 10000,
+ useExponentialBackoff: true,
+ autoRestoreSubscriptions: true, // 自动恢复订阅
+ debug: false,
+ });
+
+ // 在线用户计数主题
+ const ONLINE_COUNT_TOPIC = "/topic/online-count";
+
+ // 订阅 ID
+ let subscriptionId: string | null = null;
+
+ // 注册到全局实例管理器
+ registerWebSocketInstance("onlineCount", stomp);
+
+ /**
+ * 处理在线用户数量消息
+ */
+ const handleOnlineCountMessage = (message: any) => {
+ try {
+ const data = message.body;
+ const jsonData = JSON.parse(data) as OnlineCountMessage;
+
+ // 支持两种消息格式
+ // 1. 直接是数字: 42
+ // 2. 对象格式: { count: 42, timestamp: 1234567890 }
+ const count = typeof jsonData === "number" ? jsonData : jsonData.count;
+
+ if (count !== undefined && !isNaN(count)) {
+ onlineUserCount.value = count;
+ lastUpdateTime.value = Date.now();
+ console.log(`[useOnlineCount] 在线用户数更新: ${count}`);
+ } else {
+ console.warn("[useOnlineCount] 收到无效的在线用户数:", data);
+ }
+ } catch (error) {
+ console.error("[useOnlineCount] 解析在线用户数失败:", error);
+ }
+ };
+
+ /**
+ * 订阅在线用户计数主题
+ */
+ const subscribeToOnlineCount = () => {
+ if (subscriptionId) {
+ console.log("[useOnlineCount] 已存在订阅,跳过");
+ return;
+ }
+
+ // 订阅在线用户计数主题(useStomp 会处理重连后的订阅恢复)
+ subscriptionId = stomp.subscribe(ONLINE_COUNT_TOPIC, handleOnlineCountMessage);
+
+ if (subscriptionId) {
+ console.log(`[useOnlineCount] 已订阅主题: ${ONLINE_COUNT_TOPIC}`);
+ } else {
+ console.log(`[useOnlineCount] 暂存订阅配置,等待连接建立后自动订阅`);
+ }
+ };
+
+ /**
+ * 初始化 WebSocket 连接并订阅在线用户主题
+ */
+ const initialize = () => {
+ // 检查 WebSocket 端点是否配置
+ const wsEndpoint = import.meta.env.VITE_APP_WS_ENDPOINT;
+ if (!wsEndpoint) {
+ console.log("[useOnlineCount] 未配置 WebSocket 端点,跳过初始化");
+ return;
+ }
+
+ // 检查令牌有效性
+ const accessToken = AuthStorage.getAccessToken();
+ if (!accessToken) {
+ console.log("[useOnlineCount] 未检测到有效令牌,跳过初始化");
+ return;
+ }
+
+ console.log("[useOnlineCount] 初始化在线用户计数服务...");
+
+ // 建立 WebSocket 连接
+ stomp.connect();
+
+ // 订阅主题
+ subscribeToOnlineCount();
+ };
+
+ /**
+ * 关闭 WebSocket 连接并清理资源
+ */
+ const cleanup = () => {
+ console.log("[useOnlineCount] 清理在线用户计数服务...");
+
+ // 取消订阅
+ if (subscriptionId) {
+ stomp.unsubscribe(subscriptionId);
+ subscriptionId = null;
+ }
+
+ // 也可以通过主题地址取消订阅
+ stomp.unsubscribeDestination(ONLINE_COUNT_TOPIC);
+
+ // 断开连接
+ stomp.disconnect();
+
+ // 重置状态
+ onlineUserCount.value = 0;
+ lastUpdateTime.value = 0;
+ };
+
+ // 监听连接状态变化
+ watch(
+ stomp.isConnected,
+ (connected) => {
+ if (connected) {
+ console.log("[useOnlineCount] WebSocket 已连接");
+ } else {
+ console.log("[useOnlineCount] WebSocket 已断开");
+ }
+ },
+ { immediate: false }
+ );
+
+ return {
+ // 状态
+ onlineUserCount: readonly(onlineUserCount),
+ lastUpdateTime: readonly(lastUpdateTime),
+ isConnected: stomp.isConnected,
+ connectionState: stomp.connectionState,
+
+ // 方法
+ initialize,
+ cleanup,
+
+ // 别名方法(向后兼容)
+ initWebSocket: initialize,
+ closeWebSocket: cleanup,
+ };
+}
+
+/**
+ * 在线用户计数组合式函数(单例模式)
+ *
+ * 用于实时显示系统在线用户数量
+ *
+ * @param options 配置选项
+ * @param options.autoInit 是否在组件挂载时自动初始化(默认 true)
+ *
+ * @example
+ * ```ts
+ * // 在组件中使用
+ * const { onlineUserCount, isConnected } = useOnlineCount();
+ *
+ * // 手动控制初始化
+ * const { onlineUserCount, initialize, cleanup } = useOnlineCount({ autoInit: false });
+ * onMounted(() => initialize());
+ * onUnmounted(() => cleanup());
+ * ```
+ */
+export function useOnlineCount(options: { autoInit?: boolean } = {}) {
+ const { autoInit = true } = options;
+
+ // 获取或创建单例实例
+ if (!globalInstance) {
+ globalInstance = createOnlineCountComposable();
+ }
+
+ // 只在组件上下文中且 autoInit 为 true 时使用生命周期钩子
+ const instance = getCurrentInstance();
+ if (autoInit && instance) {
+ onMounted(() => {
+ // 只有在未连接时才尝试初始化
+ if (!globalInstance!.isConnected.value) {
+ console.log("[useOnlineCount] 组件挂载,初始化 WebSocket 连接");
+ globalInstance!.initialize();
+ } else {
+ console.log("[useOnlineCount] WebSocket 已连接,跳过初始化");
+ }
+ });
+
+ // 注意:不在卸载时关闭连接,保持全局连接
+ onUnmounted(() => {
+ console.log("[useOnlineCount] 组件卸载(保持 WebSocket 连接)");
+ });
+ }
+
+ return globalInstance;
+}
diff --git a/src/composables/websocket/useStomp.ts b/src/composables/websocket/useStomp.ts
new file mode 100644
index 0000000..88f42a3
--- /dev/null
+++ b/src/composables/websocket/useStomp.ts
@@ -0,0 +1,530 @@
+import { Client, type IMessage, type StompSubscription } from "@stomp/stompjs";
+import { AuthStorage } from "@/utils/auth";
+
+export interface UseStompOptions {
+ /** WebSocket 地址,不传时使用 VITE_APP_WS_ENDPOINT 环境变量 */
+ brokerURL?: string;
+ /** 用于鉴权的 token,不传时使用 getAccessToken() 的返回值 */
+ token?: string;
+ /** 重连延迟,单位毫秒,默认为 15000 */
+ reconnectDelay?: number;
+ /** 连接超时时间,单位毫秒,默认为 10000 */
+ connectionTimeout?: number;
+ /** 是否开启指数退避重连策略 */
+ useExponentialBackoff?: boolean;
+ /** 最大重连次数,默认为 3 */
+ maxReconnectAttempts?: number;
+ /** 最大重连延迟,单位毫秒,默认为 60000 */
+ maxReconnectDelay?: number;
+ /** 是否开启调试日志 */
+ debug?: boolean;
+ /** 是否在重连时自动恢复订阅,默认为 true */
+ autoRestoreSubscriptions?: boolean;
+}
+
+/**
+ * 订阅配置信息
+ */
+interface SubscriptionConfig {
+ destination: string;
+ callback: (message: IMessage) => void;
+}
+
+/**
+ * 连接状态枚举
+ */
+enum ConnectionState {
+ DISCONNECTED = "DISCONNECTED",
+ CONNECTING = "CONNECTING",
+ CONNECTED = "CONNECTED",
+ RECONNECTING = "RECONNECTING",
+}
+
+/**
+ * STOMP WebSocket 连接管理组合式函数
+ *
+ * 核心功能:
+ * - 自动连接管理(连接、断开、重连)
+ * - 订阅管理(订阅、取消订阅、自动恢复)
+ * - 心跳检测
+ * - Token 自动刷新
+ *
+ * @param options 配置选项
+ * @returns STOMP 客户端操作接口
+ */
+export function useStomp(options: UseStompOptions = {}) {
+ // ==================== 配置初始化 ====================
+ const defaultBrokerURL = import.meta.env.VITE_APP_WS_ENDPOINT || "";
+
+ const config = {
+ brokerURL: ref(options.brokerURL ?? defaultBrokerURL),
+ reconnectDelay: options.reconnectDelay ?? 15000,
+ connectionTimeout: options.connectionTimeout ?? 10000,
+ useExponentialBackoff: options.useExponentialBackoff ?? false,
+ maxReconnectAttempts: options.maxReconnectAttempts ?? 3,
+ maxReconnectDelay: options.maxReconnectDelay ?? 60000,
+ autoRestoreSubscriptions: options.autoRestoreSubscriptions ?? true,
+ debug: options.debug ?? false,
+ };
+
+ // ==================== 状态管理 ====================
+ const connectionState = ref(ConnectionState.DISCONNECTED);
+ const isConnected = computed(() => connectionState.value === ConnectionState.CONNECTED);
+ const reconnectAttempts = ref(0);
+
+ // ==================== 定时器管理 ====================
+ let reconnectTimer: ReturnType | null = null;
+ let connectionTimeoutTimer: ReturnType | null = null;
+
+ // ==================== 订阅管理 ====================
+ // 活动订阅:存储当前 STOMP 订阅对象
+ const activeSubscriptions = new Map();
+ // 订阅配置注册表:用于自动恢复订阅
+ const subscriptionRegistry = new Map();
+
+ // ==================== 客户端实例 ====================
+ const stompClient = ref(null);
+ let isManualDisconnect = false;
+
+ // ==================== 工具函数 ====================
+
+ /**
+ * 清理所有定时器
+ */
+ const clearAllTimers = () => {
+ if (reconnectTimer) {
+ clearTimeout(reconnectTimer);
+ reconnectTimer = null;
+ }
+ if (connectionTimeoutTimer) {
+ clearTimeout(connectionTimeoutTimer);
+ connectionTimeoutTimer = null;
+ }
+ };
+
+ /**
+ * 日志输出(支持调试模式控制)
+ */
+ const log = (...args: any[]) => {
+ if (config.debug) {
+ console.log("[useStomp]", ...args);
+ }
+ };
+
+ const logWarn = (...args: any[]) => {
+ console.warn("[useStomp]", ...args);
+ };
+
+ const logError = (...args: any[]) => {
+ console.error("[useStomp]", ...args);
+ };
+
+ /**
+ * 恢复所有订阅
+ */
+ const restoreSubscriptions = () => {
+ if (!config.autoRestoreSubscriptions || subscriptionRegistry.size === 0) {
+ return;
+ }
+
+ log(`开始恢复 ${subscriptionRegistry.size} 个订阅...`);
+
+ for (const [destination, subscriptionConfig] of subscriptionRegistry.entries()) {
+ try {
+ performSubscribe(destination, subscriptionConfig.callback);
+ } catch (error) {
+ logError(`恢复订阅 ${destination} 失败:`, error);
+ }
+ }
+ };
+
+ /**
+ * 初始化 STOMP 客户端
+ */
+ const initializeClient = () => {
+ // 如果客户端已存在且处于活动状态,直接返回
+ if (stompClient.value && (stompClient.value.active || stompClient.value.connected)) {
+ log("STOMP 客户端已存在且处于活动状态,跳过初始化");
+ return;
+ }
+
+ // 检查 WebSocket 端点是否配置
+ if (!config.brokerURL.value) {
+ logWarn("WebSocket 连接失败: 未配置 WebSocket 端点 URL");
+ return;
+ }
+
+ // 每次连接前重新获取最新令牌
+ const accessToken = AuthStorage.getAccessToken();
+ if (!accessToken) {
+ logWarn("WebSocket 连接失败:授权令牌为空,请先登录");
+ return;
+ }
+
+ // 清理旧客户端
+ if (stompClient.value) {
+ try {
+ stompClient.value.deactivate();
+ } catch (error) {
+ logWarn("清理旧客户端时出错:", error);
+ }
+ stompClient.value = null;
+ }
+
+ // 创建 STOMP 客户端
+ stompClient.value = new Client({
+ brokerURL: config.brokerURL.value,
+ connectHeaders: {
+ Authorization: `Bearer ${accessToken}`,
+ },
+ debug: config.debug ? (msg) => console.log("[STOMP]", msg) : () => {},
+ reconnectDelay: 0, // 禁用内置重连,使用自定义重连逻辑
+ heartbeatIncoming: 4000,
+ heartbeatOutgoing: 4000,
+ });
+
+ // ==================== 事件监听器 ====================
+
+ // 连接成功
+ stompClient.value.onConnect = () => {
+ connectionState.value = ConnectionState.CONNECTED;
+ reconnectAttempts.value = 0;
+ clearAllTimers();
+
+ log("✅ WebSocket 连接已建立");
+
+ // 自动恢复订阅
+ restoreSubscriptions();
+ };
+
+ // 连接断开
+ stompClient.value.onDisconnect = () => {
+ connectionState.value = ConnectionState.DISCONNECTED;
+ log("❌ WebSocket 连接已断开");
+
+ // 清空活动订阅(但保留订阅配置用于恢复)
+ activeSubscriptions.clear();
+
+ // 如果不是手动断开且未达到最大重连次数,则尝试重连
+ if (!isManualDisconnect && reconnectAttempts.value < config.maxReconnectAttempts) {
+ scheduleReconnect();
+ }
+ };
+
+ // WebSocket 关闭
+ stompClient.value.onWebSocketClose = (event) => {
+ connectionState.value = ConnectionState.DISCONNECTED;
+ log(`WebSocket 已关闭: code=${event?.code}, reason=${event?.reason}`);
+
+ // 如果是手动断开,不重连
+ if (isManualDisconnect) {
+ log("手动断开连接,不进行重连");
+ return;
+ }
+
+ // 对于异常关闭,尝试重连
+ if (
+ event?.code &&
+ [1000, 1006, 1008, 1011].includes(event.code) &&
+ reconnectAttempts.value < config.maxReconnectAttempts
+ ) {
+ log("检测到连接异常关闭,将尝试重连");
+ scheduleReconnect();
+ }
+ };
+
+ // STOMP 错误
+ stompClient.value.onStompError = (frame) => {
+ logError("STOMP 错误:", frame.headers, frame.body);
+ connectionState.value = ConnectionState.DISCONNECTED;
+
+ // 检查是否是授权错误
+ const isAuthError =
+ frame.headers?.message?.includes("Unauthorized") ||
+ frame.body?.includes("Unauthorized") ||
+ frame.body?.includes("Token") ||
+ frame.body?.includes("401");
+
+ if (isAuthError) {
+ logWarn("WebSocket 授权错误,停止重连");
+ isManualDisconnect = true; // 授权错误不进行重连
+ }
+ };
+ };
+
+ /**
+ * 调度重连任务
+ */
+ const scheduleReconnect = () => {
+ // 如果正在连接或手动断开,不重连
+ if (connectionState.value === ConnectionState.CONNECTING || isManualDisconnect) {
+ return;
+ }
+
+ // 检查是否达到最大重连次数
+ if (reconnectAttempts.value >= config.maxReconnectAttempts) {
+ logError(`已达到最大重连次数 (${config.maxReconnectAttempts}),停止重连`);
+ return;
+ }
+
+ reconnectAttempts.value++;
+ connectionState.value = ConnectionState.RECONNECTING;
+
+ // 计算重连延迟(支持指数退避)
+ const delay = config.useExponentialBackoff
+ ? Math.min(
+ config.reconnectDelay * Math.pow(2, reconnectAttempts.value - 1),
+ config.maxReconnectDelay
+ )
+ : config.reconnectDelay;
+
+ log(`准备重连 (${reconnectAttempts.value}/${config.maxReconnectAttempts}),延迟 ${delay}ms`);
+
+ // 清除之前的重连计时器
+ if (reconnectTimer) {
+ clearTimeout(reconnectTimer);
+ }
+
+ // 设置重连计时器
+ reconnectTimer = setTimeout(() => {
+ if (connectionState.value !== ConnectionState.CONNECTED && !isManualDisconnect) {
+ log(`开始第 ${reconnectAttempts.value} 次重连...`);
+ connect();
+ }
+ }, delay);
+ };
+
+ // 监听 brokerURL 的变化,自动重新初始化
+ watch(config.brokerURL, (newURL, oldURL) => {
+ if (newURL !== oldURL) {
+ log(`WebSocket 端点已更改: ${oldURL} -> ${newURL}`);
+
+ // 断开当前连接
+ if (stompClient.value && stompClient.value.connected) {
+ stompClient.value.deactivate();
+ }
+
+ // 重新初始化客户端
+ initializeClient();
+ }
+ });
+
+ // 初始化客户端
+ initializeClient();
+
+ // ==================== 公共接口 ====================
+
+ /**
+ * 建立 WebSocket 连接
+ */
+ const connect = () => {
+ // 重置手动断开标志
+ isManualDisconnect = false;
+
+ // 检查是否配置了 WebSocket 端点
+ if (!config.brokerURL.value) {
+ logError("WebSocket 连接失败: 未配置 WebSocket 端点 URL");
+ return;
+ }
+
+ // 防止重复连接
+ if (connectionState.value === ConnectionState.CONNECTING) {
+ log("WebSocket 正在连接中,跳过重复连接请求");
+ return;
+ }
+
+ // 如果客户端不存在,先初始化
+ if (!stompClient.value) {
+ initializeClient();
+ }
+
+ if (!stompClient.value) {
+ logError("STOMP 客户端初始化失败");
+ return;
+ }
+
+ // 避免重复连接:检查是否已连接
+ if (stompClient.value.connected) {
+ log("WebSocket 已连接,跳过重复连接");
+ connectionState.value = ConnectionState.CONNECTED;
+ return;
+ }
+
+ // 设置连接状态
+ connectionState.value = ConnectionState.CONNECTING;
+
+ // 设置连接超时
+ if (connectionTimeoutTimer) {
+ clearTimeout(connectionTimeoutTimer);
+ }
+
+ connectionTimeoutTimer = setTimeout(() => {
+ if (connectionState.value === ConnectionState.CONNECTING) {
+ logWarn("WebSocket 连接超时");
+ connectionState.value = ConnectionState.DISCONNECTED;
+
+ // 超时后尝试重连
+ if (!isManualDisconnect && reconnectAttempts.value < config.maxReconnectAttempts) {
+ scheduleReconnect();
+ }
+ }
+ }, config.connectionTimeout);
+
+ try {
+ stompClient.value.activate();
+ log("正在建立 WebSocket 连接...");
+ } catch (error) {
+ logError("激活 WebSocket 连接失败:", error);
+ connectionState.value = ConnectionState.DISCONNECTED;
+ }
+ };
+
+ /**
+ * 执行订阅操作(内部方法)
+ */
+ const performSubscribe = (destination: string, callback: (message: IMessage) => void): string => {
+ if (!stompClient.value || !stompClient.value.connected) {
+ logWarn(`尝试订阅 ${destination} 失败: 客户端未连接`);
+ return "";
+ }
+
+ try {
+ const subscription = stompClient.value.subscribe(destination, callback);
+ const subscriptionId = subscription.id;
+ activeSubscriptions.set(subscriptionId, subscription);
+ log(`✓ 订阅成功: ${destination} (ID: ${subscriptionId})`);
+ return subscriptionId;
+ } catch (error) {
+ logError(`订阅 ${destination} 失败:`, error);
+ return "";
+ }
+ };
+
+ /**
+ * 订阅指定主题
+ *
+ * @param destination 目标主题地址(如:/topic/message)
+ * @param callback 接收到消息时的回调函数
+ * @returns 订阅 ID,用于后续取消订阅
+ */
+ const subscribe = (destination: string, callback: (message: IMessage) => void): string => {
+ // 保存订阅配置到注册表,用于断线重连后自动恢复
+ subscriptionRegistry.set(destination, { destination, callback });
+
+ // 如果已连接,立即订阅
+ if (stompClient.value?.connected) {
+ return performSubscribe(destination, callback);
+ }
+
+ log(`暂存订阅配置: ${destination},将在连接建立后自动订阅`);
+ return "";
+ };
+
+ /**
+ * 取消订阅
+ *
+ * @param subscriptionId 订阅 ID(由 subscribe 方法返回)
+ */
+ const unsubscribe = (subscriptionId: string) => {
+ const subscription = activeSubscriptions.get(subscriptionId);
+ if (subscription) {
+ try {
+ subscription.unsubscribe();
+ activeSubscriptions.delete(subscriptionId);
+ log(`✓ 已取消订阅: ${subscriptionId}`);
+ } catch (error) {
+ logWarn(`取消订阅 ${subscriptionId} 时出错:`, error);
+ }
+ }
+ };
+
+ /**
+ * 取消指定主题的订阅(从注册表中移除)
+ *
+ * @param destination 主题地址
+ */
+ const unsubscribeDestination = (destination: string) => {
+ // 从注册表中移除
+ subscriptionRegistry.delete(destination);
+
+ // 取消所有匹配该主题的活动订阅
+ for (const [id, subscription] of activeSubscriptions.entries()) {
+ // 注意:STOMP 的 subscription 对象没有直接暴露 destination,
+ // 这里简化处理,实际使用时可能需要额外维护 id -> destination 的映射
+ try {
+ subscription.unsubscribe();
+ activeSubscriptions.delete(id);
+ } catch (error) {
+ logWarn(`取消订阅 ${id} 时出错:`, error);
+ }
+ }
+
+ log(`✓ 已移除主题订阅配置: ${destination}`);
+ };
+
+ /**
+ * 断开 WebSocket 连接
+ *
+ * @param clearSubscriptions 是否清除订阅注册表(默认为 true)
+ */
+ const disconnect = (clearSubscriptions = true) => {
+ // 设置手动断开标志
+ isManualDisconnect = true;
+
+ // 清除所有定时器
+ clearAllTimers();
+
+ // 取消所有活动订阅
+ for (const [id, subscription] of activeSubscriptions.entries()) {
+ try {
+ subscription.unsubscribe();
+ } catch (error) {
+ logWarn(`取消订阅 ${id} 时出错:`, error);
+ }
+ }
+ activeSubscriptions.clear();
+
+ // 可选:清除订阅注册表
+ if (clearSubscriptions) {
+ subscriptionRegistry.clear();
+ log("已清除所有订阅配置");
+ }
+
+ // 断开连接
+ if (stompClient.value) {
+ try {
+ if (stompClient.value.connected || stompClient.value.active) {
+ stompClient.value.deactivate();
+ log("✓ WebSocket 连接已主动断开");
+ }
+ } catch (error) {
+ logError("断开 WebSocket 连接时出错:", error);
+ }
+ stompClient.value = null;
+ }
+
+ connectionState.value = ConnectionState.DISCONNECTED;
+ reconnectAttempts.value = 0;
+ };
+
+ // ==================== 返回公共接口 ====================
+ return {
+ // 状态
+ connectionState: readonly(connectionState),
+ isConnected,
+ reconnectAttempts: readonly(reconnectAttempts),
+
+ // 连接管理
+ connect,
+ disconnect,
+
+ // 订阅管理
+ subscribe,
+ unsubscribe,
+ unsubscribeDestination,
+
+ // 统计信息
+ getActiveSubscriptionCount: () => activeSubscriptions.size,
+ getRegisteredSubscriptionCount: () => subscriptionRegistry.size,
+ };
+}
diff --git a/src/constants/index.ts b/src/constants/index.ts
new file mode 100644
index 0000000..1104d26
--- /dev/null
+++ b/src/constants/index.ts
@@ -0,0 +1,74 @@
+/**
+ * 项目常量统一管理
+ * 存储键命名规范:{prefix}:{namespace}:{key}
+ */
+
+export const APP_PREFIX = "vea";
+
+export const STORAGE_KEYS = {
+ // 用户认证相关
+ ACCESS_TOKEN: `${APP_PREFIX}:auth:access_token`, // JWT访问令牌
+ REFRESH_TOKEN: `${APP_PREFIX}:auth:refresh_token`, // JWT刷新令牌
+ REMEMBER_ME: `${APP_PREFIX}:auth:remember_me`, // 记住登录状态
+
+ // 系统核心相关
+ DICT_CACHE: `${APP_PREFIX}:system:dict_cache`, // 字典数据缓存
+
+ // UI设置相关
+ SHOW_TAGS_VIEW: `${APP_PREFIX}:ui:show_tags_view`, // 显示标签页视图
+ SHOW_APP_LOGO: `${APP_PREFIX}:ui:show_app_logo`, // 显示应用Logo
+ SHOW_WATERMARK: `${APP_PREFIX}:ui:show_watermark`, // 显示水印
+ ENABLE_AI_ASSISTANT: `${APP_PREFIX}:ui:enable_ai_assistant`, // 启用 AI 助手
+ LAYOUT: `${APP_PREFIX}:ui:layout`, // 布局模式
+ SIDEBAR_COLOR_SCHEME: `${APP_PREFIX}:ui:sidebar_color_scheme`, // 侧边栏配色方案
+ THEME: `${APP_PREFIX}:ui:theme`, // 主题模式
+ THEME_COLOR: `${APP_PREFIX}:ui:theme_color`, // 主题色
+
+ // 应用状态相关
+ DEVICE: `${APP_PREFIX}:app:device`, // 设备类型
+ SIZE: `${APP_PREFIX}:app:size`, // 屏幕尺寸
+ LANGUAGE: `${APP_PREFIX}:app:language`, // 应用语言
+ SIDEBAR_STATUS: `${APP_PREFIX}:app:sidebar_status`, // 侧边栏状态
+ ACTIVE_TOP_MENU_PATH: `${APP_PREFIX}:app:active_top_menu_path`, // 当前激活的顶部菜单路径
+} as const;
+
+export const ROLE_ROOT = "ROOT"; // 超级管理员角色
+
+// 分组键集合(便于批量操作)
+export const AUTH_KEYS = {
+ ACCESS_TOKEN: STORAGE_KEYS.ACCESS_TOKEN,
+ REFRESH_TOKEN: STORAGE_KEYS.REFRESH_TOKEN,
+ REMEMBER_ME: STORAGE_KEYS.REMEMBER_ME,
+} as const;
+
+export const SYSTEM_KEYS = {
+ DICT_CACHE: STORAGE_KEYS.DICT_CACHE,
+} as const;
+
+export const SETTINGS_KEYS = {
+ SHOW_TAGS_VIEW: STORAGE_KEYS.SHOW_TAGS_VIEW,
+ SHOW_APP_LOGO: STORAGE_KEYS.SHOW_APP_LOGO,
+ SHOW_WATERMARK: STORAGE_KEYS.SHOW_WATERMARK,
+ ENABLE_AI_ASSISTANT: STORAGE_KEYS.ENABLE_AI_ASSISTANT,
+ SIDEBAR_COLOR_SCHEME: STORAGE_KEYS.SIDEBAR_COLOR_SCHEME,
+ LAYOUT: STORAGE_KEYS.LAYOUT,
+ THEME_COLOR: STORAGE_KEYS.THEME_COLOR,
+ THEME: STORAGE_KEYS.THEME,
+} as const;
+
+export const APP_KEYS = {
+ DEVICE: STORAGE_KEYS.DEVICE,
+ SIZE: STORAGE_KEYS.SIZE,
+ LANGUAGE: STORAGE_KEYS.LANGUAGE,
+ SIDEBAR_STATUS: STORAGE_KEYS.SIDEBAR_STATUS,
+ ACTIVE_TOP_MENU_PATH: STORAGE_KEYS.ACTIVE_TOP_MENU_PATH,
+} as const;
+
+export const ALL_STORAGE_KEYS = {
+ ...AUTH_KEYS,
+ ...SYSTEM_KEYS,
+ ...SETTINGS_KEYS,
+ ...APP_KEYS,
+} as const;
+
+export type StorageKey = (typeof STORAGE_KEYS)[keyof typeof STORAGE_KEYS];
diff --git a/src/directives/index.ts b/src/directives/index.ts
new file mode 100644
index 0000000..a2ce179
--- /dev/null
+++ b/src/directives/index.ts
@@ -0,0 +1,9 @@
+import type { App } from "vue";
+import { hasPerm, hasRole } from "./permission";
+
+// 注册指令
+export function setupDirective(app: App) {
+ // 权限指令
+ app.directive("hasPerm", hasPerm);
+ app.directive("hasRole", hasRole);
+}
diff --git a/src/directives/permission/index.ts b/src/directives/permission/index.ts
new file mode 100644
index 0000000..b30fff4
--- /dev/null
+++ b/src/directives/permission/index.ts
@@ -0,0 +1,45 @@
+import type { Directive, DirectiveBinding } from "vue";
+import { useUserStoreHook } from "@/store/modules/user-store";
+import { hasPerm as checkPermission } from "@/utils/auth";
+
+/**
+ * 按钮权限指令
+ *
+ * @example
+ * 添加用户
+ * 删除用户
+ */
+export const hasPerm: Directive = {
+ mounted(el: HTMLElement, binding: DirectiveBinding) {
+ // 获取权限值
+ const { value: requiredPerm } = binding;
+
+ // 验证权限
+ if (!checkPermission(requiredPerm, "button")) {
+ // 移除元素
+ el.parentNode?.removeChild(el);
+ }
+ },
+};
+
+/**
+ * 角色权限指令
+ *
+ * @example
+ * 管理员按钮
+ * 编辑按钮
+ */
+export const hasRole: Directive = {
+ mounted(el: HTMLElement, binding: DirectiveBinding) {
+ // 获取角色值
+ const { value: requiredRole } = binding;
+
+ // 验证角色
+ if (!checkPermission(requiredRole, "role")) {
+ // 移除元素
+ el.parentNode?.removeChild(el);
+ }
+ },
+};
+
+export default hasPerm;
diff --git a/src/enums/api/code-enum.ts b/src/enums/api/code-enum.ts
new file mode 100644
index 0000000..6afe590
--- /dev/null
+++ b/src/enums/api/code-enum.ts
@@ -0,0 +1,23 @@
+/**
+ * API响应码枚举
+ */
+export const enum ApiCodeEnum {
+ /**
+ * 成功
+ */
+ SUCCESS = "00000",
+ /**
+ * 错误
+ */
+ ERROR = "B0001",
+
+ /**
+ * 访问令牌无效或过期
+ */
+ ACCESS_TOKEN_INVALID = "A0230",
+
+ /**
+ * 刷新令牌无效或过期
+ */
+ REFRESH_TOKEN_INVALID = "A0231",
+}
diff --git a/src/enums/codegen/form-enum.ts b/src/enums/codegen/form-enum.ts
new file mode 100644
index 0000000..96c5462
--- /dev/null
+++ b/src/enums/codegen/form-enum.ts
@@ -0,0 +1,15 @@
+/**
+ * 表单类型枚举
+ */
+export const FormTypeEnum: Record = {
+ INPUT: { value: 1, label: "输入框" },
+ SELECT: { value: 2, label: "下拉框" },
+ RADIO: { value: 3, label: "单选框" },
+ CHECK_BOX: { value: 4, label: "复选框" },
+ INPUT_NUMBER: { value: 5, label: "数字输入框" },
+ SWITCH: { value: 6, label: "开关" },
+ TEXT_AREA: { value: 7, label: "文本域" },
+ DATE: { value: 8, label: "日期框" },
+ DATE_TIME: { value: 9, label: "日期时间框" },
+ HIDDEN: { value: 10, label: "隐藏域" },
+};
diff --git a/src/enums/codegen/query-enum.ts b/src/enums/codegen/query-enum.ts
new file mode 100644
index 0000000..253efc0
--- /dev/null
+++ b/src/enums/codegen/query-enum.ts
@@ -0,0 +1,37 @@
+/**
+ * 查询类型枚举
+ */
+export const QueryTypeEnum: Record = {
+ /** 等于 */
+ EQ: { value: 1, label: "=" },
+
+ /** 模糊匹配 */
+ LIKE: { value: 2, label: "LIKE '%s%'" },
+
+ /** 包含 */
+ IN: { value: 3, label: "IN" },
+
+ /** 范围 */
+ BETWEEN: { value: 4, label: "BETWEEN" },
+
+ /** 大于 */
+ GT: { value: 5, label: ">" },
+
+ /** 大于等于 */
+ GE: { value: 6, label: ">=" },
+
+ /** 小于 */
+ LT: { value: 7, label: "<" },
+
+ /** 小于等于 */
+ LE: { value: 8, label: "<=" },
+
+ /** 不等于 */
+ NE: { value: 9, label: "!=" },
+
+ /** 左模糊匹配 */
+ LIKE_LEFT: { value: 10, label: "LIKE '%s'" },
+
+ /** 右模糊匹配 */
+ LIKE_RIGHT: { value: 11, label: "LIKE 's%'" },
+};
diff --git a/src/enums/index.ts b/src/enums/index.ts
new file mode 100644
index 0000000..7918929
--- /dev/null
+++ b/src/enums/index.ts
@@ -0,0 +1,11 @@
+export * from "./api/code-enum";
+
+export * from "./codegen/form-enum";
+export * from "./codegen/query-enum";
+
+export * from "./settings/layout-enum";
+export * from "./settings/theme-enum";
+export * from "./settings/locale-enum";
+export * from "./settings/device-enum";
+
+export * from "./system/menu-enum";
diff --git a/src/enums/settings/device-enum.ts b/src/enums/settings/device-enum.ts
new file mode 100644
index 0000000..709bcb3
--- /dev/null
+++ b/src/enums/settings/device-enum.ts
@@ -0,0 +1,14 @@
+/**
+ * 设备枚举
+ */
+export const enum DeviceEnum {
+ /**
+ * 宽屏设备
+ */
+ DESKTOP = "desktop",
+
+ /**
+ * 窄屏设备
+ */
+ MOBILE = "mobile",
+}
diff --git a/src/enums/settings/layout-enum.ts b/src/enums/settings/layout-enum.ts
new file mode 100644
index 0000000..e1e4406
--- /dev/null
+++ b/src/enums/settings/layout-enum.ts
@@ -0,0 +1,53 @@
+/**
+ * 菜单布局枚举
+ */
+export const enum LayoutMode {
+ /**
+ * 左侧菜单布局
+ */
+ LEFT = "left",
+ /**
+ * 顶部菜单布局
+ */
+ TOP = "top",
+
+ /**
+ * 混合菜单布局
+ */
+ MIX = "mix",
+}
+
+/**
+ * 侧边栏状态枚举
+ */
+export const enum SidebarStatus {
+ /**
+ * 展开
+ */
+ OPENED = "opened",
+
+ /**
+ * 关闭
+ */
+ CLOSED = "closed",
+}
+
+/**
+ * 组件尺寸枚举
+ */
+export const enum ComponentSize {
+ /**
+ * 默认
+ */
+ DEFAULT = "default",
+
+ /**
+ * 大型
+ */
+ LARGE = "large",
+
+ /**
+ * 小型
+ */
+ SMALL = "small",
+}
diff --git a/src/enums/settings/locale-enum.ts b/src/enums/settings/locale-enum.ts
new file mode 100644
index 0000000..a50b655
--- /dev/null
+++ b/src/enums/settings/locale-enum.ts
@@ -0,0 +1,14 @@
+/**
+ * 语言枚举
+ */
+export const enum LanguageEnum {
+ /**
+ * 中文
+ */
+ ZH_CN = "zh-cn",
+
+ /**
+ * 英文
+ */
+ EN = "en",
+}
diff --git a/src/enums/settings/theme-enum.ts b/src/enums/settings/theme-enum.ts
new file mode 100644
index 0000000..21799c3
--- /dev/null
+++ b/src/enums/settings/theme-enum.ts
@@ -0,0 +1,32 @@
+/**
+ * 主题枚举
+ */
+export const enum ThemeMode {
+ /**
+ * 明亮主题
+ */
+ LIGHT = "light",
+ /**
+ * 暗黑主题
+ */
+ DARK = "dark",
+
+ /**
+ * 系统自动
+ */
+ AUTO = "auto",
+}
+
+/**
+ * 侧边栏配色方案枚举
+ */
+export const enum SidebarColor {
+ /**
+ * 经典蓝
+ */
+ CLASSIC_BLUE = "classic-blue",
+ /**
+ * 极简白
+ */
+ MINIMAL_WHITE = "minimal-white",
+}
diff --git a/src/enums/system/menu-enum.ts b/src/enums/system/menu-enum.ts
new file mode 100644
index 0000000..3ed2f37
--- /dev/null
+++ b/src/enums/system/menu-enum.ts
@@ -0,0 +1,7 @@
+// 核心枚举定义
+export enum MenuTypeEnum {
+ CATALOG = 2, // 目录
+ MENU = 1, // 菜单
+ BUTTON = 4, // 按钮
+ EXTLINK = 3, // 外链
+}
diff --git a/src/env.d.ts b/src/env.d.ts
new file mode 100644
index 0000000..d27eb5a
--- /dev/null
+++ b/src/env.d.ts
@@ -0,0 +1,8 @@
+///
+
+declare module '*.vue' {
+ import { DefineComponent } from 'vue'
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
+ const component: DefineComponent<{}, {}, any>
+ export default component
+}
diff --git a/src/lang/index.ts b/src/lang/index.ts
new file mode 100644
index 0000000..436f123
--- /dev/null
+++ b/src/lang/index.ts
@@ -0,0 +1,27 @@
+import type { App } from "vue";
+import { createI18n } from "vue-i18n";
+import { useAppStoreHook } from "@/store/modules/app-store";
+// 本地语言包
+import enLocale from "./package/en.json";
+import zhCnLocale from "./package/zh-cn.json";
+
+const appStore = useAppStoreHook();
+
+const messages = {
+ "zh-cn": zhCnLocale,
+ en: enLocale,
+};
+
+const i18n = createI18n({
+ legacy: false,
+ locale: appStore.language,
+ messages,
+ globalInjection: true,
+});
+
+// 全局注册 i18n
+export function setupI18n(app: App) {
+ app.use(i18n);
+}
+
+export default i18n;
diff --git a/src/lang/package/en.json b/src/lang/package/en.json
new file mode 100644
index 0000000..021e409
--- /dev/null
+++ b/src/lang/package/en.json
@@ -0,0 +1,91 @@
+{
+ "route": {
+ "dashboard": "Dashboard",
+ "document": "Document"
+ },
+ "login": {
+ "themeToggle": "Theme Switch",
+ "languageToggle": "Language Switch",
+ "dark": "Dark",
+ "light": "Light",
+ "username": "Username",
+ "password": "Password",
+ "login": "Login",
+ "captchaCode": "Verify Code",
+ "capsLock": "Caps Lock is On",
+ "rememberMe": "Remember Me",
+ "forgetPassword": "Forget Password?",
+ "message": {
+ "username": {
+ "required": "Please enter Username"
+ },
+ "password": {
+ "required": "Please enter Password",
+ "min": "The password can not be less than 6 digits",
+ "confirm": "Please confirm the password again",
+ "inconformity": "The two password entries are inconsistent"
+ },
+ "captchaCode": {
+ "required": "Please enter Verify Code"
+ }
+ },
+ "otherLoginMethods": "Other",
+ "resetPassword": "Reset password",
+ "thinkOfPasswd": "Remember your password?",
+ "register": "Register account",
+ "agree": "I have read and agree to the",
+ "userAgreement": "User Agreement",
+ "haveAccount": "Already have an account?",
+ "noAccount": "Don't have an account?",
+ "quickFill": "Quick fill",
+ "reg": "Register"
+ },
+ "navbar": {
+ "dashboard": "Dashboard",
+ "logout": "Logout",
+ "document": "Document",
+ "gitee": "Gitee",
+ "profile": "User Profile"
+ },
+ "sizeSelect": {
+ "tooltip": "Layout Size",
+ "default": "Default",
+ "large": "Large",
+ "small": "Small",
+ "message": {
+ "success": "Switch Layout Size Successful!"
+ }
+ },
+ "langSelect": {
+ "message": {
+ "success": "Switch Language Successful!"
+ }
+ },
+ "settings": {
+ "project": "Project Settings",
+ "theme": "Theme",
+ "interface": "Interface",
+ "navigation": "Navigation",
+ "themeColor": "Theme Color",
+ "showTagsView": "Show Tags View",
+ "showAppLogo": "Show App Logo",
+ "sidebarColorScheme": "Sidebar Color Scheme",
+ "showWatermark": "Show Watermark",
+ "classicBlue": "Classic Blue",
+ "minimalWhite": "Minimal White",
+ "copyConfig": "Copy Config",
+ "resetConfig": "Reset Default",
+ "copySuccess": "Configuration copied to clipboard",
+ "resetSuccess": "Reset to default configuration",
+ "copyDescription": "Copy config will generate current settings code, reset will restore all settings to default",
+ "confirmReset": "Are you sure to reset all settings to default? This operation cannot be undone.",
+ "applyToFile": "Apply to File",
+ "onlyCopy": "Only Copy",
+ "leftLayout": "Left Mode",
+ "topLayout": "Top Mode",
+ "mixLayout": "Mix Mode",
+ "configManagement": "Config Management",
+ "copyConfigDescription": "Generate current settings code and copy to clipboard, then overwrite src/settings.ts file",
+ "resetConfigDescription": "Restore all settings to system default values"
+ }
+}
diff --git a/src/lang/package/zh-cn.json b/src/lang/package/zh-cn.json
new file mode 100644
index 0000000..3346ce0
--- /dev/null
+++ b/src/lang/package/zh-cn.json
@@ -0,0 +1,94 @@
+{
+ "route": {
+ "dashboard": "首页",
+ "document": "项目文档"
+ },
+ "login": {
+ "themeToggle": "主题切换",
+ "languageToggle": "语言切换",
+ "dark": "暗黑",
+ "light": "明亮",
+ "username": "用户名",
+ "password": "密码",
+ "login": "登 录",
+ "captchaCode": "验证码",
+ "capsLock": "大写锁定已打开",
+ "rememberMe": "记住我",
+ "forgetPassword": "忘记密码?",
+ "message": {
+ "username": {
+ "required": "请输入用户名"
+ },
+ "password": {
+ "required": "请输入密码",
+ "min": "密码不能少于6位",
+ "confirm": "请再次确认密码",
+ "inconformity": "两次密码输入不一致"
+ },
+ "captchaCode": {
+ "required": "请输入验证码"
+ }
+ },
+ "otherLoginMethods": "其他",
+ "resetPassword": "重置密码",
+ "thinkOfPasswd": "想起密码?",
+ "register": "注册账号",
+ "agree": "我已同意并阅读",
+ "userAgreement": "用户协议",
+ "haveAccount": "已有账号?",
+ "noAccount": "您没有账号?",
+ "quickFill": "快速填写",
+ "reg": "注 册"
+ },
+ "navbar": {
+ "dashboard": "首页",
+ "logout": "退出登录",
+ "document": "项目文档",
+ "gitee": "项目地址",
+ "profile": "个人中心"
+ },
+ "sizeSelect": {
+ "tooltip": "布局大小",
+ "default": "默认",
+ "large": "大型",
+ "small": "小型",
+ "message": {
+ "success": "切换布局大小成功!"
+ }
+ },
+ "langSelect": {
+ "message": {
+ "success": "切换语言成功!"
+ }
+ },
+ "settings": {
+ "project": "项目配置",
+ "theme": "主题设置",
+ "interface": "界面设置",
+ "navigation": "导航设置",
+ "themeColor": "主题颜色",
+ "themeColorTip": "主题颜色",
+ "darkMode": "暗黑模式",
+ "layoutSetting": "布局设置",
+ "sidebarColorScheme": "侧边栏配色",
+ "showTagsView": "显示页签",
+ "showAppLogo": "显示Logo",
+ "showWatermark": "显示水印",
+ "classicBlue": "经典蓝",
+ "minimalWhite": "极简白",
+ "copyConfig": "复制配置",
+ "resetConfig": "重置默认",
+ "copySuccess": "配置已复制到剪贴板",
+ "resetSuccess": "已重置为默认配置",
+ "copyDescription": "复制配置将生成当前设置的代码,重置将恢复所有设置为默认值",
+ "confirmReset": "确定要重置所有设置为默认值吗?此操作不可恢复。",
+ "applyToFile": "应用到文件",
+ "onlyCopy": "仅复制",
+ "leftLayout": "左侧模式",
+ "topLayout": "顶部模式",
+ "mixLayout": "混合模式",
+ "configManagement": "配置管理",
+ "copyConfigDescription": "生成当前设置的代码并复制到剪贴板,然后覆盖 src/settings.ts 文件",
+ "resetConfigDescription": "恢复所有设置为系统默认值"
+ }
+}
diff --git a/src/layouts/components/AppLogo/index.vue b/src/layouts/components/AppLogo/index.vue
new file mode 100644
index 0000000..112f82c
--- /dev/null
+++ b/src/layouts/components/AppLogo/index.vue
@@ -0,0 +1,72 @@
+
+
+
+
+ 校准后台管理系统
+
+
+
+
+
+
+
+
+
+
diff --git a/src/layouts/components/AppMain/index.vue b/src/layouts/components/AppMain/index.vue
new file mode 100644
index 0000000..2ab669b
--- /dev/null
+++ b/src/layouts/components/AppMain/index.vue
@@ -0,0 +1,91 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/layouts/components/Menu/BasicMenu.vue b/src/layouts/components/Menu/BasicMenu.vue
new file mode 100644
index 0000000..593c2ba
--- /dev/null
+++ b/src/layouts/components/Menu/BasicMenu.vue
@@ -0,0 +1,245 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/layouts/components/Menu/MixTopMenu.vue b/src/layouts/components/Menu/MixTopMenu.vue
new file mode 100644
index 0000000..eda86fa
--- /dev/null
+++ b/src/layouts/components/Menu/MixTopMenu.vue
@@ -0,0 +1,187 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/layouts/components/Menu/components/MenuItem.vue b/src/layouts/components/Menu/components/MenuItem.vue
new file mode 100644
index 0000000..4210d45
--- /dev/null
+++ b/src/layouts/components/Menu/components/MenuItem.vue
@@ -0,0 +1,229 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/layouts/components/Menu/components/MenuItemContent.vue b/src/layouts/components/Menu/components/MenuItemContent.vue
new file mode 100644
index 0000000..979cb72
--- /dev/null
+++ b/src/layouts/components/Menu/components/MenuItemContent.vue
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
diff --git a/src/layouts/components/NavBar/components/NavbarActions.vue b/src/layouts/components/NavBar/components/NavbarActions.vue
new file mode 100644
index 0000000..2ef91ad
--- /dev/null
+++ b/src/layouts/components/NavBar/components/NavbarActions.vue
@@ -0,0 +1,264 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
![]()
+
{{ userStore.userInfo.username }}
+
+
+
+
+ {{ t("navbar.profile") }}
+
+
+ {{ t("navbar.logout") }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/layouts/components/NavBar/index.vue b/src/layouts/components/NavBar/index.vue
new file mode 100644
index 0000000..fd917b1
--- /dev/null
+++ b/src/layouts/components/NavBar/index.vue
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
diff --git a/src/layouts/components/Settings/index.vue b/src/layouts/components/Settings/index.vue
new file mode 100644
index 0000000..db5f9c9
--- /dev/null
+++ b/src/layouts/components/Settings/index.vue
@@ -0,0 +1,562 @@
+
+
+
+
+ {{ t("settings.theme") }}
+
+
+
+
+
+
+
+
+ {{ t("settings.interface") }}
+
+
+ {{ t("settings.themeColor") }}
+
+
+
+
+ {{ t("settings.showTagsView") }}
+
+
+
+
+ {{ t("settings.showAppLogo") }}
+
+
+
+ {{ t("settings.sidebarColorScheme") }}
+
+
+ {{ t("settings.classicBlue") }}
+
+
+ {{ t("settings.minimalWhite") }}
+
+
+
+
+
+
+
+ {{ t("settings.navigation") }}
+
+
+
+
+
+
+
+
+
+
{{ item.label }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ resetLoading ? "重置中..." : t("settings.resetConfig") }}
+
+
+
+
+
+
+
+
+
+
diff --git a/src/layouts/components/TagsView/index.vue b/src/layouts/components/TagsView/index.vue
new file mode 100644
index 0000000..4950fa2
--- /dev/null
+++ b/src/layouts/components/TagsView/index.vue
@@ -0,0 +1,410 @@
+
+
+
+
+
+
+
diff --git a/src/layouts/index.vue b/src/layouts/index.vue
new file mode 100644
index 0000000..19248bf
--- /dev/null
+++ b/src/layouts/index.vue
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/layouts/modes/base/index.vue b/src/layouts/modes/base/index.vue
new file mode 100644
index 0000000..ea9b141
--- /dev/null
+++ b/src/layouts/modes/base/index.vue
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
diff --git a/src/layouts/modes/left/index.vue b/src/layouts/modes/left/index.vue
new file mode 100644
index 0000000..f341d1c
--- /dev/null
+++ b/src/layouts/modes/left/index.vue
@@ -0,0 +1,132 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/layouts/modes/mix/index.vue b/src/layouts/modes/mix/index.vue
new file mode 100644
index 0000000..4f8cb4d
--- /dev/null
+++ b/src/layouts/modes/mix/index.vue
@@ -0,0 +1,281 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/layouts/modes/top/index.vue b/src/layouts/modes/top/index.vue
new file mode 100644
index 0000000..5296774
--- /dev/null
+++ b/src/layouts/modes/top/index.vue
@@ -0,0 +1,136 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main.ts b/src/main.ts
new file mode 100644
index 0000000..d859969
--- /dev/null
+++ b/src/main.ts
@@ -0,0 +1,41 @@
+<<<<<<< HEAD
+import { createApp } from "vue";
+import App from "./App.vue";
+import setupPlugins from "@/plugins";
+
+// 暗黑主题样式
+import "element-plus/theme-chalk/dark/css-vars.css";
+import "vxe-table/lib/style.css";
+// 暗黑模式自定义变量
+import "@/styles/dark/css-vars.css";
+import "@/styles/index.scss";
+import "uno.css";
+
+// 过渡动画
+import "animate.css";
+
+// 自动为某些默认事件(如 touchstart、wheel 等)添加 { passive: true },提升滚动性能并消除控制台的非被动事件监听警告
+import "default-passive-events";
+
+const app = createApp(App);
+// 注册插件
+app.use(setupPlugins);
+app.mount("#app");
+=======
+import { createApp } from 'vue'
+import App from './App.vue'
+import router from "./router";
+import {store,key} from './store'
+
+
+import ElementPlus from 'element-plus'
+import 'element-plus/dist/index.css'
+
+
+const app=createApp(App)
+app
+ .use(router)
+ .use(store,key)
+ .use(ElementPlus)
+ .mount('#app')
+>>>>>>> 232db255 ('首次提交')
diff --git a/src/plugins/icons.ts b/src/plugins/icons.ts
new file mode 100644
index 0000000..fa85ba1
--- /dev/null
+++ b/src/plugins/icons.ts
@@ -0,0 +1,9 @@
+import type { App } from "vue";
+import * as ElementPlusIconsVue from "@element-plus/icons-vue";
+
+// 注册所有图标
+export function setupElIcons(app: App) {
+ for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
+ app.component(key, component);
+ }
+}
diff --git a/src/plugins/index.ts b/src/plugins/index.ts
new file mode 100644
index 0000000..96f32b8
--- /dev/null
+++ b/src/plugins/index.ts
@@ -0,0 +1,34 @@
+import type { App } from "vue";
+
+import { setupDirective } from "@/directives";
+import { setupI18n } from "@/lang";
+import { setupRouter } from "@/router";
+import { setupStore } from "@/store";
+import { setupElIcons } from "./icons";
+import { setupPermission } from "./permission";
+import { setupWebSocket } from "./websocket";
+import { InstallCodeMirror } from "codemirror-editor-vue3";
+import { setupVxeTable } from "./vxeTable";
+
+export default {
+ install(app: App) {
+ // 自定义指令(directive)
+ setupDirective(app);
+ // 路由(router)
+ setupRouter(app);
+ // 状态管理(store)
+ setupStore(app);
+ // 国际化
+ setupI18n(app);
+ // Element-plus图标
+ setupElIcons(app);
+ // 路由守卫
+ setupPermission();
+ // WebSocket服务
+ setupWebSocket();
+ // vxe-table
+ setupVxeTable(app);
+ // 注册 CodeMirror
+ app.use(InstallCodeMirror);
+ },
+};
diff --git a/src/plugins/permission.ts b/src/plugins/permission.ts
new file mode 100644
index 0000000..2821e39
--- /dev/null
+++ b/src/plugins/permission.ts
@@ -0,0 +1,56 @@
+import NProgress from "@/utils/nprogress";
+import router from "@/router";
+import { useUserStore } from "@/store";
+
+export function setupPermission() {
+ const whiteList = ["/login"];
+
+ router.beforeEach(async (to, from, next) => {
+ NProgress.start();
+
+ try {
+ const isLoggedIn = useUserStore().isLoggedIn();
+
+ // 未登录处理
+ if (!isLoggedIn) {
+ if (whiteList.includes(to.path)) {
+ next();
+ } else {
+ next(`/login?redirect=${encodeURIComponent(to.fullPath)}`);
+ NProgress.done();
+ }
+ return;
+ }
+
+ // 已登录登录页重定向
+ if (to.path === "/login") {
+ next({ path: "/" });
+ return;
+ }
+
+ // 路由404检查
+ if (to.matched.length === 0) {
+ next("/404");
+ return;
+ }
+
+ // 动态标题设置
+ const title = (to.params.title as string) || (to.query.title as string);
+ if (title) {
+ to.meta.title = title;
+ }
+
+ next();
+ } catch (error) {
+ // 错误处理:重置状态并跳转登录
+ console.error("Route guard error:", error);
+ await useUserStore().resetAllState();
+ next("/login");
+ NProgress.done();
+ }
+ });
+
+ router.afterEach(() => {
+ NProgress.done();
+ });
+}
diff --git a/src/plugins/vxeTable.ts b/src/plugins/vxeTable.ts
new file mode 100644
index 0000000..5826faf
--- /dev/null
+++ b/src/plugins/vxeTable.ts
@@ -0,0 +1,70 @@
+import type { App } from "vue";
+import VXETable from "vxe-table"; // https://vxetable.cn/v4.6/#/table/start/install
+
+// 全局默认参数
+VXETable.setConfig({
+ // 全局尺寸
+ size: "medium",
+ // 全局 zIndex 起始值,如果项目的的 z-index 样式值过大时就需要跟随设置更大,避免被遮挡
+ zIndex: 9999,
+ // 版本号,对于某些带数据缓存的功能有用到,上升版本号可以用于重置数据
+ version: 0,
+ // 全局 loading 提示内容,如果为 null 则不显示文本
+ loadingText: null,
+ table: {
+ showHeader: true,
+ showOverflow: "tooltip",
+ showHeaderOverflow: "tooltip",
+ autoResize: true,
+ // stripe: false,
+ border: "inner",
+ // round: false,
+ emptyText: "暂无数据",
+ rowConfig: {
+ isHover: true,
+ isCurrent: true,
+ // 行数据的唯一主键字段名
+ keyField: "_VXE_ID",
+ },
+ columnConfig: {
+ resizable: false,
+ },
+ align: "center",
+ headerAlign: "center",
+ },
+ pager: {
+ // size: "medium",
+ // 配套的样式
+ perfect: false,
+ pageSize: 10,
+ pagerCount: 7,
+ pageSizes: [10, 20, 50],
+ layouts: [
+ "Total",
+ "PrevJump",
+ "PrevPage",
+ "Number",
+ "NextPage",
+ "NextJump",
+ "Sizes",
+ "FullJump",
+ ],
+ },
+ modal: {
+ minWidth: 500,
+ minHeight: 400,
+ lockView: true,
+ mask: true,
+ // duration: 3000,
+ // marginSize: 20,
+ dblclickZoom: false,
+ showTitleOverflow: true,
+ transfer: true,
+ draggable: false,
+ },
+});
+
+export function setupVxeTable(app: App) {
+ // Vxe Table 组件完整引入
+ app.use(VXETable);
+}
diff --git a/src/plugins/websocket.ts b/src/plugins/websocket.ts
new file mode 100644
index 0000000..ef39248
--- /dev/null
+++ b/src/plugins/websocket.ts
@@ -0,0 +1,142 @@
+import { useDictSync } from "@/composables";
+import { AuthStorage } from "@/utils/auth";
+// 不直接导入 store 或 userStore
+
+// 全局 WebSocket 实例管理
+const websocketInstances = new Map();
+
+// 用于防止重复初始化的状态标记
+let isInitialized = false;
+let dictWebSocketInstance: ReturnType | null = null;
+
+/**
+ * 注册 WebSocket 实例
+ */
+export function registerWebSocketInstance(key: string, instance: any) {
+ websocketInstances.set(key, instance);
+ console.log(`[WebSocketPlugin] Registered WebSocket instance: ${key}`);
+}
+
+/**
+ * 获取 WebSocket 实例
+ */
+export function getWebSocketInstance(key: string) {
+ return websocketInstances.get(key);
+}
+
+/**
+ * 初始化WebSocket服务
+ */
+export function setupWebSocket() {
+ console.log("[WebSocketPlugin] 开始初始化WebSocket服务...");
+
+ // 检查是否已经初始化
+ if (isInitialized) {
+ console.log("[WebSocketPlugin] WebSocket服务已经初始化,跳过重复初始化");
+ return;
+ }
+
+ // 检查环境变量是否配置
+ const wsEndpoint = import.meta.env.VITE_APP_WS_ENDPOINT;
+ if (!wsEndpoint) {
+ console.log("[WebSocketPlugin] 未配置WebSocket端点,跳过WebSocket初始化");
+ return;
+ }
+
+ // 检查是否已登录(基于是否存在访问令牌)
+ if (!AuthStorage.getAccessToken()) {
+ console.warn(
+ "[WebSocketPlugin] 未找到访问令牌,WebSocket初始化已跳过。用户登录后将自动重新连接。"
+ );
+ return;
+ }
+
+ try {
+ // 延迟初始化,确保应用完全启动
+ setTimeout(() => {
+ // 保存实例引用
+ dictWebSocketInstance = useDictSync();
+ registerWebSocketInstance("dictSync", dictWebSocketInstance);
+
+ // 初始化字典WebSocket服务
+ dictWebSocketInstance.initWebSocket();
+ console.log("[WebSocketPlugin] 字典WebSocket初始化完成");
+
+ // 初始化在线用户计数WebSocket
+ import("@/composables").then(({ useOnlineCount }) => {
+ const onlineCountInstance = useOnlineCount({ autoInit: false });
+ onlineCountInstance.initWebSocket();
+ console.log("[WebSocketPlugin] 在线用户计数WebSocket初始化完成");
+ });
+
+ // 在窗口关闭前断开WebSocket连接
+ window.addEventListener("beforeunload", handleWindowClose);
+
+ console.log("[WebSocketPlugin] WebSocket服务初始化完成");
+ isInitialized = true;
+ }, 1000); // 延迟1秒初始化
+ } catch (error) {
+ console.error("[WebSocketPlugin] 初始化WebSocket服务失败:", error);
+ }
+}
+
+/**
+ * 处理窗口关闭
+ */
+function handleWindowClose() {
+ console.log("[WebSocketPlugin] 窗口即将关闭,断开WebSocket连接");
+ cleanupWebSocket();
+}
+
+/**
+ * 清理WebSocket连接
+ */
+export function cleanupWebSocket() {
+ // 清理字典 WebSocket
+ if (dictWebSocketInstance) {
+ try {
+ dictWebSocketInstance.closeWebSocket();
+ console.log("[WebSocketPlugin] 字典WebSocket连接已断开");
+ } catch (error) {
+ console.error("[WebSocketPlugin] 断开字典WebSocket连接失败:", error);
+ }
+ }
+
+ // 清理所有注册的 WebSocket 实例
+ websocketInstances.forEach((instance, key) => {
+ try {
+ if (instance && typeof instance.disconnect === "function") {
+ instance.disconnect();
+ console.log(`[WebSocketPlugin] ${key} WebSocket连接已断开`);
+ } else if (instance && typeof instance.closeWebSocket === "function") {
+ instance.closeWebSocket();
+ console.log(`[WebSocketPlugin] ${key} WebSocket连接已断开`);
+ }
+ } catch (error) {
+ console.error(`[WebSocketPlugin] 断开 ${key} WebSocket连接失败:`, error);
+ }
+ });
+
+ // 清空实例映射
+ websocketInstances.clear();
+
+ // 移除事件监听器
+ window.removeEventListener("beforeunload", handleWindowClose);
+
+ // 重置状态
+ dictWebSocketInstance = null;
+ isInitialized = false;
+}
+
+/**
+ * 重新初始化WebSocket(用于登录后重连)
+ */
+export function reinitializeWebSocket() {
+ // 先清理现有连接
+ cleanupWebSocket();
+
+ // 延迟后重新初始化
+ setTimeout(() => {
+ setupWebSocket();
+ }, 500);
+}
diff --git a/src/router/index.ts b/src/router/index.ts
new file mode 100644
index 0000000..1093e80
--- /dev/null
+++ b/src/router/index.ts
@@ -0,0 +1,322 @@
+<<<<<<< HEAD
+import type { App } from "vue";
+import { createRouter, createWebHashHistory, type RouteRecordRaw } from "vue-router";
+
+export const Layout = () => import("@/layouts/index.vue");
+
+// 静态路由
+export const constantRoutes: RouteRecordRaw[] = [
+ {
+ path: "/login",
+ component: () => import("@/views/login/index.vue"),
+ meta: { hidden: true },
+ },
+ {
+ path: "/",
+ name: "/",
+ component: Layout,
+ redirect: "/dashboard",
+ children: [
+ {
+ path: "dashboard",
+ component: () => import("@/views/dashboard/index.vue"),
+ // 用于 keep-alive 功能,需要与 SFC 中自动推导或显式声明的组件名称一致
+ // 参考文档: https://cn.vuejs.org/guide/built-ins/keep-alive.html#include-exclude
+ name: "Dashboard",
+ meta: {
+ title: "dashboard",
+ icon: "homepage",
+ affix: true,
+ keepAlive: true,
+ },
+ },
+ {
+ path: "401",
+ component: () => import("@/views/error/401.vue"),
+ meta: { hidden: true },
+ },
+ {
+ path: "404",
+ component: () => import("@/views/error/404.vue"),
+ meta: { hidden: true },
+ },
+ {
+ path: "profile",
+ name: "Profile",
+ component: () => import("@/views/profile/index.vue"),
+ meta: { title: "个人中心", icon: "user", hidden: true },
+ },
+ {
+ path: "my-notice",
+ name: "MyNotice",
+ component: () => import("@/views/system/notice/components/MyNotice.vue"),
+ meta: { title: "我的通知", icon: "user", hidden: true },
+ },
+ {
+ path: "/detail/:id(\\d+)",
+ name: "DemoDetail",
+ component: () => import("@/views/demo/detail.vue"),
+ meta: { title: "详情页缓存", icon: "user", hidden: true, keepAlive: true },
+ },
+ ],
+ },
+ // 人员管理模块
+ {
+ path: "/personnel",
+ component: Layout,
+ name: "Personnel",
+ meta: {
+ title: "人员管理",
+ icon: "setting",
+ },
+ children: [
+ {
+ path: "user",
+ name: "PersonnelUser",
+ component: () => import("@/views/system/user/index.vue"),
+ meta: {
+ title: "人事管理",
+ },
+ },
+ {
+ path: "role",
+ name: "PersonnelRole",
+ component: () => import("@/views/system/role/index.vue"),
+ meta: {
+ title: "角色管理",
+ },
+ },
+ ],
+ },
+ // 财务管理模块
+ {
+ path: "/finance",
+ component: Layout,
+ name: "Finance",
+ meta: {
+ title: "财务管理",
+ icon: "setting",
+ },
+ children: [
+ {
+ path: "user",
+ name: "FinanceUser",
+ component: () => import("@/views/system/user/index.vue"),
+ meta: {
+ title: "人事管理",
+ },
+ },
+ {
+ path: "role",
+ name: "FinanceRole",
+ component: () => import("@/views/system/role/index.vue"),
+ meta: {
+ title: "角色管理",
+ },
+ },
+ ],
+ },
+ // 业务管理模块
+ {
+ path: "/business",
+ component: Layout,
+ name: "Business",
+ meta: {
+ title: "业务管理",
+ icon: "setting",
+ },
+ children: [
+ {
+ path: "user",
+ name: "BusinessConflict",
+ component: () => import("@/views/business/conflict/index.vue"),
+ meta: {
+ title: "利益冲突检索",
+ },
+ },
+ {
+ path: "role",
+ name: "BusinessPreRegistration",
+ component: () => import("@/views/business/preRegistration/index.vue"),
+ meta: {
+ title: "预立案登记",
+ },
+ },
+ ],
+ },
+ // 案件管理模块
+ {
+ path: "/case",
+ component: Layout,
+ name: "Case",
+ meta: {
+ title: "案件管理",
+ icon: "setting",
+ },
+ children: [
+ {
+ path: "user",
+ name: "CaseUser",
+ component: () => import("@/views/system/user/index.vue"),
+ meta: {
+ title: "人事管理",
+ },
+ },
+ {
+ path: "role",
+ name: "CaseRole",
+ component: () => import("@/views/system/role/index.vue"),
+ meta: {
+ title: "角色管理",
+ },
+ },
+ ],
+ },
+ // 申请用印
+ {
+ path: "/stamp",
+ name: "StampApplication",
+ component: Layout,
+ meta: {
+ title: "申请用印",
+ icon: "setting",
+ },
+ redirect: "/stamp/index",
+ children: [
+ {
+ path: "index",
+ name: "StampApplicationIndex",
+ component: () => import("@/views/stamp-application/index.vue"),
+ meta: {
+ title: "申请用印",
+ },
+ },
+ ],
+ },
+ // 业绩
+ {
+ path: "/performance",
+ name: "StampPerformance",
+ component: Layout,
+ meta: {
+ title: "业绩展示",
+ icon: "setting",
+ },
+ redirect: "/performance/index",
+ children: [
+ {
+ path: "index",
+ name: "StampPerformanceIndex",
+ component: () => import("@/views/stamp-application/index.vue"),
+ meta: {
+ title: "业绩展示",
+ },
+ },
+ ],
+ },
+ // 入库登记
+ {
+ path: "/registration",
+ name: "Registration",
+ component: Layout,
+ meta: {
+ title: "入库登记",
+ icon: "setting",
+ },
+ redirect: "/registration/index",
+ children: [
+ {
+ path: "index",
+ name: "RegistrationIndex",
+ component: () => import("@/views/stamp-application/index.vue"),
+ meta: {
+ title: "入库登记",
+ },
+ },
+ ],
+ },
+ // 公告
+ {
+ path: "/notice",
+ name: "Notice",
+ component: Layout,
+ meta: {
+ title: "公告管理",
+ icon: "setting",
+ },
+ redirect: "/notice/index",
+ children: [
+ {
+ path: "index",
+ name: "NoticeIndex",
+ component: () => import("@/views/stamp-application/index.vue"),
+ meta: {
+ title: "公告管理",
+ },
+ },
+ ],
+ },
+ // 律所标准文件
+ {
+ path: "/lawyer-file",
+ name: "LawyerFile",
+ component: Layout,
+ meta: {
+ title: "律所标准文件",
+ icon: "setting",
+ },
+ redirect: "/lawyer-file/index",
+ children: [
+ {
+ path: "index",
+ name: "LawyerFileIndex",
+ component: () => import("@/views/stamp-application/index.vue"),
+ meta: {
+ title: "律所标准文件",
+ },
+ },
+ ],
+ },
+];
+
+/**
+ * 创建路由
+ */
+const router = createRouter({
+ history: createWebHashHistory(),
+ routes: constantRoutes,
+ // 刷新时,滚动条位置还原
+ scrollBehavior: () => ({ left: 0, top: 0 }),
+});
+
+// 全局注册 router
+export function setupRouter(app: App) {
+ app.use(router);
+}
+
+export default router;
+=======
+import {createRouter, createWebHashHistory, RouteRecordRaw} from 'vue-router'
+import HelloWord from '../components/HelloWorld.vue'
+
+const routes: Array = [
+ {
+ path: '',
+ redirect: (_) => {
+ return {path: '/home'}
+ }
+ },
+ {
+ path: '/home',
+ name: 'HelloWord',
+ component: HelloWord
+ }
+]
+
+const router = createRouter({
+ history: createWebHashHistory(),
+ routes: routes
+})
+
+export default router
+>>>>>>> 232db255 ('首次提交')
diff --git a/src/settings.ts b/src/settings.ts
new file mode 100644
index 0000000..26d505c
--- /dev/null
+++ b/src/settings.ts
@@ -0,0 +1,67 @@
+import { LayoutMode, ComponentSize, SidebarColor, ThemeMode, LanguageEnum } from "./enums";
+
+const { pkg } = __APP_INFO__;
+
+// 检查用户的操作系统是否使用深色模式
+const mediaQueryList = window.matchMedia("(prefers-color-scheme: dark)");
+
+export const defaultSettings: AppSettings = {
+ // 系统Title
+ title: pkg.name,
+ // 系统版本
+ version: pkg.version,
+ // 是否显示设置
+ showSettings: true,
+ // 是否显示标签视图
+ showTagsView: true,
+ // 是否显示应用Logo
+ showAppLogo: true,
+ // 布局方式,默认为左侧布局
+ layout: LayoutMode.LEFT,
+ // 主题,根据操作系统的色彩方案自动选择
+ theme: mediaQueryList.matches ? ThemeMode.DARK : ThemeMode.LIGHT,
+ // 组件大小 default | medium | small | large
+ size: ComponentSize.DEFAULT,
+ // 语言
+ language: LanguageEnum.ZH_CN,
+ // 主题颜色 - 修改此值时需同步修改 src/styles/variables.scss
+ themeColor: "#4080FF",
+ // 是否显示水印
+ showWatermark: false,
+ // 水印内容
+ watermarkContent: pkg.name,
+ // 侧边栏配色方案
+ sidebarColorScheme: SidebarColor.CLASSIC_BLUE,
+ // 是否启用 AI 助手
+ enableAiAssistant: false,
+};
+
+/**
+ * 认证功能配置
+ */
+export const authConfig = {
+ /**
+ * Token自动刷新开关
+ *
+ * true: 启用自动刷新 - ACCESS_TOKEN_INVALID时尝试刷新token
+ * false: 禁用自动刷新 - ACCESS_TOKEN_INVALID时直接跳转登录页
+ *
+ * 适用场景:后端没有刷新接口或不需要自动刷新的项目可设为false
+ */
+ enableTokenRefresh: true,
+} as const;
+
+// 主题色预设 - 经典配色方案
+// 注意:修改默认主题色时,需要同步修改 src/styles/variables.scss 中的 primary.base 值
+export const themeColorPresets = [
+ "#4080FF", // Arco Design 蓝 - 现代感强
+ "#1890FF", // Ant Design 蓝 - 经典商务
+ "#409EFF", // Element Plus 蓝 - 清新自然
+ "#FA8C16", // 活力橙 - 温暖友好
+ "#722ED1", // 优雅紫 - 高端大气
+ "#13C2C2", // 青色 - 科技感
+ "#52C41A", // 成功绿 - 活力清新
+ "#F5222D", // 警示红 - 醒目强烈
+ "#2F54EB", // 深蓝 - 稳重专业
+ "#EB2F96", // 品红 - 时尚个性
+];
diff --git a/src/store/index.ts b/src/store/index.ts
new file mode 100644
index 0000000..7afc6e2
--- /dev/null
+++ b/src/store/index.ts
@@ -0,0 +1,45 @@
+<<<<<<< HEAD
+import type { App } from "vue";
+import { createPinia } from "pinia";
+
+const store = createPinia();
+
+// 全局注册 store
+export function setupStore(app: App) {
+ app.use(store);
+}
+
+export * from "./modules/app-store";
+export * from "./modules/permission-store";
+export * from "./modules/settings-store";
+export * from "./modules/tags-view-store";
+export * from "./modules/user-store";
+export * from "./modules/dict-store";
+export { store };
+=======
+import {InjectionKey} from 'vue'
+import {createStore, Store} from 'vuex'
+
+export interface State {
+ count: number
+}
+
+export const key: InjectionKey> = Symbol()
+
+export const store = createStore({
+ state() {
+ return {
+ count: 0
+ }
+ },
+ mutations: {
+ increment(state: { count: number }) {
+ state.count++
+ }
+ }
+})
+
+
+
+
+>>>>>>> 232db255 ('首次提交')
diff --git a/src/store/modules/app-store.ts b/src/store/modules/app-store.ts
new file mode 100644
index 0000000..bd8e686
--- /dev/null
+++ b/src/store/modules/app-store.ts
@@ -0,0 +1,108 @@
+import { defaultSettings } from "@/settings";
+
+// 导入 Element Plus 中英文语言包
+import zhCn from "element-plus/es/locale/lang/zh-cn";
+import en from "element-plus/es/locale/lang/en";
+import { store } from "@/store";
+import { DeviceEnum } from "@/enums/settings/device-enum";
+import { SidebarStatus } from "@/enums/settings/layout-enum";
+import { STORAGE_KEYS } from "@/constants";
+
+export const useAppStore = defineStore("app", () => {
+ // 设备类型
+ const device = useStorage(STORAGE_KEYS.DEVICE, DeviceEnum.DESKTOP);
+ // 布局大小
+ const size = useStorage(STORAGE_KEYS.SIZE, defaultSettings.size);
+ // 语言
+ const language = useStorage(STORAGE_KEYS.LANGUAGE, defaultSettings.language);
+ // 侧边栏状态
+ const sidebarStatus = useStorage(STORAGE_KEYS.SIDEBAR_STATUS, SidebarStatus.CLOSED);
+ const sidebar = reactive({
+ opened: sidebarStatus.value === SidebarStatus.OPENED,
+ withoutAnimation: false,
+ });
+
+ // 顶部菜单激活路径
+ const activeTopMenuPath = useStorage(STORAGE_KEYS.ACTIVE_TOP_MENU_PATH, "");
+
+ /**
+ * 根据语言标识读取对应的语言包
+ */
+ const locale = computed(() => {
+ if (language?.value == "en") {
+ return en;
+ } else {
+ return zhCn;
+ }
+ });
+
+ // 切换侧边栏
+ function toggleSidebar() {
+ sidebar.opened = !sidebar.opened;
+ sidebarStatus.value = sidebar.opened ? SidebarStatus.OPENED : SidebarStatus.CLOSED;
+ }
+
+ // 关闭侧边栏
+ function closeSideBar() {
+ sidebar.opened = false;
+ sidebarStatus.value = SidebarStatus.CLOSED;
+ }
+
+ // 打开侧边栏
+ function openSideBar() {
+ sidebar.opened = true;
+ sidebarStatus.value = SidebarStatus.OPENED;
+ }
+
+ // 切换设备
+ function toggleDevice(val: string) {
+ device.value = val;
+ }
+
+ /**
+ * 改变布局大小
+ *
+ * @param val 布局大小 default | small | large
+ */
+ function changeSize(val: string) {
+ size.value = val;
+ }
+ /**
+ * 切换语言
+ *
+ * @param val
+ */
+ function changeLanguage(val: string) {
+ language.value = val;
+ }
+ /**
+ * 混合模式顶部切换
+ */
+ function activeTopMenu(val: string) {
+ activeTopMenuPath.value = val;
+ }
+ return {
+ device,
+ sidebar,
+ language,
+ locale,
+ size,
+ activeTopMenu,
+ toggleDevice,
+ changeSize,
+ changeLanguage,
+ toggleSidebar,
+ closeSideBar,
+ openSideBar,
+ activeTopMenuPath,
+ };
+});
+
+/**
+ * 用于在组件外部(如在Pinia Store 中)使用 Pinia 提供的 store 实例。
+ * 官方文档解释了如何在组件外部使用 Pinia Store:
+ * https://pinia.vuejs.org/core-concepts/outside-component-usage.html#using-a-store-outside-of-a-component
+ */
+export function useAppStoreHook() {
+ return useAppStore(store);
+}
diff --git a/src/store/modules/dict-store.ts b/src/store/modules/dict-store.ts
new file mode 100644
index 0000000..6de2fe6
--- /dev/null
+++ b/src/store/modules/dict-store.ts
@@ -0,0 +1,79 @@
+import { store } from "@/store";
+import DictAPI, { type DictItemOption } from "@/api/system/dict-api";
+import { STORAGE_KEYS } from "@/constants";
+
+export const useDictStore = defineStore("dict", () => {
+ // 字典数据缓存
+ const dictCache = useStorage>(STORAGE_KEYS.DICT_CACHE, {});
+
+ // 请求队列(防止重复请求)
+ const requestQueue: Record> = {};
+
+ /**
+ * 缓存字典数据
+ * @param dictCode 字典编码
+ * @param data 字典项列表
+ */
+ const cacheDictItems = (dictCode: string, data: DictItemOption[]) => {
+ dictCache.value[dictCode] = data;
+ };
+
+ /**
+ * 加载字典数据(如果缓存中没有则请求)
+ * @param dictCode 字典编码
+ */
+ const loadDictItems = async (dictCode: string) => {
+ if (dictCache.value[dictCode]) return;
+ // 防止重复请求
+ if (!requestQueue[dictCode]) {
+ requestQueue[dictCode] = DictAPI.getDictItems(dictCode)
+ .then((data) => {
+ cacheDictItems(dictCode, data);
+ Reflect.deleteProperty(requestQueue, dictCode);
+ })
+ .catch((error) => {
+ // 请求失败,清理队列,允许重试
+ Reflect.deleteProperty(requestQueue, dictCode);
+ throw error;
+ });
+ }
+ await requestQueue[dictCode];
+ };
+
+ /**
+ * 获取字典项列表
+ * @param dictCode 字典编码
+ * @returns 字典项列表
+ */
+ const getDictItems = (dictCode: string): DictItemOption[] => {
+ return dictCache.value[dictCode] || [];
+ };
+
+ /**
+ * 移除指定字典项
+ * @param dictCode 字典编码
+ */
+ const removeDictItem = (dictCode: string) => {
+ if (dictCache.value[dictCode]) {
+ Reflect.deleteProperty(dictCache.value, dictCode);
+ }
+ };
+
+ /**
+ * 清空字典缓存
+ */
+ const clearDictCache = () => {
+ dictCache.value = {};
+ };
+
+ return {
+ loadDictItems,
+ getDictItems,
+ removeDictItem,
+ clearDictCache,
+ };
+});
+
+export function useDictStoreHook() {
+ return useDictStore(store);
+}
diff --git a/src/store/modules/permission-store.ts b/src/store/modules/permission-store.ts
new file mode 100644
index 0000000..6727fab
--- /dev/null
+++ b/src/store/modules/permission-store.ts
@@ -0,0 +1,104 @@
+import type { RouteRecordRaw } from "vue-router";
+import { constantRoutes } from "@/router";
+import { store } from "@/store";
+import router from "@/router";
+
+import MenuAPI, { type RouteVO } from "@/api/system/menu-api";
+const modules = import.meta.glob("../../views/**/**.vue");
+const Layout = () => import("../../layouts/index.vue");
+
+export const usePermissionStore = defineStore("permission", () => {
+ // 所有路由(只使用静态路由)
+ const routes = ref(constantRoutes);
+ // 混合布局的左侧菜单路由
+ const mixLayoutSideMenus = ref([]);
+ // 动态路由是否已生成
+ const isRouteGenerated = ref(false);
+
+ /** 生成动态路由 */
+ async function generateRoutes(): Promise {
+ try {
+ const data = await MenuAPI.getRoutes(); // 获取当前登录人的菜单路由
+ const dynamicRoutes = transformRoutes(data);
+
+ routes.value = [...constantRoutes, ...dynamicRoutes];
+ isRouteGenerated.value = true;
+
+ return dynamicRoutes;
+ } catch (error) {
+ // 路由生成失败,重置状态
+ isRouteGenerated.value = false;
+ throw error;
+ }
+ }
+
+ /** 设置混合布局左侧菜单 */
+ const setMixLayoutSideMenus = (parentPath: string) => {
+ const parentMenu = routes.value.find((item) => item.path === parentPath);
+ mixLayoutSideMenus.value = parentMenu?.children || [];
+ };
+
+ /** 重置路由状态 */
+ const resetRouter = () => {
+ // 移除动态添加的路由
+ const constantRouteNames = new Set(constantRoutes.map((route) => route.name).filter(Boolean));
+ routes.value.forEach((route) => {
+ if (route.name && !constantRouteNames.has(route.name)) {
+ router.removeRoute(route.name);
+ }
+ });
+
+ // 重置所有状态
+ routes.value = [...constantRoutes];
+ mixLayoutSideMenus.value = [];
+ isRouteGenerated.value = false;
+ };
+
+ return {
+ routes,
+ mixLayoutSideMenus,
+ isRouteGenerated,
+ generateRoutes,
+ setMixLayoutSideMenus,
+ resetRouter,
+ };
+});
+
+/**
+ * 转换后端路由数据为Vue Router配置
+ * 处理组件路径映射和Layout层级嵌套
+ */
+const transformRoutes = (routes: RouteVO[], isTopLevel: boolean = true): RouteRecordRaw[] => {
+ return routes.map((route) => {
+ const { component, children, ...args } = route;
+
+ // 处理组件:顶层或非Layout保留组件,中间层Layout设为undefined
+ const processedComponent = isTopLevel || component !== "Layout" ? component : undefined;
+
+ const normalizedRoute = { ...args } as RouteRecordRaw;
+
+ if (!processedComponent) {
+ // 多级菜单的父级菜单,不需要组件
+ normalizedRoute.component = undefined;
+ } else {
+ // 动态导入组件,Layout特殊处理,找不到组件时返回404
+ normalizedRoute.component =
+ processedComponent === "Layout"
+ ? Layout
+ : modules[`../../views/${processedComponent}.vue`] ||
+ modules[`../../views/error/404.vue`];
+ }
+
+ // 递归处理子路由
+ if (children && children.length > 0) {
+ normalizedRoute.children = transformRoutes(children, false);
+ }
+
+ return normalizedRoute;
+ });
+};
+
+/** 非组件环境使用权限store */
+export function usePermissionStoreHook() {
+ return usePermissionStore(store);
+}
diff --git a/src/store/modules/settings-store.ts b/src/store/modules/settings-store.ts
new file mode 100644
index 0000000..f96c9ec
--- /dev/null
+++ b/src/store/modules/settings-store.ts
@@ -0,0 +1,176 @@
+import { defaultSettings } from "@/settings";
+import { SidebarColor, ThemeMode } from "@/enums/settings/theme-enum";
+import type { LayoutMode } from "@/enums/settings/layout-enum";
+import { applyTheme, generateThemeColors, toggleDarkMode, toggleSidebarColor } from "@/utils/theme";
+import { STORAGE_KEYS } from "@/constants";
+
+// 🎯 设置项类型定义
+interface SettingsState {
+ // 界面显示设置
+ settingsVisible: boolean;
+ showTagsView: boolean;
+ showAppLogo: boolean;
+ showWatermark: boolean;
+ enableAiAssistant: boolean;
+
+ // 布局设置
+ layout: LayoutMode;
+ sidebarColorScheme: string;
+
+ // 主题设置
+ theme: ThemeMode;
+ themeColor: string;
+}
+
+// 🎯 可变更的设置项类型
+type MutableSetting = Exclude;
+type SettingValue = SettingsState[K];
+
+export const useSettingsStore = defineStore("setting", () => {
+ // 设置面板可见性
+ const settingsVisible = ref(false);
+
+ // 是否显示标签页视图
+ const showTagsView = useStorage(
+ STORAGE_KEYS.SHOW_TAGS_VIEW,
+ defaultSettings.showTagsView
+ );
+
+ // 是否显示应用Logo
+ const showAppLogo = useStorage(STORAGE_KEYS.SHOW_APP_LOGO, defaultSettings.showAppLogo);
+
+ // 是否显示水印
+ const showWatermark = useStorage(
+ STORAGE_KEYS.SHOW_WATERMARK,
+ defaultSettings.showWatermark
+ );
+
+ // 是否启用 AI 助手
+ const enableAiAssistant = useStorage(
+ STORAGE_KEYS.ENABLE_AI_ASSISTANT,
+ defaultSettings.enableAiAssistant
+ );
+
+ // 侧边栏配色方案
+ const sidebarColorScheme = useStorage(
+ STORAGE_KEYS.SIDEBAR_COLOR_SCHEME,
+ defaultSettings.sidebarColorScheme
+ );
+
+ // 布局模式
+ const layout = useStorage(STORAGE_KEYS.LAYOUT, defaultSettings.layout as LayoutMode);
+
+ // 主题颜色
+ const themeColor = useStorage(STORAGE_KEYS.THEME_COLOR, defaultSettings.themeColor);
+
+ // 主题模式(亮色/暗色)
+ const theme = useStorage(STORAGE_KEYS.THEME, defaultSettings.theme);
+
+ // 设置项映射,用于统一管理
+ const settingsMap = {
+ showTagsView,
+ showAppLogo,
+ showWatermark,
+ enableAiAssistant,
+ sidebarColorScheme,
+ layout,
+ } as const;
+
+ // 监听主题变化,自动应用样式
+ watch(
+ [theme, themeColor],
+ ([newTheme, newThemeColor]: [ThemeMode, string]) => {
+ toggleDarkMode(newTheme === ThemeMode.DARK);
+ const colors = generateThemeColors(newThemeColor, newTheme);
+ applyTheme(colors);
+ },
+ { immediate: true }
+ );
+
+ // 监听侧边栏配色变化
+ watch(
+ [sidebarColorScheme],
+ ([newSidebarColorScheme]) => {
+ toggleSidebarColor(newSidebarColorScheme === SidebarColor.CLASSIC_BLUE);
+ },
+ { immediate: true }
+ );
+
+ // 通用设置更新方法
+ function updateSetting(key: K, value: SettingValue): void {
+ const setting = settingsMap[key];
+ if (setting) {
+ (setting as Ref).value = value;
+ }
+ }
+
+ // 主题更新方法
+ function updateTheme(newTheme: ThemeMode): void {
+ theme.value = newTheme;
+ }
+
+ function updateThemeColor(newColor: string): void {
+ themeColor.value = newColor;
+ }
+
+ function updateSidebarColorScheme(newScheme: string): void {
+ sidebarColorScheme.value = newScheme;
+ }
+
+ function updateLayout(newLayout: LayoutMode): void {
+ layout.value = newLayout;
+ }
+
+ // 设置面板控制
+ function toggleSettingsPanel(): void {
+ settingsVisible.value = !settingsVisible.value;
+ }
+
+ function showSettingsPanel(): void {
+ settingsVisible.value = true;
+ }
+
+ function hideSettingsPanel(): void {
+ settingsVisible.value = false;
+ }
+
+ // 重置所有设置
+ function resetSettings(): void {
+ showTagsView.value = defaultSettings.showTagsView;
+ showAppLogo.value = defaultSettings.showAppLogo;
+ showWatermark.value = defaultSettings.showWatermark;
+ enableAiAssistant.value = defaultSettings.enableAiAssistant;
+ sidebarColorScheme.value = defaultSettings.sidebarColorScheme;
+ layout.value = defaultSettings.layout as LayoutMode;
+ themeColor.value = defaultSettings.themeColor;
+ theme.value = defaultSettings.theme;
+ }
+
+ return {
+ // 状态
+ settingsVisible,
+ showTagsView,
+ showAppLogo,
+ showWatermark,
+ enableAiAssistant,
+ sidebarColorScheme,
+ layout,
+ themeColor,
+ theme,
+
+ // 更新方法
+ updateSetting,
+ updateTheme,
+ updateThemeColor,
+ updateSidebarColorScheme,
+ updateLayout,
+
+ // 面板控制
+ toggleSettingsPanel,
+ showSettingsPanel,
+ hideSettingsPanel,
+
+ // 重置功能
+ resetSettings,
+ };
+});
diff --git a/src/store/modules/tags-view-store.ts b/src/store/modules/tags-view-store.ts
new file mode 100644
index 0000000..310f2cc
--- /dev/null
+++ b/src/store/modules/tags-view-store.ts
@@ -0,0 +1,275 @@
+export const useTagsViewStore = defineStore("tagsView", () => {
+ const visitedViews = ref([]);
+ const cachedViews = ref([]);
+ const router = useRouter();
+ const route = useRoute();
+
+ /**
+ * 添加已访问视图到已访问视图列表中
+ */
+ function addVisitedView(view: TagView) {
+ // 如果已经存在于已访问的视图列表中或者是重定向地址,则不再添加
+ if (view.path.startsWith("/redirect")) {
+ return;
+ }
+ if (visitedViews.value.some((v) => v.path === view.path)) {
+ return;
+ }
+ // 如果视图是固定的(affix),则在已访问的视图列表的开头添加
+ if (view.affix) {
+ visitedViews.value.unshift(view);
+ } else {
+ // 如果视图不是固定的,则在已访问的视图列表的末尾添加
+ visitedViews.value.push(view);
+ }
+ }
+
+ /**
+ * 添加缓存视图到缓存视图列表中
+ */
+ function addCachedView({ fullPath, keepAlive }: TagView) {
+ // 如果缓存视图名称已经存在于缓存视图列表中,则不再添加
+ if (cachedViews.value.includes(fullPath)) {
+ return;
+ }
+
+ // 如果视图需要缓存(keepAlive),则将其路由名称添加到缓存视图列表中
+ if (keepAlive) {
+ cachedViews.value.push(fullPath);
+ }
+ }
+
+ /**
+ * 从已访问视图列表中删除指定的视图
+ */
+ function delVisitedView(view: 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]);
+ });
+ }
+
+ function delCachedView(view: TagView) {
+ const { fullPath } = view;
+ return new Promise((resolve) => {
+ const index = cachedViews.value.indexOf(fullPath);
+ if (index > -1) {
+ cachedViews.value.splice(index, 1);
+ }
+ resolve([...cachedViews.value]);
+ });
+ }
+ function delOtherVisitedViews(view: TagView) {
+ return new Promise((resolve) => {
+ visitedViews.value = visitedViews.value.filter((v) => {
+ return v?.affix || v.path === view.path;
+ });
+ resolve([...visitedViews.value]);
+ });
+ }
+
+ function delOtherCachedViews(view: TagView) {
+ const { fullPath } = view;
+ return new Promise((resolve) => {
+ const index = cachedViews.value.indexOf(fullPath);
+ if (index > -1) {
+ cachedViews.value = cachedViews.value.slice(index, index + 1);
+ } else {
+ // if index = -1, there is no cached tags
+ cachedViews.value = [];
+ }
+ resolve([...cachedViews.value]);
+ });
+ }
+
+ function updateVisitedView(view: TagView) {
+ for (let v of visitedViews.value) {
+ if (v.path === view.path) {
+ v = Object.assign(v, view);
+ break;
+ }
+ }
+ }
+
+ /**
+ * 根据路径更新标签名称
+ * @param fullPath 路径
+ * @param title 标签名称
+ */
+ function updateTagName(fullPath: string, title: string) {
+ const tag = visitedViews.value.find((tag: TagView) => tag.fullPath === fullPath);
+
+ if (tag) {
+ tag.title = title;
+ }
+ }
+
+ function addView(view: TagView) {
+ addVisitedView(view);
+ addCachedView(view);
+ }
+
+ function delView(view: TagView) {
+ return new Promise((resolve) => {
+ delVisitedView(view);
+ delCachedView(view);
+ resolve({
+ visitedViews: [...visitedViews.value],
+ cachedViews: [...cachedViews.value],
+ });
+ });
+ }
+
+ function delOtherViews(view: TagView) {
+ return new Promise((resolve) => {
+ delOtherVisitedViews(view);
+ delOtherCachedViews(view);
+ resolve({
+ visitedViews: [...visitedViews.value],
+ cachedViews: [...cachedViews.value],
+ });
+ });
+ }
+
+ function delLeftViews(view: TagView) {
+ return new Promise((resolve) => {
+ const currIndex = visitedViews.value.findIndex((v) => v.path === view.path);
+ if (currIndex === -1) {
+ return;
+ }
+ visitedViews.value = visitedViews.value.filter((item, index) => {
+ if (index >= currIndex || item?.affix) {
+ return true;
+ }
+
+ const cacheIndex = cachedViews.value.indexOf(item.fullPath);
+ if (cacheIndex > -1) {
+ cachedViews.value.splice(cacheIndex, 1);
+ }
+ return false;
+ });
+ resolve({
+ visitedViews: [...visitedViews.value],
+ });
+ });
+ }
+
+ function delRightViews(view: TagView) {
+ return new Promise((resolve) => {
+ const currIndex = visitedViews.value.findIndex((v) => v.path === view.path);
+ if (currIndex === -1) {
+ return;
+ }
+ visitedViews.value = visitedViews.value.filter((item, index) => {
+ if (index <= currIndex || item?.affix) {
+ return true;
+ }
+ const cacheIndex = cachedViews.value.indexOf(item.fullPath);
+ if (cacheIndex > -1) {
+ cachedViews.value.splice(cacheIndex, 1);
+ }
+ return false;
+ });
+ resolve({
+ visitedViews: [...visitedViews.value],
+ });
+ });
+ }
+
+ function delAllViews() {
+ return new Promise((resolve) => {
+ const affixTags = visitedViews.value.filter((tag) => tag?.affix);
+ visitedViews.value = affixTags;
+ cachedViews.value = [];
+ resolve({
+ visitedViews: [...visitedViews.value],
+ cachedViews: [...cachedViews.value],
+ });
+ });
+ }
+
+ function delAllVisitedViews() {
+ return new Promise((resolve) => {
+ const affixTags = visitedViews.value.filter((tag) => tag?.affix);
+ visitedViews.value = affixTags;
+ resolve([...visitedViews.value]);
+ });
+ }
+
+ function delAllCachedViews() {
+ return new Promise((resolve) => {
+ cachedViews.value = [];
+ resolve([...cachedViews.value]);
+ });
+ }
+
+ /**
+ * 关闭当前tagView
+ */
+ function closeCurrentView() {
+ const tags: TagView = {
+ name: route.name as string,
+ title: route.meta.title as string,
+ path: route.path,
+ fullPath: route.fullPath,
+ affix: route.meta?.affix,
+ keepAlive: route.meta?.keepAlive,
+ query: route.query,
+ };
+ delView(tags).then((res: any) => {
+ if (isActive(tags)) {
+ toLastView(res.visitedViews, tags);
+ }
+ });
+ }
+
+ function isActive(tag: TagView) {
+ return tag.path === route.path;
+ }
+
+ function toLastView(visitedViews: TagView[], view?: TagView) {
+ const latestView = visitedViews.slice(-1)[0];
+ if (latestView && latestView.fullPath) {
+ router.push(latestView.fullPath);
+ } 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("/redirect" + view.fullPath);
+ } else {
+ router.push("/");
+ }
+ }
+ }
+
+ return {
+ visitedViews,
+ cachedViews,
+ addVisitedView,
+ addCachedView,
+ delVisitedView,
+ delCachedView,
+ delOtherVisitedViews,
+ delOtherCachedViews,
+ updateVisitedView,
+ addView,
+ delView,
+ delOtherViews,
+ delLeftViews,
+ delRightViews,
+ delAllViews,
+ delAllVisitedViews,
+ delAllCachedViews,
+ closeCurrentView,
+ isActive,
+ toLastView,
+ updateTagName,
+ };
+});
diff --git a/src/store/modules/user-store.ts b/src/store/modules/user-store.ts
new file mode 100644
index 0000000..410cfff
--- /dev/null
+++ b/src/store/modules/user-store.ts
@@ -0,0 +1,157 @@
+import { store } from "@/store";
+
+import AuthAPI, { type LoginFormData } from "@/api/auth-api";
+import UserAPI, { type UserInfo } from "@/api/system/user-api";
+
+import { AuthStorage } from "@/utils/auth";
+import { usePermissionStoreHook } from "@/store/modules/permission-store";
+import { useDictStoreHook } from "@/store/modules/dict-store";
+import { useTagsViewStore } from "@/store";
+import { cleanupWebSocket } from "@/plugins/websocket";
+
+export const useUserStore = defineStore("user", () => {
+ // 用户信息
+ const userInfo = ref({} as UserInfo);
+ // 记住我状态
+ const rememberMe = ref(AuthStorage.getRememberMe());
+
+ /**
+ * 登录
+ *
+ * @param {LoginFormData}
+ * @returns
+ */
+ function login(LoginFormData: LoginFormData) {
+ return new Promise((resolve, reject) => {
+ AuthAPI.login(LoginFormData)
+ .then((data) => {
+ const { accessToken, refreshToken } = data;
+ // 保存记住我状态和token
+ rememberMe.value = LoginFormData.rememberMe;
+ AuthStorage.setTokens(accessToken, refreshToken, rememberMe.value);
+ resolve();
+ })
+ .catch((error) => {
+ reject(error);
+ });
+ });
+ }
+
+ /**
+ * 获取用户信息
+ *
+ * @returns {UserInfo} 用户信息
+ */
+ function getUserInfo() {
+ return new Promise((resolve, reject) => {
+ UserAPI.getInfo()
+ .then((data) => {
+ if (!data) {
+ reject("Verification failed, please Login again.");
+ return;
+ }
+ Object.assign(userInfo.value, { ...data });
+ resolve(data);
+ })
+ .catch((error) => {
+ reject(error);
+ });
+ });
+ }
+
+ /**
+ * 登出
+ */
+ function logout() {
+ return new Promise((resolve, reject) => {
+ AuthAPI.logout()
+ .then(() => {
+ // 重置所有系统状态
+ resetAllState();
+ resolve();
+ })
+ .catch((error) => {
+ reject(error);
+ });
+ });
+ }
+
+ /**
+ * 重置所有系统状态
+ * 统一处理所有清理工作,包括用户凭证、路由、缓存等
+ */
+ function resetAllState() {
+ // 1. 重置用户状态
+ resetUserState();
+
+ // 2. 重置其他模块状态
+ // 重置路由
+ usePermissionStoreHook().resetRouter();
+ // 清除字典缓存
+ useDictStoreHook().clearDictCache();
+ // 清除标签视图
+ useTagsViewStore().delAllViews();
+
+ // 3. 清理 WebSocket 连接
+ cleanupWebSocket();
+ console.log("[UserStore] WebSocket connections cleaned up");
+
+ return Promise.resolve();
+ }
+
+ /**
+ * 重置用户状态
+ * 仅处理用户模块内的状态
+ */
+ function resetUserState() {
+ // 清除用户凭证
+ AuthStorage.clearAuth();
+ // 重置用户信息
+ userInfo.value = {} as UserInfo;
+ }
+
+ /**
+ * 刷新 token
+ */
+ function refreshToken() {
+ const refreshToken = AuthStorage.getRefreshToken();
+
+ if (!refreshToken) {
+ return Promise.reject(new Error("没有有效的刷新令牌"));
+ }
+
+ return new Promise((resolve, reject) => {
+ AuthAPI.refreshToken(refreshToken)
+ .then((data) => {
+ const { accessToken, refreshToken: newRefreshToken } = data;
+ // 更新令牌,保持当前记住我状态
+ AuthStorage.setTokens(accessToken, newRefreshToken, AuthStorage.getRememberMe());
+ resolve();
+ })
+ .catch((error) => {
+ console.log(" refreshToken 刷新失败", error);
+ reject(error);
+ });
+ });
+ }
+
+ return {
+ userInfo,
+ rememberMe,
+ isLoggedIn: () => !!AuthStorage.getAccessToken(),
+ getUserInfo,
+ login,
+ logout,
+ resetAllState,
+ resetUserState,
+ refreshToken,
+ };
+});
+
+/**
+ * 在组件外部使用UserStore的钩子函数
+ * @see https://pinia.vuejs.org/core-concepts/outside-component-usage.html
+ */
+export function useUserStoreHook() {
+ return useUserStore(store);
+}
diff --git a/src/styles/dark/css-vars.css b/src/styles/dark/css-vars.css
new file mode 100644
index 0000000..823aabf
--- /dev/null
+++ b/src/styles/dark/css-vars.css
@@ -0,0 +1,7 @@
+/* 暗黑模式通过 CSS 自定义变量,官方链接:https://element-plus.org/zh-CN/guide/dark-mode.html#%E9%80%9A%E8%BF%87-css */
+html.dark {
+ .el-table {
+ /* 自定义表格选中高亮时当前行的背景颜色 */
+ --el-table-current-row-bg-color: var(--el-fill-color-light);
+ }
+}
diff --git a/src/styles/element-plus.scss b/src/styles/element-plus.scss
new file mode 100644
index 0000000..fc5b729
--- /dev/null
+++ b/src/styles/element-plus.scss
@@ -0,0 +1,45 @@
+$border: 1px solid var(--el-border-color-light);
+
+/* el-dialog */
+.el-dialog {
+ .el-dialog__header {
+ padding: 15px 20px;
+ margin: 0;
+ border-bottom: $border;
+ }
+
+ .el-dialog__body {
+ padding: 20px;
+ }
+
+ .el-dialog__footer {
+ padding: 15px;
+ border-top: $border;
+ }
+}
+
+/** el-drawer */
+.el-drawer {
+ .el-drawer__header {
+ padding: 15px 20px;
+ margin: 0;
+ color: inherit;
+ border-bottom: $border;
+ }
+
+ .el-drawer__body {
+ padding: 20px;
+ }
+
+ .el-drawer__footer {
+ padding: 15px;
+ border-top: $border;
+ }
+}
+
+// 抽屉和对话框底部按钮区域
+.dialog-footer {
+ display: flex;
+ gap: 8px;
+ justify-content: flex-end;
+}
diff --git a/src/styles/index.scss b/src/styles/index.scss
new file mode 100644
index 0000000..2265506
--- /dev/null
+++ b/src/styles/index.scss
@@ -0,0 +1,121 @@
+@use "./reset";
+@use "./element-plus";
+// Vxe Table
+@use "./vxe-table";
+@import url("./vxe-table.css");
+
+.app-container {
+ padding: 15px;
+}
+
+// 进度条颜色
+#nprogress .bar {
+ background-color: var(--el-color-primary);
+}
+
+// 混合布局左侧菜单的hover样式
+.layout-mix .layout__sidebar--left .el-menu {
+ .el-menu-item {
+ &:hover {
+ // 极简白主题:使用浅灰色背景
+ background-color: var(--el-fill-color-light) !important;
+ }
+ }
+
+ .el-sub-menu__title {
+ &:hover {
+ // 极简白主题:使用浅灰色背景
+ background-color: var(--el-fill-color-light) !important;
+ }
+ }
+}
+
+// 深色主题或深蓝色侧边栏配色下的左侧菜单hover样式
+html.dark .layout-mix .layout__sidebar--left .el-menu,
+html.sidebar-color-blue .layout-mix .layout__sidebar--left .el-menu {
+ .el-menu-item {
+ &:hover {
+ // 深色背景:使用CSS变量
+ background-color: var(--menu-hover) !important;
+ }
+ }
+
+ .el-sub-menu__title {
+ &:hover {
+ // 深色背景:使用CSS变量
+ background-color: var(--menu-hover) !important;
+ }
+ }
+}
+
+// 窄屏时隐藏菜单文字,只显示图标
+.hideSidebar {
+ // Top布局和Mix布局的水平菜单
+ &.layout-top .layout__header .el-menu--horizontal,
+ &.layout-mix .layout__header .el-menu--horizontal {
+ .el-menu-item,
+ .el-sub-menu__title {
+ .menu-title,
+ span:not([class*="i-svg"]):not(.el-icon) {
+ display: none !important;
+ }
+ }
+ }
+
+ // Mix布局的左侧菜单
+ &.layout-mix .layout__sidebar--left .el-menu {
+ .el-menu-item,
+ .el-sub-menu__title {
+ .menu-title,
+ span:not([class*="i-svg"]):not(.el-icon) {
+ display: none !important;
+ }
+ }
+ }
+}
+
+// 全局搜索区域样式
+.search-container {
+ padding: 18px 16px 0;
+ margin-bottom: 16px;
+ background-color: var(--el-bg-color-overlay);
+ border: 1px solid var(--el-border-color-light);
+ border-radius: 4px;
+
+ .search-buttons {
+ margin-right: 0;
+ }
+
+ .el-form-item {
+ margin-bottom: 18px;
+ }
+}
+
+// 表格区域样式
+.data-table {
+ margin-bottom: 16px;
+
+ // 表格工具栏区域
+ &__toolbar {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 16px;
+
+ &--actions,
+ &--tools {
+ display: flex;
+ gap: 8px;
+ }
+ }
+
+ // 表格内容区域
+ &__content {
+ margin: 8px 0;
+ }
+
+ // 分页区域
+ .el-pagination {
+ justify-content: flex-end;
+ margin-top: 16px;
+ }
+}
diff --git a/src/styles/reset.scss b/src/styles/reset.scss
new file mode 100644
index 0000000..b3b43c7
--- /dev/null
+++ b/src/styles/reset.scss
@@ -0,0 +1,77 @@
+*,
+::before,
+::after {
+ box-sizing: border-box;
+ border-color: currentcolor;
+ border-style: solid;
+ border-width: 0;
+}
+
+#app {
+ width: 100%;
+ height: 100%;
+}
+
+html {
+ box-sizing: border-box;
+ width: 100%;
+ height: 100%;
+ line-height: 1.5;
+ tab-size: 4;
+ text-size-adjust: 100%;
+}
+
+body {
+ width: 100%;
+ height: 100%;
+ margin: 0;
+ font-family:
+ "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑",
+ Arial, sans-serif;
+ line-height: inherit;
+ -moz-osx-font-smoothing: grayscale;
+ -webkit-font-smoothing: antialiased;
+ text-rendering: optimizelegibility;
+}
+
+a {
+ color: inherit;
+ text-decoration: inherit;
+}
+
+img,
+svg {
+ display: inline-block;
+}
+
+svg {
+ // 因icon大小被设置为和字体大小一致,而span等标签的下边缘会和字体的基线对齐,故需设置一个往下的偏移比例,来纠正视觉上的未对齐效果
+ vertical-align: -0.15em;
+}
+
+ul,
+li {
+ padding: 0;
+ margin: 0;
+ list-style: none;
+}
+
+*,
+*::before,
+*::after {
+ box-sizing: inherit;
+}
+
+a,
+a:focus,
+a:hover {
+ color: inherit;
+ text-decoration: none;
+ cursor: pointer;
+}
+
+a:focus,
+a:active,
+div:focus {
+ outline: none;
+}
diff --git a/src/styles/variables.module.scss b/src/styles/variables.module.scss
new file mode 100644
index 0000000..20a624d
--- /dev/null
+++ b/src/styles/variables.module.scss
@@ -0,0 +1,11 @@
+/* stylelint-disable property-no-unknown */
+:export {
+ sidebar-width: $sidebar-width;
+ navbar-height: $navbar-height;
+ tags-view-height: $tags-view-height;
+ menu-background: $menu-background;
+ menu-text: $menu-text;
+ menu-active-text: $menu-active-text;
+ menu-hover: $menu-hover;
+}
+/* stylelint-enable property-no-unknown */
diff --git a/src/styles/variables.scss b/src/styles/variables.scss
new file mode 100644
index 0000000..c907f33
--- /dev/null
+++ b/src/styles/variables.scss
@@ -0,0 +1,93 @@
+@forward "element-plus/theme-chalk/src/common/var.scss" with (
+ $colors: (
+ "primary": (
+ // 默认主题色 - 修改此值时需同步修改 src/settings.ts 中的 themeColor
+ "base": #4080ff,
+ ),
+ "success": (
+ "base": #23c343,
+ ),
+ "warning": (
+ "base": #ff9a2e,
+ ),
+ "danger": (
+ "base": #f76560,
+ ),
+ "info": (
+ "base": #a9aeb8,
+ ),
+ ),
+
+ $bg-color: (
+ "page": #f5f8fd,
+ )
+);
+
+/** 全局SCSS变量 */
+
+:root {
+ --menu-background: #fff; // 菜单背景色
+ --menu-text: #212121; // 菜单文字颜色 浅色主题-白色侧边栏配色下仅占位,实际颜色由 el-menu-item 组件决定
+ --menu-active-text: var(
+ --el-menu-active-color
+ ); // 菜单激活文字颜色 浅色主题-白色侧边栏配色下仅占位,实际颜色由 el-menu-item 组件决定
+
+ --menu-hover: #e6f4ff; // 菜单悬停背景色 浅色主题-白色侧边栏配色下仅占位,实际颜色由 el-menu-item 组件决定
+ --sidebar-logo-background: #f5f5f5; // 侧边栏 Logo 背景色
+ --sidebar-logo-text-color: #333; // 侧边栏 Logo 文字颜色
+}
+
+/** 浅色主题-深蓝色侧边栏配色 */
+html.sidebar-color-blue {
+ --menu-background: #304156; // 菜单背景色
+ --menu-text: #bfcbd9; // 菜单文字颜色
+ --menu-active-text: var(--el-menu-active-color); // 菜单激活文字颜色
+ --menu-hover: #263445; // 菜单悬停背景色
+ --sidebar-logo-background: #2d3748; // 侧边栏 Logo 背景色
+ --sidebar-logo-text-color: #fff; // 侧边栏 Logo 文字颜色
+}
+
+/** 暗黑主题 */
+html.dark {
+ --menu-background: var(--el-bg-color-overlay);
+ --menu-text: #fff;
+ --menu-active-text: var(--el-menu-active-color);
+ --menu-hover: rgb(0 0 0 / 20%);
+ --sidebar-logo-background: rgb(0 0 0 / 20%);
+ --sidebar-logo-text-color: #fff;
+
+ /** WangEditor Dark */
+ /* Textarea - css vars */
+ --w-e-textarea-bg-color: var(--el-bg-color); /* 深色背景 */
+ --w-e-textarea-color: var(--el-text-color-primary); /* 浅色文字 */
+ --w-e-textarea-border-color: var(--el-border-color); /* 较深的边框颜色 */
+ --w-e-textarea-slight-border-color: var(--el-border-color-lighter); /* 更淡一些的边框颜色 */
+ --w-e-textarea-slight-color: var(--el-text-color-secondary); /* 浅灰色,用于不那么重要的元素 */
+ --w-e-textarea-slight-bg-color: var(--el-bg-color-overlay); /* 稍微亮一点的背景色 */
+ --w-e-textarea-selected-border-color: var(--el-color-info-light-5); /* 选中元素时的高亮边框 */
+ --w-e-textarea-handler-bg-color: var(--el-color-primary); /* 工具按钮或交互元素的背景色 */
+
+ /* Toolbar - css vars */
+ --w-e-toolbar-color: var(--el-text-color-regular); /* 工具栏文字颜色 */
+ --w-e-toolbar-bg-color: var(--el-bg-color); /* 工具栏背景颜色 */
+ --w-e-toolbar-active-color: var(--el-text-color-primary); /* 当前激活项的文字颜色 */
+ --w-e-toolbar-active-bg-color: var(--el-fill-color-light); /* 当前激活项的背景颜色 */
+ --w-e-toolbar-disabled-color: var(--el-text-color-secondary); /* 禁用项的颜色 */
+ --w-e-toolbar-border-color: var(--el-border-color-base); /* 工具栏边框颜色 */
+
+ /* Modal - css vars */
+ --w-e-modal-button-bg-color: var(--el-bg-color-light-3); /* 弹出框按钮背景色 */
+ --w-e-modal-button-border-color: var(--el-border-color-light); /* 弹出框按钮边框颜色 */
+}
+
+$menu-background: var(--menu-background); // 菜单背景色
+$menu-text: var(--menu-text); // 菜单文字颜色
+$menu-active-text: var(--menu-active-text); // 菜单激活文字颜色
+$menu-hover: var(--menu-hover); // 菜单悬停背景色
+$sidebar-logo-background: var(--sidebar-logo-background); // 侧边栏 Logo 背景色
+$sidebar-logo-text-color: var(--sidebar-logo-text-color); // 侧边栏 Logo 文字颜色
+
+$sidebar-width: 210px; // 侧边栏宽度
+$sidebar-width-collapsed: 54px; // 侧边栏收缩宽度
+$navbar-height: 50px; // 导航栏高度
+$tags-view-height: 34px; // TagsView 高度
diff --git a/src/styles/vxe-table.css b/src/styles/vxe-table.css
new file mode 100644
index 0000000..fc1fa4b
--- /dev/null
+++ b/src/styles/vxe-table.css
@@ -0,0 +1,92 @@
+/**
+ * @description 所有主题模式下的 Vxe Table CSS 变量
+ * @description 用 Element Plus 的 CSS 变量来覆写 Vxe Table 的 CSS 变量,目的是使 Vxe Table 支持多主题模式且样式统一
+ * @description 在此查阅所有可自定义的变量:https://github.com/x-extends/vxe-table/blob/master/styles/css-variable.scss
+ */
+
+:root {
+ /* color */
+ --vxe-font-color: var(--el-text-color-regular);
+ --vxe-primary-color: var(--el-color-primary);
+ --vxe-success-color: var(--el-color-success);
+ --vxe-info-color: var(--el-color-info);
+ --vxe-warning-color: var(--el-color-warning);
+ --vxe-danger-color: var(--el-color-danger);
+ --vxe-font-lighten-color: var(--el-text-color-primary);
+ --vxe-primary-lighten-color: var(--el-color-primary-light-3);
+ --vxe-success-lighten-color: var(--el-color-success-light-3);
+ --vxe-info-lighten-color: var(--el-color-info-light-3);
+ --vxe-warning-lighten-color: var(--el-color-warning-light-3);
+ --vxe-danger-lighten-color: var(--el-color-danger-light-3);
+ --vxe-font-darken-color: var(--el-text-color-secondary);
+ --vxe-primary-darken-color: var(--el-color-primary-dark-2);
+ --vxe-success-darken-color: var(--el-color-success-dark-2);
+ --vxe-info-darken-color: var(--el-color-info-dark-2);
+ --vxe-warning-darken-color: var(--el-color-warning-dark-2);
+ --vxe-danger-darken-color: var(--el-color-danger-dark-2);
+ --vxe-font-disabled-color: var(--el-text-color-disabled);
+ --vxe-primary-disabled-color: var(--el-color-primary-light-5);
+ --vxe-success-disabled-color: var(--el-color-success-light-5);
+ --vxe-info-disabled-color: var(--el-color-info-light-5);
+ --vxe-warning-disabled-color: var(--el-color-warning-light-5);
+ --vxe-danger-disabled-color: var(--el-color-danger-light-5);
+
+ /* input/radio/checkbox */
+ --vxe-input-border-color: var(--el-border-color);
+ --vxe-input-disabled-color: var(--el-text-color-disabled);
+ --vxe-input-disabled-background-color: var(--el-fill-color-light);
+ --vxe-input-placeholder-color: var(--el-text-color-placeholder);
+
+ /* popup */
+ --vxe-table-popup-border-color: var(--el-border-color);
+
+ /* table */
+ --vxe-table-header-font-color: var(--el-text-color-regular);
+ --vxe-table-footer-font-color: var(--el-text-color-regular);
+ --vxe-table-border-color: var(--el-border-color-lighter);
+ --vxe-table-header-background-color: var(--el-bg-color);
+ --vxe-table-body-background-color: var(--el-bg-color);
+ --vxe-table-footer-background-color: var(--el-bg-color);
+ --vxe-table-row-hover-background-color: var(--el-fill-color-light);
+ --vxe-table-row-current-background-color: var(--el-fill-color-light);
+ --vxe-table-row-hover-current-background-color: var(--el-fill-color-light);
+ --vxe-table-checkbox-range-background-color: var(--el-fill-color-light);
+
+ /* menu */
+ --vxe-table-menu-background-color: var(--el-bg-color-overlay);
+
+ /* loading */
+ --vxe-loading-color: var(--el-color-primary);
+ --vxe-loading-background-color: var(--el-mask-color);
+
+ /* validate */
+ --vxe-table-validate-error-color: var(--el-color-danger);
+
+ /* toolbar */
+ --vxe-toolbar-background-color: var(--el-bg-color);
+ --vxe-toolbar-custom-active-background-color: var(--el-bg-color-overlay);
+ --vxe-toolbar-panel-background-color: var(--el-bg-color-overlay);
+
+ /* pager */
+ --vxe-pager-background-color: var(--el-bg-color);
+
+ /* modal */
+ --vxe-modal-header-background-color: var(--el-bg-color);
+ --vxe-modal-body-background-color: var(--el-bg-color);
+ --vxe-modal-border-color: var(--el-border-color);
+
+ /* button */
+ --vxe-button-default-background-color: var(--el-bg-color-overlay);
+
+ /* input */
+ --vxe-input-background-color: var(--el-fill-color-blank);
+ --vxe-input-panel-background-color: var(--el-fill-color-blank);
+
+ /* form */
+ --vxe-form-background-color: var(--el-bg-color);
+ --vxe-form-validate-error-color: var(--el-color-danger);
+
+ /* select */
+ --vxe-select-option-hover-background-color: var(--el-bg-color-overlay);
+ --vxe-select-panel-background-color: var(--el-bg-color);
+}
diff --git a/src/styles/vxe-table.scss b/src/styles/vxe-table.scss
new file mode 100644
index 0000000..f7376ee
--- /dev/null
+++ b/src/styles/vxe-table.scss
@@ -0,0 +1,39 @@
+// 自定义 Vxe Table 样式
+
+.vxe-grid {
+ // 表单
+ &--form-wrapper {
+ .vxe-form {
+ padding: 10px 20px;
+ margin-bottom: 20px;
+ }
+ }
+
+ // 工具栏
+ &--toolbar-wrapper {
+ .vxe-toolbar {
+ padding: 20px;
+ }
+ }
+
+ // 分页
+ &--pager-wrapper {
+ .vxe-pager {
+ height: 70px;
+ padding: 0 20px;
+
+ &--wrapper {
+ // 参考 Bootstrap 的响应式设计 WIDTH = 768
+ @media screen and (width <= 768px) {
+ .vxe-pager--total,
+ .vxe-pager--sizes,
+ .vxe-pager--jump,
+ .vxe-pager--jump-prev,
+ .vxe-pager--jump-next {
+ display: none;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/types/auto-imports.d.ts b/src/types/auto-imports.d.ts
new file mode 100644
index 0000000..20b192e
--- /dev/null
+++ b/src/types/auto-imports.d.ts
@@ -0,0 +1,991 @@
+/* prettier-ignore */
+// @ts-nocheck
+// noinspection JSUnusedGlobalSymbols
+// Generated by unplugin-auto-import
+export {}
+declare global {
+ const EffectScope: (typeof import("vue"))["EffectScope"];
+ const ElForm: (typeof import("element-plus/es"))["ElForm"];
+ const ElMessage: (typeof import("element-plus/es"))["ElMessage"];
+ const ElMessageBox: (typeof import("element-plus/es"))["ElMessageBox"];
+ const ElNotification: (typeof import("element-plus/es"))["ElNotification"];
+ const ElTree: (typeof import("element-plus/es"))["ElTree"];
+ const acceptHMRUpdate: (typeof import("pinia"))["acceptHMRUpdate"];
+ const asyncComputed: (typeof import("@vueuse/core"))["asyncComputed"];
+ const autoResetRef: (typeof import("@vueuse/core"))["autoResetRef"];
+ const computed: (typeof import("vue"))["computed"];
+ const computedAsync: (typeof import("@vueuse/core"))["computedAsync"];
+ const computedEager: (typeof import("@vueuse/core"))["computedEager"];
+ const computedInject: (typeof import("@vueuse/core"))["computedInject"];
+ const computedWithControl: (typeof import("@vueuse/core"))["computedWithControl"];
+ const controlledComputed: (typeof import("@vueuse/core"))["controlledComputed"];
+ const controlledRef: (typeof import("@vueuse/core"))["controlledRef"];
+ const createApp: (typeof import("vue"))["createApp"];
+ const createEventHook: (typeof import("@vueuse/core"))["createEventHook"];
+ const createGlobalState: (typeof import("@vueuse/core"))["createGlobalState"];
+ const createInjectionState: (typeof import("@vueuse/core"))["createInjectionState"];
+ const createPinia: (typeof import("pinia"))["createPinia"];
+ const createReactiveFn: (typeof import("@vueuse/core"))["createReactiveFn"];
+ const createReusableTemplate: (typeof import("@vueuse/core"))["createReusableTemplate"];
+ const createSharedComposable: (typeof import("@vueuse/core"))["createSharedComposable"];
+ const createTemplatePromise: (typeof import("@vueuse/core"))["createTemplatePromise"];
+ const createUnrefFn: (typeof import("@vueuse/core"))["createUnrefFn"];
+ const customRef: (typeof import("vue"))["customRef"];
+ const debouncedRef: (typeof import("@vueuse/core"))["debouncedRef"];
+ const debouncedWatch: (typeof import("@vueuse/core"))["debouncedWatch"];
+ const defineAsyncComponent: (typeof import("vue"))["defineAsyncComponent"];
+ const defineComponent: (typeof import("vue"))["defineComponent"];
+ const defineStore: (typeof import("pinia"))["defineStore"];
+ const eagerComputed: (typeof import("@vueuse/core"))["eagerComputed"];
+ const effectScope: (typeof import("vue"))["effectScope"];
+ const extendRef: (typeof import("@vueuse/core"))["extendRef"];
+ const getActivePinia: (typeof import("pinia"))["getActivePinia"];
+ const getCurrentInstance: (typeof import("vue"))["getCurrentInstance"];
+ const getCurrentScope: (typeof import("vue"))["getCurrentScope"];
+ const h: (typeof import("vue"))["h"];
+ const ignorableWatch: (typeof import("@vueuse/core"))["ignorableWatch"];
+ const inject: (typeof import("vue"))["inject"];
+ const injectLocal: (typeof import("@vueuse/core"))["injectLocal"];
+ const isDefined: (typeof import("@vueuse/core"))["isDefined"];
+ const isProxy: (typeof import("vue"))["isProxy"];
+ const isReactive: (typeof import("vue"))["isReactive"];
+ const isReadonly: (typeof import("vue"))["isReadonly"];
+ const isRef: (typeof import("vue"))["isRef"];
+ const makeDestructurable: (typeof import("@vueuse/core"))["makeDestructurable"];
+ const mapActions: (typeof import("pinia"))["mapActions"];
+ const mapGetters: (typeof import("pinia"))["mapGetters"];
+ const mapState: (typeof import("pinia"))["mapState"];
+ const mapStores: (typeof import("pinia"))["mapStores"];
+ const mapWritableState: (typeof import("pinia"))["mapWritableState"];
+ const markRaw: (typeof import("vue"))["markRaw"];
+ const nextTick: (typeof import("vue"))["nextTick"];
+ const onActivated: (typeof import("vue"))["onActivated"];
+ const onBeforeMount: (typeof import("vue"))["onBeforeMount"];
+ const onBeforeRouteLeave: (typeof import("vue-router"))["onBeforeRouteLeave"];
+ const onBeforeRouteUpdate: (typeof import("vue-router"))["onBeforeRouteUpdate"];
+ const onBeforeUnmount: (typeof import("vue"))["onBeforeUnmount"];
+ const onBeforeUpdate: (typeof import("vue"))["onBeforeUpdate"];
+ const onClickOutside: (typeof import("@vueuse/core"))["onClickOutside"];
+ const onDeactivated: (typeof import("vue"))["onDeactivated"];
+ const onErrorCaptured: (typeof import("vue"))["onErrorCaptured"];
+ const onKeyStroke: (typeof import("@vueuse/core"))["onKeyStroke"];
+ const onLongPress: (typeof import("@vueuse/core"))["onLongPress"];
+ const onMounted: (typeof import("vue"))["onMounted"];
+ const onRenderTracked: (typeof import("vue"))["onRenderTracked"];
+ const onRenderTriggered: (typeof import("vue"))["onRenderTriggered"];
+ const onScopeDispose: (typeof import("vue"))["onScopeDispose"];
+ const onServerPrefetch: (typeof import("vue"))["onServerPrefetch"];
+ const onStartTyping: (typeof import("@vueuse/core"))["onStartTyping"];
+ const onUnmounted: (typeof import("vue"))["onUnmounted"];
+ const onUpdated: (typeof import("vue"))["onUpdated"];
+ const pausableWatch: (typeof import("@vueuse/core"))["pausableWatch"];
+ const provide: (typeof import("vue"))["provide"];
+ const provideLocal: (typeof import("@vueuse/core"))["provideLocal"];
+ const reactify: (typeof import("@vueuse/core"))["reactify"];
+ const reactifyObject: (typeof import("@vueuse/core"))["reactifyObject"];
+ const reactive: (typeof import("vue"))["reactive"];
+ const reactiveComputed: (typeof import("@vueuse/core"))["reactiveComputed"];
+ const reactiveOmit: (typeof import("@vueuse/core"))["reactiveOmit"];
+ const reactivePick: (typeof import("@vueuse/core"))["reactivePick"];
+ const readonly: (typeof import("vue"))["readonly"];
+ const ref: (typeof import("vue"))["ref"];
+ const refAutoReset: (typeof import("@vueuse/core"))["refAutoReset"];
+ const refDebounced: (typeof import("@vueuse/core"))["refDebounced"];
+ const refDefault: (typeof import("@vueuse/core"))["refDefault"];
+ const refThrottled: (typeof import("@vueuse/core"))["refThrottled"];
+ const refWithControl: (typeof import("@vueuse/core"))["refWithControl"];
+ const resolveComponent: (typeof import("vue"))["resolveComponent"];
+ const resolveRef: (typeof import("@vueuse/core"))["resolveRef"];
+ const resolveUnref: (typeof import("@vueuse/core"))["resolveUnref"];
+ const setActivePinia: (typeof import("pinia"))["setActivePinia"];
+ const setMapStoreSuffix: (typeof import("pinia"))["setMapStoreSuffix"];
+ const shallowReactive: (typeof import("vue"))["shallowReactive"];
+ const shallowReadonly: (typeof import("vue"))["shallowReadonly"];
+ const shallowRef: (typeof import("vue"))["shallowRef"];
+ const storeToRefs: (typeof import("pinia"))["storeToRefs"];
+ const syncRef: (typeof import("@vueuse/core"))["syncRef"];
+ const syncRefs: (typeof import("@vueuse/core"))["syncRefs"];
+ const templateRef: (typeof import("@vueuse/core"))["templateRef"];
+ const throttledRef: (typeof import("@vueuse/core"))["throttledRef"];
+ const throttledWatch: (typeof import("@vueuse/core"))["throttledWatch"];
+ const toRaw: (typeof import("vue"))["toRaw"];
+ const toReactive: (typeof import("@vueuse/core"))["toReactive"];
+ const toRef: (typeof import("vue"))["toRef"];
+ const toRefs: (typeof import("vue"))["toRefs"];
+ const toValue: (typeof import("vue"))["toValue"];
+ const triggerRef: (typeof import("vue"))["triggerRef"];
+ const tryOnBeforeMount: (typeof import("@vueuse/core"))["tryOnBeforeMount"];
+ const tryOnBeforeUnmount: (typeof import("@vueuse/core"))["tryOnBeforeUnmount"];
+ const tryOnMounted: (typeof import("@vueuse/core"))["tryOnMounted"];
+ const tryOnScopeDispose: (typeof import("@vueuse/core"))["tryOnScopeDispose"];
+ const tryOnUnmounted: (typeof import("@vueuse/core"))["tryOnUnmounted"];
+ const unref: (typeof import("vue"))["unref"];
+ const unrefElement: (typeof import("@vueuse/core"))["unrefElement"];
+ const until: (typeof import("@vueuse/core"))["until"];
+ const useActiveElement: (typeof import("@vueuse/core"))["useActiveElement"];
+ const useAnimate: (typeof import("@vueuse/core"))["useAnimate"];
+ const useArrayDifference: (typeof import("@vueuse/core"))["useArrayDifference"];
+ const useArrayEvery: (typeof import("@vueuse/core"))["useArrayEvery"];
+ const useArrayFilter: (typeof import("@vueuse/core"))["useArrayFilter"];
+ const useArrayFind: (typeof import("@vueuse/core"))["useArrayFind"];
+ const useArrayFindIndex: (typeof import("@vueuse/core"))["useArrayFindIndex"];
+ const useArrayFindLast: (typeof import("@vueuse/core"))["useArrayFindLast"];
+ const useArrayIncludes: (typeof import("@vueuse/core"))["useArrayIncludes"];
+ const useArrayJoin: (typeof import("@vueuse/core"))["useArrayJoin"];
+ const useArrayMap: (typeof import("@vueuse/core"))["useArrayMap"];
+ const useArrayReduce: (typeof import("@vueuse/core"))["useArrayReduce"];
+ const useArraySome: (typeof import("@vueuse/core"))["useArraySome"];
+ const useArrayUnique: (typeof import("@vueuse/core"))["useArrayUnique"];
+ const useAsyncQueue: (typeof import("@vueuse/core"))["useAsyncQueue"];
+ const useAsyncState: (typeof import("@vueuse/core"))["useAsyncState"];
+ const useAttrs: (typeof import("vue"))["useAttrs"];
+ const useBase64: (typeof import("@vueuse/core"))["useBase64"];
+ const useBattery: (typeof import("@vueuse/core"))["useBattery"];
+ const useBluetooth: (typeof import("@vueuse/core"))["useBluetooth"];
+ const useBreakpoints: (typeof import("@vueuse/core"))["useBreakpoints"];
+ const useBroadcastChannel: (typeof import("@vueuse/core"))["useBroadcastChannel"];
+ const useBrowserLocation: (typeof import("@vueuse/core"))["useBrowserLocation"];
+ const useCached: (typeof import("@vueuse/core"))["useCached"];
+ const useClipboard: (typeof import("@vueuse/core"))["useClipboard"];
+ const useClipboardItems: (typeof import("@vueuse/core"))["useClipboardItems"];
+ const useCloned: (typeof import("@vueuse/core"))["useCloned"];
+ const useColorMode: (typeof import("@vueuse/core"))["useColorMode"];
+ const useConfirmDialog: (typeof import("@vueuse/core"))["useConfirmDialog"];
+ const useCounter: (typeof import("@vueuse/core"))["useCounter"];
+ const useCssModule: (typeof import("vue"))["useCssModule"];
+ const useCssVar: (typeof import("@vueuse/core"))["useCssVar"];
+ const useCssVars: (typeof import("vue"))["useCssVars"];
+ const useCurrentElement: (typeof import("@vueuse/core"))["useCurrentElement"];
+ const useCycleList: (typeof import("@vueuse/core"))["useCycleList"];
+ const useDark: (typeof import("@vueuse/core"))["useDark"];
+ const useDateFormat: (typeof import("@vueuse/core"))["useDateFormat"];
+ const useDebounce: (typeof import("@vueuse/core"))["useDebounce"];
+ const useDebounceFn: (typeof import("@vueuse/core"))["useDebounceFn"];
+ const useDebouncedRefHistory: (typeof import("@vueuse/core"))["useDebouncedRefHistory"];
+ const useDeviceMotion: (typeof import("@vueuse/core"))["useDeviceMotion"];
+ const useDeviceOrientation: (typeof import("@vueuse/core"))["useDeviceOrientation"];
+ const useDevicePixelRatio: (typeof import("@vueuse/core"))["useDevicePixelRatio"];
+ const useDevicesList: (typeof import("@vueuse/core"))["useDevicesList"];
+ const useDisplayMedia: (typeof import("@vueuse/core"))["useDisplayMedia"];
+ const useDocumentVisibility: (typeof import("@vueuse/core"))["useDocumentVisibility"];
+ const useDraggable: (typeof import("@vueuse/core"))["useDraggable"];
+ const useDropZone: (typeof import("@vueuse/core"))["useDropZone"];
+ const useElementBounding: (typeof import("@vueuse/core"))["useElementBounding"];
+ const useElementByPoint: (typeof import("@vueuse/core"))["useElementByPoint"];
+ const useElementHover: (typeof import("@vueuse/core"))["useElementHover"];
+ const useElementSize: (typeof import("@vueuse/core"))["useElementSize"];
+ const useElementVisibility: (typeof import("@vueuse/core"))["useElementVisibility"];
+ const useEventBus: (typeof import("@vueuse/core"))["useEventBus"];
+ const useEventListener: (typeof import("@vueuse/core"))["useEventListener"];
+ const useEventSource: (typeof import("@vueuse/core"))["useEventSource"];
+ const useEyeDropper: (typeof import("@vueuse/core"))["useEyeDropper"];
+ const useFavicon: (typeof import("@vueuse/core"))["useFavicon"];
+ const useFetch: (typeof import("@vueuse/core"))["useFetch"];
+ const useFileDialog: (typeof import("@vueuse/core"))["useFileDialog"];
+ const useFileSystemAccess: (typeof import("@vueuse/core"))["useFileSystemAccess"];
+ const useFocus: (typeof import("@vueuse/core"))["useFocus"];
+ const useFocusWithin: (typeof import("@vueuse/core"))["useFocusWithin"];
+ const useFps: (typeof import("@vueuse/core"))["useFps"];
+ const useFullscreen: (typeof import("@vueuse/core"))["useFullscreen"];
+ const useGamepad: (typeof import("@vueuse/core"))["useGamepad"];
+ const useGeolocation: (typeof import("@vueuse/core"))["useGeolocation"];
+ const useI18n: (typeof import("vue-i18n"))["useI18n"];
+ const useIdle: (typeof import("@vueuse/core"))["useIdle"];
+ const useImage: (typeof import("@vueuse/core"))["useImage"];
+ const useInfiniteScroll: (typeof import("@vueuse/core"))["useInfiniteScroll"];
+ const useIntersectionObserver: (typeof import("@vueuse/core"))["useIntersectionObserver"];
+ const useInterval: (typeof import("@vueuse/core"))["useInterval"];
+ const useIntervalFn: (typeof import("@vueuse/core"))["useIntervalFn"];
+ const useKeyModifier: (typeof import("@vueuse/core"))["useKeyModifier"];
+ const useLastChanged: (typeof import("@vueuse/core"))["useLastChanged"];
+ const useLink: (typeof import("vue-router"))["useLink"];
+ const useLocalStorage: (typeof import("@vueuse/core"))["useLocalStorage"];
+ const useMagicKeys: (typeof import("@vueuse/core"))["useMagicKeys"];
+ const useManualRefHistory: (typeof import("@vueuse/core"))["useManualRefHistory"];
+ const useMediaControls: (typeof import("@vueuse/core"))["useMediaControls"];
+ const useMediaQuery: (typeof import("@vueuse/core"))["useMediaQuery"];
+ const useMemoize: (typeof import("@vueuse/core"))["useMemoize"];
+ const useMemory: (typeof import("@vueuse/core"))["useMemory"];
+ const useMounted: (typeof import("@vueuse/core"))["useMounted"];
+ const useMouse: (typeof import("@vueuse/core"))["useMouse"];
+ const useMouseInElement: (typeof import("@vueuse/core"))["useMouseInElement"];
+ const useMousePressed: (typeof import("@vueuse/core"))["useMousePressed"];
+ const useMutationObserver: (typeof import("@vueuse/core"))["useMutationObserver"];
+ const useNavigatorLanguage: (typeof import("@vueuse/core"))["useNavigatorLanguage"];
+ const useNetwork: (typeof import("@vueuse/core"))["useNetwork"];
+ const useNow: (typeof import("@vueuse/core"))["useNow"];
+ const useObjectUrl: (typeof import("@vueuse/core"))["useObjectUrl"];
+ const useOffsetPagination: (typeof import("@vueuse/core"))["useOffsetPagination"];
+ const useOnline: (typeof import("@vueuse/core"))["useOnline"];
+ const usePageLeave: (typeof import("@vueuse/core"))["usePageLeave"];
+ const useParallax: (typeof import("@vueuse/core"))["useParallax"];
+ const useParentElement: (typeof import("@vueuse/core"))["useParentElement"];
+ const usePerformanceObserver: (typeof import("@vueuse/core"))["usePerformanceObserver"];
+ const usePermission: (typeof import("@vueuse/core"))["usePermission"];
+ const usePointer: (typeof import("@vueuse/core"))["usePointer"];
+ const usePointerLock: (typeof import("@vueuse/core"))["usePointerLock"];
+ const usePointerSwipe: (typeof import("@vueuse/core"))["usePointerSwipe"];
+ const usePreferredColorScheme: (typeof import("@vueuse/core"))["usePreferredColorScheme"];
+ const usePreferredContrast: (typeof import("@vueuse/core"))["usePreferredContrast"];
+ const usePreferredDark: (typeof import("@vueuse/core"))["usePreferredDark"];
+ const usePreferredLanguages: (typeof import("@vueuse/core"))["usePreferredLanguages"];
+ const usePreferredReducedMotion: (typeof import("@vueuse/core"))["usePreferredReducedMotion"];
+ const usePrevious: (typeof import("@vueuse/core"))["usePrevious"];
+ const useRafFn: (typeof import("@vueuse/core"))["useRafFn"];
+ const useRefHistory: (typeof import("@vueuse/core"))["useRefHistory"];
+ const useResizeObserver: (typeof import("@vueuse/core"))["useResizeObserver"];
+ const useRoute: (typeof import("vue-router"))["useRoute"];
+ const useRouter: (typeof import("vue-router"))["useRouter"];
+ const useScreenOrientation: (typeof import("@vueuse/core"))["useScreenOrientation"];
+ const useScreenSafeArea: (typeof import("@vueuse/core"))["useScreenSafeArea"];
+ const useScriptTag: (typeof import("@vueuse/core"))["useScriptTag"];
+ const useScroll: (typeof import("@vueuse/core"))["useScroll"];
+ const useScrollLock: (typeof import("@vueuse/core"))["useScrollLock"];
+ const useSessionStorage: (typeof import("@vueuse/core"))["useSessionStorage"];
+ const useShare: (typeof import("@vueuse/core"))["useShare"];
+ const useSlots: (typeof import("vue"))["useSlots"];
+ const useSorted: (typeof import("@vueuse/core"))["useSorted"];
+ const useSpeechRecognition: (typeof import("@vueuse/core"))["useSpeechRecognition"];
+ const useSpeechSynthesis: (typeof import("@vueuse/core"))["useSpeechSynthesis"];
+ const useStepper: (typeof import("@vueuse/core"))["useStepper"];
+ const useStorage: (typeof import("@vueuse/core"))["useStorage"];
+ const useStorageAsync: (typeof import("@vueuse/core"))["useStorageAsync"];
+ const useStyleTag: (typeof import("@vueuse/core"))["useStyleTag"];
+ const useSupported: (typeof import("@vueuse/core"))["useSupported"];
+ const useSwipe: (typeof import("@vueuse/core"))["useSwipe"];
+ const useTemplateRefsList: (typeof import("@vueuse/core"))["useTemplateRefsList"];
+ const useTextDirection: (typeof import("@vueuse/core"))["useTextDirection"];
+ const useTextSelection: (typeof import("@vueuse/core"))["useTextSelection"];
+ const useTextareaAutosize: (typeof import("@vueuse/core"))["useTextareaAutosize"];
+ const useThrottle: (typeof import("@vueuse/core"))["useThrottle"];
+ const useThrottleFn: (typeof import("@vueuse/core"))["useThrottleFn"];
+ const useThrottledRefHistory: (typeof import("@vueuse/core"))["useThrottledRefHistory"];
+ const useTimeAgo: (typeof import("@vueuse/core"))["useTimeAgo"];
+ const useTimeout: (typeof import("@vueuse/core"))["useTimeout"];
+ const useTimeoutFn: (typeof import("@vueuse/core"))["useTimeoutFn"];
+ const useTimeoutPoll: (typeof import("@vueuse/core"))["useTimeoutPoll"];
+ const useTimestamp: (typeof import("@vueuse/core"))["useTimestamp"];
+ const useTitle: (typeof import("@vueuse/core"))["useTitle"];
+ const useToNumber: (typeof import("@vueuse/core"))["useToNumber"];
+ const useToString: (typeof import("@vueuse/core"))["useToString"];
+ const useToggle: (typeof import("@vueuse/core"))["useToggle"];
+ const useTransition: (typeof import("@vueuse/core"))["useTransition"];
+ const useUrlSearchParams: (typeof import("@vueuse/core"))["useUrlSearchParams"];
+ const useUserMedia: (typeof import("@vueuse/core"))["useUserMedia"];
+ const useVModel: (typeof import("@vueuse/core"))["useVModel"];
+ const useVModels: (typeof import("@vueuse/core"))["useVModels"];
+ const useVibrate: (typeof import("@vueuse/core"))["useVibrate"];
+ const useVirtualList: (typeof import("@vueuse/core"))["useVirtualList"];
+ const useWakeLock: (typeof import("@vueuse/core"))["useWakeLock"];
+ const useWebNotification: (typeof import("@vueuse/core"))["useWebNotification"];
+ const useWebSocket: (typeof import("@vueuse/core"))["useWebSocket"];
+ const useWebWorker: (typeof import("@vueuse/core"))["useWebWorker"];
+ const useWebWorkerFn: (typeof import("@vueuse/core"))["useWebWorkerFn"];
+ const useWindowFocus: (typeof import("@vueuse/core"))["useWindowFocus"];
+ const useWindowScroll: (typeof import("@vueuse/core"))["useWindowScroll"];
+ const useWindowSize: (typeof import("@vueuse/core"))["useWindowSize"];
+ const watch: (typeof import("vue"))["watch"];
+ const watchArray: (typeof import("@vueuse/core"))["watchArray"];
+ const watchAtMost: (typeof import("@vueuse/core"))["watchAtMost"];
+ const watchDebounced: (typeof import("@vueuse/core"))["watchDebounced"];
+ const watchDeep: (typeof import("@vueuse/core"))["watchDeep"];
+ const watchEffect: (typeof import("vue"))["watchEffect"];
+ const watchIgnorable: (typeof import("@vueuse/core"))["watchIgnorable"];
+ const watchImmediate: (typeof import("@vueuse/core"))["watchImmediate"];
+ const watchOnce: (typeof import("@vueuse/core"))["watchOnce"];
+ const watchPausable: (typeof import("@vueuse/core"))["watchPausable"];
+ const watchPostEffect: (typeof import("vue"))["watchPostEffect"];
+ const watchSyncEffect: (typeof import("vue"))["watchSyncEffect"];
+ const watchThrottled: (typeof import("@vueuse/core"))["watchThrottled"];
+ const watchTriggerable: (typeof import("@vueuse/core"))["watchTriggerable"];
+ const watchWithFilter: (typeof import("@vueuse/core"))["watchWithFilter"];
+ const whenever: (typeof import("@vueuse/core"))["whenever"];
+}
+// for type re-export
+declare global {
+ // @ts-ignore
+ export type {
+ Component,
+ ComponentPublicInstance,
+ ComputedRef,
+ ExtractDefaultPropTypes,
+ ExtractPropTypes,
+ ExtractPublicPropTypes,
+ InjectionKey,
+ PropType,
+ Ref,
+ VNode,
+ WritableComputedRef,
+ } from "vue";
+ import("vue");
+}
+// for vue template auto import
+import { UnwrapRef } from "vue";
+declare module "vue" {
+ interface GlobalComponents {}
+ interface ComponentCustomProperties {
+ readonly EffectScope: UnwrapRef<(typeof import("vue"))["EffectScope"]>;
+ readonly ElMessage: UnwrapRef<(typeof import("element-plus/es"))["ElMessage"]>;
+ readonly ElMessageBox: UnwrapRef<(typeof import("element-plus/es"))["ElMessageBox"]>;
+ readonly acceptHMRUpdate: UnwrapRef<(typeof import("pinia"))["acceptHMRUpdate"]>;
+ readonly asyncComputed: UnwrapRef<(typeof import("@vueuse/core"))["asyncComputed"]>;
+ readonly autoResetRef: UnwrapRef<(typeof import("@vueuse/core"))["autoResetRef"]>;
+ readonly computed: UnwrapRef<(typeof import("vue"))["computed"]>;
+ readonly computedAsync: UnwrapRef<(typeof import("@vueuse/core"))["computedAsync"]>;
+ readonly computedEager: UnwrapRef<(typeof import("@vueuse/core"))["computedEager"]>;
+ readonly computedInject: UnwrapRef<(typeof import("@vueuse/core"))["computedInject"]>;
+ readonly computedWithControl: UnwrapRef<(typeof import("@vueuse/core"))["computedWithControl"]>;
+ readonly controlledComputed: UnwrapRef<(typeof import("@vueuse/core"))["controlledComputed"]>;
+ readonly controlledRef: UnwrapRef<(typeof import("@vueuse/core"))["controlledRef"]>;
+ readonly createApp: UnwrapRef<(typeof import("vue"))["createApp"]>;
+ readonly createEventHook: UnwrapRef<(typeof import("@vueuse/core"))["createEventHook"]>;
+ readonly createGlobalState: UnwrapRef<(typeof import("@vueuse/core"))["createGlobalState"]>;
+ readonly createInjectionState: UnwrapRef<
+ (typeof import("@vueuse/core"))["createInjectionState"]
+ >;
+ readonly createPinia: UnwrapRef<(typeof import("pinia"))["createPinia"]>;
+ readonly createReactiveFn: UnwrapRef<(typeof import("@vueuse/core"))["createReactiveFn"]>;
+ readonly createReusableTemplate: UnwrapRef<
+ (typeof import("@vueuse/core"))["createReusableTemplate"]
+ >;
+ readonly createSharedComposable: UnwrapRef<
+ (typeof import("@vueuse/core"))["createSharedComposable"]
+ >;
+ readonly createTemplatePromise: UnwrapRef<
+ (typeof import("@vueuse/core"))["createTemplatePromise"]
+ >;
+ readonly createUnrefFn: UnwrapRef<(typeof import("@vueuse/core"))["createUnrefFn"]>;
+ readonly customRef: UnwrapRef<(typeof import("vue"))["customRef"]>;
+ readonly debouncedRef: UnwrapRef<(typeof import("@vueuse/core"))["debouncedRef"]>;
+ readonly debouncedWatch: UnwrapRef<(typeof import("@vueuse/core"))["debouncedWatch"]>;
+ readonly defineAsyncComponent: UnwrapRef<(typeof import("vue"))["defineAsyncComponent"]>;
+ readonly defineComponent: UnwrapRef<(typeof import("vue"))["defineComponent"]>;
+ readonly defineStore: UnwrapRef<(typeof import("pinia"))["defineStore"]>;
+ readonly eagerComputed: UnwrapRef<(typeof import("@vueuse/core"))["eagerComputed"]>;
+ readonly effectScope: UnwrapRef<(typeof import("vue"))["effectScope"]>;
+ readonly extendRef: UnwrapRef<(typeof import("@vueuse/core"))["extendRef"]>;
+ readonly getActivePinia: UnwrapRef<(typeof import("pinia"))["getActivePinia"]>;
+ readonly getCurrentInstance: UnwrapRef<(typeof import("vue"))["getCurrentInstance"]>;
+ readonly getCurrentScope: UnwrapRef<(typeof import("vue"))["getCurrentScope"]>;
+ readonly h: UnwrapRef<(typeof import("vue"))["h"]>;
+ readonly ignorableWatch: UnwrapRef<(typeof import("@vueuse/core"))["ignorableWatch"]>;
+ readonly inject: UnwrapRef<(typeof import("vue"))["inject"]>;
+ readonly injectLocal: UnwrapRef<(typeof import("@vueuse/core"))["injectLocal"]>;
+ readonly isDefined: UnwrapRef<(typeof import("@vueuse/core"))["isDefined"]>;
+ readonly isProxy: UnwrapRef<(typeof import("vue"))["isProxy"]>;
+ readonly isReactive: UnwrapRef<(typeof import("vue"))["isReactive"]>;
+ readonly isReadonly: UnwrapRef<(typeof import("vue"))["isReadonly"]>;
+ readonly isRef: UnwrapRef<(typeof import("vue"))["isRef"]>;
+ readonly makeDestructurable: UnwrapRef<(typeof import("@vueuse/core"))["makeDestructurable"]>;
+ readonly mapActions: UnwrapRef<(typeof import("pinia"))["mapActions"]>;
+ readonly mapGetters: UnwrapRef<(typeof import("pinia"))["mapGetters"]>;
+ readonly mapState: UnwrapRef<(typeof import("pinia"))["mapState"]>;
+ readonly mapStores: UnwrapRef<(typeof import("pinia"))["mapStores"]>;
+ readonly mapWritableState: UnwrapRef<(typeof import("pinia"))["mapWritableState"]>;
+ readonly markRaw: UnwrapRef<(typeof import("vue"))["markRaw"]>;
+ readonly nextTick: UnwrapRef<(typeof import("vue"))["nextTick"]>;
+ readonly onActivated: UnwrapRef<(typeof import("vue"))["onActivated"]>;
+ readonly onBeforeMount: UnwrapRef<(typeof import("vue"))["onBeforeMount"]>;
+ readonly onBeforeRouteLeave: UnwrapRef<(typeof import("vue-router"))["onBeforeRouteLeave"]>;
+ readonly onBeforeRouteUpdate: UnwrapRef<(typeof import("vue-router"))["onBeforeRouteUpdate"]>;
+ readonly onBeforeUnmount: UnwrapRef<(typeof import("vue"))["onBeforeUnmount"]>;
+ readonly onBeforeUpdate: UnwrapRef<(typeof import("vue"))["onBeforeUpdate"]>;
+ readonly onClickOutside: UnwrapRef<(typeof import("@vueuse/core"))["onClickOutside"]>;
+ readonly onDeactivated: UnwrapRef<(typeof import("vue"))["onDeactivated"]>;
+ readonly onErrorCaptured: UnwrapRef<(typeof import("vue"))["onErrorCaptured"]>;
+ readonly onKeyStroke: UnwrapRef<(typeof import("@vueuse/core"))["onKeyStroke"]>;
+ readonly onLongPress: UnwrapRef<(typeof import("@vueuse/core"))["onLongPress"]>;
+ readonly onMounted: UnwrapRef<(typeof import("vue"))["onMounted"]>;
+ readonly onRenderTracked: UnwrapRef<(typeof import("vue"))["onRenderTracked"]>;
+ readonly onRenderTriggered: UnwrapRef<(typeof import("vue"))["onRenderTriggered"]>;
+ readonly onScopeDispose: UnwrapRef<(typeof import("vue"))["onScopeDispose"]>;
+ readonly onServerPrefetch: UnwrapRef<(typeof import("vue"))["onServerPrefetch"]>;
+ readonly onStartTyping: UnwrapRef<(typeof import("@vueuse/core"))["onStartTyping"]>;
+ readonly onUnmounted: UnwrapRef<(typeof import("vue"))["onUnmounted"]>;
+ readonly onUpdated: UnwrapRef<(typeof import("vue"))["onUpdated"]>;
+ readonly pausableWatch: UnwrapRef<(typeof import("@vueuse/core"))["pausableWatch"]>;
+ readonly provide: UnwrapRef<(typeof import("vue"))["provide"]>;
+ readonly provideLocal: UnwrapRef<(typeof import("@vueuse/core"))["provideLocal"]>;
+ readonly reactify: UnwrapRef<(typeof import("@vueuse/core"))["reactify"]>;
+ readonly reactifyObject: UnwrapRef<(typeof import("@vueuse/core"))["reactifyObject"]>;
+ readonly reactive: UnwrapRef<(typeof import("vue"))["reactive"]>;
+ readonly reactiveComputed: UnwrapRef<(typeof import("@vueuse/core"))["reactiveComputed"]>;
+ readonly reactiveOmit: UnwrapRef<(typeof import("@vueuse/core"))["reactiveOmit"]>;
+ readonly reactivePick: UnwrapRef<(typeof import("@vueuse/core"))["reactivePick"]>;
+ readonly readonly: UnwrapRef<(typeof import("vue"))["readonly"]>;
+ readonly ref: UnwrapRef<(typeof import("vue"))["ref"]>;
+ readonly refAutoReset: UnwrapRef<(typeof import("@vueuse/core"))["refAutoReset"]>;
+ readonly refDebounced: UnwrapRef<(typeof import("@vueuse/core"))["refDebounced"]>;
+ readonly refDefault: UnwrapRef<(typeof import("@vueuse/core"))["refDefault"]>;
+ readonly refThrottled: UnwrapRef<(typeof import("@vueuse/core"))["refThrottled"]>;
+ readonly refWithControl: UnwrapRef<(typeof import("@vueuse/core"))["refWithControl"]>;
+ readonly resolveComponent: UnwrapRef<(typeof import("vue"))["resolveComponent"]>;
+ readonly resolveRef: UnwrapRef<(typeof import("@vueuse/core"))["resolveRef"]>;
+ readonly resolveUnref: UnwrapRef<(typeof import("@vueuse/core"))["resolveUnref"]>;
+ readonly setActivePinia: UnwrapRef<(typeof import("pinia"))["setActivePinia"]>;
+ readonly setMapStoreSuffix: UnwrapRef<(typeof import("pinia"))["setMapStoreSuffix"]>;
+ readonly shallowReactive: UnwrapRef<(typeof import("vue"))["shallowReactive"]>;
+ readonly shallowReadonly: UnwrapRef<(typeof import("vue"))["shallowReadonly"]>;
+ readonly shallowRef: UnwrapRef<(typeof import("vue"))["shallowRef"]>;
+ readonly storeToRefs: UnwrapRef<(typeof import("pinia"))["storeToRefs"]>;
+ readonly syncRef: UnwrapRef<(typeof import("@vueuse/core"))["syncRef"]>;
+ readonly syncRefs: UnwrapRef<(typeof import("@vueuse/core"))["syncRefs"]>;
+ readonly templateRef: UnwrapRef<(typeof import("@vueuse/core"))["templateRef"]>;
+ readonly throttledRef: UnwrapRef<(typeof import("@vueuse/core"))["throttledRef"]>;
+ readonly throttledWatch: UnwrapRef<(typeof import("@vueuse/core"))["throttledWatch"]>;
+ readonly toRaw: UnwrapRef<(typeof import("vue"))["toRaw"]>;
+ readonly toReactive: UnwrapRef<(typeof import("@vueuse/core"))["toReactive"]>;
+ readonly toRef: UnwrapRef<(typeof import("vue"))["toRef"]>;
+ readonly toRefs: UnwrapRef<(typeof import("vue"))["toRefs"]>;
+ readonly toValue: UnwrapRef<(typeof import("vue"))["toValue"]>;
+ readonly triggerRef: UnwrapRef<(typeof import("vue"))["triggerRef"]>;
+ readonly tryOnBeforeMount: UnwrapRef<(typeof import("@vueuse/core"))["tryOnBeforeMount"]>;
+ readonly tryOnBeforeUnmount: UnwrapRef<(typeof import("@vueuse/core"))["tryOnBeforeUnmount"]>;
+ readonly tryOnMounted: UnwrapRef<(typeof import("@vueuse/core"))["tryOnMounted"]>;
+ readonly tryOnScopeDispose: UnwrapRef<(typeof import("@vueuse/core"))["tryOnScopeDispose"]>;
+ readonly tryOnUnmounted: UnwrapRef<(typeof import("@vueuse/core"))["tryOnUnmounted"]>;
+ readonly unref: UnwrapRef<(typeof import("vue"))["unref"]>;
+ readonly unrefElement: UnwrapRef<(typeof import("@vueuse/core"))["unrefElement"]>;
+ readonly until: UnwrapRef<(typeof import("@vueuse/core"))["until"]>;
+ readonly useActiveElement: UnwrapRef<(typeof import("@vueuse/core"))["useActiveElement"]>;
+ readonly useAnimate: UnwrapRef<(typeof import("@vueuse/core"))["useAnimate"]>;
+ readonly useArrayDifference: UnwrapRef<(typeof import("@vueuse/core"))["useArrayDifference"]>;
+ readonly useArrayEvery: UnwrapRef<(typeof import("@vueuse/core"))["useArrayEvery"]>;
+ readonly useArrayFilter: UnwrapRef<(typeof import("@vueuse/core"))["useArrayFilter"]>;
+ readonly useArrayFind: UnwrapRef<(typeof import("@vueuse/core"))["useArrayFind"]>;
+ readonly useArrayFindIndex: UnwrapRef<(typeof import("@vueuse/core"))["useArrayFindIndex"]>;
+ readonly useArrayFindLast: UnwrapRef<(typeof import("@vueuse/core"))["useArrayFindLast"]>;
+ readonly useArrayIncludes: UnwrapRef<(typeof import("@vueuse/core"))["useArrayIncludes"]>;
+ readonly useArrayJoin: UnwrapRef<(typeof import("@vueuse/core"))["useArrayJoin"]>;
+ readonly useArrayMap: UnwrapRef<(typeof import("@vueuse/core"))["useArrayMap"]>;
+ readonly useArrayReduce: UnwrapRef<(typeof import("@vueuse/core"))["useArrayReduce"]>;
+ readonly useArraySome: UnwrapRef<(typeof import("@vueuse/core"))["useArraySome"]>;
+ readonly useArrayUnique: UnwrapRef<(typeof import("@vueuse/core"))["useArrayUnique"]>;
+ readonly useAsyncQueue: UnwrapRef<(typeof import("@vueuse/core"))["useAsyncQueue"]>;
+ readonly useAsyncState: UnwrapRef<(typeof import("@vueuse/core"))["useAsyncState"]>;
+ readonly useAttrs: UnwrapRef<(typeof import("vue"))["useAttrs"]>;
+ readonly useBase64: UnwrapRef<(typeof import("@vueuse/core"))["useBase64"]>;
+ readonly useBattery: UnwrapRef<(typeof import("@vueuse/core"))["useBattery"]>;
+ readonly useBluetooth: UnwrapRef<(typeof import("@vueuse/core"))["useBluetooth"]>;
+ readonly useBreakpoints: UnwrapRef<(typeof import("@vueuse/core"))["useBreakpoints"]>;
+ readonly useBroadcastChannel: UnwrapRef<(typeof import("@vueuse/core"))["useBroadcastChannel"]>;
+ readonly useBrowserLocation: UnwrapRef<(typeof import("@vueuse/core"))["useBrowserLocation"]>;
+ readonly useCached: UnwrapRef<(typeof import("@vueuse/core"))["useCached"]>;
+ readonly useClipboard: UnwrapRef<(typeof import("@vueuse/core"))["useClipboard"]>;
+ readonly useClipboardItems: UnwrapRef<(typeof import("@vueuse/core"))["useClipboardItems"]>;
+ readonly useCloned: UnwrapRef<(typeof import("@vueuse/core"))["useCloned"]>;
+ readonly useColorMode: UnwrapRef<(typeof import("@vueuse/core"))["useColorMode"]>;
+ readonly useConfirmDialog: UnwrapRef<(typeof import("@vueuse/core"))["useConfirmDialog"]>;
+ readonly useCounter: UnwrapRef<(typeof import("@vueuse/core"))["useCounter"]>;
+ readonly useCssModule: UnwrapRef<(typeof import("vue"))["useCssModule"]>;
+ readonly useCssVar: UnwrapRef<(typeof import("@vueuse/core"))["useCssVar"]>;
+ readonly useCssVars: UnwrapRef<(typeof import("vue"))["useCssVars"]>;
+ readonly useCurrentElement: UnwrapRef<(typeof import("@vueuse/core"))["useCurrentElement"]>;
+ readonly useCycleList: UnwrapRef<(typeof import("@vueuse/core"))["useCycleList"]>;
+ readonly useDark: UnwrapRef<(typeof import("@vueuse/core"))["useDark"]>;
+ readonly useDateFormat: UnwrapRef<(typeof import("@vueuse/core"))["useDateFormat"]>;
+ readonly useDebounce: UnwrapRef<(typeof import("@vueuse/core"))["useDebounce"]>;
+ readonly useDebounceFn: UnwrapRef<(typeof import("@vueuse/core"))["useDebounceFn"]>;
+ readonly useDebouncedRefHistory: UnwrapRef<
+ (typeof import("@vueuse/core"))["useDebouncedRefHistory"]
+ >;
+ readonly useDeviceMotion: UnwrapRef<(typeof import("@vueuse/core"))["useDeviceMotion"]>;
+ readonly useDeviceOrientation: UnwrapRef<
+ (typeof import("@vueuse/core"))["useDeviceOrientation"]
+ >;
+ readonly useDevicePixelRatio: UnwrapRef<(typeof import("@vueuse/core"))["useDevicePixelRatio"]>;
+ readonly useDevicesList: UnwrapRef<(typeof import("@vueuse/core"))["useDevicesList"]>;
+ readonly useDisplayMedia: UnwrapRef<(typeof import("@vueuse/core"))["useDisplayMedia"]>;
+ readonly useDocumentVisibility: UnwrapRef<
+ (typeof import("@vueuse/core"))["useDocumentVisibility"]
+ >;
+ readonly useDraggable: UnwrapRef<(typeof import("@vueuse/core"))["useDraggable"]>;
+ readonly useDropZone: UnwrapRef<(typeof import("@vueuse/core"))["useDropZone"]>;
+ readonly useElementBounding: UnwrapRef<(typeof import("@vueuse/core"))["useElementBounding"]>;
+ readonly useElementByPoint: UnwrapRef<(typeof import("@vueuse/core"))["useElementByPoint"]>;
+ readonly useElementHover: UnwrapRef<(typeof import("@vueuse/core"))["useElementHover"]>;
+ readonly useElementSize: UnwrapRef<(typeof import("@vueuse/core"))["useElementSize"]>;
+ readonly useElementVisibility: UnwrapRef<
+ (typeof import("@vueuse/core"))["useElementVisibility"]
+ >;
+ readonly useEventBus: UnwrapRef<(typeof import("@vueuse/core"))["useEventBus"]>;
+ readonly useEventListener: UnwrapRef<(typeof import("@vueuse/core"))["useEventListener"]>;
+ readonly useEventSource: UnwrapRef<(typeof import("@vueuse/core"))["useEventSource"]>;
+ readonly useEyeDropper: UnwrapRef<(typeof import("@vueuse/core"))["useEyeDropper"]>;
+ readonly useFavicon: UnwrapRef<(typeof import("@vueuse/core"))["useFavicon"]>;
+ readonly useFetch: UnwrapRef<(typeof import("@vueuse/core"))["useFetch"]>;
+ readonly useFileDialog: UnwrapRef<(typeof import("@vueuse/core"))["useFileDialog"]>;
+ readonly useFileSystemAccess: UnwrapRef<(typeof import("@vueuse/core"))["useFileSystemAccess"]>;
+ readonly useFocus: UnwrapRef<(typeof import("@vueuse/core"))["useFocus"]>;
+ readonly useFocusWithin: UnwrapRef<(typeof import("@vueuse/core"))["useFocusWithin"]>;
+ readonly useFps: UnwrapRef<(typeof import("@vueuse/core"))["useFps"]>;
+ readonly useFullscreen: UnwrapRef<(typeof import("@vueuse/core"))["useFullscreen"]>;
+ readonly useGamepad: UnwrapRef<(typeof import("@vueuse/core"))["useGamepad"]>;
+ readonly useGeolocation: UnwrapRef<(typeof import("@vueuse/core"))["useGeolocation"]>;
+ readonly useI18n: UnwrapRef<(typeof import("vue-i18n"))["useI18n"]>;
+ readonly useIdle: UnwrapRef<(typeof import("@vueuse/core"))["useIdle"]>;
+ readonly useImage: UnwrapRef<(typeof import("@vueuse/core"))["useImage"]>;
+ readonly useInfiniteScroll: UnwrapRef<(typeof import("@vueuse/core"))["useInfiniteScroll"]>;
+ readonly useIntersectionObserver: UnwrapRef<
+ (typeof import("@vueuse/core"))["useIntersectionObserver"]
+ >;
+ readonly useInterval: UnwrapRef<(typeof import("@vueuse/core"))["useInterval"]>;
+ readonly useIntervalFn: UnwrapRef<(typeof import("@vueuse/core"))["useIntervalFn"]>;
+ readonly useKeyModifier: UnwrapRef<(typeof import("@vueuse/core"))["useKeyModifier"]>;
+ readonly useLastChanged: UnwrapRef<(typeof import("@vueuse/core"))["useLastChanged"]>;
+ readonly useLink: UnwrapRef<(typeof import("vue-router"))["useLink"]>;
+ readonly useLocalStorage: UnwrapRef<(typeof import("@vueuse/core"))["useLocalStorage"]>;
+ readonly useMagicKeys: UnwrapRef<(typeof import("@vueuse/core"))["useMagicKeys"]>;
+ readonly useManualRefHistory: UnwrapRef<(typeof import("@vueuse/core"))["useManualRefHistory"]>;
+ readonly useMediaControls: UnwrapRef<(typeof import("@vueuse/core"))["useMediaControls"]>;
+ readonly useMediaQuery: UnwrapRef<(typeof import("@vueuse/core"))["useMediaQuery"]>;
+ readonly useMemoize: UnwrapRef<(typeof import("@vueuse/core"))["useMemoize"]>;
+ readonly useMemory: UnwrapRef<(typeof import("@vueuse/core"))["useMemory"]>;
+ readonly useMounted: UnwrapRef<(typeof import("@vueuse/core"))["useMounted"]>;
+ readonly useMouse: UnwrapRef<(typeof import("@vueuse/core"))["useMouse"]>;
+ readonly useMouseInElement: UnwrapRef<(typeof import("@vueuse/core"))["useMouseInElement"]>;
+ readonly useMousePressed: UnwrapRef<(typeof import("@vueuse/core"))["useMousePressed"]>;
+ readonly useMutationObserver: UnwrapRef<(typeof import("@vueuse/core"))["useMutationObserver"]>;
+ readonly useNavigatorLanguage: UnwrapRef<
+ (typeof import("@vueuse/core"))["useNavigatorLanguage"]
+ >;
+ readonly useNetwork: UnwrapRef<(typeof import("@vueuse/core"))["useNetwork"]>;
+ readonly useNow: UnwrapRef<(typeof import("@vueuse/core"))["useNow"]>;
+ readonly useObjectUrl: UnwrapRef<(typeof import("@vueuse/core"))["useObjectUrl"]>;
+ readonly useOffsetPagination: UnwrapRef<(typeof import("@vueuse/core"))["useOffsetPagination"]>;
+ readonly useOnline: UnwrapRef<(typeof import("@vueuse/core"))["useOnline"]>;
+ readonly usePageLeave: UnwrapRef<(typeof import("@vueuse/core"))["usePageLeave"]>;
+ readonly useParallax: UnwrapRef<(typeof import("@vueuse/core"))["useParallax"]>;
+ readonly useParentElement: UnwrapRef<(typeof import("@vueuse/core"))["useParentElement"]>;
+ readonly usePerformanceObserver: UnwrapRef<
+ (typeof import("@vueuse/core"))["usePerformanceObserver"]
+ >;
+ readonly usePermission: UnwrapRef<(typeof import("@vueuse/core"))["usePermission"]>;
+ readonly usePointer: UnwrapRef<(typeof import("@vueuse/core"))["usePointer"]>;
+ readonly usePointerLock: UnwrapRef<(typeof import("@vueuse/core"))["usePointerLock"]>;
+ readonly usePointerSwipe: UnwrapRef<(typeof import("@vueuse/core"))["usePointerSwipe"]>;
+ readonly usePreferredColorScheme: UnwrapRef<
+ (typeof import("@vueuse/core"))["usePreferredColorScheme"]
+ >;
+ readonly usePreferredContrast: UnwrapRef<
+ (typeof import("@vueuse/core"))["usePreferredContrast"]
+ >;
+ readonly usePreferredDark: UnwrapRef<(typeof import("@vueuse/core"))["usePreferredDark"]>;
+ readonly usePreferredLanguages: UnwrapRef<
+ (typeof import("@vueuse/core"))["usePreferredLanguages"]
+ >;
+ readonly usePreferredReducedMotion: UnwrapRef<
+ (typeof import("@vueuse/core"))["usePreferredReducedMotion"]
+ >;
+ readonly usePrevious: UnwrapRef<(typeof import("@vueuse/core"))["usePrevious"]>;
+ readonly useRafFn: UnwrapRef<(typeof import("@vueuse/core"))["useRafFn"]>;
+ readonly useRefHistory: UnwrapRef<(typeof import("@vueuse/core"))["useRefHistory"]>;
+ readonly useResizeObserver: UnwrapRef<(typeof import("@vueuse/core"))["useResizeObserver"]>;
+ readonly useRoute: UnwrapRef<(typeof import("vue-router"))["useRoute"]>;
+ readonly useRouter: UnwrapRef<(typeof import("vue-router"))["useRouter"]>;
+ readonly useScreenOrientation: UnwrapRef<
+ (typeof import("@vueuse/core"))["useScreenOrientation"]
+ >;
+ readonly useScreenSafeArea: UnwrapRef<(typeof import("@vueuse/core"))["useScreenSafeArea"]>;
+ readonly useScriptTag: UnwrapRef<(typeof import("@vueuse/core"))["useScriptTag"]>;
+ readonly useScroll: UnwrapRef<(typeof import("@vueuse/core"))["useScroll"]>;
+ readonly useScrollLock: UnwrapRef<(typeof import("@vueuse/core"))["useScrollLock"]>;
+ readonly useSessionStorage: UnwrapRef<(typeof import("@vueuse/core"))["useSessionStorage"]>;
+ readonly useShare: UnwrapRef<(typeof import("@vueuse/core"))["useShare"]>;
+ readonly useSlots: UnwrapRef<(typeof import("vue"))["useSlots"]>;
+ readonly useSorted: UnwrapRef<(typeof import("@vueuse/core"))["useSorted"]>;
+ readonly useSpeechRecognition: UnwrapRef<
+ (typeof import("@vueuse/core"))["useSpeechRecognition"]
+ >;
+ readonly useSpeechSynthesis: UnwrapRef<(typeof import("@vueuse/core"))["useSpeechSynthesis"]>;
+ readonly useStepper: UnwrapRef<(typeof import("@vueuse/core"))["useStepper"]>;
+ readonly useStorage: UnwrapRef<(typeof import("@vueuse/core"))["useStorage"]>;
+ readonly useStorageAsync: UnwrapRef<(typeof import("@vueuse/core"))["useStorageAsync"]>;
+ readonly useStyleTag: UnwrapRef<(typeof import("@vueuse/core"))["useStyleTag"]>;
+ readonly useSupported: UnwrapRef<(typeof import("@vueuse/core"))["useSupported"]>;
+ readonly useSwipe: UnwrapRef<(typeof import("@vueuse/core"))["useSwipe"]>;
+ readonly useTemplateRefsList: UnwrapRef<(typeof import("@vueuse/core"))["useTemplateRefsList"]>;
+ readonly useTextDirection: UnwrapRef<(typeof import("@vueuse/core"))["useTextDirection"]>;
+ readonly useTextSelection: UnwrapRef<(typeof import("@vueuse/core"))["useTextSelection"]>;
+ readonly useTextareaAutosize: UnwrapRef<(typeof import("@vueuse/core"))["useTextareaAutosize"]>;
+ readonly useThrottle: UnwrapRef<(typeof import("@vueuse/core"))["useThrottle"]>;
+ readonly useThrottleFn: UnwrapRef<(typeof import("@vueuse/core"))["useThrottleFn"]>;
+ readonly useThrottledRefHistory: UnwrapRef<
+ (typeof import("@vueuse/core"))["useThrottledRefHistory"]
+ >;
+ readonly useTimeAgo: UnwrapRef<(typeof import("@vueuse/core"))["useTimeAgo"]>;
+ readonly useTimeout: UnwrapRef<(typeof import("@vueuse/core"))["useTimeout"]>;
+ readonly useTimeoutFn: UnwrapRef<(typeof import("@vueuse/core"))["useTimeoutFn"]>;
+ readonly useTimeoutPoll: UnwrapRef<(typeof import("@vueuse/core"))["useTimeoutPoll"]>;
+ readonly useTimestamp: UnwrapRef<(typeof import("@vueuse/core"))["useTimestamp"]>;
+ readonly useTitle: UnwrapRef<(typeof import("@vueuse/core"))["useTitle"]>;
+ readonly useToNumber: UnwrapRef<(typeof import("@vueuse/core"))["useToNumber"]>;
+ readonly useToString: UnwrapRef<(typeof import("@vueuse/core"))["useToString"]>;
+ readonly useToggle: UnwrapRef<(typeof import("@vueuse/core"))["useToggle"]>;
+ readonly useTransition: UnwrapRef<(typeof import("@vueuse/core"))["useTransition"]>;
+ readonly useUrlSearchParams: UnwrapRef<(typeof import("@vueuse/core"))["useUrlSearchParams"]>;
+ readonly useUserMedia: UnwrapRef<(typeof import("@vueuse/core"))["useUserMedia"]>;
+ readonly useVModel: UnwrapRef<(typeof import("@vueuse/core"))["useVModel"]>;
+ readonly useVModels: UnwrapRef<(typeof import("@vueuse/core"))["useVModels"]>;
+ readonly useVibrate: UnwrapRef<(typeof import("@vueuse/core"))["useVibrate"]>;
+ readonly useVirtualList: UnwrapRef<(typeof import("@vueuse/core"))["useVirtualList"]>;
+ readonly useWakeLock: UnwrapRef<(typeof import("@vueuse/core"))["useWakeLock"]>;
+ readonly useWebNotification: UnwrapRef<(typeof import("@vueuse/core"))["useWebNotification"]>;
+ readonly useWebSocket: UnwrapRef<(typeof import("@vueuse/core"))["useWebSocket"]>;
+ readonly useWebWorker: UnwrapRef<(typeof import("@vueuse/core"))["useWebWorker"]>;
+ readonly useWebWorkerFn: UnwrapRef<(typeof import("@vueuse/core"))["useWebWorkerFn"]>;
+ readonly useWindowFocus: UnwrapRef<(typeof import("@vueuse/core"))["useWindowFocus"]>;
+ readonly useWindowScroll: UnwrapRef<(typeof import("@vueuse/core"))["useWindowScroll"]>;
+ readonly useWindowSize: UnwrapRef<(typeof import("@vueuse/core"))["useWindowSize"]>;
+ readonly watch: UnwrapRef<(typeof import("vue"))["watch"]>;
+ readonly watchArray: UnwrapRef<(typeof import("@vueuse/core"))["watchArray"]>;
+ readonly watchAtMost: UnwrapRef<(typeof import("@vueuse/core"))["watchAtMost"]>;
+ readonly watchDebounced: UnwrapRef<(typeof import("@vueuse/core"))["watchDebounced"]>;
+ readonly watchDeep: UnwrapRef<(typeof import("@vueuse/core"))["watchDeep"]>;
+ readonly watchEffect: UnwrapRef<(typeof import("vue"))["watchEffect"]>;
+ readonly watchIgnorable: UnwrapRef<(typeof import("@vueuse/core"))["watchIgnorable"]>;
+ readonly watchImmediate: UnwrapRef<(typeof import("@vueuse/core"))["watchImmediate"]>;
+ readonly watchOnce: UnwrapRef<(typeof import("@vueuse/core"))["watchOnce"]>;
+ readonly watchPausable: UnwrapRef<(typeof import("@vueuse/core"))["watchPausable"]>;
+ readonly watchPostEffect: UnwrapRef<(typeof import("vue"))["watchPostEffect"]>;
+ readonly watchSyncEffect: UnwrapRef<(typeof import("vue"))["watchSyncEffect"]>;
+ readonly watchThrottled: UnwrapRef<(typeof import("@vueuse/core"))["watchThrottled"]>;
+ readonly watchTriggerable: UnwrapRef<(typeof import("@vueuse/core"))["watchTriggerable"]>;
+ readonly watchWithFilter: UnwrapRef<(typeof import("@vueuse/core"))["watchWithFilter"]>;
+ readonly whenever: UnwrapRef<(typeof import("@vueuse/core"))["whenever"]>;
+ }
+}
+declare module "@vue/runtime-core" {
+ interface GlobalComponents {}
+ interface ComponentCustomProperties {
+ readonly EffectScope: UnwrapRef<(typeof import("vue"))["EffectScope"]>;
+ readonly ElMessage: UnwrapRef<(typeof import("element-plus/es"))["ElMessage"]>;
+ readonly ElMessageBox: UnwrapRef<(typeof import("element-plus/es"))["ElMessageBox"]>;
+ readonly acceptHMRUpdate: UnwrapRef<(typeof import("pinia"))["acceptHMRUpdate"]>;
+ readonly asyncComputed: UnwrapRef<(typeof import("@vueuse/core"))["asyncComputed"]>;
+ readonly autoResetRef: UnwrapRef<(typeof import("@vueuse/core"))["autoResetRef"]>;
+ readonly computed: UnwrapRef<(typeof import("vue"))["computed"]>;
+ readonly computedAsync: UnwrapRef<(typeof import("@vueuse/core"))["computedAsync"]>;
+ readonly computedEager: UnwrapRef<(typeof import("@vueuse/core"))["computedEager"]>;
+ readonly computedInject: UnwrapRef<(typeof import("@vueuse/core"))["computedInject"]>;
+ readonly computedWithControl: UnwrapRef<(typeof import("@vueuse/core"))["computedWithControl"]>;
+ readonly controlledComputed: UnwrapRef<(typeof import("@vueuse/core"))["controlledComputed"]>;
+ readonly controlledRef: UnwrapRef<(typeof import("@vueuse/core"))["controlledRef"]>;
+ readonly createApp: UnwrapRef<(typeof import("vue"))["createApp"]>;
+ readonly createEventHook: UnwrapRef<(typeof import("@vueuse/core"))["createEventHook"]>;
+ readonly createGlobalState: UnwrapRef<(typeof import("@vueuse/core"))["createGlobalState"]>;
+ readonly createInjectionState: UnwrapRef<
+ (typeof import("@vueuse/core"))["createInjectionState"]
+ >;
+ readonly createPinia: UnwrapRef<(typeof import("pinia"))["createPinia"]>;
+ readonly createReactiveFn: UnwrapRef<(typeof import("@vueuse/core"))["createReactiveFn"]>;
+ readonly createReusableTemplate: UnwrapRef<
+ (typeof import("@vueuse/core"))["createReusableTemplate"]
+ >;
+ readonly createSharedComposable: UnwrapRef<
+ (typeof import("@vueuse/core"))["createSharedComposable"]
+ >;
+ readonly createTemplatePromise: UnwrapRef<
+ (typeof import("@vueuse/core"))["createTemplatePromise"]
+ >;
+ readonly createUnrefFn: UnwrapRef<(typeof import("@vueuse/core"))["createUnrefFn"]>;
+ readonly customRef: UnwrapRef<(typeof import("vue"))["customRef"]>;
+ readonly debouncedRef: UnwrapRef<(typeof import("@vueuse/core"))["debouncedRef"]>;
+ readonly debouncedWatch: UnwrapRef<(typeof import("@vueuse/core"))["debouncedWatch"]>;
+ readonly defineAsyncComponent: UnwrapRef<(typeof import("vue"))["defineAsyncComponent"]>;
+ readonly defineComponent: UnwrapRef<(typeof import("vue"))["defineComponent"]>;
+ readonly defineStore: UnwrapRef<(typeof import("pinia"))["defineStore"]>;
+ readonly eagerComputed: UnwrapRef<(typeof import("@vueuse/core"))["eagerComputed"]>;
+ readonly effectScope: UnwrapRef<(typeof import("vue"))["effectScope"]>;
+ readonly extendRef: UnwrapRef<(typeof import("@vueuse/core"))["extendRef"]>;
+ readonly getActivePinia: UnwrapRef<(typeof import("pinia"))["getActivePinia"]>;
+ readonly getCurrentInstance: UnwrapRef<(typeof import("vue"))["getCurrentInstance"]>;
+ readonly getCurrentScope: UnwrapRef<(typeof import("vue"))["getCurrentScope"]>;
+ readonly h: UnwrapRef<(typeof import("vue"))["h"]>;
+ readonly ignorableWatch: UnwrapRef<(typeof import("@vueuse/core"))["ignorableWatch"]>;
+ readonly inject: UnwrapRef<(typeof import("vue"))["inject"]>;
+ readonly injectLocal: UnwrapRef<(typeof import("@vueuse/core"))["injectLocal"]>;
+ readonly isDefined: UnwrapRef<(typeof import("@vueuse/core"))["isDefined"]>;
+ readonly isProxy: UnwrapRef<(typeof import("vue"))["isProxy"]>;
+ readonly isReactive: UnwrapRef<(typeof import("vue"))["isReactive"]>;
+ readonly isReadonly: UnwrapRef<(typeof import("vue"))["isReadonly"]>;
+ readonly isRef: UnwrapRef<(typeof import("vue"))["isRef"]>;
+ readonly makeDestructurable: UnwrapRef<(typeof import("@vueuse/core"))["makeDestructurable"]>;
+ readonly mapActions: UnwrapRef<(typeof import("pinia"))["mapActions"]>;
+ readonly mapGetters: UnwrapRef<(typeof import("pinia"))["mapGetters"]>;
+ readonly mapState: UnwrapRef<(typeof import("pinia"))["mapState"]>;
+ readonly mapStores: UnwrapRef<(typeof import("pinia"))["mapStores"]>;
+ readonly mapWritableState: UnwrapRef<(typeof import("pinia"))["mapWritableState"]>;
+ readonly markRaw: UnwrapRef<(typeof import("vue"))["markRaw"]>;
+ readonly nextTick: UnwrapRef<(typeof import("vue"))["nextTick"]>;
+ readonly onActivated: UnwrapRef<(typeof import("vue"))["onActivated"]>;
+ readonly onBeforeMount: UnwrapRef<(typeof import("vue"))["onBeforeMount"]>;
+ readonly onBeforeRouteLeave: UnwrapRef<(typeof import("vue-router"))["onBeforeRouteLeave"]>;
+ readonly onBeforeRouteUpdate: UnwrapRef<(typeof import("vue-router"))["onBeforeRouteUpdate"]>;
+ readonly onBeforeUnmount: UnwrapRef<(typeof import("vue"))["onBeforeUnmount"]>;
+ readonly onBeforeUpdate: UnwrapRef<(typeof import("vue"))["onBeforeUpdate"]>;
+ readonly onClickOutside: UnwrapRef<(typeof import("@vueuse/core"))["onClickOutside"]>;
+ readonly onDeactivated: UnwrapRef<(typeof import("vue"))["onDeactivated"]>;
+ readonly onErrorCaptured: UnwrapRef<(typeof import("vue"))["onErrorCaptured"]>;
+ readonly onKeyStroke: UnwrapRef<(typeof import("@vueuse/core"))["onKeyStroke"]>;
+ readonly onLongPress: UnwrapRef<(typeof import("@vueuse/core"))["onLongPress"]>;
+ readonly onMounted: UnwrapRef<(typeof import("vue"))["onMounted"]>;
+ readonly onRenderTracked: UnwrapRef<(typeof import("vue"))["onRenderTracked"]>;
+ readonly onRenderTriggered: UnwrapRef<(typeof import("vue"))["onRenderTriggered"]>;
+ readonly onScopeDispose: UnwrapRef<(typeof import("vue"))["onScopeDispose"]>;
+ readonly onServerPrefetch: UnwrapRef<(typeof import("vue"))["onServerPrefetch"]>;
+ readonly onStartTyping: UnwrapRef<(typeof import("@vueuse/core"))["onStartTyping"]>;
+ readonly onUnmounted: UnwrapRef<(typeof import("vue"))["onUnmounted"]>;
+ readonly onUpdated: UnwrapRef<(typeof import("vue"))["onUpdated"]>;
+ readonly pausableWatch: UnwrapRef<(typeof import("@vueuse/core"))["pausableWatch"]>;
+ readonly provide: UnwrapRef<(typeof import("vue"))["provide"]>;
+ readonly provideLocal: UnwrapRef<(typeof import("@vueuse/core"))["provideLocal"]>;
+ readonly reactify: UnwrapRef<(typeof import("@vueuse/core"))["reactify"]>;
+ readonly reactifyObject: UnwrapRef<(typeof import("@vueuse/core"))["reactifyObject"]>;
+ readonly reactive: UnwrapRef<(typeof import("vue"))["reactive"]>;
+ readonly reactiveComputed: UnwrapRef<(typeof import("@vueuse/core"))["reactiveComputed"]>;
+ readonly reactiveOmit: UnwrapRef<(typeof import("@vueuse/core"))["reactiveOmit"]>;
+ readonly reactivePick: UnwrapRef<(typeof import("@vueuse/core"))["reactivePick"]>;
+ readonly readonly: UnwrapRef<(typeof import("vue"))["readonly"]>;
+ readonly ref: UnwrapRef<(typeof import("vue"))["ref"]>;
+ readonly refAutoReset: UnwrapRef<(typeof import("@vueuse/core"))["refAutoReset"]>;
+ readonly refDebounced: UnwrapRef<(typeof import("@vueuse/core"))["refDebounced"]>;
+ readonly refDefault: UnwrapRef<(typeof import("@vueuse/core"))["refDefault"]>;
+ readonly refThrottled: UnwrapRef<(typeof import("@vueuse/core"))["refThrottled"]>;
+ readonly refWithControl: UnwrapRef<(typeof import("@vueuse/core"))["refWithControl"]>;
+ readonly resolveComponent: UnwrapRef<(typeof import("vue"))["resolveComponent"]>;
+ readonly resolveRef: UnwrapRef<(typeof import("@vueuse/core"))["resolveRef"]>;
+ readonly resolveUnref: UnwrapRef<(typeof import("@vueuse/core"))["resolveUnref"]>;
+ readonly setActivePinia: UnwrapRef<(typeof import("pinia"))["setActivePinia"]>;
+ readonly setMapStoreSuffix: UnwrapRef<(typeof import("pinia"))["setMapStoreSuffix"]>;
+ readonly shallowReactive: UnwrapRef<(typeof import("vue"))["shallowReactive"]>;
+ readonly shallowReadonly: UnwrapRef<(typeof import("vue"))["shallowReadonly"]>;
+ readonly shallowRef: UnwrapRef<(typeof import("vue"))["shallowRef"]>;
+ readonly storeToRefs: UnwrapRef<(typeof import("pinia"))["storeToRefs"]>;
+ readonly syncRef: UnwrapRef<(typeof import("@vueuse/core"))["syncRef"]>;
+ readonly syncRefs: UnwrapRef<(typeof import("@vueuse/core"))["syncRefs"]>;
+ readonly templateRef: UnwrapRef<(typeof import("@vueuse/core"))["templateRef"]>;
+ readonly throttledRef: UnwrapRef<(typeof import("@vueuse/core"))["throttledRef"]>;
+ readonly throttledWatch: UnwrapRef<(typeof import("@vueuse/core"))["throttledWatch"]>;
+ readonly toRaw: UnwrapRef<(typeof import("vue"))["toRaw"]>;
+ readonly toReactive: UnwrapRef<(typeof import("@vueuse/core"))["toReactive"]>;
+ readonly toRef: UnwrapRef<(typeof import("vue"))["toRef"]>;
+ readonly toRefs: UnwrapRef<(typeof import("vue"))["toRefs"]>;
+ readonly toValue: UnwrapRef<(typeof import("vue"))["toValue"]>;
+ readonly triggerRef: UnwrapRef<(typeof import("vue"))["triggerRef"]>;
+ readonly tryOnBeforeMount: UnwrapRef<(typeof import("@vueuse/core"))["tryOnBeforeMount"]>;
+ readonly tryOnBeforeUnmount: UnwrapRef<(typeof import("@vueuse/core"))["tryOnBeforeUnmount"]>;
+ readonly tryOnMounted: UnwrapRef<(typeof import("@vueuse/core"))["tryOnMounted"]>;
+ readonly tryOnScopeDispose: UnwrapRef<(typeof import("@vueuse/core"))["tryOnScopeDispose"]>;
+ readonly tryOnUnmounted: UnwrapRef<(typeof import("@vueuse/core"))["tryOnUnmounted"]>;
+ readonly unref: UnwrapRef<(typeof import("vue"))["unref"]>;
+ readonly unrefElement: UnwrapRef<(typeof import("@vueuse/core"))["unrefElement"]>;
+ readonly until: UnwrapRef<(typeof import("@vueuse/core"))["until"]>;
+ readonly useActiveElement: UnwrapRef<(typeof import("@vueuse/core"))["useActiveElement"]>;
+ readonly useAnimate: UnwrapRef<(typeof import("@vueuse/core"))["useAnimate"]>;
+ readonly useArrayDifference: UnwrapRef<(typeof import("@vueuse/core"))["useArrayDifference"]>;
+ readonly useArrayEvery: UnwrapRef<(typeof import("@vueuse/core"))["useArrayEvery"]>;
+ readonly useArrayFilter: UnwrapRef<(typeof import("@vueuse/core"))["useArrayFilter"]>;
+ readonly useArrayFind: UnwrapRef<(typeof import("@vueuse/core"))["useArrayFind"]>;
+ readonly useArrayFindIndex: UnwrapRef<(typeof import("@vueuse/core"))["useArrayFindIndex"]>;
+ readonly useArrayFindLast: UnwrapRef<(typeof import("@vueuse/core"))["useArrayFindLast"]>;
+ readonly useArrayIncludes: UnwrapRef<(typeof import("@vueuse/core"))["useArrayIncludes"]>;
+ readonly useArrayJoin: UnwrapRef<(typeof import("@vueuse/core"))["useArrayJoin"]>;
+ readonly useArrayMap: UnwrapRef<(typeof import("@vueuse/core"))["useArrayMap"]>;
+ readonly useArrayReduce: UnwrapRef<(typeof import("@vueuse/core"))["useArrayReduce"]>;
+ readonly useArraySome: UnwrapRef<(typeof import("@vueuse/core"))["useArraySome"]>;
+ readonly useArrayUnique: UnwrapRef<(typeof import("@vueuse/core"))["useArrayUnique"]>;
+ readonly useAsyncQueue: UnwrapRef<(typeof import("@vueuse/core"))["useAsyncQueue"]>;
+ readonly useAsyncState: UnwrapRef<(typeof import("@vueuse/core"))["useAsyncState"]>;
+ readonly useAttrs: UnwrapRef<(typeof import("vue"))["useAttrs"]>;
+ readonly useBase64: UnwrapRef<(typeof import("@vueuse/core"))["useBase64"]>;
+ readonly useBattery: UnwrapRef<(typeof import("@vueuse/core"))["useBattery"]>;
+ readonly useBluetooth: UnwrapRef<(typeof import("@vueuse/core"))["useBluetooth"]>;
+ readonly useBreakpoints: UnwrapRef<(typeof import("@vueuse/core"))["useBreakpoints"]>;
+ readonly useBroadcastChannel: UnwrapRef<(typeof import("@vueuse/core"))["useBroadcastChannel"]>;
+ readonly useBrowserLocation: UnwrapRef<(typeof import("@vueuse/core"))["useBrowserLocation"]>;
+ readonly useCached: UnwrapRef<(typeof import("@vueuse/core"))["useCached"]>;
+ readonly useClipboard: UnwrapRef<(typeof import("@vueuse/core"))["useClipboard"]>;
+ readonly useClipboardItems: UnwrapRef<(typeof import("@vueuse/core"))["useClipboardItems"]>;
+ readonly useCloned: UnwrapRef<(typeof import("@vueuse/core"))["useCloned"]>;
+ readonly useColorMode: UnwrapRef<(typeof import("@vueuse/core"))["useColorMode"]>;
+ readonly useConfirmDialog: UnwrapRef<(typeof import("@vueuse/core"))["useConfirmDialog"]>;
+ readonly useCounter: UnwrapRef<(typeof import("@vueuse/core"))["useCounter"]>;
+ readonly useCssModule: UnwrapRef<(typeof import("vue"))["useCssModule"]>;
+ readonly useCssVar: UnwrapRef<(typeof import("@vueuse/core"))["useCssVar"]>;
+ readonly useCssVars: UnwrapRef<(typeof import("vue"))["useCssVars"]>;
+ readonly useCurrentElement: UnwrapRef<(typeof import("@vueuse/core"))["useCurrentElement"]>;
+ readonly useCycleList: UnwrapRef<(typeof import("@vueuse/core"))["useCycleList"]>;
+ readonly useDark: UnwrapRef<(typeof import("@vueuse/core"))["useDark"]>;
+ readonly useDateFormat: UnwrapRef<(typeof import("@vueuse/core"))["useDateFormat"]>;
+ readonly useDebounce: UnwrapRef<(typeof import("@vueuse/core"))["useDebounce"]>;
+ readonly useDebounceFn: UnwrapRef<(typeof import("@vueuse/core"))["useDebounceFn"]>;
+ readonly useDebouncedRefHistory: UnwrapRef<
+ (typeof import("@vueuse/core"))["useDebouncedRefHistory"]
+ >;
+ readonly useDeviceMotion: UnwrapRef<(typeof import("@vueuse/core"))["useDeviceMotion"]>;
+ readonly useDeviceOrientation: UnwrapRef<
+ (typeof import("@vueuse/core"))["useDeviceOrientation"]
+ >;
+ readonly useDevicePixelRatio: UnwrapRef<(typeof import("@vueuse/core"))["useDevicePixelRatio"]>;
+ readonly useDevicesList: UnwrapRef<(typeof import("@vueuse/core"))["useDevicesList"]>;
+ readonly useDisplayMedia: UnwrapRef<(typeof import("@vueuse/core"))["useDisplayMedia"]>;
+ readonly useDocumentVisibility: UnwrapRef<
+ (typeof import("@vueuse/core"))["useDocumentVisibility"]
+ >;
+ readonly useDraggable: UnwrapRef<(typeof import("@vueuse/core"))["useDraggable"]>;
+ readonly useDropZone: UnwrapRef<(typeof import("@vueuse/core"))["useDropZone"]>;
+ readonly useElementBounding: UnwrapRef<(typeof import("@vueuse/core"))["useElementBounding"]>;
+ readonly useElementByPoint: UnwrapRef<(typeof import("@vueuse/core"))["useElementByPoint"]>;
+ readonly useElementHover: UnwrapRef<(typeof import("@vueuse/core"))["useElementHover"]>;
+ readonly useElementSize: UnwrapRef<(typeof import("@vueuse/core"))["useElementSize"]>;
+ readonly useElementVisibility: UnwrapRef<
+ (typeof import("@vueuse/core"))["useElementVisibility"]
+ >;
+ readonly useEventBus: UnwrapRef<(typeof import("@vueuse/core"))["useEventBus"]>;
+ readonly useEventListener: UnwrapRef<(typeof import("@vueuse/core"))["useEventListener"]>;
+ readonly useEventSource: UnwrapRef<(typeof import("@vueuse/core"))["useEventSource"]>;
+ readonly useEyeDropper: UnwrapRef<(typeof import("@vueuse/core"))["useEyeDropper"]>;
+ readonly useFavicon: UnwrapRef<(typeof import("@vueuse/core"))["useFavicon"]>;
+ readonly useFetch: UnwrapRef<(typeof import("@vueuse/core"))["useFetch"]>;
+ readonly useFileDialog: UnwrapRef<(typeof import("@vueuse/core"))["useFileDialog"]>;
+ readonly useFileSystemAccess: UnwrapRef<(typeof import("@vueuse/core"))["useFileSystemAccess"]>;
+ readonly useFocus: UnwrapRef<(typeof import("@vueuse/core"))["useFocus"]>;
+ readonly useFocusWithin: UnwrapRef<(typeof import("@vueuse/core"))["useFocusWithin"]>;
+ readonly useFps: UnwrapRef<(typeof import("@vueuse/core"))["useFps"]>;
+ readonly useFullscreen: UnwrapRef<(typeof import("@vueuse/core"))["useFullscreen"]>;
+ readonly useGamepad: UnwrapRef<(typeof import("@vueuse/core"))["useGamepad"]>;
+ readonly useGeolocation: UnwrapRef<(typeof import("@vueuse/core"))["useGeolocation"]>;
+ readonly useI18n: UnwrapRef<(typeof import("vue-i18n"))["useI18n"]>;
+ readonly useIdle: UnwrapRef<(typeof import("@vueuse/core"))["useIdle"]>;
+ readonly useImage: UnwrapRef<(typeof import("@vueuse/core"))["useImage"]>;
+ readonly useInfiniteScroll: UnwrapRef<(typeof import("@vueuse/core"))["useInfiniteScroll"]>;
+ readonly useIntersectionObserver: UnwrapRef<
+ (typeof import("@vueuse/core"))["useIntersectionObserver"]
+ >;
+ readonly useInterval: UnwrapRef<(typeof import("@vueuse/core"))["useInterval"]>;
+ readonly useIntervalFn: UnwrapRef<(typeof import("@vueuse/core"))["useIntervalFn"]>;
+ readonly useKeyModifier: UnwrapRef<(typeof import("@vueuse/core"))["useKeyModifier"]>;
+ readonly useLastChanged: UnwrapRef<(typeof import("@vueuse/core"))["useLastChanged"]>;
+ readonly useLink: UnwrapRef<(typeof import("vue-router"))["useLink"]>;
+ readonly useLocalStorage: UnwrapRef<(typeof import("@vueuse/core"))["useLocalStorage"]>;
+ readonly useMagicKeys: UnwrapRef<(typeof import("@vueuse/core"))["useMagicKeys"]>;
+ readonly useManualRefHistory: UnwrapRef<(typeof import("@vueuse/core"))["useManualRefHistory"]>;
+ readonly useMediaControls: UnwrapRef<(typeof import("@vueuse/core"))["useMediaControls"]>;
+ readonly useMediaQuery: UnwrapRef<(typeof import("@vueuse/core"))["useMediaQuery"]>;
+ readonly useMemoize: UnwrapRef<(typeof import("@vueuse/core"))["useMemoize"]>;
+ readonly useMemory: UnwrapRef<(typeof import("@vueuse/core"))["useMemory"]>;
+ readonly useMounted: UnwrapRef<(typeof import("@vueuse/core"))["useMounted"]>;
+ readonly useMouse: UnwrapRef<(typeof import("@vueuse/core"))["useMouse"]>;
+ readonly useMouseInElement: UnwrapRef<(typeof import("@vueuse/core"))["useMouseInElement"]>;
+ readonly useMousePressed: UnwrapRef<(typeof import("@vueuse/core"))["useMousePressed"]>;
+ readonly useMutationObserver: UnwrapRef<(typeof import("@vueuse/core"))["useMutationObserver"]>;
+ readonly useNavigatorLanguage: UnwrapRef<
+ (typeof import("@vueuse/core"))["useNavigatorLanguage"]
+ >;
+ readonly useNetwork: UnwrapRef<(typeof import("@vueuse/core"))["useNetwork"]>;
+ readonly useNow: UnwrapRef<(typeof import("@vueuse/core"))["useNow"]>;
+ readonly useObjectUrl: UnwrapRef<(typeof import("@vueuse/core"))["useObjectUrl"]>;
+ readonly useOffsetPagination: UnwrapRef<(typeof import("@vueuse/core"))["useOffsetPagination"]>;
+ readonly useOnline: UnwrapRef<(typeof import("@vueuse/core"))["useOnline"]>;
+ readonly usePageLeave: UnwrapRef<(typeof import("@vueuse/core"))["usePageLeave"]>;
+ readonly useParallax: UnwrapRef<(typeof import("@vueuse/core"))["useParallax"]>;
+ readonly useParentElement: UnwrapRef<(typeof import("@vueuse/core"))["useParentElement"]>;
+ readonly usePerformanceObserver: UnwrapRef<
+ (typeof import("@vueuse/core"))["usePerformanceObserver"]
+ >;
+ readonly usePermission: UnwrapRef<(typeof import("@vueuse/core"))["usePermission"]>;
+ readonly usePointer: UnwrapRef<(typeof import("@vueuse/core"))["usePointer"]>;
+ readonly usePointerLock: UnwrapRef<(typeof import("@vueuse/core"))["usePointerLock"]>;
+ readonly usePointerSwipe: UnwrapRef<(typeof import("@vueuse/core"))["usePointerSwipe"]>;
+ readonly usePreferredColorScheme: UnwrapRef<
+ (typeof import("@vueuse/core"))["usePreferredColorScheme"]
+ >;
+ readonly usePreferredContrast: UnwrapRef<
+ (typeof import("@vueuse/core"))["usePreferredContrast"]
+ >;
+ readonly usePreferredDark: UnwrapRef<(typeof import("@vueuse/core"))["usePreferredDark"]>;
+ readonly usePreferredLanguages: UnwrapRef<
+ (typeof import("@vueuse/core"))["usePreferredLanguages"]
+ >;
+ readonly usePreferredReducedMotion: UnwrapRef<
+ (typeof import("@vueuse/core"))["usePreferredReducedMotion"]
+ >;
+ readonly usePrevious: UnwrapRef<(typeof import("@vueuse/core"))["usePrevious"]>;
+ readonly useRafFn: UnwrapRef<(typeof import("@vueuse/core"))["useRafFn"]>;
+ readonly useRefHistory: UnwrapRef<(typeof import("@vueuse/core"))["useRefHistory"]>;
+ readonly useResizeObserver: UnwrapRef<(typeof import("@vueuse/core"))["useResizeObserver"]>;
+ readonly useRoute: UnwrapRef<(typeof import("vue-router"))["useRoute"]>;
+ readonly useRouter: UnwrapRef<(typeof import("vue-router"))["useRouter"]>;
+ readonly useScreenOrientation: UnwrapRef<
+ (typeof import("@vueuse/core"))["useScreenOrientation"]
+ >;
+ readonly useScreenSafeArea: UnwrapRef<(typeof import("@vueuse/core"))["useScreenSafeArea"]>;
+ readonly useScriptTag: UnwrapRef<(typeof import("@vueuse/core"))["useScriptTag"]>;
+ readonly useScroll: UnwrapRef<(typeof import("@vueuse/core"))["useScroll"]>;
+ readonly useScrollLock: UnwrapRef<(typeof import("@vueuse/core"))["useScrollLock"]>;
+ readonly useSessionStorage: UnwrapRef<(typeof import("@vueuse/core"))["useSessionStorage"]>;
+ readonly useShare: UnwrapRef<(typeof import("@vueuse/core"))["useShare"]>;
+ readonly useSlots: UnwrapRef<(typeof import("vue"))["useSlots"]>;
+ readonly useSorted: UnwrapRef<(typeof import("@vueuse/core"))["useSorted"]>;
+ readonly useSpeechRecognition: UnwrapRef<
+ (typeof import("@vueuse/core"))["useSpeechRecognition"]
+ >;
+ readonly useSpeechSynthesis: UnwrapRef<(typeof import("@vueuse/core"))["useSpeechSynthesis"]>;
+ readonly useStepper: UnwrapRef<(typeof import("@vueuse/core"))["useStepper"]>;
+ readonly useStorage: UnwrapRef<(typeof import("@vueuse/core"))["useStorage"]>;
+ readonly useStorageAsync: UnwrapRef<(typeof import("@vueuse/core"))["useStorageAsync"]>;
+ readonly useStyleTag: UnwrapRef<(typeof import("@vueuse/core"))["useStyleTag"]>;
+ readonly useSupported: UnwrapRef<(typeof import("@vueuse/core"))["useSupported"]>;
+ readonly useSwipe: UnwrapRef<(typeof import("@vueuse/core"))["useSwipe"]>;
+ readonly useTemplateRefsList: UnwrapRef<(typeof import("@vueuse/core"))["useTemplateRefsList"]>;
+ readonly useTextDirection: UnwrapRef<(typeof import("@vueuse/core"))["useTextDirection"]>;
+ readonly useTextSelection: UnwrapRef<(typeof import("@vueuse/core"))["useTextSelection"]>;
+ readonly useTextareaAutosize: UnwrapRef<(typeof import("@vueuse/core"))["useTextareaAutosize"]>;
+ readonly useThrottle: UnwrapRef<(typeof import("@vueuse/core"))["useThrottle"]>;
+ readonly useThrottleFn: UnwrapRef<(typeof import("@vueuse/core"))["useThrottleFn"]>;
+ readonly useThrottledRefHistory: UnwrapRef<
+ (typeof import("@vueuse/core"))["useThrottledRefHistory"]
+ >;
+ readonly useTimeAgo: UnwrapRef<(typeof import("@vueuse/core"))["useTimeAgo"]>;
+ readonly useTimeout: UnwrapRef<(typeof import("@vueuse/core"))["useTimeout"]>;
+ readonly useTimeoutFn: UnwrapRef<(typeof import("@vueuse/core"))["useTimeoutFn"]>;
+ readonly useTimeoutPoll: UnwrapRef<(typeof import("@vueuse/core"))["useTimeoutPoll"]>;
+ readonly useTimestamp: UnwrapRef<(typeof import("@vueuse/core"))["useTimestamp"]>;
+ readonly useTitle: UnwrapRef<(typeof import("@vueuse/core"))["useTitle"]>;
+ readonly useToNumber: UnwrapRef<(typeof import("@vueuse/core"))["useToNumber"]>;
+ readonly useToString: UnwrapRef<(typeof import("@vueuse/core"))["useToString"]>;
+ readonly useToggle: UnwrapRef<(typeof import("@vueuse/core"))["useToggle"]>;
+ readonly useTransition: UnwrapRef<(typeof import("@vueuse/core"))["useTransition"]>;
+ readonly useUrlSearchParams: UnwrapRef<(typeof import("@vueuse/core"))["useUrlSearchParams"]>;
+ readonly useUserMedia: UnwrapRef<(typeof import("@vueuse/core"))["useUserMedia"]>;
+ readonly useVModel: UnwrapRef<(typeof import("@vueuse/core"))["useVModel"]>;
+ readonly useVModels: UnwrapRef<(typeof import("@vueuse/core"))["useVModels"]>;
+ readonly useVibrate: UnwrapRef<(typeof import("@vueuse/core"))["useVibrate"]>;
+ readonly useVirtualList: UnwrapRef<(typeof import("@vueuse/core"))["useVirtualList"]>;
+ readonly useWakeLock: UnwrapRef<(typeof import("@vueuse/core"))["useWakeLock"]>;
+ readonly useWebNotification: UnwrapRef<(typeof import("@vueuse/core"))["useWebNotification"]>;
+ readonly useWebSocket: UnwrapRef<(typeof import("@vueuse/core"))["useWebSocket"]>;
+ readonly useWebWorker: UnwrapRef<(typeof import("@vueuse/core"))["useWebWorker"]>;
+ readonly useWebWorkerFn: UnwrapRef<(typeof import("@vueuse/core"))["useWebWorkerFn"]>;
+ readonly useWindowFocus: UnwrapRef<(typeof import("@vueuse/core"))["useWindowFocus"]>;
+ readonly useWindowScroll: UnwrapRef<(typeof import("@vueuse/core"))["useWindowScroll"]>;
+ readonly useWindowSize: UnwrapRef<(typeof import("@vueuse/core"))["useWindowSize"]>;
+ readonly watch: UnwrapRef<(typeof import("vue"))["watch"]>;
+ readonly watchArray: UnwrapRef<(typeof import("@vueuse/core"))["watchArray"]>;
+ readonly watchAtMost: UnwrapRef<(typeof import("@vueuse/core"))["watchAtMost"]>;
+ readonly watchDebounced: UnwrapRef<(typeof import("@vueuse/core"))["watchDebounced"]>;
+ readonly watchDeep: UnwrapRef<(typeof import("@vueuse/core"))["watchDeep"]>;
+ readonly watchEffect: UnwrapRef<(typeof import("vue"))["watchEffect"]>;
+ readonly watchIgnorable: UnwrapRef<(typeof import("@vueuse/core"))["watchIgnorable"]>;
+ readonly watchImmediate: UnwrapRef<(typeof import("@vueuse/core"))["watchImmediate"]>;
+ readonly watchOnce: UnwrapRef<(typeof import("@vueuse/core"))["watchOnce"]>;
+ readonly watchPausable: UnwrapRef<(typeof import("@vueuse/core"))["watchPausable"]>;
+ readonly watchPostEffect: UnwrapRef<(typeof import("vue"))["watchPostEffect"]>;
+ readonly watchSyncEffect: UnwrapRef<(typeof import("vue"))["watchSyncEffect"]>;
+ readonly watchThrottled: UnwrapRef<(typeof import("@vueuse/core"))["watchThrottled"]>;
+ readonly watchTriggerable: UnwrapRef<(typeof import("@vueuse/core"))["watchTriggerable"]>;
+ readonly watchWithFilter: UnwrapRef<(typeof import("@vueuse/core"))["watchWithFilter"]>;
+ readonly whenever: UnwrapRef<(typeof import("@vueuse/core"))["whenever"]>;
+ }
+}
diff --git a/src/types/components.d.ts b/src/types/components.d.ts
new file mode 100644
index 0000000..6e194f0
--- /dev/null
+++ b/src/types/components.d.ts
@@ -0,0 +1,90 @@
+/* prettier-ignore */
+// @ts-nocheck
+// Generated by unplugin-vue-components
+// Read more: https://github.com/vuejs/core/pull/3399
+export {}
+
+declare module "vue" {
+ export interface GlobalComponents {
+ AppLink: (typeof import("./../components/AppLink/index.vue"))["default"];
+ Breadcrumb: (typeof import("./../components/Breadcrumb/index.vue"))["default"];
+ CopyButton: (typeof import("./../components/CopyButton/index.vue"))["default"];
+ CURD: (typeof import("./../components/CURD/index.vue"))["default"];
+ Dict: (typeof import("./../components/Dict/index.vue"))["default"];
+ DictLabel: (typeof import("./../components/Dict/DictLabel.vue"))["default"];
+ ECharts: (typeof import("./../components/ECharts/index.vue"))["default"];
+ ElBacktop: (typeof import("element-plus/es"))["ElBacktop"];
+ ElBreadcrumb: (typeof import("element-plus/es"))["ElBreadcrumb"];
+ ElBreadcrumbItem: (typeof import("element-plus/es"))["ElBreadcrumbItem"];
+ ElButton: (typeof import("element-plus/es"))["ElButton"];
+ ElCard: (typeof import("element-plus/es"))["ElCard"];
+ ElCascader: (typeof import("element-plus/es"))["ElCascader"];
+ ElCheckbox: (typeof import("element-plus/es"))["ElCheckbox"];
+ ElCheckboxGroup: (typeof import("element-plus/es"))["ElCheckboxGroup"];
+ ElCol: (typeof import("element-plus/es"))["ElCol"];
+ ElColorPicker: (typeof import("element-plus/es"))["ElColorPicker"];
+ ElConfigProvider: (typeof import("element-plus/es"))["ElConfigProvider"];
+ ElDatePicker: (typeof import("element-plus/es"))["ElDatePicker"];
+ ElDialog: (typeof import("element-plus/es"))["ElDialog"];
+ ElDivider: (typeof import("element-plus/es"))["ElDivider"];
+ ElDrawer: (typeof import("element-plus/es"))["ElDrawer"];
+ ElDropdown: (typeof import("element-plus/es"))["ElDropdown"];
+ ElDropdownItem: (typeof import("element-plus/es"))["ElDropdownItem"];
+ ElDropdownMenu: (typeof import("element-plus/es"))["ElDropdownMenu"];
+ ElForm: (typeof import("element-plus/es"))["ElForm"];
+ ElFormItem: (typeof import("element-plus/es"))["ElFormItem"];
+ ElIcon: (typeof import("element-plus/es"))["ElIcon"];
+ ElImage: (typeof import("element-plus/es"))["ElImage"];
+ ElInput: (typeof import("element-plus/es"))["ElInput"];
+ ElInputTag: (typeof import("element-plus/es"))["ElInputTag"];
+ ElInputNumber: (typeof import("element-plus/es"))["ElInputNumber"];
+ ElLink: (typeof import("element-plus/es"))["ElLink"];
+ ElMenu: (typeof import("element-plus/es"))["ElMenu"];
+ ElMenuItem: (typeof import("element-plus/es"))["ElMenuItem"];
+ ElOption: (typeof import("element-plus/es"))["ElOption"];
+ ElPagination: (typeof import("element-plus/es"))["ElPagination"];
+ ElPopover: (typeof import("element-plus/es"))["ElPopover"];
+ ElRadio: (typeof import("element-plus/es"))["ElRadio"];
+ ElRadioGroup: (typeof import("element-plus/es"))["ElRadioGroup"];
+ ElRow: (typeof import("element-plus/es"))["ElRow"];
+ ElScrollbar: (typeof import("element-plus/es"))["ElScrollbar"];
+ ElSelect: (typeof import("element-plus/es"))["ElSelect"];
+ ElStatistic: (typeof import("element-plus/es"))["ElStatistic"];
+ ElSubMenu: (typeof import("element-plus/es"))["ElSubMenu"];
+ ElSwitch: (typeof import("element-plus/es"))["ElSwitch"];
+ ElTable: (typeof import("element-plus/es"))["ElTable"];
+ ElTableColumn: (typeof import("element-plus/es"))["ElTableColumn"];
+ ElTag: (typeof import("element-plus/es"))["ElTag"];
+ ElText: (typeof import("element-plus/es"))["ElText"];
+ ElTimeSelect: (typeof import("element-plus/es"))["ElTimeSelect"];
+ ElTooltip: (typeof import("element-plus/es"))["ElTooltip"];
+ ElTree: (typeof import("element-plus/es"))["ElTree"];
+ ElTreeSelect: (typeof import("element-plus/es"))["ElTreeSelect"];
+ ElUpload: (typeof import("element-plus/es"))["ElUpload"];
+ ElWatermark: (typeof import("element-plus/es"))["ElWatermark"];
+ ElSkeleton: (typeof import("element-plus/es"))["ElSkeleton"];
+ FileUpload: (typeof import("./../components/Upload/FileUpload.vue"))["default"];
+ Form: (typeof import("./../components/CURD/Form.vue"))["default"];
+ Fullscreen: (typeof import("./../components/Fullscreen/index.vue"))["default"];
+ GithubCorner: (typeof import("./../components/GithubCorner/index.vue"))["default"];
+ Hamburger: (typeof import("./../components/Hamburger/index.vue"))["default"];
+ IconSelect: (typeof import("./../components/IconSelect/index.vue"))["default"];
+ LangSelect: (typeof import("./../components/LangSelect/index.vue"))["default"];
+ MenuSearch: (typeof import("./../components/MenuSearch/index.vue"))["default"];
+ MultiImageUpload: (typeof import("./../components/Upload/MultiImageUpload.vue"))["default"];
+ Notification: (typeof import("./../components/Notification/index.vue"))["default"];
+ PageContent: (typeof import("./../components/CURD/PageContent.vue"))["default"];
+ PageModal: (typeof import("./../components/CURD/PageModal.vue"))["default"];
+ PageSearch: (typeof import("./../components/CURD/PageSearch.vue"))["default"];
+ Pagination: (typeof import("./../components/Pagination/index.vue"))["default"];
+ RouterLink: (typeof import("vue-router"))["RouterLink"];
+ RouterView: (typeof import("vue-router"))["RouterView"];
+ SingleImageUpload: (typeof import("./../components/Upload/SingleImageUpload.vue"))["default"];
+ SizeSelect: (typeof import("./../components/SizeSelect/index.vue"))["default"];
+ TableSelect: (typeof import("./../components/TableSelect/index.vue"))["default"];
+ WangEditor: (typeof import("./../components/WangEditor/index.vue"))["default"];
+ }
+ export interface ComponentCustomProperties {
+ vLoading: (typeof import("element-plus/es"))["ElLoadingDirective"];
+ }
+}
diff --git a/src/types/env.d.ts b/src/types/env.d.ts
new file mode 100644
index 0000000..1342f61
--- /dev/null
+++ b/src/types/env.d.ts
@@ -0,0 +1,35 @@
+// https://cn.vitejs.dev/guide/env-and-mode
+
+// TypeScript 类型提示都为 string: https://github.com/vitejs/vite/issues/6930
+interface ImportMetaEnv {
+ /** 应用端口 */
+ VITE_APP_PORT: number;
+ /** 应用名称 */
+ VITE_APP_NAME: string;
+ /** API 基础路径(代理前缀) */
+ VITE_APP_BASE_API: string;
+ /** API 地址 */
+ VITE_APP_API_URL: string;
+ /** 是否开启 Mock 服务 */
+ VITE_MOCK_DEV_SERVER: boolean;
+}
+
+interface ImportMeta {
+ readonly env: ImportMetaEnv;
+}
+
+/**
+ * 平台的名称、版本、运行所需的`node`版本、依赖、构建时间的类型提示
+ */
+declare const __APP_INFO__: {
+ pkg: {
+ name: string;
+ version: string;
+ engines: {
+ node: string;
+ };
+ dependencies: Record;
+ devDependencies: Record;
+ };
+ buildTimestamp: number;
+};
diff --git a/src/types/global.d.ts b/src/types/global.d.ts
new file mode 100644
index 0000000..1806e21
--- /dev/null
+++ b/src/types/global.d.ts
@@ -0,0 +1,111 @@
+declare global {
+ /**
+ * 响应数据
+ */
+ interface ApiResponse {
+ code: string;
+ data: T;
+ msg: string;
+ }
+
+ /**
+ * 分页查询参数
+ */
+ interface PageQuery {
+ pageNum: number;
+ pageSize: number;
+ }
+
+ /**
+ * 分页响应对象
+ */
+ interface PageResult {
+ /** 数据列表 */
+ list: T;
+ /** 总数 */
+ total: number;
+ }
+
+ /**
+ * 页签对象
+ */
+ interface TagView {
+ /** 页签名称 */
+ name: string;
+ /** 页签标题 */
+ title: string;
+ /** 页签路由路径 */
+ path: string;
+ /** 页签路由完整路径 */
+ fullPath: string;
+ /** 页签图标 */
+ icon?: string;
+ /** 是否固定页签 */
+ affix?: boolean;
+ /** 是否开启缓存 */
+ keepAlive?: boolean;
+ /** 路由查询参数 */
+ query?: any;
+ }
+
+ /**
+ * 系统设置
+ */
+ interface AppSettings {
+ /** 系统标题 */
+ title: string;
+ /** 系统版本 */
+ version: string;
+ /** 是否显示设置 */
+ showSettings: boolean;
+ /** 是否显示多标签导航 */
+ showTagsView: boolean;
+ /** 是否显示应用Logo */
+ showAppLogo: boolean;
+ /** 导航栏布局(left|top|mix) */
+ layout: "left" | "top" | "mix";
+ /** 主题颜色 */
+ themeColor: string;
+ /** 主题模式(dark|light) */
+ theme: import("@/enums/settings/theme-enum").ThemeMode;
+ /** 布局大小(default |large |small) */
+ size: string;
+ /** 语言( zh-cn| en) */
+ language: string;
+ /** 是否显示水印 */
+ showWatermark: boolean;
+ /** 水印内容 */
+ watermarkContent: string;
+ /** 侧边栏配色方案 */
+ sidebarColorScheme: "classic-blue" | "minimal-white";
+ /** 是否启用 AI 助手 */
+ enableAiAssistant: boolean;
+ }
+
+ /**
+ * 下拉选项数据类型
+ */
+ interface OptionType {
+ /** 值 */
+ value: string | number;
+ /** 文本 */
+ label: string;
+ /** 子列表 */
+ children?: OptionType[];
+ }
+
+ /**
+ * 导入结果
+ */
+ interface ExcelResult {
+ /** 状态码 */
+ code: string;
+ /** 无效数据条数 */
+ invalidCount: number;
+ /** 有效数据条数 */
+ validCount: number;
+ /** 错误信息 */
+ messageList: Array;
+ }
+}
+export {};
diff --git a/src/types/router.d.ts b/src/types/router.d.ts
new file mode 100644
index 0000000..17a3cb4
--- /dev/null
+++ b/src/types/router.d.ts
@@ -0,0 +1,54 @@
+import "vue-router";
+
+declare module "vue-router" {
+ // https://router.vuejs.org/zh/guide/advanced/meta.html#typescript
+ // 可以通过扩展 RouteMeta 接口来输入 meta 字段
+ interface RouteMeta {
+ /**
+ * 菜单名称
+ * @example 'Dashboard'
+ */
+ title?: string;
+
+ /**
+ * 菜单图标
+ * @example 'el-icon-edit'
+ */
+ icon?: string;
+
+ /**
+ * 是否隐藏菜单
+ * true 隐藏, false 显示
+ * @default false
+ */
+ hidden?: boolean;
+
+ /**
+ * 始终显示父级菜单,即使只有一个子菜单
+ * true 显示父级菜单, false 隐藏父级菜单,显示唯一子节点
+ * @default false
+ */
+ alwaysShow?: boolean;
+
+ /**
+ * 是否固定在页签上
+ * true 固定, false 不固定
+ * @default false
+ */
+ affix?: boolean;
+
+ /**
+ * 是否缓存页面
+ * true 缓存, false 不缓存
+ * @default false
+ */
+ keepAlive?: boolean;
+
+ /**
+ * 是否在面包屑导航中隐藏
+ * true 隐藏, false 显示
+ * @default false
+ */
+ breadcrumb?: boolean;
+ }
+}
diff --git a/src/types/shims-vue.d.ts b/src/types/shims-vue.d.ts
new file mode 100644
index 0000000..d77b62b
--- /dev/null
+++ b/src/types/shims-vue.d.ts
@@ -0,0 +1,5 @@
+declare module "*.vue" {
+ import type { DefineComponent } from "vue";
+ const component: DefineComponent<{}, {}, any>;
+ export default component;
+}
diff --git a/src/types/socket.d.ts b/src/types/socket.d.ts
new file mode 100644
index 0000000..15a8ed7
--- /dev/null
+++ b/src/types/socket.d.ts
@@ -0,0 +1,6 @@
+// https://github.com/sockjs/sockjs-client/issues/565
+
+declare module "sockjs-client/dist/sockjs.min.js" {
+ import Client from "sockjs-client";
+ export default Client;
+}
diff --git a/src/utils/auth.ts b/src/utils/auth.ts
new file mode 100644
index 0000000..c2e7986
--- /dev/null
+++ b/src/utils/auth.ts
@@ -0,0 +1,76 @@
+import { Storage } from "./storage";
+import { AUTH_KEYS, ROLE_ROOT } from "@/constants";
+import { useUserStoreHook } from "@/store/modules/user-store";
+import router from "@/router";
+
+// 负责本地凭证与偏好的读写
+export const AuthStorage = {
+ getAccessToken(): string {
+ const isRememberMe = Storage.get(AUTH_KEYS.REMEMBER_ME, false);
+ return isRememberMe
+ ? Storage.get(AUTH_KEYS.ACCESS_TOKEN, "")
+ : Storage.sessionGet(AUTH_KEYS.ACCESS_TOKEN, "");
+ },
+
+ getRefreshToken(): string {
+ const isRememberMe = Storage.get(AUTH_KEYS.REMEMBER_ME, false);
+ return isRememberMe
+ ? Storage.get(AUTH_KEYS.REFRESH_TOKEN, "")
+ : Storage.sessionGet(AUTH_KEYS.REFRESH_TOKEN, "");
+ },
+
+ setTokens(accessToken: string, refreshToken: string, rememberMe: boolean): void {
+ Storage.set(AUTH_KEYS.REMEMBER_ME, rememberMe);
+ if (rememberMe) {
+ Storage.set(AUTH_KEYS.ACCESS_TOKEN, accessToken);
+ Storage.set(AUTH_KEYS.REFRESH_TOKEN, refreshToken);
+ } else {
+ Storage.sessionSet(AUTH_KEYS.ACCESS_TOKEN, accessToken);
+ Storage.sessionSet(AUTH_KEYS.REFRESH_TOKEN, refreshToken);
+ Storage.remove(AUTH_KEYS.ACCESS_TOKEN);
+ Storage.remove(AUTH_KEYS.REFRESH_TOKEN);
+ }
+ },
+
+ clearAuth(): void {
+ Storage.remove(AUTH_KEYS.ACCESS_TOKEN);
+ Storage.remove(AUTH_KEYS.REFRESH_TOKEN);
+ Storage.sessionRemove(AUTH_KEYS.ACCESS_TOKEN);
+ Storage.sessionRemove(AUTH_KEYS.REFRESH_TOKEN);
+ },
+
+ getRememberMe(): boolean {
+ return Storage.get(AUTH_KEYS.REMEMBER_ME, false);
+ },
+};
+
+/**
+ * 权限判断 - 已移除
+ */
+export function hasPerm(value?: string | string[], type?: "button" | "role"): boolean {
+ return true;
+}
+
+/**
+ * 重定向到登录页面
+ */
+export async function redirectToLogin(message: string = "请重新登录"): Promise {
+ ElNotification({
+ title: "提示",
+ message,
+ type: "warning",
+ duration: 3000,
+ });
+
+ await useUserStoreHook().resetAllState();
+
+ try {
+ // 跳转到登录页,保留当前路由用于登录后跳转
+ const currentPath = router.currentRoute.value.fullPath;
+ await router.push(`/login?redirect=${encodeURIComponent(currentPath)}`);
+ } catch (error) {
+ console.error("Redirect to login error:", error);
+ // 强制跳转,即使路由重定向失败
+ window.location.href = "/login";
+ }
+}
diff --git a/src/utils/i18n.ts b/src/utils/i18n.ts
new file mode 100644
index 0000000..17ed904
--- /dev/null
+++ b/src/utils/i18n.ts
@@ -0,0 +1,12 @@
+// translate router.meta.title, be used in breadcrumb sidebar tagsview
+import i18n from "@/lang/index";
+
+export function translateRouteTitle(title: any) {
+ // 判断是否存在国际化配置,如果没有原生返回
+ 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..67fce78
--- /dev/null
+++ b/src/utils/index.ts
@@ -0,0 +1,58 @@
+/**
+ * Check if an element has a class
+ * @param {HTMLElement} ele
+ * @param {string} cls
+ * @returns {boolean}
+ */
+export function hasClass(ele: HTMLElement, cls: string) {
+ return !!ele.className.match(new RegExp("(\\s|^)" + cls + "(\\s|$)"));
+}
+
+/**
+ * Add class to element
+ * @param {HTMLElement} ele
+ * @param {string} cls
+ */
+export function addClass(ele: HTMLElement, cls: string) {
+ if (!hasClass(ele, cls)) ele.className += " " + cls;
+}
+
+/**
+ * Remove class from element
+ * @param {HTMLElement} ele
+ * @param {string} cls
+ */
+export function 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 function isExternal(path: string) {
+ const isExternal = /^(https?:|http?:|mailto:|tel:)/.test(path);
+ return isExternal;
+}
+
+/**
+ * 格式化增长率,保留两位小数 ,并且去掉末尾的0 取绝对值
+ *
+ * @param growthRate
+ * @returns
+ */
+export function formatGrowthRate(growthRate: number) {
+ if (growthRate === 0) {
+ return "-";
+ }
+
+ const formattedRate = Math.abs(growthRate * 100)
+ .toFixed(2)
+ .replace(/\.?0+$/, "");
+ return formattedRate + "%";
+}
diff --git a/src/utils/nprogress.ts b/src/utils/nprogress.ts
new file mode 100644
index 0000000..c1d5f23
--- /dev/null
+++ b/src/utils/nprogress.ts
@@ -0,0 +1,18 @@
+import NProgress from "nprogress";
+import "nprogress/nprogress.css";
+
+// 进度条
+NProgress.configure({
+ // 动画方式
+ easing: "ease",
+ // 递增进度条的速度
+ speed: 500,
+ // 是否显示加载ico
+ showSpinner: false,
+ // 自动递增间隔
+ trickleSpeed: 200,
+ // 初始化时的最小百分比
+ minimum: 0.3,
+});
+
+export default NProgress;
diff --git a/src/utils/request.ts b/src/utils/request.ts
new file mode 100644
index 0000000..6da8547
--- /dev/null
+++ b/src/utils/request.ts
@@ -0,0 +1,101 @@
+import axios, { type InternalAxiosRequestConfig, type AxiosResponse } from "axios";
+import qs from "qs";
+import { ApiCodeEnum } from "@/enums/api/code-enum";
+import { AuthStorage, redirectToLogin } from "@/utils/auth";
+import { useTokenRefresh } from "@/composables/auth/useTokenRefresh";
+import { authConfig } from "@/settings";
+
+// 初始化token刷新组合式函数
+const { refreshTokenAndRetry } = useTokenRefresh();
+
+/**
+ * 创建 HTTP 请求实例
+ */
+const httpRequest = axios.create({
+ baseURL: import.meta.env.VITE_APP_BASE_API,
+ timeout: 50000,
+ headers: { "Content-Type": "application/json;charset=utf-8" },
+ paramsSerializer: (params) => qs.stringify(params),
+});
+
+/**
+ * 请求拦截器 - 添加 Authorization 头
+ */
+httpRequest.interceptors.request.use(
+ (config: InternalAxiosRequestConfig) => {
+ const accessToken = AuthStorage.getAccessToken();
+
+ // 如果 Authorization 设置为 no-auth,则不携带 Token
+ if (config.headers.Authorization !== "no-auth" && accessToken) {
+ config.headers.Authorization = `Bearer ${accessToken}`;
+ } else {
+ delete config.headers.Authorization;
+ }
+
+ return config;
+ },
+ (error) => {
+ console.error("Request interceptor error:", error);
+ return Promise.reject(error);
+ }
+);
+
+/**
+ * 响应拦截器 - 统一处理响应和错误
+ */
+httpRequest.interceptors.response.use(
+ (response: AxiosResponse) => {
+ // 如果响应是二进制数据,则直接返回response对象(用于文件下载、Excel导出、图片显示等)
+ if (response.config.responseType === "blob" || response.config.responseType === "arraybuffer") {
+ return response;
+ }
+
+ const { code, data, msg } = response.data;
+
+ // 请求成功
+ if (code === ApiCodeEnum.SUCCESS) {
+ return data;
+ }
+
+ // 业务错误
+ ElMessage.error(msg || "系统出错");
+ return Promise.reject(new Error(msg || "Business Error"));
+ },
+ async (error) => {
+ console.error("Response interceptor error:", error);
+
+ const { config, response } = error;
+
+ // 网络错误或服务器无响应
+ if (!response) {
+ ElMessage.error("网络连接失败,请检查网络设置");
+ return Promise.reject(error);
+ }
+
+ const { code, msg } = response.data as ApiResponse;
+
+ switch (code) {
+ case ApiCodeEnum.ACCESS_TOKEN_INVALID:
+ // Access Token 过期
+ if (authConfig.enableTokenRefresh) {
+ // 启用了token刷新,尝试刷新
+ return refreshTokenAndRetry(config, httpRequest);
+ } else {
+ // 未启用token刷新,直接跳转登录页
+ await redirectToLogin("登录已过期,请重新登录");
+ return Promise.reject(new Error(msg || "Access Token Invalid"));
+ }
+
+ case ApiCodeEnum.REFRESH_TOKEN_INVALID:
+ // Refresh Token 过期,跳转登录页
+ await redirectToLogin("登录已过期,请重新登录");
+ return Promise.reject(new Error(msg || "Refresh Token Invalid"));
+
+ default:
+ ElMessage.error(msg || "请求失败");
+ return Promise.reject(new Error(msg || "Request Error"));
+ }
+ }
+);
+
+export default httpRequest;
diff --git a/src/utils/storage.ts b/src/utils/storage.ts
new file mode 100644
index 0000000..75d5e3f
--- /dev/null
+++ b/src/utils/storage.ts
@@ -0,0 +1,101 @@
+import { STORAGE_KEYS, APP_PREFIX } from "@/constants";
+
+/**
+ * 存储工具类
+ * 提供localStorage和sessionStorage操作方法
+ */
+export class Storage {
+ /**
+ * localStorage 存储
+ */
+ static set(key: string, value: any): void {
+ localStorage.setItem(key, JSON.stringify(value));
+ }
+
+ static get(key: string, defaultValue?: T): T {
+ const value = localStorage.getItem(key);
+ if (!value) return defaultValue as T;
+
+ try {
+ return JSON.parse(value);
+ } catch {
+ // 如果解析失败,返回原始字符串
+ return value as unknown as T;
+ }
+ }
+
+ static remove(key: string): void {
+ localStorage.removeItem(key);
+ }
+
+ /**
+ * sessionStorage 存储
+ */
+ static sessionSet(key: string, value: any): void {
+ sessionStorage.setItem(key, JSON.stringify(value));
+ }
+
+ static sessionGet(key: string, defaultValue?: T): T {
+ const value = sessionStorage.getItem(key);
+ if (!value) return defaultValue as T;
+
+ try {
+ return JSON.parse(value);
+ } catch {
+ // 如果解析失败,返回原始字符串
+ return value as unknown as T;
+ }
+ }
+
+ static sessionRemove(key: string): void {
+ sessionStorage.removeItem(key);
+ }
+
+ /**
+ * 存储清理工具方法
+ */
+ // 清理指定键的存储(localStorage + sessionStorage)
+ static clear(key: string): void {
+ localStorage.removeItem(key);
+ sessionStorage.removeItem(key);
+ }
+
+ // 批量清理存储
+ static clearMultiple(keys: string[]): void {
+ keys.forEach((key) => {
+ localStorage.removeItem(key);
+ sessionStorage.removeItem(key);
+ });
+ }
+
+ // 清理指定前缀的存储
+ static clearByPrefix(prefix: string): void {
+ // localStorage 清理
+ const localKeys = Object.keys(localStorage).filter((key) => key.startsWith(prefix));
+ localKeys.forEach((key) => localStorage.removeItem(key));
+
+ // sessionStorage 清理
+ const sessionKeys = Object.keys(sessionStorage).filter((key) => key.startsWith(prefix));
+ sessionKeys.forEach((key) => sessionStorage.removeItem(key));
+ }
+
+ /**
+ * 项目特定的清理便利方法
+ */
+ // 清理所有项目相关的存储
+ static clearAllProject(): void {
+ const keys = Object.values(STORAGE_KEYS);
+ this.clearMultiple(keys);
+ }
+
+ // 清理特定分类的存储
+ static clearByCategory(category: "auth" | "system" | "ui" | "app"): void {
+ const prefix = `${APP_PREFIX}:${category}:`;
+ this.clearByPrefix(prefix);
+ }
+
+ // 获取所有项目相关的存储键
+ static getAllProjectKeys(): string[] {
+ return Object.values(STORAGE_KEYS);
+ }
+}
diff --git a/src/utils/theme.ts b/src/utils/theme.ts
new file mode 100644
index 0000000..e4aa356
--- /dev/null
+++ b/src/utils/theme.ts
@@ -0,0 +1,112 @@
+import { ThemeMode } from "@/enums";
+
+// 辅助函数:将十六进制颜色转换为 RGB
+function hexToRgb(hex: string): [number, number, number] {
+ const bigint = parseInt(hex.slice(1), 16);
+ return [(bigint >> 16) & 255, (bigint >> 8) & 255, bigint & 255];
+}
+
+// 辅助函数:将 RGB 转换为十六进制颜色
+function rgbToHex(r: number, g: number, b: number): string {
+ return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
+}
+
+// 辅助函数:调整颜色亮度
+/** function adjustBrightness(hex: string, factor: number, theme: string): string {
+ const rgb = hexToRgb(hex);
+ // 是否是暗黑模式
+ const isDarkMode = theme === "dark" ? 0 : 255;
+ const newRgb = rgb.map((val) =>
+ Math.max(0, Math.min(255, Math.round(val + (isDarkMode - val) * factor)))
+ ) as [number, number, number];
+ return rgbToHex(...newRgb);
+} */
+
+/**
+ * 加深颜色值
+ * @param {String} color 颜色值字符串
+ * @param {Number} level 加深的程度,限0-1之间
+ * @returns {String} 返回处理后的颜色值
+ */
+export function getDarkColor(color: string, level: number): string {
+ const rgb = hexToRgb(color);
+ for (let i = 0; i < 3; i++) rgb[i] = Math.round(20.5 * level + rgb[i] * (1 - level));
+ return rgbToHex(rgb[0], rgb[1], rgb[2]);
+}
+
+/**
+ * 变浅颜色值
+ * @param {String} color 颜色值字符串
+ * @param {Number} level 加深的程度,限0-1之间
+ * @returns {String} 返回处理后的颜色值
+ */
+export const getLightColor = (color: string, level: number): string => {
+ const rgb = hexToRgb(color);
+ for (let i = 0; i < 3; i++) rgb[i] = Math.round(255 * level + rgb[i] * (1 - level));
+ return rgbToHex(rgb[0], rgb[1], rgb[2]);
+};
+
+/**
+ * 生成主题色
+ * @param primary 主题色
+ * @param theme 主题类型
+ */
+export function generateThemeColors(primary: string, theme: ThemeMode) {
+ const colors: Record = {
+ primary,
+ };
+
+ // 生成浅色变体
+ for (let i = 1; i <= 9; i++) {
+ colors[`primary-light-${i}`] =
+ theme === ThemeMode.LIGHT
+ ? `${getLightColor(primary, i / 10)}`
+ : `${getDarkColor(primary, i / 10)}`;
+ }
+
+ // 生成深色变体
+ colors["primary-dark-2"] =
+ theme === ThemeMode.LIGHT ? `${getLightColor(primary, 0.2)}` : `${getDarkColor(primary, 0.3)}`;
+
+ return colors;
+}
+
+export function applyTheme(colors: Record) {
+ const el = document.documentElement;
+
+ Object.entries(colors).forEach(([key, value]) => {
+ el.style.setProperty(`--el-color-${key}`, value);
+ });
+
+ // 确保主题色立即生效,强制重新渲染
+ requestAnimationFrame(() => {
+ // 触发样式重新计算
+ el.style.setProperty("--theme-update-trigger", Date.now().toString());
+ });
+}
+
+/**
+ * 切换暗黑模式
+ *
+ * @param isDark 是否启用暗黑模式
+ */
+export function toggleDarkMode(isDark: boolean) {
+ if (isDark) {
+ document.documentElement.classList.add(ThemeMode.DARK);
+ } else {
+ document.documentElement.classList.remove(ThemeMode.DARK);
+ }
+}
+
+/**
+ * 切换浅色主题下的侧边栏颜色方案
+ *
+ * @param isBlue 布尔值,表示是否开启深蓝色侧边栏颜色方案
+ */
+export function toggleSidebarColor(isBuleSidebar: boolean) {
+ if (isBuleSidebar) {
+ document.documentElement.classList.add("sidebar-color-blue");
+ } else {
+ document.documentElement.classList.remove("sidebar-color-blue");
+ }
+}
diff --git a/src/views/ai/command-record/index.vue b/src/views/ai/command-record/index.vue
new file mode 100644
index 0000000..e840c7e
--- /dev/null
+++ b/src/views/ai/command-record/index.vue
@@ -0,0 +1,412 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+ 重置
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ row.parseSuccess ? "成功" : "失败" }}
+
+
+
+
+
+
+ {{ statusText[row.executeStatus] }}
+
+ -
+
+
+
+
+ 风险
+ -
+
+
+
+
+
+ {{ (row.confidence * 100).toFixed(0) }}%
+
+ -
+
+
+
+
+
+
+
+
+ 详情
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ currentRow.id }}
+
+
+ {{ currentRow.username }}
+
+
+
+ {{ currentRow.provider || "-" }}
+
+
+ {{ currentRow.model || "-" }}
+
+
+
+
+ {{ currentRow.parseSuccess ? "成功" : "失败" }}
+
+
+
+
+ {{ (currentRow.confidence * 100).toFixed(0) }}%
+
+ -
+
+
+
+ {{ formatNumber(currentRow.parseTime) }} ms
+
+
+ 输入 {{ currentRow.inputTokens || 0 }} / 输出 {{ currentRow.outputTokens || 0 }} / 总计
+ {{ currentRow.totalTokens || 0 }}
+
+
+
+
+
+
+
+ {{ currentRow.explanation }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ currentRow.functionName || "-" }}
+
+
+
+ {{ statusText[currentRow.executeStatus] }}
+
+ -
+
+
+
+ {{ formatNumber(currentRow.executionTime) }} ms
+
+
+ {{ formatNumber(currentRow.affectedRows) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 风险操作
+ -
+
+
+
+ {{ currentRow.userConfirmed ? "已确认" : "待确认" }}
+
+ -
+
+
+
+ {{ currentRow.ipAddress || "-" }}
+
+
+ {{ currentRow.currentRoute || "-" }}
+
+
+ {{ currentRow.userAgent || "-" }}
+
+
+
+ {{ currentRow.createTime }}
+
+
+ {{ currentRow.updateTime || "-" }}
+
+
+ {{ currentRow.remark || "-" }}
+
+
+
+
+ 关闭
+
+
+
+
+
+
+
+
diff --git a/src/views/business/conflict/components/DeptTree.vue b/src/views/business/conflict/components/DeptTree.vue
new file mode 100644
index 0000000..19e41db
--- /dev/null
+++ b/src/views/business/conflict/components/DeptTree.vue
@@ -0,0 +1,70 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/business/conflict/components/UserImport.vue b/src/views/business/conflict/components/UserImport.vue
new file mode 100644
index 0000000..0f00f9e
--- /dev/null
+++ b/src/views/business/conflict/components/UserImport.vue
@@ -0,0 +1,198 @@
+
+
+
+
+
+
+
+
+
+ 将文件拖到此处,或
+ 点击上传
+
+
+
+ 格式为*.xlsx / *.xls,文件不超过一个
+
+ 下载模板
+
+
+
+
+
+
+
+
+
+
+ 错误信息
+
+
+ 确 定
+
+ 取 消
+
+
+
+
+
+
+
+
+
+
+ {{ scope.row }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/business/conflict/index.vue b/src/views/business/conflict/index.vue
new file mode 100644
index 0000000..51ccb0b
--- /dev/null
+++ b/src/views/business/conflict/index.vue
@@ -0,0 +1,567 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+ 重置
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 重置密码
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/business/preRegistration/index.vue b/src/views/business/preRegistration/index.vue
new file mode 100644
index 0000000..4cf0d3e
--- /dev/null
+++ b/src/views/business/preRegistration/index.vue
@@ -0,0 +1,347 @@
+
+
+
预立案登记
+
+
+
+
+
+
+
+
+
+
委托人信息
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 复制
+
+ 删除
+
+
+
+
+
+
+
+
+ 新增一项
+
+
+
+ 批量删除
+
+
+
+
+
+
+
相对方信息
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 复制
+
+ 删除
+
+
+
+
+
+
+
+
+ 新增一项
+
+
+
+ 批量删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 提交
+ 暂存
+
+
+
+
+
+
+
diff --git a/src/views/codegen/index.vue b/src/views/codegen/index.vue
new file mode 100644
index 0000000..d91cdf1
--- /dev/null
+++ b/src/views/codegen/index.vue
@@ -0,0 +1,1325 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+
+
+
+
+
+ 重置
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 生成代码
+
+
+
+
+
+
+ 重置配置
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 普通
+ 封装(CURD)
+
+
+
+
+
+
+
+
+
+
+ 上级菜单
+
+
+ 选择上级菜单,生成代码后会自动创建对应菜单。
+
+ 注意1:生成菜单后需分配权限给角色,否则菜单将无法显示。
+
+ 注意2:演示环境默认不生成菜单,如需生成,请在本地部署数据库。
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 批量设置
+
+
+ 查询
+
+
+ 全选
+ 全不选
+
+
+
+
+ 列表
+
+
+ 全选
+ 全不选
+
+
+
+
+ 表单
+
+
+ 全选
+ 全不选
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ scope.row.columnName }}
+
+
+
+
+
+ {{ scope.row.columnType }}
+
+
+
+
+
+
+
+
+
+ {{ scope.row.fieldType }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+ 预览范围
+
+ 全部
+ 前端
+ 后端
+
+ 类型
+
+
+ {{ t }}
+
+
+
+
+
+
+
+
+
+ {{ data.label }}
+
+
+
+
+
+
+
+
+
+
+
+ 一键复制
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ prevBtnText }}
+
+
+ {{ nextBtnText }}
+
+
+
+
+
+
+
+
+
+
+
+ 写入本地
+
+
+
+
+
+
+
+
+
+
+
+
+ 选择
+
+
+
+
+
+ 选择
+
+
+
+
+ 全部
+ 仅前端
+ 仅后端
+
+
+
+
+ 覆盖
+ 跳过已存在
+ 仅变更覆盖
+
+
+
+
+
+
+
+ {{ writeProgress.done }}/{{ writeProgress.total }} {{ writeProgress.current }}
+
+
+
+
+
+ 取 消
+
+ 写 入
+
+
+
+
+
+
+
diff --git a/src/views/dashboard/index.vue b/src/views/dashboard/index.vue
new file mode 100644
index 0000000..6903933
--- /dev/null
+++ b/src/views/dashboard/index.vue
@@ -0,0 +1,579 @@
+
+
+
+
+
+
+

+
+
{{ greetings }}
+
今日天气晴朗,气温在15℃至25℃之间,东南风。
+
+
+
+
+
+
+
+
+ 新增项目
+
+
+
+ 新增项目
+
+
+
+ 新增项目
+
+
+
+ 新增项目
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 在线用户
+ 实时
+
+
+
+
+
+
+ {{ onlineUserCount }}
+
+
+
+ 已连接
+
+
+
+ 未连接
+
+
+
+
+
+
+ 更新时间
+ {{ formattedTime }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 访客数(UV)
+ 日
+
+
+
+
+
+ {{ Math.round(transitionUvCount) }}
+
+
+
+
+
+ {{ formatGrowthRate(visitStatsData.uvGrowthRate) }}
+
+
+
+
+
+
+ 总访客数
+ {{ Math.round(transitionTotalUvCount) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 浏览量(PV)
+ 日
+
+
+
+
+
+ {{ Math.round(transitionPvCount) }}
+
+
+
+
+
+ {{ formatGrowthRate(visitStatsData.pvGrowthRate) }}
+
+
+
+
+
+
+ 总浏览量
+ {{ Math.round(transitionTotalPvCount) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 访问趋势
+
+ 近7天
+ 近30天
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 完整记录
+
+
+
+
+
+
+
+
+
+
+ {{ item.title }}
+
+ {{ item.tag }}
+
+
+
+
{{ item.content }}
+
+
+
+ 详情
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/demo/api/apifox.vue b/src/views/demo/api/apifox.vue
new file mode 100644
index 0000000..0eb334b
--- /dev/null
+++ b/src/views/demo/api/apifox.vue
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
diff --git a/src/views/demo/auto-operation-column.vue b/src/views/demo/auto-operation-column.vue
new file mode 100644
index 0000000..fe71621
--- /dev/null
+++ b/src/views/demo/auto-operation-column.vue
@@ -0,0 +1,100 @@
+
+
+
+
+ 示例源码 请点击>>>>
+
+
+
+
自适应表格操作列
+
+ 该组件适用于含有操作列的表格。在某些情况下,按钮可能需要根据数据状态或其他条件动态展示,无法预设固定宽度。操作列组件能根据按钮数量自适应宽度,不需要再手动设置宽度。
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 查看
+
+ 超过了六个字会怎么样
+
+ 新增
+ 返回很多个字
+ 编辑
+ 默认
+
+
+
+
+
+
+
+
diff --git a/src/views/demo/curd-single.vue b/src/views/demo/curd-single.vue
new file mode 100644
index 0000000..98ed786
--- /dev/null
+++ b/src/views/demo/curd-single.vue
@@ -0,0 +1,593 @@
+
+
+
+
+ 整合版示例源码 请点击>>>>
+
+
+
+
+
+
+
+
+
+
+ {{ scope.row[scope.prop] == 1 ? "启用" : "禁用" }}
+
+
+
+
+
+
+ {{ scope.row[scope.prop] }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/demo/curd/config/add.ts b/src/views/demo/curd/config/add.ts
new file mode 100644
index 0000000..6d24fb6
--- /dev/null
+++ b/src/views/demo/curd/config/add.ts
@@ -0,0 +1,137 @@
+import UserAPI, { type UserForm } from "@/api/system/user-api";
+import type { IModalConfig } from "@/components/CURD/types";
+import { deptArr, roleArr } from "./options";
+
+const modalConfig: IModalConfig = {
+ permPrefix: "sys:user",
+ dialog: {
+ title: "新增用户",
+ width: 800,
+ draggable: true,
+ },
+ form: {
+ labelWidth: 100,
+ },
+ formAction: UserAPI.create,
+ beforeSubmit(data) {
+ console.log("提交之前处理", data);
+ },
+ formItems: [
+ {
+ label: "用户名",
+ prop: "username",
+ rules: [{ required: true, message: "用户名不能为空", trigger: "blur" }],
+ type: "input",
+ attrs: {
+ placeholder: "请输入用户名",
+ },
+ col: {
+ xs: 24,
+ sm: 12,
+ },
+ },
+ {
+ label: "用户昵称",
+ prop: "nickname",
+ rules: [{ required: true, message: "用户昵称不能为空", trigger: "blur" }],
+ type: "input",
+ attrs: {
+ placeholder: "请输入用户昵称",
+ },
+ col: {
+ xs: 24,
+ sm: 12,
+ },
+ },
+ {
+ label: "所属部门",
+ prop: "deptId",
+ rules: [{ required: true, message: "所属部门不能为空", trigger: "change" }],
+ type: "tree-select",
+ attrs: {
+ placeholder: "请选择所属部门",
+ data: deptArr,
+ filterable: true,
+ "check-strictly": true,
+ "render-after-expand": false,
+ },
+ // async initFn(formItem) {
+ // // 注意:如果initFn函数不是箭头函数,this会指向此配置项对象,那么也就可以用this来替代形参formItem
+ // formItem.attrs.data = await DeptAPI.getOptions();
+ // },
+ },
+ {
+ type: "custom",
+ label: "性别",
+ prop: "gender",
+ initialValue: 1,
+ attrs: { style: { width: "100%" } },
+ },
+ {
+ label: "角色",
+ prop: "roleIds",
+ rules: [{ required: true, message: "用户角色不能为空", trigger: "change" }],
+ type: "select",
+ attrs: {
+ placeholder: "请选择",
+ multiple: true,
+ },
+ options: roleArr,
+ initialValue: [],
+ // async initFn(formItem) {
+ // formItem.options = await RoleAPI.getOptions();
+ // },
+ },
+ {
+ type: "input",
+ label: "手机号码",
+ prop: "mobile",
+ rules: [
+ {
+ pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/,
+ message: "请输入正确的手机号码",
+ trigger: "blur",
+ },
+ ],
+ attrs: {
+ placeholder: "请输入手机号码",
+ maxlength: 11,
+ },
+ },
+ {
+ label: "邮箱",
+ prop: "email",
+ rules: [
+ {
+ pattern: /\w[-\w.+]*@([A-Za-z0-9][-A-Za-z0-9]+\.)+[A-Za-z]{2,14}/,
+ message: "请输入正确的邮箱地址",
+ trigger: "blur",
+ },
+ ],
+ type: "input",
+ attrs: {
+ placeholder: "请输入邮箱",
+ maxlength: 50,
+ },
+ },
+ {
+ label: "状态",
+ prop: "status",
+ type: "radio",
+ options: [
+ { label: "正常", value: 1 },
+ { label: "禁用", value: 0 },
+ ],
+ initialValue: 1,
+ },
+ {
+ type: "custom",
+ label: "二级弹窗",
+ prop: "openModal",
+ slotName: "openModal",
+ },
+ ],
+};
+
+// 如果有异步数据会修改配置的,推荐用reactive包裹,而纯静态配置的可以直接导出
+export default reactive(modalConfig);
diff --git a/src/views/demo/curd/config/content.ts b/src/views/demo/curd/config/content.ts
new file mode 100644
index 0000000..88d26a1
--- /dev/null
+++ b/src/views/demo/curd/config/content.ts
@@ -0,0 +1,137 @@
+import UserAPI from "@/api/system/user-api";
+import RoleAPI from "@/api/system/role-api";
+import type { UserPageQuery } from "@/api/system/user-api";
+import type { IContentConfig } from "@/components/CURD/types";
+
+const contentConfig: IContentConfig = {
+ permPrefix: "sys:user", // 不写不进行按钮权限校验
+ table: {
+ border: true,
+ highlightCurrentRow: true,
+ },
+ pagination: {
+ background: true,
+ layout: "prev,pager,next,jumper,total,sizes",
+ pageSize: 20,
+ pageSizes: [10, 20, 30, 50],
+ },
+ parseData(res) {
+ return {
+ total: res.total,
+ list: res.list,
+ };
+ },
+ indexAction(params) {
+ return UserAPI.getPage(params);
+ },
+ deleteAction: UserAPI.deleteByIds,
+ importAction(file) {
+ return UserAPI.import("1", file);
+ },
+ exportAction: UserAPI.export,
+ importTemplate: UserAPI.downloadTemplate,
+ importsAction(data) {
+ // 模拟导入数据
+ console.log("importsAction", data);
+ return Promise.resolve();
+ },
+ async exportsAction(params) {
+ // 模拟获取到的是全量数据
+ const res = await UserAPI.getPage(params);
+ console.log("exportsAction", res.list);
+ return res.list;
+ },
+ pk: "id",
+ toolbar: [
+ "add",
+ "delete",
+ "import",
+ "export",
+ {
+ name: "custom1",
+ text: "自定义1",
+ perm: "add",
+ attrs: { icon: "plus", color: "#626AEF" },
+ },
+ ],
+ defaultToolbar: ["refresh", "filter", "imports", "exports", "search"],
+ cols: [
+ { type: "selection", width: 50, align: "center" },
+ { label: "编号", align: "center", prop: "id", width: 100, show: false },
+ { label: "用户名", align: "center", prop: "username" },
+ { label: "头像", align: "center", prop: "avatar", templet: "image" },
+ { label: "用户昵称", align: "center", prop: "nickname", width: 120 },
+ {
+ label: "性别",
+ align: "center",
+ prop: "gender",
+ width: 100,
+ templet: "custom",
+ slotName: "gender",
+ },
+ { label: "部门", align: "center", prop: "deptName", width: 120 },
+ {
+ label: "角色",
+ align: "center",
+ prop: "roleNames",
+ width: 120,
+ columnKey: "roleIds",
+ filters: [],
+ filterMultiple: true,
+ filterJoin: ",",
+ async initFn(colItem) {
+ const roleOptions = await RoleAPI.getOptions();
+ colItem.filters = roleOptions.map((item) => {
+ return { text: item.label, value: item.value };
+ });
+ },
+ },
+ {
+ label: "手机号码",
+ align: "center",
+ prop: "mobile",
+ templet: "custom",
+ slotName: "mobile",
+ width: 150,
+ },
+ {
+ label: "状态",
+ align: "center",
+ prop: "status",
+ templet: "custom",
+ slotName: "status",
+ },
+ { label: "创建时间", align: "center", prop: "createTime", width: 180 },
+ {
+ label: "操作",
+ align: "center",
+ fixed: "right",
+ width: 280,
+ templet: "tool",
+ operat: [
+ {
+ name: "detail",
+ text: "详情",
+ attrs: { icon: "Document", type: "primary" },
+ },
+ {
+ name: "reset_pwd",
+ text: "重置密码",
+ // perm: "password-reset",
+ attrs: {
+ icon: "refresh-left",
+ // color: "#626AEF", // 使用 text 属性,颜色不生效
+ style: {
+ "--el-button-text-color": "#626AEF",
+ "--el-button-hover-link-text-color": "#9197f4",
+ },
+ },
+ },
+ "edit",
+ "delete",
+ ],
+ },
+ ],
+};
+
+export default contentConfig;
diff --git a/src/views/demo/curd/config/edit.ts b/src/views/demo/curd/config/edit.ts
new file mode 100644
index 0000000..708ca5c
--- /dev/null
+++ b/src/views/demo/curd/config/edit.ts
@@ -0,0 +1,127 @@
+import UserAPI, { type UserForm } from "@/api/system/user-api";
+import type { IModalConfig } from "@/components/CURD/types";
+import { DeviceEnum } from "@/enums/settings/device-enum";
+import { useAppStore } from "@/store";
+import { deptArr, roleArr } from "./options";
+
+const modalConfig: IModalConfig = {
+ permPrefix: "sys:user",
+ component: "drawer",
+ drawer: {
+ title: "修改用户",
+ size: useAppStore().device === DeviceEnum.MOBILE ? "80%" : 500,
+ },
+ pk: "id",
+ beforeSubmit(data) {
+ console.log("beforeSubmit", data);
+ },
+ formAction(data) {
+ return UserAPI.update(data.id as string, data);
+ },
+ formItems: [
+ {
+ label: "用户名",
+ prop: "username",
+ rules: [{ required: true, message: "用户名不能为空", trigger: "blur" }],
+ type: "input",
+ attrs: {
+ placeholder: "请输入用户名",
+ readonly: true,
+ },
+ },
+ {
+ label: "用户昵称",
+ prop: "nickname",
+ rules: [{ required: true, message: "用户昵称不能为空", trigger: "blur" }],
+ type: "input",
+ attrs: {
+ placeholder: "请输入用户昵称",
+ },
+ },
+ {
+ label: "所属部门",
+ prop: "deptId",
+ rules: [{ required: true, message: "所属部门不能为空", trigger: "blur" }],
+ type: "tree-select",
+ attrs: {
+ placeholder: "请选择所属部门",
+ data: deptArr, // setup,Vue会自动解包ref,不需要.value
+ filterable: true,
+ "check-strictly": true,
+ "render-after-expand": false,
+ },
+ // async initFn(formItem) {
+ // // 注意:如果initFn函数不是箭头函数,this会指向此配置项对象,那么也就可以用this来替代形参formItem
+ // formItem.attrs.data = await DeptAPI.getOptions();
+ // },
+ },
+ {
+ type: "custom",
+ label: "性别",
+ prop: "gender",
+ initialValue: 1,
+ attrs: { style: { width: "100%" } },
+ },
+ {
+ label: "角色",
+ prop: "roleIds",
+ rules: [{ required: true, message: "用户角色不能为空", trigger: "blur" }],
+ type: "select",
+ attrs: {
+ placeholder: "请选择",
+ multiple: true,
+ },
+ options: roleArr,
+ initialValue: [],
+ // async initFn(formItem) {
+ // formItem.options = await RoleAPI.getOptions();
+ // },
+ },
+ {
+ type: "input",
+ label: "手机号码",
+ prop: "mobile",
+ rules: [
+ {
+ pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/,
+ message: "请输入正确的手机号码",
+ trigger: "blur",
+ },
+ ],
+ attrs: {
+ placeholder: "请输入手机号码",
+ maxlength: 11,
+ },
+ },
+ {
+ label: "邮箱",
+ prop: "email",
+ rules: [
+ {
+ pattern: /\w[-\w.+]*@([A-Za-z0-9][-A-Za-z0-9]+\.)+[A-Za-z]{2,14}/,
+ message: "请输入正确的邮箱地址",
+ trigger: "blur",
+ },
+ ],
+ type: "input",
+ attrs: {
+ placeholder: "请输入邮箱",
+ maxlength: 50,
+ },
+ },
+ {
+ label: "状态",
+ prop: "status",
+ type: "switch",
+ attrs: {
+ inlinePrompt: true,
+ activeText: "正常",
+ inactiveText: "禁用",
+ activeValue: 1,
+ inactiveValue: 0,
+ },
+ },
+ ],
+};
+
+export default reactive(modalConfig);
diff --git a/src/views/demo/curd/config/options.ts b/src/views/demo/curd/config/options.ts
new file mode 100644
index 0000000..c154bd4
--- /dev/null
+++ b/src/views/demo/curd/config/options.ts
@@ -0,0 +1,31 @@
+/** 公共下载数据,减少重复请求次数 */
+import DeptAPI from "@/api/system/dept-api";
+import RoleAPI from "@/api/system/role-api";
+
+interface OptionType {
+ label: string;
+ value: any;
+ [key: string]: any; // 允许其他属性
+}
+
+// 明确指定类型为 OptionType[]
+export const deptArr = ref([]);
+export const roleArr = ref([]);
+export const stateArr = ref([
+ { label: "启用", value: 1 },
+ { label: "禁用", value: 0 },
+]);
+
+// 初始化逻辑,在 onMounted 钩子中调用
+export const initOptions = async () => {
+ try {
+ // 使用Promise.all并行请求
+ const [dept, roles] = await Promise.all([DeptAPI.getOptions(), RoleAPI.getOptions()]);
+ // 获取部门选项并赋值
+ deptArr.value = dept;
+ // 获取角色选项并赋值
+ roleArr.value = roles;
+ } catch (error) {
+ console.error("初始化选项失败:", error);
+ }
+};
diff --git a/src/views/demo/curd/config/search.ts b/src/views/demo/curd/config/search.ts
new file mode 100644
index 0000000..35f2942
--- /dev/null
+++ b/src/views/demo/curd/config/search.ts
@@ -0,0 +1,63 @@
+import type { ISearchConfig } from "@/components/CURD/types";
+import { deptArr, stateArr } from "./options";
+
+const searchConfig: ISearchConfig = {
+ permPrefix: "sys:user",
+ formItems: [
+ {
+ tips: "支持模糊搜索",
+ type: "input",
+ label: "关键字",
+ prop: "keywords",
+ attrs: {
+ placeholder: "用户名/昵称/手机号",
+ clearable: true,
+ style: { width: "200px" },
+ },
+ },
+ {
+ type: "tree-select",
+ label: "部门",
+ prop: "deptId",
+ attrs: {
+ placeholder: "请选择",
+ data: deptArr,
+ filterable: true,
+ "check-strictly": true,
+ "render-after-expand": false,
+ clearable: true,
+ style: { width: "200px" },
+ },
+ // async initFn(formItem) {
+ // // 注意:如果initFn函数不是箭头函数,this会指向此配置项对象,那么也就可以用this来替代形参formItem
+ // formItem.attrs.data = await DeptAPI.getOptions();
+ // },
+ },
+ {
+ type: "select",
+ label: "状态",
+ prop: "status",
+ attrs: {
+ placeholder: "全部",
+ clearable: true,
+ style: { width: "200px" },
+ },
+ options: stateArr,
+ },
+ {
+ type: "date-picker",
+ label: "创建时间",
+ prop: "createTime",
+ attrs: {
+ type: "daterange",
+ "range-separator": "~",
+ "start-placeholder": "开始时间",
+ "end-placeholder": "截止时间",
+ "value-format": "YYYY-MM-DD",
+ style: { width: "200px" },
+ },
+ },
+ ],
+};
+
+export default searchConfig;
diff --git a/src/views/demo/curd/config2/add.ts b/src/views/demo/curd/config2/add.ts
new file mode 100644
index 0000000..0e27288
--- /dev/null
+++ b/src/views/demo/curd/config2/add.ts
@@ -0,0 +1,55 @@
+import type { UserForm } from "@/api/system/user-api";
+import type { IModalConfig } from "@/components/CURD/types";
+import { deptArr } from "../config/options";
+
+const modalConfig: IModalConfig = {
+ colon: true,
+ dialog: {
+ title: "二级弹窗",
+ width: 500,
+ draggable: true,
+ },
+ form: {
+ labelWidth: "auto",
+ labelPosition: "top",
+ },
+ formItems: [
+ {
+ label: "用户名",
+ prop: "username",
+ rules: [{ required: true, message: "用户名不能为空", trigger: "blur" }],
+ type: "input",
+ attrs: { placeholder: "请输入" },
+ },
+ {
+ label: "用户昵称",
+ prop: "nickname",
+ rules: [{ required: true, message: "用户昵称不能为空", trigger: "blur" }],
+ type: "input",
+ attrs: { placeholder: "请输入" },
+ },
+ {
+ label: "所属部门",
+ prop: "deptId",
+ rules: [{ required: true, message: "所属部门不能为空", trigger: "change" }],
+ type: "tree-select",
+ attrs: {
+ placeholder: "请选择",
+ data: deptArr,
+ filterable: true,
+ "check-strictly": true,
+ "render-after-expand": false,
+ },
+ },
+ {
+ type: "custom",
+ label: "性别",
+ prop: "gender",
+ initialValue: 1,
+ attrs: { style: { width: "100%" } },
+ },
+ ],
+};
+
+// 如果有异步数据会修改配置的,推荐用reactive包裹,而纯静态配置的可以直接导出
+export default reactive(modalConfig);
diff --git a/src/views/demo/curd/config2/content.ts b/src/views/demo/curd/config2/content.ts
new file mode 100644
index 0000000..72b0dd2
--- /dev/null
+++ b/src/views/demo/curd/config2/content.ts
@@ -0,0 +1,136 @@
+import type { IContentConfig } from "@/components/CURD/types";
+
+const contentConfig: IContentConfig = {
+ // permPrefix: "sys:demo", // 不写不进行按钮权限校验
+ table: {
+ showOverflowTooltip: true,
+ },
+ pagePosition: "right",
+ toolbar: [],
+ indexAction(params) {
+ // 模拟发起网络请求获取列表数据
+ console.log("indexAction:", params);
+ return Promise.resolve({
+ total: 2,
+ list: [
+ {
+ id: 1,
+ username: "root",
+ avatar: "https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif",
+ percent: 99,
+ price: 10,
+ url: "https://www.baidu.com",
+ icon: "el-icon-setting",
+ gender: 1,
+ status: 1,
+ status2: 1,
+ sort: 99,
+ createTime: 1715647982437,
+ },
+ {
+ id: 2,
+ username: "jerry",
+ avatar: "https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif",
+ percent: 88,
+ price: 999,
+ url: "https://www.google.com",
+ icon: "el-icon-user",
+ gender: 0,
+ status: 0,
+ status2: 0,
+ sort: 0,
+ createTime: 1715648977426,
+ },
+ ],
+ });
+ },
+ modifyAction(data) {
+ // 模拟发起网络请求修改字段
+ // console.log("modifyAction:", data);
+ ElMessage.success(JSON.stringify(data));
+ return Promise.resolve(null);
+ },
+ cols: [
+ { type: "index", width: 50, align: "center" },
+ { label: "ID", align: "center", prop: "id", show: false },
+ { label: "文本", align: "center", prop: "username" },
+ { label: "图片", align: "center", prop: "avatar", templet: "image" },
+ {
+ label: "百分比",
+ align: "center",
+ prop: "percent",
+ templet: "percent",
+ },
+ {
+ label: "货币符",
+ align: "center",
+ prop: "price",
+ templet: "price",
+ priceFormat: "$",
+ },
+ { label: "链接", align: "center", prop: "url", width: 180, templet: "url" },
+ { label: "图标", align: "center", prop: "icon", templet: "icon" },
+ {
+ label: "列表值",
+ align: "center",
+ prop: "gender",
+ templet: "list",
+ selectList: { "0": "女", "1": "男" },
+ },
+ {
+ label: "自定义",
+ align: "center",
+ prop: "status",
+ templet: "custom",
+ slotName: "status",
+ },
+ {
+ label: "Switch",
+ align: "center",
+ prop: "status2",
+ templet: "switch",
+ activeValue: 1,
+ inactiveValue: 0,
+ activeText: "启用",
+ inactiveText: "禁用",
+ },
+ {
+ label: "输入框",
+ align: "center",
+ prop: "sort",
+ templet: "input",
+ inputType: "number",
+ },
+ {
+ label: "日期格式化",
+ align: "center",
+ prop: "createTime",
+ minWidth: 120,
+ templet: "date",
+ dateFormat: "YYYY/MM/DD HH:mm:ss",
+ },
+ {
+ label: "操作栏",
+ align: "center",
+ fixed: "right",
+ width: 220,
+ templet: "tool",
+ operat: [
+ "view",
+ "edit",
+ {
+ name: "delete",
+ text: "展示删除",
+ perm: "delete",
+ attrs: { icon: "delete", type: "danger" },
+ render(row) {
+ // 根据条件,显示或隐藏
+ return row.id !== 1;
+ },
+ },
+ ],
+ },
+ ],
+};
+
+export default contentConfig;
diff --git a/src/views/demo/curd/config2/edit.ts b/src/views/demo/curd/config2/edit.ts
new file mode 100644
index 0000000..7127379
--- /dev/null
+++ b/src/views/demo/curd/config2/edit.ts
@@ -0,0 +1,105 @@
+import type { IModalConfig } from "@/components/CURD/types";
+import { DeviceEnum } from "@/enums/settings/device-enum";
+import { useAppStore } from "@/store";
+
+const modalConfig: IModalConfig = {
+ permPrefix: "sys:user",
+ component: "drawer",
+ colon: true,
+ pk: "id",
+ drawer: {
+ title: "修改用户",
+ size: useAppStore().device === DeviceEnum.MOBILE ? "80%" : 500,
+ },
+ form: { labelPosition: "right", labelWidth: "auto" },
+ beforeSubmit(data) {
+ console.log("beforeSubmit", data);
+ },
+ formAction(data) {
+ // return UserAPI.update(data.id as string, data);
+ // 模拟发起网络请求修改字段
+ ElMessage.success(JSON.stringify(data));
+ return Promise.resolve(null);
+ },
+ formItems: [
+ {
+ tips: { effect: "light", placement: "top", content: "自定义文字提示" },
+ type: "input",
+ label: "文本",
+ prop: "username",
+ attrs: { placeholder: "请输入", clearable: true },
+ },
+ {
+ type: "input-number",
+ label: "百分比",
+ prop: "percent",
+ attrs: { placeholder: "请输入", controls: false },
+ slotName: "suffix",
+ },
+ {
+ type: "input-number",
+ label: "货币符",
+ prop: "price",
+ attrs: { placeholder: "请输入", controls: false },
+ slotName: "prefix",
+ },
+ {
+ type: "input",
+ label: "链接",
+ prop: "url",
+ attrs: { placeholder: "请输入", clearable: true },
+ },
+ {
+ type: "icon-select",
+ label: "链接",
+ prop: "icon",
+ },
+ {
+ type: "custom",
+ label: "列表值",
+ prop: "gender",
+ slotName: "gender",
+ attrs: { style: { width: "100%" } },
+ },
+ {
+ type: "select",
+ label: "自定义",
+ prop: "status",
+ attrs: { placeholder: "全部", clearable: true },
+ options: [
+ { label: "启用", value: 1 },
+ { label: "禁用", value: 0 },
+ ],
+ },
+ {
+ type: "switch",
+ label: "Switch",
+ prop: "status2",
+ attrs: {
+ inlinePrompt: true,
+ activeValue: 1,
+ inactiveValue: 0,
+ activeText: "启用",
+ inactiveText: "禁用",
+ },
+ },
+ {
+ type: "input-number",
+ label: "输入框",
+ prop: "sort",
+ attrs: { placeholder: "请输入", controls: false },
+ },
+ {
+ type: "date-picker",
+ label: "日期格式化",
+ prop: "createTime",
+ attrs: {
+ type: "datetime",
+ format: "YYYY/MM/DD hh:mm:ss",
+ "value-format": "x",
+ },
+ },
+ ],
+};
+
+export default reactive(modalConfig);
diff --git a/src/views/demo/curd/config2/search.ts b/src/views/demo/curd/config2/search.ts
new file mode 100644
index 0000000..83c1a10
--- /dev/null
+++ b/src/views/demo/curd/config2/search.ts
@@ -0,0 +1,152 @@
+import type { ISearchConfig } from "@/components/CURD/types";
+import { deptArr, stateArr } from "../config/options";
+
+const searchConfig: ISearchConfig = {
+ grid: "right",
+ colon: true,
+ showNumber: 3,
+ form: { labelPosition: "right", labelWidth: "90px" },
+ cardAttrs: { shadow: "hover", style: { "margin-bottom": "12px" } },
+ formItems: [
+ {
+ tips: { effect: "light", placement: "top", content: "自定义文字提示" },
+ type: "input",
+ label: "输入框",
+ prop: "testInput",
+ attrs: { placeholder: "请输入", clearable: true },
+ events: {
+ change: (e) => {
+ console.log("输入框的值: ", e);
+ // 级联操作示例,需要使用reactive提前定义数组
+ // selectOptions.push({ label: e, value: e });
+ },
+ },
+ },
+ {
+ type: "input-number",
+ label: "数字输入框",
+ prop: "testInputNumber",
+ attrs: { placeholder: "请输入", controls: false },
+ },
+ {
+ type: "select",
+ label: "下拉选择框",
+ prop: "testSelect",
+ attrs: { placeholder: "全部", clearable: true },
+ options: stateArr as any,
+ events: {
+ change(e) {
+ console.log("选中的值: ", e);
+ },
+ },
+ },
+ {
+ type: "tree-select",
+ label: "树形选择框",
+ prop: "testTreeSelect",
+ attrs: {
+ placeholder: "请选择",
+ data: deptArr,
+ filterable: true,
+ "check-strictly": true,
+ "render-after-expand": false,
+ clearable: true,
+ },
+ // async initFn(formItem) {
+ // // 注意:如果initFn函数不是箭头函数,this会指向此配置项对象,那么也就可以用this来替代形参formItem
+ // formItem.attrs.data = await DeptAPI.getOptions();
+ // },
+ },
+ {
+ type: "cascader",
+ label: "级联选择器",
+ prop: "testCascader",
+ attrs: {
+ placeholder: "请选择",
+ clearable: true,
+ props: {
+ expandTrigger: "hover",
+ label: "label",
+ value: "value",
+ children: "children",
+ },
+ options: [
+ {
+ value: "guide",
+ label: "Guide",
+ children: [
+ {
+ value: "disciplines",
+ label: "Disciplines",
+ children: [
+ {
+ value: "consistency",
+ label: "Consistency",
+ },
+ ],
+ },
+ {
+ value: "navigation",
+ label: "Navigation",
+ children: [
+ {
+ value: "side nav",
+ label: "Side Navigation",
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ },
+ {
+ type: "date-picker",
+ label: "范围选择器",
+ prop: "createAt",
+ attrs: {
+ type: "daterange",
+ "range-separator": "~",
+ "start-placeholder": "开始时间",
+ "end-placeholder": "截止时间",
+ "value-format": "YYYY-MM-DD",
+ },
+ },
+ {
+ type: "date-picker",
+ label: "日期选择器",
+ prop: "testDataPicker",
+ attrs: { placeholder: "请选择", type: "date" },
+ },
+ {
+ type: "time-picker",
+ label: "时间选择器",
+ prop: "testTimePicker",
+ attrs: { placeholder: "请选择", clearable: true },
+ },
+ {
+ type: "time-select",
+ label: "时间选择",
+ prop: "testTimeSelect",
+ attrs: { placeholder: "请选择", clearable: true },
+ },
+ {
+ type: "input-tag",
+ label: "标签选择器",
+ prop: "testInputTags",
+ attrs: { placeholder: "请选择", clearable: true },
+ },
+ {
+ type: "custom-tag",
+ label: "标签选择器",
+ prop: "testCustomTags",
+ attrs: {
+ buttonAttrs: { btnText: "+ New Tag" },
+ inputAttrs: {},
+ tagAttrs: {},
+ },
+ },
+ ],
+};
+
+export default searchConfig;
diff --git a/src/views/demo/curd/index.vue b/src/views/demo/curd/index.vue
new file mode 100644
index 0000000..3960bd9
--- /dev/null
+++ b/src/views/demo/curd/index.vue
@@ -0,0 +1,213 @@
+
+
+
+
+ 示例源码 请点击>>>>
+
+ 切换示例
+
+
+
+
+
+
+
+
+
+
+
+ {{ scope.row[scope.prop] == 1 ? "启用" : "禁用" }}
+
+
+
+
+
+
+ {{ scope.row[scope.prop] }}
+
+
+
+
+
+
+
+
+
+
+ 打开二级弹窗
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ scope.row[scope.prop] == 1 ? "启用" : "禁用" }}
+
+
+
+
+
+
+ %
+
+
+ $
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/demo/detail.vue b/src/views/demo/detail.vue
new file mode 100644
index 0000000..e4cc0f6
--- /dev/null
+++ b/src/views/demo/detail.vue
@@ -0,0 +1,17 @@
+
+
+
params: {{ route.params }}
+
query: {{ route.query }}
+
+
+
+
diff --git a/src/views/demo/dict-sync.vue b/src/views/demo/dict-sync.vue
new file mode 100644
index 0000000..ddb915f
--- /dev/null
+++ b/src/views/demo/dict-sync.vue
@@ -0,0 +1,308 @@
+
+
+
+
+
+
+
+
+ 本示例展示WebSocket实时更新字典缓存的效果。您可以编辑"男"性别字典项,保存后后端将通过WebSocket通知所有客户端刷新缓存。
+
+
+
+
+
+
+
+ 性别字典项 - 男
+ 重新加载
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ success
+
+
+ warning
+
+
+ danger
+
+
+ info
+
+
+ primary
+
+
+
+
+ 保存
+ 重置
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 字典组件展示
+
+ 手动刷新
+
+
+
+
+
性别组件
+
+
+ {{ item.label }}
+
+
+
+
性别标签
+
+
+ {{ item.label }}
+
+
+
+
+
已选择值: {{ selectedGender }}
+
最后更新: {{ lastUpdateTime }}
+
+
+
+
+
+
+
+
+
+
+
字典缓存数据
+
+
+ 已缓存
+
+ 未缓存
+
+
+
+
+
{{
+ JSON.stringify(dictStore.getDictItems("gender"), null, 2)
+ }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/demo/dictionary.vue b/src/views/demo/dictionary.vue
new file mode 100644
index 0000000..c69cb9c
--- /dev/null
+++ b/src/views/demo/dictionary.vue
@@ -0,0 +1,48 @@
+
+
+
+
+ 示例源码 请点击>>>>
+
+
+
+
+
+ 值为String: const value = ref("1");
+
+
+
+
+
+
+ 值为Number: const value = ref(1);
+
+
+
+
+
+
+ 值为Number: const value = ref(1);
+
+
+
+
+
+
+ 值为Array: const value = ref(["1", "2"]);
+
+
+
+
+
+
+
diff --git a/src/views/demo/drag.vue b/src/views/demo/drag.vue
new file mode 100644
index 0000000..783dd98
--- /dev/null
+++ b/src/views/demo/drag.vue
@@ -0,0 +1,123 @@
+
+
+
+
+
+
+
+
+ {{ item.name }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.name }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 移动
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/demo/icon-select.vue b/src/views/demo/icon-select.vue
new file mode 100644
index 0000000..d3a7968
--- /dev/null
+++ b/src/views/demo/icon-select.vue
@@ -0,0 +1,21 @@
+
+
+
+
+ 示例源码 请点击>>>>
+
+
+
+
+
+
diff --git a/src/views/demo/icons.vue b/src/views/demo/icons.vue
new file mode 100644
index 0000000..d6b8c24
--- /dev/null
+++ b/src/views/demo/icons.vue
@@ -0,0 +1,129 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ name }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/demo/internal-doc.vue b/src/views/demo/internal-doc.vue
new file mode 100644
index 0000000..8ac1098
--- /dev/null
+++ b/src/views/demo/internal-doc.vue
@@ -0,0 +1,25 @@
+
+
+
+
+
+
diff --git a/src/views/demo/multi-level/children/children/detail.ts b/src/views/demo/multi-level/children/children/detail.ts
new file mode 100644
index 0000000..3920725
--- /dev/null
+++ b/src/views/demo/multi-level/children/children/detail.ts
@@ -0,0 +1,40 @@
+import router from "@/router";
+import { ElButton } from "element-plus";
+import { useTagsViewStore } from "@/store";
+
+export default defineComponent({
+ name: "ToDetail",
+ setup() {
+ const route = useRoute();
+ const tagsViewStore = useTagsViewStore();
+
+ // 跳转详情
+ const navigateToDetail = async (id: number) => {
+ await router.push({
+ path: "/detail/" + id,
+ query: { message: `msg${id}` },
+ });
+ // 更改标题
+ tagsViewStore.updateTagName(route.fullPath, `详情页缓存(id=${id})`);
+ };
+ return () =>
+ h("div", null, [
+ h(
+ ElButton,
+ {
+ type: "primary",
+ onClick: () => navigateToDetail(1),
+ },
+ { default: () => "跳转详情1" }
+ ),
+ h(
+ ElButton,
+ {
+ type: "success",
+ onClick: () => navigateToDetail(2),
+ },
+ { default: () => "跳转详情2" }
+ ),
+ ]);
+ },
+});
diff --git a/src/views/demo/multi-level/children/children/level3-1.vue b/src/views/demo/multi-level/children/children/level3-1.vue
new file mode 100644
index 0000000..cd84a28
--- /dev/null
+++ b/src/views/demo/multi-level/children/children/level3-1.vue
@@ -0,0 +1,23 @@
+
+
+
+
diff --git a/src/views/demo/multi-level/children/children/level3-2.vue b/src/views/demo/multi-level/children/children/level3-2.vue
new file mode 100644
index 0000000..f20d350
--- /dev/null
+++ b/src/views/demo/multi-level/children/children/level3-2.vue
@@ -0,0 +1,23 @@
+
+
+
+
+
diff --git a/src/views/demo/multi-level/children/level2.vue b/src/views/demo/multi-level/children/level2.vue
new file mode 100644
index 0000000..8c9d951
--- /dev/null
+++ b/src/views/demo/multi-level/children/level2.vue
@@ -0,0 +1,13 @@
+
+
+
+
+
diff --git a/src/views/demo/multi-level/level1.vue b/src/views/demo/multi-level/level1.vue
new file mode 100644
index 0000000..caf7a96
--- /dev/null
+++ b/src/views/demo/multi-level/level1.vue
@@ -0,0 +1,10 @@
+
+
+
+
diff --git a/src/views/demo/route-param.vue b/src/views/demo/route-param.vue
new file mode 100644
index 0000000..a904ee2
--- /dev/null
+++ b/src/views/demo/route-param.vue
@@ -0,0 +1,16 @@
+
+ 路由参数type:{{ query }}
+
+
+
+
+
diff --git a/src/views/demo/signature.vue b/src/views/demo/signature.vue
new file mode 100644
index 0000000..d61e1ee
--- /dev/null
+++ b/src/views/demo/signature.vue
@@ -0,0 +1,185 @@
+
+
+
基于canvas实现的签名组件
+
+
+
![签名]()
+
+
+
+
diff --git a/src/views/demo/table-select/config/select.ts b/src/views/demo/table-select/config/select.ts
new file mode 100644
index 0000000..98200d2
--- /dev/null
+++ b/src/views/demo/table-select/config/select.ts
@@ -0,0 +1,121 @@
+import UserAPI from "@/api/system/user-api";
+import type { ISelectConfig } from "@/components/TableSelect/index.vue";
+
+const selectConfig: ISelectConfig = {
+ pk: "id",
+ width: "70%",
+ placeholder: "请选择用户",
+ formItems: [
+ {
+ type: "input",
+ label: "关键字",
+ prop: "keywords",
+ attrs: {
+ placeholder: "用户名/昵称/手机号",
+ clearable: true,
+ style: {
+ width: "200px",
+ },
+ },
+ },
+ {
+ type: "tree-select",
+ label: "部门",
+ prop: "deptId",
+ attrs: {
+ placeholder: "请选择",
+ data: [
+ {
+ value: 1,
+ label: "有来技术",
+ children: [
+ {
+ value: 2,
+ label: "研发部门",
+ },
+ {
+ value: 3,
+ label: "测试部门",
+ },
+ ],
+ },
+ ],
+ filterable: true,
+ "check-strictly": true,
+ "render-after-expand": false,
+ clearable: true,
+ style: {
+ width: "150px",
+ },
+ },
+ },
+ {
+ type: "select",
+ label: "状态",
+ prop: "status",
+ attrs: {
+ placeholder: "全部",
+ clearable: true,
+ style: {
+ width: "100px",
+ },
+ },
+ options: [
+ { label: "启用", value: 1 },
+ { label: "禁用", value: 0 },
+ ],
+ },
+ {
+ type: "date-picker",
+ label: "创建时间",
+ prop: "createAt",
+ attrs: {
+ type: "daterange",
+ "range-separator": "~",
+ "start-placeholder": "开始时间",
+ "end-placeholder": "截止时间",
+ "value-format": "YYYY-MM-DD",
+ style: {
+ width: "240px",
+ },
+ },
+ },
+ ],
+ indexAction(params) {
+ if ("createAt" in params) {
+ const createAt = params.createAt as string[];
+ if (createAt?.length > 1) {
+ params.startTime = createAt[0];
+ params.endTime = createAt[1];
+ }
+ delete params.createAt;
+ }
+ return UserAPI.getPage(params);
+ },
+ tableColumns: [
+ { type: "selection", width: 50, align: "center" },
+ { label: "编号", align: "center", prop: "id", width: 100 },
+ { label: "用户名", align: "center", prop: "username" },
+ { label: "用户昵称", align: "center", prop: "nickname", width: 120 },
+ {
+ label: "性别",
+ align: "center",
+ prop: "gender",
+ width: 100,
+ templet: "custom",
+ slotName: "gender",
+ },
+ { label: "部门", align: "center", prop: "deptName", width: 120 },
+ { label: "手机号码", align: "center", prop: "mobile", width: 120 },
+ {
+ label: "状态",
+ align: "center",
+ prop: "status",
+ templet: "custom",
+ slotName: "status",
+ },
+ { label: "创建时间", align: "center", prop: "createTime", width: 180 },
+ ],
+};
+
+export default selectConfig;
diff --git a/src/views/demo/table-select/index.vue b/src/views/demo/table-select/index.vue
new file mode 100644
index 0000000..c9c8397
--- /dev/null
+++ b/src/views/demo/table-select/index.vue
@@ -0,0 +1,54 @@
+
+
+
+
+ 示例源码 请点击>>>>
+
+
+
+
+ {{ scope.row[scope.prop] == 1 ? "启用" : "禁用" }}
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/demo/text-scroll.vue b/src/views/demo/text-scroll.vue
new file mode 100644
index 0000000..e700046
--- /dev/null
+++ b/src/views/demo/text-scroll.vue
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/demo/upload.vue b/src/views/demo/upload.vue
new file mode 100644
index 0000000..0b28141
--- /dev/null
+++ b/src/views/demo/upload.vue
@@ -0,0 +1,40 @@
+
+
+
+
+ 示例源码 请点击>>>>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/demo/vxe-table/index.vue b/src/views/demo/vxe-table/index.vue
new file mode 100644
index 0000000..841aff8
--- /dev/null
+++ b/src/views/demo/vxe-table/index.vue
@@ -0,0 +1,632 @@
+
+
+
+
+
+
+
+
+
+ 新增用户
+
+
+ 批量删除
+
+
+
+
+
+
+ -
+ ID:
+ {{ row.id }}
+
+ -
+ UserName:
+ {{ row.username }}
+
+ -
+ CreateTime:
+ {{ row.createTime }}
+
+
+
+
+
+
+
+ {{ role }}
+
+
+
+
+ 修改
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/demo/wang-editor.vue b/src/views/demo/wang-editor.vue
new file mode 100644
index 0000000..dbb1525
--- /dev/null
+++ b/src/views/demo/wang-editor.vue
@@ -0,0 +1,24 @@
+
+
+
+
+ 示例源码 请点击>>>>
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/demo/websocket.vue b/src/views/demo/websocket.vue
new file mode 100644
index 0000000..56e9cc6
--- /dev/null
+++ b/src/views/demo/websocket.vue
@@ -0,0 +1,242 @@
+
+
+
+ 示例源码 请点击>>>>
+
+
+
+
+
+
+
+
+ 连接
+
+
+ 断开
+
+
+
+ 连接状态:
+ 已连接
+ 已断开
+
+
+
+
+
+
+
+
+
+
+ 发送广播
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 发送点对点消息
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ message.sender }}
+
+
{{ message.content }}
+
+
+
{{ message.content }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/error/401.vue b/src/views/error/401.vue
new file mode 100644
index 0000000..e91f37b
--- /dev/null
+++ b/src/views/error/401.vue
@@ -0,0 +1,25 @@
+
+
+
返回
+
+
+ Oops!
+ 你没有权限去该页面
+ 如有不满请联系你领导
+
+ 或者你可以去:
+ 回首页
+ 随便看看
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/error/404.vue b/src/views/error/404.vue
new file mode 100644
index 0000000..28138d5
--- /dev/null
+++ b/src/views/error/404.vue
@@ -0,0 +1,64 @@
+
+
+

+
+
OOPS!
+
+ 该页面无法访问。
+
+ 有来开源官网
+
+
+
抱歉,您访问的页面不存在。
+
+ 请确认您输入的网址是否正确,或者点击下方按钮返回首页。
+
+
返回首页
+
+
+
+
+
+
+
diff --git a/src/views/login/components/Login.vue b/src/views/login/components/Login.vue
new file mode 100644
index 0000000..60d4669
--- /dev/null
+++ b/src/views/login/components/Login.vue
@@ -0,0 +1,222 @@
+
+
+
{{ t("login.login") }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
![captchaCode]()
+
点击获取验证码
+
+
+
+
+
+ {{ t("login.rememberMe") }}
+
+
+
+
+
+ {{ t("login.login") }}
+
+
+
+
+
+ {{ t("login.noAccount") }}
+
+ {{ t("login.reg") }}
+
+
+
+
+
+
+
diff --git a/src/views/login/components/Register.vue b/src/views/login/components/Register.vue
new file mode 100644
index 0000000..0358474
--- /dev/null
+++ b/src/views/login/components/Register.vue
@@ -0,0 +1,203 @@
+
+
+
{{ t("login.reg") }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
![code]()
+
+
+
+
+
+
+ {{ t("login.agree") }}
+ {{ t("login.userAgreement") }}
+
+
+
+
+
+
+ {{ t("login.register") }}
+
+
+
+
+ {{ t("login.haveAccount") }}
+ {{ t("login.login") }}
+
+
+
+
diff --git a/src/views/login/components/ResetPwd.vue b/src/views/login/components/ResetPwd.vue
new file mode 100644
index 0000000..734fc54
--- /dev/null
+++ b/src/views/login/components/ResetPwd.vue
@@ -0,0 +1,58 @@
+
+
+
{{ t("login.resetPassword") }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ t("login.resetPassword") }}
+
+
+
+
+
+ {{ t("login.thinkOfPasswd") }}
+ {{ t("login.login") }}
+
+
+
+
diff --git a/src/views/login/index.vue b/src/views/login/index.vue
new file mode 100644
index 0000000..aefb98f
--- /dev/null
+++ b/src/views/login/index.vue
@@ -0,0 +1,122 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/profile/index.vue b/src/views/profile/index.vue
new file mode 100644
index 0000000..ab5bdfd
--- /dev/null
+++ b/src/views/profile/index.vue
@@ -0,0 +1,649 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ userProfile.nickname }}
+
+
+
+
+
{{ userProfile.roleNames }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ userProfile.username }}
+
+
+
+
+
+
+
+
+ {{ userProfile.mobile || "未绑定" }}
+ handleOpenDialog(DialogType.MOBILE)"
+ >
+ 更换
+
+ handleOpenDialog(DialogType.MOBILE)"
+ >
+ 绑定
+
+
+
+ {{ userProfile.email || "未绑定" }}
+ handleOpenDialog(DialogType.EMAIL)"
+ >
+ 更换
+
+ handleOpenDialog(DialogType.EMAIL)"
+ >
+ 绑定
+
+
+
+ {{ userProfile.deptName }}
+
+
+ {{ userProfile.createTime }}
+
+
+
+
+
+
+
+
+
+
+
账户密码
+
定期修改密码有助于保护账户安全
+
+
handleOpenDialog(DialogType.PASSWORD)">
+ 修改
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ mobileCountdown > 0 ? `${mobileCountdown}s后重新发送` : "发送验证码" }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ emailCountdown > 0 ? `${emailCountdown}s后重新发送` : "发送验证码" }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/redirect/index.vue b/src/views/redirect/index.vue
new file mode 100644
index 0000000..2b61386
--- /dev/null
+++ b/src/views/redirect/index.vue
@@ -0,0 +1,15 @@
+
+
+
+
+
diff --git a/src/views/stamp-application/index.vue b/src/views/stamp-application/index.vue
new file mode 100644
index 0000000..373858a
--- /dev/null
+++ b/src/views/stamp-application/index.vue
@@ -0,0 +1,26 @@
+
+
+
申请用印页面
+
这是一个没有子节点的路由示例页面
+
+
+
+
+
+
diff --git a/src/views/system/config/index.vue b/src/views/system/config/index.vue
new file mode 100644
index 0000000..3035806
--- /dev/null
+++ b/src/views/system/config/index.vue
@@ -0,0 +1,295 @@
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+ 重置
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 编辑
+
+
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/system/dept/index.vue b/src/views/system/dept/index.vue
new file mode 100644
index 0000000..fd92b8b
--- /dev/null
+++ b/src/views/system/dept/index.vue
@@ -0,0 +1,320 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+
+ 重置
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 正常
+ 禁用
+
+
+
+
+
+
+
+
+ 新增
+
+
+ 编辑
+
+
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 正常
+ 禁用
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/system/dict/dict-item.vue b/src/views/system/dict/dict-item.vue
new file mode 100644
index 0000000..7ba92fc
--- /dev/null
+++ b/src/views/system/dict/dict-item.vue
@@ -0,0 +1,314 @@
+
+
+
+
+
+
+
+
+
+
+ 搜索
+ 重置
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ scope.row.status === 1 ? "启用" : "禁用" }}
+
+
+
+
+
+
+
+ 编辑
+
+
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 启用
+ 禁用
+
+
+
+
+
+
+
+
+ 标签类型
+
+ 回显样式,为空时则显示 '文本'
+
+
+
+
+
+
+
+
+
+ {{ formData.label ? formData.label : "字典标签" }}
+
+
+
+
+
+ {{ formData.label ?? "字典标签" }}
+ {{ type }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/system/dict/index.vue b/src/views/system/dict/index.vue
new file mode 100644
index 0000000..43542b0
--- /dev/null
+++ b/src/views/system/dict/index.vue
@@ -0,0 +1,298 @@
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+ 重置
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ scope.row.status === 1 ? "启用" : "禁用" }}
+
+
+
+
+
+
+
+
+
+ 字典数据
+
+
+
+ 编辑
+
+
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 启用
+ 禁用
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/system/log/index.vue b/src/views/system/log/index.vue
new file mode 100644
index 0000000..532ca3a
--- /dev/null
+++ b/src/views/system/log/index.vue
@@ -0,0 +1,118 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+ 重置
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/system/menu/index.vue b/src/views/system/menu/index.vue
new file mode 100644
index 0000000..9c04554
--- /dev/null
+++ b/src/views/system/menu/index.vue
@@ -0,0 +1,547 @@
+
+
+
+
+
+
+
+
+
+
+ 搜索
+ 重置
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ scope.row.name }}
+
+
+
+
+
+ 目录
+ 菜单
+ 按钮
+ 外链
+
+
+
+
+
+
+
+
+ 显示
+ 隐藏
+
+
+
+
+
+
+ 新增
+
+
+
+ 编辑
+
+
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 目录
+ 菜单
+ 按钮
+ 外链
+
+
+
+
+
+
+
+
+
+
+ 路由名称
+
+
+ 如果需要开启缓存,需保证页面 defineOptions 中的 name 与此处一致,建议使用驼峰。
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 路由路径
+
+
+ 定义应用中不同页面对应的 URL 路径,目录需以 / 开头,菜单项不用。例如:系统管理目录
+ /system,系统管理下的用户管理菜单 user。
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 组件路径
+
+
+ 组件页面完整路径,相对于 src/views/,如 system/user/index,缺省后缀 .vue
+
+
+
+
+
+
+
+
+
+ src/views/
+ .vue
+
+
+
+
+
+
+ 路由参数
+
+
+ 组件页面使用 `useRoute().query.参数名` 获取路由参数值。
+
+
+
+
+
+
+
+
+
+
+ 添加路由参数
+
+
+
+
+
+
+
+ =
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 显示
+ 隐藏
+
+
+
+
+
+
+ 始终显示
+
+
+ 选择"是",即使目录或菜单下只有一个子节点,也会显示父节点。
+
+ 选择"否",如果目录或菜单下只有一个子节点,则只显示该子节点,隐藏父节点。
+
+ 如果是叶子节点,请选择"否"。
+
+
+
+
+
+
+
+
+
+ 是
+ 否
+
+
+
+
+
+ 开启
+ 关闭
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/system/notice/components/MyNotice.vue b/src/views/system/notice/components/MyNotice.vue
new file mode 100644
index 0000000..1048508
--- /dev/null
+++ b/src/views/system/notice/components/MyNotice.vue
@@ -0,0 +1,204 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+
+
+
+
+
+ 重置
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 已读
+ 未读
+
+
+
+
+
+ 查看
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ noticeDetail.publisherName }}
+
+
+
+ {{ noticeDetail.publishTime }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/system/notice/index.vue b/src/views/system/notice/index.vue
new file mode 100644
index 0000000..d8ff8d3
--- /dev/null
+++ b/src/views/system/notice/index.vue
@@ -0,0 +1,471 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+ 重置
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 全体
+ 指定
+
+
+
+
+ 未发布
+ 已发布
+ 已撤回
+
+
+
+
+
+ 创建时间:
+ {{ scope.row.createTime || "-" }}
+
+
+
+ 发布时间:
+ {{ scope.row.publishTime || "-" }}
+
+
+ 撤回时间:
+ {{ scope.row.revokeTime || "-" }}
+
+
+
+
+
+
+ 查看
+
+
+ 发布
+
+
+ 撤回
+
+
+ 编辑
+
+
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 全体
+ 指定
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ currentNotice.title }}
+
+
+ 未发布
+ 已发布
+ 已撤回
+
+
+ {{ currentNotice.publisherName }}
+
+
+ {{ currentNotice.publishTime }}
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/system/role/index.vue b/src/views/system/role/index.vue
new file mode 100644
index 0000000..db7aaa4
--- /dev/null
+++ b/src/views/system/role/index.vue
@@ -0,0 +1,445 @@
+
+
+
+
+
+
+
+
+
+
+ 搜索
+ 重置
+
+
+
+
+
+
+
+
+
+
+
+
+ 正常
+ 禁用
+
+
+
+
+
+
+
+
+ 编辑
+
+
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ isExpanded ? "收缩" : "展开" }}
+
+
+ 父子联动
+
+
+
+
+ 如果只需勾选菜单权限,不需要勾选子菜单或者按钮权限,请关闭父子联动
+
+
+
+
+
+
+
+
+
+
+ {{ data.label }}
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/system/user/components/DeptTree.vue b/src/views/system/user/components/DeptTree.vue
new file mode 100644
index 0000000..19e41db
--- /dev/null
+++ b/src/views/system/user/components/DeptTree.vue
@@ -0,0 +1,70 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/system/user/components/UserImport.vue b/src/views/system/user/components/UserImport.vue
new file mode 100644
index 0000000..0f00f9e
--- /dev/null
+++ b/src/views/system/user/components/UserImport.vue
@@ -0,0 +1,198 @@
+
+
+
+
+
+
+
+
+
+ 将文件拖到此处,或
+ 点击上传
+
+
+
+ 格式为*.xlsx / *.xls,文件不超过一个
+
+ 下载模板
+
+
+
+
+
+
+
+
+
+
+ 错误信息
+
+
+ 确 定
+
+ 取 消
+
+
+
+
+
+
+
+
+
+
+ {{ scope.row }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/system/user/index.vue b/src/views/system/user/index.vue
new file mode 100644
index 0000000..b8b9bfd
--- /dev/null
+++ b/src/views/system/user/index.vue
@@ -0,0 +1,666 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+ 重置
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ scope.row.status == 1 ? "正常" : "禁用" }}
+
+
+
+
+
+
+
+ 重置密码
+
+
+ 编辑
+
+
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..8e1a1fa
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,55 @@
+{
+ "compilerOptions": {
+ "target": "esnext",
+<<<<<<< HEAD
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "lib": ["esnext", "dom"],
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["src/*"]
+ },
+
+ // 严格性和类型检查相关配置
+ "strict": true,
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true,
+
+ // 模块和兼容性相关配置
+ "allowSyntheticDefaultImports": true,
+ "esModuleInterop": true,
+ "resolveJsonModule": true,
+
+ // 调试和兼容性相关配置
+ "sourceMap": true,
+ "useDefineForClassFields": true,
+ "allowJs": true,
+
+ // 类型声明相关配置
+ "types": ["node", "vite/client", "element-plus/global"]
+ },
+
+ "include": [
+ "mock/**/*.ts",
+ "src/**/*.ts",
+ "src/**/*.vue",
+ "vite.config.ts",
+ "eslint.config.ts",
+ "uno.config.ts"
+ ],
+ "exclude": ["node_modules", "dist"]
+=======
+ "useDefineForClassFields": true,
+ "module": "esnext",
+ "moduleResolution": "node",
+ "strict": true,
+ "jsx": "preserve",
+ "sourceMap": true,
+ "resolveJsonModule": true,
+ "esModuleInterop": true,
+ "lib": ["esnext", "dom"]
+ },
+ "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
+
+>>>>>>> 232db255 ('首次提交')
+}
diff --git a/uno.config.ts b/uno.config.ts
new file mode 100644
index 0000000..82913ca
--- /dev/null
+++ b/uno.config.ts
@@ -0,0 +1,83 @@
+// https://unocss.nodejs.cn/guide/config-file
+import {
+ defineConfig,
+ presetAttributify,
+ presetIcons,
+ presetTypography,
+ presetUno,
+ presetWebFonts,
+ transformerDirectives,
+ transformerVariantGroup,
+} from "unocss";
+
+import { FileSystemIconLoader } from "@iconify/utils/lib/loader/node-loaders";
+import fs from "fs";
+
+// 本地SVG图标目录
+const iconsDir = "./src/assets/icons";
+
+// 读取本地 SVG 目录,自动生成 safelist
+const generateSafeList = () => {
+ try {
+ return fs
+ .readdirSync(iconsDir)
+ .filter((file) => file.endsWith(".svg"))
+ .map((file) => `i-svg:${file.replace(".svg", "")}`);
+ } catch (error) {
+ console.error("无法读取图标目录:", error);
+ return [];
+ }
+};
+
+export default defineConfig({
+ // 自定义快捷类
+ shortcuts: {
+ "wh-full": "w-full h-full",
+ "flex-center": "flex justify-center items-center",
+ "flex-x-center": "flex justify-center",
+ "flex-y-center": "flex items-center",
+ "flex-x-start": "flex items-center justify-start",
+ "flex-x-between": "flex items-center justify-between",
+ "flex-x-end": "flex items-center justify-end",
+ },
+ theme: {
+ colors: {
+ primary: "var(--el-color-primary)",
+ primary_dark: "var(--el-color-primary-light-5)",
+ },
+ breakpoints: Object.fromEntries(
+ [640, 768, 1024, 1280, 1536, 1920, 2560].map((size, index) => [
+ ["sm", "md", "lg", "xl", "2xl", "3xl", "4xl"][index],
+ `${size}px`,
+ ])
+ ),
+ },
+ presets: [
+ presetUno(),
+ presetAttributify(),
+ presetIcons({
+ // 额外属性
+ extraProperties: {
+ display: "inline-block",
+ width: "1em",
+ height: "1em",
+ },
+ // 图表集合
+ collections: {
+ // svg 是图标集合名称,使用 `i-svg:图标名` 调用
+ svg: FileSystemIconLoader(iconsDir, (svg) => {
+ // 如果 `fill` 没有定义,则添加 `fill="currentColor"`
+ return svg.includes('fill="') ? svg : svg.replace(/^