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": [ + "", + "", + "", + "", + "", + "" + ], + "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": [ + "", + "", + "", + "", + "", + "" + ], + "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 @@ + + ic/csdn + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + 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 @@ + + 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 @@ + + + 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 @@ + + 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 @@ + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + 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 @@ + + + + + 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 @@ + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + 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 @@ + + + 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 @@ + + + + + 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 @@ + +