commit 759813095e6f8b999b35fae17a7ec235fd89f561 Author: 雷校云 <14135925+chenxilxy@user.noreply.gitee.com> Date: Thu Feb 26 15:11:34 2026 +0800 项目初始化 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..b03c6a3 --- /dev/null +++ b/.env.development @@ -0,0 +1,19 @@ +# 应用端口 +VITE_APP_PORT=3000 +# 项目名称 +VITE_APP_TITLE=vue3-element-admin +# 代理前缀 +VITE_APP_BASE_API=/dev-api + +# 生产环境配置 +VITE_APP_ENV = 'development' + +# 接口地址 +# VITE_APP_API_URL=http://192.168.0.61:8007 # 本地 +VITE_APP_API_URL=http://26.151.107.60:8007 # 本地 + +# 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..98c1b32 --- /dev/null +++ b/.env.production @@ -0,0 +1,19 @@ +# 应用端口 +VITE_APP_PORT=3000 +# 项目名称 +VITE_APP_TITLE=vue3-element-admin +# 代理前缀 +VITE_APP_BASE_API=/dev-api + +# 生产环境配置 +VITE_APP_ENV = 'production' + +# 接口地址 +VITE_APP_API_URL=http://8.137.99.82:9000 # 线上 +# 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/.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..c6484bf --- /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: false +# 使用双引号而不是单引号 +singleQuote: true +# 缩进使用 2 个空格 +tabWidth: 2 +# 在多行元素的末尾添加逗号(ES5 支持的对象、数组等) +trailingComma: "none" +# 使用空格而不是制表符缩进 +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 ` + + + 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..6cc9b37 --- /dev/null +++ b/package.json @@ -0,0 +1,131 @@ +{ + "name": "jurs-zun", + "description": "Vue3 + Vite + TypeScript + Element-Plus 的后台管理模板,vue-element-admin 的 Vue3 版本", + "version": "3.4.2", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "dev:prod": "vite --mode production", + "build": "vue-tsc --noEmit & vite build", + "build:dev": "vue-tsc --noEmit & vite build --mode development", + "build:prod": "vue-tsc --noEmit & vite build --mode production", + "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", + "license": "MIT" +} 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/public/img/logo.png b/public/img/logo.png new file mode 100644 index 0000000..be095c0 Binary files /dev/null and b/public/img/logo.png differ diff --git a/src/App.vue b/src/App.vue new file mode 100644 index 0000000..c45169e --- /dev/null +++ b/src/App.vue @@ -0,0 +1,17 @@ + + + diff --git a/src/api/BoosAccountManagement/index.ts b/src/api/BoosAccountManagement/index.ts new file mode 100644 index 0000000..a747500 --- /dev/null +++ b/src/api/BoosAccountManagement/index.ts @@ -0,0 +1,65 @@ +import request from '@/utils/request' + +/* + * boos账号管理 + * */ + +// 查账号状态表(查所有) +export const ApiAccounts = () => { + return request({ + url: `/api/accounts`, + method: 'get' + }) +} + +// 查询指定电脑的账号状态 +export const ApiAccountsId = (id: string) => { + return request({ + url: `/api/accounts/${id}`, + method: 'get' + }) +} + +// 绑定账号到电脑 +export const ApiAccountsAdd = (data: any) => { + const formData = new FormData() + formData.append('browser_name', data.browser_name) + formData.append('worker_id', data.worker_id) + return request({ + url: `/api/accounts`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// // 编辑申请用印 +// export const BusinessEditApplication = (data: any) => { +// const formData = new FormData() +// formData.append('id', data.id) +// formData.append('Printingpurpose', data.Printingpurpose) +// formData.append('case_id', data.case_id) +// formData.append('Reason', data.Reason) +// formData.append('seal_number', data.seal_number) +// formData.append('seal_type', data.seal_type) +// if (isFile(data.file)) formData.append('file', data.file) +// formData.append('approvers', data.approvers) +// return request({ +// url: `/business/editApplication`, +// method: 'post', +// data: formData, +// headers: { +// 'Content-Type': 'multipart/form-data' +// } +// }) +// } + +// 删除指定账号 +export const ApiAccountsDelete = (id: string) => { + return request({ + url: `/api/accounts/${id}`, + method: 'delete' + }) +} diff --git a/src/api/TaskManagement/index.ts b/src/api/TaskManagement/index.ts new file mode 100644 index 0000000..0e944b1 --- /dev/null +++ b/src/api/TaskManagement/index.ts @@ -0,0 +1,67 @@ +import request from '@/utils/request' + +/* + * 任务管理 + * */ + +// 查询任务列表 +export const ApiTasks = () => { + return request({ + url: `/api/tasks`, + method: 'get' + }) +} + +// 查询指定任务的状态和结果 +export const ApiTasksTaskId = (task_id: string) => { + return request({ + url: `/api/tasks/${task_id}`, + method: 'get' + }) +} + +// 提交新任务 +export const ApiTasksAdd = (data: any) => { + const formData = new FormData() + formData.append('task_type', data.task_type) + formData.append('worker_id', data.worker_id) + formData.append('account_name', data.account_name) + formData.append('params', data.params) + return request({ + url: `/api/tasks`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// // 编辑申请用印 +// export const BusinessEditApplication = (data: any) => { +// const formData = new FormData() +// formData.append('id', data.id) +// formData.append('Printingpurpose', data.Printingpurpose) +// formData.append('case_id', data.case_id) +// formData.append('Reason', data.Reason) +// formData.append('seal_number', data.seal_number) +// formData.append('seal_type', data.seal_type) +// if (isFile(data.file)) formData.append('file', data.file) +// formData.append('approvers', data.approvers) +// return request({ +// url: `/business/editApplication`, +// method: 'post', +// data: formData, +// headers: { +// 'Content-Type': 'multipart/form-data' +// } +// }) +// } +// +// // 删除指定账号 +// export const ApiAccountsDelete = (id: string) => { +// return request({ +// url: `/api/accounts/${id}`, +// method: 'delete' +// }) +// } diff --git a/src/api/ai/index.ts b/src/api/ai/index.ts new file mode 100644 index 0000000..5705c5f --- /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..a0074ac --- /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', 'admin') + 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/calibration/accountAdjustmentApplication/index.ts b/src/api/calibration/accountAdjustmentApplication/index.ts new file mode 100644 index 0000000..b1f52b6 --- /dev/null +++ b/src/api/calibration/accountAdjustmentApplication/index.ts @@ -0,0 +1,74 @@ +import request from '@/utils/request' + +/* + * 调账申请 + * */ + +// 新增调账申请 +export const FinanceLoan = (data: any) => { + const formData = new FormData() + formData.append('times', data.times) + formData.append('case_id', data.case_id) + formData.append('amount', data.amount) + formData.append('situation', data.situation) + return request({ + url: `/finance/loan`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 编辑调账申请 +export const FinanceEditAccounts = (data: any) => { + const formData = new FormData() + formData.append('id', data.id) + formData.append('times', data.times) + formData.append('case_id', data.case_id) + formData.append('amount', data.amount) + formData.append('situation', data.situation) + return request({ + url: `/finance/editAccounts`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 调账申请展示 +export const FinanceLoandisplay = (data: any) => { + const formData = new FormData() + formData.append('page', data.pageNum) + formData.append('per_page', data.pageSize) + if (data.times && data.times.length) { + formData.append('times', data.times[0]) + formData.append('end_time', data.times[1]) + } + formData.append('CustomerID', data.CustomerID) + return request({ + url: `/finance/loandisplay`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 删除调账申请 +export const FinanceDeleteAccounts = (id: string) => { + const formData = new FormData() + formData.append('id', id) + return request({ + url: `/finance/deleteAccounts`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} diff --git a/src/api/calibration/announcementManagement/index.ts b/src/api/calibration/announcementManagement/index.ts new file mode 100644 index 0000000..8268885 --- /dev/null +++ b/src/api/calibration/announcementManagement/index.ts @@ -0,0 +1,75 @@ +import request from '@/utils/request' +import { isFile } from '@/utils/auxiliaryFunction' + +/* + * 公告管理 + * */ + +// 新增公告 +export const BusinessBulletin = (data: any) => { + const formData = new FormData() + formData.append('title', data.title) + formData.append('content', data.content) + formData.append('file', data.file) + formData.append('state', data.state) + return request({ + url: `/business/bulletin`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 编辑公告 +export const BusinessEditBulletin = (data: any) => { + const formData = new FormData() + formData.append('id', data.id) + formData.append('title', data.title) + formData.append('content', data.content) + if (isFile(data.file)) formData.append('file', data.file) + formData.append('state', data.state) + return request({ + url: `/business/editBulletin`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 公告展示 +export const BusinessBulletindetail = (data: any) => { + const formData = new FormData() + formData.append('page', data.pageNum) + formData.append('per_page', data.pageSize) + // if (data.times && data.times.length) { + // formData.append('times', data.times[0]) + // formData.append('end_time', data.times[1]) + // } + // formData.append('unit', data.unit) + return request({ + url: `/business/bulletindetail`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 删除公告 +export const BusinessDeleteBulletin = (id: string) => { + const formData = new FormData() + formData.append('id', id) + return request({ + url: `/business/deleteBulletin`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} diff --git a/src/api/calibration/applicationForSealApproval/index.ts b/src/api/calibration/applicationForSealApproval/index.ts new file mode 100644 index 0000000..af74b0f --- /dev/null +++ b/src/api/calibration/applicationForSealApproval/index.ts @@ -0,0 +1,82 @@ +import request from '@/utils/request' +import { isFile } from '@/utils/auxiliaryFunction' + +/* + * 申请用印 + * */ + +// 新增申请用印 +export const BusinessApplication = (data: any) => { + const formData = new FormData() + formData.append('Printingpurpose', data.Printingpurpose) + formData.append('case_id', data.case_id) + formData.append('Reason', data.Reason) + formData.append('seal_number', data.seal_number) + formData.append('seal_type', data.seal_type) + formData.append('file', data.file) + formData.append('approvers', data.approvers) + return request({ + url: `/business/application`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 编辑申请用印 +export const BusinessEditApplication = (data: any) => { + const formData = new FormData() + formData.append('id', data.id) + formData.append('Printingpurpose', data.Printingpurpose) + formData.append('case_id', data.case_id) + formData.append('Reason', data.Reason) + formData.append('seal_number', data.seal_number) + formData.append('seal_type', data.seal_type) + if (isFile(data.file)) formData.append('file', data.file) + formData.append('approvers', data.approvers) + return request({ + url: `/business/editApplication`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 申请用印展示 +export const BusinessApplicationdetail = (data: any) => { + const formData = new FormData() + formData.append('page', data.pageNum) + formData.append('per_page', data.pageSize) + if (data.times && data.times.length) { + formData.append('times', data.times[0]) + formData.append('end_time', data.times[1]) + } + formData.append('seal_type', data.seal_type) + formData.append('CaseNumber', data.CaseNumber) + return request({ + url: `/business/applicationdetail`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 删除申请用印 +export const BusinessDeleteApplication = (id: string) => { + const formData = new FormData() + formData.append('id', id) + return request({ + url: `/business/deleteApplication`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} diff --git a/src/api/calibration/approval/index.ts b/src/api/calibration/approval/index.ts new file mode 100644 index 0000000..5850a75 --- /dev/null +++ b/src/api/calibration/approval/index.ts @@ -0,0 +1,56 @@ +import request from '@/utils/request' + +/* + * 审批 + * */ + +// 审批接口 +export const UserApprovalProcessing = (data: any) => { + const formData = new FormData() + formData.append('id', data.id) + formData.append('type', data.type) + formData.append('state', data.state) + if (data.rejection_reason) formData.append('rejection_reason', data.rejection_reason) + if (data.settlement_salary) formData.append('settlement_salary', data.settlement_salary) + if (data.allocate) formData.append('allocate', data.allocate) + return request({ + url: `/user/approval_processing`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 查询待办审核状态接口 +export const UserApprovalStatusCheck = (data: any) => { + const formData = new FormData() + formData.append('id', data.id) + formData.append('type', data.type) + return request({ + url: `/user/approval-status-check`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 审批分页接口 +export const UserRoxyexhibition = (data: any) => { + const formData = new FormData() + formData.append('page', data.pageNum) + formData.append('per_page', data.pageSize) + formData.append('type', data.type) + formData.append('status', data.status) + return request({ + url: `/user/roxyexhibition`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} diff --git a/src/api/calibration/bidRegistration/index.ts b/src/api/calibration/bidRegistration/index.ts new file mode 100644 index 0000000..58bf77f --- /dev/null +++ b/src/api/calibration/bidRegistration/index.ts @@ -0,0 +1,78 @@ +import request from '@/utils/request' +import { isFile } from '@/utils/auxiliaryFunction' + +/* + * 投标登记 + * */ + +// 新增投标登记 +export const BusinessBid = (data: any) => { + const formData = new FormData() + formData.append('BiddingUnit', data.BiddingUnit) + formData.append('ProjectName', data.ProjectName) + formData.append('times', data.times) + formData.append('BiddingAnnouncement', data.BiddingAnnouncement) + formData.append('approvers', data.approvers) + return request({ + url: `/business/bid`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 编辑投标登记 +export const BusinessEditBid = (data: any) => { + const formData = new FormData() + formData.append('id', data.id) + formData.append('BiddingUnit', data.BiddingUnit) + formData.append('ProjectName', data.ProjectName) + formData.append('times', data.times) + if (isFile(data.BiddingAnnouncement)) + formData.append('BiddingAnnouncement', data.BiddingAnnouncement) + formData.append('approvers', data.approvers) + return request({ + url: `/business/editBid`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 投标登记展示 +export const BusinessBiddetail = (data: any) => { + const formData = new FormData() + formData.append('page', data.pageNum) + formData.append('per_page', data.pageSize) + if (data.times && data.times.length) { + formData.append('times', data.times[0]) + formData.append('end_time', data.times[1]) + } + formData.append('client_username', data.client_username) + return request({ + url: `/business/biddetail`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 删除投标登记 +export const BusinessDeleteBid = (id: string) => { + const formData = new FormData() + formData.append('id', id) + return request({ + url: `/business/deleteBid`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} diff --git a/src/api/calibration/caseLabel/index.ts b/src/api/calibration/caseLabel/index.ts new file mode 100644 index 0000000..44f10d2 --- /dev/null +++ b/src/api/calibration/caseLabel/index.ts @@ -0,0 +1,128 @@ +import request from '@/utils/request' +import { isFile } from '@/utils/auxiliaryFunction' + +/* + * 标签管理 + * */ + +// 新增标签 +export const BusinessCaseTagCreate = (data: any) => { + const formData = new FormData() + formData.append('name', data.name) + formData.append('color', data.color) + formData.append('description', data.description) + return request({ + url: `/business/case-tag-create`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 编辑标签 +export const BusinessCaseTagEdit = (data: any) => { + const formData = new FormData() + formData.append('id', data.id) + formData.append('name', data.name) + formData.append('color', data.color) + formData.append('description', data.description) + return request({ + url: `/business/case-tag-edit`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 标签下拉列表 +export const BusinessCaseTagDropdownList = () => { + return request({ + url: `/business/case-tag-dropdown-list`, + method: 'post', + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 标签列表查询 +export const BusinessCaseTagList = (data: any) => { + const formData = new FormData() + formData.append('page', data.pageNum) + formData.append('per_page', data.pageSize) + formData.append('name', data.name) + return request({ + url: `/business/case-tag-list`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 标签详情查询 +export const BusinessCaseTagDetail = (id: string) => { + const formData = new FormData() + formData.append('id', id) + return request({ + url: `/business/case-tag-detail`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 删除标签 +export const BusinessCaseTagDelete = (id: string) => { + const formData = new FormData() + formData.append('id', id) + return request({ + url: `/business/case-tag-delete`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 按标签筛选案件 +export const BusinessCaseListByTag = (data: any) => { + const formData = new FormData() + formData.append('tag_ids', data.tag_ids) + formData.append('page', data.page) + formData.append('per_page', data.per_page) + formData.append('times', data.times) + formData.append('end_time', data.times) + formData.append('type', data.type) + return request({ + url: `/business/case-list-by-tag`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 案件设置标签接口 +export const BusinessSetCaseTags = (data: any) => { + const formData = new FormData() + formData.append('case_id', data.case_id) + formData.append('tag_ids', data.tag_ids) + return request({ + url: `/business/set-case-tags`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} diff --git a/src/api/calibration/caseManagement/index.ts b/src/api/calibration/caseManagement/index.ts new file mode 100644 index 0000000..6f07532 --- /dev/null +++ b/src/api/calibration/caseManagement/index.ts @@ -0,0 +1,272 @@ +import request from '@/utils/request' +import { isFile } from '@/utils/auxiliaryFunction' + +/* + * 案件管理 + * */ + +// 新增案件 +export const BusinessCasemanagement = (data: any) => { + const formData = new FormData() + formData.append('project_id', data.project_id) + formData.append('times', data.times) + if (isFile(data.AgencyContract)) formData.append('AgencyContract', data.AgencyContract) + if (isFile(data.Contractreturn)) formData.append('Contractreturn', data.Contractreturn) + if (isFile(data.Closingapplication)) + formData.append('Closingapplication', data.Closingapplication) + formData.append('approvers', JSON.stringify(data.approvers)) + return request({ + url: `/business/casemanagement`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 编辑案件管理信息 +export const BusinessEditCase = (data: any) => { + const formData = new FormData() + formData.append('id', data.id) + formData.append('times', data.times) + if (isFile(data.AgencyContract)) formData.append('AgencyContract', data.AgencyContract) + if (isFile(data.Contractreturn)) formData.append('Contractreturn', data.Contractreturn) + if (isFile(data.Closingapplication)) + formData.append('Closingapplication', data.Closingapplication) + formData.append('ChangeRequest', data.ChangeRequest) + formData.append('paymentcollection', data.paymentcollection) + return request({ + url: `/business/editCase`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 案件分页查询 +export const BusinessCasemanagementdetail = (data: any) => { + const formData = new FormData() + formData.append('page', data.pageNum) + formData.append('per_page', data.pageSize) + if (data.times && data.times.length) { + formData.append('times', data.times[0]) + formData.append('end_time', data.times[1]) + } + formData.append('type', data.type) + formData.append('contract_no', data.contract_no) + formData.append('client_name', data.client_name) + formData.append('party_name', data.party_name) + if (data.tags && data.tags.length) formData.append('tags', JSON.stringify(data.tags)) + return request({ + url: `/business/casemanagementdetail`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 删除案件管理 +export const BusinessDeleteCase = (id: string) => { + const formData = new FormData() + formData.append('id', id) + return request({ + url: `/business/deleteCase`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 案件已收款(累加) +export const BusinessAccumulate = (data: any) => { + const formData = new FormData() + formData.append('user_id', data.user_id) + formData.append('paymentcollection', data.paymentcollection) + return request({ + url: `/business/accumulate`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 上传发票 +export const BusinessUploadinvoice = (data: any) => { + const formData = new FormData() + formData.append('user_id', data.user_id) + formData.append('amount', data.amount) + formData.append('file', data.file) + return request({ + url: `/business/uploadinvoice`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 发票详情 +export const BusinessInvoicedetail = (id: string) => { + const formData = new FormData() + formData.append('id', id) + return request({ + url: `/business/invoicedetail`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 案件日志 +export const BusinessLog = (data: any) => { + const formData = new FormData() + formData.append('content', data.content) + formData.append('file', data.file) + formData.append('case_id', data.case_id) + return request({ + url: `/business/log`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 案件日志导出Excel +export const BusinessExportCaseLogExcel = (data: any) => { + const formData = new FormData() + formData.append('case_id', data.case_id) + return request({ + url: `/business/export-case-log-excel`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 案件日志分页查询 +export const BusinessLogdetail = (data: any) => { + const formData = new FormData() + formData.append('page', data.pageNum) + formData.append('per_page', data.pageSize) + formData.append('case_id', data.case_id) + return request({ + url: `/business/logdetail`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 案件材料更新 +export const BusinessCaseAttachmentUpdate = (data: any) => { + const formData = new FormData() + formData.append('type', data.type) + if (isFile(data.file)) formData.append('file', data.file) + formData.append('case_id', data.case_id) + if (data.Contractreturn) formData.append('Contractreturn', data.Contractreturn) + if (data.approvers) formData.append('approvers', JSON.stringify(data.approvers)) + return request({ + url: `/business/case-attachment-update`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 创建案件下拉列表接口 +export const BusinessCaseDropdownList = (data: any) => { + const formData = new FormData() + formData.append('state', data.state) + return request({ + url: `/business/case-dropdown-list`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 创建申请变更 +export const BusinessCaseChangeRequestCreate = (data: any) => { + const formData = new FormData() + formData.append('case_id', data.case_id) + formData.append('contract_no', data.contract_no) + formData.append('change_item', data.change_item) + formData.append('change_reason', data.change_reason) + formData.append('change_agreement', data.change_agreement) + if (data.approvers) formData.append('approvers', JSON.stringify(data.approvers)) + return request({ + url: `/business/case-change-request-create`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 申请变更列表 +export const BusinessCaseChangeRequestList = (data: any) => { + const formData = new FormData() + formData.append('page', data.pageNum) + formData.append('per_page', data.pageSize) + formData.append('case_id', data.case_id) + return request({ + url: `/business/case-change-request-list`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 申请变更详情 +export const ChangeRequestId = (change_request_id: string) => { + const formData = new FormData() + formData.append('change_request_id', change_request_id) + return request({ + url: `change_request_id`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 利益冲突检索接口 +export const BusinessConflictSearch = (data: any) => { + const formData = new FormData() + formData.append('client_info', data.client_info) + formData.append('party_info', data.party_info) + return request({ + url: `/business/conflict-search`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} diff --git a/src/api/calibration/conflictOfInterestSearch/index.ts b/src/api/calibration/conflictOfInterestSearch/index.ts new file mode 100644 index 0000000..3594beb --- /dev/null +++ b/src/api/calibration/conflictOfInterestSearch/index.ts @@ -0,0 +1,110 @@ +import request from '@/utils/request' +import { isFile } from '@/utils/auxiliaryFunction' + +/* + * 立项登记 + * */ + +// 新增立项登记 +export const BusinessProject = (data: any) => { + const formData = new FormData() + formData.append('type', data.type.split(',')[0]) + formData.append('ContractNo', data.ContractNo) + formData.append('times', data.times) + formData.append('responsiblefor', data.responsiblefor) + formData.append('charge', data.charge) + formData.append('contract', data.contract) + formData.append('client_info', data.client_info) + formData.append('party_info', data.party_info) + formData.append('description', data.description) + if (data.approvers) formData.append('approvers', JSON.stringify(data.approvers)) + return request({ + url: `/business/project`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 编辑立案登记 +export const BusinessEditproject = (data: any) => { + const formData = new FormData() + formData.append('id', data.id) + formData.append('times', data.times) + formData.append('responsiblefor', data.responsiblefor) + formData.append('charge', data.charge) + if (isFile(data.contract)) formData.append('contract', data.contract) + return request({ + url: `/business/editproject`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 立案登记类型查询 +export const BusinessProjectquerytype = (data: any) => { + const formData = new FormData() + formData.append('year', data.year) + formData.append('type', data.type) + return request({ + url: `/business/projectquerytype`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 立项登记展示 +export const BusinessProjectdetail = (data: any) => { + const formData = new FormData() + formData.append('page', data.pageNum) + formData.append('per_page', data.pageSize) + if (data.times && data.times.length) { + formData.append('times', data.times[0]) + formData.append('end_time', data.times[1]) + } + formData.append('client_username', data.client_username) + return request({ + url: `/business/projectdetail`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 删除立项接口 +export const BusinessDeleteProject = (id: string) => { + const formData = new FormData() + formData.append('id', id) + return request({ + url: `/business/deleteProject`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 获取已通过审核的立项登记列表 +export const BusinessProjectDropdownList = (exclude_has_case: string = 'true') => { + const formData = new FormData() + formData.append('exclude_has_case', exclude_has_case) + return request({ + url: `/business/project-dropdown-list`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} diff --git a/src/api/calibration/department/index.ts b/src/api/calibration/department/index.ts new file mode 100644 index 0000000..ed5d053 --- /dev/null +++ b/src/api/calibration/department/index.ts @@ -0,0 +1,63 @@ +import request from '@/utils/request' + +/* + * 部门管理 + * */ + +// 公司部门列表 +export const UserDepartment = (name: string) => { + const formData = new FormData() + formData.append('name', name) + return request({ + url: `/user/department`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 添加部门 +export const UserAddDepartment = (name: string) => { + const formData = new FormData() + formData.append('name', name) + return request({ + url: `/user/add_department`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 删除部门 +export const UserDeleteDepartment = (id: string) => { + const formData = new FormData() + formData.append('id', id) + return request({ + url: `/user/delete_department`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 部门分页查询 +export const UserPersonlist = (data: any) => { + const formData = new FormData() + formData.append('per_page', data.pageSize) + formData.append('page', data.pageNum) + formData.append('name', data.name) + return request({ + url: `/user/personlist`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} diff --git a/src/api/calibration/departureFinancialRegistration/index.ts b/src/api/calibration/departureFinancialRegistration/index.ts new file mode 100644 index 0000000..e87cc01 --- /dev/null +++ b/src/api/calibration/departureFinancialRegistration/index.ts @@ -0,0 +1,72 @@ +import request from '@/utils/request' + +/* + * 离职财务登记 + * */ + +// 新增离职财务登记 +export const FinanceUserDeparture = (data: any) => { + const formData = new FormData() + formData.append('username', data.username) + formData.append('Dateofdeparture', data.Dateofdeparture) + formData.append('approvers', data.approvers) + return request({ + url: `/finance/user-departure`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 编辑离职登记信息 +export const FinanceEditUserDeparture = (data: any) => { + const formData = new FormData() + formData.append('id', data.id) + formData.append('Dateofdeparture', data.Dateofdeparture) + formData.append('approvers', data.approvers) + return request({ + url: `/finance/editUserDeparture`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 查询离职登记列表 +export const FinanceUserDepartureDetail = (data: any) => { + const formData = new FormData() + formData.append('page', data.pageNum) + formData.append('per_page', data.pageSize) + if (data.times && data.times.length) { + formData.append('times', data.times[0]) + formData.append('end_time', data.times[1]) + } + formData.append('username', data.username) + if (data.state) formData.append('state', data.state) + return request({ + url: `/finance/user-departure-detail`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 删除离职登记并恢复用户状态 +export const FinanceDeleteUserDeparture = (id: string) => { + const formData = new FormData() + formData.append('id', id) + return request({ + url: `/finance/deleteUserDeparture`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} diff --git a/src/api/calibration/inventoryRegistration/index.ts b/src/api/calibration/inventoryRegistration/index.ts new file mode 100644 index 0000000..7369881 --- /dev/null +++ b/src/api/calibration/inventoryRegistration/index.ts @@ -0,0 +1,77 @@ +import request from '@/utils/request' +import { isFile } from '@/utils/auxiliaryFunction' + +/* + * 入库登记 + * */ + +// 新增入库登记 +export const BusinessWarehousing = (data: any) => { + const formData = new FormData() + formData.append('unit', data.unit) + formData.append('mark', data.mark) + formData.append('lawyer', data.lawyer) + formData.append('deadline', data.deadline) + formData.append('contract', data.contract) + return request({ + url: `/business/warehousing`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 编辑入库登记 +export const BusinessEditWarehousing = (data: any) => { + const formData = new FormData() + formData.append('id', data.id) + formData.append('unit', data.unit) + formData.append('mark', data.mark) + formData.append('lawyer', data.lawyer) + formData.append('deadline', data.deadline) + if (isFile(data.contract)) formData.append('contract', data.contract) + return request({ + url: `/business/editWarehousing`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 入库登记 展示 +export const BusinessWarehousingdetail = (data: any) => { + const formData = new FormData() + formData.append('page', data.pageNum) + formData.append('per_page', data.pageSize) + if (data.times && data.times.length) { + formData.append('times', data.times[0]) + formData.append('end_time', data.times[1]) + } + formData.append('unit', data.unit) + return request({ + url: `/business/warehousingdetail`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 删除入库登记 +export const BusinessDeleteWarehousing = (id: string) => { + const formData = new FormData() + formData.append('id', id) + return request({ + url: `/business/deleteWarehousing`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} diff --git a/src/api/calibration/invoiceApplication/index.ts b/src/api/calibration/invoiceApplication/index.ts new file mode 100644 index 0000000..9f11c91 --- /dev/null +++ b/src/api/calibration/invoiceApplication/index.ts @@ -0,0 +1,81 @@ +import request from '@/utils/request' + +/* + * 开票申请 + * */ + +// 开发票申请 +export const FinanceIssueInvoice = (data: any) => { + const formData = new FormData() + formData.append('case_id', data.case_id) + formData.append('amount', data.amount) + formData.append('type', data.type) + formData.append('unit', data.unit) + formData.append('number', data.number) + formData.append('address_telephone', data.address_telephone) + formData.append('bank', data.bank) + return request({ + url: `/finance/issue-invoice`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 编辑开票申请 +export const FinanceEditInvoice = (data: any) => { + const formData = new FormData() + formData.append('id', data.id) + formData.append('case_id', data.case_id) + formData.append('amount', data.amount) + formData.append('type', data.type) + formData.append('unit', data.unit) + formData.append('number', data.number) + formData.append('address_telephone', data.address_telephone) + formData.append('bank', data.bank) + return request({ + url: `/finance/editInvoice`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 开票分页查询 +export const FinanceIssueDetail = (data: any) => { + const formData = new FormData() + formData.append('page', data.pageNum) + formData.append('per_page', data.pageSize) + if (data.times && data.times.length) { + formData.append('times', data.times[0]) + formData.append('end_time', data.times[1]) + } + formData.append('unit', data.unit) + formData.append('ContractNo', data.ContractNo) + return request({ + url: `/finance/issue-Detail`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 删除开票申请 +export const FinanceDeleteInvoice = (id: string) => { + const formData = new FormData() + formData.append('id', id) + return request({ + url: `/finance/deleteInvoice`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} diff --git a/src/api/calibration/lawFirmStandardDocuments/index.ts b/src/api/calibration/lawFirmStandardDocuments/index.ts new file mode 100644 index 0000000..943a5de --- /dev/null +++ b/src/api/calibration/lawFirmStandardDocuments/index.ts @@ -0,0 +1,45 @@ +import request from '@/utils/request' + +/* + * 律所标准文件 + * */ + +// 新增律所标准文件 +export const BusinessLawyerflie = (data: any) => { + const formData = new FormData() + // formData.append('title', data.title) + formData.append('file', data.file) + return request({ + url: `/business/Lawyerflie`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 律所标准文件展示 +export const BusinessLawdisplay = (data: any = {}) => { + const formData = new FormData() + if (data.title) formData.append('title', data.title) + return request({ + url: `/business/lawdisplay`, + method: 'post', + data: formData + }) +} + +// 删除律所标准文件 +export const BusinessLwaDetail = (id: string) => { + const formData = new FormData() + formData.append('id', id) + return request({ + url: `/business/LwaDetail`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} diff --git a/src/api/calibration/lmportantScheduleManagement/index.ts b/src/api/calibration/lmportantScheduleManagement/index.ts new file mode 100644 index 0000000..ab6ce48 --- /dev/null +++ b/src/api/calibration/lmportantScheduleManagement/index.ts @@ -0,0 +1,85 @@ +import request from '@/utils/request' + +/* + * 重要日程提示 + * */ + +// 新增重要日程提示 +export const BusinessSchedule = (data: any) => { + const formData = new FormData() + formData.append('title', data.title) + formData.append('tiems', data.tiems) + formData.append('end_time', data.end_time) + formData.append('remark', data.remark) + return request({ + url: `/business/schedule`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 编辑日程 +export const BusinessEditSchedule = (data: any) => { + const formData = new FormData() + formData.append('id', data.id) + formData.append('title', data.title) + formData.append('tiems', data.tiems) + formData.append('end_time', data.end_time) + formData.append('remark', data.remark) + return request({ + url: `/business/editSchedule`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 日程展示 +export const BusinessScheduleDetail = (data: any) => { + const formData = new FormData() + formData.append('page', data.pageNum) + formData.append('per_page', data.pageSize) + if (data.title) formData.append('title', data.title) + return request({ + url: `/business/ScheduleDetail`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 删除日程 +export const BusinessDscheduledetail = (id: string) => { + const formData = new FormData() + formData.append('id', id) + return request({ + url: `/business/scheduledetail`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 完成代办 +export const BusinessHandleSchedule = (data: any) => { + const formData = new FormData() + formData.append('id', data.id) + formData.append('state', data.state) + return request({ + url: `/business/handleSchedule`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} diff --git a/src/api/calibration/login/index.ts b/src/api/calibration/login/index.ts new file mode 100644 index 0000000..99483ed --- /dev/null +++ b/src/api/calibration/login/index.ts @@ -0,0 +1,40 @@ +import request from '@/utils/request' + +/* + * 登录 + * */ + +export const userLogin = (data: any) => { + const formData = new FormData() + formData.append('username', data.username) + formData.append('password', data.password) + return request({ + url: `/api/auth/login`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 用户信息 +export const UserGetInfo = () => { + return request({ + url: `/api/auth/get_info`, + method: 'get' + }) +} + +export const BusinessPropagandaEit = (data: any) => { + const formData = new FormData() + formData.append('content', data.content) + return request({ + url: `/business/PropagandaEit`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} diff --git a/src/api/calibration/onboardingRegistration/index.ts b/src/api/calibration/onboardingRegistration/index.ts new file mode 100644 index 0000000..3567588 --- /dev/null +++ b/src/api/calibration/onboardingRegistration/index.ts @@ -0,0 +1,54 @@ +import request from '@/utils/request' + +/* + * 入职财务登记 + * */ + +// 入职财务 +export const FinanceUserRegister = (data: any) => { + const formData = new FormData() + formData.append('username', data.username) + formData.append('Dateofjoining', data.Dateofjoining) + formData.append('salary', data.salary) + if (data.approvers) formData.append('approvers', JSON.stringify(data.approvers)) + return request({ + url: `/finance/user-register`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 未入职财务登记查询接口 +export const FinanceUserRegisterDetail = (data: any) => { + const formData = new FormData() + formData.append('page', data.pageNum) + formData.append('per_page', data.pageSize) + return request({ + url: `/finance/user-register-detail`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 已入职登记的用户 +export const FinanceRegisteredUserList = (data: any) => { + const formData = new FormData() + formData.append('page', data.pageNum) + formData.append('per_page', data.pageSize) + formData.append('username', '') + formData.append('department', '') + return request({ + url: `/finance/registered-user-list`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} diff --git a/src/api/calibration/paymentApplicationForm/index.ts b/src/api/calibration/paymentApplicationForm/index.ts new file mode 100644 index 0000000..5d6e3b4 --- /dev/null +++ b/src/api/calibration/paymentApplicationForm/index.ts @@ -0,0 +1,84 @@ +import request from '@/utils/request' + +/* + * 付款申请单 + * */ + +// 新增付款请单 +export const FinancePaymentRequest = (data: any) => { + const formData = new FormData() + formData.append('payment_reason', data.payment_reason) + formData.append('amount', data.amount) + formData.append('times', data.times) + formData.append('payee_name', data.payee_name) + formData.append('payee_account', data.payee_account) + formData.append('payee_bank', data.payee_bank) + formData.append('payment_description', data.payment_description) + formData.append('approvers', data.approvers) + formData.append('applicant', data.applicant) + return request({ + url: `/finance/payment-request`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 编辑付款申请 +export const FinanceEditPayment = (data: any) => { + const formData = new FormData() + formData.append('id', data.id) + formData.append('payment_reason', data.payment_reason) + formData.append('amount', data.amount) + formData.append('times', data.times) + formData.append('payee_name', data.payee_name) + formData.append('payee_account', data.payee_account) + formData.append('payee_bank', data.payee_bank) + formData.append('payment_description', data.payment_description) + formData.append('approvers', data.approvers) + formData.append('applicant', data.applicant) + return request({ + url: `/finance/editPayment`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 付款申请展示 +export const FinancePaymentDisplay = (data: any) => { + const formData = new FormData() + formData.append('page', data.pageNum) + formData.append('per_page', data.pageSize) + if (data.times && data.times.length) { + formData.append('times', data.times[0]) + formData.append('end_time', data.times[1]) + } + formData.append('payee', data.payee) + return request({ + url: `/finance/PaymentDisplay`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 删除付款申请 +export const FinanceDeletePayment = (id: string) => { + const formData = new FormData() + formData.append('id', id) + return request({ + url: `/finance/deletePayment`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} diff --git a/src/api/calibration/permissionManagement/index.ts b/src/api/calibration/permissionManagement/index.ts new file mode 100644 index 0000000..0b4e22c --- /dev/null +++ b/src/api/calibration/permissionManagement/index.ts @@ -0,0 +1,72 @@ +import request from '@/utils/request' + +/* + * 权限管理 + * */ + +// 新增权限 +export const BusinessAddRermission = (data: any) => { + const formData = new FormData() + formData.append('permission_name', data.permission_name) + formData.append('permission_logo', data.permission_logo) + formData.append('parent', data.parent) + return request({ + url: `/business/addRermission`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 编辑权限 +export const BusinessEditRermission = (data: any) => { + const formData = new FormData() + formData.append('id', data.id) + formData.append('permission_name', data.permission_name) + formData.append('permission_logo', data.permission_logo) + formData.append('parent', data.parent) + return request({ + url: `/business/editRermission`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 权限展示 +export const BusinessDisplayRermission = () => { + // const formData = new FormData() + // formData.append('page', data.pageNum) + // formData.append('per_page', data.pageSize) + // if (data.times && data.times.length) { + // formData.append('times', data.times[0]) + // formData.append('end_time', data.times[1]) + // } + // formData.append('unit', data.unit) + return request({ + url: `/business/displayRermission`, + method: 'post' + // data: formData, + // headers: { + // 'Content-Type': 'multipart/form-data' + // } + }) +} + +// 删除权限 +export const BusinessDeleteRermission = (id: string) => { + const formData = new FormData() + formData.append('id', id) + return request({ + url: `/business/deleteRermission`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} diff --git a/src/api/calibration/personnelManagement/index.ts b/src/api/calibration/personnelManagement/index.ts new file mode 100644 index 0000000..806ef77 --- /dev/null +++ b/src/api/calibration/personnelManagement/index.ts @@ -0,0 +1,125 @@ +import { isFile } from '@/utils/auxiliaryFunction' +import request from '@/utils/request' + +/* + * 人事管理 + * */ + +// 人员分页查询 +export const UserPersonnelList = (data: any) => { + const formData = new FormData() + formData.append('page', data.pageNum) + formData.append('per_page', data.pageSize) + formData.append('username', data.username) + if (data.position) formData.append('position', data.position) + if (data.team) formData.append('team', data.team) + if (data.academic) formData.append('academic', data.academic) + if (data.endtime) formData.append('endtime', data.endtime) + formData.append('department', data.department === '全部' ? '' : data.department) + return request({ + url: `/user/personnel-list`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 人事管理-人员添加 +export const UserCreateUser = (data: any) => { + const formData = new FormData() + formData.append('username', data.username) + formData.append('account', data.account) + formData.append('password', data.password) + formData.append('nation', data.nation) + formData.append('IdCard', data.IdCard) + formData.append('department', JSON.stringify(data.department)) + formData.append('role', JSON.stringify(data.role)) + formData.append('mobilePhone', data.mobilePhone) + formData.append('position', data.position) + formData.append('team', data.team) + formData.append('Dateofjoining', data.Dateofjoining) + formData.append('Confirmationtime', data.Confirmationtime) + formData.append('Practicingcertificatetime', data.Practicingcertificatetime) + formData.append('AcademicResume', data.AcademicResume) + + formData.append('academic', JSON.stringify(data.academic)) + formData.append('contract', data.contract) + formData.append('ApplicationForm', data.ApplicationForm) + return request({ + url: `/user/create-user`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 人事管理-人员编辑 +export const UserEditorialStaff = (data: any) => { + const formData = new FormData() + formData.append('id', data.id) + formData.append('username', data.username) + formData.append('account', data.account) + if (data.password) formData.append('password', data.password) + formData.append('nation', data.nation) + formData.append('IdCard', data.IdCard) + formData.append('department', data.department) + formData.append('role', JSON.stringify(data.role)) + formData.append('mobilePhone', data.mobilePhone) + formData.append('position', data.position) + formData.append('team', data.team) + formData.append('Dateofjoining', data.Dateofjoining) + formData.append('Confirmationtime', data.Confirmationtime) + formData.append('Practicingcertificatetime', data.Practicingcertificatetime) + formData.append('academic', JSON.stringify(data.academic)) + if (isFile(data.AcademicResume)) formData.append('AcademicResume', data.AcademicResume) + if (isFile(data.contract)) formData.append('contract', data.contract) + if (isFile(data.ApplicationForm)) formData.append('ApplicationForm', data.ApplicationForm) + return request({ + url: `/user/editorial-staff`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 人员列表 +export const UserPersonneldisplay = () => { + return request({ + url: `/user/personneldisplay`, + method: 'get' + }) +} + +// 修改密码 +export const UserChangePassword = (data: any) => { + const formData = new FormData() + formData.append('old_password', data.old_password) + formData.append('new_password', data.new_password) + return request({ + url: `/user/change-password`, + method: 'post', + data: formData + }) +} + +// 未进行入职登记人员查询接口 +export const FinanceUnregisteredUserList = (username: string) => { + const formData = new FormData() + formData.append('page', '1') + formData.append('per_page', '9999') + formData.append('username', username) + return request({ + url: `/finance/unregistered-user-list`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} diff --git a/src/api/calibration/preRegistration/index.ts b/src/api/calibration/preRegistration/index.ts new file mode 100644 index 0000000..ae5d532 --- /dev/null +++ b/src/api/calibration/preRegistration/index.ts @@ -0,0 +1,96 @@ +import request from '@/utils/request' + +/* + * 预立案登记 + * */ + +// 新增预立案登记 +export const BusinessRegister = (data: any) => { + const formData = new FormData() + formData.append('times', data.times) + formData.append('client_username', data.client_username) + formData.append('client_card', data.client_card) + formData.append('party_username', data.party_username) + formData.append('party_card', data.party_card) + formData.append('description', data.description) + formData.append('Undertaker', data.Undertaker) + return request({ + url: `/business/register`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 编辑预立案 +export const BusinessEditRegistration = (data: any) => { + const formData = new FormData() + formData.append('id', data.id) + formData.append('times', data.times) + formData.append('client_username', data.client_username) + formData.append('client_card', data.client_card) + formData.append('party_username', data.party_username) + formData.append('party_card', data.party_card) + formData.append('description', data.description) + formData.append('Undertaker', data.Undertaker) + return request({ + url: `/business/editRegistration`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 预立案登记展示 +export const BusinessRegisterdetail = (data: any) => { + const formData = new FormData() + formData.append('page', data.pageNum) + formData.append('per_page', data.pageSize) + if (data.times && data.times.length) { + formData.append('times', data.times[0]) + formData.append('end_time', data.times[1]) + } + if (data.Undertaker) formData.append('Undertaker', data.Undertaker) + return request({ + url: `/business/registerdetail`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 预立案登记列表接口 +export const BusinessRegistrationlist = () => { + return request({ + url: `/business/registrationlist`, + method: 'post' + }) +} + +// 预立案关联立案列表接口 +export const BusinessPreFilingLinkedCases = () => { + return request({ + url: `/business/preFilingLinkedCases`, + method: 'post' + }) +} + +// 删除预立案登记接口 +export const BusinessDeleteRegistration = (id: string) => { + const formData = new FormData() + formData.append('id', id) + return request({ + url: `/business/deleteRegistration`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} diff --git a/src/api/calibration/registrationPlatform/index.ts b/src/api/calibration/registrationPlatform/index.ts new file mode 100644 index 0000000..339b82e --- /dev/null +++ b/src/api/calibration/registrationPlatform/index.ts @@ -0,0 +1,74 @@ +import request from '@/utils/request' + +/* + * 注册平台登记 + * */ + +// 新增注册平台登记 +export const BusinessPlatformRegistration = (data: any) => { + const formData = new FormData() + formData.append('platform', data.platform) + formData.append('number', data.number) + formData.append('password', data.password) + formData.append('username', data.username) + return request({ + url: `/business/PlatformRegistration`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 注册平台登记编辑 +export const BusinessEditPlatform = (data: any) => { + const formData = new FormData() + formData.append('id', data.id) + formData.append('platform', data.platform) + formData.append('number', data.number) + formData.append('password', data.password) + formData.append('username', data.username) + return request({ + url: `/business/editPlatform`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 注册平台登记展示 +export const BusinessPlatformDetail = (data: any) => { + const formData = new FormData() + formData.append('page', data.pageNum) + formData.append('per_page', data.pageSize) + // if (data.times && data.times.length) { + // formData.append('times', data.times[0]) + // formData.append('end_time', data.times[1]) + // } + // formData.append('unit', data.unit) + return request({ + url: `/business/PlatformDetail`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 注册平台登记删除 +export const BusinessDeletePlatform = (id: string) => { + const formData = new FormData() + formData.append('id', id) + return request({ + url: `/business/deletePlatform`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} diff --git a/src/api/calibration/reimbursement/index.ts b/src/api/calibration/reimbursement/index.ts new file mode 100644 index 0000000..937f5e2 --- /dev/null +++ b/src/api/calibration/reimbursement/index.ts @@ -0,0 +1,78 @@ +import request from '@/utils/request' + +/* + * 报销 + * */ + +// 新增报销 +export const FinanceReimbursement = (data: any) => { + const formData = new FormData() + formData.append('person', data.person) + formData.append('times', data.times) + formData.append('reason', data.reason) + formData.append('amount', data.amount) + formData.append('FeeDescription', data.FeeDescription) + formData.append('approvers', data.approvers) + return request({ + url: `/finance/reimbursement`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 编辑报销申请 +export const FinanceEditReimbursement = (data: any) => { + const formData = new FormData() + formData.append('id', data.id) + formData.append('person', data.person) + formData.append('times', data.times) + formData.append('reason', data.reason) + formData.append('amount', data.amount) + formData.append('FeeDescription', data.FeeDescription) + formData.append('approvers', data.approvers) + return request({ + url: `/finance/editReimbursement`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 报销展示 +export const FinanceReidetail = (data: any) => { + const formData = new FormData() + formData.append('page', data.pageNum) + formData.append('per_page', data.pageSize) + if (data.times && data.times.length) { + formData.append('times', data.times[0]) + formData.append('end_time', data.times[1]) + } + formData.append('person', data.person) + return request({ + url: `/finance/reidetail`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 删除报销申请 +export const FinanceDeleteReimbursement = (id: string) => { + const formData = new FormData() + formData.append('id', id) + return request({ + url: `/finance/deleteReimbursement`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} diff --git a/src/api/calibration/revenueRecognition/index.ts b/src/api/calibration/revenueRecognition/index.ts new file mode 100644 index 0000000..af15927 --- /dev/null +++ b/src/api/calibration/revenueRecognition/index.ts @@ -0,0 +1,76 @@ +import request from '@/utils/request' + +/* + * 收入确认 + * */ + +// 新增收入确认 +export const FinanceConfirm = (data: any) => { + const formData = new FormData() + formData.append('times', data.times) + formData.append('case_id', data.case_id) + formData.append('CustomerID', data.CustomerID) + formData.append('amount', data.amount) + formData.append('approvers', data.approvers) + return request({ + url: `/finance/confirm`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 编辑收入确认 +export const FinanceEditIncome = (data: any) => { + const formData = new FormData() + formData.append('id', data.id) + formData.append('times', data.times) + formData.append('case_id', data.case_id) + formData.append('CustomerID', data.CustomerID) + formData.append('amount', data.amount) + formData.append('approvers', data.approvers) + return request({ + url: `/finance/editIncome`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 收入确认展示 +export const FinanceConfirmdisplay = (data: any) => { + const formData = new FormData() + formData.append('page', data.pageNum) + formData.append('per_page', data.pageSize) + if (data.times && data.times.length) { + formData.append('times', data.times[0]) + formData.append('end_time', data.times[1]) + } + formData.append('CustomerID', data.CustomerID) + return request({ + url: `/finance/confirmdisplay`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 删除收入确认 +export const FinanceDeleteIncome = (id: string) => { + const formData = new FormData() + formData.append('id', id) + return request({ + url: `/finance/deleteIncome`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} diff --git a/src/api/calibration/roleManagement/index.ts b/src/api/calibration/roleManagement/index.ts new file mode 100644 index 0000000..b4d5481 --- /dev/null +++ b/src/api/calibration/roleManagement/index.ts @@ -0,0 +1,94 @@ +// roleManagement +import request from '@/utils/request' + +/* + * 角色管理 + * */ + +// 添加角色 +export const BusinessAddRole = (data: any) => { + const formData = new FormData() + formData.append('RoleName', data.RoleName) + formData.append('remark', data.remark) + return request({ + url: `/business/addRole`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 编辑角色 +export const BusinessEditRole = (data: any) => { + const formData = new FormData() + formData.append('id', data.id) + formData.append('RoleName', data.RoleName) + formData.append('remark', data.remark) + return request({ + url: `/business/EditRole`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 角色展示 +export const BusinessDisplayRole = (data: any) => { + const formData = new FormData() + formData.append('RoleName', data.RoleName) + return request({ + url: `/business/displayRole`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 删除角色 +export const BusinessDeleteRole = (id: string) => { + const formData = new FormData() + formData.append('id', id) + return request({ + url: `/business/DeleteRole`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 赋予权限 +export const BusinessModifypermissions = (data: any) => { + const formData = new FormData() + formData.append('id', data.id) + formData.append('permissionId', JSON.stringify(data.permissionId)) + return request({ + url: `/business/modifypermissions`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 根据角色查询权限 +export const BusinessGetRolePermissions = (roleId: string) => { + const formData = new FormData() + formData.append('roleId', roleId) + return request({ + url: `/business/getRolePermissions`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} diff --git a/src/api/calibration/salaryBonusAdjustment/index.ts b/src/api/calibration/salaryBonusAdjustment/index.ts new file mode 100644 index 0000000..c3d4638 --- /dev/null +++ b/src/api/calibration/salaryBonusAdjustment/index.ts @@ -0,0 +1,74 @@ +import request from '@/utils/request' + +/* + * 工资/奖金变更 + * */ + +// 新增工资/奖金变更 +export const FinanceChange = (data: any) => { + const formData = new FormData() + formData.append('username', data.username) + formData.append('type', data.type) + formData.append('Instructions', data.Instructions) + formData.append('approvers', data.approvers) + return request({ + url: `/finance/change`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 编辑工资/奖金变更 +export const FinanceEditBonusChange = (data: any) => { + const formData = new FormData() + formData.append('id', data.id) + formData.append('username', data.username) + formData.append('type', data.type) + formData.append('Instructions', data.Instructions) + formData.append('approvers', data.approvers) + return request({ + url: `/finance/editBonusChange`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 工资/奖金变更展示 +export const FinanceChangeDetail = (data: any) => { + const formData = new FormData() + formData.append('page', data.pageNum) + formData.append('per_page', data.pageSize) + if (data.times && data.times.length) { + formData.append('times', data.times[0]) + formData.append('end_time', data.times[1]) + } + formData.append('username', data.username) + return request({ + url: `/finance/ChangeDetail`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 删除工资/奖金变更 +export const FinanceDeleteBonusChange = (id: string) => { + const formData = new FormData() + formData.append('id', id) + return request({ + url: `/finance/deleteBonusChange`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} diff --git a/src/api/calibration/systemManagement/index.ts b/src/api/calibration/systemManagement/index.ts new file mode 100644 index 0000000..8cac8f5 --- /dev/null +++ b/src/api/calibration/systemManagement/index.ts @@ -0,0 +1,75 @@ +import request from '@/utils/request' +import { isFile } from '@/utils/auxiliaryFunction' + +/* + * 制度管理 + * */ + +// 新增制度 +export const BusinessAddSystem = (data: any) => { + const formData = new FormData() + formData.append('title', data.title) + formData.append('content', data.content) + if (isFile(data.file)) formData.append('file', data.file) + formData.append('state', data.state) + return request({ + url: `/business/addSystem`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 编制制度 +export const BusinessEitSystem = (data: any) => { + const formData = new FormData() + formData.append('id', data.id) + formData.append('title', data.title) + formData.append('content', data.content) + if (isFile(data.file)) formData.append('file', data.file) + formData.append('state', data.state) + return request({ + url: `/business/eitSystem`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 制度分页 +export const BusinessSystemList = (data: any) => { + const formData = new FormData() + formData.append('page', data.pageNum) + formData.append('per_page', data.pageSize) + // if (data.times && data.times.length) { + // formData.append('times', data.times[0]) + // formData.append('end_time', data.times[1]) + // } + // formData.append('unit', data.unit) + return request({ + url: `/business/SystemList`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 删除制度 +export const BusinessDeleteSystem = (id: string) => { + const formData = new FormData() + formData.append('id', id) + return request({ + url: `/business/deleteSystem`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} diff --git a/src/api/calibration/teamManagement/index.ts b/src/api/calibration/teamManagement/index.ts new file mode 100644 index 0000000..952d283 --- /dev/null +++ b/src/api/calibration/teamManagement/index.ts @@ -0,0 +1,82 @@ +import request from '@/utils/request' + +/* + * 团队管理 + * */ + +// 添加团队 +export const UserAdd_team = (data: any) => { + const formData = new FormData() + formData.append('name', data.name) + formData.append('team_type', data.team_type) + formData.append('description', data.description) + return request({ + url: `/user/add_team`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 编辑团队 +export const UserEdit_team = (data: any) => { + const formData = new FormData() + formData.append('id', data.id) + formData.append('name', data.name) + formData.append('team_type', data.team_type) + formData.append('description', data.description) + return request({ + url: `/user/edit_team`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 获取团队列表(下拉选项使用) +export const UserTeam = (data: any) => { + const formData = new FormData() + formData.append('name', data.name) + return request({ + url: `/user/team`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 获取团队列表(分页) +export const UserTeamList = (data: any) => { + const formData = new FormData() + formData.append('page', data.pageNum) + formData.append('per_page', data.pageSize) + formData.append('name', data.name) + return request({ + url: `/user/team-list`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 删除团队 +export const UserDelete_team = (id: string) => { + const formData = new FormData() + formData.append('id', id) + return request({ + url: `/user/delete_team`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} diff --git a/src/api/system/config-api.ts b/src/api/system/config-api.ts new file mode 100644 index 0000000..f7da4c8 --- /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..537f7b7 --- /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..5c3d4dd --- /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..9c061ec --- /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..7eaeb86 --- /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..ee90d0d --- /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..e76e63b --- /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..2068362 --- /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 | number) { + 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/font-icon/Excel.svg b/src/assets/font-icon/Excel.svg new file mode 100644 index 0000000..5fe7a4a --- /dev/null +++ b/src/assets/font-icon/Excel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/font-icon/PDF.svg b/src/assets/font-icon/PDF.svg new file mode 100644 index 0000000..48f0c5c --- /dev/null +++ b/src/assets/font-icon/PDF.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/font-icon/Ppt.svg b/src/assets/font-icon/Ppt.svg new file mode 100644 index 0000000..c564d77 --- /dev/null +++ b/src/assets/font-icon/Ppt.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/font-icon/Txt.svg b/src/assets/font-icon/Txt.svg new file mode 100644 index 0000000..667df07 --- /dev/null +++ b/src/assets/font-icon/Txt.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/font-icon/Word.svg b/src/assets/font-icon/Word.svg new file mode 100644 index 0000000..f46a9fc --- /dev/null +++ b/src/assets/font-icon/Word.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/font-icon/shiping.svg b/src/assets/font-icon/shiping.svg new file mode 100644 index 0000000..2750554 --- /dev/null +++ b/src/assets/font-icon/shiping.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/font-icon/tupian.svg b/src/assets/font-icon/tupian.svg new file mode 100644 index 0000000..05b9a23 --- /dev/null +++ b/src/assets/font-icon/tupian.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/font-icon/weizhi.svg b/src/assets/font-icon/weizhi.svg new file mode 100644 index 0000000..4f772fc --- /dev/null +++ b/src/assets/font-icon/weizhi.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/font-icon/yasuo.svg b/src/assets/font-icon/yasuo.svg new file mode 100644 index 0000000..8c2d7c6 --- /dev/null +++ b/src/assets/font-icon/yasuo.svg @@ -0,0 +1 @@ + \ No newline at end of file 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/AppLink/index.vue b/src/components/AppLink/index.vue new file mode 100644 index 0000000..3169cf1 --- /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..6a314c7 --- /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..c9e470d --- /dev/null +++ b/src/components/CURD/PageContent.vue @@ -0,0 +1,933 @@ + + + + + diff --git a/src/components/CURD/PageModal.vue b/src/components/CURD/PageModal.vue new file mode 100644 index 0000000..152189c --- /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..32510ec --- /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..b4a2b16 --- /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..2108b6c --- /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..025f837 --- /dev/null +++ b/src/components/CommonWrapper/index.vue @@ -0,0 +1,21 @@ + + + diff --git a/src/components/DarkModeSwitch/index.vue b/src/components/DarkModeSwitch/index.vue new file mode 100644 index 0000000..01bd8e4 --- /dev/null +++ b/src/components/DarkModeSwitch/index.vue @@ -0,0 +1,39 @@ + + diff --git a/src/components/Fullscreen/index.vue b/src/components/Fullscreen/index.vue new file mode 100644 index 0000000..23a4071 --- /dev/null +++ b/src/components/Fullscreen/index.vue @@ -0,0 +1,11 @@ + + + + + diff --git a/src/components/Hamburger/index.vue b/src/components/Hamburger/index.vue new file mode 100644 index 0000000..4d94bba --- /dev/null +++ b/src/components/Hamburger/index.vue @@ -0,0 +1,66 @@ + + + + + diff --git a/src/components/LangSelect/index.vue b/src/components/LangSelect/index.vue new file mode 100644 index 0000000..4a03c52 --- /dev/null +++ b/src/components/LangSelect/index.vue @@ -0,0 +1,49 @@ + + + diff --git a/src/components/Notification/ApprovalForm.vue b/src/components/Notification/ApprovalForm.vue new file mode 100644 index 0000000..f0c710b --- /dev/null +++ b/src/components/Notification/ApprovalForm.vue @@ -0,0 +1,126 @@ + + + + + diff --git a/src/components/Notification/ConfirmationOfProxyServiceForm.vue b/src/components/Notification/ConfirmationOfProxyServiceForm.vue new file mode 100644 index 0000000..b5da81b --- /dev/null +++ b/src/components/Notification/ConfirmationOfProxyServiceForm.vue @@ -0,0 +1,48 @@ + + + + + diff --git a/src/components/Notification/ViewTheConflictList.vue b/src/components/Notification/ViewTheConflictList.vue new file mode 100644 index 0000000..5cb355a --- /dev/null +++ b/src/components/Notification/ViewTheConflictList.vue @@ -0,0 +1,189 @@ + + + + + diff --git a/src/components/Notification/index.vue b/src/components/Notification/index.vue new file mode 100644 index 0000000..7da4724 --- /dev/null +++ b/src/components/Notification/index.vue @@ -0,0 +1,207 @@ + + + + + diff --git a/src/components/Pagination/index.vue b/src/components/Pagination/index.vue new file mode 100644 index 0000000..e854b60 --- /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..dd2a9fb --- /dev/null +++ b/src/components/SizeSelect/index.vue @@ -0,0 +1,40 @@ + + + diff --git a/src/composables/auth/useTokenRefresh.ts b/src/composables/auth/useTokenRefresh.ts new file mode 100644 index 0000000..2d37880 --- /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..185e184 --- /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..b34b5cf --- /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..f730938 --- /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..bb15424 --- /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..ca9b1f3 --- /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..52e9719 --- /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..2748972 --- /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..50127ff --- /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..1a6a7fe --- /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..090bb04 --- /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..db867f5 --- /dev/null +++ b/src/directives/index.ts @@ -0,0 +1,8 @@ +import type { App } from 'vue' +import { permission } from './permission' + +// 注册指令 +export function setupDirective(app: App) { + // 权限指令 + app.directive('permission', permission) +} diff --git a/src/directives/permission/index.ts b/src/directives/permission/index.ts new file mode 100644 index 0000000..3ce35f9 --- /dev/null +++ b/src/directives/permission/index.ts @@ -0,0 +1,58 @@ +import type { Directive, DirectiveBinding } from 'vue' +import { useUserStore } from '@/store' + +export interface PermissionBinding { + value: string | string[] +} +function filterByDoubleColon(strArray: string[]): string[] { + return strArray.filter((str) => { + const matches = str.match(/:/g) + const hasTwoColons = matches && matches.length === 2 + if (!hasTwoColons) return false + const firstColonIndex = str.indexOf(':') + const secondColonIndex = str.indexOf(':', firstColonIndex + 1) + const afterSecondColon = str.substring(secondColonIndex + 1) + const startsWithView = afterSecondColon.startsWith('view') + return hasTwoColons && !startsWithView + }) +} +export const permission: Directive = { + mounted(el: HTMLElement, binding: DirectiveBinding) { + const { value } = binding + const userStore = useUserStore() + + // 指令逻辑 + if (!checkPermission(value, filterByDoubleColon(userStore.userInfo?.permission_data || []))) { + el.style.display = 'none' // 隐藏元素而不是删除 + } + }, + updated(el: HTMLElement, binding: DirectiveBinding) { + const { value } = binding + const userStore = useUserStore() + // 更新时重新检查权限 + if (!checkPermission(value, filterByDoubleColon(userStore.userInfo?.permission_data || []))) { + el.style.display = 'none' // 隐藏元素 + } else { + el.style.display = '' // 恢复默认显示状态 + } + } +} + +// 权限检查逻辑 +function checkPermission(permission: string | string[], userPermissions: string[]): boolean { + if (!permission || !userPermissions) return false + + // 检查是否为超级管理员权限 + if (userPermissions.includes('*:*:*')) { + return true + } + + // 将传入的权限转换为数组 + const permissions = Array.isArray(permission) ? permission : [permission] + + // 过滤用户权限数据 + const filteredPermissions = filterByDoubleColon(userPermissions) + + // 检查是否有任意一个权限匹配 + return permissions.some((perm) => filteredPermissions.includes(perm)) +} diff --git a/src/enums/api/code-enum.ts b/src/enums/api/code-enum.ts new file mode 100644 index 0000000..f5328fb --- /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..b9eaef2 --- /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..2f321fe --- /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..520e2bd --- /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..152aa77 --- /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..9b3ab4d --- /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..dc443ea --- /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..4115560 --- /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..e32efb5 --- /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..b1ce460 --- /dev/null +++ b/src/env.d.ts @@ -0,0 +1,8 @@ +/// + +declare module '*.vue' { + import { DefineComponent } from 'vue' + // eslint-disable-next-line @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..759cb43 --- /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..89f2cc8 --- /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..afcde20 --- /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..8d4fecd --- /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..9b9cbbc --- /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..03a67f4 --- /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..560c07a --- /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..2476c26 --- /dev/null +++ b/src/layouts/components/NavBar/components/NavbarActions.vue @@ -0,0 +1,272 @@ + + + + + diff --git a/src/layouts/components/NavBar/index.vue b/src/layouts/components/NavBar/index.vue new file mode 100644 index 0000000..a775e92 --- /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..259b9fc --- /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..7aba209 --- /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..0252dd1 --- /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..c5d1087 --- /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..e20b092 --- /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..d2c5c27 --- /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..8bccb02 --- /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..efe498d --- /dev/null +++ b/src/main.ts @@ -0,0 +1,24 @@ +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' +// import { setupDirective } from '@/directives' + +const app = createApp(App) +// 注册插件 +app.use(setupPlugins) +// setupDirective(app) +app.mount('#app') diff --git a/src/plugins/icons.ts b/src/plugins/icons.ts new file mode 100644 index 0000000..86c7fa5 --- /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..511f470 --- /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..ee37fd8 --- /dev/null +++ b/src/plugins/permission.ts @@ -0,0 +1,195 @@ +import NProgress from '@/utils/nprogress' +import router from '@/router' +import { useUserStore } from '@/store' +import { RouteRecordRaw } from 'vue-router' + +function filterByDoubleColon(strArray: string[]): string[] { + return strArray.filter((str) => (str.match(/:/g) || []).length === 2) +} + +/** + * 从权限字符串中提取模块名称并去重 + * @param permissionStrings 权限字符串数组 + * @returns 去重后的模块名称数组 + */ +function extractAndDeduplicateModules(permissionStrings: string[]): string[] { + const modules = permissionStrings.map((permission: string) => { + // 分割字符串并取第一部分 + return permission.split(':')[0] + }) + + // 使用 Set 去重 + return [...new Set(modules)] +} + +/** + * 从路由配置中筛选出 meta 对象中存在 permissionIdentifier 字段的路由 + * @param routes 路由配置数组 + * @returns 包含 permissionIdentifier 的路由对象数组 + */ +function getRoutesWithPermissionIdentifier(routes: RouteRecordRaw[]): RouteRecordRaw[] { + const result: RouteRecordRaw[] = [] + + const traverseRoutes = (routeList: RouteRecordRaw[]) => { + for (const route of routeList) { + // 检查当前路由的 meta 中是否有 permissionIdentifier + if (route.meta && (route.meta as any).permissionIdentifier) { + result.push(route) + } + + // 如果有子路由,递归遍历 + if (route.children && route.children.length > 0) { + traverseRoutes(route.children) + } + } + } + + traverseRoutes(routes) + return result +} + +/** + * 根据权限字符串数组隐藏匹配的路由 + * @param routes 路由配置数组对象 + * @param permissionStrings 权限字符串数组 + */ +function hideRoutesByPermissions(routes: RouteRecordRaw[], permissionStrings: string[]): void { + const permissionSet = new Set(permissionStrings) + + const traverseRoutes = (routeList: RouteRecordRaw[]) => { + for (const route of routeList) { + // 检查当前路由的 meta 中是否有 permissionIdentifier + if (route.meta && (route.meta as any).permissionIdentifier) { + const routePermission = (route.meta as any).permissionIdentifier + + // 如果权限字符串数组中包含当前路由的权限标识,则隐藏该路由 + if (permissionSet.has(routePermission)) { + ;(route.meta as any).hidden = true + ;(route.meta as any)._wasHiddenByPermission = true // 标记是由权限引起的隐藏 + } else { + // 如果不再需要隐藏,移除隐藏标记 + if ((route.meta as any)._wasHiddenByPermission) { + delete (route.meta as any).hidden + delete (route.meta as any)._wasHiddenByPermission + } + } + } + + // 如果有子路由,递归遍历 + if (route.children && route.children.length > 0) { + traverseRoutes(route.children) + } + } + } + + traverseRoutes(routes) +} + +/** + * 重置有 permissionIdentifier 的路由隐藏状态 + */ +function resetPermissionBasedRoutes(routes: RouteRecordRaw[]): void { + const traverseRoutes = (routeList: RouteRecordRaw[]) => { + for (const route of routeList) { + if (route.meta && (route.meta as any).permissionIdentifier) { + // 只重置有 permissionIdentifier 的路由状态 + if ((route.meta as any).hidden && (route.meta as any)._wasHiddenByPermission) { + // 恢复原始隐藏状态或设为可见 + delete (route.meta as any).hidden + delete (route.meta as any)._wasHiddenByPermission + } + } + + if (route.children && route.children.length > 0) { + traverseRoutes(route.children) + } + } + } + + traverseRoutes(routes) +} + +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 + } + const userStore = useUserStore() + // 如果用户信息不存在或已过期,获取用户信息 + + if (!userStore.userInfo?.id) { + // const res: any = await userStore.getUserInfo() + await userStore.getUserInfo() + + // // 获取路由配置 + // const routes = router.getRoutes() + // const routesWithPermission = getRoutesWithPermissionIdentifier(routes) + // userStore.setPermissionCharactersArr([ + // ...new Set(routesWithPermission.map((route: any) => route.meta.permissionIdentifier)) + // ]) + // const allPermissions = userStore.permissionCharactersArr + // + // resetPermissionBasedRoutes(routes) + // // 检查是否为超级管理员权限 + // if (res.data.permission_data.includes('*:*:*')) { + // // 如果是超级管理员,不对路由进行隐藏处理,所有路由都可访问 + // } else { + // // 非超级管理员,按原有逻辑处理权限 + // const userPermissions = [ + // ...filterByDoubleColon(res.data.permission_data), + // ...extractAndDeduplicateModules(res.data.permission_data) + // ] + // + // const missingPermissions = allPermissions.filter( + // (permission) => !userPermissions.includes(permission) + // ) + // hideRoutesByPermissions(routesWithPermission, missingPermissions) + // } + } + + 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..0c33e8c --- /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..fe5e9c7 --- /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..8d20c8c --- /dev/null +++ b/src/router/index.ts @@ -0,0 +1,437 @@ +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: '/boos/boosAccountManagement', + 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: '/personnel', + // component: Layout, + // name: 'Personnel', + // meta: { + // title: '人员管理', + // icon: 'setting', + // alwaysShow: true, + // permissionIdentifier: 'Personnel' + // }, + // children: [ + // { + // path: 'user', + // name: 'PersonnelUser', + // component: () => import('@/views/calibration/personnelManagement/index.vue'), + // meta: { + // title: '人事管理', + // permissionIdentifier: 'Personnel:PersonnelUser:view' + // } + // }, + // { + // path: 'department', + // name: 'Department', + // component: () => import('@/views/calibration/department/index.vue'), + // meta: { + // title: '部门管理', + // permissionIdentifier: 'Personnel:Department:view' + // } + // }, + // { + // path: 'roleManagement', + // name: 'RoleManagement', + // component: () => import('@/views/calibration/roleManagement/index.vue'), + // meta: { + // title: '角色管理', + // permissionIdentifier: 'Personnel:RoleManagement:view' + // } + // }, + // { + // path: 'teamManagement', + // name: 'TeamManagement', + // component: () => import('@/views/calibration/teamManagement/index.vue'), + // meta: { + // title: '团队管理', + // permissionIdentifier: 'Personnel:TeamManagement:view' + // } + // }, + // { + // path: 'permissionManagement', + // name: 'PermissionManagement', + // component: () => import('@/views/calibration/permissionManagement/index.vue'), + // meta: { + // title: '权限管理', + // permissionIdentifier: 'Personnel:PermissionManagement:view' + // } + // } + // ] + // }, + // // 财务管理模块 + // { + // path: '/finance', + // component: Layout, + // name: 'Finance', + // meta: { + // title: '财务管理', + // icon: 'setting', + // alwaysShow: true, + // permissionIdentifier: 'Finance' + // }, + // children: [ + // { + // path: 'onboardingRegistration', + // name: 'OnboardingRegistration', + // component: () => import('@/views/calibration/onboardingRegistration/index.vue'), + // meta: { + // title: '入职财务登记', + // permissionIdentifier: 'Finance:OnboardingRegistration:view' + // } + // }, + // { + // path: 'departure', + // name: 'Departure', + // component: () => import('@/views/calibration/departureFinancialRegistration/index.vue'), + // meta: { + // title: '离职财务登记', + // permissionIdentifier: 'Finance:Departure:view' + // } + // }, + // { + // path: 'invoiceApplication', + // name: 'InvoiceApplication', + // component: () => import('@/views/calibration/invoiceApplication/index.vue'), + // meta: { + // title: '开票申请', + // permissionIdentifier: 'Finance:InvoiceApplication:view' + // } + // }, + // { + // path: 'revenueRecognition', + // name: 'RevenueRecognition', + // component: () => import('@/views/calibration/revenueRecognition/index.vue'), + // meta: { + // title: '收入确认', + // permissionIdentifier: 'Finance:RevenueRecognition:view' + // } + // }, + // { + // path: 'accountAdjustmentApplication', + // name: 'AccountAdjustmentApplication', + // component: () => import('@/views/calibration/accountAdjustmentApplication/index.vue'), + // meta: { + // title: '调账申请', + // permissionIdentifier: 'Finance:AccountAdjustmentApplication:view' + // } + // }, + // { + // path: 'paymentApplicationForm', + // name: 'PaymentApplicationForm', + // component: () => import('@/views/calibration/paymentApplicationForm/index.vue'), + // meta: { + // title: '付款申请单', + // permissionIdentifier: 'Finance:PaymentApplicationForm:view' + // } + // }, + // { + // path: 'reimbursement', + // name: 'Reimbursement', + // component: () => import('@/views/calibration/reimbursement/index.vue'), + // meta: { + // title: '报销', + // permissionIdentifier: 'Finance:Reimbursement:view' + // } + // }, + // { + // path: 'salaryBonusAdjustment', + // name: 'SalaryBonusAdjustment', + // component: () => import('@/views/calibration/salaryBonusAdjustment/index.vue'), + // meta: { + // title: '工资/奖金变更', + // permissionIdentifier: 'Finance:SalaryBonusAdjustment:view' + // } + // } + // ] + // }, + // // 业务管理模块 + // { + // path: '/business', + // component: Layout, + // name: 'Business', + // meta: { + // title: '业务管理', + // icon: 'setting', + // alwaysShow: true, + // permissionIdentifier: 'Business' + // }, + // children: [ + // { + // path: 'conflictOfInterestSearch', + // name: 'ConflictOfInterestSearch', + // component: () => + // import('@/views/calibration/businessSystem/conflictOfInterestSearch/index.vue'), + // meta: { + // title: '利益冲突检索', + // permissionIdentifier: 'Business:ConflictOfInterestSearch:view' + // } + // }, + // { + // path: 'preRegistration', + // name: 'PreRegistration', + // component: () => import('@/views/calibration/businessSystem/preRegistration/index.vue'), + // meta: { + // title: '预立案登记', + // permissionIdentifier: 'Business:PreRegistration:view' + // } + // }, + // { + // path: 'bidRegistration', + // name: 'BidRegistration', + // component: () => import('@/views/calibration/businessSystem/bidRegistration/index.vue'), + // meta: { + // title: '投标登记', + // permissionIdentifier: 'Business:BidRegistration:view' + // } + // }, + // { + // path: 'projectRegistration', + // name: 'ProjectRegistration', + // component: () => import('@/views/calibration/businessSystem/projectRegistration/index.vue'), + // meta: { + // title: '立项登记', + // permissionIdentifier: 'Business:ProjectRegistration:view' + // } + // } + // ] + // }, + // // 案件管理模块 + // { + // path: '/case', + // component: Layout, + // name: 'Case', + // meta: { + // title: '案件管理', + // icon: 'setting', + // permissionIdentifier: 'Case' + // }, + // children: [ + // { + // path: 'caseManagement', + // name: 'CaseManagement', + // component: () => import('@/views/calibration/caseManagement/index.vue'), + // meta: { + // title: '案件管理', + // permissionIdentifier: 'Case:CaseManagement:view' + // } + // }, + // { + // path: 'caseLabel', + // name: 'CaseLabel', + // component: () => import('@/views/calibration/caseLabel/index.vue'), + // meta: { + // title: '案件标签', + // permissionIdentifier: 'Case:CaseLabel:view' + // } + // } + // ] + // }, + // 申请用印 + { + path: '/boos', + name: 'Boos', + component: Layout, + meta: { + title: 'BOSS账号管理', + icon: 'setting' + }, + children: [ + { + path: 'boosAccountManagement', + name: 'BoosAccountManagement', + component: () => import('@/views/BoosAccountManagement/index.vue'), + meta: { + title: 'BOSS账号管理' + } + } + ] + }, + + // 入库登记 + { + path: '/task', + name: 'Task', + component: Layout, + meta: { + title: '任务管理', + icon: 'setting' + }, + children: [ + { + path: 'taskManagement', + name: 'TaskManagement', + component: () => import('@/views/TaskManagement/index.vue'), + meta: { + title: '任务管理' + } + } + ] + }, + // 注册平台登记 + { + path: '/filingRegistration', + name: 'FilingRegistration', + component: Layout, + meta: { + title: '注册平台登记', + icon: 'setting', + permissionIdentifier: 'FilingRegistration' + }, + children: [ + { + path: 'registrationPlatform', + name: 'RegistrationPlatform', + component: () => import('@/views/calibration/registrationPlatform/index.vue'), + meta: { + title: '注册平台登记', + permissionIdentifier: 'FilingRegistration:RegistrationPlatform:view' + } + } + ] + }, + // 公告 + { + path: '/notice', + name: 'Notice', + component: Layout, + meta: { + title: '公告管理', + icon: 'setting', + permissionIdentifier: 'Notice' + }, + children: [ + { + path: 'announcementManagement', + name: 'AnnouncementManagement', + component: () => import('@/views/calibration/announcementManagement/index.vue'), + meta: { + title: '公告管理', + permissionIdentifier: 'Notice:AnnouncementManagement:view' + } + } + ] + }, + // 律所标准文件 + { + path: '/lawyer-file', + name: 'LawyerFile', + component: Layout, + meta: { + title: '律所标准文件', + icon: 'setting', + permissionIdentifier: 'LawyerFile' + }, + children: [ + { + path: 'lawFirmStandardDocuments', + name: 'LawFirmStandardDocuments', + component: () => import('@/views/calibration/lawFirmStandardDocuments/index.vue'), + meta: { + title: '律所标准文件', + permissionIdentifier: 'LawyerFile:LawFirmStandardDocuments:view' + } + } + ] + }, + { + path: '/lmportantSchedule', + name: 'LmportantSchedule', + component: Layout, + meta: { + title: '日程管理', + icon: 'setting', + permissionIdentifier: 'LmportantSchedule' + }, + children: [ + { + path: 'lmportantScheduleManagement', + name: 'LmportantScheduleManagement', + component: () => import('@/views/calibration/lmportantScheduleManagement/index.vue'), + meta: { + title: '日程管理', + permissionIdentifier: 'LmportantSchedule:LmportantScheduleManagement:view' + } + } + ] + }, + { + path: '/system', + name: 'System', + component: Layout, + meta: { + title: '制度管理', + icon: 'setting', + permissionIdentifier: 'System' + }, + children: [ + { + path: 'systemManagement', + name: 'SystemManagement', + component: () => import('@/views/calibration/systemManagement/index.vue'), + meta: { + title: '制度管理', + permissionIdentifier: 'System:SystemManagement:view' + } + } + ] + } +] + +/** + * 创建路由 + */ +const router = createRouter({ + history: createWebHashHistory(), + routes: constantRoutes, + // 刷新时,滚动条位置还原 + scrollBehavior: () => ({ left: 0, top: 0 }) +}) + +// 全局注册 router +export function setupRouter(app: App) { + app.use(router) +} + +export default router diff --git a/src/settings.ts b/src/settings.ts new file mode 100644 index 0000000..8c931b8 --- /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..dce5a36 --- /dev/null +++ b/src/store/index.ts @@ -0,0 +1,17 @@ +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 } diff --git a/src/store/modules/app-store.ts b/src/store/modules/app-store.ts new file mode 100644 index 0000000..be9980e --- /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..3f2286b --- /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..4120e46 --- /dev/null +++ b/src/store/modules/permission-store.ts @@ -0,0 +1,103 @@ +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..daecb10 --- /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..2520382 --- /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..4719332 --- /dev/null +++ b/src/store/modules/user-store.ts @@ -0,0 +1,165 @@ +import { store } from '@/store' + +import AuthAPI from '@/api/auth-api' +import 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' +import { UserGetInfo, userLogin } from '@/api/calibration/login' + +export const useUserStore = defineStore('user', () => { + // 用户信息 + const userInfo = ref({}) + // 记住我状态 + const rememberMe = ref(AuthStorage.getRememberMe()) + const permissionCharactersArr = ref([]) + + const setPermissionCharactersArr = (arr: string[]) => { + permissionCharactersArr.value = arr + } + + // 登录 + function login(LoginFormData: any) { + return new Promise((resolve, reject) => { + // 保存记住我状态和token + rememberMe.value = true + + userLogin(LoginFormData) + .then((res: any) => { + AuthStorage.setTokens(res.data.token, res.data.token, rememberMe.value) + resolve() + }) + .catch((error) => { + reject(error) + }) + }) + } + + /** + * 获取用户信息 + * + * @returns {UserInfo} 用户信息 + */ + function getUserInfo() { + return new Promise((resolve, reject) => { + UserGetInfo() + .then((res: any) => { + if (!res) { + reject('Verification failed, please Login again.') + return + } + setUserInfo(res.data) + resolve(res) + }) + .catch((error) => { + reject(error) + }) + }) + } + + const setUserInfo = (info: any) => { + userInfo.value = info + } + /** + * 登出 + */ + function logout() { + return new Promise((resolve) => { + // 重置所有系统状态 + resetAllState() + resolve() + }) + } + + /** + * 重置所有系统状态 + * 统一处理所有清理工作,包括用户凭证、路由、缓存等 + */ + function resetAllState() { + // 1. 重置用户状态 + resetUserState() + + // 2. 重置其他模块状态 + // 重置路由 + usePermissionStoreHook().resetRouter() + // 清除字典缓存 + useDictStoreHook().clearDictCache() + // 清除标签视图 + useTagsViewStore().delAllViews() + + // 3. 清理 WebSocket 连接 + + return Promise.resolve() + } + + /** + * 重置用户状态 + * 仅处理用户模块内的状态 + */ + function resetUserState() { + // 清除用户凭证 + AuthStorage.clearAuth() + // 重置用户信息 + userInfo.value = {} as UserInfo + setPermissionCharactersArr([]) + } + + /** + * 刷新 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) + }) + }) + } + + const checkPermission = (permission: string): boolean => { + if (userInfo.value?.permission_data.includes('*:*:*')) { + return true + } + return userInfo.value?.permission_data.includes(permission) + } + + return { + userInfo, + rememberMe, + isLoggedIn: () => !!AuthStorage.getAccessToken(), + getUserInfo, + setUserInfo, + login, + logout, + resetAllState, + resetUserState, + refreshToken, + permissionCharactersArr, + setPermissionCharactersArr, + checkPermission + } +}) + +/** + * 在组件外部使用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..868a94d --- /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..5a0d0f6 --- /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..8f992ad --- /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..2b97bd9 --- /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..34c3e65 --- /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/TimerManager/index.ts b/src/utils/TimerManager/index.ts new file mode 100644 index 0000000..88f250b --- /dev/null +++ b/src/utils/TimerManager/index.ts @@ -0,0 +1,30 @@ +export class TimerManager { + private timerId: number | null = null + + start(callback: () => void) { + // 清理之前的计时器 + this.stop() + + // 立即执行一次 + callback() + + // 启动新的计时器 + this.timerId = setInterval(callback, 5000) as unknown as number + } + + stop() { + if (this.timerId !== null) { + clearInterval(this.timerId) + this.timerId = null + } + } +} + +export function createTimer() { + const timerManager = new TimerManager() + + return { + startTimer: (callback: () => void) => timerManager.start(callback), + stopTimer: () => timerManager.stop() + } +} diff --git a/src/utils/auth.ts b/src/utils/auth.ts new file mode 100644 index 0000000..e258c70 --- /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/auxiliaryFunction/index.ts b/src/utils/auxiliaryFunction/index.ts new file mode 100644 index 0000000..0f2f11b --- /dev/null +++ b/src/utils/auxiliaryFunction/index.ts @@ -0,0 +1,257 @@ +/** + * 节流函数 - 限制函数执行频率 + * @param func 需要节流的函数 + * @param delay 节流延迟时间(毫秒) + * @returns 节流后的函数 + */ +export function throttle any>( + func: T, + delay: number = 300 +): (...args: Parameters) => void { + let lastExecTime = 0 + + return function (this: ThisParameterType, ...args: Parameters) { + const now = Date.now() + + // 只有超过延迟时间才执行 + if (now - lastExecTime >= delay) { + func.apply(this, args) + lastExecTime = now + } + } +} + +/** + * 防抖函数 + * @param func 需要防抖的函数 + * @param delay 延迟时间(毫秒) + * @returns 防抖后的函数 + */ +export function debounce any>( + func: T, + delay: number = 300 +): (...args: Parameters) => void { + let timeoutId: NodeJS.Timeout | null = null + + return function (this: ThisParameterType, ...args: Parameters) { + if (timeoutId) { + clearTimeout(timeoutId) + } + + timeoutId = setTimeout(() => { + func.apply(this, args) + timeoutId = null + }, delay) + } +} + +/** + * 将文件路径数组转换为对象数组 + * @param filePaths 文件路径数组或单个文件路径字符串 + * @returns 包含文件信息的对象数组 + */ +export function convertFilePathsToObject(filePaths: string | string[]): Array<{ + url: string + name: string + domain: string + uuid: string + extension: string // 新增后缀名字段 +}> { + // 如果是字符串,先转换为数组 + const paths = Array.isArray(filePaths) ? filePaths : [filePaths] + + return paths.map((path) => { + // 确保path是字符串类型 + if (typeof path !== 'string') { + return { + url: '', + domain: '', + name: '', + uuid: '', + extension: '' // 新增后缀名字段 + } + } + + // 自动补全协议头 + let fullPath = path + if (!path.startsWith('http://') && !path.startsWith('https://')) { + fullPath = `http://${path}` + } + + // 分割域名和文件部分 + const urlObj = new URL(fullPath) + const domain = urlObj.origin + const filePart = urlObj.pathname.substring(1) // 移除开头的 / + + // 优化文件名解析逻辑 + // 对于路径 "cd78824383f7新建 文本文档.txt" + // 我们不再强制要求下划线分隔格式,而是提取完整的文件名 + let name = '' + let uuid = '' + let extension = '' // 新增后缀名变量 + + // 尝试从文件名中提取 UUID(假设 UUID 是文件名开头的一段字母数字组合) + const uuidMatch = filePart.match(/^([a-f0-9]+)/i) + if (uuidMatch && uuidMatch[1]) { + uuid = uuidMatch[1] + // 文件名是去掉 UUID 后的部分 + name = filePart.substring(uuid.length) + // 如果文件名以斜杠开头则去除 + if (name.startsWith('/')) { + name = name.substring(1) + } + } else { + // 如果没有匹配到 UUID 格式,整个作为文件名处理 + name = filePart + } + + // 如果 name 为空,则使用完整文件部分作为文件名 + if (!name) { + name = filePart + } + + // 提取文件扩展名 + const lastDotIndex = name.lastIndexOf('.') + if (lastDotIndex > 0) { + // 确保点号不在开头 + extension = name.substring(lastDotIndex + 1).toLowerCase() + name = name.substring(0, lastDotIndex) // 移除扩展名部分,只保留文件名 + } + + // 解码 URL 编码的文件名 + try { + name = decodeURIComponent(name) + } catch (e) { + // 如果解码失败,保持原始文件名 + console.warn('文件名解码失败:', name) + } + + return { + url: fullPath, + domain, + name, + uuid, + extension // 新增后缀名字段 + } + }) +} + +// 安全的文件信息提取函数 +// 更新返回类型定义 +export function getFileInfo(fileStr: string): { + url: string + name: string + extension: string // 新增扩展名字段 +} | null { + try { + if (!fileStr) return null + + // 如果是有效的JSON字符串,按原有逻辑处理 + if (isValidJson(fileStr)) { + const parsed = JSON.parse(fileStr) + const files = convertFilePathsToObject(parsed) + return files && files.length > 0 + ? { + url: files[0].url, + name: files[0].name, + extension: files[0].extension // 新增扩展名 + } + : null + } + // 如果是URL字符串,直接使用 convertFilePathsToObject 处理 + else if (typeof fileStr === 'string') { + const files = convertFilePathsToObject(fileStr) + return files && files.length > 0 + ? { + url: files[0].url, + name: files[0].name, + extension: files[0].extension // 新增扩展名 + } + : null + } + + return null + } catch (e) { + console.error('解析文件信息失败:', e) + return null + } +} + +export function deepCloneByJSON(obj: any) { + try { + return JSON.parse(JSON.stringify(obj)) + } catch (error) { + console.error('深拷贝失败:', error) + return obj + } +} + +export function isString(value: any): boolean { + return typeof value === 'string' +} +export function isFile(obj: any): obj is File { + return obj instanceof File +} +// JSON验证 +export function isValidJson(str: string): boolean { + try { + JSON.parse(str) + return true + } catch (e) { + return false + } +} + +export function isValidFormat(data: any): boolean { + let parsedData = data + + // 如果输入是字符串,尝试解析为 JSON + if (typeof data === 'string') { + try { + parsedData = JSON.parse(data) + } catch (e) { + return false // JSON 解析失败,直接返回 false + } + } + + // 检查是否为数组 + if (!Array.isArray(parsedData)) { + return false + } + + // 检查数组中的每个元素 + return parsedData.every((item) => { + // 检查是否为对象且不为 null + if (typeof item !== 'object' || item === null) { + return false + } + + // 检查必需的属性是否存在,且类型是否正确 + const hasIndex = typeof item.index === 'number' + const hasName = typeof item.name === 'string' + const hasIdNumber = typeof item.idNumber === 'string' + + return hasIndex && hasName && hasIdNumber + }) +} + +/** + * 比较当前时间与指定时间(精确到秒) + * @param timeParam - 要比较的时间参数,支持日期字符串、时间戳或Date对象 + * @returns 当前时间小于timeParam时返回true,否则返回false + */ +export function isCurrentTimeLessThan(timeParam: string | number | Date): boolean { + // 将传入的时间参数转换为Date对象 + const paramTime = new Date(timeParam) + + // 获取当前时间 + const currentTime = new Date() + + // 检查时间参数是否有效 + if (isNaN(paramTime.getTime())) { + throw new Error('Invalid time parameter provided') + } + + // 比较时间(精确到秒):当前时间 < 参数时间 返回true,否则返回false + return Math.floor(currentTime.getTime() / 1000) < Math.floor(paramTime.getTime() / 1000) +} diff --git a/src/utils/functionDialogBox/index.ts b/src/utils/functionDialogBox/index.ts new file mode 100644 index 0000000..ab4f3ac --- /dev/null +++ b/src/utils/functionDialogBox/index.ts @@ -0,0 +1,102 @@ +import { ElButton, ElDialog } from 'element-plus' +import { Component, DefineComponent } from 'vue' +import { h, createApp } from 'vue' +import { throttle } from '@/utils/auxiliaryFunction' +import ElementPlus from 'element-plus' +import zhCn from 'element-plus/es/locale/lang/zh-cn' + +// 定义模态框属性接口 +interface ModalProps { + title?: string + width?: string | number + ok?: (result: any) => void + cancel?: () => void + footerVisible?: boolean + [key: string]: any // 允许其他 Element Plus Dialog 属性 +} + +interface DialogInstance { + unmount: () => void +} + +// 函数式弹窗组件 +export const functionDialogBox = ( + component: Component | DefineComponent | null, + props: Record, + modalProps: ModalProps +): DialogInstance => { + const open = ref(true) + const formRef = ref() + const isLoading = ref(false) + const dialog = () => + h( + ElDialog, + { + closeOnClickModal: false, + ...modalProps, + modelValue: open.value, + onOpen() {}, + onClosed() { + app.unmount() + document.body.removeChild(div) + } + }, + { + default: () => h(component || h('div'), { ref: formRef, ...props }), + footer: () => { + if (modalProps.footerVisible !== false) { + return [ + h( + ElButton, + { + type: 'primary', + loading: isLoading.value, + onClick: confirm + }, + () => '确定' + ), + h( + ElButton, + { + onClick: cancel + }, + () => '取消' + ) + ] + } + return null + } + } + ) + + const app = createApp(dialog) + + const div = document.createElement('div') + document.body.appendChild(div) + app.use(ElementPlus, { + locale: zhCn + }) + app.mount(div) + + function unmount() { + open.value = false + } + const confirm = throttle(async () => { + isLoading.value = true + try { + await formRef.value?.submit?.() + const formData = formRef.value?.getForm?.() + modalProps?.ok?.(formData) + unmount() + } finally { + isLoading.value = false + } + }) + const cancel = throttle(() => { + modalProps?.cancel?.() + unmount() + }) + return { + unmount + } +} diff --git a/src/utils/i18n.ts b/src/utils/i18n.ts new file mode 100644 index 0000000..23dea1e --- /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..21f9da8 --- /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..c82e1f9 --- /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..d648b22 --- /dev/null +++ b/src/utils/request.ts @@ -0,0 +1,99 @@ +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_API_URL, + timeout: 20000, + 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 = `${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, msg } = response.data + // 请求成功 + if (code === ApiCodeEnum.SUCCESS || code == '0' || code == '200') { + return response.data as any + } + + // 业务错误 + 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, message } = response.data as any + + 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(message || 'Access Token Invalid')) + } + + case ApiCodeEnum.REFRESH_TOKEN_INVALID: + // Refresh Token 过期,跳转登录页 + await redirectToLogin('登录已过期,请重新登录') + return Promise.reject(new Error(message || 'Refresh Token Invalid')) + + default: + ElMessage.error(message || '请求失败') + return Promise.reject(new Error(message || 'Request Error')) + } + } +) + +export default httpRequest diff --git a/src/utils/storage.ts b/src/utils/storage.ts new file mode 100644 index 0000000..5ba784c --- /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..5cfd1ac --- /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/BoosAccountManagement/components/BoosAccountDetails.vue b/src/views/BoosAccountManagement/components/BoosAccountDetails.vue new file mode 100644 index 0000000..11c9665 --- /dev/null +++ b/src/views/BoosAccountManagement/components/BoosAccountDetails.vue @@ -0,0 +1,7 @@ + + + + + diff --git a/src/views/BoosAccountManagement/components/BoosAccountForm.vue b/src/views/BoosAccountManagement/components/BoosAccountForm.vue new file mode 100644 index 0000000..ebec391 --- /dev/null +++ b/src/views/BoosAccountManagement/components/BoosAccountForm.vue @@ -0,0 +1,108 @@ + + + + + diff --git a/src/views/BoosAccountManagement/index.vue b/src/views/BoosAccountManagement/index.vue new file mode 100644 index 0000000..ba56f2f --- /dev/null +++ b/src/views/BoosAccountManagement/index.vue @@ -0,0 +1,257 @@ + + + diff --git a/src/views/TaskManagement/components/TaskDetails.vue b/src/views/TaskManagement/components/TaskDetails.vue new file mode 100644 index 0000000..13d026b --- /dev/null +++ b/src/views/TaskManagement/components/TaskDetails.vue @@ -0,0 +1,7 @@ + + + + + diff --git a/src/views/TaskManagement/components/TaskForm.vue b/src/views/TaskManagement/components/TaskForm.vue new file mode 100644 index 0000000..876a20b --- /dev/null +++ b/src/views/TaskManagement/components/TaskForm.vue @@ -0,0 +1,114 @@ + + + + + diff --git a/src/views/TaskManagement/index.vue b/src/views/TaskManagement/index.vue new file mode 100644 index 0000000..820aea0 --- /dev/null +++ b/src/views/TaskManagement/index.vue @@ -0,0 +1,254 @@ + + + diff --git a/src/views/calibration/accountAdjustmentApplication/components/DeptTree.vue b/src/views/calibration/accountAdjustmentApplication/components/DeptTree.vue new file mode 100644 index 0000000..a0a12af --- /dev/null +++ b/src/views/calibration/accountAdjustmentApplication/components/DeptTree.vue @@ -0,0 +1,83 @@ + + + + diff --git a/src/views/calibration/accountAdjustmentApplication/components/UserImport.vue b/src/views/calibration/accountAdjustmentApplication/components/UserImport.vue new file mode 100644 index 0000000..3b3b138 --- /dev/null +++ b/src/views/calibration/accountAdjustmentApplication/components/UserImport.vue @@ -0,0 +1,198 @@ + + + diff --git a/src/views/calibration/accountAdjustmentApplication/index.vue b/src/views/calibration/accountAdjustmentApplication/index.vue new file mode 100644 index 0000000..5b9354d --- /dev/null +++ b/src/views/calibration/accountAdjustmentApplication/index.vue @@ -0,0 +1,323 @@ + + + diff --git a/src/views/calibration/announcementManagement/components/AnnouncementForm.vue b/src/views/calibration/announcementManagement/components/AnnouncementForm.vue new file mode 100644 index 0000000..b1a79c9 --- /dev/null +++ b/src/views/calibration/announcementManagement/components/AnnouncementForm.vue @@ -0,0 +1,163 @@ + + + + + diff --git a/src/views/calibration/announcementManagement/index.vue b/src/views/calibration/announcementManagement/index.vue new file mode 100644 index 0000000..b7fe927 --- /dev/null +++ b/src/views/calibration/announcementManagement/index.vue @@ -0,0 +1,234 @@ + + + diff --git a/src/views/calibration/applicationForSealApproval/components/StampingForm.vue b/src/views/calibration/applicationForSealApproval/components/StampingForm.vue new file mode 100644 index 0000000..ebc0bea --- /dev/null +++ b/src/views/calibration/applicationForSealApproval/components/StampingForm.vue @@ -0,0 +1,213 @@ + + + + + diff --git a/src/views/calibration/applicationForSealApproval/index.vue b/src/views/calibration/applicationForSealApproval/index.vue new file mode 100644 index 0000000..6dce7c0 --- /dev/null +++ b/src/views/calibration/applicationForSealApproval/index.vue @@ -0,0 +1,294 @@ + + + diff --git a/src/views/calibration/businessSystem/bidRegistration/components/BidRegistrationForm.vue b/src/views/calibration/businessSystem/bidRegistration/components/BidRegistrationForm.vue new file mode 100644 index 0000000..f97af67 --- /dev/null +++ b/src/views/calibration/businessSystem/bidRegistration/components/BidRegistrationForm.vue @@ -0,0 +1,269 @@ + + + + + diff --git a/src/views/calibration/businessSystem/bidRegistration/components/BusinessProjectForm.vue b/src/views/calibration/businessSystem/bidRegistration/components/BusinessProjectForm.vue new file mode 100644 index 0000000..1ec4e3f --- /dev/null +++ b/src/views/calibration/businessSystem/bidRegistration/components/BusinessProjectForm.vue @@ -0,0 +1,257 @@ + + + + + diff --git a/src/views/calibration/businessSystem/bidRegistration/index.vue b/src/views/calibration/businessSystem/bidRegistration/index.vue new file mode 100644 index 0000000..5a1b2b2 --- /dev/null +++ b/src/views/calibration/businessSystem/bidRegistration/index.vue @@ -0,0 +1,313 @@ + + + + diff --git a/src/views/calibration/businessSystem/conflictOfInterestSearch/components/BidRegistrationForm.vue b/src/views/calibration/businessSystem/conflictOfInterestSearch/components/BidRegistrationForm.vue new file mode 100644 index 0000000..7deed6a --- /dev/null +++ b/src/views/calibration/businessSystem/conflictOfInterestSearch/components/BidRegistrationForm.vue @@ -0,0 +1,305 @@ + + + + + diff --git a/src/views/calibration/businessSystem/conflictOfInterestSearch/components/BusinessProjectForm.vue b/src/views/calibration/businessSystem/conflictOfInterestSearch/components/BusinessProjectForm.vue new file mode 100644 index 0000000..1ec4e3f --- /dev/null +++ b/src/views/calibration/businessSystem/conflictOfInterestSearch/components/BusinessProjectForm.vue @@ -0,0 +1,257 @@ + + + + + diff --git a/src/views/calibration/businessSystem/conflictOfInterestSearch/index.vue b/src/views/calibration/businessSystem/conflictOfInterestSearch/index.vue new file mode 100644 index 0000000..b57a314 --- /dev/null +++ b/src/views/calibration/businessSystem/conflictOfInterestSearch/index.vue @@ -0,0 +1,486 @@ + + + + + + diff --git a/src/views/calibration/businessSystem/preRegistration/components/PreRegistrationForm.vue b/src/views/calibration/businessSystem/preRegistration/components/PreRegistrationForm.vue new file mode 100644 index 0000000..edda6c6 --- /dev/null +++ b/src/views/calibration/businessSystem/preRegistration/components/PreRegistrationForm.vue @@ -0,0 +1,364 @@ + + + + + diff --git a/src/views/calibration/businessSystem/preRegistration/index.vue b/src/views/calibration/businessSystem/preRegistration/index.vue new file mode 100644 index 0000000..887c971 --- /dev/null +++ b/src/views/calibration/businessSystem/preRegistration/index.vue @@ -0,0 +1,307 @@ + + + diff --git a/src/views/calibration/businessSystem/projectRegistration/components/AdjustTheYearForm.vue b/src/views/calibration/businessSystem/projectRegistration/components/AdjustTheYearForm.vue new file mode 100644 index 0000000..99018ec --- /dev/null +++ b/src/views/calibration/businessSystem/projectRegistration/components/AdjustTheYearForm.vue @@ -0,0 +1,91 @@ + + + + + diff --git a/src/views/calibration/businessSystem/projectRegistration/components/BidRegistrationForm.vue b/src/views/calibration/businessSystem/projectRegistration/components/BidRegistrationForm.vue new file mode 100644 index 0000000..7deed6a --- /dev/null +++ b/src/views/calibration/businessSystem/projectRegistration/components/BidRegistrationForm.vue @@ -0,0 +1,305 @@ + + + + + diff --git a/src/views/calibration/businessSystem/projectRegistration/components/BusinessProjectForm.vue b/src/views/calibration/businessSystem/projectRegistration/components/BusinessProjectForm.vue new file mode 100644 index 0000000..69f4531 --- /dev/null +++ b/src/views/calibration/businessSystem/projectRegistration/components/BusinessProjectForm.vue @@ -0,0 +1,555 @@ + + + + + diff --git a/src/views/calibration/businessSystem/projectRegistration/index.vue b/src/views/calibration/businessSystem/projectRegistration/index.vue new file mode 100644 index 0000000..2b1c955 --- /dev/null +++ b/src/views/calibration/businessSystem/projectRegistration/index.vue @@ -0,0 +1,342 @@ + + + + diff --git a/src/views/calibration/caseLabel/components/CaseLabelForm.vue b/src/views/calibration/caseLabel/components/CaseLabelForm.vue new file mode 100644 index 0000000..3e60046 --- /dev/null +++ b/src/views/calibration/caseLabel/components/CaseLabelForm.vue @@ -0,0 +1,108 @@ + + + + + diff --git a/src/views/calibration/caseLabel/index.vue b/src/views/calibration/caseLabel/index.vue new file mode 100644 index 0000000..64b966c --- /dev/null +++ b/src/views/calibration/caseLabel/index.vue @@ -0,0 +1,261 @@ + + + + + diff --git a/src/views/calibration/caseManagement/components/CaseLogComponent/components/CaseLogForm.vue b/src/views/calibration/caseManagement/components/CaseLogComponent/components/CaseLogForm.vue new file mode 100644 index 0000000..03f1c45 --- /dev/null +++ b/src/views/calibration/caseManagement/components/CaseLogComponent/components/CaseLogForm.vue @@ -0,0 +1,103 @@ + + + + + diff --git a/src/views/calibration/caseManagement/components/CaseLogComponent/index.vue b/src/views/calibration/caseManagement/components/CaseLogComponent/index.vue new file mode 100644 index 0000000..e699f53 --- /dev/null +++ b/src/views/calibration/caseManagement/components/CaseLogComponent/index.vue @@ -0,0 +1,181 @@ + + + + + diff --git a/src/views/calibration/caseManagement/components/CaseManagementForm.vue b/src/views/calibration/caseManagement/components/CaseManagementForm.vue new file mode 100644 index 0000000..f1ae4de --- /dev/null +++ b/src/views/calibration/caseManagement/components/CaseManagementForm.vue @@ -0,0 +1,227 @@ + + + + + diff --git a/src/views/calibration/caseManagement/components/ChangeApplicationComponent/components/CaseInvoiceForm.vue b/src/views/calibration/caseManagement/components/ChangeApplicationComponent/components/CaseInvoiceForm.vue new file mode 100644 index 0000000..092c951 --- /dev/null +++ b/src/views/calibration/caseManagement/components/ChangeApplicationComponent/components/CaseInvoiceForm.vue @@ -0,0 +1,133 @@ + + + + + diff --git a/src/views/calibration/caseManagement/components/ChangeApplicationComponent/index.vue b/src/views/calibration/caseManagement/components/ChangeApplicationComponent/index.vue new file mode 100644 index 0000000..a4c2b96 --- /dev/null +++ b/src/views/calibration/caseManagement/components/ChangeApplicationComponent/index.vue @@ -0,0 +1,190 @@ + + + + + diff --git a/src/views/calibration/caseManagement/components/CumulativeReceivedPaymentForm.vue b/src/views/calibration/caseManagement/components/CumulativeReceivedPaymentForm.vue new file mode 100644 index 0000000..f5122cc --- /dev/null +++ b/src/views/calibration/caseManagement/components/CumulativeReceivedPaymentForm.vue @@ -0,0 +1,88 @@ + + + + + diff --git a/src/views/calibration/caseManagement/components/UploadOfDataForm.vue b/src/views/calibration/caseManagement/components/UploadOfDataForm.vue new file mode 100644 index 0000000..42b249e --- /dev/null +++ b/src/views/calibration/caseManagement/components/UploadOfDataForm.vue @@ -0,0 +1,140 @@ + + + + + diff --git a/src/views/calibration/caseManagement/index.vue b/src/views/calibration/caseManagement/index.vue new file mode 100644 index 0000000..18374b0 --- /dev/null +++ b/src/views/calibration/caseManagement/index.vue @@ -0,0 +1,604 @@ + + + + + diff --git a/src/views/calibration/department/index.vue b/src/views/calibration/department/index.vue new file mode 100644 index 0000000..a33023b --- /dev/null +++ b/src/views/calibration/department/index.vue @@ -0,0 +1,258 @@ + + + diff --git a/src/views/calibration/departureFinancialRegistration/components/SeparationRegistrationForm.vue b/src/views/calibration/departureFinancialRegistration/components/SeparationRegistrationForm.vue new file mode 100644 index 0000000..78172ee --- /dev/null +++ b/src/views/calibration/departureFinancialRegistration/components/SeparationRegistrationForm.vue @@ -0,0 +1,132 @@ + + + + + diff --git a/src/views/calibration/departureFinancialRegistration/components/StampingForm.vue b/src/views/calibration/departureFinancialRegistration/components/StampingForm.vue new file mode 100644 index 0000000..c4b6fd4 --- /dev/null +++ b/src/views/calibration/departureFinancialRegistration/components/StampingForm.vue @@ -0,0 +1,217 @@ + + + + + diff --git a/src/views/calibration/departureFinancialRegistration/index.vue b/src/views/calibration/departureFinancialRegistration/index.vue new file mode 100644 index 0000000..8898cff --- /dev/null +++ b/src/views/calibration/departureFinancialRegistration/index.vue @@ -0,0 +1,284 @@ + + + diff --git a/src/views/calibration/inventoryRegistration/components/StampingForm.vue b/src/views/calibration/inventoryRegistration/components/StampingForm.vue new file mode 100644 index 0000000..0c99431 --- /dev/null +++ b/src/views/calibration/inventoryRegistration/components/StampingForm.vue @@ -0,0 +1,176 @@ + + + + + diff --git a/src/views/calibration/inventoryRegistration/index.vue b/src/views/calibration/inventoryRegistration/index.vue new file mode 100644 index 0000000..6ae3efd --- /dev/null +++ b/src/views/calibration/inventoryRegistration/index.vue @@ -0,0 +1,260 @@ + + + diff --git a/src/views/calibration/invoiceApplication/components/DeptTree.vue b/src/views/calibration/invoiceApplication/components/DeptTree.vue new file mode 100644 index 0000000..a0a12af --- /dev/null +++ b/src/views/calibration/invoiceApplication/components/DeptTree.vue @@ -0,0 +1,83 @@ + + + + diff --git a/src/views/calibration/invoiceApplication/components/UserImport.vue b/src/views/calibration/invoiceApplication/components/UserImport.vue new file mode 100644 index 0000000..3b3b138 --- /dev/null +++ b/src/views/calibration/invoiceApplication/components/UserImport.vue @@ -0,0 +1,198 @@ + + + diff --git a/src/views/calibration/invoiceApplication/index.vue b/src/views/calibration/invoiceApplication/index.vue new file mode 100644 index 0000000..2151f2b --- /dev/null +++ b/src/views/calibration/invoiceApplication/index.vue @@ -0,0 +1,346 @@ + + + diff --git a/src/views/calibration/lawFirmStandardDocuments/components/LawFirmStandardsForm.vue b/src/views/calibration/lawFirmStandardDocuments/components/LawFirmStandardsForm.vue new file mode 100644 index 0000000..8a01b05 --- /dev/null +++ b/src/views/calibration/lawFirmStandardDocuments/components/LawFirmStandardsForm.vue @@ -0,0 +1,153 @@ + + + + + diff --git a/src/views/calibration/lawFirmStandardDocuments/index.vue b/src/views/calibration/lawFirmStandardDocuments/index.vue new file mode 100644 index 0000000..f805bda --- /dev/null +++ b/src/views/calibration/lawFirmStandardDocuments/index.vue @@ -0,0 +1,255 @@ + + + + + diff --git a/src/views/calibration/lmportantScheduleManagement/components/LmportantScheduleForm.vue b/src/views/calibration/lmportantScheduleManagement/components/LmportantScheduleForm.vue new file mode 100644 index 0000000..2683855 --- /dev/null +++ b/src/views/calibration/lmportantScheduleManagement/components/LmportantScheduleForm.vue @@ -0,0 +1,137 @@ + + + + + diff --git a/src/views/calibration/lmportantScheduleManagement/index.vue b/src/views/calibration/lmportantScheduleManagement/index.vue new file mode 100644 index 0000000..e116f3d --- /dev/null +++ b/src/views/calibration/lmportantScheduleManagement/index.vue @@ -0,0 +1,265 @@ + + + diff --git a/src/views/calibration/onboardingRegistration/components/DepartmentSelectionForm.vue b/src/views/calibration/onboardingRegistration/components/DepartmentSelectionForm.vue new file mode 100644 index 0000000..45565d9 --- /dev/null +++ b/src/views/calibration/onboardingRegistration/components/DepartmentSelectionForm.vue @@ -0,0 +1,111 @@ + + + + + diff --git a/src/views/calibration/onboardingRegistration/index.vue b/src/views/calibration/onboardingRegistration/index.vue new file mode 100644 index 0000000..2facedc --- /dev/null +++ b/src/views/calibration/onboardingRegistration/index.vue @@ -0,0 +1,294 @@ + + + diff --git a/src/views/calibration/paymentApplicationForm/components/DeptTree.vue b/src/views/calibration/paymentApplicationForm/components/DeptTree.vue new file mode 100644 index 0000000..a0a12af --- /dev/null +++ b/src/views/calibration/paymentApplicationForm/components/DeptTree.vue @@ -0,0 +1,83 @@ + + + + diff --git a/src/views/calibration/paymentApplicationForm/components/UserImport.vue b/src/views/calibration/paymentApplicationForm/components/UserImport.vue new file mode 100644 index 0000000..3b3b138 --- /dev/null +++ b/src/views/calibration/paymentApplicationForm/components/UserImport.vue @@ -0,0 +1,198 @@ + + + diff --git a/src/views/calibration/paymentApplicationForm/index.vue b/src/views/calibration/paymentApplicationForm/index.vue new file mode 100644 index 0000000..acbe6d0 --- /dev/null +++ b/src/views/calibration/paymentApplicationForm/index.vue @@ -0,0 +1,368 @@ + + + diff --git a/src/views/calibration/permissionManagement/components/permissionForm.vue b/src/views/calibration/permissionManagement/components/permissionForm.vue new file mode 100644 index 0000000..8fe4362 --- /dev/null +++ b/src/views/calibration/permissionManagement/components/permissionForm.vue @@ -0,0 +1,134 @@ + + + + + diff --git a/src/views/calibration/permissionManagement/index.vue b/src/views/calibration/permissionManagement/index.vue new file mode 100644 index 0000000..c70a425 --- /dev/null +++ b/src/views/calibration/permissionManagement/index.vue @@ -0,0 +1,235 @@ + + + diff --git a/src/views/calibration/personnelManagement/components/DeptTree.vue b/src/views/calibration/personnelManagement/components/DeptTree.vue new file mode 100644 index 0000000..072822b --- /dev/null +++ b/src/views/calibration/personnelManagement/components/DeptTree.vue @@ -0,0 +1,83 @@ + + + + diff --git a/src/views/calibration/personnelManagement/components/EditorPasswordForm.vue b/src/views/calibration/personnelManagement/components/EditorPasswordForm.vue new file mode 100644 index 0000000..90fd452 --- /dev/null +++ b/src/views/calibration/personnelManagement/components/EditorPasswordForm.vue @@ -0,0 +1,112 @@ + + + + + diff --git a/src/views/calibration/personnelManagement/components/UserImport.vue b/src/views/calibration/personnelManagement/components/UserImport.vue new file mode 100644 index 0000000..3b3b138 --- /dev/null +++ b/src/views/calibration/personnelManagement/components/UserImport.vue @@ -0,0 +1,198 @@ + + + diff --git a/src/views/calibration/personnelManagement/index.vue b/src/views/calibration/personnelManagement/index.vue new file mode 100644 index 0000000..5fefabf --- /dev/null +++ b/src/views/calibration/personnelManagement/index.vue @@ -0,0 +1,1088 @@ + + + + + + + diff --git a/src/views/calibration/registrationPlatform/components/RegistrationPlatformForm.vue b/src/views/calibration/registrationPlatform/components/RegistrationPlatformForm.vue new file mode 100644 index 0000000..5820eb3 --- /dev/null +++ b/src/views/calibration/registrationPlatform/components/RegistrationPlatformForm.vue @@ -0,0 +1,130 @@ + + + + + diff --git a/src/views/calibration/registrationPlatform/index.vue b/src/views/calibration/registrationPlatform/index.vue new file mode 100644 index 0000000..b921de5 --- /dev/null +++ b/src/views/calibration/registrationPlatform/index.vue @@ -0,0 +1,221 @@ + + + diff --git a/src/views/calibration/reimbursement/components/DeptTree.vue b/src/views/calibration/reimbursement/components/DeptTree.vue new file mode 100644 index 0000000..a0a12af --- /dev/null +++ b/src/views/calibration/reimbursement/components/DeptTree.vue @@ -0,0 +1,83 @@ + + + + diff --git a/src/views/calibration/reimbursement/components/UserImport.vue b/src/views/calibration/reimbursement/components/UserImport.vue new file mode 100644 index 0000000..3b3b138 --- /dev/null +++ b/src/views/calibration/reimbursement/components/UserImport.vue @@ -0,0 +1,198 @@ + + + diff --git a/src/views/calibration/reimbursement/index.vue b/src/views/calibration/reimbursement/index.vue new file mode 100644 index 0000000..ee8d9c9 --- /dev/null +++ b/src/views/calibration/reimbursement/index.vue @@ -0,0 +1,349 @@ + + + diff --git a/src/views/calibration/revenueRecognition/components/DeptTree.vue b/src/views/calibration/revenueRecognition/components/DeptTree.vue new file mode 100644 index 0000000..a0a12af --- /dev/null +++ b/src/views/calibration/revenueRecognition/components/DeptTree.vue @@ -0,0 +1,83 @@ + + + + diff --git a/src/views/calibration/revenueRecognition/components/UserImport.vue b/src/views/calibration/revenueRecognition/components/UserImport.vue new file mode 100644 index 0000000..3b3b138 --- /dev/null +++ b/src/views/calibration/revenueRecognition/components/UserImport.vue @@ -0,0 +1,198 @@ + + + diff --git a/src/views/calibration/revenueRecognition/index.vue b/src/views/calibration/revenueRecognition/index.vue new file mode 100644 index 0000000..8d31fbb --- /dev/null +++ b/src/views/calibration/revenueRecognition/index.vue @@ -0,0 +1,343 @@ + + + diff --git a/src/views/calibration/roleManagement/components/GrantPermissionsForm.vue b/src/views/calibration/roleManagement/components/GrantPermissionsForm.vue new file mode 100644 index 0000000..9776afb --- /dev/null +++ b/src/views/calibration/roleManagement/components/GrantPermissionsForm.vue @@ -0,0 +1,106 @@ + + + + + diff --git a/src/views/calibration/roleManagement/components/RoleForm.vue b/src/views/calibration/roleManagement/components/RoleForm.vue new file mode 100644 index 0000000..c30bd39 --- /dev/null +++ b/src/views/calibration/roleManagement/components/RoleForm.vue @@ -0,0 +1,104 @@ + + + + + diff --git a/src/views/calibration/roleManagement/index.vue b/src/views/calibration/roleManagement/index.vue new file mode 100644 index 0000000..eab496f --- /dev/null +++ b/src/views/calibration/roleManagement/index.vue @@ -0,0 +1,261 @@ + + + diff --git a/src/views/calibration/salaryBonusAdjustment/components/DeptTree.vue b/src/views/calibration/salaryBonusAdjustment/components/DeptTree.vue new file mode 100644 index 0000000..a0a12af --- /dev/null +++ b/src/views/calibration/salaryBonusAdjustment/components/DeptTree.vue @@ -0,0 +1,83 @@ + + + + diff --git a/src/views/calibration/salaryBonusAdjustment/components/UserImport.vue b/src/views/calibration/salaryBonusAdjustment/components/UserImport.vue new file mode 100644 index 0000000..3b3b138 --- /dev/null +++ b/src/views/calibration/salaryBonusAdjustment/components/UserImport.vue @@ -0,0 +1,198 @@ + + + diff --git a/src/views/calibration/salaryBonusAdjustment/index.vue b/src/views/calibration/salaryBonusAdjustment/index.vue new file mode 100644 index 0000000..d62e655 --- /dev/null +++ b/src/views/calibration/salaryBonusAdjustment/index.vue @@ -0,0 +1,340 @@ + + + diff --git a/src/views/calibration/systemManagement/components/SystemForm.vue b/src/views/calibration/systemManagement/components/SystemForm.vue new file mode 100644 index 0000000..69a6379 --- /dev/null +++ b/src/views/calibration/systemManagement/components/SystemForm.vue @@ -0,0 +1,161 @@ + + + diff --git a/src/views/calibration/systemManagement/index.vue b/src/views/calibration/systemManagement/index.vue new file mode 100644 index 0000000..2892153 --- /dev/null +++ b/src/views/calibration/systemManagement/index.vue @@ -0,0 +1,234 @@ + + + diff --git a/src/views/calibration/teamManagement/components/GrantPermissionsForm.vue b/src/views/calibration/teamManagement/components/GrantPermissionsForm.vue new file mode 100644 index 0000000..9776afb --- /dev/null +++ b/src/views/calibration/teamManagement/components/GrantPermissionsForm.vue @@ -0,0 +1,106 @@ + + + + + diff --git a/src/views/calibration/teamManagement/components/RoleForm.vue b/src/views/calibration/teamManagement/components/RoleForm.vue new file mode 100644 index 0000000..c30bd39 --- /dev/null +++ b/src/views/calibration/teamManagement/components/RoleForm.vue @@ -0,0 +1,104 @@ + + + + + diff --git a/src/views/calibration/teamManagement/components/TeamForm.vue b/src/views/calibration/teamManagement/components/TeamForm.vue new file mode 100644 index 0000000..bda5880 --- /dev/null +++ b/src/views/calibration/teamManagement/components/TeamForm.vue @@ -0,0 +1,111 @@ + + + + + diff --git a/src/views/calibration/teamManagement/index.vue b/src/views/calibration/teamManagement/index.vue new file mode 100644 index 0000000..fe54f35 --- /dev/null +++ b/src/views/calibration/teamManagement/index.vue @@ -0,0 +1,230 @@ + + + diff --git a/src/views/dashboard/AnnouncementDetails.vue b/src/views/dashboard/AnnouncementDetails.vue new file mode 100644 index 0000000..ed60255 --- /dev/null +++ b/src/views/dashboard/AnnouncementDetails.vue @@ -0,0 +1,79 @@ + + + + + diff --git a/src/views/dashboard/config.ts b/src/views/dashboard/config.ts new file mode 100644 index 0000000..43cde8f --- /dev/null +++ b/src/views/dashboard/config.ts @@ -0,0 +1,106 @@ +export const quickConfigurationList = [ + { + name: 'PersonnelUser', + title: '人事管理' + }, + { + name: 'Department', + title: '部门管理' + }, + { + name: 'RoleManagement', + title: '角色管理' + }, + { + name: 'TeamManagement', + title: '团队管理' + }, + { + name: 'PermissionManagement', + title: '权限管理' + }, + { + name: 'OnboardingRegistration', + title: '入职财务登记' + }, + { + name: 'Departure', + title: '离职财务登记' + }, + { + name: 'InvoiceApplication', + title: '开票申请' + }, + { + name: 'RevenueRecognition', + title: '收入确认' + }, + { + name: 'AccountAdjustmentApplication', + title: '调账申请' + }, + { + name: 'PaymentApplicationForm', + title: '付款申请单' + }, + { + name: 'Reimbursement', + title: '报销' + }, + { + name: 'SalaryBonusAdjustment', + title: '工资/奖金变更' + }, + { + name: 'ConflictOfInterestSearch', + title: '利益冲突检索' + }, + { + name: 'PreRegistration', + title: '预立案登记' + }, + { + name: 'BidRegistration', + title: '投标登记' + }, + { + name: 'ProjectRegistration', + title: '立项登记' + }, + { + name: 'CaseManagement', + title: '案件管理' + }, + { + name: 'CaseLabel', + title: '案件标签' + }, + { + name: 'ApplicationForSealApproval', + title: '申请用印' + }, + { + name: 'StampPerformanceIndex', + title: '业绩展示' + }, + { + name: 'InventoryRegistration', + title: '入库登记' + }, + { + name: 'RegistrationPlatform', + title: '注册平台登记' + }, + { + name: 'AnnouncementManagement', + title: '公告管理' + }, + { + name: 'LawFirmStandardDocuments', + title: '律所标准文件' + }, + { + name: 'LmportantScheduleManagement', + title: '日程管理' + } +] diff --git a/src/views/dashboard/index.vue b/src/views/dashboard/index.vue new file mode 100644 index 0000000..7c81769 --- /dev/null +++ b/src/views/dashboard/index.vue @@ -0,0 +1,588 @@ + + + + + diff --git a/src/views/error/401.vue b/src/views/error/401.vue new file mode 100644 index 0000000..38cee4a --- /dev/null +++ b/src/views/error/401.vue @@ -0,0 +1,25 @@ + + + diff --git a/src/views/error/404.vue b/src/views/error/404.vue new file mode 100644 index 0000000..0d9a5c6 --- /dev/null +++ b/src/views/error/404.vue @@ -0,0 +1,64 @@ + + + + + diff --git a/src/views/login/components/Login.vue b/src/views/login/components/Login.vue new file mode 100644 index 0000000..9d26351 --- /dev/null +++ b/src/views/login/components/Login.vue @@ -0,0 +1,156 @@ + + + + diff --git a/src/views/login/index.vue b/src/views/login/index.vue new file mode 100644 index 0000000..1d42b40 --- /dev/null +++ b/src/views/login/index.vue @@ -0,0 +1,118 @@ + + + + + diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..4b4ebba --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,40 @@ +{ + "compilerOptions": { + "target": "esnext", + "module": "esnext", + "moduleResolution": "bundler", + "lib": ["esnext", "dom"], + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + }, + + // 严格性和类型检查相关配置 + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + + // 模块和兼容性相关配置 + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "resolveJsonModule": true, + + // 调试和兼容性相关配置 + "sourceMap": true, + "useDefineForClassFields": true, + "allowJs": true, + + // 类型声明相关配置 + "types": ["node", "vite/client", "element-plus/global"] + }, + + "include": [ + "mock/**/*.ts", + "src/**/*.ts", + "src/**/*.vue", + "vite.config.ts", + "eslint.config.ts", + "uno.config.ts" + ], + "exclude": ["node_modules", "dist"] +} diff --git a/uno.config.ts b/uno.config.ts new file mode 100644 index 0000000..eaf5eb3 --- /dev/null +++ b/uno.config.ts @@ -0,0 +1,82 @@ +import { + defineConfig, + presetAttributify, + presetIcons, + presetTypography, + presetUno, + presetWebFonts, + transformerDirectives, + transformerVariantGroup +} from 'unocss' + +import { FileSystemIconLoader } from '@iconify/utils/lib/loader/node-loaders' +import fs from 'fs' + +// 本地SVG图标目录 +const iconsDir = './src/assets/icons' + +// 读取本地 SVG 目录,自动生成 safelist +const generateSafeList = () => { + try { + return fs + .readdirSync(iconsDir) + .filter((file) => file.endsWith('.svg')) + .map((file) => `i-svg:${file.replace('.svg', '')}`) + } catch (error) { + console.error('无法读取图标目录:', error) + return [] + } +} + +export default defineConfig({ + // 自定义快捷类 + shortcuts: { + 'wh-full': 'w-full h-full', + 'flex-center': 'flex justify-center items-center', + 'flex-x-center': 'flex justify-center', + 'flex-y-center': 'flex items-center', + 'flex-x-start': 'flex items-center justify-start', + 'flex-x-between': 'flex items-center justify-between', + 'flex-x-end': 'flex items-center justify-end' + }, + theme: { + colors: { + primary: 'var(--el-color-primary)', + primary_dark: 'var(--el-color-primary-light-5)' + }, + breakpoints: Object.fromEntries( + [640, 768, 1024, 1280, 1536, 1920, 2560].map((size, index) => [ + ['sm', 'md', 'lg', 'xl', '2xl', '3xl', '4xl'][index], + `${size}px` + ]) + ) + }, + presets: [ + presetUno(), + presetAttributify(), + presetIcons({ + // 额外属性 + extraProperties: { + display: 'inline-block', + width: '1em', + height: '1em' + }, + // 图表集合 + collections: { + // svg 是图标集合名称,使用 `i-svg:图标名` 调用 + svg: FileSystemIconLoader(iconsDir, (svg) => { + // 如果 `fill` 没有定义,则添加 `fill="currentColor"` + return svg.includes('fill="') ? svg : svg.replace(/^ { + const env = loadEnv(mode, process.cwd()) + const isProduction = mode === 'production' + + return { + base: './', // 设置为相对路径,支持直接打开index.html + resolve: { + alias: { + '@': pathSrc + } + }, + css: { + preprocessorOptions: { + // 定义全局 SCSS 变量 + scss: { + additionalData: `@use "@/styles/variables.scss" as *;` + } + } + }, + server: { + host: '0.0.0.0', + port: +(env.VITE_APP_PORT as string), + open: true, + proxy: { + '/dev-api/api2': { + changeOrigin: true, + // target: 'http://47.108.113.7:8000', + // target: 'http://47.108.113.7:8000', + // target: 'http://192.168.31.69:8006', + target: 'http://199.168.137.123:8000', + rewrite: (path: string) => { + return path.replace(/^\/dev-api\/api2/, '') + } + }, + // 代理 /dev-api 的请求 + [env.VITE_APP_BASE_API as string]: { + changeOrigin: true, + // 代理目标地址:https://api.youlai.tech + target: env.VITE_APP_API_URL as string, + rewrite: (path: string) => { + return path.replace(new RegExp('^' + (env.VITE_APP_BASE_API as string)), '') + } + } + } + }, + plugins: [ + vue(), + ...(env.VITE_MOCK_DEV_SERVER === 'true' ? [mockDevServerPlugin()] : []), + UnoCSS(), + // API 自动导入 + AutoImport({ + // 导入 Vue 函数,如:ref, reactive, toRef 等 + imports: ['vue', '@vueuse/core', 'pinia', 'vue-router', 'vue-i18n'], + resolvers: [ + // 导入 Element Plus函数,如:ElMessage, ElMessageBox 等 + ElementPlusResolver({ importStyle: 'sass' }) + ], + eslintrc: { + enabled: false, + filepath: './.eslintrc-auto-import.json', + globalsPropValue: true + }, + vueTemplate: true, + // 导入函数类型声明文件路径 (false:关闭自动生成) + dts: false + // dts: "src/types/auto-imports.d.ts", + }), + // 组件自动导入 + Components({ + resolvers: [ + // 导入 Element Plus 组件 + ElementPlusResolver({ importStyle: 'sass' }) + ], + // 指定自定义组件位置(默认:src/components) + dirs: ['src/components', 'src/**/components'], + // 导入组件类型声明文件路径 (false:关闭自动生成) + dts: false + // dts: "src/types/components.d.ts", + }) + ], + // 预加载项目必需的组件 + optimizeDeps: { + include: [ + 'vue', + 'vue-router', + 'element-plus', + 'pinia', + 'axios', + '@vueuse/core', + 'codemirror-editor-vue3', + 'default-passive-events', + 'exceljs', + 'path-to-regexp', + 'echarts/core', + 'echarts/renderers', + 'echarts/charts', + 'echarts/components', + 'vue-i18n', + 'nprogress', + 'sortablejs', + 'qs', + 'path-browserify', + '@stomp/stompjs', + '@element-plus/icons-vue', + 'element-plus/es', + 'element-plus/es/locale/lang/en', + 'element-plus/es/locale/lang/zh-cn', + 'element-plus/es/components/alert/style/index', + 'element-plus/es/components/avatar/style/index', + 'element-plus/es/components/backtop/style/index', + 'element-plus/es/components/badge/style/index', + 'element-plus/es/components/base/style/index', + 'element-plus/es/components/breadcrumb-item/style/index', + 'element-plus/es/components/breadcrumb/style/index', + 'element-plus/es/components/button/style/index', + 'element-plus/es/components/card/style/index', + 'element-plus/es/components/cascader/style/index', + 'element-plus/es/components/checkbox-group/style/index', + 'element-plus/es/components/checkbox/style/index', + 'element-plus/es/components/col/style/index', + 'element-plus/es/components/color-picker/style/index', + 'element-plus/es/components/config-provider/style/index', + 'element-plus/es/components/date-picker/style/index', + 'element-plus/es/components/descriptions-item/style/index', + 'element-plus/es/components/descriptions/style/index', + 'element-plus/es/components/dialog/style/index', + 'element-plus/es/components/divider/style/index', + 'element-plus/es/components/drawer/style/index', + 'element-plus/es/components/dropdown-item/style/index', + 'element-plus/es/components/dropdown-menu/style/index', + 'element-plus/es/components/dropdown/style/index', + 'element-plus/es/components/empty/style/index', + 'element-plus/es/components/form-item/style/index', + 'element-plus/es/components/form/style/index', + 'element-plus/es/components/icon/style/index', + 'element-plus/es/components/image-viewer/style/index', + 'element-plus/es/components/image/style/index', + 'element-plus/es/components/input-number/style/index', + 'element-plus/es/components/input-tag/style/index', + 'element-plus/es/components/input/style/index', + 'element-plus/es/components/link/style/index', + 'element-plus/es/components/loading/style/index', + 'element-plus/es/components/menu-item/style/index', + 'element-plus/es/components/menu/style/index', + 'element-plus/es/components/message-box/style/index', + 'element-plus/es/components/message/style/index', + 'element-plus/es/components/notification/style/index', + 'element-plus/es/components/option/style/index', + 'element-plus/es/components/pagination/style/index', + 'element-plus/es/components/popover/style/index', + 'element-plus/es/components/progress/style/index', + 'element-plus/es/components/radio-button/style/index', + 'element-plus/es/components/radio-group/style/index', + 'element-plus/es/components/radio/style/index', + 'element-plus/es/components/row/style/index', + 'element-plus/es/components/scrollbar/style/index', + 'element-plus/es/components/select/style/index', + 'element-plus/es/components/skeleton-item/style/index', + 'element-plus/es/components/skeleton/style/index', + 'element-plus/es/components/step/style/index', + 'element-plus/es/components/steps/style/index', + 'element-plus/es/components/sub-menu/style/index', + 'element-plus/es/components/switch/style/index', + 'element-plus/es/components/tab-pane/style/index', + 'element-plus/es/components/table-column/style/index', + 'element-plus/es/components/table/style/index', + 'element-plus/es/components/tabs/style/index', + 'element-plus/es/components/tag/style/index', + 'element-plus/es/components/text/style/index', + 'element-plus/es/components/time-picker/style/index', + 'element-plus/es/components/time-select/style/index', + 'element-plus/es/components/timeline-item/style/index', + 'element-plus/es/components/timeline/style/index', + 'element-plus/es/components/tooltip/style/index', + 'element-plus/es/components/tree-select/style/index', + 'element-plus/es/components/tree/style/index', + 'element-plus/es/components/upload/style/index', + 'element-plus/es/components/watermark/style/index', + 'element-plus/es/components/checkbox-button/style/index', + 'element-plus/es/components/space/style/index' + ] + }, + // 构建配置 + build: { + chunkSizeWarningLimit: 2000, // 消除打包大小超过500kb警告 + minify: isProduction ? ('terser' as const) : false, // 只在生产环境启用压缩 + terserOptions: isProduction + ? { + compress: { + keep_infinity: true, // 防止 Infinity 被压缩成 1/0,这可能会导致 Chrome 上的性能问题 + drop_console: true, // 生产环境去除 console.log, console.warn, console.error 等 + drop_debugger: true, // 生产环境去除 debugger + pure_funcs: ['console.log', 'console.info'] // 移除指定的函数调用 + }, + format: { + comments: false // 删除注释 + } + } + : {}, + rollupOptions: { + output: { + // manualChunks: { + // "vue-i18n": ["vue-i18n"], + // }, + // 用于从入口点创建的块的打包输出格式[name]表示文件名,[hash]表示该文件内容hash值 + entryFileNames: 'js/[name].[hash].js', + // 用于命名代码拆分时创建的共享块的输出命名 + chunkFileNames: 'js/[name].[hash].js', + // 用于输出静态资源的命名,[ext]表示文件扩展名 + assetFileNames: (assetInfo: any) => { + const info = assetInfo.name.split('.') + let extType = info[info.length - 1] + // console.log('文件信息', assetInfo.name) + if (/\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/i.test(assetInfo.name)) { + extType = 'media' + } else if (/\.(png|jpe?g|gif|svg)(\?.*)?$/.test(assetInfo.name)) { + extType = 'img' + } else if (/\.(woff2?|eot|ttf|otf)(\?.*)?$/i.test(assetInfo.name)) { + extType = 'fonts' + } + return `${extType}/[name].[hash].[ext]` + } + } + } + }, + define: { + __APP_INFO__: JSON.stringify(__APP_INFO__) + } + } +})