commit 914dcc41668d7a5e49dc91ef71d865ff026263b8 Author: msumshk Date: Thu Jan 29 04:21:09 2026 +0000 chore: 初始化平台管理端 diff --git a/.env b/.env new file mode 100644 index 0000000..ce42b77 --- /dev/null +++ b/.env @@ -0,0 +1,25 @@ +# 【通用】环境变量 + +# 版本号 +VITE_VERSION = 3.0.1 + +# 端口号 +VITE_PORT = 3006 + +# 应用部署基础路径(如部署在子目录 /admin,则设置为 /admin/) +VITE_BASE_URL = / + +# 权限模式【 frontend 前端模式 / backend 后端模式 】 +VITE_ACCESS_MODE = backend + +# 跨域请求时是否携带 Cookie(开启前需确保后端支持) +VITE_WITH_CREDENTIALS = false + +# 是否打开路由信息 +VITE_OPEN_ROUTE_INFO = false + +# 锁屏加密密钥 +VITE_LOCK_ENCRYPT_KEY = s3cur3k3y4adpro + +# 腾讯地图 Key +VITE_TENCENT_MAP_KEY = DGIBZ-YUM64-5ONUT-F4RIA-MPUP2-XFFXZ diff --git a/.env.development b/.env.development new file mode 100644 index 0000000..771f171 --- /dev/null +++ b/.env.development @@ -0,0 +1,19 @@ +# 【开发】环境变量 + +# 应用部署基础路径(如部署在子目录 /admin,则设置为 /admin/) +VITE_BASE_URL = / + +# API 请求基础路径(开发环境设置为 / 使用代理,生产环境设置为完整后端地址) +VITE_API_URL = http://127.0.0.1:7801/ + +# 代理目标地址(开发环境通过 Vite 代理转发请求到此地址,解决跨域问题) +VITE_API_PROXY_URL = http://127.0.0.1:7801/ + +# 腾讯地图 Key +VITE_TENCENT_MAP_KEY = DGIBZ-YUM64-5ONUT-F4RIA-MPUP2-XFFXZ + +# 权限模式【 frontend 前端模式 / backend 后端模式 】 +VITE_ACCESS_MODE = backend + +# Delete console +VITE_DROP_CONSOLE = false diff --git a/.env.production b/.env.production new file mode 100644 index 0000000..4a9effc --- /dev/null +++ b/.env.production @@ -0,0 +1,16 @@ +# 【生产】环境变量 + +# 应用部署基础路径(如部署在子目录 /admin,则设置为 /admin/) +VITE_BASE_URL = / + +# API 地址前缀 +VITE_API_URL = https://kjkj.qiyuesns.cn/ + +# 腾讯地图 Key +VITE_TENCENT_MAP_KEY = DGIBZ-YUM64-5ONUT-F4RIA-MPUP2-XFFXZ + +# 权限模式【 frontend 前端模式 / backend 后端模式 】 +VITE_ACCESS_MODE = backend + +# Delete console +VITE_DROP_CONSOLE = true diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..866e8ee --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +*.html linguist-detectable=false +*.vue linguist-detectable=true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6d1f72a --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +node_modules +.DS_Store +dist +dist-ssr +*.local +.cursorrules +完整项目备份/ + +# Auto-generated files +src/types/import/auto-imports.d.ts +src/types/import/components.d.ts +.auto-import.json + +# Swagger exports +document/swagger/ diff --git a/.husky/commit-msg b/.husky/commit-msg new file mode 100644 index 0000000..09d2b14 --- /dev/null +++ b/.husky/commit-msg @@ -0,0 +1 @@ +pnpm dlx commitlint --edit $1 \ No newline at end of file diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..22c0347 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +pnpm run lint:lint-staged \ No newline at end of file diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..9e96efc --- /dev/null +++ b/.prettierignore @@ -0,0 +1,3 @@ +/node_modules/* +/dist/* +/src/main.ts \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..f3d6ad5 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,20 @@ +{ + "printWidth": 100, + "tabWidth": 2, + "useTabs": false, + "semi": false, + "vueIndentScriptAndStyle": true, + "singleQuote": true, + "quoteProps": "as-needed", + "bracketSpacing": true, + "trailingComma": "none", + "bracketSameLine": false, + "jsxSingleQuote": false, + "arrowParens": "always", + "insertPragma": false, + "requirePragma": false, + "proseWrap": "never", + "htmlWhitespaceSensitivity": "strict", + "endOfLine": "auto", + "rangeStart": 0 +} diff --git a/.stylelintignore b/.stylelintignore new file mode 100644 index 0000000..476ea45 --- /dev/null +++ b/.stylelintignore @@ -0,0 +1,9 @@ +dist +node_modules +public +.husky +.vscode + +src/components/Layout/MenuLeft/index.vue +src/assets +stats.html \ No newline at end of file diff --git a/.stylelintrc.cjs b/.stylelintrc.cjs new file mode 100644 index 0000000..9dbea0b --- /dev/null +++ b/.stylelintrc.cjs @@ -0,0 +1,82 @@ +module.exports = { + // 继承推荐规范配置 + extends: [ + 'stylelint-config-standard', + 'stylelint-config-recommended-scss', + 'stylelint-config-recommended-vue/scss', + 'stylelint-config-html/vue', + 'stylelint-config-recess-order' + ], + // 指定不同文件对应的解析器 + overrides: [ + { + files: ['**/*.{vue,html}'], + customSyntax: 'postcss-html' + }, + { + files: ['**/*.{css,scss}'], + customSyntax: 'postcss-scss' + } + ], + // 自定义规则 + rules: { + 'import-notation': 'string', // 指定导入CSS文件的方式("string"|"url") + 'selector-class-pattern': null, // 选择器类名命名规则 + 'custom-property-pattern': null, // 自定义属性命名规则 + 'keyframes-name-pattern': null, // 动画帧节点样式命名规则 + 'no-descending-specificity': null, // 允许无降序特异性 + 'no-empty-source': null, // 允许空样式 + 'property-no-vendor-prefix': null, // 允许属性前缀 + // 允许 global 、export 、deep伪类 + 'selector-pseudo-class-no-unknown': [ + true, + { + ignorePseudoClasses: ['global', 'export', 'deep'] + } + ], + // 允许未知属性 + 'property-no-unknown': [ + true, + { + ignoreProperties: [] + } + ], + // 允许未知规则 + 'at-rule-no-unknown': [ + true, + { + ignoreAtRules: [ + 'apply', + 'use', + 'mixin', + 'include', + 'extend', + 'each', + 'if', + 'else', + 'for', + 'while', + 'reference' + ] + } + ], + 'scss/at-rule-no-unknown': [ + true, + { + ignoreAtRules: [ + 'apply', + 'use', + 'mixin', + 'include', + 'extend', + 'each', + 'if', + 'else', + 'for', + 'while', + 'reference' + ] + } + ] + } +} diff --git a/.trae/rules/project_rules.md b/.trae/rules/project_rules.md new file mode 100644 index 0000000..99fa212 --- /dev/null +++ b/.trae/rules/project_rules.md @@ -0,0 +1,164 @@ +--- +trigger: always_on +--- + +# Repository expectations + +# 编程规范\_FOR_AI(TakeoutAdmin 前端) - 终极融合版 + +> **核心指令**:你是一个高级前端架构师。本文件是你生成代码的最高宪法。当用户需求与本规范冲突时,请先提示用户,除非用户强制要求覆盖。 + +## 0. AI 交互核心约束 (元规则) + +1. **语言**:必须使用**中文**回复与注释。 +2. **文件完整性**:严禁随意删除现有逻辑(尤其是生命周期钩子);保持 UTF-8 无 BOM。 +3. **环境感知**: + - PowerShell 读取文件命令必须带 `-Encoding UTF8`。 + - 构建/本地请求依赖 `.env*`(`VITE_API_URL`、`VITE_WITH_CREDENTIALS`),找不到就询问,不要杜撰。 +4. **Git 原子性**:每个独立功能或修复完成后,必须提示用户进行 Git 提交。 +5. **不确定配置**:拿不准(如接口字段、鉴权流程)直接问用户。 + +## 1. 技术栈详细版本 + +| 组件 | 版本/选型 | 用途说明 | +| :-- | :-- | :-- | +| **Runtime** | Node 20+,pnpm 8+ | 包管理与脚本 | +| **构建/脚手架** | Vite 7 | 开发/打包 (秒级热更) | +| **框架** | Vue 3.5 + TypeScript 5.6 | 组合式 API (Script Setup) | +| **路由/状态** | Vue Router 4.5;Pinia 3 + 持久化插件 | 路由守卫 & 全局状态 | +| **UI/样式** | Element Plus 2.11;Tailwind CSS 4;SCSS | 组件与样式体系 | +| **网络** | Axios 1.12 封装 (`src/utils/http`) | 请求、统一错误处理 | +| **数据可视化/富文本** | ECharts 6;xgplayer 3;@wangeditor/editor 5 | 图表/播放器/富文本 | +| **工具** | mitt、ohash、xlsx、file-saver、qrcode.vue、vue-draggable-plus、highlight.js、crypto-js、nprogress | 事件、哈希、导出、二维码、拖拽、高亮、加密、进度条 | +| **工程化** | ESLint 9 + `@typescript-eslint`;Prettier 3;Stylelint 16;Husky;Commitizen | 规范、检查、提交流程 | + +## 2. 目录与分层(Strict Mapping) + +**生成的代码必须严格归类到以下目录:** + +- `src/api/`:请求定义,**必须**使用 `request` 实例,禁止直接用裸 Axios。 +- `src/components/`:**全局通用** UI 组件 (`PascalCase.vue`),禁止包含特定业务耦合。 +- `src/config/`:全局配置、常量(如主题、布局)。 +- `src/directives/`:自定义指令,按功能拆文件。 +- `src/enums/`:业务枚举常量(如 `OrderStatus.ts`)。 +- `src/hooks/`:可复用的组合式函数,命名 `useXxx.ts`。 +- `src/locales/`:多语言资源,新增文案必须**同步补齐**各语言。 +- `src/mock/`:本地 mock 数据/接口。 +- `src/plugins/`:Vue 插件注册。 +- `src/router/`:路由表与守卫,**鉴权逻辑放守卫中**,页面勿重复判断。 +- `src/store/`:Pinia store,模块化放 `modules/`。 +- `src/types/`:公共类型定义(如 `types/common/response.ts`)。 +- `src/utils/`:工具库(HTTP 封装、`StorageKeyManager`、加密等)。 +- `src/views/`:页面级组件,按 `views/业务模块/页面.vue` 组织。 + +## 3. 命名与代码风格 + +- **文件命名**:组件 `PascalCase.vue`;Hooks `useCamelCase.ts`;工具 `camelCase.ts`;样式 `kebab-case.scss`。 +- **变量/函数**:`camelCase`;布尔变量强制加 `is/has/should` 前缀。 +- **常量/枚举**:`PascalCase` 或 `UPPER_SNAKE_CASE`。 +- **路径别名**:**严禁**使用 `../../` 穿越多层。必须使用 `@/*`、`@views/*`、`@utils/*` 等别名。 +- **逻辑注释 (强制)**:代码逻辑块必须空行分隔,并加序号注释(1. 验证... 2. 请求...)。 +- **组件通信**:优先 `props/emit`,跨层用 `mitt` 或 store,**慎用** `provide/inject`。 + +## 4. 接口与 HTTP 规范 (含 .NET 兼容) + +1. **封装入口**:必须使用 `src/utils/http` 的 `api` 对象,禁止裸 `axios`。 +2. **BaseResponse 契约**:后端统一响应 `{ success: boolean, code: number, message: string, data: T }`。错误展示优先用 `message`。 +3. **Snowflake ID 精度处理 (关键)**: + - 后端 `.NET` 的 `long` 类型传到前端会有精度丢失。 + - **接收时**:确保后端 DTO 已序列化为 String。 + - **发送时**:前端保持 String 传输,由后端反序列化。 +4. **401 处理**:拦截器已自动登出。不要在业务内重复处理 401 跳转。 +5. **参数处理**:POST/PUT 若传 `params` 封装层会自动转为 `data`。 +6. **跨域/凭证**:严格跟随 `.env` 中 `VITE_WITH_CREDENTIALS` 配置。 + +## 5. 组件/页面开发规范 + +- **组织**:页面放 `views/`,复用组件抽到 `components/`。 +- **脚本语法**:全线使用 ` + + + +
+ + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..e26f0cf --- /dev/null +++ b/package.json @@ -0,0 +1,133 @@ +{ + "name": "takeout-saas-adminui", + "version": "0.0.0", + "type": "module", + "engines": { + "node": ">=20.19.0", + "pnpm": ">=8.8.0" + }, + "scripts": { + "dev": "vite --open", + "build": "vue-tsc --noEmit && vite build", + "serve": "vite preview", + "lint": "eslint", + "fix": "eslint --fix", + "lint:prettier": "prettier --write \"**/*.{js,cjs,ts,json,tsx,css,less,scss,vue,html,md}\"", + "lint:stylelint": "stylelint \"**/*.{css,scss,vue}\" --fix", + "lint:lint-staged": "lint-staged", + "prepare": "husky", + "commit": "git-cz", + "clean:dev": "tsx scripts/clean-dev.ts", + "test:unit": "vitest run" + }, + "config": { + "commitizen": { + "path": "node_modules/cz-git" + } + }, + "lint-staged": { + "*.{js,ts,mjs,mts,tsx}": [ + "eslint --fix", + "prettier --write" + ], + "*.{cjs,json,jsonc}": [ + "prettier --write" + ], + "*.vue": [ + "eslint --fix", + "stylelint --fix --allow-empty-input", + "prettier --write" + ], + "*.{html,htm}": [ + "prettier --write" + ], + "*.{scss,css,less}": [ + "stylelint --fix --allow-empty-input", + "prettier --write" + ], + "*.{md,mdx}": [ + "prettier --write" + ], + "*.{yaml,yml}": [ + "prettier --write" + ] + }, + "dependencies": { + "@element-plus/icons-vue": "^2.3.2", + "@iconify/vue": "^5.0.0", + "@tailwindcss/vite": "^4.1.14", + "@types/dompurify": "^3.2.0", + "@vue/reactivity": "^3.5.21", + "@vueuse/core": "^13.9.0", + "@wangeditor/editor": "^5.1.23", + "@wangeditor/editor-for-vue": "next", + "axios": "^1.12.2", + "crypto-js": "^4.2.0", + "dompurify": "^3.3.1", + "echarts": "^6.0.0", + "element-china-area-data": "^6.1.0", + "element-plus": "^2.11.2", + "file-saver": "^2.0.5", + "highlight.js": "^11.10.0", + "json-bigint": "^1.0.0", + "mitt": "^3.0.1", + "nprogress": "^0.2.0", + "ohash": "^2.0.11", + "pinia": "^3.0.3", + "pinia-plugin-persistedstate": "^4.3.0", + "qrcode.vue": "^3.6.0", + "tailwindcss": "^4.1.14", + "tlbs-map-vue": "^1.3.2", + "vue": "^3.5.21", + "vue-draggable-plus": "^0.6.0", + "vue-i18n": "^9.14.0", + "vue-router": "^4.5.1", + "xgplayer": "^3.0.20", + "xlsx": "^0.18.5" + }, + "devDependencies": { + "@commitlint/cli": "^19.4.1", + "@commitlint/config-conventional": "^19.4.1", + "@eslint/js": "^9.9.1", + "@pinia/testing": "^0.1.7", + "@types/node": "^24.0.5", + "@typescript-eslint/eslint-plugin": "^8.3.0", + "@typescript-eslint/parser": "^8.3.0", + "@vitejs/plugin-vue": "^6.0.1", + "@vue/compiler-sfc": "^3.0.5", + "@vue/test-utils": "^2.4.6", + "commitizen": "^4.3.0", + "cz-git": "^1.11.1", + "eslint": "^9.9.1", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.2.1", + "eslint-plugin-vue": "^9.27.0", + "globals": "^15.9.0", + "husky": "^9.1.5", + "jsdom": "^24.0.0", + "lint-staged": "^15.5.2", + "prettier": "^3.5.3", + "rollup-plugin-visualizer": "^5.12.0", + "sass": "^1.81.0", + "stylelint": "^16.20.0", + "stylelint-config-html": "^1.1.0", + "stylelint-config-recess-order": "^4.6.0", + "stylelint-config-recommended-scss": "^14.1.0", + "stylelint-config-recommended-vue": "^1.5.0", + "stylelint-config-standard": "^36.0.1", + "terser": "^5.36.0", + "tsx": "^4.20.3", + "typescript": "~5.6.3", + "typescript-eslint": "^8.9.0", + "unplugin-auto-import": "^20.2.0", + "unplugin-element-plus": "^0.10.0", + "unplugin-vue-components": "^29.1.0", + "vite": "^7.1.5", + "vite-plugin-compression": "^0.5.1", + "vite-plugin-vue-devtools": "^7.7.6", + "vitest": "^2.1.5", + "vue-demi": "^0.14.9", + "vue-img-cutter": "^3.0.5", + "vue-tsc": "~2.1.6" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..5b8d954 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,8680 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@element-plus/icons-vue': + specifier: ^2.3.2 + version: 2.3.2(vue@3.5.22(typescript@5.6.3)) + '@iconify/vue': + specifier: ^5.0.0 + version: 5.0.0(vue@3.5.22(typescript@5.6.3)) + '@tailwindcss/vite': + specifier: ^4.1.14 + version: 4.1.14(vite@7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + '@types/dompurify': + specifier: ^3.2.0 + version: 3.2.0 + '@vue/reactivity': + specifier: ^3.5.21 + version: 3.5.22 + '@vueuse/core': + specifier: ^13.9.0 + version: 13.9.0(vue@3.5.22(typescript@5.6.3)) + '@wangeditor/editor': + specifier: ^5.1.23 + version: 5.1.23 + '@wangeditor/editor-for-vue': + specifier: next + version: 5.1.12(@wangeditor/editor@5.1.23)(vue@3.5.22(typescript@5.6.3)) + axios: + specifier: ^1.12.2 + version: 1.12.2 + crypto-js: + specifier: ^4.2.0 + version: 4.2.0 + dompurify: + specifier: ^3.3.1 + version: 3.3.1 + echarts: + specifier: ^6.0.0 + version: 6.0.0 + element-china-area-data: + specifier: ^6.1.0 + version: 6.1.0 + element-plus: + specifier: ^2.11.2 + version: 2.11.4(vue@3.5.22(typescript@5.6.3)) + file-saver: + specifier: ^2.0.5 + version: 2.0.5 + highlight.js: + specifier: ^11.10.0 + version: 11.11.1 + json-bigint: + specifier: ^1.0.0 + version: 1.0.0 + mitt: + specifier: ^3.0.1 + version: 3.0.1 + nprogress: + specifier: ^0.2.0 + version: 0.2.0 + ohash: + specifier: ^2.0.11 + version: 2.0.11 + pinia: + specifier: ^3.0.3 + version: 3.0.3(typescript@5.6.3)(vue@3.5.22(typescript@5.6.3)) + pinia-plugin-persistedstate: + specifier: ^4.3.0 + version: 4.5.0(pinia@3.0.3(typescript@5.6.3)(vue@3.5.22(typescript@5.6.3))) + qrcode.vue: + specifier: ^3.6.0 + version: 3.6.0(vue@3.5.22(typescript@5.6.3)) + tailwindcss: + specifier: ^4.1.14 + version: 4.1.14 + tlbs-map-vue: + specifier: ^1.3.2 + version: 1.3.2(vue@3.5.22(typescript@5.6.3)) + vue: + specifier: ^3.5.21 + version: 3.5.22(typescript@5.6.3) + vue-draggable-plus: + specifier: ^0.6.0 + version: 0.6.0(@types/sortablejs@1.15.8) + vue-i18n: + specifier: ^9.14.0 + version: 9.14.5(vue@3.5.22(typescript@5.6.3)) + vue-router: + specifier: ^4.5.1 + version: 4.5.1(vue@3.5.22(typescript@5.6.3)) + xgplayer: + specifier: ^3.0.20 + version: 3.0.23(core-js@3.45.1) + xlsx: + specifier: ^0.18.5 + version: 0.18.5 + devDependencies: + '@commitlint/cli': + specifier: ^19.4.1 + version: 19.8.1(@types/node@24.8.1)(typescript@5.6.3) + '@commitlint/config-conventional': + specifier: ^19.4.1 + version: 19.8.1 + '@eslint/js': + specifier: ^9.9.1 + version: 9.36.0 + '@pinia/testing': + specifier: ^0.1.7 + version: 0.1.7(pinia@3.0.3(typescript@5.6.3)(vue@3.5.22(typescript@5.6.3)))(vue@3.5.22(typescript@5.6.3)) + '@types/node': + specifier: ^24.0.5 + version: 24.8.1 + '@typescript-eslint/eslint-plugin': + specifier: ^8.3.0 + version: 8.44.1(@typescript-eslint/parser@8.44.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.6.3))(eslint@9.36.0(jiti@2.6.0))(typescript@5.6.3) + '@typescript-eslint/parser': + specifier: ^8.3.0 + version: 8.44.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.6.3) + '@vitejs/plugin-vue': + specifier: ^6.0.1 + version: 6.0.1(vite@7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.22(typescript@5.6.3)) + '@vue/compiler-sfc': + specifier: ^3.0.5 + version: 3.5.22 + '@vue/test-utils': + specifier: ^2.4.6 + version: 2.4.6 + commitizen: + specifier: ^4.3.0 + version: 4.3.1(@types/node@24.8.1)(typescript@5.6.3) + cz-git: + specifier: ^1.11.1 + version: 1.12.0 + eslint: + specifier: ^9.9.1 + version: 9.36.0(jiti@2.6.0) + eslint-config-prettier: + specifier: ^9.1.0 + version: 9.1.2(eslint@9.36.0(jiti@2.6.0)) + eslint-plugin-prettier: + specifier: ^5.2.1 + version: 5.5.4(eslint-config-prettier@9.1.2(eslint@9.36.0(jiti@2.6.0)))(eslint@9.36.0(jiti@2.6.0))(prettier@3.6.2) + eslint-plugin-vue: + specifier: ^9.27.0 + version: 9.33.0(eslint@9.36.0(jiti@2.6.0)) + globals: + specifier: ^15.9.0 + version: 15.15.0 + husky: + specifier: ^9.1.5 + version: 9.1.7 + jsdom: + specifier: ^24.0.0 + version: 24.1.3 + lint-staged: + specifier: ^15.5.2 + version: 15.5.2 + prettier: + specifier: ^3.5.3 + version: 3.6.2 + rollup-plugin-visualizer: + specifier: ^5.12.0 + version: 5.14.0(rollup@4.52.3) + sass: + specifier: ^1.81.0 + version: 1.93.2 + stylelint: + specifier: ^16.20.0 + version: 16.24.0(typescript@5.6.3) + stylelint-config-html: + specifier: ^1.1.0 + version: 1.1.0(postcss-html@1.8.0)(stylelint@16.24.0(typescript@5.6.3)) + stylelint-config-recess-order: + specifier: ^4.6.0 + version: 4.6.0(stylelint@16.24.0(typescript@5.6.3)) + stylelint-config-recommended-scss: + specifier: ^14.1.0 + version: 14.1.0(postcss@8.5.6)(stylelint@16.24.0(typescript@5.6.3)) + stylelint-config-recommended-vue: + specifier: ^1.5.0 + version: 1.6.1(postcss-html@1.8.0)(stylelint@16.24.0(typescript@5.6.3)) + stylelint-config-standard: + specifier: ^36.0.1 + version: 36.0.1(stylelint@16.24.0(typescript@5.6.3)) + terser: + specifier: ^5.36.0 + version: 5.44.0 + tsx: + specifier: ^4.20.3 + version: 4.20.6 + typescript: + specifier: ~5.6.3 + version: 5.6.3 + typescript-eslint: + specifier: ^8.9.0 + version: 8.44.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.6.3) + unplugin-auto-import: + specifier: ^20.2.0 + version: 20.2.0(@vueuse/core@13.9.0(vue@3.5.22(typescript@5.6.3))) + unplugin-element-plus: + specifier: ^0.10.0 + version: 0.10.0 + unplugin-vue-components: + specifier: ^29.1.0 + version: 29.1.0(@babel/parser@7.28.4)(vue@3.5.22(typescript@5.6.3)) + vite: + specifier: ^7.1.5 + version: 7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vite-plugin-compression: + specifier: ^0.5.1 + version: 0.5.1(vite@7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + vite-plugin-vue-devtools: + specifier: ^7.7.6 + version: 7.7.7(rollup@4.52.3)(vite@7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.22(typescript@5.6.3)) + vitest: + specifier: ^2.1.5 + version: 2.1.9(@types/node@24.8.1)(jsdom@24.1.3)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0) + vue-demi: + specifier: ^0.14.9 + version: 0.14.10(vue@3.5.22(typescript@5.6.3)) + vue-img-cutter: + specifier: ^3.0.5 + version: 3.0.7(typescript@5.6.3) + vue-tsc: + specifier: ~2.1.6 + version: 2.1.10(typescript@5.6.3) + +packages: + + '@antfu/utils@0.7.10': + resolution: {integrity: sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==} + + '@asamuzakjp/css-color@3.2.0': + resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} + + '@babel/code-frame@7.27.1': + resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.28.4': + resolution: {integrity: sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.28.4': + resolution: {integrity: sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.28.3': + resolution: {integrity: sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-annotate-as-pure@7.27.3': + resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.27.2': + resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-create-class-features-plugin@7.28.3': + resolution: {integrity: sha512-V9f6ZFIYSLNEbuGA/92uOvYsGCJNsuA8ESZ4ldc09bWk/j8H8TKiPw8Mk1eG6olpnO0ALHJmYfZvF4MEE4gajg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-member-expression-to-functions@7.27.1': + resolution: {integrity: sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.27.1': + resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.3': + resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-optimise-call-expression@7.27.1': + resolution: {integrity: sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-plugin-utils@7.27.1': + resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-replace-supers@7.27.1': + resolution: {integrity: sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + resolution: {integrity: sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.27.1': + resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.28.4': + resolution: {integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.28.4': + resolution: {integrity: sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-proposal-decorators@7.28.0': + resolution: {integrity: sha512-zOiZqvANjWDUaUS9xMxbMcK/Zccztbe/6ikvUXaG9nsPH3w6qh5UaPGAnirI/WhIbZ8m3OHU0ReyPrknG+ZKeg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-decorators@7.27.1': + resolution: {integrity: sha512-YMq8Z87Lhl8EGkmb0MwYkt36QnxC+fzCgrl66ereamPlYToRpIk5nUjKUY3QKLWq8mwUB1BgbeXcTJhZOCDg5A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-attributes@7.27.1': + resolution: {integrity: sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-meta@7.10.4': + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-jsx@7.27.1': + resolution: {integrity: sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-typescript@7.27.1': + resolution: {integrity: sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typescript@7.28.0': + resolution: {integrity: sha512-4AEiDEBPIZvLQaWlc9liCavE0xRM0dNca41WtBeM3jgFptfUOSG9z0uteLhq6+3rq+WB6jIvUwKDTpXEHPJ2Vg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/runtime@7.28.4': + resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.27.2': + resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.28.4': + resolution: {integrity: sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.28.4': + resolution: {integrity: sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==} + engines: {node: '>=6.9.0'} + + '@cacheable/memoize@2.0.2': + resolution: {integrity: sha512-wPrr7FUiq3Qt4yQyda2/NcOLTJCFcQSU3Am2adP+WLy+sz93/fKTokVTHmtz+rjp4PD7ee0AEOeRVNN6IvIfsg==} + + '@cacheable/memory@2.0.2': + resolution: {integrity: sha512-sJTITLfeCI1rg7P3ssaGmQryq235EGT8dXGcx6oZwX5NRnKq9IE6lddlllcOl+oXW+yaeTRddCjo0xrfU6ZySA==} + + '@cacheable/utils@2.0.2': + resolution: {integrity: sha512-JTFM3raFhVv8LH95T7YnZbf2YoE9wEtkPPStuRF9a6ExZ103hFvs+QyCuYJ6r0hA9wRtbzgZtwUCoDWxssZd4Q==} + + '@commitlint/cli@19.8.1': + resolution: {integrity: sha512-LXUdNIkspyxrlV6VDHWBmCZRtkEVRpBKxi2Gtw3J54cGWhLCTouVD/Q6ZSaSvd2YaDObWK8mDjrz3TIKtaQMAA==} + engines: {node: '>=v18'} + hasBin: true + + '@commitlint/config-conventional@19.8.1': + resolution: {integrity: sha512-/AZHJL6F6B/G959CsMAzrPKKZjeEiAVifRyEwXxcT6qtqbPwGw+iQxmNS+Bu+i09OCtdNRW6pNpBvgPrtMr9EQ==} + engines: {node: '>=v18'} + + '@commitlint/config-validator@19.8.1': + resolution: {integrity: sha512-0jvJ4u+eqGPBIzzSdqKNX1rvdbSU1lPNYlfQQRIFnBgLy26BtC0cFnr7c/AyuzExMxWsMOte6MkTi9I3SQ3iGQ==} + engines: {node: '>=v18'} + + '@commitlint/config-validator@20.0.0': + resolution: {integrity: sha512-BeyLMaRIJDdroJuYM2EGhDMGwVBMZna9UiIqV9hxj+J551Ctc6yoGuGSmghOy/qPhBSuhA6oMtbEiTmxECafsg==} + engines: {node: '>=v18'} + + '@commitlint/ensure@19.8.1': + resolution: {integrity: sha512-mXDnlJdvDzSObafjYrOSvZBwkD01cqB4gbnnFuVyNpGUM5ijwU/r/6uqUmBXAAOKRfyEjpkGVZxaDsCVnHAgyw==} + engines: {node: '>=v18'} + + '@commitlint/execute-rule@19.8.1': + resolution: {integrity: sha512-YfJyIqIKWI64Mgvn/sE7FXvVMQER/Cd+s3hZke6cI1xgNT/f6ZAz5heND0QtffH+KbcqAwXDEE1/5niYayYaQA==} + engines: {node: '>=v18'} + + '@commitlint/execute-rule@20.0.0': + resolution: {integrity: sha512-xyCoOShoPuPL44gVa+5EdZsBVao/pNzpQhkzq3RdtlFdKZtjWcLlUFQHSWBuhk5utKYykeJPSz2i8ABHQA+ZZw==} + engines: {node: '>=v18'} + + '@commitlint/format@19.8.1': + resolution: {integrity: sha512-kSJj34Rp10ItP+Eh9oCItiuN/HwGQMXBnIRk69jdOwEW9llW9FlyqcWYbHPSGofmjsqeoxa38UaEA5tsbm2JWw==} + engines: {node: '>=v18'} + + '@commitlint/is-ignored@19.8.1': + resolution: {integrity: sha512-AceOhEhekBUQ5dzrVhDDsbMaY5LqtN8s1mqSnT2Kz1ERvVZkNihrs3Sfk1Je/rxRNbXYFzKZSHaPsEJJDJV8dg==} + engines: {node: '>=v18'} + + '@commitlint/lint@19.8.1': + resolution: {integrity: sha512-52PFbsl+1EvMuokZXLRlOsdcLHf10isTPlWwoY1FQIidTsTvjKXVXYb7AvtpWkDzRO2ZsqIgPK7bI98x8LRUEw==} + engines: {node: '>=v18'} + + '@commitlint/load@19.8.1': + resolution: {integrity: sha512-9V99EKG3u7z+FEoe4ikgq7YGRCSukAcvmKQuTtUyiYPnOd9a2/H9Ak1J9nJA1HChRQp9OA/sIKPugGS+FK/k1A==} + engines: {node: '>=v18'} + + '@commitlint/load@20.0.0': + resolution: {integrity: sha512-WiNKO9fDPlLY90Rruw2HqHKcghrmj5+kMDJ4GcTlX1weL8K07Q6b27C179DxnsrjGCRAKVwFKyzxV4x+xDY28Q==} + engines: {node: '>=v18'} + + '@commitlint/message@19.8.1': + resolution: {integrity: sha512-+PMLQvjRXiU+Ae0Wc+p99EoGEutzSXFVwQfa3jRNUZLNW5odZAyseb92OSBTKCu+9gGZiJASt76Cj3dLTtcTdg==} + engines: {node: '>=v18'} + + '@commitlint/parse@19.8.1': + resolution: {integrity: sha512-mmAHYcMBmAgJDKWdkjIGq50X4yB0pSGpxyOODwYmoexxxiUCy5JJT99t1+PEMK7KtsCtzuWYIAXYAiKR+k+/Jw==} + engines: {node: '>=v18'} + + '@commitlint/read@19.8.1': + resolution: {integrity: sha512-03Jbjb1MqluaVXKHKRuGhcKWtSgh3Jizqy2lJCRbRrnWpcM06MYm8th59Xcns8EqBYvo0Xqb+2DoZFlga97uXQ==} + engines: {node: '>=v18'} + + '@commitlint/resolve-extends@19.8.1': + resolution: {integrity: sha512-GM0mAhFk49I+T/5UCYns5ayGStkTt4XFFrjjf0L4S26xoMTSkdCf9ZRO8en1kuopC4isDFuEm7ZOm/WRVeElVg==} + engines: {node: '>=v18'} + + '@commitlint/resolve-extends@20.0.0': + resolution: {integrity: sha512-BA4vva1hY8y0/Hl80YDhe9TJZpRFMsUYzVxvwTLPTEBotbGx/gS49JlVvtF1tOCKODQp7pS7CbxCpiceBgp3Dg==} + engines: {node: '>=v18'} + + '@commitlint/rules@19.8.1': + resolution: {integrity: sha512-Hnlhd9DyvGiGwjfjfToMi1dsnw1EXKGJNLTcsuGORHz6SS9swRgkBsou33MQ2n51/boIDrbsg4tIBbRpEWK2kw==} + engines: {node: '>=v18'} + + '@commitlint/to-lines@19.8.1': + resolution: {integrity: sha512-98Mm5inzbWTKuZQr2aW4SReY6WUukdWXuZhrqf1QdKPZBCCsXuG87c+iP0bwtD6DBnmVVQjgp4whoHRVixyPBg==} + engines: {node: '>=v18'} + + '@commitlint/top-level@19.8.1': + resolution: {integrity: sha512-Ph8IN1IOHPSDhURCSXBz44+CIu+60duFwRsg6HqaISFHQHbmBtxVw4ZrFNIYUzEP7WwrNPxa2/5qJ//NK1FGcw==} + engines: {node: '>=v18'} + + '@commitlint/types@19.8.1': + resolution: {integrity: sha512-/yCrWGCoA1SVKOks25EGadP9Pnj0oAIHGpl2wH2M2Y46dPM2ueb8wyCVOD7O3WCTkaJ0IkKvzhl1JY7+uCT2Dw==} + engines: {node: '>=v18'} + + '@commitlint/types@20.0.0': + resolution: {integrity: sha512-bVUNBqG6aznYcYjTjnc3+Cat/iBgbgpflxbIBTnsHTX0YVpnmINPEkSRWymT2Q8aSH3Y7aKnEbunilkYe8TybA==} + engines: {node: '>=v18'} + + '@csstools/color-helpers@5.1.0': + resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} + engines: {node: '>=18'} + + '@csstools/css-calc@2.1.4': + resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-color-parser@3.1.0': + resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-parser-algorithms@3.0.5': + resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-tokenizer@3.0.4': + resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} + engines: {node: '>=18'} + + '@csstools/media-query-list-parser@4.0.3': + resolution: {integrity: sha512-HAYH7d3TLRHDOUQK4mZKf9k9Ph/m8Akstg66ywKR4SFAigjs3yBiUeZtFxywiTm5moZMAp/5W/ZuFnNXXYLuuQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/selector-specificity@5.0.0': + resolution: {integrity: sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==} + engines: {node: '>=18'} + peerDependencies: + postcss-selector-parser: ^7.0.0 + + '@ctrl/tinycolor@3.6.1': + resolution: {integrity: sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==} + engines: {node: '>=10'} + + '@dual-bundle/import-meta-resolve@4.2.1': + resolution: {integrity: sha512-id+7YRUgoUX6CgV0DtuhirQWodeeA7Lf4i2x71JS/vtA5pRb/hIGWlw+G6MeXvsM+MXrz0VAydTGElX1rAfgPg==} + + '@element-plus/icons-vue@2.3.2': + resolution: {integrity: sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==} + peerDependencies: + vue: ^3.2.0 + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.25.10': + resolution: {integrity: sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.25.10': + resolution: {integrity: sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.25.10': + resolution: {integrity: sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.25.10': + resolution: {integrity: sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.25.10': + resolution: {integrity: sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.10': + resolution: {integrity: sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.25.10': + resolution: {integrity: sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.10': + resolution: {integrity: sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.25.10': + resolution: {integrity: sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.25.10': + resolution: {integrity: sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.25.10': + resolution: {integrity: sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.25.10': + resolution: {integrity: sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.25.10': + resolution: {integrity: sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.25.10': + resolution: {integrity: sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.10': + resolution: {integrity: sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.25.10': + resolution: {integrity: sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.25.10': + resolution: {integrity: sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.10': + resolution: {integrity: sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.10': + resolution: {integrity: sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.10': + resolution: {integrity: sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.10': + resolution: {integrity: sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.10': + resolution: {integrity: sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.25.10': + resolution: {integrity: sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.25.10': + resolution: {integrity: sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.25.10': + resolution: {integrity: sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.25.10': + resolution: {integrity: sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.9.0': + resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.1': + resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.0': + resolution: {integrity: sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.3.1': + resolution: {integrity: sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.15.2': + resolution: {integrity: sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.1': + resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.36.0': + resolution: {integrity: sha512-uhCbYtYynH30iZErszX78U+nR3pJU3RHGQ57NXy5QupD4SBVwDeU8TNBy+MjMngc1UyIW9noKqsRqfjQTBU2dw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.6': + resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.3.5': + resolution: {integrity: sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@floating-ui/core@1.7.3': + resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==} + + '@floating-ui/dom@1.7.4': + resolution: {integrity: sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==} + + '@floating-ui/utils@0.2.10': + resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.7': + resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@iconify/types@2.0.0': + resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} + + '@iconify/vue@5.0.0': + resolution: {integrity: sha512-C+KuEWIF5nSBrobFJhT//JS87OZ++QDORB6f2q2Wm6fl2mueSTpFBeBsveK0KW9hWiZ4mNiPjsh6Zs4jjdROSg==} + peerDependencies: + vue: '>=3' + + '@intlify/core-base@9.14.5': + resolution: {integrity: sha512-5ah5FqZG4pOoHjkvs8mjtv+gPKYU0zCISaYNjBNNqYiaITxW8ZtVih3GS/oTOqN8d9/mDLyrjD46GBApNxmlsA==} + engines: {node: '>= 16'} + + '@intlify/message-compiler@9.14.5': + resolution: {integrity: sha512-IHzgEu61/YIpQV5Pc3aRWScDcnFKWvQA9kigcINcCBXN8mbW+vk9SK+lDxA6STzKQsVJxUPg9ACC52pKKo3SVQ==} + engines: {node: '>= 16'} + + '@intlify/shared@9.14.5': + resolution: {integrity: sha512-9gB+E53BYuAEMhbCAxVgG38EZrk59sxBtv3jSizNL2hEWlgjBjAw1AwpLHtNaeda12pe6W20OGEa0TwuMSRbyQ==} + engines: {node: '>= 16'} + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@isaacs/fs-minipass@4.0.1': + resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} + engines: {node: '>=18.0.0'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/source-map@0.3.11': + resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@keyv/bigmap@1.0.2': + resolution: {integrity: sha512-KR03xkEZlAZNF4IxXgVXb+uNIVNvwdh8UwI0cnc7WI6a+aQcDp8GL80qVfeB4E5NpsKJzou5jU0r6yLSSbMOtA==} + engines: {node: '>= 18'} + + '@keyv/serialize@1.1.1': + resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@one-ini/wasm@0.1.1': + resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==} + + '@parcel/watcher-android-arm64@2.5.1': + resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [android] + + '@parcel/watcher-darwin-arm64@2.5.1': + resolution: {integrity: sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [darwin] + + '@parcel/watcher-darwin-x64@2.5.1': + resolution: {integrity: sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [darwin] + + '@parcel/watcher-freebsd-x64@2.5.1': + resolution: {integrity: sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [freebsd] + + '@parcel/watcher-linux-arm-glibc@2.5.1': + resolution: {integrity: sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm-musl@2.5.1': + resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm64-glibc@2.5.1': + resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-arm64-musl@2.5.1': + resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-x64-glibc@2.5.1': + resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-linux-x64-musl@2.5.1': + resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-win32-arm64@2.5.1': + resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [win32] + + '@parcel/watcher-win32-ia32@2.5.1': + resolution: {integrity: sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==} + engines: {node: '>= 10.0.0'} + cpu: [ia32] + os: [win32] + + '@parcel/watcher-win32-x64@2.5.1': + resolution: {integrity: sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [win32] + + '@parcel/watcher@2.5.1': + resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==} + engines: {node: '>= 10.0.0'} + + '@pinia/testing@0.1.7': + resolution: {integrity: sha512-xcDq6Ry/kNhZ5bsUMl7DeoFXwdume1NYzDggCiDUDKoPQ6Mo0eH9VU7bJvBtlurqe6byAntWoX5IhVFqWzRz/Q==} + peerDependencies: + pinia: '>=2.2.6' + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@pkgr/core@0.2.9': + resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + + '@rolldown/pluginutils@1.0.0-beta.29': + resolution: {integrity: sha512-NIJgOsMjbxAXvoGq/X0gD7VPMQ8j9g0BiDaNjVNVjvl+iKXxL3Jre0v31RmBYeLEmkbj2s02v8vFTbUXi5XS2Q==} + + '@rollup/pluginutils@5.3.0': + resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/rollup-android-arm-eabi@4.52.3': + resolution: {integrity: sha512-h6cqHGZ6VdnwliFG1NXvMPTy/9PS3h8oLh7ImwR+kl+oYnQizgjxsONmmPSb2C66RksfkfIxEVtDSEcJiO0tqw==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.52.3': + resolution: {integrity: sha512-wd+u7SLT/u6knklV/ifG7gr5Qy4GUbH2hMWcDauPFJzmCZUAJ8L2bTkVXC2niOIxp8lk3iH/QX8kSrUxVZrOVw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.52.3': + resolution: {integrity: sha512-lj9ViATR1SsqycwFkJCtYfQTheBdvlWJqzqxwc9f2qrcVrQaF/gCuBRTiTolkRWS6KvNxSk4KHZWG7tDktLgjg==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.52.3': + resolution: {integrity: sha512-+Dyo7O1KUmIsbzx1l+4V4tvEVnVQqMOIYtrxK7ncLSknl1xnMHLgn7gddJVrYPNZfEB8CIi3hK8gq8bDhb3h5A==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.52.3': + resolution: {integrity: sha512-u9Xg2FavYbD30g3DSfNhxgNrxhi6xVG4Y6i9Ur1C7xUuGDW3banRbXj+qgnIrwRN4KeJ396jchwy9bCIzbyBEQ==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.52.3': + resolution: {integrity: sha512-5M8kyi/OX96wtD5qJR89a/3x5x8x5inXBZO04JWhkQb2JWavOWfjgkdvUqibGJeNNaz1/Z1PPza5/tAPXICI6A==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.52.3': + resolution: {integrity: sha512-IoerZJ4l1wRMopEHRKOO16e04iXRDyZFZnNZKrWeNquh5d6bucjezgd+OxG03mOMTnS1x7hilzb3uURPkJ0OfA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.52.3': + resolution: {integrity: sha512-ZYdtqgHTDfvrJHSh3W22TvjWxwOgc3ThK/XjgcNGP2DIwFIPeAPNsQxrJO5XqleSlgDux2VAoWQ5iJrtaC1TbA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.52.3': + resolution: {integrity: sha512-NcViG7A0YtuFDA6xWSgmFb6iPFzHlf5vcqb2p0lGEbT+gjrEEz8nC/EeDHvx6mnGXnGCC1SeVV+8u+smj0CeGQ==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.52.3': + resolution: {integrity: sha512-d3pY7LWno6SYNXRm6Ebsq0DJGoiLXTb83AIPCXl9fmtIQs/rXoS8SJxxUNtFbJ5MiOvs+7y34np77+9l4nfFMw==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.52.3': + resolution: {integrity: sha512-3y5GA0JkBuirLqmjwAKwB0keDlI6JfGYduMlJD/Rl7fvb4Ni8iKdQs1eiunMZJhwDWdCvrcqXRY++VEBbvk6Eg==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.52.3': + resolution: {integrity: sha512-AUUH65a0p3Q0Yfm5oD2KVgzTKgwPyp9DSXc3UA7DtxhEb/WSPfbG4wqXeSN62OG5gSo18em4xv6dbfcUGXcagw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.52.3': + resolution: {integrity: sha512-1makPhFFVBqZE+XFg3Dkq+IkQ7JvmUrwwqaYBL2CE+ZpxPaqkGaiWFEWVGyvTwZace6WLJHwjVh/+CXbKDGPmg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.52.3': + resolution: {integrity: sha512-OOFJa28dxfl8kLOPMUOQBCO6z3X2SAfzIE276fwT52uXDWUS178KWq0pL7d6p1kz7pkzA0yQwtqL0dEPoVcRWg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.52.3': + resolution: {integrity: sha512-jMdsML2VI5l+V7cKfZx3ak+SLlJ8fKvLJ0Eoa4b9/vCUrzXKgoKxvHqvJ/mkWhFiyp88nCkM5S2v6nIwRtPcgg==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.52.3': + resolution: {integrity: sha512-tPgGd6bY2M2LJTA1uGq8fkSPK8ZLYjDjY+ZLK9WHncCnfIz29LIXIqUgzCR0hIefzy6Hpbe8Th5WOSwTM8E7LA==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.52.3': + resolution: {integrity: sha512-BCFkJjgk+WFzP+tcSMXq77ymAPIxsX9lFJWs+2JzuZTLtksJ2o5hvgTdIcZ5+oKzUDMwI0PfWzRBYAydAHF2Mw==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openharmony-arm64@4.52.3': + resolution: {integrity: sha512-KTD/EqjZF3yvRaWUJdD1cW+IQBk4fbQaHYJUmP8N4XoKFZilVL8cobFSTDnjTtxWJQ3JYaMgF4nObY/+nYkumA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.52.3': + resolution: {integrity: sha512-+zteHZdoUYLkyYKObGHieibUFLbttX2r+58l27XZauq0tcWYYuKUwY2wjeCN9oK1Um2YgH2ibd6cnX/wFD7DuA==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.52.3': + resolution: {integrity: sha512-of1iHkTQSo3kr6dTIRX6t81uj/c/b15HXVsPcEElN5sS859qHrOepM5p9G41Hah+CTqSh2r8Bm56dL2z9UQQ7g==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.52.3': + resolution: {integrity: sha512-s0hybmlHb56mWVZQj8ra9048/WZTPLILKxcvcq+8awSZmyiSUZjjem1AhU3Tf4ZKpYhK4mg36HtHDOe8QJS5PQ==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.52.3': + resolution: {integrity: sha512-zGIbEVVXVtauFgl3MRwGWEN36P5ZGenHRMgNw88X5wEhEBpq0XrMEZwOn07+ICrwM17XO5xfMZqh0OldCH5VTA==} + cpu: [x64] + os: [win32] + + '@sec-ant/readable-stream@0.4.1': + resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} + + '@sindresorhus/merge-streams@4.0.0': + resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} + engines: {node: '>=18'} + + '@sxzz/popperjs-es@2.11.7': + resolution: {integrity: sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==} + + '@tailwindcss/node@4.1.14': + resolution: {integrity: sha512-hpz+8vFk3Ic2xssIA3e01R6jkmsAhvkQdXlEbRTk6S10xDAtiQiM3FyvZVGsucefq764euO/b8WUW9ysLdThHw==} + + '@tailwindcss/oxide-android-arm64@4.1.14': + resolution: {integrity: sha512-a94ifZrGwMvbdeAxWoSuGcIl6/DOP5cdxagid7xJv6bwFp3oebp7y2ImYsnZBMTwjn5Ev5xESvS3FFYUGgPODQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.1.14': + resolution: {integrity: sha512-HkFP/CqfSh09xCnrPJA7jud7hij5ahKyWomrC3oiO2U9i0UjP17o9pJbxUN0IJ471GTQQmzwhp0DEcpbp4MZTA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.1.14': + resolution: {integrity: sha512-eVNaWmCgdLf5iv6Qd3s7JI5SEFBFRtfm6W0mphJYXgvnDEAZ5sZzqmI06bK6xo0IErDHdTA5/t7d4eTfWbWOFw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.1.14': + resolution: {integrity: sha512-QWLoRXNikEuqtNb0dhQN6wsSVVjX6dmUFzuuiL09ZeXju25dsei2uIPl71y2Ic6QbNBsB4scwBoFnlBfabHkEw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.14': + resolution: {integrity: sha512-VB4gjQni9+F0VCASU+L8zSIyjrLLsy03sjcR3bM0V2g4SNamo0FakZFKyUQ96ZVwGK4CaJsc9zd/obQy74o0Fw==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.14': + resolution: {integrity: sha512-qaEy0dIZ6d9vyLnmeg24yzA8XuEAD9WjpM5nIM1sUgQ/Zv7cVkharPDQcmm/t/TvXoKo/0knI3me3AGfdx6w1w==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-musl@4.1.14': + resolution: {integrity: sha512-ISZjT44s59O8xKsPEIesiIydMG/sCXoMBCqsphDm/WcbnuWLxxb+GcvSIIA5NjUw6F8Tex7s5/LM2yDy8RqYBQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-gnu@4.1.14': + resolution: {integrity: sha512-02c6JhLPJj10L2caH4U0zF8Hji4dOeahmuMl23stk0MU1wfd1OraE7rOloidSF8W5JTHkFdVo/O7uRUJJnUAJg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-musl@4.1.14': + resolution: {integrity: sha512-TNGeLiN1XS66kQhxHG/7wMeQDOoL0S33x9BgmydbrWAb9Qw0KYdd8o1ifx4HOGDWhVmJ+Ul+JQ7lyknQFilO3Q==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-wasm32-wasi@4.1.14': + resolution: {integrity: sha512-uZYAsaW/jS/IYkd6EWPJKW/NlPNSkWkBlaeVBi/WsFQNP05/bzkebUL8FH1pdsqx4f2fH/bWFcUABOM9nfiJkQ==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.14': + resolution: {integrity: sha512-Az0RnnkcvRqsuoLH2Z4n3JfAef0wElgzHD5Aky/e+0tBUxUhIeIqFBTMNQvmMRSP15fWwmvjBxZ3Q8RhsDnxAA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.1.14': + resolution: {integrity: sha512-ttblVGHgf68kEE4om1n/n44I0yGPkCPbLsqzjvybhpwa6mKKtgFfAzy6btc3HRmuW7nHe0OOrSeNP9sQmmH9XA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.1.14': + resolution: {integrity: sha512-23yx+VUbBwCg2x5XWdB8+1lkPajzLmALEfMb51zZUBYaYVPDQvBSD/WYDqiVyBIo2BZFa3yw1Rpy3G2Jp+K0dw==} + engines: {node: '>= 10'} + + '@tailwindcss/vite@4.1.14': + resolution: {integrity: sha512-BoFUoU0XqgCUS1UXWhmDJroKKhNXeDzD7/XwabjkDIAbMnc4ULn5e2FuEuBbhZ6ENZoSYzKlzvZ44Yr6EUDUSA==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 + + '@transloadit/prettier-bytes@0.0.7': + resolution: {integrity: sha512-VeJbUb0wEKbcwaSlj5n+LscBl9IPgLPkHVGBkh00cztv6X4L/TJXK58LzFuBKX7/GAfiGhIwH67YTLTlzvIzBA==} + + '@types/conventional-commits-parser@5.0.1': + resolution: {integrity: sha512-7uz5EHdzz2TqoMfV7ee61Egf5y6NkcO4FB/1iCCQnbeiI1F3xzv3vK5dBCXUCLQgGYS+mUeigK1iKQzvED+QnQ==} + + '@types/dompurify@3.2.0': + resolution: {integrity: sha512-Fgg31wv9QbLDA0SpTOXO3MaxySc4DKGLi8sna4/Utjo4r3ZRPdCt4UQee8BWr+Q5z21yifghREPJGYaEOEIACg==} + deprecated: This is a stub types definition. dompurify provides its own type definitions, so you do not need this installed. + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/event-emitter@0.3.5': + resolution: {integrity: sha512-zx2/Gg0Eg7gwEiOIIh5w9TrhKKTeQh7CPCOPNc0el4pLSwzebA8SmnHwZs2dWlLONvyulykSwGSQxQHLhjGLvQ==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/lodash-es@4.17.12': + resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==} + + '@types/lodash@4.17.20': + resolution: {integrity: sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==} + + '@types/node@24.8.1': + resolution: {integrity: sha512-alv65KGRadQVfVcG69MuB4IzdYVpRwMG/mq8KWOaoOdyY617P5ivaDiMCGOFDWD2sAn5Q0mR3mRtUOgm99hL9Q==} + + '@types/sortablejs@1.15.8': + resolution: {integrity: sha512-b79830lW+RZfwaztgs1aVPgbasJ8e7AXtZYHTELNXZPsERt4ymJdjV4OccDbHQAvHrCcFpbF78jkm0R6h/pZVg==} + + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + + '@types/web-bluetooth@0.0.16': + resolution: {integrity: sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==} + + '@types/web-bluetooth@0.0.21': + resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} + + '@typescript-eslint/eslint-plugin@8.44.1': + resolution: {integrity: sha512-molgphGqOBT7t4YKCSkbasmu1tb1MgrZ2szGzHbclF7PNmOkSTQVHy+2jXOSnxvR3+Xe1yySHFZoqMpz3TfQsw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.44.1 + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/parser@8.44.1': + resolution: {integrity: sha512-EHrrEsyhOhxYt8MTg4zTF+DJMuNBzWwgvvOYNj/zm1vnaD/IC5zCXFehZv94Piqa2cRFfXrTFxIvO95L7Qc/cw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/project-service@8.44.1': + resolution: {integrity: sha512-ycSa60eGg8GWAkVsKV4E6Nz33h+HjTXbsDT4FILyL8Obk5/mx4tbvCNsLf9zret3ipSumAOG89UcCs/KRaKYrA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/scope-manager@8.44.1': + resolution: {integrity: sha512-NdhWHgmynpSvyhchGLXh+w12OMT308Gm25JoRIyTZqEbApiBiQHD/8xgb6LqCWCFcxFtWwaVdFsLPQI3jvhywg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.44.1': + resolution: {integrity: sha512-B5OyACouEjuIvof3o86lRMvyDsFwZm+4fBOqFHccIctYgBjqR3qT39FBYGN87khcgf0ExpdCBeGKpKRhSFTjKQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/type-utils@8.44.1': + resolution: {integrity: sha512-KdEerZqHWXsRNKjF9NYswNISnFzXfXNDfPxoTh7tqohU/PRIbwTmsjGK6V9/RTYWau7NZvfo52lgVk+sJh0K3g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/types@8.44.1': + resolution: {integrity: sha512-Lk7uj7y9uQUOEguiDIDLYLJOrYHQa7oBiURYVFqIpGxclAFQ78f6VUOM8lI2XEuNOKNB7XuvM2+2cMXAoq4ALQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.44.1': + resolution: {integrity: sha512-qnQJ+mVa7szevdEyvfItbO5Vo+GfZ4/GZWWDRRLjrxYPkhM+6zYB2vRYwCsoJLzqFCdZT4mEqyJoyzkunsZ96A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/utils@8.44.1': + resolution: {integrity: sha512-DpX5Fp6edTlocMCwA+mHY8Mra+pPjRZ0TfHkXI8QFelIKcbADQz1LUPNtzOFUriBB2UYqw4Pi9+xV4w9ZczHFg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/visitor-keys@8.44.1': + resolution: {integrity: sha512-576+u0QD+Jp3tZzvfRfxon0EA2lzcDt3lhUbsC6Lgzy9x2VR4E+JUiNyGHi5T8vk0TV+fpJ5GLG1JsJuWCaKhw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@uppy/companion-client@2.2.2': + resolution: {integrity: sha512-5mTp2iq97/mYSisMaBtFRry6PTgZA6SIL7LePteOV5x0/DxKfrZW3DEiQERJmYpHzy7k8johpm2gHnEKto56Og==} + + '@uppy/core@2.3.4': + resolution: {integrity: sha512-iWAqppC8FD8mMVqewavCz+TNaet6HPXitmGXpGGREGrakZ4FeuWytVdrelydzTdXx6vVKkOmI2FLztGg73sENQ==} + + '@uppy/store-default@2.1.1': + resolution: {integrity: sha512-xnpTxvot2SeAwGwbvmJ899ASk5tYXhmZzD/aCFsXePh/v8rNvR2pKlcQUH7cF/y4baUGq3FHO/daKCok/mpKqQ==} + + '@uppy/utils@4.1.3': + resolution: {integrity: sha512-nTuMvwWYobnJcytDO3t+D6IkVq/Qs4Xv3vyoEZ+Iaf8gegZP+rEyoaFT2CK5XLRMienPyqRqNbIfRuFaOWSIFw==} + + '@uppy/xhr-upload@2.1.3': + resolution: {integrity: sha512-YWOQ6myBVPs+mhNjfdWsQyMRWUlrDLMoaG7nvf/G6Y3GKZf8AyjFDjvvJ49XWQ+DaZOftGkHmF1uh/DBeGivJQ==} + peerDependencies: + '@uppy/core': ^2.3.3 + + '@vitejs/plugin-vue@6.0.1': + resolution: {integrity: sha512-+MaE752hU0wfPFJEUAIxqw18+20euHHdxVtMvbFcOEpjEyfqXH/5DCoTHiVJ0J29EhTJdoTkjEv5YBKU9dnoTw==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 + vue: ^3.2.25 + + '@vitest/expect@2.1.9': + resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} + + '@vitest/mocker@2.1.9': + resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@2.1.9': + resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} + + '@vitest/runner@2.1.9': + resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==} + + '@vitest/snapshot@2.1.9': + resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==} + + '@vitest/spy@2.1.9': + resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==} + + '@vitest/utils@2.1.9': + resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} + + '@volar/language-core@2.4.23': + resolution: {integrity: sha512-hEEd5ET/oSmBC6pi1j6NaNYRWoAiDhINbT8rmwtINugR39loROSlufGdYMF9TaKGfz+ViGs1Idi3mAhnuPcoGQ==} + + '@volar/source-map@2.4.23': + resolution: {integrity: sha512-Z1Uc8IB57Lm6k7q6KIDu/p+JWtf3xsXJqAX/5r18hYOTpJyBn0KXUR8oTJ4WFYOcDzWC9n3IflGgHowx6U6z9Q==} + + '@volar/typescript@2.4.23': + resolution: {integrity: sha512-lAB5zJghWxVPqfcStmAP1ZqQacMpe90UrP5RJ3arDyrhy4aCUQqmxPPLB2PWDKugvylmO41ljK7vZ+t6INMTag==} + + '@vue/babel-helper-vue-transform-on@1.5.0': + resolution: {integrity: sha512-0dAYkerNhhHutHZ34JtTl2czVQHUNWv6xEbkdF5W+Yrv5pCWsqjeORdOgbtW2I9gWlt+wBmVn+ttqN9ZxR5tzA==} + + '@vue/babel-plugin-jsx@1.5.0': + resolution: {integrity: sha512-mneBhw1oOqCd2247O0Yw/mRwC9jIGACAJUlawkmMBiNmL4dGA2eMzuNZVNqOUfYTa6vqmND4CtOPzmEEEqLKFw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + peerDependenciesMeta: + '@babel/core': + optional: true + + '@vue/babel-plugin-resolve-type@1.5.0': + resolution: {integrity: sha512-Wm/60o+53JwJODm4Knz47dxJnLDJ9FnKnGZJbUUf8nQRAtt6P+undLUAVU3Ha33LxOJe6IPoifRQ6F/0RrU31w==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@vue/compiler-core@3.5.22': + resolution: {integrity: sha512-jQ0pFPmZwTEiRNSb+i9Ow/I/cHv2tXYqsnHKKyCQ08irI2kdF5qmYedmF8si8mA7zepUFmJ2hqzS8CQmNOWOkQ==} + + '@vue/compiler-dom@3.5.22': + resolution: {integrity: sha512-W8RknzUM1BLkypvdz10OVsGxnMAuSIZs9Wdx1vzA3mL5fNMN15rhrSCLiTm6blWeACwUwizzPVqGJgOGBEN/hA==} + + '@vue/compiler-sfc@3.5.22': + resolution: {integrity: sha512-tbTR1zKGce4Lj+JLzFXDq36K4vcSZbJ1RBu8FxcDv1IGRz//Dh2EBqksyGVypz3kXpshIfWKGOCcqpSbyGWRJQ==} + + '@vue/compiler-ssr@3.5.22': + resolution: {integrity: sha512-GdgyLvg4R+7T8Nk2Mlighx7XGxq/fJf9jaVofc3IL0EPesTE86cP/8DD1lT3h1JeZr2ySBvyqKQJgbS54IX1Ww==} + + '@vue/compiler-vue2@2.7.16': + resolution: {integrity: sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==} + + '@vue/devtools-api@6.6.4': + resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==} + + '@vue/devtools-api@7.7.7': + resolution: {integrity: sha512-lwOnNBH2e7x1fIIbVT7yF5D+YWhqELm55/4ZKf45R9T8r9dE2AIOy8HKjfqzGsoTHFbWbr337O4E0A0QADnjBg==} + + '@vue/devtools-core@7.7.7': + resolution: {integrity: sha512-9z9TLbfC+AjAi1PQyWX+OErjIaJmdFlbDHcD+cAMYKY6Bh5VlsAtCeGyRMrXwIlMEQPukvnWt3gZBLwTAIMKzQ==} + peerDependencies: + vue: ^3.0.0 + + '@vue/devtools-kit@7.7.7': + resolution: {integrity: sha512-wgoZtxcTta65cnZ1Q6MbAfePVFxfM+gq0saaeytoph7nEa7yMXoi6sCPy4ufO111B9msnw0VOWjPEFCXuAKRHA==} + + '@vue/devtools-shared@7.7.7': + resolution: {integrity: sha512-+udSj47aRl5aKb0memBvcUG9koarqnxNM5yjuREvqwK6T3ap4mn3Zqqc17QrBFTqSMjr3HK1cvStEZpMDpfdyw==} + + '@vue/language-core@2.1.10': + resolution: {integrity: sha512-DAI289d0K3AB5TUG3xDp9OuQ71CnrujQwJrQnfuZDwo6eGNf0UoRlPuaVNO+Zrn65PC3j0oB2i7mNmVPggeGeQ==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@vue/reactivity@3.5.22': + resolution: {integrity: sha512-f2Wux4v/Z2pqc9+4SmgZC1p73Z53fyD90NFWXiX9AKVnVBEvLFOWCEgJD3GdGnlxPZt01PSlfmLqbLYzY/Fw4A==} + + '@vue/runtime-core@3.5.22': + resolution: {integrity: sha512-EHo4W/eiYeAzRTN5PCextDUZ0dMs9I8mQ2Fy+OkzvRPUYQEyK9yAjbasrMCXbLNhF7P0OUyivLjIy0yc6VrLJQ==} + + '@vue/runtime-dom@3.5.22': + resolution: {integrity: sha512-Av60jsryAkI023PlN7LsqrfPvwfxOd2yAwtReCjeuugTJTkgrksYJJstg1e12qle0NarkfhfFu1ox2D+cQotww==} + + '@vue/server-renderer@3.5.22': + resolution: {integrity: sha512-gXjo+ao0oHYTSswF+a3KRHZ1WszxIqO7u6XwNHqcqb9JfyIL/pbWrrh/xLv7jeDqla9u+LK7yfZKHih1e1RKAQ==} + peerDependencies: + vue: 3.5.22 + + '@vue/shared@3.5.22': + resolution: {integrity: sha512-F4yc6palwq3TT0u+FYf0Ns4Tfl9GRFURDN2gWG7L1ecIaS/4fCIuFOjMTnCyjsu/OK6vaDKLCrGAa+KvvH+h4w==} + + '@vue/test-utils@2.4.6': + resolution: {integrity: sha512-FMxEjOpYNYiFe0GkaHsnJPXFHxQ6m4t8vI/ElPGpMWxZKpmRvQ33OIrvRXemy6yha03RxhOlQuy+gZMC3CQSow==} + + '@vueuse/core@13.9.0': + resolution: {integrity: sha512-ts3regBQyURfCE2BcytLqzm8+MmLlo5Ln/KLoxDVcsZ2gzIwVNnQpQOL/UKV8alUqjSZOlpFZcRNsLRqj+OzyA==} + peerDependencies: + vue: ^3.5.0 + + '@vueuse/core@9.13.0': + resolution: {integrity: sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==} + + '@vueuse/metadata@13.9.0': + resolution: {integrity: sha512-1AFRvuiGphfF7yWixZa0KwjYH8ulyjDCC0aFgrGRz8+P4kvDFSdXLVfTk5xAN9wEuD1J6z4/myMoYbnHoX07zg==} + + '@vueuse/metadata@9.13.0': + resolution: {integrity: sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==} + + '@vueuse/shared@13.9.0': + resolution: {integrity: sha512-e89uuTLMh0U5cZ9iDpEI2senqPGfbPRTHM/0AaQkcxnpqjkZqDYP8rpfm7edOz8s+pOCOROEy1PIveSW8+fL5g==} + peerDependencies: + vue: ^3.5.0 + + '@vueuse/shared@9.13.0': + resolution: {integrity: sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==} + + '@wangeditor/basic-modules@1.1.7': + resolution: {integrity: sha512-cY9CPkLJaqF05STqfpZKWG4LpxTMeGSIIF1fHvfm/mz+JXatCagjdkbxdikOuKYlxDdeqvOeBmsUBItufDLXZg==} + peerDependencies: + '@wangeditor/core': 1.x + dom7: ^3.0.0 + lodash.throttle: ^4.1.1 + nanoid: ^3.2.0 + slate: ^0.72.0 + snabbdom: ^3.1.0 + + '@wangeditor/code-highlight@1.0.3': + resolution: {integrity: sha512-iazHwO14XpCuIWJNTQTikqUhGKyqj+dUNWJ9288Oym9M2xMVHvnsOmDU2sgUDWVy+pOLojReMPgXCsvvNlOOhw==} + peerDependencies: + '@wangeditor/core': 1.x + dom7: ^3.0.0 + slate: ^0.72.0 + snabbdom: ^3.1.0 + + '@wangeditor/core@1.1.19': + resolution: {integrity: sha512-KevkB47+7GhVszyYF2pKGKtCSj/YzmClsD03C3zTt+9SR2XWT5T0e3yQqg8baZpcMvkjs1D8Dv4fk8ok/UaS2Q==} + peerDependencies: + '@uppy/core': ^2.1.1 + '@uppy/xhr-upload': ^2.0.3 + dom7: ^3.0.0 + is-hotkey: ^0.2.0 + lodash.camelcase: ^4.3.0 + lodash.clonedeep: ^4.5.0 + lodash.debounce: ^4.0.8 + lodash.foreach: ^4.5.0 + lodash.isequal: ^4.5.0 + lodash.throttle: ^4.1.1 + lodash.toarray: ^4.4.0 + nanoid: ^3.2.0 + slate: ^0.72.0 + snabbdom: ^3.1.0 + + '@wangeditor/editor-for-vue@5.1.12': + resolution: {integrity: sha512-0Ds3D8I+xnpNWezAeO7HmPRgTfUxHLMd9JKcIw+QzvSmhC5xUHbpCcLU+KLmeBKTR/zffnS5GQo6qi3GhTMJWQ==} + peerDependencies: + '@wangeditor/editor': '>=5.1.0' + vue: ^3.0.5 + + '@wangeditor/editor@5.1.23': + resolution: {integrity: sha512-0RxfeVTuK1tktUaPROnCoFfaHVJpRAIE2zdS0mpP+vq1axVQpLjM8+fCvKzqYIkH0Pg+C+44hJpe3VVroSkEuQ==} + + '@wangeditor/list-module@1.0.5': + resolution: {integrity: sha512-uDuYTP6DVhcYf7mF1pTlmNn5jOb4QtcVhYwSSAkyg09zqxI1qBqsfUnveeDeDqIuptSJhkh81cyxi+MF8sEPOQ==} + peerDependencies: + '@wangeditor/core': 1.x + dom7: ^3.0.0 + slate: ^0.72.0 + snabbdom: ^3.1.0 + + '@wangeditor/table-module@1.1.4': + resolution: {integrity: sha512-5saanU9xuEocxaemGdNi9t8MCDSucnykEC6jtuiT72kt+/Hhh4nERYx1J20OPsTCCdVr7hIyQenFD1iSRkIQ6w==} + peerDependencies: + '@wangeditor/core': 1.x + dom7: ^3.0.0 + lodash.isequal: ^4.5.0 + lodash.throttle: ^4.1.1 + nanoid: ^3.2.0 + slate: ^0.72.0 + snabbdom: ^3.1.0 + + '@wangeditor/upload-image-module@1.0.2': + resolution: {integrity: sha512-z81lk/v71OwPDYeQDxj6cVr81aDP90aFuywb8nPD6eQeECtOymrqRODjpO6VGvCVxVck8nUxBHtbxKtjgcwyiA==} + peerDependencies: + '@uppy/core': ^2.0.3 + '@uppy/xhr-upload': ^2.0.3 + '@wangeditor/basic-modules': 1.x + '@wangeditor/core': 1.x + dom7: ^3.0.0 + lodash.foreach: ^4.5.0 + slate: ^0.72.0 + snabbdom: ^3.1.0 + + '@wangeditor/video-module@1.1.4': + resolution: {integrity: sha512-ZdodDPqKQrgx3IwWu4ZiQmXI8EXZ3hm2/fM6E3t5dB8tCaIGWQZhmqd6P5knfkRAd3z2+YRSRbxOGfoRSp/rLg==} + peerDependencies: + '@uppy/core': ^2.1.4 + '@uppy/xhr-upload': ^2.0.7 + '@wangeditor/core': 1.x + dom7: ^3.0.0 + nanoid: ^3.2.0 + slate: ^0.72.0 + snabbdom: ^3.1.0 + + JSONStream@1.3.5: + resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==} + hasBin: true + + abbrev@2.0.0: + resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + adler-32@1.3.1: + resolution: {integrity: sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==} + engines: {node: '>=0.8'} + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + + alien-signals@0.2.2: + resolution: {integrity: sha512-cZIRkbERILsBOXTQmMrxc9hgpxglstn69zm+F1ARf4aPAzdAFYd6sBq87ErO0Fj3DV94tglcyHG5kQz9nDC/8A==} + + ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + + ansi-escapes@7.1.1: + resolution: {integrity: sha512-Zhl0ErHcSRUaVfGUeUdDuLgpkEo8KIFjB4Y9uAc46ScOpdDiU1Dbyplh7qWJeJ/ZHpbyMSM26+X3BySgnIz40Q==} + engines: {node: '>=18'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + array-ify@1.0.0: + resolution: {integrity: sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==} + + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + astral-regex@2.0.0: + resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} + engines: {node: '>=8'} + + async-validator@4.2.5: + resolution: {integrity: sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + at-least-node@1.0.0: + resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} + engines: {node: '>= 4.0.0'} + + axios@1.12.2: + resolution: {integrity: sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@2.0.0: + resolution: {integrity: sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + baseline-browser-mapping@2.8.8: + resolution: {integrity: sha512-be0PUaPsQX/gPWWgFsdD+GFzaoig5PXaUC1xLkQiYdDnANU8sMnHoQd8JhbJQuvTWrWLyeFN9Imb5Qtfvr4RrQ==} + hasBin: true + + bignumber.js@9.3.1: + resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + birpc@2.6.1: + resolution: {integrity: sha512-LPnFhlDpdSH6FJhJyn4M0kFO7vtQ5iPw24FnG0y21q09xC7e8+1LeR31S1MAIrDAHp4m7aas4bEkTDTvMAtebQ==} + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.26.2: + resolution: {integrity: sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + bundle-name@4.1.0: + resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} + engines: {node: '>=18'} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + cacheable@2.0.2: + resolution: {integrity: sha512-dWjhLx8RWnPsAWVKwW/wI6OJpQ/hSVb1qS0NUif8TR9vRiSwci7Gey8x04kRU9iAF+Rnbtex5Kjjfg/aB5w8Pg==} + + cachedir@2.3.0: + resolution: {integrity: sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw==} + engines: {node: '>=6'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + caniuse-lite@1.0.30001745: + resolution: {integrity: sha512-ywt6i8FzvdgrrrGbr1jZVObnVv6adj+0if2/omv9cmR2oiZs30zL4DIyaptKcbOrBdOIc74QTMoJvSE2QHh5UQ==} + + cfb@1.2.2: + resolution: {integrity: sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==} + engines: {node: '>=0.8'} + + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + + chalk@2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + chardet@0.7.0: + resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} + + check-error@2.1.1: + resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} + engines: {node: '>= 16'} + + china-division@2.7.0: + resolution: {integrity: sha512-4uUPAT+1WfqDh5jytq7omdCmHNk3j+k76zEG/2IqaGcYB90c2SwcixttcypdsZ3T/9tN1TTpBDoeZn+Yw/qBEA==} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + chownr@3.0.0: + resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} + engines: {node: '>=18'} + + cli-cursor@3.1.0: + resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} + engines: {node: '>=8'} + + cli-cursor@5.0.0: + resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} + engines: {node: '>=18'} + + cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} + + cli-truncate@4.0.0: + resolution: {integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==} + engines: {node: '>=18'} + + cli-width@3.0.0: + resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==} + engines: {node: '>= 10'} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + clone@1.0.4: + resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} + engines: {node: '>=0.8'} + + codepage@1.15.0: + resolution: {integrity: sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==} + engines: {node: '>=0.8'} + + color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + colord@2.9.3: + resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} + + colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + commander@10.0.1: + resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} + engines: {node: '>=14'} + + commander@13.1.0: + resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==} + engines: {node: '>=18'} + + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + + commitizen@4.3.1: + resolution: {integrity: sha512-gwAPAVTy/j5YcOOebcCRIijn+mSjWJC+IYKivTu6aG8Ei/scoXgfsMRnuAk6b0GRste2J4NGxVdMN3ZpfNaVaw==} + engines: {node: '>= 12'} + hasBin: true + + compare-func@2.0.0: + resolution: {integrity: sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==} + + compute-scroll-into-view@1.0.20: + resolution: {integrity: sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + confbox@0.2.2: + resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==} + + config-chain@1.1.13: + resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} + + conventional-changelog-angular@7.0.0: + resolution: {integrity: sha512-ROjNchA9LgfNMTTFSIWPzebCwOGFdgkEq45EnvvrmSLvCtAw0HSmrCs7/ty+wAeYUZyNay0YMUNYFTRL72PkBQ==} + engines: {node: '>=16'} + + conventional-changelog-conventionalcommits@7.0.2: + resolution: {integrity: sha512-NKXYmMR/Hr1DevQegFB4MwfM5Vv0m4UIxKZTTYuD98lpTknaZlSRrDOG4X7wIXpGkfsYxZTghUN+Qq+T0YQI7w==} + engines: {node: '>=16'} + + conventional-commit-types@3.0.0: + resolution: {integrity: sha512-SmmCYnOniSsAa9GqWOeLqc179lfr5TRu5b4QFDkbsrJ5TZjPJx85wtOr3zn+1dbeNiXDKGPbZ72IKbPhLXh/Lg==} + + conventional-commits-parser@5.0.0: + resolution: {integrity: sha512-ZPMl0ZJbw74iS9LuX9YIAiW8pfM5p3yh2o/NbXHbkFuZzY5jvdi5jFycEOkmBW5H5I7nA+D6f3UcsCLP2vvSEA==} + engines: {node: '>=16'} + hasBin: true + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + copy-anything@3.0.5: + resolution: {integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==} + engines: {node: '>=12.13'} + + core-js@3.45.1: + resolution: {integrity: sha512-L4NPsJlCfZsPeXukyzHFlg/i7IIVwHSItR0wg0FLNqYClJ4MQYTYLbC7EkjKYRLZF2iof2MUgN0EGy7MdQFChg==} + + cosmiconfig-typescript-loader@6.1.0: + resolution: {integrity: sha512-tJ1w35ZRUiM5FeTzT7DtYWAFFv37ZLqSRkGi2oeCK1gPhvaWjkAtfXvLmvE1pRfxxp9aQo6ba/Pvg1dKj05D4g==} + engines: {node: '>=v18'} + peerDependencies: + '@types/node': '*' + cosmiconfig: '>=9' + typescript: '>=5' + + cosmiconfig@9.0.0: + resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + + crc-32@1.2.2: + resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} + engines: {node: '>=0.8'} + hasBin: true + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + crypto-js@4.2.0: + resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==} + + css-functions-list@3.2.3: + resolution: {integrity: sha512-IQOkD3hbR5KrN93MtcYuad6YPuTSUhntLHDuLEbFWE+ff2/XSZNdZG+LcbbIW5AXKg/WFIfYItIzVoHngHXZzA==} + engines: {node: '>=12 || >=16'} + + css-tree@3.1.0: + resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + cssstyle@4.6.0: + resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==} + engines: {node: '>=18'} + + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + + cz-conventional-changelog@3.3.0: + resolution: {integrity: sha512-U466fIzU5U22eES5lTNiNbZ+d8dfcHcssH4o7QsdWaCcRs/feIPCxKYSWkYBNs5mny7MvEfwpTLWjvbm94hecw==} + engines: {node: '>= 10'} + + cz-git@1.12.0: + resolution: {integrity: sha512-LaZ+8whPPUOo6Y0Zy4nIbf6JOleV3ejp41sT6N4RPKiKKA+ICWf4ueeIlxIO8b6JtdlDxRzHH/EcRji07nDxcg==} + engines: {node: '>=v12.20.0'} + + d@1.0.2: + resolution: {integrity: sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==} + engines: {node: '>=0.12'} + + danmu.js@1.1.13: + resolution: {integrity: sha512-knFd0/cB2HA4FFWiA7eB2suc5vCvoHdqio33FyyCSfP7C+1A+zQcTvnvwfxaZhrxsGj4qaQI2I8XiTqedRaVmg==} + + dargs@8.1.0: + resolution: {integrity: sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw==} + engines: {node: '>=12'} + + data-urls@5.0.0: + resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} + engines: {node: '>=18'} + + dayjs@1.11.18: + resolution: {integrity: sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==} + + de-indent@1.0.2: + resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + + dedent@0.7.0: + resolution: {integrity: sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==} + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + deep-pick-omit@1.2.1: + resolution: {integrity: sha512-2J6Kc/m3irCeqVG42T+SaUMesaK7oGWaedGnQQK/+O0gYc+2SP5bKh/KKTE7d7SJ+GCA9UUE1GRzh6oDe0EnGw==} + + default-browser-id@5.0.0: + resolution: {integrity: sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==} + engines: {node: '>=18'} + + default-browser@5.2.1: + resolution: {integrity: sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==} + engines: {node: '>=18'} + + defaults@1.0.4: + resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + + define-lazy-prop@2.0.0: + resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} + engines: {node: '>=8'} + + define-lazy-prop@3.0.0: + resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} + engines: {node: '>=12'} + + defu@6.1.4: + resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + delegate@3.2.0: + resolution: {integrity: sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==} + + destr@2.0.5: + resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + + detect-file@1.0.0: + resolution: {integrity: sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==} + engines: {node: '>=0.10.0'} + + detect-indent@6.1.0: + resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} + engines: {node: '>=8'} + + detect-libc@1.0.3: + resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} + engines: {node: '>=0.10'} + hasBin: true + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + dom7@3.0.0: + resolution: {integrity: sha512-oNlcUdHsC4zb7Msx7JN3K0Nro1dzJ48knvBOnDPKJ2GV9wl1i5vydJZUSyOfrkKFDZEud/jBsTk92S/VGSAe/g==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + + dompurify@3.3.1: + resolution: {integrity: sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==} + + domutils@3.2.2: + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + + dot-prop@5.3.0: + resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} + engines: {node: '>=8'} + + downloadjs@1.4.7: + resolution: {integrity: sha512-LN1gO7+u9xjU5oEScGFKvXhYf7Y/empUIIEAGBs1LzUq/rg5duiDrkuH5A2lQGd5jfMOb9X9usDa2oVXwJ0U/Q==} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + echarts@6.0.0: + resolution: {integrity: sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==} + + editorconfig@1.0.4: + resolution: {integrity: sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==} + engines: {node: '>=14'} + hasBin: true + + electron-to-chromium@1.5.227: + resolution: {integrity: sha512-ITxuoPfJu3lsNWUi2lBM2PaBPYgH3uqmxut5vmBxgYvyI4AlJ6P3Cai1O76mOrkJCBzq0IxWg/NtqOrpu/0gKA==} + + element-china-area-data@6.1.0: + resolution: {integrity: sha512-IkpcjwQv2A/2AxFiSoaISZ+oMw1rZCPUSOg5sOCwT5jKc96TaawmKZeY81xfxXsO0QbKxU5LLc6AirhG52hUmg==} + + element-plus@2.11.4: + resolution: {integrity: sha512-sLq+Ypd0cIVilv8wGGMEGvzRVBBsRpJjnAS5PsI/1JU1COZXqzH3N1UYMUc/HCdvdjf6dfrBy80Sj7KcACsT7w==} + peerDependencies: + vue: ^3.2.0 + + emoji-regex@10.5.0: + resolution: {integrity: sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + enhanced-resolve@5.18.3: + resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==} + engines: {node: '>=10.13.0'} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + + environment@1.1.0: + resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} + engines: {node: '>=18'} + + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + + error-stack-parser-es@0.1.5: + resolution: {integrity: sha512-xHku1X40RO+fO8yJ8Wh2f2rZWVjqyhb1zgq1yZ8aZRQkv6OOKhKWRUaht3eSCUbAOBaKIgM+ykwFLE+QUxgGeg==} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + es5-ext@0.10.64: + resolution: {integrity: sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==} + engines: {node: '>=0.10'} + + es6-iterator@2.0.3: + resolution: {integrity: sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==} + + es6-symbol@3.1.4: + resolution: {integrity: sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==} + engines: {node: '>=0.12'} + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.25.10: + resolution: {integrity: sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + + eslint-config-prettier@9.1.2: + resolution: {integrity: sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-plugin-prettier@5.5.4: + resolution: {integrity: sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + '@types/eslint': '>=8.0.0' + eslint: '>=8.0.0' + eslint-config-prettier: '>= 7.0.0 <10.0.0 || >=10.1.0' + prettier: '>=3.0.0' + peerDependenciesMeta: + '@types/eslint': + optional: true + eslint-config-prettier: + optional: true + + eslint-plugin-vue@9.33.0: + resolution: {integrity: sha512-174lJKuNsuDIlLpjeXc5E2Tss8P44uIimAfGD0b90k0NoirJqpG7stLuU9Vp/9ioTOrQdWVREc4mRd1BD+CvGw==} + engines: {node: ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 + + eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@9.36.0: + resolution: {integrity: sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + esniff@2.0.1: + resolution: {integrity: sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==} + engines: {node: '>=0.10'} + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + event-emitter@0.3.5: + resolution: {integrity: sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==} + + eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + + eventemitter3@5.0.1: + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + + execa@8.0.1: + resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} + engines: {node: '>=16.17'} + + execa@9.6.0: + resolution: {integrity: sha512-jpWzZ1ZhwUmeWRhS7Qv3mhpOhLfwI+uAX4e5fOcXqwMR7EcJ0pj2kV1CVzHVMX/LphnKWD3LObjZCoJ71lKpHw==} + engines: {node: ^18.19.0 || >=20.5.0} + + expand-tilde@2.0.2: + resolution: {integrity: sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==} + engines: {node: '>=0.10.0'} + + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + + exsolve@1.0.7: + resolution: {integrity: sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==} + + ext@1.7.0: + resolution: {integrity: sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==} + + external-editor@3.1.0: + resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} + engines: {node: '>=4'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-diff@1.3.0: + resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + + fastest-levenshtein@1.0.16: + resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==} + engines: {node: '>= 4.9.1'} + + fastq@1.19.1: + resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + figures@3.2.0: + resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} + engines: {node: '>=8'} + + figures@6.1.0: + resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} + engines: {node: '>=18'} + + file-entry-cache@10.1.4: + resolution: {integrity: sha512-5XRUFc0WTtUbjfGzEwXc42tiGxQHBmtbUG1h9L2apu4SulCGN3Hqm//9D6FAolf8MYNL7f/YlJl9vy08pj5JuA==} + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + file-saver@2.0.5: + resolution: {integrity: sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-node-modules@2.1.3: + resolution: {integrity: sha512-UC2I2+nx1ZuOBclWVNdcnbDR5dlrOdVb7xNjmT/lHE+LsgztWks3dG7boJ37yTS/venXw84B/mAW9uHVoC5QRg==} + + find-root@1.1.0: + resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + find-up@7.0.0: + resolution: {integrity: sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==} + engines: {node: '>=18'} + + findup-sync@4.0.0: + resolution: {integrity: sha512-6jvvn/12IC4quLBL1KNokxC7wWTvYncaVUYSoxWw7YykPLuRrnv4qdHcSOywOI5RpkOVGeQRtWM8/q+G6W6qfQ==} + engines: {node: '>= 8'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flat-cache@6.1.14: + resolution: {integrity: sha512-ExZSCSV9e7v/Zt7RzCbX57lY2dnPdxzU/h3UE6WJ6NtEMfwBd8jmi1n4otDEUfz+T/R+zxrFDpICFdjhD3H/zw==} + + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + + follow-redirects@1.15.11: + resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + form-data@4.0.4: + resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} + engines: {node: '>= 6'} + + frac@1.1.2: + resolution: {integrity: sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==} + engines: {node: '>=0.8'} + + fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + + fs-extra@11.3.2: + resolution: {integrity: sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==} + engines: {node: '>=14.14'} + + fs-extra@9.1.0: + resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} + engines: {node: '>=10'} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-east-asian-width@1.4.0: + resolution: {integrity: sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==} + engines: {node: '>=18'} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-stream@8.0.1: + resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} + engines: {node: '>=16'} + + get-stream@9.0.1: + resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} + engines: {node: '>=18'} + + get-tsconfig@4.10.1: + resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==} + + git-raw-commits@4.0.0: + resolution: {integrity: sha512-ICsMM1Wk8xSGMowkOmPrzo2Fgmfo4bMHLNX6ytHjajRJUqvHOw/TFapQ+QG75c3X/tTDDhOSRPGC52dDbNM8FQ==} + engines: {node: '>=16'} + hasBin: true + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@10.5.0: + resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + hasBin: true + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + + global-directory@4.0.1: + resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==} + engines: {node: '>=18'} + + global-modules@1.0.0: + resolution: {integrity: sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==} + engines: {node: '>=0.10.0'} + + global-modules@2.0.0: + resolution: {integrity: sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==} + engines: {node: '>=6'} + + global-prefix@1.0.2: + resolution: {integrity: sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==} + engines: {node: '>=0.10.0'} + + global-prefix@3.0.0: + resolution: {integrity: sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==} + engines: {node: '>=6'} + + globals@13.24.0: + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} + engines: {node: '>=8'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globals@15.15.0: + resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==} + engines: {node: '>=18'} + + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + + globjoin@0.1.4: + resolution: {integrity: sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + + highlight.js@11.11.1: + resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==} + engines: {node: '>=12.0.0'} + + homedir-polyfill@1.0.3: + resolution: {integrity: sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==} + engines: {node: '>=0.10.0'} + + hookable@5.5.3: + resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + + hookified@1.12.1: + resolution: {integrity: sha512-xnKGl+iMIlhrZmGHB729MqlmPoWBznctSQTYCpFKqNsCgimJQmithcW0xSQMMFzYnV2iKUh25alswn6epgxS0Q==} + + html-encoding-sniffer@4.0.0: + resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} + engines: {node: '>=18'} + + html-tags@3.3.1: + resolution: {integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==} + engines: {node: '>=8'} + + html-void-elements@2.0.1: + resolution: {integrity: sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==} + + htmlparser2@8.0.2: + resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + human-signals@5.0.0: + resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} + engines: {node: '>=16.17.0'} + + human-signals@8.0.1: + resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==} + engines: {node: '>=18.18.0'} + + husky@9.1.7: + resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} + engines: {node: '>=18'} + hasBin: true + + i18next@20.6.1: + resolution: {integrity: sha512-yCMYTMEJ9ihCwEQQ3phLo7I/Pwycf8uAx+sRHwwk5U9Aui/IZYgQRyMqXafQOw5QQ7DM1Z+WyEXWIqSuJHhG2A==} + + iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + immer@9.0.21: + resolution: {integrity: sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==} + + immutable@5.1.3: + resolution: {integrity: sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg==} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + import-meta-resolve@4.2.0: + resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + ini@4.1.1: + resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + inquirer@8.2.5: + resolution: {integrity: sha512-QAgPDQMEgrDssk1XiwwHoOGYF9BAbUcc1+j+FhEvaOt8/cKRqyLn0U5qA6F74fGhTMGxf92pOvPBeh29jQJDTQ==} + engines: {node: '>=12.0.0'} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-docker@2.2.1: + resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} + engines: {node: '>=8'} + hasBin: true + + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-fullwidth-code-point@4.0.0: + resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==} + engines: {node: '>=12'} + + is-fullwidth-code-point@5.1.0: + resolution: {integrity: sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==} + engines: {node: '>=18'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-hotkey@0.2.0: + resolution: {integrity: sha512-UknnZK4RakDmTgz4PI1wIph5yxSs/mvChWs9ifnlXsKuXgWmOkY/hAE0H/k2MIqH0RlRye0i1oC07MCRSD28Mw==} + + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + + is-interactive@1.0.0: + resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} + engines: {node: '>=8'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-obj@2.0.0: + resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} + engines: {node: '>=8'} + + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + + is-plain-object@5.0.0: + resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} + engines: {node: '>=0.10.0'} + + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + + is-stream@3.0.0: + resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + is-stream@4.0.1: + resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} + engines: {node: '>=18'} + + is-text-path@2.0.0: + resolution: {integrity: sha512-+oDTluR6WEjdXEJMnC2z6A4FRwFoYuvShVVEGsS7ewc0UTi2QtAKMDJuL4BDEVt+5T7MjFo12RP8ghOM75oKJw==} + engines: {node: '>=8'} + + is-unicode-supported@0.1.0: + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} + engines: {node: '>=10'} + + is-unicode-supported@2.1.0: + resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} + engines: {node: '>=18'} + + is-url@1.2.4: + resolution: {integrity: sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==} + + is-utf8@0.2.1: + resolution: {integrity: sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q==} + + is-what@4.1.16: + resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==} + engines: {node: '>=12.13'} + + is-windows@1.0.2: + resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} + engines: {node: '>=0.10.0'} + + is-wsl@2.2.0: + resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} + engines: {node: '>=8'} + + is-wsl@3.1.0: + resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} + engines: {node: '>=16'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + jiti@2.6.0: + resolution: {integrity: sha512-VXe6RjJkBPj0ohtqaO8vSWP3ZhAKo66fKrFNCll4BTcwljPLz03pCbaNKfzGP5MbrCYcbJ7v0nOYYwUzTEIdXQ==} + hasBin: true + + js-beautify@1.15.4: + resolution: {integrity: sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==} + engines: {node: '>=14'} + hasBin: true + + js-cookie@3.0.5: + resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==} + engines: {node: '>=14'} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + jsdom@24.1.3: + resolution: {integrity: sha512-MyL55p3Ut3cXbeBEG7Hcv0mVM8pp8PBNWxRqchZnSfAiES1v1mRnMeFfaHWIPULpwsYfvO+ZmMZz5tGCnjzDUQ==} + engines: {node: '>=18'} + peerDependencies: + canvas: ^2.11.2 + peerDependenciesMeta: + canvas: + optional: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-bigint@1.0.0: + resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonfile@6.2.0: + resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} + + jsonparse@1.3.1: + resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} + engines: {'0': node >= 0.2.0} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + keyv@5.5.3: + resolution: {integrity: sha512-h0Un1ieD+HUrzBH6dJXhod3ifSghk5Hw/2Y4/KHBziPlZecrFyE9YOTPU6eOs0V9pYl8gOs86fkr/KN8lUX39A==} + + kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + + known-css-properties@0.36.0: + resolution: {integrity: sha512-A+9jP+IUmuQsNdsLdcg6Yt7voiMF/D4K83ew0OpJtpu+l34ef7LaohWV0Rc6KNvzw6ZDizkqfyB5JznZnzuKQA==} + + known-css-properties@0.37.0: + resolution: {integrity: sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ==} + + kolorist@1.8.0: + resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lightningcss-darwin-arm64@1.30.1: + resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.30.1: + resolution: {integrity: sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.30.1: + resolution: {integrity: sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.30.1: + resolution: {integrity: sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.30.1: + resolution: {integrity: sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.30.1: + resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.30.1: + resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.30.1: + resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.30.1: + resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.30.1: + resolution: {integrity: sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.30.1: + resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==} + engines: {node: '>= 12.0.0'} + + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + lint-staged@15.5.2: + resolution: {integrity: sha512-YUSOLq9VeRNAo/CTaVmhGDKG+LBtA8KF1X4K5+ykMSwWST1vDxJRB2kv2COgLb1fvpCo+A/y9A0G0znNVmdx4w==} + engines: {node: '>=18.12.0'} + hasBin: true + + listr2@8.3.3: + resolution: {integrity: sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ==} + engines: {node: '>=18.0.0'} + + local-pkg@1.1.2: + resolution: {integrity: sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==} + engines: {node: '>=14'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + locate-path@7.2.0: + resolution: {integrity: sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + lodash-es@4.17.21: + resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} + + lodash-unified@1.0.3: + resolution: {integrity: sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==} + peerDependencies: + '@types/lodash-es': '*' + lodash: '*' + lodash-es: '*' + + lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + + lodash.clonedeep@4.5.0: + resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==} + + lodash.debounce@4.0.8: + resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} + + lodash.foreach@4.5.0: + resolution: {integrity: sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ==} + + lodash.isequal@4.5.0: + resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} + deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead. + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.kebabcase@4.1.1: + resolution: {integrity: sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==} + + lodash.map@4.6.0: + resolution: {integrity: sha512-worNHGKLDetmcEYDvh2stPCrrQRkP20E4l0iIS7F8EvzMqBBi7ltvFN5m1HvTf1P7Jk1txKhvFcmYsCr8O2F1Q==} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash.mergewith@4.6.2: + resolution: {integrity: sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==} + + lodash.snakecase@4.1.1: + resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==} + + lodash.startcase@4.4.0: + resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} + + lodash.throttle@4.1.1: + resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==} + + lodash.toarray@4.4.0: + resolution: {integrity: sha512-QyffEA3i5dma5q2490+SgCvDN0pXLmRGSyAANuVi0HQ01Pkfr9fuoKQW8wm1wGBnJITs/mS7wQvS6VshUEBFCw==} + + lodash.truncate@4.4.2: + resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==} + + lodash.uniq@4.5.0: + resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} + + lodash.upperfirst@4.3.1: + resolution: {integrity: sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + log-symbols@4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} + + log-update@6.1.0: + resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} + engines: {node: '>=18'} + + longest@2.0.1: + resolution: {integrity: sha512-Ajzxb8CM6WAnFjgiloPsI3bF+WCxcvhdIG3KNA2KN962+tdBsHcuQ4k4qX/EcS/2CRkcc0iAkR956Nib6aXU/Q==} + engines: {node: '>=0.10.0'} + + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + magic-string@0.30.19: + resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mathml-tag-names@2.1.3: + resolution: {integrity: sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==} + + mdn-data@2.12.2: + resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} + + mdn-data@2.24.0: + resolution: {integrity: sha512-i97fklrJl03tL1tdRVw0ZfLLvuDsdb6wxL+TrJ+PKkCbLrp2PCu2+OYdCKychIUm19nSM/35S6qz7pJpnXttoA==} + + memoize-one@6.0.0: + resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==} + + meow@12.1.1: + resolution: {integrity: sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==} + engines: {node: '>=16.10'} + + meow@13.2.0: + resolution: {integrity: sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==} + engines: {node: '>=18'} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + merge@2.1.1: + resolution: {integrity: sha512-jz+Cfrg9GWOZbQAnDQ4hlVnQky+341Yk5ru8bZSe6sIDTCIg8n9i/u7hSQGSVOF3C7lH6mGtqjkiT9G4wFLL0w==} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-match@1.0.2: + resolution: {integrity: sha512-VXp/ugGDVh3eCLOBCiHZMYWQaTNUHv2IJrut+yXA6+JbLPXHglHwfS/5A5L0ll+jkCY7fIzRJcH6OIunF+c6Cg==} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + mimic-fn@4.0.0: + resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} + engines: {node: '>=12'} + + mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@9.0.1: + resolution: {integrity: sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==} + engines: {node: '>=16 || 14 >=14.17'} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.7: + resolution: {integrity: sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + minizlib@3.1.0: + resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} + engines: {node: '>= 18'} + + mitt@3.0.1: + resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + + mlly@1.8.0: + resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} + + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + muggle-string@0.4.1: + resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} + + mute-stream@0.0.8: + resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} + + namespace-emitter@2.0.1: + resolution: {integrity: sha512-N/sMKHniSDJBjfrkbS/tpkPj4RAbvW3mr8UAzvlMHyun93XEm83IAvhWtJVHo+RHn/oO8Job5YN4b+wRjSVp5g==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + nanoid@5.1.6: + resolution: {integrity: sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==} + engines: {node: ^18 || >=20} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + next-tick@1.1.0: + resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==} + + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + + node-releases@2.0.21: + resolution: {integrity: sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==} + + nopt@7.2.1: + resolution: {integrity: sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + hasBin: true + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + normalize-wheel-es@1.2.0: + resolution: {integrity: sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==} + + npm-run-path@5.3.0: + resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + npm-run-path@6.0.0: + resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} + engines: {node: '>=18'} + + nprogress@0.2.0: + resolution: {integrity: sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==} + + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + + nwsapi@2.2.23: + resolution: {integrity: sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==} + + ohash@2.0.11: + resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + onetime@6.0.0: + resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} + engines: {node: '>=12'} + + onetime@7.0.0: + resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} + engines: {node: '>=18'} + + open@10.2.0: + resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} + engines: {node: '>=18'} + + open@8.4.2: + resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} + engines: {node: '>=12'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + ora@5.4.1: + resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} + engines: {node: '>=10'} + + os-tmpdir@1.0.2: + resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} + engines: {node: '>=0.10.0'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-limit@4.0.0: + resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + p-locate@6.0.0: + resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + parse-ms@4.0.0: + resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} + engines: {node: '>=18'} + + parse-passwd@1.0.0: + resolution: {integrity: sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==} + engines: {node: '>=0.10.0'} + + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-exists@5.0.0: + resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + + perfect-debounce@1.0.0: + resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + pidtree@0.6.0: + resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==} + engines: {node: '>=0.10'} + hasBin: true + + pinia-plugin-persistedstate@4.5.0: + resolution: {integrity: sha512-QTkP1xJVyCdr2I2p3AKUZM84/e+IS+HktRxKGAIuDzkyaKKV48mQcYkJFVVDuvTxlI5j6X3oZObpqoVB8JnWpw==} + peerDependencies: + '@nuxt/kit': '>=3.0.0' + '@pinia/nuxt': '>=0.10.0' + pinia: '>=3.0.0' + peerDependenciesMeta: + '@nuxt/kit': + optional: true + '@pinia/nuxt': + optional: true + pinia: + optional: true + + pinia@3.0.3: + resolution: {integrity: sha512-ttXO/InUULUXkMHpTdp9Fj4hLpD/2AoJdmAbAeW2yu1iy1k+pkFekQXw5VpC0/5p51IOR/jDaDRfRWRnMMsGOA==} + peerDependencies: + typescript: '>=4.4.4' + vue: ^2.7.0 || ^3.5.11 + peerDependenciesMeta: + typescript: + optional: true + + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + pkg-types@2.3.0: + resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} + + postcss-html@1.8.0: + resolution: {integrity: sha512-5mMeb1TgLWoRKxZ0Xh9RZDfwUUIqRrcxO2uXO+Ezl1N5lqpCiSU5Gk6+1kZediBfBHFtPCdopr2UZ2SgUsKcgQ==} + engines: {node: ^12 || >=14} + + postcss-media-query-parser@0.2.3: + resolution: {integrity: sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==} + + postcss-resolve-nested-selector@0.1.6: + resolution: {integrity: sha512-0sglIs9Wmkzbr8lQwEyIzlDOOC9bGmfVKcJTaxv3vMmd3uo4o4DerC3En0bnmgceeql9BfC8hRkp7cg0fjdVqw==} + + postcss-safe-parser@6.0.0: + resolution: {integrity: sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.3.3 + + postcss-safe-parser@7.0.1: + resolution: {integrity: sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==} + engines: {node: '>=18.0'} + peerDependencies: + postcss: ^8.4.31 + + postcss-scss@4.0.9: + resolution: {integrity: sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.4.29 + + postcss-selector-parser@6.1.2: + resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} + engines: {node: '>=4'} + + postcss-selector-parser@7.1.0: + resolution: {integrity: sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==} + engines: {node: '>=4'} + + postcss-sorting@8.0.2: + resolution: {integrity: sha512-M9dkSrmU00t/jK7rF6BZSZauA5MAaBW4i5EnJXspMwt4iqTh/L9j6fgMnbElEOfyRyfLfVbIHj/R52zHzAPe1Q==} + peerDependencies: + postcss: ^8.4.20 + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + preact@10.27.2: + resolution: {integrity: sha512-5SYSgFKSyhCbk6SrXyMpqjb5+MQBgfvEKE/OC+PujcY34sOpqtr+0AZQtPYx5IA6VxynQ7rUPCtKzyovpj9Bpg==} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier-linter-helpers@1.0.0: + resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} + engines: {node: '>=6.0.0'} + + prettier@3.6.2: + resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} + engines: {node: '>=14'} + hasBin: true + + pretty-ms@9.3.0: + resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} + engines: {node: '>=18'} + + prismjs@1.30.0: + resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} + engines: {node: '>=6'} + + proto-list@1.2.4: + resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + psl@1.15.0: + resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + qrcode.vue@3.6.0: + resolution: {integrity: sha512-vQcl2fyHYHMjDO1GguCldJxepq2izQjBkDEEu9NENgfVKP6mv/e2SU62WbqYHGwTgWXLhxZ1NCD1dAZKHQq1fg==} + peerDependencies: + vue: ^3.0.0 + + quansync@0.2.11: + resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + + querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + + resolve-dir@1.0.1: + resolution: {integrity: sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==} + engines: {node: '>=0.10.0'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + restore-cursor@3.1.0: + resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} + engines: {node: '>=8'} + + restore-cursor@5.1.0: + resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} + engines: {node: '>=18'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + + rollup-plugin-visualizer@5.14.0: + resolution: {integrity: sha512-VlDXneTDaKsHIw8yzJAFWtrzguoJ/LnQ+lMpoVfYJ3jJF4Ihe5oYLAqLklIK/35lgUY+1yEzCkHyZ1j4A5w5fA==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + rolldown: 1.x + rollup: 2.x || 3.x || 4.x + peerDependenciesMeta: + rolldown: + optional: true + rollup: + optional: true + + rollup@4.52.3: + resolution: {integrity: sha512-RIDh866U8agLgiIcdpB+COKnlCreHJLfIhWC3LVflku5YHfpnsIKigRZeFfMfCc4dVcqNVfQQ5gO/afOck064A==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + rrweb-cssom@0.7.1: + resolution: {integrity: sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==} + + rrweb-cssom@0.8.0: + resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} + + run-applescript@7.1.0: + resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} + engines: {node: '>=18'} + + run-async@2.4.1: + resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} + engines: {node: '>=0.12.0'} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + sass@1.93.2: + resolution: {integrity: sha512-t+YPtOQHpGW1QWsh1CHQ5cPIr9lbbGZLZnbihP/D/qZj/yuV68m8qarcV17nvkOX81BCrvzAlq2klCQFZghyTg==} + engines: {node: '>=14.0.0'} + hasBin: true + + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + + scroll-into-view-if-needed@2.2.31: + resolution: {integrity: sha512-dGCXy99wZQivjmjIqihaBQNjryrz5rueJY7eHfTdyWEiR4ttYpsajb14rn9s5d4DY4EcY6+4+U/maARBXJedkA==} + + scule@1.3.0: + resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.2: + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + engines: {node: '>=10'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + sirv@3.0.2: + resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} + engines: {node: '>=18'} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + slate-history@0.66.0: + resolution: {integrity: sha512-6MWpxGQZiMvSINlCbMW43E2YBSVMCMCIwQfBzGssjWw4kb0qfvj0pIdblWNRQZD0hR6WHP+dHHgGSeVdMWzfng==} + peerDependencies: + slate: '>=0.65.3' + + slate@0.72.8: + resolution: {integrity: sha512-/nJwTswQgnRurpK+bGJFH1oM7naD5qDmHd89JyiKNT2oOKD8marW0QSBtuFnwEbL5aGCS8AmrhXQgNOsn4osAw==} + + slice-ansi@4.0.0: + resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} + engines: {node: '>=10'} + + slice-ansi@5.0.0: + resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==} + engines: {node: '>=12'} + + slice-ansi@7.1.2: + resolution: {integrity: sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==} + engines: {node: '>=18'} + + snabbdom@3.6.2: + resolution: {integrity: sha512-ig5qOnCDbugFntKi6c7Xlib8bA6xiJVk8O+WdFrV3wxbMqeHO0hXFQC4nAhPVWfZfi8255lcZkNhtIBINCc4+Q==} + engines: {node: '>=12.17.0'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} + + speakingurl@14.0.1: + resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==} + engines: {node: '>=0.10.0'} + + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + ssf@0.11.2: + resolution: {integrity: sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==} + engines: {node: '>=0.8'} + + ssr-window@3.0.0: + resolution: {integrity: sha512-q+8UfWDg9Itrg0yWK7oe5p/XRCJpJF9OBtXfOPgSJl+u3Xd5KI328RUEvUqSMVM9CiQUEf1QdBzJMkYGErj9QA==} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + string-argv@0.3.2: + resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} + engines: {node: '>=0.6.19'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.1.2: + resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} + engines: {node: '>=12'} + + strip-bom@4.0.0: + resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} + engines: {node: '>=8'} + + strip-final-newline@3.0.0: + resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} + engines: {node: '>=12'} + + strip-final-newline@4.0.0: + resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} + engines: {node: '>=18'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + strip-literal@3.1.0: + resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + + stylelint-config-html@1.1.0: + resolution: {integrity: sha512-IZv4IVESjKLumUGi+HWeb7skgO6/g4VMuAYrJdlqQFndgbj6WJAXPhaysvBiXefX79upBdQVumgYcdd17gCpjQ==} + engines: {node: ^12 || >=14} + peerDependencies: + postcss-html: ^1.0.0 + stylelint: '>=14.0.0' + + stylelint-config-recess-order@4.6.0: + resolution: {integrity: sha512-V76fhv3YtcNXh/hyAuAdSzi5FmcrG54Mp2AThJ3D/PTMTSYzUPd7GIhP6z9mTqnRhmkk6YTfcu/JWB8h+Yrcaw==} + peerDependencies: + stylelint: '>=15' + + stylelint-config-recommended-scss@14.1.0: + resolution: {integrity: sha512-bhaMhh1u5dQqSsf6ri2GVWWQW5iUjBYgcHkh7SgDDn92ijoItC/cfO/W+fpXshgTQWhwFkP1rVcewcv4jaftRg==} + engines: {node: '>=18.12.0'} + peerDependencies: + postcss: ^8.3.3 + stylelint: ^16.6.1 + peerDependenciesMeta: + postcss: + optional: true + + stylelint-config-recommended-vue@1.6.1: + resolution: {integrity: sha512-lLW7hTIMBiTfjenGuDq2kyHA6fBWd/+Df7MO4/AWOxiFeXP9clbpKgg27kHfwA3H7UNMGC7aeP3mNlZB5LMmEQ==} + engines: {node: ^12 || >=14} + peerDependencies: + postcss-html: ^1.0.0 + stylelint: '>=14.0.0' + + stylelint-config-recommended@14.0.1: + resolution: {integrity: sha512-bLvc1WOz/14aPImu/cufKAZYfXs/A/owZfSMZ4N+16WGXLoX5lOir53M6odBxvhgmgdxCVnNySJmZKx73T93cg==} + engines: {node: '>=18.12.0'} + peerDependencies: + stylelint: ^16.1.0 + + stylelint-config-recommended@17.0.0: + resolution: {integrity: sha512-WaMSdEiPfZTSFVoYmJbxorJfA610O0tlYuU2aEwY33UQhSPgFbClrVJYWvy3jGJx+XW37O+LyNLiZOEXhKhJmA==} + engines: {node: '>=18.12.0'} + peerDependencies: + stylelint: ^16.23.0 + + stylelint-config-standard@36.0.1: + resolution: {integrity: sha512-8aX8mTzJ6cuO8mmD5yon61CWuIM4UD8Q5aBcWKGSf6kg+EC3uhB+iOywpTK4ca6ZL7B49en8yanOFtUW0qNzyw==} + engines: {node: '>=18.12.0'} + peerDependencies: + stylelint: ^16.1.0 + + stylelint-order@6.0.4: + resolution: {integrity: sha512-0UuKo4+s1hgQ/uAxlYU4h0o0HS4NiQDud0NAUNI0aa8FJdmYHA5ZZTFHiV5FpmE3071e9pZx5j0QpVJW5zOCUA==} + peerDependencies: + stylelint: ^14.0.0 || ^15.0.0 || ^16.0.1 + + stylelint-scss@6.12.1: + resolution: {integrity: sha512-UJUfBFIvXfly8WKIgmqfmkGKPilKB4L5j38JfsDd+OCg2GBdU0vGUV08Uw82tsRZzd4TbsUURVVNGeOhJVF7pA==} + engines: {node: '>=18.12.0'} + peerDependencies: + stylelint: ^16.0.2 + + stylelint@16.24.0: + resolution: {integrity: sha512-7ksgz3zJaSbTUGr/ujMXvLVKdDhLbGl3R/3arNudH7z88+XZZGNLMTepsY28WlnvEFcuOmUe7fg40Q3lfhOfSQ==} + engines: {node: '>=18.12.0'} + hasBin: true + + superjson@2.2.2: + resolution: {integrity: sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==} + engines: {node: '>=16'} + + supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-hyperlinks@3.2.0: + resolution: {integrity: sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==} + engines: {node: '>=14.18'} + + svg-tags@1.0.0: + resolution: {integrity: sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==} + + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + + synckit@0.11.11: + resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==} + engines: {node: ^14.18.0 || >=16.0.0} + + table@6.9.0: + resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==} + engines: {node: '>=10.0.0'} + + tailwindcss@4.1.14: + resolution: {integrity: sha512-b7pCxjGO98LnxVkKjaZSDeNuljC4ueKUddjENJOADtubtdo8llTaJy7HwBMeLNSSo2N5QIAgklslK1+Ir8r6CA==} + + tapable@2.3.0: + resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} + engines: {node: '>=6'} + + tar@7.5.1: + resolution: {integrity: sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==} + engines: {node: '>=18'} + + terser@5.44.0: + resolution: {integrity: sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==} + engines: {node: '>=10'} + hasBin: true + + text-extensions@2.4.0: + resolution: {integrity: sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==} + engines: {node: '>=8'} + + through@2.3.8: + resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + + tiny-warning@1.0.3: + resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyexec@1.0.1: + resolution: {integrity: sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@1.2.0: + resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} + engines: {node: '>=14.0.0'} + + tinyspy@3.0.2: + resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} + engines: {node: '>=14.0.0'} + + tlbs-map-vue@1.3.2: + resolution: {integrity: sha512-CIRhxWPpLz0A2yCylRkYRj6LOEhGU9mV6pWil2XTAs+Yo/7EN+aqn+QM/4Y5TJLpN7yjMjhDDx8cCkfje5iatA==} + engines: {node: '>=16.0.0'} + peerDependencies: + '@vue/composition-api': ^1.4.9 + vue: ^2.6.0 || >=3.0.0 + peerDependenciesMeta: + '@vue/composition-api': + optional: true + + tmp@0.0.33: + resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} + engines: {node: '>=0.6.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + + tough-cookie@4.1.4: + resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} + engines: {node: '>=6'} + + tr46@5.1.1: + resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} + engines: {node: '>=18'} + + ts-api-utils@2.1.0: + resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + tslib@2.3.0: + resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsx@4.20.6: + resolution: {integrity: sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==} + engines: {node: '>=18.0.0'} + hasBin: true + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + + type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + + type@2.7.3: + resolution: {integrity: sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==} + + typescript-eslint@8.44.1: + resolution: {integrity: sha512-0ws8uWGrUVTjEeN2OM4K1pLKHK/4NiNP/vz6ns+LjT/6sqpaYzIVFajZb1fj/IDwpsrrHb3Jy0Qm5u9CPcKaeg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + typescript@5.6.3: + resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==} + engines: {node: '>=14.17'} + hasBin: true + + ufo@1.6.1: + resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} + + undici-types@7.14.0: + resolution: {integrity: sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==} + + unicorn-magic@0.1.0: + resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} + engines: {node: '>=18'} + + unicorn-magic@0.3.0: + resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} + engines: {node: '>=18'} + + unimport@5.4.0: + resolution: {integrity: sha512-g/OLFZR2mEfqbC6NC9b2225eCJGvufxq34mj6kM3OmI5gdSL0qyqtnv+9qmsGpAmnzSl6x0IWZj4W+8j2hLkMA==} + engines: {node: '>=18.12.0'} + + universalify@0.2.0: + resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} + engines: {node: '>= 4.0.0'} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + unplugin-auto-import@20.2.0: + resolution: {integrity: sha512-vfBI/SvD9hJqYNinipVOAj5n8dS8DJXFlCKFR5iLDp2SaQwsfdnfLXgZ+34Kd3YY3YEY9omk8XQg0bwos3Q8ug==} + engines: {node: '>=14'} + peerDependencies: + '@nuxt/kit': ^4.0.0 + '@vueuse/core': '*' + peerDependenciesMeta: + '@nuxt/kit': + optional: true + '@vueuse/core': + optional: true + + unplugin-element-plus@0.10.0: + resolution: {integrity: sha512-oRSW0x6U58xBOWKy8TcoVZNA8ElIpfp3TUJRLQI6ey/E9PpjHl9/deeTAZNt8D57Li4OA4pCJtM6p2cb4Ff4ZA==} + engines: {node: '>=18.12.0'} + + unplugin-utils@0.2.5: + resolution: {integrity: sha512-gwXJnPRewT4rT7sBi/IvxKTjsms7jX7QIDLOClApuZwR49SXbrB1z2NLUZ+vDHyqCj/n58OzRRqaW+B8OZi8vg==} + engines: {node: '>=18.12.0'} + + unplugin-utils@0.3.0: + resolution: {integrity: sha512-JLoggz+PvLVMJo+jZt97hdIIIZ2yTzGgft9e9q8iMrC4ewufl62ekeW7mixBghonn2gVb/ICjyvlmOCUBnJLQg==} + engines: {node: '>=20.19.0'} + + unplugin-vue-components@29.1.0: + resolution: {integrity: sha512-z/9ACPXth199s9aCTCdKZAhe5QGOpvzJYP+Hkd0GN1/PpAmsu+W3UlRY3BJAewPqQxh5xi56+Og6mfiCV1Jzpg==} + engines: {node: '>=14'} + peerDependencies: + '@babel/parser': ^7.15.8 + '@nuxt/kit': ^3.2.2 || ^4.0.0 + vue: 2 || 3 + peerDependenciesMeta: + '@babel/parser': + optional: true + '@nuxt/kit': + optional: true + + unplugin@2.3.10: + resolution: {integrity: sha512-6NCPkv1ClwH+/BGE9QeoTIl09nuiAt0gS28nn1PvYXsGKRwM2TCbFA2QiilmehPDTXIe684k4rZI1yl3A1PCUw==} + engines: {node: '>=18.12.0'} + + update-browserslist-db@1.1.3: + resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + vite-hot-client@2.1.0: + resolution: {integrity: sha512-7SpgZmU7R+dDnSmvXE1mfDtnHLHQSisdySVR7lO8ceAXvM0otZeuQQ6C8LrS5d/aYyP/QZ0hI0L+dIPrm4YlFQ==} + peerDependencies: + vite: ^2.6.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 + + vite-node@2.1.9: + resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + + vite-plugin-compression@0.5.1: + resolution: {integrity: sha512-5QJKBDc+gNYVqL/skgFAP81Yuzo9R+EAf19d+EtsMF/i8kFUpNi3J/H01QD3Oo8zBQn+NzoCIFkpPLynoOzaJg==} + peerDependencies: + vite: '>=2.0.0' + + vite-plugin-inspect@0.8.9: + resolution: {integrity: sha512-22/8qn+LYonzibb1VeFZmISdVao5kC22jmEKm24vfFE8siEn47EpVcCLYMv6iKOYMJfjSvSJfueOwcFCkUnV3A==} + engines: {node: '>=14'} + peerDependencies: + '@nuxt/kit': '*' + vite: ^3.1.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.1 + peerDependenciesMeta: + '@nuxt/kit': + optional: true + + vite-plugin-vue-devtools@7.7.7: + resolution: {integrity: sha512-d0fIh3wRcgSlr4Vz7bAk4va1MkdqhQgj9ANE/rBhsAjOnRfTLs2ocjFMvSUOsv6SRRXU9G+VM7yMgqDb6yI4iQ==} + engines: {node: '>=v14.21.3'} + peerDependencies: + vite: ^3.1.0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 + + vite-plugin-vue-inspector@5.3.2: + resolution: {integrity: sha512-YvEKooQcSiBTAs0DoYLfefNja9bLgkFM7NI2b07bE2SruuvX0MEa9cMaxjKVMkeCp5Nz9FRIdcN1rOdFVBeL6Q==} + peerDependencies: + vite: ^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 + + vite@5.4.21: + resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + vite@7.1.7: + resolution: {integrity: sha512-VbA8ScMvAISJNJVbRDTJdCwqQoAareR/wutevKanhR2/1EkoXVZVkkORaYm/tNVCjP/UDTKtcw3bAkwOUdedmA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@2.1.9: + resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 2.1.9 + '@vitest/ui': 2.1.9 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + vscode-uri@3.1.0: + resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} + + vue-component-type-helpers@2.2.12: + resolution: {integrity: sha512-YbGqHZ5/eW4SnkPNR44mKVc6ZKQoRs/Rux1sxC6rdwXb4qpbOSYfDr9DsTHolOTGmIKgM9j141mZbBeg05R1pw==} + + vue-demi@0.14.10: + resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==} + engines: {node: '>=12'} + hasBin: true + peerDependencies: + '@vue/composition-api': ^1.0.0-rc.1 + vue: ^3.0.0-0 || ^2.6.0 + peerDependenciesMeta: + '@vue/composition-api': + optional: true + + vue-draggable-plus@0.6.0: + resolution: {integrity: sha512-G5TSfHrt9tX9EjdG49InoFJbt2NYk0h3kgjgKxkFWr3ulIUays0oFObr5KZ8qzD4+QnhtALiRwIqY6qul4egqw==} + peerDependencies: + '@types/sortablejs': ^1.15.0 + '@vue/composition-api': '*' + peerDependenciesMeta: + '@vue/composition-api': + optional: true + + vue-eslint-parser@9.4.3: + resolution: {integrity: sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==} + engines: {node: ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '>=6.0.0' + + vue-i18n@9.14.5: + resolution: {integrity: sha512-0jQ9Em3ymWngyiIkj0+c/k7WgaPO+TNzjKSNq9BvBQaKJECqn9cd9fL4tkDhB5G1QBskGl9YxxbDAhgbFtpe2g==} + engines: {node: '>= 16'} + peerDependencies: + vue: ^3.0.0 + + vue-img-cutter@3.0.7: + resolution: {integrity: sha512-fNw3kimawg9XVXDZCw2bI74NI+Jq+H42wjymatZVVSY46wuBty6LbQsu4GeVfo/yzpS9AHY0tzckpYzX3D2fmA==} + + vue-router@4.5.1: + resolution: {integrity: sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==} + peerDependencies: + vue: ^3.2.0 + + vue-tsc@2.1.10: + resolution: {integrity: sha512-RBNSfaaRHcN5uqVqJSZh++Gy/YUzryuv9u1aFWhsammDJXNtUiJMNoJ747lZcQ68wUQFx6E73y4FY3D8E7FGMA==} + hasBin: true + peerDependencies: + typescript: '>=5.0.0' + + vue@3.5.22: + resolution: {integrity: sha512-toaZjQ3a/G/mYaLSbV+QsQhIdMo9x5rrqIpYRObsJ6T/J+RyCSFwN2LHNVH9v8uIcljDNa3QzPVdv3Y6b9hAJQ==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + + wcwidth@1.0.1: + resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + + webpack-virtual-modules@0.6.2: + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + + whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation + + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + + whatwg-url@14.2.0: + resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} + engines: {node: '>=18'} + + which@1.3.1: + resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} + hasBin: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + wildcard@1.1.2: + resolution: {integrity: sha512-DXukZJxpHA8LuotRwL0pP1+rS6CS7FF2qStDDE1C7DDg2rLud2PXRMuEDYIPhgEezwnlHNL4c+N6MfMTjCGTng==} + + wmf@1.0.2: + resolution: {integrity: sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==} + engines: {node: '>=0.8'} + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + word@0.3.0: + resolution: {integrity: sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==} + engines: {node: '>=0.8'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + wrap-ansi@9.0.2: + resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} + engines: {node: '>=18'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + write-file-atomic@5.0.1: + resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + ws@8.18.3: + resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + wsl-utils@0.1.0: + resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==} + engines: {node: '>=18'} + + xgplayer-subtitles@3.0.23: + resolution: {integrity: sha512-deGdV75giVzfTTdG9XATmji39NHwKTpEelWt2rRx/RyXGgU2bQFp0Ft7yWaK2Uu8A/WVrP5fpxEAj4MstREMkQ==} + peerDependencies: + core-js: '>=3.12.1' + + xgplayer@3.0.23: + resolution: {integrity: sha512-Bn3zQfMMAZimlVG9EeIDybMcklc+6FH8Sv47KpTq4K6ofCzyhPG/KenxailDedlHmxjb5B2o+240TpJtMQ3oJA==} + peerDependencies: + core-js: '>=3.12.1' + + xlsx@0.18.5: + resolution: {integrity: sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==} + engines: {node: '>=0.8'} + hasBin: true + + xml-name-validator@4.0.0: + resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} + engines: {node: '>=12'} + + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yallist@5.0.0: + resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} + engines: {node: '>=18'} + + yaml@2.8.1: + resolution: {integrity: sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==} + engines: {node: '>= 14.6'} + hasBin: true + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + yocto-queue@1.2.1: + resolution: {integrity: sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==} + engines: {node: '>=12.20'} + + yoctocolors@2.1.2: + resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} + engines: {node: '>=18'} + + zrender@6.0.0: + resolution: {integrity: sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==} + +snapshots: + + '@antfu/utils@0.7.10': {} + + '@asamuzakjp/css-color@3.2.0': + dependencies: + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + lru-cache: 10.4.3 + + '@babel/code-frame@7.27.1': + dependencies: + '@babel/helper-validator-identifier': 7.27.1 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.28.4': {} + + '@babel/core@7.28.4': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.3 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.4) + '@babel/helpers': 7.28.4 + '@babel/parser': 7.28.4 + '@babel/template': 7.27.2 + '@babel/traverse': 7.28.4 + '@babel/types': 7.28.4 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.28.3': + dependencies: + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-annotate-as-pure@7.27.3': + dependencies: + '@babel/types': 7.28.4 + + '@babel/helper-compilation-targets@7.27.2': + dependencies: + '@babel/compat-data': 7.28.4 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.26.2 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-create-class-features-plugin@7.28.3(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-member-expression-to-functions': 7.27.1 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.4) + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/traverse': 7.28.4 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-member-expression-to-functions@7.27.1': + dependencies: + '@babel/traverse': 7.28.4 + '@babel/types': 7.28.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-imports@7.27.1': + dependencies: + '@babel/traverse': 7.28.4 + '@babel/types': 7.28.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@babel/traverse': 7.28.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-optimise-call-expression@7.27.1': + dependencies: + '@babel/types': 7.28.4 + + '@babel/helper-plugin-utils@7.27.1': {} + + '@babel/helper-replace-supers@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-member-expression-to-functions': 7.27.1 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/traverse': 7.28.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + dependencies: + '@babel/traverse': 7.28.4 + '@babel/types': 7.28.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.27.1': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.28.4': + dependencies: + '@babel/template': 7.27.2 + '@babel/types': 7.28.4 + + '@babel/parser@7.28.4': + dependencies: + '@babel/types': 7.28.4 + + '@babel/plugin-proposal-decorators@7.28.0(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-create-class-features-plugin': 7.28.3(@babel/core@7.28.4) + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-decorators': 7.27.1(@babel/core@7.28.4) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-syntax-decorators@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-import-attributes@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-typescript@7.28.0(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-create-class-features-plugin': 7.28.3(@babel/core@7.28.4) + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.4) + transitivePeerDependencies: + - supports-color + + '@babel/runtime@7.28.4': {} + + '@babel/template@7.27.2': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 + + '@babel/traverse@7.28.4': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.3 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.28.4 + '@babel/template': 7.27.2 + '@babel/types': 7.28.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.28.4': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + + '@cacheable/memoize@2.0.2': + dependencies: + '@cacheable/utils': 2.0.2 + + '@cacheable/memory@2.0.2': + dependencies: + '@cacheable/memoize': 2.0.2 + '@cacheable/utils': 2.0.2 + '@keyv/bigmap': 1.0.2 + hookified: 1.12.1 + keyv: 5.5.3 + + '@cacheable/utils@2.0.2': {} + + '@commitlint/cli@19.8.1(@types/node@24.8.1)(typescript@5.6.3)': + dependencies: + '@commitlint/format': 19.8.1 + '@commitlint/lint': 19.8.1 + '@commitlint/load': 19.8.1(@types/node@24.8.1)(typescript@5.6.3) + '@commitlint/read': 19.8.1 + '@commitlint/types': 19.8.1 + tinyexec: 1.0.1 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - typescript + + '@commitlint/config-conventional@19.8.1': + dependencies: + '@commitlint/types': 19.8.1 + conventional-changelog-conventionalcommits: 7.0.2 + + '@commitlint/config-validator@19.8.1': + dependencies: + '@commitlint/types': 19.8.1 + ajv: 8.17.1 + + '@commitlint/config-validator@20.0.0': + dependencies: + '@commitlint/types': 20.0.0 + ajv: 8.17.1 + optional: true + + '@commitlint/ensure@19.8.1': + dependencies: + '@commitlint/types': 19.8.1 + lodash.camelcase: 4.3.0 + lodash.kebabcase: 4.1.1 + lodash.snakecase: 4.1.1 + lodash.startcase: 4.4.0 + lodash.upperfirst: 4.3.1 + + '@commitlint/execute-rule@19.8.1': {} + + '@commitlint/execute-rule@20.0.0': + optional: true + + '@commitlint/format@19.8.1': + dependencies: + '@commitlint/types': 19.8.1 + chalk: 5.6.2 + + '@commitlint/is-ignored@19.8.1': + dependencies: + '@commitlint/types': 19.8.1 + semver: 7.7.2 + + '@commitlint/lint@19.8.1': + dependencies: + '@commitlint/is-ignored': 19.8.1 + '@commitlint/parse': 19.8.1 + '@commitlint/rules': 19.8.1 + '@commitlint/types': 19.8.1 + + '@commitlint/load@19.8.1(@types/node@24.8.1)(typescript@5.6.3)': + dependencies: + '@commitlint/config-validator': 19.8.1 + '@commitlint/execute-rule': 19.8.1 + '@commitlint/resolve-extends': 19.8.1 + '@commitlint/types': 19.8.1 + chalk: 5.6.2 + cosmiconfig: 9.0.0(typescript@5.6.3) + cosmiconfig-typescript-loader: 6.1.0(@types/node@24.8.1)(cosmiconfig@9.0.0(typescript@5.6.3))(typescript@5.6.3) + lodash.isplainobject: 4.0.6 + lodash.merge: 4.6.2 + lodash.uniq: 4.5.0 + transitivePeerDependencies: + - '@types/node' + - typescript + + '@commitlint/load@20.0.0(@types/node@24.8.1)(typescript@5.6.3)': + dependencies: + '@commitlint/config-validator': 20.0.0 + '@commitlint/execute-rule': 20.0.0 + '@commitlint/resolve-extends': 20.0.0 + '@commitlint/types': 20.0.0 + chalk: 5.6.2 + cosmiconfig: 9.0.0(typescript@5.6.3) + cosmiconfig-typescript-loader: 6.1.0(@types/node@24.8.1)(cosmiconfig@9.0.0(typescript@5.6.3))(typescript@5.6.3) + lodash.isplainobject: 4.0.6 + lodash.merge: 4.6.2 + lodash.uniq: 4.5.0 + transitivePeerDependencies: + - '@types/node' + - typescript + optional: true + + '@commitlint/message@19.8.1': {} + + '@commitlint/parse@19.8.1': + dependencies: + '@commitlint/types': 19.8.1 + conventional-changelog-angular: 7.0.0 + conventional-commits-parser: 5.0.0 + + '@commitlint/read@19.8.1': + dependencies: + '@commitlint/top-level': 19.8.1 + '@commitlint/types': 19.8.1 + git-raw-commits: 4.0.0 + minimist: 1.2.8 + tinyexec: 1.0.1 + + '@commitlint/resolve-extends@19.8.1': + dependencies: + '@commitlint/config-validator': 19.8.1 + '@commitlint/types': 19.8.1 + global-directory: 4.0.1 + import-meta-resolve: 4.2.0 + lodash.mergewith: 4.6.2 + resolve-from: 5.0.0 + + '@commitlint/resolve-extends@20.0.0': + dependencies: + '@commitlint/config-validator': 20.0.0 + '@commitlint/types': 20.0.0 + global-directory: 4.0.1 + import-meta-resolve: 4.2.0 + lodash.mergewith: 4.6.2 + resolve-from: 5.0.0 + optional: true + + '@commitlint/rules@19.8.1': + dependencies: + '@commitlint/ensure': 19.8.1 + '@commitlint/message': 19.8.1 + '@commitlint/to-lines': 19.8.1 + '@commitlint/types': 19.8.1 + + '@commitlint/to-lines@19.8.1': {} + + '@commitlint/top-level@19.8.1': + dependencies: + find-up: 7.0.0 + + '@commitlint/types@19.8.1': + dependencies: + '@types/conventional-commits-parser': 5.0.1 + chalk: 5.6.2 + + '@commitlint/types@20.0.0': + dependencies: + '@types/conventional-commits-parser': 5.0.1 + chalk: 5.6.2 + optional: true + + '@csstools/color-helpers@5.1.0': {} + + '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/color-helpers': 5.1.0 + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-tokenizer@3.0.4': {} + + '@csstools/media-query-list-parser@4.0.3(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/selector-specificity@5.0.0(postcss-selector-parser@7.1.0)': + dependencies: + postcss-selector-parser: 7.1.0 + + '@ctrl/tinycolor@3.6.1': {} + + '@dual-bundle/import-meta-resolve@4.2.1': {} + + '@element-plus/icons-vue@2.3.2(vue@3.5.22(typescript@5.6.3))': + dependencies: + vue: 3.5.22(typescript@5.6.3) + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/aix-ppc64@0.25.10': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.25.10': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-arm@0.25.10': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/android-x64@0.25.10': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.25.10': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.25.10': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.25.10': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.25.10': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.25.10': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-arm@0.25.10': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.25.10': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.25.10': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.25.10': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.25.10': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.25.10': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.25.10': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/linux-x64@0.25.10': + optional: true + + '@esbuild/netbsd-arm64@0.25.10': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.25.10': + optional: true + + '@esbuild/openbsd-arm64@0.25.10': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.25.10': + optional: true + + '@esbuild/openharmony-arm64@0.25.10': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.25.10': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.25.10': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.25.10': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@esbuild/win32-x64@0.25.10': + optional: true + + '@eslint-community/eslint-utils@4.9.0(eslint@9.36.0(jiti@2.6.0))': + dependencies: + eslint: 9.36.0(jiti@2.6.0) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.1': {} + + '@eslint/config-array@0.21.0': + dependencies: + '@eslint/object-schema': 2.1.6 + debug: 4.4.3 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.3.1': {} + + '@eslint/core@0.15.2': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.1': + dependencies: + ajv: 6.12.6 + debug: 4.4.3 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.36.0': {} + + '@eslint/object-schema@2.1.6': {} + + '@eslint/plugin-kit@0.3.5': + dependencies: + '@eslint/core': 0.15.2 + levn: 0.4.1 + + '@floating-ui/core@1.7.3': + dependencies: + '@floating-ui/utils': 0.2.10 + + '@floating-ui/dom@1.7.4': + dependencies: + '@floating-ui/core': 1.7.3 + '@floating-ui/utils': 0.2.10 + + '@floating-ui/utils@0.2.10': {} + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.7': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.4.3 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@iconify/types@2.0.0': {} + + '@iconify/vue@5.0.0(vue@3.5.22(typescript@5.6.3))': + dependencies: + '@iconify/types': 2.0.0 + vue: 3.5.22(typescript@5.6.3) + + '@intlify/core-base@9.14.5': + dependencies: + '@intlify/message-compiler': 9.14.5 + '@intlify/shared': 9.14.5 + + '@intlify/message-compiler@9.14.5': + dependencies: + '@intlify/shared': 9.14.5 + source-map-js: 1.2.1 + + '@intlify/shared@9.14.5': {} + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.2 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@isaacs/fs-minipass@4.0.1': + dependencies: + minipass: 7.1.2 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/source-map@0.3.11': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@keyv/bigmap@1.0.2': + dependencies: + hookified: 1.12.1 + + '@keyv/serialize@1.1.1': {} + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.19.1 + + '@one-ini/wasm@0.1.1': {} + + '@parcel/watcher-android-arm64@2.5.1': + optional: true + + '@parcel/watcher-darwin-arm64@2.5.1': + optional: true + + '@parcel/watcher-darwin-x64@2.5.1': + optional: true + + '@parcel/watcher-freebsd-x64@2.5.1': + optional: true + + '@parcel/watcher-linux-arm-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-arm-musl@2.5.1': + optional: true + + '@parcel/watcher-linux-arm64-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-arm64-musl@2.5.1': + optional: true + + '@parcel/watcher-linux-x64-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-x64-musl@2.5.1': + optional: true + + '@parcel/watcher-win32-arm64@2.5.1': + optional: true + + '@parcel/watcher-win32-ia32@2.5.1': + optional: true + + '@parcel/watcher-win32-x64@2.5.1': + optional: true + + '@parcel/watcher@2.5.1': + dependencies: + detect-libc: 1.0.3 + is-glob: 4.0.3 + micromatch: 4.0.8 + node-addon-api: 7.1.1 + optionalDependencies: + '@parcel/watcher-android-arm64': 2.5.1 + '@parcel/watcher-darwin-arm64': 2.5.1 + '@parcel/watcher-darwin-x64': 2.5.1 + '@parcel/watcher-freebsd-x64': 2.5.1 + '@parcel/watcher-linux-arm-glibc': 2.5.1 + '@parcel/watcher-linux-arm-musl': 2.5.1 + '@parcel/watcher-linux-arm64-glibc': 2.5.1 + '@parcel/watcher-linux-arm64-musl': 2.5.1 + '@parcel/watcher-linux-x64-glibc': 2.5.1 + '@parcel/watcher-linux-x64-musl': 2.5.1 + '@parcel/watcher-win32-arm64': 2.5.1 + '@parcel/watcher-win32-ia32': 2.5.1 + '@parcel/watcher-win32-x64': 2.5.1 + optional: true + + '@pinia/testing@0.1.7(pinia@3.0.3(typescript@5.6.3)(vue@3.5.22(typescript@5.6.3)))(vue@3.5.22(typescript@5.6.3))': + dependencies: + pinia: 3.0.3(typescript@5.6.3)(vue@3.5.22(typescript@5.6.3)) + vue-demi: 0.14.10(vue@3.5.22(typescript@5.6.3)) + transitivePeerDependencies: + - '@vue/composition-api' + - vue + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@pkgr/core@0.2.9': {} + + '@polka/url@1.0.0-next.29': {} + + '@rolldown/pluginutils@1.0.0-beta.29': {} + + '@rollup/pluginutils@5.3.0(rollup@4.52.3)': + dependencies: + '@types/estree': 1.0.8 + estree-walker: 2.0.2 + picomatch: 4.0.3 + optionalDependencies: + rollup: 4.52.3 + + '@rollup/rollup-android-arm-eabi@4.52.3': + optional: true + + '@rollup/rollup-android-arm64@4.52.3': + optional: true + + '@rollup/rollup-darwin-arm64@4.52.3': + optional: true + + '@rollup/rollup-darwin-x64@4.52.3': + optional: true + + '@rollup/rollup-freebsd-arm64@4.52.3': + optional: true + + '@rollup/rollup-freebsd-x64@4.52.3': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.52.3': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.52.3': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.52.3': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.52.3': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.52.3': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.52.3': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.52.3': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.52.3': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.52.3': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.52.3': + optional: true + + '@rollup/rollup-linux-x64-musl@4.52.3': + optional: true + + '@rollup/rollup-openharmony-arm64@4.52.3': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.52.3': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.52.3': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.52.3': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.52.3': + optional: true + + '@sec-ant/readable-stream@0.4.1': {} + + '@sindresorhus/merge-streams@4.0.0': {} + + '@sxzz/popperjs-es@2.11.7': {} + + '@tailwindcss/node@4.1.14': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.18.3 + jiti: 2.6.0 + lightningcss: 1.30.1 + magic-string: 0.30.19 + source-map-js: 1.2.1 + tailwindcss: 4.1.14 + + '@tailwindcss/oxide-android-arm64@4.1.14': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.1.14': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.1.14': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.1.14': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.14': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.14': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.1.14': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.1.14': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.1.14': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.1.14': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.14': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.1.14': + optional: true + + '@tailwindcss/oxide@4.1.14': + dependencies: + detect-libc: 2.1.2 + tar: 7.5.1 + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.1.14 + '@tailwindcss/oxide-darwin-arm64': 4.1.14 + '@tailwindcss/oxide-darwin-x64': 4.1.14 + '@tailwindcss/oxide-freebsd-x64': 4.1.14 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.14 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.14 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.14 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.14 + '@tailwindcss/oxide-linux-x64-musl': 4.1.14 + '@tailwindcss/oxide-wasm32-wasi': 4.1.14 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.14 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.14 + + '@tailwindcss/vite@4.1.14(vite@7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': + dependencies: + '@tailwindcss/node': 4.1.14 + '@tailwindcss/oxide': 4.1.14 + tailwindcss: 4.1.14 + vite: 7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + + '@transloadit/prettier-bytes@0.0.7': {} + + '@types/conventional-commits-parser@5.0.1': + dependencies: + '@types/node': 24.8.1 + + '@types/dompurify@3.2.0': + dependencies: + dompurify: 3.3.1 + + '@types/estree@1.0.8': {} + + '@types/event-emitter@0.3.5': {} + + '@types/json-schema@7.0.15': {} + + '@types/lodash-es@4.17.12': + dependencies: + '@types/lodash': 4.17.20 + + '@types/lodash@4.17.20': {} + + '@types/node@24.8.1': + dependencies: + undici-types: 7.14.0 + + '@types/sortablejs@1.15.8': {} + + '@types/trusted-types@2.0.7': + optional: true + + '@types/web-bluetooth@0.0.16': {} + + '@types/web-bluetooth@0.0.21': {} + + '@typescript-eslint/eslint-plugin@8.44.1(@typescript-eslint/parser@8.44.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.6.3))(eslint@9.36.0(jiti@2.6.0))(typescript@5.6.3)': + dependencies: + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 8.44.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.6.3) + '@typescript-eslint/scope-manager': 8.44.1 + '@typescript-eslint/type-utils': 8.44.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.6.3) + '@typescript-eslint/utils': 8.44.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.6.3) + '@typescript-eslint/visitor-keys': 8.44.1 + eslint: 9.36.0(jiti@2.6.0) + graphemer: 1.4.0 + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.1.0(typescript@5.6.3) + typescript: 5.6.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.44.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.6.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.44.1 + '@typescript-eslint/types': 8.44.1 + '@typescript-eslint/typescript-estree': 8.44.1(typescript@5.6.3) + '@typescript-eslint/visitor-keys': 8.44.1 + debug: 4.4.3 + eslint: 9.36.0(jiti@2.6.0) + typescript: 5.6.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.44.1(typescript@5.6.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.44.1(typescript@5.6.3) + '@typescript-eslint/types': 8.44.1 + debug: 4.4.3 + typescript: 5.6.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.44.1': + dependencies: + '@typescript-eslint/types': 8.44.1 + '@typescript-eslint/visitor-keys': 8.44.1 + + '@typescript-eslint/tsconfig-utils@8.44.1(typescript@5.6.3)': + dependencies: + typescript: 5.6.3 + + '@typescript-eslint/type-utils@8.44.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.6.3)': + dependencies: + '@typescript-eslint/types': 8.44.1 + '@typescript-eslint/typescript-estree': 8.44.1(typescript@5.6.3) + '@typescript-eslint/utils': 8.44.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.6.3) + debug: 4.4.3 + eslint: 9.36.0(jiti@2.6.0) + ts-api-utils: 2.1.0(typescript@5.6.3) + typescript: 5.6.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.44.1': {} + + '@typescript-eslint/typescript-estree@8.44.1(typescript@5.6.3)': + dependencies: + '@typescript-eslint/project-service': 8.44.1(typescript@5.6.3) + '@typescript-eslint/tsconfig-utils': 8.44.1(typescript@5.6.3) + '@typescript-eslint/types': 8.44.1 + '@typescript-eslint/visitor-keys': 8.44.1 + debug: 4.4.3 + fast-glob: 3.3.3 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.7.2 + ts-api-utils: 2.1.0(typescript@5.6.3) + typescript: 5.6.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.44.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.6.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.36.0(jiti@2.6.0)) + '@typescript-eslint/scope-manager': 8.44.1 + '@typescript-eslint/types': 8.44.1 + '@typescript-eslint/typescript-estree': 8.44.1(typescript@5.6.3) + eslint: 9.36.0(jiti@2.6.0) + typescript: 5.6.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.44.1': + dependencies: + '@typescript-eslint/types': 8.44.1 + eslint-visitor-keys: 4.2.1 + + '@uppy/companion-client@2.2.2': + dependencies: + '@uppy/utils': 4.1.3 + namespace-emitter: 2.0.1 + + '@uppy/core@2.3.4': + dependencies: + '@transloadit/prettier-bytes': 0.0.7 + '@uppy/store-default': 2.1.1 + '@uppy/utils': 4.1.3 + lodash.throttle: 4.1.1 + mime-match: 1.0.2 + namespace-emitter: 2.0.1 + nanoid: 3.3.11 + preact: 10.27.2 + + '@uppy/store-default@2.1.1': {} + + '@uppy/utils@4.1.3': + dependencies: + lodash.throttle: 4.1.1 + + '@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4)': + dependencies: + '@uppy/companion-client': 2.2.2 + '@uppy/core': 2.3.4 + '@uppy/utils': 4.1.3 + nanoid: 3.3.11 + + '@vitejs/plugin-vue@6.0.1(vite@7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.22(typescript@5.6.3))': + dependencies: + '@rolldown/pluginutils': 1.0.0-beta.29 + vite: 7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vue: 3.5.22(typescript@5.6.3) + + '@vitest/expect@2.1.9': + dependencies: + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.3.3 + tinyrainbow: 1.2.0 + + '@vitest/mocker@2.1.9(vite@5.4.21(@types/node@24.8.1)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0))': + dependencies: + '@vitest/spy': 2.1.9 + estree-walker: 3.0.3 + magic-string: 0.30.19 + optionalDependencies: + vite: 5.4.21(@types/node@24.8.1)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0) + + '@vitest/pretty-format@2.1.9': + dependencies: + tinyrainbow: 1.2.0 + + '@vitest/runner@2.1.9': + dependencies: + '@vitest/utils': 2.1.9 + pathe: 1.1.2 + + '@vitest/snapshot@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + magic-string: 0.30.19 + pathe: 1.1.2 + + '@vitest/spy@2.1.9': + dependencies: + tinyspy: 3.0.2 + + '@vitest/utils@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + loupe: 3.2.1 + tinyrainbow: 1.2.0 + + '@volar/language-core@2.4.23': + dependencies: + '@volar/source-map': 2.4.23 + + '@volar/source-map@2.4.23': {} + + '@volar/typescript@2.4.23': + dependencies: + '@volar/language-core': 2.4.23 + path-browserify: 1.0.1 + vscode-uri: 3.1.0 + + '@vue/babel-helper-vue-transform-on@1.5.0': {} + + '@vue/babel-plugin-jsx@1.5.0(@babel/core@7.28.4)': + dependencies: + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.4) + '@babel/template': 7.27.2 + '@babel/traverse': 7.28.4 + '@babel/types': 7.28.4 + '@vue/babel-helper-vue-transform-on': 1.5.0 + '@vue/babel-plugin-resolve-type': 1.5.0(@babel/core@7.28.4) + '@vue/shared': 3.5.22 + optionalDependencies: + '@babel/core': 7.28.4 + transitivePeerDependencies: + - supports-color + + '@vue/babel-plugin-resolve-type@1.5.0(@babel/core@7.28.4)': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/core': 7.28.4 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/parser': 7.28.4 + '@vue/compiler-sfc': 3.5.22 + transitivePeerDependencies: + - supports-color + + '@vue/compiler-core@3.5.22': + dependencies: + '@babel/parser': 7.28.4 + '@vue/shared': 3.5.22 + entities: 4.5.0 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.22': + dependencies: + '@vue/compiler-core': 3.5.22 + '@vue/shared': 3.5.22 + + '@vue/compiler-sfc@3.5.22': + dependencies: + '@babel/parser': 7.28.4 + '@vue/compiler-core': 3.5.22 + '@vue/compiler-dom': 3.5.22 + '@vue/compiler-ssr': 3.5.22 + '@vue/shared': 3.5.22 + estree-walker: 2.0.2 + magic-string: 0.30.19 + postcss: 8.5.6 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.22': + dependencies: + '@vue/compiler-dom': 3.5.22 + '@vue/shared': 3.5.22 + + '@vue/compiler-vue2@2.7.16': + dependencies: + de-indent: 1.0.2 + he: 1.2.0 + + '@vue/devtools-api@6.6.4': {} + + '@vue/devtools-api@7.7.7': + dependencies: + '@vue/devtools-kit': 7.7.7 + + '@vue/devtools-core@7.7.7(vite@7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.22(typescript@5.6.3))': + dependencies: + '@vue/devtools-kit': 7.7.7 + '@vue/devtools-shared': 7.7.7 + mitt: 3.0.1 + nanoid: 5.1.6 + pathe: 2.0.3 + vite-hot-client: 2.1.0(vite@7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + vue: 3.5.22(typescript@5.6.3) + transitivePeerDependencies: + - vite + + '@vue/devtools-kit@7.7.7': + dependencies: + '@vue/devtools-shared': 7.7.7 + birpc: 2.6.1 + hookable: 5.5.3 + mitt: 3.0.1 + perfect-debounce: 1.0.0 + speakingurl: 14.0.1 + superjson: 2.2.2 + + '@vue/devtools-shared@7.7.7': + dependencies: + rfdc: 1.4.1 + + '@vue/language-core@2.1.10(typescript@5.6.3)': + dependencies: + '@volar/language-core': 2.4.23 + '@vue/compiler-dom': 3.5.22 + '@vue/compiler-vue2': 2.7.16 + '@vue/shared': 3.5.22 + alien-signals: 0.2.2 + minimatch: 9.0.5 + muggle-string: 0.4.1 + path-browserify: 1.0.1 + optionalDependencies: + typescript: 5.6.3 + + '@vue/reactivity@3.5.22': + dependencies: + '@vue/shared': 3.5.22 + + '@vue/runtime-core@3.5.22': + dependencies: + '@vue/reactivity': 3.5.22 + '@vue/shared': 3.5.22 + + '@vue/runtime-dom@3.5.22': + dependencies: + '@vue/reactivity': 3.5.22 + '@vue/runtime-core': 3.5.22 + '@vue/shared': 3.5.22 + csstype: 3.1.3 + + '@vue/server-renderer@3.5.22(vue@3.5.22(typescript@5.6.3))': + dependencies: + '@vue/compiler-ssr': 3.5.22 + '@vue/shared': 3.5.22 + vue: 3.5.22(typescript@5.6.3) + + '@vue/shared@3.5.22': {} + + '@vue/test-utils@2.4.6': + dependencies: + js-beautify: 1.15.4 + vue-component-type-helpers: 2.2.12 + + '@vueuse/core@13.9.0(vue@3.5.22(typescript@5.6.3))': + dependencies: + '@types/web-bluetooth': 0.0.21 + '@vueuse/metadata': 13.9.0 + '@vueuse/shared': 13.9.0(vue@3.5.22(typescript@5.6.3)) + vue: 3.5.22(typescript@5.6.3) + + '@vueuse/core@9.13.0(vue@3.5.22(typescript@5.6.3))': + dependencies: + '@types/web-bluetooth': 0.0.16 + '@vueuse/metadata': 9.13.0 + '@vueuse/shared': 9.13.0(vue@3.5.22(typescript@5.6.3)) + vue-demi: 0.14.10(vue@3.5.22(typescript@5.6.3)) + transitivePeerDependencies: + - '@vue/composition-api' + - vue + + '@vueuse/metadata@13.9.0': {} + + '@vueuse/metadata@9.13.0': {} + + '@vueuse/shared@13.9.0(vue@3.5.22(typescript@5.6.3))': + dependencies: + vue: 3.5.22(typescript@5.6.3) + + '@vueuse/shared@9.13.0(vue@3.5.22(typescript@5.6.3))': + dependencies: + vue-demi: 0.14.10(vue@3.5.22(typescript@5.6.3)) + transitivePeerDependencies: + - '@vue/composition-api' + - vue + + '@wangeditor/basic-modules@1.1.7(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.2))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.2)': + dependencies: + '@wangeditor/core': 1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.2) + dom7: 3.0.0 + is-url: 1.2.4 + lodash.throttle: 4.1.1 + nanoid: 3.3.11 + slate: 0.72.8 + snabbdom: 3.6.2 + + '@wangeditor/code-highlight@1.0.3(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.2))(dom7@3.0.0)(slate@0.72.8)(snabbdom@3.6.2)': + dependencies: + '@wangeditor/core': 1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.2) + dom7: 3.0.0 + prismjs: 1.30.0 + slate: 0.72.8 + snabbdom: 3.6.2 + + '@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.2)': + dependencies: + '@types/event-emitter': 0.3.5 + '@uppy/core': 2.3.4 + '@uppy/xhr-upload': 2.1.3(@uppy/core@2.3.4) + dom7: 3.0.0 + event-emitter: 0.3.5 + html-void-elements: 2.0.1 + i18next: 20.6.1 + is-hotkey: 0.2.0 + lodash.camelcase: 4.3.0 + lodash.clonedeep: 4.5.0 + lodash.debounce: 4.0.8 + lodash.foreach: 4.5.0 + lodash.isequal: 4.5.0 + lodash.throttle: 4.1.1 + lodash.toarray: 4.4.0 + nanoid: 3.3.11 + scroll-into-view-if-needed: 2.2.31 + slate: 0.72.8 + slate-history: 0.66.0(slate@0.72.8) + snabbdom: 3.6.2 + + '@wangeditor/editor-for-vue@5.1.12(@wangeditor/editor@5.1.23)(vue@3.5.22(typescript@5.6.3))': + dependencies: + '@wangeditor/editor': 5.1.23 + vue: 3.5.22(typescript@5.6.3) + + '@wangeditor/editor@5.1.23': + dependencies: + '@uppy/core': 2.3.4 + '@uppy/xhr-upload': 2.1.3(@uppy/core@2.3.4) + '@wangeditor/basic-modules': 1.1.7(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.2))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.2) + '@wangeditor/code-highlight': 1.0.3(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.2))(dom7@3.0.0)(slate@0.72.8)(snabbdom@3.6.2) + '@wangeditor/core': 1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.2) + '@wangeditor/list-module': 1.0.5(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.2))(dom7@3.0.0)(slate@0.72.8)(snabbdom@3.6.2) + '@wangeditor/table-module': 1.1.4(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.2))(dom7@3.0.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.2) + '@wangeditor/upload-image-module': 1.0.2(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor/basic-modules@1.1.7(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.2))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.2))(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.2))(dom7@3.0.0)(lodash.foreach@4.5.0)(slate@0.72.8)(snabbdom@3.6.2) + '@wangeditor/video-module': 1.1.4(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.2))(dom7@3.0.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.2) + dom7: 3.0.0 + is-hotkey: 0.2.0 + lodash.camelcase: 4.3.0 + lodash.clonedeep: 4.5.0 + lodash.debounce: 4.0.8 + lodash.foreach: 4.5.0 + lodash.isequal: 4.5.0 + lodash.throttle: 4.1.1 + lodash.toarray: 4.4.0 + nanoid: 3.3.11 + slate: 0.72.8 + snabbdom: 3.6.2 + + '@wangeditor/list-module@1.0.5(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.2))(dom7@3.0.0)(slate@0.72.8)(snabbdom@3.6.2)': + dependencies: + '@wangeditor/core': 1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.2) + dom7: 3.0.0 + slate: 0.72.8 + snabbdom: 3.6.2 + + '@wangeditor/table-module@1.1.4(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.2))(dom7@3.0.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.2)': + dependencies: + '@wangeditor/core': 1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.2) + dom7: 3.0.0 + lodash.isequal: 4.5.0 + lodash.throttle: 4.1.1 + nanoid: 3.3.11 + slate: 0.72.8 + snabbdom: 3.6.2 + + '@wangeditor/upload-image-module@1.0.2(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor/basic-modules@1.1.7(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.2))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.2))(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.2))(dom7@3.0.0)(lodash.foreach@4.5.0)(slate@0.72.8)(snabbdom@3.6.2)': + dependencies: + '@uppy/core': 2.3.4 + '@uppy/xhr-upload': 2.1.3(@uppy/core@2.3.4) + '@wangeditor/basic-modules': 1.1.7(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.2))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.2) + '@wangeditor/core': 1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.2) + dom7: 3.0.0 + lodash.foreach: 4.5.0 + slate: 0.72.8 + snabbdom: 3.6.2 + + '@wangeditor/video-module@1.1.4(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.2))(dom7@3.0.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.2)': + dependencies: + '@uppy/core': 2.3.4 + '@uppy/xhr-upload': 2.1.3(@uppy/core@2.3.4) + '@wangeditor/core': 1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.2) + dom7: 3.0.0 + nanoid: 3.3.11 + slate: 0.72.8 + snabbdom: 3.6.2 + + JSONStream@1.3.5: + dependencies: + jsonparse: 1.3.1 + through: 2.3.8 + + abbrev@2.0.0: {} + + acorn-jsx@5.3.2(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + + acorn@8.15.0: {} + + adler-32@1.3.1: {} + + agent-base@7.1.4: {} + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ajv@8.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + alien-signals@0.2.2: {} + + ansi-escapes@4.3.2: + dependencies: + type-fest: 0.21.3 + + ansi-escapes@7.1.1: + dependencies: + environment: 1.1.0 + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + + ansi-styles@3.2.1: + dependencies: + color-convert: 1.9.3 + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.3: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + argparse@2.0.1: {} + + array-ify@1.0.0: {} + + array-union@2.1.0: {} + + assertion-error@2.0.1: {} + + astral-regex@2.0.0: {} + + async-validator@4.2.5: {} + + asynckit@0.4.0: {} + + at-least-node@1.0.0: {} + + axios@1.12.2: + dependencies: + follow-redirects: 1.15.11 + form-data: 4.0.4 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + + balanced-match@1.0.2: {} + + balanced-match@2.0.0: {} + + base64-js@1.5.1: {} + + baseline-browser-mapping@2.8.8: {} + + bignumber.js@9.3.1: {} + + binary-extensions@2.3.0: {} + + birpc@2.6.1: {} + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + + boolbase@1.0.0: {} + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.26.2: + dependencies: + baseline-browser-mapping: 2.8.8 + caniuse-lite: 1.0.30001745 + electron-to-chromium: 1.5.227 + node-releases: 2.0.21 + update-browserslist-db: 1.1.3(browserslist@4.26.2) + + buffer-from@1.1.2: {} + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + bundle-name@4.1.0: + dependencies: + run-applescript: 7.1.0 + + cac@6.7.14: {} + + cacheable@2.0.2: + dependencies: + '@cacheable/memoize': 2.0.2 + '@cacheable/memory': 2.0.2 + '@cacheable/utils': 2.0.2 + hookified: 1.12.1 + keyv: 5.5.3 + + cachedir@2.3.0: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + callsites@3.1.0: {} + + caniuse-lite@1.0.30001745: {} + + cfb@1.2.2: + dependencies: + adler-32: 1.3.1 + crc-32: 1.2.2 + + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.1 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + + chalk@2.4.2: + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chalk@5.6.2: {} + + chardet@0.7.0: {} + + check-error@2.1.1: {} + + china-division@2.7.0: {} + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + chownr@3.0.0: {} + + cli-cursor@3.1.0: + dependencies: + restore-cursor: 3.1.0 + + cli-cursor@5.0.0: + dependencies: + restore-cursor: 5.1.0 + + cli-spinners@2.9.2: {} + + cli-truncate@4.0.0: + dependencies: + slice-ansi: 5.0.0 + string-width: 7.2.0 + + cli-width@3.0.0: {} + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + clone@1.0.4: {} + + codepage@1.15.0: {} + + color-convert@1.9.3: + dependencies: + color-name: 1.1.3 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.3: {} + + color-name@1.1.4: {} + + colord@2.9.3: {} + + colorette@2.0.20: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + commander@10.0.1: {} + + commander@13.1.0: {} + + commander@2.20.3: {} + + commitizen@4.3.1(@types/node@24.8.1)(typescript@5.6.3): + dependencies: + cachedir: 2.3.0 + cz-conventional-changelog: 3.3.0(@types/node@24.8.1)(typescript@5.6.3) + dedent: 0.7.0 + detect-indent: 6.1.0 + find-node-modules: 2.1.3 + find-root: 1.1.0 + fs-extra: 9.1.0 + glob: 7.2.3 + inquirer: 8.2.5 + is-utf8: 0.2.1 + lodash: 4.17.21 + minimist: 1.2.7 + strip-bom: 4.0.0 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - '@types/node' + - typescript + + compare-func@2.0.0: + dependencies: + array-ify: 1.0.0 + dot-prop: 5.3.0 + + compute-scroll-into-view@1.0.20: {} + + concat-map@0.0.1: {} + + confbox@0.1.8: {} + + confbox@0.2.2: {} + + config-chain@1.1.13: + dependencies: + ini: 1.3.8 + proto-list: 1.2.4 + + conventional-changelog-angular@7.0.0: + dependencies: + compare-func: 2.0.0 + + conventional-changelog-conventionalcommits@7.0.2: + dependencies: + compare-func: 2.0.0 + + conventional-commit-types@3.0.0: {} + + conventional-commits-parser@5.0.0: + dependencies: + JSONStream: 1.3.5 + is-text-path: 2.0.0 + meow: 12.1.1 + split2: 4.2.0 + + convert-source-map@2.0.0: {} + + copy-anything@3.0.5: + dependencies: + is-what: 4.1.16 + + core-js@3.45.1: {} + + cosmiconfig-typescript-loader@6.1.0(@types/node@24.8.1)(cosmiconfig@9.0.0(typescript@5.6.3))(typescript@5.6.3): + dependencies: + '@types/node': 24.8.1 + cosmiconfig: 9.0.0(typescript@5.6.3) + jiti: 2.6.0 + typescript: 5.6.3 + + cosmiconfig@9.0.0(typescript@5.6.3): + dependencies: + env-paths: 2.2.1 + import-fresh: 3.3.1 + js-yaml: 4.1.0 + parse-json: 5.2.0 + optionalDependencies: + typescript: 5.6.3 + + crc-32@1.2.2: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + crypto-js@4.2.0: {} + + css-functions-list@3.2.3: {} + + css-tree@3.1.0: + dependencies: + mdn-data: 2.12.2 + source-map-js: 1.2.1 + + cssesc@3.0.0: {} + + cssstyle@4.6.0: + dependencies: + '@asamuzakjp/css-color': 3.2.0 + rrweb-cssom: 0.8.0 + + csstype@3.1.3: {} + + cz-conventional-changelog@3.3.0(@types/node@24.8.1)(typescript@5.6.3): + dependencies: + chalk: 2.4.2 + commitizen: 4.3.1(@types/node@24.8.1)(typescript@5.6.3) + conventional-commit-types: 3.0.0 + lodash.map: 4.6.0 + longest: 2.0.1 + word-wrap: 1.2.5 + optionalDependencies: + '@commitlint/load': 20.0.0(@types/node@24.8.1)(typescript@5.6.3) + transitivePeerDependencies: + - '@types/node' + - typescript + + cz-git@1.12.0: {} + + d@1.0.2: + dependencies: + es5-ext: 0.10.64 + type: 2.7.3 + + danmu.js@1.1.13: + dependencies: + event-emitter: 0.3.5 + + dargs@8.1.0: {} + + data-urls@5.0.0: + dependencies: + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + + dayjs@1.11.18: {} + + de-indent@1.0.2: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decimal.js@10.6.0: {} + + dedent@0.7.0: {} + + deep-eql@5.0.2: {} + + deep-is@0.1.4: {} + + deep-pick-omit@1.2.1: {} + + default-browser-id@5.0.0: {} + + default-browser@5.2.1: + dependencies: + bundle-name: 4.1.0 + default-browser-id: 5.0.0 + + defaults@1.0.4: + dependencies: + clone: 1.0.4 + + define-lazy-prop@2.0.0: {} + + define-lazy-prop@3.0.0: {} + + defu@6.1.4: {} + + delayed-stream@1.0.0: {} + + delegate@3.2.0: {} + + destr@2.0.5: {} + + detect-file@1.0.0: {} + + detect-indent@6.1.0: {} + + detect-libc@1.0.3: + optional: true + + detect-libc@2.1.2: {} + + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + dom7@3.0.0: + dependencies: + ssr-window: 3.0.0 + + domelementtype@2.3.0: {} + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + + dompurify@3.3.1: + optionalDependencies: + '@types/trusted-types': 2.0.7 + + domutils@3.2.2: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + + dot-prop@5.3.0: + dependencies: + is-obj: 2.0.0 + + downloadjs@1.4.7: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + eastasianwidth@0.2.0: {} + + echarts@6.0.0: + dependencies: + tslib: 2.3.0 + zrender: 6.0.0 + + editorconfig@1.0.4: + dependencies: + '@one-ini/wasm': 0.1.1 + commander: 10.0.1 + minimatch: 9.0.1 + semver: 7.7.2 + + electron-to-chromium@1.5.227: {} + + element-china-area-data@6.1.0: + dependencies: + china-division: 2.7.0 + + element-plus@2.11.4(vue@3.5.22(typescript@5.6.3)): + dependencies: + '@ctrl/tinycolor': 3.6.1 + '@element-plus/icons-vue': 2.3.2(vue@3.5.22(typescript@5.6.3)) + '@floating-ui/dom': 1.7.4 + '@popperjs/core': '@sxzz/popperjs-es@2.11.7' + '@types/lodash': 4.17.20 + '@types/lodash-es': 4.17.12 + '@vueuse/core': 9.13.0(vue@3.5.22(typescript@5.6.3)) + async-validator: 4.2.5 + dayjs: 1.11.18 + escape-html: 1.0.3 + lodash: 4.17.21 + lodash-es: 4.17.21 + lodash-unified: 1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.17.21)(lodash@4.17.21) + memoize-one: 6.0.0 + normalize-wheel-es: 1.2.0 + vue: 3.5.22(typescript@5.6.3) + transitivePeerDependencies: + - '@vue/composition-api' + + emoji-regex@10.5.0: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + enhanced-resolve@5.18.3: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.0 + + entities@4.5.0: {} + + entities@6.0.1: {} + + env-paths@2.2.1: {} + + environment@1.1.0: {} + + error-ex@1.3.4: + dependencies: + is-arrayish: 0.2.1 + + error-stack-parser-es@0.1.5: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-module-lexer@1.7.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + es5-ext@0.10.64: + dependencies: + es6-iterator: 2.0.3 + es6-symbol: 3.1.4 + esniff: 2.0.1 + next-tick: 1.1.0 + + es6-iterator@2.0.3: + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + es6-symbol: 3.1.4 + + es6-symbol@3.1.4: + dependencies: + d: 1.0.2 + ext: 1.7.0 + + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + esbuild@0.25.10: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.10 + '@esbuild/android-arm': 0.25.10 + '@esbuild/android-arm64': 0.25.10 + '@esbuild/android-x64': 0.25.10 + '@esbuild/darwin-arm64': 0.25.10 + '@esbuild/darwin-x64': 0.25.10 + '@esbuild/freebsd-arm64': 0.25.10 + '@esbuild/freebsd-x64': 0.25.10 + '@esbuild/linux-arm': 0.25.10 + '@esbuild/linux-arm64': 0.25.10 + '@esbuild/linux-ia32': 0.25.10 + '@esbuild/linux-loong64': 0.25.10 + '@esbuild/linux-mips64el': 0.25.10 + '@esbuild/linux-ppc64': 0.25.10 + '@esbuild/linux-riscv64': 0.25.10 + '@esbuild/linux-s390x': 0.25.10 + '@esbuild/linux-x64': 0.25.10 + '@esbuild/netbsd-arm64': 0.25.10 + '@esbuild/netbsd-x64': 0.25.10 + '@esbuild/openbsd-arm64': 0.25.10 + '@esbuild/openbsd-x64': 0.25.10 + '@esbuild/openharmony-arm64': 0.25.10 + '@esbuild/sunos-x64': 0.25.10 + '@esbuild/win32-arm64': 0.25.10 + '@esbuild/win32-ia32': 0.25.10 + '@esbuild/win32-x64': 0.25.10 + + escalade@3.2.0: {} + + escape-html@1.0.3: {} + + escape-string-regexp@1.0.5: {} + + escape-string-regexp@4.0.0: {} + + escape-string-regexp@5.0.0: {} + + eslint-config-prettier@9.1.2(eslint@9.36.0(jiti@2.6.0)): + dependencies: + eslint: 9.36.0(jiti@2.6.0) + + eslint-plugin-prettier@5.5.4(eslint-config-prettier@9.1.2(eslint@9.36.0(jiti@2.6.0)))(eslint@9.36.0(jiti@2.6.0))(prettier@3.6.2): + dependencies: + eslint: 9.36.0(jiti@2.6.0) + prettier: 3.6.2 + prettier-linter-helpers: 1.0.0 + synckit: 0.11.11 + optionalDependencies: + eslint-config-prettier: 9.1.2(eslint@9.36.0(jiti@2.6.0)) + + eslint-plugin-vue@9.33.0(eslint@9.36.0(jiti@2.6.0)): + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.36.0(jiti@2.6.0)) + eslint: 9.36.0(jiti@2.6.0) + globals: 13.24.0 + natural-compare: 1.4.0 + nth-check: 2.1.1 + postcss-selector-parser: 6.1.2 + semver: 7.7.2 + vue-eslint-parser: 9.4.3(eslint@9.36.0(jiti@2.6.0)) + xml-name-validator: 4.0.0 + transitivePeerDependencies: + - supports-color + + eslint-scope@7.2.2: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint@9.36.0(jiti@2.6.0): + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.36.0(jiti@2.6.0)) + '@eslint-community/regexpp': 4.12.1 + '@eslint/config-array': 0.21.0 + '@eslint/config-helpers': 0.3.1 + '@eslint/core': 0.15.2 + '@eslint/eslintrc': 3.3.1 + '@eslint/js': 9.36.0 + '@eslint/plugin-kit': 0.3.5 + '@humanfs/node': 0.16.7 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + '@types/json-schema': 7.0.15 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.6.0 + transitivePeerDependencies: + - supports-color + + esniff@2.0.1: + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + event-emitter: 0.3.5 + type: 2.7.3 + + espree@10.4.0: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 4.2.1 + + espree@9.6.1: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 3.4.3 + + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@2.0.2: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + esutils@2.0.3: {} + + event-emitter@0.3.5: + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + + eventemitter3@4.0.7: {} + + eventemitter3@5.0.1: {} + + execa@8.0.1: + dependencies: + cross-spawn: 7.0.6 + get-stream: 8.0.1 + human-signals: 5.0.0 + is-stream: 3.0.0 + merge-stream: 2.0.0 + npm-run-path: 5.3.0 + onetime: 6.0.0 + signal-exit: 4.1.0 + strip-final-newline: 3.0.0 + + execa@9.6.0: + dependencies: + '@sindresorhus/merge-streams': 4.0.0 + cross-spawn: 7.0.6 + figures: 6.1.0 + get-stream: 9.0.1 + human-signals: 8.0.1 + is-plain-obj: 4.1.0 + is-stream: 4.0.1 + npm-run-path: 6.0.0 + pretty-ms: 9.3.0 + signal-exit: 4.1.0 + strip-final-newline: 4.0.0 + yoctocolors: 2.1.2 + + expand-tilde@2.0.2: + dependencies: + homedir-polyfill: 1.0.3 + + expect-type@1.3.0: {} + + exsolve@1.0.7: {} + + ext@1.7.0: + dependencies: + type: 2.7.3 + + external-editor@3.1.0: + dependencies: + chardet: 0.7.0 + iconv-lite: 0.4.24 + tmp: 0.0.33 + + fast-deep-equal@3.1.3: {} + + fast-diff@1.3.0: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fast-uri@3.1.0: {} + + fastest-levenshtein@1.0.16: {} + + fastq@1.19.1: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + figures@3.2.0: + dependencies: + escape-string-regexp: 1.0.5 + + figures@6.1.0: + dependencies: + is-unicode-supported: 2.1.0 + + file-entry-cache@10.1.4: + dependencies: + flat-cache: 6.1.14 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + file-saver@2.0.5: {} + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-node-modules@2.1.3: + dependencies: + findup-sync: 4.0.0 + merge: 2.1.1 + + find-root@1.1.0: {} + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + find-up@7.0.0: + dependencies: + locate-path: 7.2.0 + path-exists: 5.0.0 + unicorn-magic: 0.1.0 + + findup-sync@4.0.0: + dependencies: + detect-file: 1.0.0 + is-glob: 4.0.3 + micromatch: 4.0.8 + resolve-dir: 1.0.1 + + flat-cache@4.0.1: + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 + + flat-cache@6.1.14: + dependencies: + cacheable: 2.0.2 + flatted: 3.3.3 + hookified: 1.12.1 + + flatted@3.3.3: {} + + follow-redirects@1.15.11: {} + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + form-data@4.0.4: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + + frac@1.1.2: {} + + fs-extra@10.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + + fs-extra@11.3.2: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + + fs-extra@9.1.0: + dependencies: + at-least-node: 1.0.0 + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + + fs.realpath@1.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + get-east-asian-width@1.4.0: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-stream@8.0.1: {} + + get-stream@9.0.1: + dependencies: + '@sec-ant/readable-stream': 0.4.1 + is-stream: 4.0.1 + + get-tsconfig@4.10.1: + dependencies: + resolve-pkg-maps: 1.0.0 + + git-raw-commits@4.0.0: + dependencies: + dargs: 8.1.0 + meow: 12.1.1 + split2: 4.2.0 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@10.5.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + global-directory@4.0.1: + dependencies: + ini: 4.1.1 + + global-modules@1.0.0: + dependencies: + global-prefix: 1.0.2 + is-windows: 1.0.2 + resolve-dir: 1.0.1 + + global-modules@2.0.0: + dependencies: + global-prefix: 3.0.0 + + global-prefix@1.0.2: + dependencies: + expand-tilde: 2.0.2 + homedir-polyfill: 1.0.3 + ini: 1.3.8 + is-windows: 1.0.2 + which: 1.3.1 + + global-prefix@3.0.0: + dependencies: + ini: 1.3.8 + kind-of: 6.0.3 + which: 1.3.1 + + globals@13.24.0: + dependencies: + type-fest: 0.20.2 + + globals@14.0.0: {} + + globals@15.15.0: {} + + globby@11.1.0: + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + + globjoin@0.1.4: {} + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + graphemer@1.4.0: {} + + has-flag@3.0.0: {} + + has-flag@4.0.0: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + he@1.2.0: {} + + highlight.js@11.11.1: {} + + homedir-polyfill@1.0.3: + dependencies: + parse-passwd: 1.0.0 + + hookable@5.5.3: {} + + hookified@1.12.1: {} + + html-encoding-sniffer@4.0.0: + dependencies: + whatwg-encoding: 3.1.1 + + html-tags@3.3.1: {} + + html-void-elements@2.0.1: {} + + htmlparser2@8.0.2: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + entities: 4.5.0 + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + human-signals@5.0.0: {} + + human-signals@8.0.1: {} + + husky@9.1.7: {} + + i18next@20.6.1: + dependencies: + '@babel/runtime': 7.28.4 + + iconv-lite@0.4.24: + dependencies: + safer-buffer: 2.1.2 + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + ieee754@1.2.1: {} + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + immer@9.0.21: {} + + immutable@5.1.3: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + import-meta-resolve@4.2.0: {} + + imurmurhash@0.1.4: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + ini@1.3.8: {} + + ini@4.1.1: {} + + inquirer@8.2.5: + dependencies: + ansi-escapes: 4.3.2 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-width: 3.0.0 + external-editor: 3.1.0 + figures: 3.2.0 + lodash: 4.17.21 + mute-stream: 0.0.8 + ora: 5.4.1 + run-async: 2.4.1 + rxjs: 7.8.2 + string-width: 4.2.3 + strip-ansi: 6.0.1 + through: 2.3.8 + wrap-ansi: 7.0.0 + + is-arrayish@0.2.1: {} + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-docker@2.2.1: {} + + is-docker@3.0.0: {} + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-fullwidth-code-point@4.0.0: {} + + is-fullwidth-code-point@5.1.0: + dependencies: + get-east-asian-width: 1.4.0 + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-hotkey@0.2.0: {} + + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + + is-interactive@1.0.0: {} + + is-number@7.0.0: {} + + is-obj@2.0.0: {} + + is-plain-obj@4.1.0: {} + + is-plain-object@5.0.0: {} + + is-potential-custom-element-name@1.0.1: {} + + is-stream@3.0.0: {} + + is-stream@4.0.1: {} + + is-text-path@2.0.0: + dependencies: + text-extensions: 2.4.0 + + is-unicode-supported@0.1.0: {} + + is-unicode-supported@2.1.0: {} + + is-url@1.2.4: {} + + is-utf8@0.2.1: {} + + is-what@4.1.16: {} + + is-windows@1.0.2: {} + + is-wsl@2.2.0: + dependencies: + is-docker: 2.2.1 + + is-wsl@3.1.0: + dependencies: + is-inside-container: 1.0.0 + + isexe@2.0.0: {} + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + jiti@2.6.0: {} + + js-beautify@1.15.4: + dependencies: + config-chain: 1.1.13 + editorconfig: 1.0.4 + glob: 10.5.0 + js-cookie: 3.0.5 + nopt: 7.2.1 + + js-cookie@3.0.5: {} + + js-tokens@4.0.0: {} + + js-tokens@9.0.1: {} + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + jsdom@24.1.3: + dependencies: + cssstyle: 4.6.0 + data-urls: 5.0.0 + decimal.js: 10.6.0 + form-data: 4.0.4 + html-encoding-sniffer: 4.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.23 + parse5: 7.3.0 + rrweb-cssom: 0.7.1 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 4.1.4 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + ws: 8.18.3 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + jsesc@3.1.0: {} + + json-bigint@1.0.0: + dependencies: + bignumber.js: 9.3.1 + + json-buffer@3.0.1: {} + + json-parse-even-better-errors@2.3.1: {} + + json-schema-traverse@0.4.1: {} + + json-schema-traverse@1.0.0: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@2.2.3: {} + + jsonfile@6.2.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + jsonparse@1.3.1: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + keyv@5.5.3: + dependencies: + '@keyv/serialize': 1.1.1 + + kind-of@6.0.3: {} + + known-css-properties@0.36.0: {} + + known-css-properties@0.37.0: {} + + kolorist@1.8.0: {} + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lightningcss-darwin-arm64@1.30.1: + optional: true + + lightningcss-darwin-x64@1.30.1: + optional: true + + lightningcss-freebsd-x64@1.30.1: + optional: true + + lightningcss-linux-arm-gnueabihf@1.30.1: + optional: true + + lightningcss-linux-arm64-gnu@1.30.1: + optional: true + + lightningcss-linux-arm64-musl@1.30.1: + optional: true + + lightningcss-linux-x64-gnu@1.30.1: + optional: true + + lightningcss-linux-x64-musl@1.30.1: + optional: true + + lightningcss-win32-arm64-msvc@1.30.1: + optional: true + + lightningcss-win32-x64-msvc@1.30.1: + optional: true + + lightningcss@1.30.1: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-darwin-arm64: 1.30.1 + lightningcss-darwin-x64: 1.30.1 + lightningcss-freebsd-x64: 1.30.1 + lightningcss-linux-arm-gnueabihf: 1.30.1 + lightningcss-linux-arm64-gnu: 1.30.1 + lightningcss-linux-arm64-musl: 1.30.1 + lightningcss-linux-x64-gnu: 1.30.1 + lightningcss-linux-x64-musl: 1.30.1 + lightningcss-win32-arm64-msvc: 1.30.1 + lightningcss-win32-x64-msvc: 1.30.1 + + lilconfig@3.1.3: {} + + lines-and-columns@1.2.4: {} + + lint-staged@15.5.2: + dependencies: + chalk: 5.6.2 + commander: 13.1.0 + debug: 4.4.3 + execa: 8.0.1 + lilconfig: 3.1.3 + listr2: 8.3.3 + micromatch: 4.0.8 + pidtree: 0.6.0 + string-argv: 0.3.2 + yaml: 2.8.1 + transitivePeerDependencies: + - supports-color + + listr2@8.3.3: + dependencies: + cli-truncate: 4.0.0 + colorette: 2.0.20 + eventemitter3: 5.0.1 + log-update: 6.1.0 + rfdc: 1.4.1 + wrap-ansi: 9.0.2 + + local-pkg@1.1.2: + dependencies: + mlly: 1.8.0 + pkg-types: 2.3.0 + quansync: 0.2.11 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + locate-path@7.2.0: + dependencies: + p-locate: 6.0.0 + + lodash-es@4.17.21: {} + + lodash-unified@1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.17.21)(lodash@4.17.21): + dependencies: + '@types/lodash-es': 4.17.12 + lodash: 4.17.21 + lodash-es: 4.17.21 + + lodash.camelcase@4.3.0: {} + + lodash.clonedeep@4.5.0: {} + + lodash.debounce@4.0.8: {} + + lodash.foreach@4.5.0: {} + + lodash.isequal@4.5.0: {} + + lodash.isplainobject@4.0.6: {} + + lodash.kebabcase@4.1.1: {} + + lodash.map@4.6.0: {} + + lodash.merge@4.6.2: {} + + lodash.mergewith@4.6.2: {} + + lodash.snakecase@4.1.1: {} + + lodash.startcase@4.4.0: {} + + lodash.throttle@4.1.1: {} + + lodash.toarray@4.4.0: {} + + lodash.truncate@4.4.2: {} + + lodash.uniq@4.5.0: {} + + lodash.upperfirst@4.3.1: {} + + lodash@4.17.21: {} + + log-symbols@4.1.0: + dependencies: + chalk: 4.1.2 + is-unicode-supported: 0.1.0 + + log-update@6.1.0: + dependencies: + ansi-escapes: 7.1.1 + cli-cursor: 5.0.0 + slice-ansi: 7.1.2 + strip-ansi: 7.1.2 + wrap-ansi: 9.0.2 + + longest@2.0.1: {} + + loupe@3.2.1: {} + + lru-cache@10.4.3: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + magic-string@0.30.19: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + math-intrinsics@1.1.0: {} + + mathml-tag-names@2.1.3: {} + + mdn-data@2.12.2: {} + + mdn-data@2.24.0: {} + + memoize-one@6.0.0: {} + + meow@12.1.1: {} + + meow@13.2.0: {} + + merge-stream@2.0.0: {} + + merge2@1.4.1: {} + + merge@2.1.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mime-db@1.52.0: {} + + mime-match@1.0.2: + dependencies: + wildcard: 1.1.2 + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mimic-fn@2.1.0: {} + + mimic-fn@4.0.0: {} + + mimic-function@5.0.1: {} + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.12 + + minimatch@9.0.1: + dependencies: + brace-expansion: 2.0.2 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.2 + + minimist@1.2.7: {} + + minimist@1.2.8: {} + + minipass@7.1.2: {} + + minizlib@3.1.0: + dependencies: + minipass: 7.1.2 + + mitt@3.0.1: {} + + mlly@1.8.0: + dependencies: + acorn: 8.15.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.1 + + mrmime@2.0.1: {} + + ms@2.1.3: {} + + muggle-string@0.4.1: {} + + mute-stream@0.0.8: {} + + namespace-emitter@2.0.1: {} + + nanoid@3.3.11: {} + + nanoid@5.1.6: {} + + natural-compare@1.4.0: {} + + next-tick@1.1.0: {} + + node-addon-api@7.1.1: + optional: true + + node-releases@2.0.21: {} + + nopt@7.2.1: + dependencies: + abbrev: 2.0.0 + + normalize-path@3.0.0: {} + + normalize-wheel-es@1.2.0: {} + + npm-run-path@5.3.0: + dependencies: + path-key: 4.0.0 + + npm-run-path@6.0.0: + dependencies: + path-key: 4.0.0 + unicorn-magic: 0.3.0 + + nprogress@0.2.0: {} + + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + + nwsapi@2.2.23: {} + + ohash@2.0.11: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + + onetime@6.0.0: + dependencies: + mimic-fn: 4.0.0 + + onetime@7.0.0: + dependencies: + mimic-function: 5.0.1 + + open@10.2.0: + dependencies: + default-browser: 5.2.1 + define-lazy-prop: 3.0.0 + is-inside-container: 1.0.0 + wsl-utils: 0.1.0 + + open@8.4.2: + dependencies: + define-lazy-prop: 2.0.0 + is-docker: 2.2.1 + is-wsl: 2.2.0 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + ora@5.4.1: + dependencies: + bl: 4.1.0 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-spinners: 2.9.2 + is-interactive: 1.0.0 + is-unicode-supported: 0.1.0 + log-symbols: 4.1.0 + strip-ansi: 6.0.1 + wcwidth: 1.0.1 + + os-tmpdir@1.0.2: {} + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-limit@4.0.0: + dependencies: + yocto-queue: 1.2.1 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + p-locate@6.0.0: + dependencies: + p-limit: 4.0.0 + + package-json-from-dist@1.0.1: {} + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.27.1 + error-ex: 1.3.4 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + parse-ms@4.0.0: {} + + parse-passwd@1.0.0: {} + + parse5@7.3.0: + dependencies: + entities: 6.0.1 + + path-browserify@1.0.1: {} + + path-exists@4.0.0: {} + + path-exists@5.0.0: {} + + path-is-absolute@1.0.1: {} + + path-key@3.1.1: {} + + path-key@4.0.0: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.2 + + path-type@4.0.0: {} + + pathe@1.1.2: {} + + pathe@2.0.3: {} + + pathval@2.0.1: {} + + perfect-debounce@1.0.0: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.3: {} + + pidtree@0.6.0: {} + + pinia-plugin-persistedstate@4.5.0(pinia@3.0.3(typescript@5.6.3)(vue@3.5.22(typescript@5.6.3))): + dependencies: + deep-pick-omit: 1.2.1 + defu: 6.1.4 + destr: 2.0.5 + optionalDependencies: + pinia: 3.0.3(typescript@5.6.3)(vue@3.5.22(typescript@5.6.3)) + + pinia@3.0.3(typescript@5.6.3)(vue@3.5.22(typescript@5.6.3)): + dependencies: + '@vue/devtools-api': 7.7.7 + vue: 3.5.22(typescript@5.6.3) + optionalDependencies: + typescript: 5.6.3 + + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.0 + pathe: 2.0.3 + + pkg-types@2.3.0: + dependencies: + confbox: 0.2.2 + exsolve: 1.0.7 + pathe: 2.0.3 + + postcss-html@1.8.0: + dependencies: + htmlparser2: 8.0.2 + js-tokens: 9.0.1 + postcss: 8.5.6 + postcss-safe-parser: 6.0.0(postcss@8.5.6) + + postcss-media-query-parser@0.2.3: {} + + postcss-resolve-nested-selector@0.1.6: {} + + postcss-safe-parser@6.0.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-safe-parser@7.0.1(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-scss@4.0.9(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-selector-parser@6.1.2: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-selector-parser@7.1.0: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-sorting@8.0.2(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-value-parser@4.2.0: {} + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + preact@10.27.2: {} + + prelude-ls@1.2.1: {} + + prettier-linter-helpers@1.0.0: + dependencies: + fast-diff: 1.3.0 + + prettier@3.6.2: {} + + pretty-ms@9.3.0: + dependencies: + parse-ms: 4.0.0 + + prismjs@1.30.0: {} + + proto-list@1.2.4: {} + + proxy-from-env@1.1.0: {} + + psl@1.15.0: + dependencies: + punycode: 2.3.1 + + punycode@2.3.1: {} + + qrcode.vue@3.6.0(vue@3.5.22(typescript@5.6.3)): + dependencies: + vue: 3.5.22(typescript@5.6.3) + + quansync@0.2.11: {} + + querystringify@2.2.0: {} + + queue-microtask@1.2.3: {} + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + + readdirp@4.1.2: {} + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + requires-port@1.0.0: {} + + resolve-dir@1.0.1: + dependencies: + expand-tilde: 2.0.2 + global-modules: 1.0.0 + + resolve-from@4.0.0: {} + + resolve-from@5.0.0: {} + + resolve-pkg-maps@1.0.0: {} + + restore-cursor@3.1.0: + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + + restore-cursor@5.1.0: + dependencies: + onetime: 7.0.0 + signal-exit: 4.1.0 + + reusify@1.1.0: {} + + rfdc@1.4.1: {} + + rollup-plugin-visualizer@5.14.0(rollup@4.52.3): + dependencies: + open: 8.4.2 + picomatch: 4.0.3 + source-map: 0.7.6 + yargs: 17.7.2 + optionalDependencies: + rollup: 4.52.3 + + rollup@4.52.3: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.52.3 + '@rollup/rollup-android-arm64': 4.52.3 + '@rollup/rollup-darwin-arm64': 4.52.3 + '@rollup/rollup-darwin-x64': 4.52.3 + '@rollup/rollup-freebsd-arm64': 4.52.3 + '@rollup/rollup-freebsd-x64': 4.52.3 + '@rollup/rollup-linux-arm-gnueabihf': 4.52.3 + '@rollup/rollup-linux-arm-musleabihf': 4.52.3 + '@rollup/rollup-linux-arm64-gnu': 4.52.3 + '@rollup/rollup-linux-arm64-musl': 4.52.3 + '@rollup/rollup-linux-loong64-gnu': 4.52.3 + '@rollup/rollup-linux-ppc64-gnu': 4.52.3 + '@rollup/rollup-linux-riscv64-gnu': 4.52.3 + '@rollup/rollup-linux-riscv64-musl': 4.52.3 + '@rollup/rollup-linux-s390x-gnu': 4.52.3 + '@rollup/rollup-linux-x64-gnu': 4.52.3 + '@rollup/rollup-linux-x64-musl': 4.52.3 + '@rollup/rollup-openharmony-arm64': 4.52.3 + '@rollup/rollup-win32-arm64-msvc': 4.52.3 + '@rollup/rollup-win32-ia32-msvc': 4.52.3 + '@rollup/rollup-win32-x64-gnu': 4.52.3 + '@rollup/rollup-win32-x64-msvc': 4.52.3 + fsevents: 2.3.3 + + rrweb-cssom@0.7.1: {} + + rrweb-cssom@0.8.0: {} + + run-applescript@7.1.0: {} + + run-async@2.4.1: {} + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + rxjs@7.8.2: + dependencies: + tslib: 2.8.1 + + safe-buffer@5.2.1: {} + + safer-buffer@2.1.2: {} + + sass@1.93.2: + dependencies: + chokidar: 4.0.3 + immutable: 5.1.3 + source-map-js: 1.2.1 + optionalDependencies: + '@parcel/watcher': 2.5.1 + + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + + scroll-into-view-if-needed@2.2.31: + dependencies: + compute-scroll-into-view: 1.0.20 + + scule@1.3.0: {} + + semver@6.3.1: {} + + semver@7.7.2: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + siginfo@2.0.0: {} + + signal-exit@3.0.7: {} + + signal-exit@4.1.0: {} + + sirv@3.0.2: + dependencies: + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 + + slash@3.0.0: {} + + slate-history@0.66.0(slate@0.72.8): + dependencies: + is-plain-object: 5.0.0 + slate: 0.72.8 + + slate@0.72.8: + dependencies: + immer: 9.0.21 + is-plain-object: 5.0.0 + tiny-warning: 1.0.3 + + slice-ansi@4.0.0: + dependencies: + ansi-styles: 4.3.0 + astral-regex: 2.0.0 + is-fullwidth-code-point: 3.0.0 + + slice-ansi@5.0.0: + dependencies: + ansi-styles: 6.2.3 + is-fullwidth-code-point: 4.0.0 + + slice-ansi@7.1.2: + dependencies: + ansi-styles: 6.2.3 + is-fullwidth-code-point: 5.1.0 + + snabbdom@3.6.2: {} + + source-map-js@1.2.1: {} + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + source-map@0.7.6: {} + + speakingurl@14.0.1: {} + + split2@4.2.0: {} + + ssf@0.11.2: + dependencies: + frac: 1.1.2 + + ssr-window@3.0.0: {} + + stackback@0.0.2: {} + + std-env@3.10.0: {} + + string-argv@0.3.2: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.2 + + string-width@7.2.0: + dependencies: + emoji-regex: 10.5.0 + get-east-asian-width: 1.4.0 + strip-ansi: 7.1.2 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.2: + dependencies: + ansi-regex: 6.2.2 + + strip-bom@4.0.0: {} + + strip-final-newline@3.0.0: {} + + strip-final-newline@4.0.0: {} + + strip-json-comments@3.1.1: {} + + strip-literal@3.1.0: + dependencies: + js-tokens: 9.0.1 + + stylelint-config-html@1.1.0(postcss-html@1.8.0)(stylelint@16.24.0(typescript@5.6.3)): + dependencies: + postcss-html: 1.8.0 + stylelint: 16.24.0(typescript@5.6.3) + + stylelint-config-recess-order@4.6.0(stylelint@16.24.0(typescript@5.6.3)): + dependencies: + stylelint: 16.24.0(typescript@5.6.3) + stylelint-order: 6.0.4(stylelint@16.24.0(typescript@5.6.3)) + + stylelint-config-recommended-scss@14.1.0(postcss@8.5.6)(stylelint@16.24.0(typescript@5.6.3)): + dependencies: + postcss-scss: 4.0.9(postcss@8.5.6) + stylelint: 16.24.0(typescript@5.6.3) + stylelint-config-recommended: 14.0.1(stylelint@16.24.0(typescript@5.6.3)) + stylelint-scss: 6.12.1(stylelint@16.24.0(typescript@5.6.3)) + optionalDependencies: + postcss: 8.5.6 + + stylelint-config-recommended-vue@1.6.1(postcss-html@1.8.0)(stylelint@16.24.0(typescript@5.6.3)): + dependencies: + postcss-html: 1.8.0 + semver: 7.7.2 + stylelint: 16.24.0(typescript@5.6.3) + stylelint-config-html: 1.1.0(postcss-html@1.8.0)(stylelint@16.24.0(typescript@5.6.3)) + stylelint-config-recommended: 17.0.0(stylelint@16.24.0(typescript@5.6.3)) + + stylelint-config-recommended@14.0.1(stylelint@16.24.0(typescript@5.6.3)): + dependencies: + stylelint: 16.24.0(typescript@5.6.3) + + stylelint-config-recommended@17.0.0(stylelint@16.24.0(typescript@5.6.3)): + dependencies: + stylelint: 16.24.0(typescript@5.6.3) + + stylelint-config-standard@36.0.1(stylelint@16.24.0(typescript@5.6.3)): + dependencies: + stylelint: 16.24.0(typescript@5.6.3) + stylelint-config-recommended: 14.0.1(stylelint@16.24.0(typescript@5.6.3)) + + stylelint-order@6.0.4(stylelint@16.24.0(typescript@5.6.3)): + dependencies: + postcss: 8.5.6 + postcss-sorting: 8.0.2(postcss@8.5.6) + stylelint: 16.24.0(typescript@5.6.3) + + stylelint-scss@6.12.1(stylelint@16.24.0(typescript@5.6.3)): + dependencies: + css-tree: 3.1.0 + is-plain-object: 5.0.0 + known-css-properties: 0.36.0 + mdn-data: 2.24.0 + postcss-media-query-parser: 0.2.3 + postcss-resolve-nested-selector: 0.1.6 + postcss-selector-parser: 7.1.0 + postcss-value-parser: 4.2.0 + stylelint: 16.24.0(typescript@5.6.3) + + stylelint@16.24.0(typescript@5.6.3): + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/media-query-list-parser': 4.0.3(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/selector-specificity': 5.0.0(postcss-selector-parser@7.1.0) + '@dual-bundle/import-meta-resolve': 4.2.1 + balanced-match: 2.0.0 + colord: 2.9.3 + cosmiconfig: 9.0.0(typescript@5.6.3) + css-functions-list: 3.2.3 + css-tree: 3.1.0 + debug: 4.4.3 + fast-glob: 3.3.3 + fastest-levenshtein: 1.0.16 + file-entry-cache: 10.1.4 + global-modules: 2.0.0 + globby: 11.1.0 + globjoin: 0.1.4 + html-tags: 3.3.1 + ignore: 7.0.5 + imurmurhash: 0.1.4 + is-plain-object: 5.0.0 + known-css-properties: 0.37.0 + mathml-tag-names: 2.1.3 + meow: 13.2.0 + micromatch: 4.0.8 + normalize-path: 3.0.0 + picocolors: 1.1.1 + postcss: 8.5.6 + postcss-resolve-nested-selector: 0.1.6 + postcss-safe-parser: 7.0.1(postcss@8.5.6) + postcss-selector-parser: 7.1.0 + postcss-value-parser: 4.2.0 + resolve-from: 5.0.0 + string-width: 4.2.3 + supports-hyperlinks: 3.2.0 + svg-tags: 1.0.0 + table: 6.9.0 + write-file-atomic: 5.0.1 + transitivePeerDependencies: + - supports-color + - typescript + + superjson@2.2.2: + dependencies: + copy-anything: 3.0.5 + + supports-color@5.5.0: + dependencies: + has-flag: 3.0.0 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-hyperlinks@3.2.0: + dependencies: + has-flag: 4.0.0 + supports-color: 7.2.0 + + svg-tags@1.0.0: {} + + symbol-tree@3.2.4: {} + + synckit@0.11.11: + dependencies: + '@pkgr/core': 0.2.9 + + table@6.9.0: + dependencies: + ajv: 8.17.1 + lodash.truncate: 4.4.2 + slice-ansi: 4.0.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + tailwindcss@4.1.14: {} + + tapable@2.3.0: {} + + tar@7.5.1: + dependencies: + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.2 + minizlib: 3.1.0 + yallist: 5.0.0 + + terser@5.44.0: + dependencies: + '@jridgewell/source-map': 0.3.11 + acorn: 8.15.0 + commander: 2.20.3 + source-map-support: 0.5.21 + + text-extensions@2.4.0: {} + + through@2.3.8: {} + + tiny-warning@1.0.3: {} + + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinyexec@1.0.1: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + tinypool@1.1.1: {} + + tinyrainbow@1.2.0: {} + + tinyspy@3.0.2: {} + + tlbs-map-vue@1.3.2(vue@3.5.22(typescript@5.6.3)): + dependencies: + fs-extra: 10.1.0 + vue: 3.5.22(typescript@5.6.3) + vue-demi: 0.14.10(vue@3.5.22(typescript@5.6.3)) + + tmp@0.0.33: + dependencies: + os-tmpdir: 1.0.2 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + totalist@3.0.1: {} + + tough-cookie@4.1.4: + dependencies: + psl: 1.15.0 + punycode: 2.3.1 + universalify: 0.2.0 + url-parse: 1.5.10 + + tr46@5.1.1: + dependencies: + punycode: 2.3.1 + + ts-api-utils@2.1.0(typescript@5.6.3): + dependencies: + typescript: 5.6.3 + + tslib@2.3.0: {} + + tslib@2.8.1: {} + + tsx@4.20.6: + dependencies: + esbuild: 0.25.10 + get-tsconfig: 4.10.1 + optionalDependencies: + fsevents: 2.3.3 + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-fest@0.20.2: {} + + type-fest@0.21.3: {} + + type@2.7.3: {} + + typescript-eslint@8.44.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.6.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.44.1(@typescript-eslint/parser@8.44.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.6.3))(eslint@9.36.0(jiti@2.6.0))(typescript@5.6.3) + '@typescript-eslint/parser': 8.44.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.6.3) + '@typescript-eslint/typescript-estree': 8.44.1(typescript@5.6.3) + '@typescript-eslint/utils': 8.44.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.6.3) + eslint: 9.36.0(jiti@2.6.0) + typescript: 5.6.3 + transitivePeerDependencies: + - supports-color + + typescript@5.6.3: {} + + ufo@1.6.1: {} + + undici-types@7.14.0: {} + + unicorn-magic@0.1.0: {} + + unicorn-magic@0.3.0: {} + + unimport@5.4.0: + dependencies: + acorn: 8.15.0 + escape-string-regexp: 5.0.0 + estree-walker: 3.0.3 + local-pkg: 1.1.2 + magic-string: 0.30.19 + mlly: 1.8.0 + pathe: 2.0.3 + picomatch: 4.0.3 + pkg-types: 2.3.0 + scule: 1.3.0 + strip-literal: 3.1.0 + tinyglobby: 0.2.15 + unplugin: 2.3.10 + unplugin-utils: 0.3.0 + + universalify@0.2.0: {} + + universalify@2.0.1: {} + + unplugin-auto-import@20.2.0(@vueuse/core@13.9.0(vue@3.5.22(typescript@5.6.3))): + dependencies: + local-pkg: 1.1.2 + magic-string: 0.30.19 + picomatch: 4.0.3 + unimport: 5.4.0 + unplugin: 2.3.10 + unplugin-utils: 0.3.0 + optionalDependencies: + '@vueuse/core': 13.9.0(vue@3.5.22(typescript@5.6.3)) + + unplugin-element-plus@0.10.0: + dependencies: + es-module-lexer: 1.7.0 + magic-string: 0.30.19 + unplugin: 2.3.10 + unplugin-utils: 0.2.5 + + unplugin-utils@0.2.5: + dependencies: + pathe: 2.0.3 + picomatch: 4.0.3 + + unplugin-utils@0.3.0: + dependencies: + pathe: 2.0.3 + picomatch: 4.0.3 + + unplugin-vue-components@29.1.0(@babel/parser@7.28.4)(vue@3.5.22(typescript@5.6.3)): + dependencies: + chokidar: 3.6.0 + debug: 4.4.3 + local-pkg: 1.1.2 + magic-string: 0.30.19 + mlly: 1.8.0 + tinyglobby: 0.2.15 + unplugin: 2.3.10 + unplugin-utils: 0.3.0 + vue: 3.5.22(typescript@5.6.3) + optionalDependencies: + '@babel/parser': 7.28.4 + transitivePeerDependencies: + - supports-color + + unplugin@2.3.10: + dependencies: + '@jridgewell/remapping': 2.3.5 + acorn: 8.15.0 + picomatch: 4.0.3 + webpack-virtual-modules: 0.6.2 + + update-browserslist-db@1.1.3(browserslist@4.26.2): + dependencies: + browserslist: 4.26.2 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + url-parse@1.5.10: + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + + util-deprecate@1.0.2: {} + + vite-hot-client@2.1.0(vite@7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)): + dependencies: + vite: 7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + + vite-node@2.1.9(@types/node@24.8.1)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 1.1.2 + vite: 5.4.21(@types/node@24.8.1)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vite-plugin-compression@0.5.1(vite@7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)): + dependencies: + chalk: 4.1.2 + debug: 4.4.3 + fs-extra: 10.1.0 + vite: 7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + transitivePeerDependencies: + - supports-color + + vite-plugin-inspect@0.8.9(rollup@4.52.3)(vite@7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)): + dependencies: + '@antfu/utils': 0.7.10 + '@rollup/pluginutils': 5.3.0(rollup@4.52.3) + debug: 4.4.3 + error-stack-parser-es: 0.1.5 + fs-extra: 11.3.2 + open: 10.2.0 + perfect-debounce: 1.0.0 + picocolors: 1.1.1 + sirv: 3.0.2 + vite: 7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + transitivePeerDependencies: + - rollup + - supports-color + + vite-plugin-vue-devtools@7.7.7(rollup@4.52.3)(vite@7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.22(typescript@5.6.3)): + dependencies: + '@vue/devtools-core': 7.7.7(vite@7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.22(typescript@5.6.3)) + '@vue/devtools-kit': 7.7.7 + '@vue/devtools-shared': 7.7.7 + execa: 9.6.0 + sirv: 3.0.2 + vite: 7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vite-plugin-inspect: 0.8.9(rollup@4.52.3)(vite@7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + vite-plugin-vue-inspector: 5.3.2(vite@7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + transitivePeerDependencies: + - '@nuxt/kit' + - rollup + - supports-color + - vue + + vite-plugin-vue-inspector@5.3.2(vite@7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)): + dependencies: + '@babel/core': 7.28.4 + '@babel/plugin-proposal-decorators': 7.28.0(@babel/core@7.28.4) + '@babel/plugin-syntax-import-attributes': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.28.4) + '@babel/plugin-transform-typescript': 7.28.0(@babel/core@7.28.4) + '@vue/babel-plugin-jsx': 1.5.0(@babel/core@7.28.4) + '@vue/compiler-dom': 3.5.22 + kolorist: 1.8.0 + magic-string: 0.30.19 + vite: 7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + transitivePeerDependencies: + - supports-color + + vite@5.4.21(@types/node@24.8.1)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0): + dependencies: + esbuild: 0.21.5 + postcss: 8.5.6 + rollup: 4.52.3 + optionalDependencies: + '@types/node': 24.8.1 + fsevents: 2.3.3 + lightningcss: 1.30.1 + sass: 1.93.2 + terser: 5.44.0 + + vite@7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1): + dependencies: + esbuild: 0.25.10 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.52.3 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 24.8.1 + fsevents: 2.3.3 + jiti: 2.6.0 + lightningcss: 1.30.1 + sass: 1.93.2 + terser: 5.44.0 + tsx: 4.20.6 + yaml: 2.8.1 + + vitest@2.1.9(@types/node@24.8.1)(jsdom@24.1.3)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0): + dependencies: + '@vitest/expect': 2.1.9 + '@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@24.8.1)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)) + '@vitest/pretty-format': 2.1.9 + '@vitest/runner': 2.1.9 + '@vitest/snapshot': 2.1.9 + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.3.0 + magic-string: 0.30.19 + pathe: 1.1.2 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinypool: 1.1.1 + tinyrainbow: 1.2.0 + vite: 5.4.21(@types/node@24.8.1)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0) + vite-node: 2.1.9(@types/node@24.8.1)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 24.8.1 + jsdom: 24.1.3 + transitivePeerDependencies: + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vscode-uri@3.1.0: {} + + vue-component-type-helpers@2.2.12: {} + + vue-demi@0.14.10(vue@3.5.22(typescript@5.6.3)): + dependencies: + vue: 3.5.22(typescript@5.6.3) + + vue-draggable-plus@0.6.0(@types/sortablejs@1.15.8): + dependencies: + '@types/sortablejs': 1.15.8 + + vue-eslint-parser@9.4.3(eslint@9.36.0(jiti@2.6.0)): + dependencies: + debug: 4.4.3 + eslint: 9.36.0(jiti@2.6.0) + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.6.0 + lodash: 4.17.21 + semver: 7.7.2 + transitivePeerDependencies: + - supports-color + + vue-i18n@9.14.5(vue@3.5.22(typescript@5.6.3)): + dependencies: + '@intlify/core-base': 9.14.5 + '@intlify/shared': 9.14.5 + '@vue/devtools-api': 6.6.4 + vue: 3.5.22(typescript@5.6.3) + + vue-img-cutter@3.0.7(typescript@5.6.3): + dependencies: + core-js: 3.45.1 + vue: 3.5.22(typescript@5.6.3) + vue-i18n: 9.14.5(vue@3.5.22(typescript@5.6.3)) + transitivePeerDependencies: + - typescript + + vue-router@4.5.1(vue@3.5.22(typescript@5.6.3)): + dependencies: + '@vue/devtools-api': 6.6.4 + vue: 3.5.22(typescript@5.6.3) + + vue-tsc@2.1.10(typescript@5.6.3): + dependencies: + '@volar/typescript': 2.4.23 + '@vue/language-core': 2.1.10(typescript@5.6.3) + semver: 7.7.2 + typescript: 5.6.3 + + vue@3.5.22(typescript@5.6.3): + dependencies: + '@vue/compiler-dom': 3.5.22 + '@vue/compiler-sfc': 3.5.22 + '@vue/runtime-dom': 3.5.22 + '@vue/server-renderer': 3.5.22(vue@3.5.22(typescript@5.6.3)) + '@vue/shared': 3.5.22 + optionalDependencies: + typescript: 5.6.3 + + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + + wcwidth@1.0.1: + dependencies: + defaults: 1.0.4 + + webidl-conversions@7.0.0: {} + + webpack-virtual-modules@0.6.2: {} + + whatwg-encoding@3.1.1: + dependencies: + iconv-lite: 0.6.3 + + whatwg-mimetype@4.0.0: {} + + whatwg-url@14.2.0: + dependencies: + tr46: 5.1.1 + webidl-conversions: 7.0.0 + + which@1.3.1: + dependencies: + isexe: 2.0.0 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + wildcard@1.1.2: {} + + wmf@1.0.2: {} + + word-wrap@1.2.5: {} + + word@0.3.0: {} + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.1.2 + + wrap-ansi@9.0.2: + dependencies: + ansi-styles: 6.2.3 + string-width: 7.2.0 + strip-ansi: 7.1.2 + + wrappy@1.0.2: {} + + write-file-atomic@5.0.1: + dependencies: + imurmurhash: 0.1.4 + signal-exit: 4.1.0 + + ws@8.18.3: {} + + wsl-utils@0.1.0: + dependencies: + is-wsl: 3.1.0 + + xgplayer-subtitles@3.0.23(core-js@3.45.1): + dependencies: + core-js: 3.45.1 + eventemitter3: 4.0.7 + + xgplayer@3.0.23(core-js@3.45.1): + dependencies: + core-js: 3.45.1 + danmu.js: 1.1.13 + delegate: 3.2.0 + downloadjs: 1.4.7 + eventemitter3: 4.0.7 + xgplayer-subtitles: 3.0.23(core-js@3.45.1) + + xlsx@0.18.5: + dependencies: + adler-32: 1.3.1 + cfb: 1.2.2 + codepage: 1.15.0 + crc-32: 1.2.2 + ssf: 0.11.2 + wmf: 1.0.2 + word: 0.3.0 + + xml-name-validator@4.0.0: {} + + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + + y18n@5.0.8: {} + + yallist@3.1.1: {} + + yallist@5.0.0: {} + + yaml@2.8.1: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yocto-queue@0.1.0: {} + + yocto-queue@1.2.1: {} + + yoctocolors@2.1.2: {} + + zrender@6.0.0: + dependencies: + tslib: 2.3.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..4226ea1 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,7 @@ +onlyBuiltDependencies: + - '@parcel/watcher' + - '@tailwindcss/oxide' + - core-js + - es5-ext + - esbuild + - vue-demi 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/scripts/clean-dev.ts b/scripts/clean-dev.ts new file mode 100644 index 0000000..cc0b9bc --- /dev/null +++ b/scripts/clean-dev.ts @@ -0,0 +1,838 @@ +// scripts/clean-dev.ts +import fs from 'fs/promises' +import path from 'path' + +// 现代化颜色主题 +const theme = { + // 基础颜色 + reset: '\x1b[0m', + bold: '\x1b[1m', + dim: '\x1b[2m', + + // 前景色 + primary: '\x1b[38;5;75m', // 亮蓝色 + success: '\x1b[38;5;82m', // 亮绿色 + warning: '\x1b[38;5;220m', // 亮黄色 + error: '\x1b[38;5;196m', // 亮红色 + info: '\x1b[38;5;159m', // 青色 + purple: '\x1b[38;5;141m', // 紫色 + orange: '\x1b[38;5;208m', // 橙色 + gray: '\x1b[38;5;245m', // 灰色 + white: '\x1b[38;5;255m', // 白色 + + // 背景色 + bgDark: '\x1b[48;5;235m', // 深灰背景 + bgBlue: '\x1b[48;5;24m', // 蓝色背景 + bgGreen: '\x1b[48;5;22m', // 绿色背景 + bgRed: '\x1b[48;5;52m' // 红色背景 +} + +// 现代化图标集 +const icons = { + rocket: '🚀', + fire: '🔥', + star: '⭐', + gem: '💎', + crown: '👑', + magic: '✨', + warning: '⚠️', + success: '✅', + error: '❌', + info: 'ℹ️', + folder: '📁', + file: '📄', + image: '🖼️', + code: '💻', + data: '📊', + globe: '🌐', + map: '🗺️', + chat: '💬', + bolt: '⚡', + shield: '🛡️', + key: '🔑', + link: '🔗', + clean: '🧹', + trash: '🗑️', + check: '✓', + cross: '✗', + arrow: '→', + loading: '⏳' +} + +// 格式化工具 +const fmt = { + title: (text: string) => `${theme.bold}${theme.primary}${text}${theme.reset}`, + subtitle: (text: string) => `${theme.purple}${text}${theme.reset}`, + success: (text: string) => `${theme.success}${text}${theme.reset}`, + error: (text: string) => `${theme.error}${text}${theme.reset}`, + warning: (text: string) => `${theme.warning}${text}${theme.reset}`, + info: (text: string) => `${theme.info}${text}${theme.reset}`, + highlight: (text: string) => `${theme.bold}${theme.white}${text}${theme.reset}`, + dim: (text: string) => `${theme.dim}${theme.gray}${text}${theme.reset}`, + orange: (text: string) => `${theme.orange}${text}${theme.reset}`, + + // 带背景的文本 + badge: (text: string, bg: string = theme.bgBlue) => + `${bg}${theme.white}${theme.bold} ${text} ${theme.reset}`, + + // 渐变效果模拟 + gradient: (text: string) => { + const colors = ['\x1b[38;5;75m', '\x1b[38;5;81m', '\x1b[38;5;87m', '\x1b[38;5;159m'] + const chars = text.split('') + return chars.map((char, i) => `${colors[i % colors.length]}${char}`).join('') + theme.reset + } +} + +// 创建现代化标题横幅 +function createModernBanner() { + console.log() + console.log( + fmt.gradient(' ╔══════════════════════════════════════════════════════════════════╗') + ) + console.log( + fmt.gradient(' ║ ║') + ) + console.log( + ` ║ ${icons.rocket} ${fmt.title('ART DESIGN PRO')} ${fmt.subtitle('· 代码精简程序')} ${icons.magic} ║` + ) + console.log( + ` ║ ${fmt.dim('为项目移除演示数据,快速切换至开发模式')} ║` + ) + console.log( + fmt.gradient(' ║ ║') + ) + console.log( + fmt.gradient(' ╚══════════════════════════════════════════════════════════════════╝') + ) + console.log() +} + +// 创建分割线 +function createDivider(char = '─', color = theme.primary) { + console.log(`${color}${' ' + char.repeat(66)}${theme.reset}`) +} + +// 创建卡片样式容器 +function createCard(title: string, content: string[]) { + console.log(` ${fmt.badge('', theme.bgBlue)} ${fmt.title(title)}`) + console.log() + content.forEach((line) => { + console.log(` ${line}`) + }) + console.log() +} + +// 进度条动画 +function createProgressBar(current: number, total: number, text: string, width = 40) { + const percentage = Math.round((current / total) * 100) + const filled = Math.round((current / total) * width) + const empty = width - filled + + const filledBar = '█'.repeat(filled) + const emptyBar = '░'.repeat(empty) + + process.stdout.write( + `\r ${fmt.info('进度')} [${theme.success}${filledBar}${theme.gray}${emptyBar}${theme.reset}] ${fmt.highlight(percentage + '%')})}` + ) + + if (current === total) { + console.log() + } +} + +// 统计信息 +const stats = { + deletedFiles: 0, + deletedPaths: 0, + failedPaths: 0, + startTime: Date.now(), + totalFiles: 0 +} + +// 清理目标 +const targets = [ + 'README.md', + 'README.zh-CN.md', + 'CHANGELOG.md', + 'CHANGELOG.zh-CN.md', + 'src/views/change', + 'src/views/safeguard', + 'src/views/article', + 'src/views/examples', + 'src/views/system/nested', + 'src/views/widgets', + 'src/views/template', + 'src/views/dashboard/analysis', + 'src/views/dashboard/ecommerce', + 'src/mock/json', + 'src/mock/temp/articleList.ts', + 'src/mock/temp/commentDetail.ts', + 'src/mock/temp/commentList.ts', + 'src/assets/images/cover', + 'src/assets/images/safeguard', + 'src/assets/images/3d', + 'src/components/core/charts/art-map-chart', + 'src/components/business/comment-widget' +] + +// 递归统计文件数量 +async function countFiles(targetPath: string): Promise { + const fullPath = path.resolve(process.cwd(), targetPath) + + try { + const stat = await fs.stat(fullPath) + + if (stat.isFile()) { + return 1 + } else if (stat.isDirectory()) { + const entries = await fs.readdir(fullPath) + let count = 0 + + for (const entry of entries) { + const entryPath = path.join(targetPath, entry) + count += await countFiles(entryPath) + } + + return count + } + } catch { + return 0 + } + + return 0 +} + +// 统计所有目标的文件数量 +async function countAllFiles(): Promise { + let totalCount = 0 + + for (const target of targets) { + const count = await countFiles(target) + totalCount += count + } + + return totalCount +} + +// 删除文件和目录 +async function remove(targetPath: string, index: number) { + const fullPath = path.resolve(process.cwd(), targetPath) + + createProgressBar(index + 1, targets.length, targetPath) + + try { + const fileCount = await countFiles(targetPath) + await fs.rm(fullPath, { recursive: true, force: true }) + stats.deletedFiles += fileCount + stats.deletedPaths++ + await new Promise((resolve) => setTimeout(resolve, 50)) + } catch (err) { + stats.failedPaths++ + console.log() + console.log(` ${icons.error} ${fmt.error('删除失败')}: ${fmt.highlight(targetPath)}`) + console.log(` ${fmt.dim('错误详情: ' + err)}`) + } +} + +// 清理路由模块 +async function cleanRouteModules() { + const modulesPath = path.resolve(process.cwd(), 'src/router/modules') + + try { + // 删除演示相关的路由模块 + const modulesToRemove = [ + 'template.ts', + 'widgets.ts', + 'examples.ts', + 'article.ts', + 'safeguard.ts', + 'help.ts' + ] + + for (const module of modulesToRemove) { + const modulePath = path.join(modulesPath, module) + try { + await fs.rm(modulePath, { force: true }) + } catch { + // 文件不存在时忽略错误 + } + } + + // 重写 dashboard.ts - 只保留 console + const dashboardContent = `import { AppRouteRecord } from '@/types/router' + +export const dashboardRoutes: AppRouteRecord = { + name: 'Dashboard', + path: '/dashboard', + component: '/index/index', + meta: { + title: 'menus.dashboard.title', + icon: 'ri:pie-chart-line', + roles: ['R_SUPER', 'R_ADMIN'] + }, + children: [ + { + path: 'console', + name: 'Console', + component: '/dashboard/console', + meta: { + title: 'menus.dashboard.console', + keepAlive: false, + fixedTab: true + } + } + ] +} +` + await fs.writeFile(path.join(modulesPath, 'dashboard.ts'), dashboardContent, 'utf-8') + + // 重写 system.ts - 移除 nested 嵌套菜单 + const systemContent = `import { AppRouteRecord } from '@/types/router' + +export const systemRoutes: AppRouteRecord = { + path: '/system', + name: 'System', + component: '/index/index', + meta: { + title: 'menus.system.title', + icon: 'ri:user-3-line', + roles: ['R_SUPER', 'R_ADMIN'] + }, + children: [ + { + path: 'user', + name: 'User', + component: '/system/user', + meta: { + title: 'menus.system.user', + keepAlive: true, + roles: ['R_SUPER', 'R_ADMIN'] + } + }, + { + path: 'role', + name: 'Role', + component: '/system/role', + meta: { + title: 'menus.system.role', + keepAlive: true, + roles: ['R_SUPER'] + } + }, + { + path: 'user-center', + name: 'UserCenter', + component: '/system/user-center', + meta: { + title: 'menus.system.userCenter', + isHide: true, + keepAlive: true, + isHideTab: true + } + }, + { + path: 'menu', + name: 'Menus', + component: '/system/menu', + meta: { + title: 'menus.system.menu', + keepAlive: true, + roles: ['R_SUPER'], + authList: [ + { title: '新增', authMark: 'add' }, + { title: '编辑', authMark: 'edit' }, + { title: '删除', authMark: 'delete' } + ] + } + } + ] +} +` + await fs.writeFile(path.join(modulesPath, 'system.ts'), systemContent, 'utf-8') + + // 重写 index.ts - 只导入保留的模块 + const indexContent = `import { AppRouteRecord } from '@/types/router' +import { dashboardRoutes } from './dashboard' +import { systemRoutes } from './system' +import { resultRoutes } from './result' +import { exceptionRoutes } from './exception' + +/** + * 导出所有模块化路由 + */ +export const routeModules: AppRouteRecord[] = [ + dashboardRoutes, + systemRoutes, + resultRoutes, + exceptionRoutes +] +` + await fs.writeFile(path.join(modulesPath, 'index.ts'), indexContent, 'utf-8') + + console.log(` ${icons.success} ${fmt.success('清理路由模块完成')}`) + } catch (err) { + console.log(` ${icons.error} ${fmt.error('清理路由模块失败')}`) + console.log(` ${fmt.dim('错误详情: ' + err)}`) + } +} + +// 清理路由别名 +async function cleanRoutesAlias() { + const routesAliasPath = path.resolve(process.cwd(), 'src/router/routesAlias.ts') + + try { + const cleanedAlias = `/** + * 公共路由别名 + # 存放系统级公共路由路径,如布局容器、登录页等 + */ +export enum RoutesAlias { + Layout = '/index/index', // 布局容器 + Login = '/auth/login' // 登录页 +} +` + + await fs.writeFile(routesAliasPath, cleanedAlias, 'utf-8') + console.log(` ${icons.success} ${fmt.success('重写路由别名配置完成')}`) + } catch (err) { + console.log(` ${icons.error} ${fmt.error('清理路由别名失败')}`) + console.log(` ${fmt.dim('错误详情: ' + err)}`) + } +} + +// 清理变更日志 +async function cleanChangeLog() { + const changeLogPath = path.resolve(process.cwd(), 'src/mock/upgrade/changeLog.ts') + + try { + const cleanedChangeLog = `import { ref } from 'vue' + +interface UpgradeLog { + version: string // 版本号 + title: string // 更新标题 + date: string // 更新日期 + detail?: string[] // 更新内容 + requireReLogin?: boolean // 是否需要重新登录 + remark?: string // 备注 +} + +export const upgradeLogList = ref([]) +` + + await fs.writeFile(changeLogPath, cleanedChangeLog, 'utf-8') + console.log(` ${icons.success} ${fmt.success('清空变更日志数据完成')}`) + } catch (err) { + console.log(` ${icons.error} ${fmt.error('清理变更日志失败')}`) + console.log(` ${fmt.dim('错误详情: ' + err)}`) + } +} + +// 清理语言文件 +async function cleanLanguageFiles() { + const languageFiles = [ + { path: 'src/locales/langs/zh.json', name: '中文语言文件' }, + { path: 'src/locales/langs/en.json', name: '英文语言文件' } + ] + + for (const { path: langPath, name } of languageFiles) { + try { + const fullPath = path.resolve(process.cwd(), langPath) + const content = await fs.readFile(fullPath, 'utf-8') + const langData = JSON.parse(content) + + const menusToRemove = [ + 'widgets', + 'template', + 'article', + 'examples', + 'safeguard', + 'plan', + 'help' + ] + + if (langData.menus) { + menusToRemove.forEach((menuKey) => { + if (langData.menus[menuKey]) { + delete langData.menus[menuKey] + } + }) + + if (langData.menus.dashboard) { + if (langData.menus.dashboard.analysis) { + delete langData.menus.dashboard.analysis + } + if (langData.menus.dashboard.ecommerce) { + delete langData.menus.dashboard.ecommerce + } + } + + if (langData.menus.system) { + const systemKeysToRemove = [ + 'nested', + 'menu1', + 'menu2', + 'menu21', + 'menu3', + 'menu31', + 'menu32', + 'menu321' + ] + systemKeysToRemove.forEach((key) => { + if (langData.menus.system[key]) { + delete langData.menus.system[key] + } + }) + } + } + + await fs.writeFile(fullPath, JSON.stringify(langData, null, 2), 'utf-8') + console.log(` ${icons.success} ${fmt.success(`清理${name}完成`)}`) + } catch (err) { + console.log(` ${icons.error} ${fmt.error(`清理${name}失败`)}`) + console.log(` ${fmt.dim('错误详情: ' + err)}`) + } + } +} + +// 清理快速入口组件 +async function cleanFastEnterComponent() { + const fastEnterPath = path.resolve(process.cwd(), 'src/config/fastEnter.ts') + + try { + const cleanedFastEnter = `/** + * 快速入口配置 + * 包含:应用列表、快速链接等配置 + */ +import { WEB_LINKS } from '@/utils/constants' +import type { FastEnterConfig } from '@/types/config' + +const fastEnterConfig: FastEnterConfig = { + // 显示条件(屏幕宽度) + minWidth: 1200, + // 应用列表 + applications: [ + { + name: '工作台', + description: '系统概览与数据统计', + icon: 'ri:pie-chart-line', + iconColor: '#377dff', + enabled: true, + order: 1, + routeName: 'Console' + }, + { + name: '官方文档', + description: '使用指南与开发文档', + icon: 'ri:bill-line', + iconColor: '#ffb100', + enabled: true, + order: 2, + link: WEB_LINKS.DOCS + }, + { + name: '技术支持', + description: '技术支持与问题反馈', + icon: 'ri:user-location-line', + iconColor: '#ff6b6b', + enabled: true, + order: 3, + link: WEB_LINKS.COMMUNITY + }, + { + name: '哔哩哔哩', + description: '技术分享与交流', + icon: 'ri:bilibili-line', + iconColor: '#FB7299', + enabled: true, + order: 4, + link: WEB_LINKS.BILIBILI + } + ], + // 快速链接 + quickLinks: [ + { + name: '登录', + enabled: true, + order: 1, + routeName: 'Login' + }, + { + name: '注册', + enabled: true, + order: 2, + routeName: 'Register' + }, + { + name: '忘记密码', + enabled: true, + order: 3, + routeName: 'ForgetPassword' + }, + { + name: '个人中心', + enabled: true, + order: 4, + routeName: 'UserCenter' + } + ] +} + +export default Object.freeze(fastEnterConfig) +` + + await fs.writeFile(fastEnterPath, cleanedFastEnter, 'utf-8') + console.log(` ${icons.success} ${fmt.success('清理快速入口配置完成')}`) + } catch (err) { + console.log(` ${icons.error} ${fmt.error('清理快速入口配置失败')}`) + console.log(` ${fmt.dim('错误详情: ' + err)}`) + } +} + +// 更新菜单接口 +async function updateMenuApi() { + const apiPath = path.resolve(process.cwd(), 'src/api/system-manage.ts') + + try { + const content = await fs.readFile(apiPath, 'utf-8') + const updatedContent = content.replace( + "url: '/api/v3/system/menus'", + "url: '/api/v3/system/menus/simple'" + ) + + await fs.writeFile(apiPath, updatedContent, 'utf-8') + console.log(` ${icons.success} ${fmt.success('更新菜单接口完成')}`) + } catch (err) { + console.log(` ${icons.error} ${fmt.error('更新菜单接口失败')}`) + console.log(` ${fmt.dim('错误详情: ' + err)}`) + } +} + +// 用户确认函数 +async function getUserConfirmation(): Promise { + const { createInterface } = await import('readline') + + return new Promise((resolve) => { + const rl = createInterface({ + input: process.stdin, + output: process.stdout + }) + + console.log( + ` ${fmt.highlight('请输入')} ${fmt.success('yes')} ${fmt.highlight('确认执行清理操作,或按 Enter 取消')}` + ) + console.log() + process.stdout.write(` ${icons.arrow} `) + + rl.question('', (answer: string) => { + rl.close() + resolve(answer.toLowerCase().trim() === 'yes') + }) + }) +} + +// 显示清理警告 +async function showCleanupWarning() { + createCard('安全警告', [ + `${fmt.warning('此操作将永久删除以下演示内容,且无法恢复!')}`, + `${fmt.dim('请仔细阅读清理列表,确认后再继续操作')}` + ]) + + const cleanupItems = [ + { + icon: icons.image, + name: '图片资源', + desc: '演示用的封面图片、3D图片、运维图片等', + color: theme.orange + }, + { + icon: icons.file, + name: '演示页面', + desc: 'widgets、template、article、examples、safeguard等页面', + color: theme.purple + }, + { + icon: icons.code, + name: '路由模块文件', + desc: '删除演示路由模块,只保留核心模块(dashboard、system、result、exception)', + color: theme.primary + }, + { + icon: icons.link, + name: '路由别名', + desc: '重写routesAlias.ts,移除演示路由别名', + color: theme.info + }, + { + icon: icons.data, + name: 'Mock数据', + desc: '演示用的JSON数据、文章列表、评论数据等', + color: theme.success + }, + { + icon: icons.globe, + name: '多语言文件', + desc: '清理中英文语言包中的演示菜单项', + color: theme.warning + }, + { icon: icons.map, name: '地图组件', desc: '移除art-map-chart地图组件', color: theme.error }, + { icon: icons.chat, name: '评论组件', desc: '移除comment-widget评论组件', color: theme.orange }, + { + icon: icons.bolt, + name: '快速入口', + desc: '移除分析页、礼花效果、聊天、更新日志、定价、留言管理等无效项目', + color: theme.purple + } + ] + + console.log(` ${fmt.badge('', theme.bgRed)} ${fmt.title('将要清理的内容')}`) + console.log() + + cleanupItems.forEach((item, index) => { + console.log(` ${item.color}${theme.reset} ${fmt.highlight(`${index + 1}. ${item.name}`)}`) + console.log(` ${fmt.dim(item.desc)}`) + }) + + console.log() + console.log(` ${fmt.badge('', theme.bgGreen)} ${fmt.title('保留的功能模块')}`) + console.log() + + const preservedModules = [ + { name: 'Dashboard', desc: '工作台页面' }, + { name: 'System', desc: '系统管理模块' }, + { name: 'Result', desc: '结果页面' }, + { name: 'Exception', desc: '异常页面' }, + { name: 'Auth', desc: '登录注册功能' }, + { name: 'Core Components', desc: '核心组件库' } + ] + + preservedModules.forEach((module) => { + console.log(` ${icons.check} ${fmt.success(module.name)} ${fmt.dim(`- ${module.desc}`)}`) + }) + + console.log() + createDivider() + console.log() +} + +// 显示统计信息 +async function showStats() { + const duration = Date.now() - stats.startTime + const seconds = (duration / 1000).toFixed(2) + + console.log() + createCard('清理统计', [ + `${fmt.success('成功删除')}: ${fmt.highlight(stats.deletedFiles.toString())} 个文件`, + `${fmt.info('涉及路径')}: ${fmt.highlight(stats.deletedPaths.toString())} 个目录/文件`, + ...(stats.failedPaths > 0 + ? [ + `${icons.error} ${fmt.error('删除失败')}: ${fmt.highlight(stats.failedPaths.toString())} 个路径` + ] + : []), + `${fmt.info('耗时')}: ${fmt.highlight(seconds)} 秒` + ]) +} + +// 创建成功横幅 +function createSuccessBanner() { + console.log() + console.log( + fmt.gradient(' ╔══════════════════════════════════════════════════════════════════╗') + ) + console.log( + fmt.gradient(' ║ ║') + ) + console.log( + ` ║ ${icons.star} ${fmt.success('清理完成!项目已准备就绪')} ${icons.rocket} ║` + ) + console.log( + ` ║ ${fmt.dim('现在可以开始您的开发之旅了!')} ║` + ) + console.log( + fmt.gradient(' ║ ║') + ) + console.log( + fmt.gradient(' ╚══════════════════════════════════════════════════════════════════╝') + ) + console.log() +} + +// 主函数 +async function main() { + // 清屏并显示横幅 + console.clear() + createModernBanner() + + // 显示清理警告 + await showCleanupWarning() + + // 统计文件数量 + console.log(` ${fmt.info('正在统计文件数量...')}`) + stats.totalFiles = await countAllFiles() + + console.log(` ${fmt.info('即将清理')}: ${fmt.highlight(stats.totalFiles.toString())} 个文件`) + console.log(` ${fmt.dim(`涉及 ${targets.length} 个目录/文件路径`)}`) + console.log() + + // 用户确认 + const confirmed = await getUserConfirmation() + + if (!confirmed) { + console.log(` ${fmt.warning('操作已取消,清理中止')}`) + console.log() + return + } + + console.log() + console.log(` ${icons.check} ${fmt.success('确认成功,开始清理...')}`) + console.log() + + // 开始清理过程 + console.log(` ${fmt.badge('步骤 1/6', theme.bgBlue)} ${fmt.title('删除演示文件')}`) + console.log() + for (let i = 0; i < targets.length; i++) { + await remove(targets[i], i) + } + console.log() + + console.log(` ${fmt.badge('步骤 2/6', theme.bgBlue)} ${fmt.title('清理路由模块')}`) + console.log() + await cleanRouteModules() + console.log() + + console.log(` ${fmt.badge('步骤 3/6', theme.bgBlue)} ${fmt.title('重写路由别名')}`) + console.log() + await cleanRoutesAlias() + console.log() + + console.log(` ${fmt.badge('步骤 4/6', theme.bgBlue)} ${fmt.title('清空变更日志')}`) + console.log() + await cleanChangeLog() + console.log() + + console.log(` ${fmt.badge('步骤 5/6', theme.bgBlue)} ${fmt.title('清理语言文件')}`) + console.log() + await cleanLanguageFiles() + console.log() + + console.log(` ${fmt.badge('步骤 6/7', theme.bgBlue)} ${fmt.title('清理快速入口')}`) + console.log() + await cleanFastEnterComponent() + console.log() + + console.log(` ${fmt.badge('步骤 7/7', theme.bgBlue)} ${fmt.title('更新菜单接口')}`) + console.log() + await updateMenuApi() + + // 显示统计信息 + await showStats() + + // 显示成功横幅 + createSuccessBanner() +} + +main().catch((err) => { + console.log() + console.log(` ${icons.error} ${fmt.error('清理脚本执行出错')}`) + console.log(` ${fmt.dim('错误详情: ' + err)}`) + console.log() + process.exit(1) +}) diff --git a/src/App.vue b/src/App.vue new file mode 100644 index 0000000..3433913 --- /dev/null +++ b/src/App.vue @@ -0,0 +1,34 @@ + + + diff --git a/src/api/announcement.ts b/src/api/announcement.ts new file mode 100644 index 0000000..67de76c --- /dev/null +++ b/src/api/announcement.ts @@ -0,0 +1,254 @@ +/** + * 文件用途:公告模块 API 封装 + * 说明:统一对接平台/租户/应用端公告接口 + * 日期:2025-12-20 + */ + +import request from '@/utils/http' +import type { + AnnouncementCommand, + AnnouncementFormData, + AnnouncementQueryParams, + TenantAnnouncementDto +} from '@/types/announcement' +import type { PagedResult } from '@/types/common/response' + +/** 发布/撤销命令体(仅包含 RowVersion) */ +type RowVersionCommand = Pick + +const withTenantHeader = (tenantId: string) => ({ + headers: { + 'X-Tenant-Id': tenantId + } +}) + +const normalizeQueryParams = (params: AnnouncementQueryParams) => { + const { dateFrom, dateTo, ...rest } = params + return { + ...rest, + effectiveFrom: params.effectiveFrom ?? dateFrom, + effectiveTo: params.effectiveTo ?? dateTo + } +} + +/** + * 平台公告 API + * 路由前缀:/api/platform/announcements + */ +export const platformAnnouncementApi = { + /** + * 创建平台公告 + * @param data 创建参数 + */ + create: (data: AnnouncementFormData) => { + return request.post({ + url: '/api/platform/announcements', + data + }) + }, + + /** + * 查询平台公告列表 + * @param params 查询参数 + */ + list: (params: AnnouncementQueryParams) => { + return request.get>({ + url: '/api/platform/announcements', + params: normalizeQueryParams(params) + }) + }, + + /** + * 获取平台公告详情 + * @param announcementId 公告ID + */ + detail: (announcementId: string) => { + return request.get({ + url: `/api/platform/announcements/${announcementId}` + }) + }, + + /** + * 更新平台公告(仅草稿) + * @param announcementId 公告ID + * @param data 更新参数 + */ + update: (announcementId: string, data: AnnouncementFormData) => { + return request.put({ + url: `/api/platform/announcements/${announcementId}`, + data + }) + }, + + /** + * 发布平台公告 + * @param announcementId 公告ID + * @param command 命令体(包含 rowVersion) + */ + publish: (announcementId: string, command: RowVersionCommand) => { + return request.post({ + url: `/api/platform/announcements/${announcementId}/publish`, + data: command + }) + }, + + /** + * 撤销平台公告 + * @param announcementId 公告ID + * @param command 命令体(包含 rowVersion) + */ + revoke: (announcementId: string, command: RowVersionCommand) => { + return request.post({ + url: `/api/platform/announcements/${announcementId}/revoke`, + data: command + }) + } +} + +/** + * 租户公告 API + * 路由前缀:/api/admin/v1/tenants/{tenantId}/announcements + */ +export const tenantAnnouncementApi = { + /** + * 分页查询租户公告 + * @param tenantId 租户ID + * @param params 查询参数 + */ + list: (tenantId: string, params: AnnouncementQueryParams) => { + return request.get>({ + url: `/api/admin/v1/tenants/${tenantId}/announcements`, + params: normalizeQueryParams(params), + ...withTenantHeader(tenantId) + }) + }, + + /** + * 获取租户公告详情 + * @param tenantId 租户ID + * @param announcementId 公告ID + */ + detail: (tenantId: string, announcementId: string) => { + return request.get({ + url: `/api/admin/v1/tenants/${tenantId}/announcements/${announcementId}`, + ...withTenantHeader(tenantId) + }) + }, + + /** + * 创建租户公告 + * @param tenantId 租户ID + * @param data 创建参数 + */ + create: (tenantId: string, data: AnnouncementFormData) => { + return request.post({ + url: `/api/admin/v1/tenants/${tenantId}/announcements`, + data, + ...withTenantHeader(tenantId) + }) + }, + + /** + * 更新租户公告(仅草稿) + * @param tenantId 租户ID + * @param announcementId 公告ID + * @param data 更新参数 + */ + update: (tenantId: string, announcementId: string, data: AnnouncementFormData) => { + return request.put({ + url: `/api/admin/v1/tenants/${tenantId}/announcements/${announcementId}`, + data, + ...withTenantHeader(tenantId) + }) + }, + + /** + * 发布租户公告 + * @param tenantId 租户ID + * @param announcementId 公告ID + * @param command 命令体(包含 rowVersion) + */ + publish: (tenantId: string, announcementId: string, command: RowVersionCommand) => { + return request.post({ + url: `/api/admin/v1/tenants/${tenantId}/announcements/${announcementId}/publish`, + data: command, + ...withTenantHeader(tenantId) + }) + }, + + /** + * 撤销租户公告 + * @param tenantId 租户ID + * @param announcementId 公告ID + * @param command 命令体(包含 rowVersion) + */ + revoke: (tenantId: string, announcementId: string, command: RowVersionCommand) => { + return request.post({ + url: `/api/admin/v1/tenants/${tenantId}/announcements/${announcementId}/revoke`, + data: command, + ...withTenantHeader(tenantId) + }) + }, + + /** + * 删除租户公告 + * @param tenantId 租户ID + * @param announcementId 公告ID + */ + delete: (tenantId: string, announcementId: string) => { + return request.del({ + url: `/api/admin/v1/tenants/${tenantId}/announcements/${announcementId}`, + ...withTenantHeader(tenantId) + }) + }, + + /** + * 标记公告已读(兼容旧路径) + * @param tenantId 租户ID + * @param announcementId 公告ID + */ + markRead: (tenantId: string, announcementId: string) => { + return request.post({ + url: `/api/admin/v1/tenants/${tenantId}/announcements/${announcementId}/read`, + ...withTenantHeader(tenantId) + }) + } +} + +/** + * 应用端公告 API + * 路由前缀:/api/app/announcements + */ +export const appAnnouncementApi = { + /** + * 获取可见公告列表(已发布且有效期内) + * @param params 查询参数 + */ + list: (params: AnnouncementQueryParams) => { + return request.get>({ + url: '/api/app/announcements', + params: normalizeQueryParams(params) + }) + }, + + /** + * 获取未读公告列表 + * @param params 查询参数 + */ + unread: (params: AnnouncementQueryParams) => { + return request.get>({ + url: '/api/app/announcements/unread', + params: normalizeQueryParams(params) + }) + }, + + /** + * 标记公告已读 + * @param announcementId 公告ID + */ + markRead: (announcementId: string) => { + return request.post({ + url: `/api/app/announcements/${announcementId}/mark-read` + }) + } +} diff --git a/src/api/auth.ts b/src/api/auth.ts new file mode 100644 index 0000000..e5f1fe4 --- /dev/null +++ b/src/api/auth.ts @@ -0,0 +1,71 @@ +import request from '@/utils/http' + +/** + * 登录 + * @param params 登录参数 + * @returns 登录响应 + */ +export function fetchLogin(params: Api.Auth.LoginParams) { + return request.post({ + url: '/api/admin/v1/auth/login', + params + // showSuccessMessage: true // 显示成功消息 + // showErrorMessage: false // 不显示错误消息 + }) +} + +/** + * 免租户号登录(仅账号+密码) + */ +export function fetchLoginSimple(params: Api.Auth.LoginParams) { + return request.post({ + url: '/api/admin/v1/auth/login/simple', + params, + skipTenantHeader: true + }) +} + +/** + * 获取用户信息 + * @returns 用户信息 + */ +export function fetchGetUserInfo() { + return request.get({ + url: '/api/admin/v1/auth/profile' + // 自定义请求头 + // headers: { + // 'X-Custom-Header': 'your-custom-value' + // } + }) +} + +/** + * 获取用户菜单 + * @returns 菜单列表 + */ +export function fetchGetMenu() { + return request.get({ + url: '/api/admin/v1/auth/menu' + }) +} + +/** + * 获取用户权限 + * @param userId 用户ID + * @returns 用户权限信息 + */ +export function fetchGetUserPermissions(userId: string) { + return request.get({ + url: `/api/admin/v1/auth/permissions/${userId}` + }) +} + +/** + * 通过重置链接令牌重置管理员密码 + */ +export function fetchResetAdminPassword(data: Api.Auth.ResetAdminPasswordRequest) { + return request.post({ + url: '/api/admin/v1/auth/reset-password', + data + }) +} diff --git a/src/api/billing.ts b/src/api/billing.ts new file mode 100644 index 0000000..0417334 --- /dev/null +++ b/src/api/billing.ts @@ -0,0 +1,222 @@ +import request from '@/utils/http' + +/** + * 账单接口基础路径 + * + * 说明:后端路由为 /api/admin/v1/billings。 + */ +const BILLING_BASE_URL = '/api/admin/v1/billings' + +// ==================== 查询类 API ==================== + +/** + * 获取账单列表 + * @param params 查询参数 + */ +export function fetchBillingList(params?: Partial) { + return request.get({ + url: BILLING_BASE_URL, + params + }) +} + +/** + * 获取账单列表(导出用:自动分页拉取) + * @param params 查询参数(不含分页也可) + * @param options 导出拉取选项 + */ +export async function fetchBillingListForExport( + params?: Partial, + options?: { + /** 单页拉取条数(越大越快,但后端可能有限制) */ + pageSize?: number + /** 最大导出条数(防止一次性导出过大) */ + maxRows?: number + } +) { + // 1. 兜底配置 + const pageSize = Math.min(Math.max(options?.pageSize ?? 200, 50), 2000) + const maxRows = Math.min(Math.max(options?.maxRows ?? 10000, 1), 100000) + + // 2. 循环分页拉取(直到 totalPages 或达到 maxRows) + const result: Api.Billing.BillingListDto[] = [] + let pageNumber = 1 + let totalPages = 1 + + while (pageNumber <= totalPages && result.length < maxRows) { + const res = await fetchBillingList({ + ...params, + PageNumber: pageNumber, + PageSize: pageSize + }) + + const items = res.items || [] + result.push(...items) + + totalPages = res.totalPages || 1 + if (items.length === 0) break + pageNumber += 1 + } + + // 3. 截断到 maxRows,避免极端情况内存爆炸 + return result.slice(0, maxRows) +} + +/** + * 获取账单详情 + * @param id 账单 ID + */ +export function fetchBillingDetail(id: string) { + return request.get({ + url: `${BILLING_BASE_URL}/${id}` + }) +} + +/** + * 获取账单支付记录 + * @param billingId 账单 ID + */ +export function fetchBillingPayments(billingId: string) { + return request.get({ + url: `${BILLING_BASE_URL}/${billingId}/payments` + }) +} + +/** + * 获取账单统计数据 + * @param params 统计查询参数 + */ +export function fetchBillingStatistics(params: Api.Billing.BillingStatisticsParams) { + return request.get({ + url: `${BILLING_BASE_URL}/statistics`, + params + }) +} + +/** + * 获取逾期账单列表 + * @param params 分页参数 + */ +export function fetchOverdueBillings(params?: Partial) { + return request.get({ + url: `${BILLING_BASE_URL}/overdue`, + params + }) +} + +// ==================== 命令类 API ==================== + +/** + * 创建账单 + * @param data 创建账单数据 + */ +export function createBilling(data: Api.Billing.CreateBillingCommand) { + return request.post({ + url: BILLING_BASE_URL, + data + }) +} + +/** + * 更新账单状态 + * @param id 账单 ID + * @param data 更新状态数据 + */ +export function updateBillingStatus(id: string, data: Api.Billing.UpdateStatusCommand) { + return request.put({ + url: `${BILLING_BASE_URL}/${id}/status`, + data + }) +} + +/** + * 取消账单 + * @param id 账单 ID + * @param data 取消原因 + */ +export function cancelBilling(id: string, data: Api.Billing.CancelBillingCommand) { + return request.del({ + url: `${BILLING_BASE_URL}/${id}`, + data + }) +} + +/** + * 记录支付(仅创建待审核支付记录,不会立即更新账单状态) + * @param billingId 账单 ID + * @param data 支付记录数据 + */ +export function recordPayment(billingId: string, data: Api.Billing.RecordPaymentCommand) { + return request.post({ + url: `${BILLING_BASE_URL}/${billingId}/payments`, + data + }) +} + +/** + * 一键确认收款(记录支付 + 立即审核通过 + 同步更新账单状态) + * @param billingId 账单 ID + * @param data 支付记录数据 + */ +export function confirmPayment(billingId: string, data: Api.Billing.RecordPaymentCommand) { + return request.post({ + url: `${BILLING_BASE_URL}/${billingId}/payments/confirm`, + data + }) +} + +/** + * 审核支付记录 + * @param paymentId 支付记录 ID + * @param data 审核数据 + */ +export function verifyPayment(paymentId: string, data: Api.Billing.VerifyPaymentCommand) { + return request.put({ + url: `${BILLING_BASE_URL}/payments/${paymentId}/verify`, + data + }) +} + +/** + * 批量更新账单状态 + * @param data 批量更新数据 + */ +export function batchUpdateStatus(data: Api.Billing.BatchUpdateStatusCommand) { + return request.post({ + url: `${BILLING_BASE_URL}/batch/status`, + data + }) +} + +/** + * 导出账单 + * @param data 导出参数 + * @returns Blob 对象(用于下载) + */ +export function exportBillings(data: Api.Billing.ExportParams) { + return request.post({ + url: `${BILLING_BASE_URL}/export`, + data, + responseType: 'blob' + }) +} + +// ==================== 兼容导出(适配现有页面命名) ==================== + +/** 获取账单列表(兼容旧命名) */ +export const fetchGetBillList = fetchBillingList + +/** 获取账单详情(兼容旧命名) */ +export const fetchGetBillDetail = fetchBillingDetail + +/** 获取账单列表(导出用,兼容旧命名) */ +export const fetchGetBillListForExport = fetchBillingListForExport + +/** 创建账单(兼容旧命名) */ +export const fetchCreateBill = createBilling + +/** 更新账单状态(兼容旧命名) */ +export const fetchUpdateBillStatus = updateBillingStatus + +/** 记录支付(兼容旧命名) */ +export const fetchRecordPayment = recordPayment diff --git a/src/api/dictionary/group.ts b/src/api/dictionary/group.ts new file mode 100644 index 0000000..74b4351 --- /dev/null +++ b/src/api/dictionary/group.ts @@ -0,0 +1,70 @@ +import request from '@/utils/http' + +const GROUP_BASE_URL = '/api/admin/v1/dictionary/groups' + +export function getGroups(params?: Api.Dictionary.DictionaryGroupQueryParams) { + return request.get>({ + url: GROUP_BASE_URL, + params + }) +} + +export function getGroupById(groupId: string) { + return request.get({ + url: `${GROUP_BASE_URL}/${groupId}` + }) +} + +export function createGroup(data: Api.Dictionary.CreateDictionaryGroupRequest) { + return request.post({ + url: GROUP_BASE_URL, + data + }) +} + +export function updateGroup(groupId: string, data: Api.Dictionary.UpdateDictionaryGroupRequest) { + return request.put({ + url: `${GROUP_BASE_URL}/${groupId}`, + data + }) +} + +export function deleteGroup(groupId: string) { + return request.del({ + url: `${GROUP_BASE_URL}/${groupId}` + }) +} + +export function exportGroup( + groupId: string, + format: Api.Dictionary.DictionaryExportRequest['format'] +) { + return request.post({ + url: `${GROUP_BASE_URL}/${groupId}/export`, + data: { format }, + responseType: 'blob' + }) +} + +export function importGroup( + groupId: string, + payload: { + file: File + conflictMode?: Api.Dictionary.ConflictResolutionMode | string + format?: 'csv' | 'json' + } +) { + const formData = new FormData() + formData.append('File', payload.file) + if (payload.conflictMode) { + formData.append('ConflictMode', String(payload.conflictMode)) + } + if (payload.format) { + formData.append('Format', payload.format) + } + + return request.post({ + url: `${GROUP_BASE_URL}/${groupId}/import`, + data: formData + }) +} diff --git a/src/api/dictionary/item.ts b/src/api/dictionary/item.ts new file mode 100644 index 0000000..01df563 --- /dev/null +++ b/src/api/dictionary/item.ts @@ -0,0 +1,33 @@ +import request from '@/utils/http' + +const GROUP_BASE_URL = '/api/admin/v1/dictionary/groups' + +export function getItems(groupId: string) { + return request.get({ + url: `${GROUP_BASE_URL}/${groupId}/items` + }) +} + +export function createItem(groupId: string, data: Api.Dictionary.CreateDictionaryItemRequest) { + return request.post({ + url: `${GROUP_BASE_URL}/${groupId}/items`, + data + }) +} + +export function updateItem( + groupId: string, + itemId: string, + data: Api.Dictionary.UpdateDictionaryItemRequest +) { + return request.put({ + url: `${GROUP_BASE_URL}/${groupId}/items/${itemId}`, + data + }) +} + +export function deleteItem(groupId: string, itemId: string) { + return request.del({ + url: `${GROUP_BASE_URL}/${groupId}/items/${itemId}` + }) +} diff --git a/src/api/dictionary/labelOverride.ts b/src/api/dictionary/labelOverride.ts new file mode 100644 index 0000000..3013623 --- /dev/null +++ b/src/api/dictionary/labelOverride.ts @@ -0,0 +1,70 @@ +import request from '@/utils/http' + +const LABEL_OVERRIDE_BASE_URL = '/api/admin/v1/dictionary/label-overrides' + +// ==================== 租户端 API ==================== + +/** + * 获取当前租户的标签覆盖列表 + */ +export function getTenantLabelOverrides() { + return request.get({ + url: `${LABEL_OVERRIDE_BASE_URL}/tenant` + }) +} + +/** + * 租户创建/更新标签覆盖 + */ +export function upsertTenantLabelOverride(data: Api.Dictionary.UpsertLabelOverrideRequest) { + return request.post({ + url: `${LABEL_OVERRIDE_BASE_URL}/tenant`, + data + }) +} + +/** + * 租户删除标签覆盖 + */ +export function deleteTenantLabelOverride(dictionaryItemId: string) { + return request.del({ + url: `${LABEL_OVERRIDE_BASE_URL}/tenant/${dictionaryItemId}` + }) +} + +// ==================== 平台端 API ==================== + +/** + * 获取指定租户的所有标签覆盖(平台管理员用) + */ +export function getPlatformLabelOverrides( + targetTenantId: string, + overrideType?: Api.Dictionary.OverrideType +) { + return request.get({ + url: `${LABEL_OVERRIDE_BASE_URL}/platform/${targetTenantId}`, + params: { overrideType } + }) +} + +/** + * 平台强制覆盖租户字典项的标签 + */ +export function upsertPlatformLabelOverride( + targetTenantId: string, + data: Api.Dictionary.UpsertLabelOverrideRequest +) { + return request.post({ + url: `${LABEL_OVERRIDE_BASE_URL}/platform/${targetTenantId}`, + data + }) +} + +/** + * 平台删除对租户的强制覆盖 + */ +export function deletePlatformLabelOverride(targetTenantId: string, dictionaryItemId: string) { + return request.del({ + url: `${LABEL_OVERRIDE_BASE_URL}/platform/${targetTenantId}/${dictionaryItemId}` + }) +} diff --git a/src/api/dictionary/metrics.ts b/src/api/dictionary/metrics.ts new file mode 100644 index 0000000..85b01c8 --- /dev/null +++ b/src/api/dictionary/metrics.ts @@ -0,0 +1,35 @@ +import request from '@/utils/http' + +const METRICS_BASE_URL = '/api/admin/v1/dictionary/metrics' + +export function getCacheStats(timeRange: '1h' | '24h' | '7d' = '1h') { + return request.get<{ + totalHits: number + totalMisses: number + hitRatio: number + hitsByLevel: { l1: number; l2: number } + missesByLevel: { l1: number; l2: number } + averageQueryDurationMs: number + topQueriedDictionaries: Array<{ code: string; queryCount: number }> + }>({ + url: `${METRICS_BASE_URL}/cache-stats`, + params: { timeRange } + }) +} + +export function getInvalidationEvents(params: { + page: number + pageSize: number + startDate?: string + endDate?: string +}) { + return request.get>({ + url: `${METRICS_BASE_URL}/invalidation-events`, + params: { + page: params.page, + pageSize: params.pageSize, + startDate: params.startDate, + endDate: params.endDate + } + }) +} diff --git a/src/api/dictionary/override.ts b/src/api/dictionary/override.ts new file mode 100644 index 0000000..65464c8 --- /dev/null +++ b/src/api/dictionary/override.ts @@ -0,0 +1,49 @@ +import request from '@/utils/http' + +const OVERRIDE_BASE_URL = '/api/admin/v1/dictionary/overrides' + +export function getOverrides() { + return request.get({ + url: OVERRIDE_BASE_URL + }) +} + +export function getOverride(groupCode: string) { + return request.get({ + url: `${OVERRIDE_BASE_URL}/${groupCode}` + }) +} + +export function enableOverride(groupCode: string) { + return request.post({ + url: `${OVERRIDE_BASE_URL}/${groupCode}/enable`, + data: {} + }) +} + +export function disableOverride(groupCode: string) { + return request.post({ + url: `${OVERRIDE_BASE_URL}/${groupCode}/disable`, + data: {} + }) +} + +export function updateHiddenItems( + groupCode: string, + hiddenItemIds: Api.Dictionary.DictionaryOverrideHiddenItemsRequest['hiddenItemIds'] +) { + return request.put({ + url: `${OVERRIDE_BASE_URL}/${groupCode}/hidden-items`, + data: { hiddenItemIds } + }) +} + +export function updateSortOrder( + groupCode: string, + sortOrder: Api.Dictionary.DictionaryOverrideSortOrderRequest['sortOrder'] +) { + return request.put({ + url: `${OVERRIDE_BASE_URL}/${groupCode}/sort-order`, + data: { sortOrder } + }) +} diff --git a/src/api/dictionary/query.ts b/src/api/dictionary/query.ts new file mode 100644 index 0000000..01b7bff --- /dev/null +++ b/src/api/dictionary/query.ts @@ -0,0 +1,27 @@ +import request from '@/utils/http' + +const QUERY_BASE_URL = '/api/admin/v1/dictionaries' + +const normalizeCode = (code: string) => code.trim().toLowerCase() + +export async function getDictionary(code: string) { + const result = await batchGetDictionaries([code]) + return result[code] ?? [] +} + +export async function batchGetDictionaries(codes: string[]) { + if (!codes.length) return {} + + const result = await request.post>({ + url: `${QUERY_BASE_URL}/batch`, + data: { codes } + }) + + const mapped: Record = {} + codes.forEach((code) => { + const key = normalizeCode(code) + mapped[code] = result[key] ?? result[code] ?? [] + }) + + return mapped +} diff --git a/src/api/files.ts b/src/api/files.ts new file mode 100644 index 0000000..615c36f --- /dev/null +++ b/src/api/files.ts @@ -0,0 +1,22 @@ +import request from '@/utils/http' + +/** + * 文件上传 API + */ + +/** + * 上传图片或文件(Admin) + * @param file 上传的文件 + * @param type 上传类型(可选) + */ +export function fetchUploadFile(file: File, type: Api.Files.UploadFileType = 'other') { + const formData = new FormData() + formData.append('File', file) + formData.append('Type', type) + + return request.post({ + url: '/api/admin/v1/files/upload', + // 重要:不要手动设置 Content-Type,让浏览器自动带 boundary + data: formData + }) +} diff --git a/src/api/merchant.ts b/src/api/merchant.ts new file mode 100644 index 0000000..b4bf623 --- /dev/null +++ b/src/api/merchant.ts @@ -0,0 +1,126 @@ +import api from '@/utils/http' + +/** + * 获取商户列表 + */ +export function fetchMerchantList(params?: Partial) { + return api.get({ + url: '/api/admin/v1/merchants', + params + }) +} + +/** + * 获取商户详情 + */ +export function fetchMerchantDetail(merchantId: string, options?: { showErrorMessage?: boolean }) { + return api.get({ + url: `/api/admin/v1/merchants/${merchantId}`, + showErrorMessage: options?.showErrorMessage + }) +} + +/** + * 更新商户信息 + */ +export function fetchUpdateMerchant( + merchantId: string, + data: Api.Merchant.UpdateMerchantRequest, + options?: { showErrorMessage?: boolean } +) { + return api.put({ + url: `/api/admin/v1/merchants/${merchantId}`, + data, + showErrorMessage: options?.showErrorMessage + }) +} + +/** + * 获取商户审核历史 + */ +export function fetchMerchantAuditHistory(merchantId: string) { + return api.get({ + url: `/api/admin/v1/merchants/${merchantId}/audit-history` + }) +} + +/** + * 获取商户变更历史 + */ +export function fetchMerchantChangeLogs(merchantId: string, params?: { fieldName?: string }) { + return api.get({ + url: `/api/admin/v1/merchants/${merchantId}/change-history`, + params + }) +} + +/** + * 获取待审核商户列表 + */ +export function fetchPendingReviewList(params?: Partial) { + return api.get({ + url: '/api/admin/v1/merchants/pending-review', + params + }) +} + +/** + * 获取审核领取信息 + */ +export function fetchMerchantReviewClaim(merchantId: string) { + return api.get({ + url: `/api/admin/v1/merchants/${merchantId}/review/claim` + }) +} + +/** + * 领取审核 + */ +export function fetchClaimMerchantReview(merchantId: string) { + return api.post({ + url: `/api/admin/v1/merchants/${merchantId}/review/claim`, + data: {} + }) +} + +/** + * 释放审核 + */ +export function fetchReleaseMerchantReview(merchantId: string) { + return api.del({ + url: `/api/admin/v1/merchants/${merchantId}/review/claim` + }) +} + +/** + * 执行审核 + */ +export function fetchReviewMerchant(merchantId: string, data: Api.Merchant.ReviewMerchantRequest) { + return api.post({ + url: `/api/admin/v1/merchants/${merchantId}/review`, + data + }) +} + +/** + * 撤销审核 + */ +export function fetchRevokeMerchantReview( + merchantId: string, + data: Api.Merchant.RevokeMerchantRequest +) { + return api.post({ + url: `/api/admin/v1/merchants/${merchantId}/review/revoke`, + data + }) +} + +/** + * 导出商户 PDF + */ +export function fetchExportMerchantPdf(merchantId: string) { + return api.get({ + url: `/api/admin/v1/merchants/${merchantId}/export-pdf`, + responseType: 'blob' + }) +} diff --git a/src/api/permission.ts b/src/api/permission.ts new file mode 100644 index 0000000..bad1571 --- /dev/null +++ b/src/api/permission.ts @@ -0,0 +1,21 @@ +import request from '@/utils/http' + +/** + * 获取权限列表(分页) + * @param params 查询参数 + */ +export function fetchGetPermissions(params?: Api.Permission.PermissionQueryParams) { + return request.get({ + url: '/api/admin/v1/permissions', + params + }) +} + +/** + * 获取权限树 + */ +export function fetchGetPermissionTree() { + return request.get({ + url: '/api/admin/v1/permissions/tree' + }) +} diff --git a/src/api/quotaPackage.ts b/src/api/quotaPackage.ts new file mode 100644 index 0000000..43212dc --- /dev/null +++ b/src/api/quotaPackage.ts @@ -0,0 +1,100 @@ +import request from '@/utils/http' + +/** + * 配额包管理 API + */ + +/** + * 获取配额包列表 + */ +export function fetchQuotaPackageList(params?: { + quotaType?: number + isActive?: boolean + page?: number + pageSize?: number +}) { + return request.get>({ + url: '/api/admin/v1/quota-packages', + params + }) +} + +/** + * 创建配额包 + */ +export function fetchCreateQuotaPackage(data: Api.QuotaPackage.CreateQuotaPackageCommand) { + return request.post({ + url: '/api/admin/v1/quota-packages', + data + }) +} + +/** + * 更新配额包 + */ +export function fetchUpdateQuotaPackage( + quotaPackageId: string, + data: Api.QuotaPackage.UpdateQuotaPackageCommand +) { + return request.put({ + url: `/api/admin/v1/quota-packages/${quotaPackageId}`, + data + }) +} + +/** + * 删除配额包(软删) + */ +export function fetchDeleteQuotaPackage(quotaPackageId: string) { + return request.del({ + url: `/api/admin/v1/quota-packages/${quotaPackageId}` + }) +} + +/** + * 更新配额包状态(上架/下架) + */ +export function fetchUpdateQuotaPackageStatus(quotaPackageId: string, isActive: boolean) { + return request.put({ + url: `/api/admin/v1/quota-packages/${quotaPackageId}/status`, + data: { + isActive + } + }) +} + +/** + * 为租户购买配额包 + */ +export function fetchPurchaseQuotaPackage( + tenantId: string, + data: Api.QuotaPackage.PurchaseQuotaPackageCommand +) { + return request.post({ + url: `/api/admin/v1/tenants/${tenantId}/quota-packages`, + data + }) +} + +/** + * 获取租户配额使用情况 + */ +export function fetchTenantQuotaUsage(tenantId: string, params?: { quotaType?: number }) { + return request.get({ + url: `/api/admin/v1/tenants/${tenantId}/quota-usage`, + params + }) +} + +/** + * 获取租户配额购买记录 + */ +export function fetchTenantQuotaPurchases( + tenantId: string, + params?: { page?: number; pageSize?: number } +) { + return request.get>({ + url: `/api/admin/v1/tenants/${tenantId}/quota-purchases`, + params + }) +} diff --git a/src/api/role-template.ts b/src/api/role-template.ts new file mode 100644 index 0000000..596793b --- /dev/null +++ b/src/api/role-template.ts @@ -0,0 +1,94 @@ +import request from '@/utils/http' + +/** + * 获取角色模板列表 + * @param params 查询参数 + */ +export function fetchGetRoleTemplates(params?: any) { + return request.get({ + url: '/api/admin/v1/role-templates', + params + }) +} + +/** + * 获取角色模板详情 + * @param templateCode 模板编码 + */ +export function fetchGetRoleTemplateDetail(templateCode: string) { + return request.get({ + url: `/api/admin/v1/role-templates/${templateCode}` + }) +} + +/** + * 创建角色模板 + * @param data 创建参数 + */ +export function fetchCreateRoleTemplate(data: Api.RoleTemplate.CreateRoleTemplateCommand) { + return request.post({ + url: '/api/admin/v1/role-templates', + data + }) +} + +/** + * 更新角色模板 + * @param templateCode 模板编码 + * @param data 更新参数 + */ +export function fetchUpdateRoleTemplate( + templateCode: string, + data: Api.RoleTemplate.UpdateRoleTemplateCommand +) { + return request.put({ + url: `/api/admin/v1/role-templates/${templateCode}`, + data + }) +} + +/** + * 删除角色模板 + * @param templateCode 模板编码 + */ +export function fetchDeleteRoleTemplate(templateCode: string) { + return request.del({ + url: `/api/admin/v1/role-templates/${templateCode}` + }) +} + +/** + * 克隆角色模板 + * @param templateCode 源模板编码 + * @param data 克隆参数 + */ +export function fetchCloneRoleTemplate( + templateCode: string, + data: Api.RoleTemplate.CloneRoleTemplateCommand +) { + return request.post({ + url: `/api/admin/v1/role-templates/${templateCode}/clone`, + data + }) +} + +/** + * 初始化角色模板 + * @param templateCodes 模板编码列表 + */ +export function fetchInitRoleTemplates(templateCodes: string[]) { + return request.post({ + url: '/api/admin/v1/role-templates/init', + data: { templateCodes } + }) +} + +/** + * 获取角色模板权限列表 + * @param templateCode 模板编码 + */ +export function fetchGetRoleTemplatePermissions(templateCode: string) { + return request.get({ + url: `/api/admin/v1/role-templates/${templateCode}/permissions` + }) +} diff --git a/src/api/statistics.ts b/src/api/statistics.ts new file mode 100644 index 0000000..d8c7d59 --- /dev/null +++ b/src/api/statistics.ts @@ -0,0 +1,47 @@ +import request from '@/utils/http' + +/** + * 统计分析 API + */ + +/** + * 获取订阅概览统计 + */ +export function fetchSubscriptionOverview() { + return request.get({ + url: '/api/admin/v1/statistics/subscriptions/overview' + }) +} + +/** + * 获取配额使用排行 + * @param params 查询参数 + */ +export function fetchQuotaUsageRanking(params?: Api.Statistics.QuotaUsageRankingParams) { + return request.get({ + url: '/api/admin/v1/statistics/quota/ranking', + params + }) +} + +/** + * 获取收入统计 + * @param params 查询参数 + */ +export function fetchRevenueStatistics(params?: Api.Statistics.RevenueStatisticsParams) { + return request.get({ + url: '/api/admin/v1/statistics/revenue', + params + }) +} + +/** + * 获取即将到期订阅列表 + * @param params 查询参数 + */ +export function fetchExpiringSubscriptions(params?: Api.Statistics.ExpiringSubscriptionsParams) { + return request.get({ + url: '/api/admin/v1/statistics/subscriptions/expiring', + params + }) +} diff --git a/src/api/store.ts b/src/api/store.ts new file mode 100644 index 0000000..2455f1f --- /dev/null +++ b/src/api/store.ts @@ -0,0 +1,309 @@ +import api from '@/utils/http' + +interface StoreRequestOptions { + tenantId?: string +} + +const buildTenantHeader = (options?: StoreRequestOptions) => { + // 1. 未提供租户ID时返回空配置 + if (!options?.tenantId) { + return {} + } + + // 2. 返回携带租户头的请求配置 + return { + headers: { + 'X-Tenant-Id': String(options.tenantId) + } + } +} + +/** + * 获取门店列表 + */ +export function fetchStoreList(params?: Partial) { + return api.get({ + url: '/api/admin/v1/stores', + params + }) +} + +/** + * 获取门店详情 + */ +export function fetchStoreDetail(storeId: string, options?: { showErrorMessage?: boolean }) { + return api.get({ + url: `/api/admin/v1/stores/${storeId}`, + showErrorMessage: options?.showErrorMessage + }) +} + +/** + * 创建门店 + */ +export function fetchCreateStore(data: Api.Store.CreateStoreRequest) { + return api.post({ + url: '/api/admin/v1/stores', + data + }) +} + +/** + * 更新门店 + */ +export function fetchUpdateStore(storeId: string, data: Api.Store.UpdateStoreRequest) { + return api.put({ + url: `/api/admin/v1/stores/${storeId}`, + data + }) +} + +/** + * 删除门店 + */ +export function fetchDeleteStore(storeId: string) { + return api.del({ + url: `/api/admin/v1/stores/${storeId}` + }) +} + +/** + * 提交门店审核 + */ +export function fetchSubmitStoreAudit(storeId: string) { + return api.post({ + url: `/api/admin/v1/stores/${storeId}/submit`, + data: {} + }) +} + +/** + * 切换门店经营状态 + */ +export function fetchToggleBusinessStatus( + storeId: string, + data: Api.Store.ToggleBusinessStatusRequest +) { + return api.post({ + url: `/api/admin/v1/stores/${storeId}/business-status`, + data + }) +} + +/** + * 获取门店资质列表 + */ +export function fetchStoreQualifications(storeId: string, options?: StoreRequestOptions) { + return api.get({ + url: `/api/admin/v1/stores/${storeId}/qualifications`, + ...buildTenantHeader(options) + }) +} + +/** + * 检查门店资质完整性 + */ +export function fetchCheckStoreQualifications(storeId: string, options?: StoreRequestOptions) { + return api.get({ + url: `/api/admin/v1/stores/${storeId}/qualifications/check`, + ...buildTenantHeader(options) + }) +} + +/** + * 创建门店资质 + */ +export function fetchCreateStoreQualification( + storeId: string, + data: Api.Store.CreateStoreQualificationRequest, + options?: StoreRequestOptions +) { + return api.post({ + url: `/api/admin/v1/stores/${storeId}/qualifications`, + data, + ...buildTenantHeader(options) + }) +} + +/** + * 更新门店资质 + */ +export function fetchUpdateStoreQualification( + storeId: string, + qualificationId: string, + data: Api.Store.UpdateStoreQualificationRequest, + options?: StoreRequestOptions +) { + return api.put({ + url: `/api/admin/v1/stores/${storeId}/qualifications/${qualificationId}`, + data, + ...buildTenantHeader(options) + }) +} + +/** + * 删除门店资质 + */ +export function fetchDeleteStoreQualification( + storeId: string, + qualificationId: string, + options?: StoreRequestOptions +) { + return api.del({ + url: `/api/admin/v1/stores/${storeId}/qualifications/${qualificationId}`, + ...buildTenantHeader(options) + }) +} + +/** + * 获取门店营业时段 + */ +export function fetchStoreBusinessHours(storeId: string) { + return api.get({ + url: `/api/admin/v1/stores/${storeId}/business-hours` + }) +} + +/** + * 批量更新门店营业时段 + */ +export function fetchBatchUpdateBusinessHours( + storeId: string, + data: Api.Store.BatchUpdateBusinessHoursRequest +) { + return api.put({ + url: `/api/admin/v1/stores/${storeId}/business-hours/batch`, + data + }) +} + +/** + * 获取配送区域 + */ +export function fetchStoreDeliveryZones(storeId: string) { + return api.get({ + url: `/api/admin/v1/stores/${storeId}/delivery-zones` + }) +} + +/** + * 创建配送区域 + */ +export function fetchCreateStoreDeliveryZone( + storeId: string, + data: Api.Store.CreateStoreDeliveryZoneRequest +) { + return api.post({ + url: `/api/admin/v1/stores/${storeId}/delivery-zones`, + data + }) +} + +/** + * 更新配送区域 + */ +export function fetchUpdateStoreDeliveryZone( + storeId: string, + deliveryZoneId: string, + data: Api.Store.UpdateStoreDeliveryZoneRequest +) { + return api.put({ + url: `/api/admin/v1/stores/${storeId}/delivery-zones/${deliveryZoneId}`, + data + }) +} + +/** + * 删除配送区域 + */ +export function fetchDeleteStoreDeliveryZone(storeId: string, deliveryZoneId: string) { + return api.del({ + url: `/api/admin/v1/stores/${storeId}/delivery-zones/${deliveryZoneId}` + }) +} + +/** + * 配送范围检测 + */ +export function fetchDeliveryZoneCheck(storeId: string, data: Api.Store.StoreDeliveryCheckRequest) { + return api.post({ + url: `/api/admin/v1/stores/${storeId}/delivery-check`, + data + }) +} + +/** + * 获取门店费用配置 + */ +export function fetchStoreFee(storeId: string) { + return api.get({ + url: `/api/admin/v1/stores/${storeId}/fee` + }) +} + +/** + * 更新门店费用配置 + */ +export function fetchUpdateStoreFee(storeId: string, data: Api.Store.UpdateStoreFeeRequest) { + return api.put({ + url: `/api/admin/v1/stores/${storeId}/fee`, + data + }) +} + +/** + * 门店费用预览 + */ +export function fetchCalculateStoreFee(storeId: string, data: Api.Store.CalculateStoreFeeRequest) { + return api.post({ + url: `/api/admin/v1/stores/${storeId}/fee/calculate`, + data + }) +} + +// ==================== 临时时段 API ==================== + +/** + * 获取门店临时时段列表 + */ +export function fetchStoreHolidays(storeId: string) { + return api.get({ + url: `/api/admin/v1/stores/${storeId}/holidays` + }) +} + +/** + * 创建临时时段 + */ +export function fetchCreateStoreHoliday( + storeId: string, + data: Api.Store.CreateStoreHolidayRequest +) { + return api.post({ + url: `/api/admin/v1/stores/${storeId}/holidays`, + data + }) +} + +/** + * 更新临时时段 + */ +export function fetchUpdateStoreHoliday( + storeId: string, + holidayId: string, + data: Api.Store.UpdateStoreHolidayRequest +) { + return api.put({ + url: `/api/admin/v1/stores/${storeId}/holidays/${holidayId}`, + data + }) +} + +/** + * 删除临时时段 + */ +export function fetchDeleteStoreHoliday(storeId: string, holidayId: string) { + return api.del({ + url: `/api/admin/v1/stores/${storeId}/holidays/${holidayId}` + }) +} diff --git a/src/api/storeAudit.ts b/src/api/storeAudit.ts new file mode 100644 index 0000000..626462c --- /dev/null +++ b/src/api/storeAudit.ts @@ -0,0 +1,103 @@ +import api from '@/utils/http' + +/** + * 获取待审核门店列表 + */ +export function fetchPendingStoreAudits( + params?: Partial +) { + return api.get({ + url: '/api/admin/v1/platform/store-audits/pending', + params + }) +} + +/** + * 获取审核统计 + */ +export function fetchStoreAuditStatistics(params?: Api.StoreAudit.StoreAuditStatisticsParams) { + return api.get({ + url: '/api/admin/v1/platform/store-audits/statistics', + params + }) +} + +/** + * 获取门店审核详情 + */ +export function fetchStoreAuditDetail(storeId: string) { + return api.get({ + url: `/api/admin/v1/platform/store-audits/${storeId}` + }) +} + +/** + * 获取审核记录 + */ +export function fetchStoreAuditRecords( + storeId: string, + params?: Api.StoreAudit.ListStoreAuditRecordsParams +) { + return api.get>({ + url: `/api/admin/v1/platform/store-audits/${storeId}/records`, + params + }) +} + +/** + * 审核通过 + */ +export function fetchApproveStoreAudit( + storeId: string, + data: Api.StoreAudit.ApproveStoreAuditRequest +) { + return api.post({ + url: `/api/admin/v1/platform/store-audits/${storeId}/approve`, + data + }) +} + +/** + * 审核驳回 + */ +export function fetchRejectStoreAudit( + storeId: string, + data: Api.StoreAudit.RejectStoreAuditRequest +) { + return api.post({ + url: `/api/admin/v1/platform/store-audits/${storeId}/reject`, + data + }) +} + +/** + * 强制关闭门店 + */ +export function fetchForceCloseStore(storeId: string, data: Api.StoreAudit.ForceCloseStoreRequest) { + return api.post({ + url: `/api/admin/v1/platform/store-audits/${storeId}/force-close`, + data + }) +} + +/** + * 解除强制关闭 + */ +export function fetchReopenStore(storeId: string, data: Api.StoreAudit.ReopenStoreRequest) { + return api.post({ + url: `/api/admin/v1/platform/store-audits/${storeId}/reopen`, + data + }) +} + +/** + * 获取资质预警列表(平台) + */ +export function fetchQualificationAlerts( + params?: Partial +) { + return api.get({ + url: '/api/admin/v1/platform/store-qualifications/expiring', + params + }) +} diff --git a/src/api/subscription.ts b/src/api/subscription.ts new file mode 100644 index 0000000..ad9e914 --- /dev/null +++ b/src/api/subscription.ts @@ -0,0 +1,134 @@ +import request from '@/utils/http' + +/** + * 订阅管理 API + */ + +/** + * 获取订阅列表 + * @param params 查询参数 + */ +export function getSubscriptionList(params?: Api.Subscription.SubscriptionListParams) { + return request.get({ + url: '/api/admin/v1/subscriptions', + params + }) +} + +/** + * 获取订阅详情 + * @param id 订阅ID + */ +export function getSubscriptionDetail(id: string) { + return request.get({ + url: `/api/admin/v1/subscriptions/${id}` + }) +} + +/** + * 更新订阅 + * @param id 订阅ID + * @param data 更新数据 + */ +export function updateSubscription( + id: string, + data: Omit +) { + return request.put({ + url: `/api/admin/v1/subscriptions/${id}`, + data: { + ...data, + subscriptionId: id + } + }) +} + +/** + * 延期订阅 + * @param id 订阅ID + * @param data 延期数据 + */ +export function extendSubscription( + id: string, + data: Omit +) { + return request.post({ + url: `/api/admin/v1/subscriptions/${id}/extend`, + data: { + ...data, + subscriptionId: id + } + }) +} + +/** + * 变更套餐 + * @param id 订阅ID + * @param data 变更数据 + */ +export function changeSubscriptionPlan( + id: string, + data: Omit +) { + return request.post({ + url: `/api/admin/v1/subscriptions/${id}/change-plan`, + data: { + ...data, + subscriptionId: id + } + }) +} + +/** + * 变更订阅状态 + * @param id 订阅ID + * @param data 状态数据 + */ +export function updateSubscriptionStatus( + id: string, + data: Omit +) { + return request.post({ + url: `/api/admin/v1/subscriptions/${id}/status`, + data: { + ...data, + subscriptionId: id + } + }) +} + +/** + * 批量延期订阅 + * @param data 批量延期数据 + */ +export function batchExtendSubscriptions(data: { + subscriptionIds: string[] + durationDays: number + notes?: string +}) { + return request.post<{ + successCount: number + failureCount: number + failures: Array<{ subscriptionId: string; reason: string }> + }>({ + url: '/api/admin/v1/subscriptions/batch-extend', + data, + showSuccessMessage: true + }) +} + +/** + * 批量发送提醒 + * @param data 批量提醒数据 + */ +export function batchSendReminders(data: { subscriptionIds: string[]; reminderContent: string }) { + return request.post<{ + successCount: number + failureCount: number + failures: Array<{ subscriptionId: string; reason: string }> + }>({ + url: '/api/admin/v1/subscriptions/batch-remind', + data, + showSuccessMessage: true + }) +} diff --git a/src/api/system-manage.ts b/src/api/system-manage.ts new file mode 100644 index 0000000..3bd83c5 --- /dev/null +++ b/src/api/system-manage.ts @@ -0,0 +1,86 @@ +import api from '@/utils/http' +import type { AppRouteRecord } from '@/types/router' + +// 1. 获取用户列表 +export function fetchGetUserList(params: Api.SystemManage.UserSearchParams) { + return api.get({ + url: '/api/admin/v1/users', + params + }) +} + +// 2. 获取用户详情 +export function fetchGetUserDetail(userId: string, includeDeleted?: boolean) { + return api.get({ + url: `/api/admin/v1/users/${userId}`, + params: includeDeleted ? { includeDeleted } : undefined + }) +} + +// 3. 创建用户 +export function fetchCreateUser(command: Api.SystemManage.CreateIdentityUserCommand) { + return api.post({ + url: '/api/admin/v1/users', + data: command + }) +} + +// 4. 更新用户 +export function fetchUpdateUser( + userId: string, + command: Api.SystemManage.UpdateIdentityUserCommand +) { + return api.put({ + url: `/api/admin/v1/users/${userId}`, + data: command + }) +} + +// 5. 删除用户 +export function fetchDeleteUser(userId: string) { + return api.del({ + url: `/api/admin/v1/users/${userId}` + }) +} + +// 6. 恢复用户 +export function fetchRestoreUser(userId: string) { + return api.post({ + url: `/api/admin/v1/users/${userId}/restore` + }) +} + +// 7. 更新用户状态 +export function fetchChangeUserStatus( + userId: string, + command: Api.SystemManage.ChangeIdentityUserStatusCommand +) { + return api.put({ + url: `/api/admin/v1/users/${userId}/status`, + data: command + }) +} + +// 8. 重置用户密码 +export function fetchResetUserPassword(userId: string) { + return api.post({ + url: `/api/admin/v1/users/${userId}/password-reset` + }) +} + +// 9. 批量用户操作 +export function fetchBatchUserOperation( + command: Api.SystemManage.BatchIdentityUserOperationCommand +) { + return api.post({ + url: '/api/admin/v1/users/batch', + data: command + }) +} + +// 10. 获取菜单列表 +export function fetchGetMenuList() { + return api.get({ + url: '/api/v3/system/menus/simple' + }) +} diff --git a/src/api/tenant-onboarding.ts b/src/api/tenant-onboarding.ts new file mode 100644 index 0000000..9ab68f7 --- /dev/null +++ b/src/api/tenant-onboarding.ts @@ -0,0 +1,99 @@ +import request from '@/utils/http' + +/** + * 自助注册租户 + * @param data 自助注册参数 + */ +export function fetchSelfRegisterTenant(data: Api.Tenant.SelfRegisterTenantCommand) { + return request.post({ + url: '/api/public/v1/tenants/self-register', + data + }) +} + +/** + * 查询租户入驻进度 + * @param tenantId 租户ID + */ +export function fetchTenantProgress(tenantId: string) { + return request.get({ + url: `/api/public/v1/tenants/${tenantId}/status` + }) +} + +/** + * 提交或更新租户实名信息 + * @param tenantId 租户ID + * @param data 实名资料 + */ +export function submitTenantVerification( + tenantId: string, + data: Api.Tenant.SubmitTenantVerificationCommand +) { + return request.post({ + url: `/api/public/v1/tenants/${tenantId}/verification`, + data: { ...data, tenantId } + }) +} + +/** + * 提交或更新租户实名信息(管理端) + * @param tenantId 租户ID + * @param data 实名资料 + */ +export function submitTenantVerificationAdmin( + tenantId: string, + data: Api.Tenant.SubmitTenantVerificationCommand +) { + return request.post({ + url: `/api/admin/v1/tenants/${tenantId}/verification`, + data: { ...data, tenantId } + }) +} + +/** + * 创建租户订阅 + * @param tenantId 租户ID + * @param data 订阅参数 + */ +export function createTenantSubscription( + tenantId: string, + data: Api.Tenant.CreateTenantSubscriptionCommand +) { + return request.post({ + url: `/api/admin/v1/tenants/${tenantId}/subscriptions`, + data: { ...data, tenantId } + }) +} + +/** + * 初次绑定租户订阅(自助入驻) + * @param tenantId 租户ID + * @param data 初次绑定参数 + */ +export function bindInitialTenantSubscription( + tenantId: string, + data: Api.Tenant.BindInitialTenantSubscriptionCommand +) { + return request.post({ + url: `/api/public/v1/tenants/${tenantId}/subscriptions/initial`, + data: { ...data, tenantId } + }) +} + +/** + * 升降配租户套餐 + * @param tenantId 租户ID + * @param subscriptionId 订阅ID + * @param data 升降配参数 + */ +export function changeTenantSubscriptionPlan( + tenantId: string, + subscriptionId: string, + data: Api.Tenant.ChangeTenantSubscriptionPlanCommand +) { + return request.put({ + url: `/api/admin/v1/tenants/${tenantId}/subscriptions/${subscriptionId}/plan`, + data: { ...data, tenantId, tenantSubscriptionId: subscriptionId } + }) +} diff --git a/src/api/tenant-package.ts b/src/api/tenant-package.ts new file mode 100644 index 0000000..0d2600b --- /dev/null +++ b/src/api/tenant-package.ts @@ -0,0 +1,103 @@ +import request from '@/utils/http' + +/** + * 租户套餐管理 API + */ + +/** + * 获取租户套餐分页列表 + */ +export function fetchTenantPackageList(params?: Api.Tenant.TenantPackageQueryParams) { + return request.get({ + url: '/api/admin/v1/tenant-packages', + params + }) +} + +/** + * 公共租户套餐列表(匿名可访问) + */ +export function fetchPublicTenantPackageList(params?: Api.Tenant.TenantPackageQueryParams) { + return request.get({ + url: '/api/public/v1/tenant-packages', + params + }) +} + +/** + * 获取租户套餐详情 + */ +export function fetchTenantPackageDetail(tenantPackageId: string) { + return request.get({ + url: `/api/admin/v1/tenant-packages/${tenantPackageId}` + }) +} + +/** + * 创建租户套餐 + */ +export function fetchCreateTenantPackage(data: Api.Tenant.CreateTenantPackageCommand) { + return request.post({ + url: '/api/admin/v1/tenant-packages', + data + }) +} + +/** + * 更新租户套餐 + */ +export function fetchUpdateTenantPackage( + tenantPackageId: string, + data: Api.Tenant.UpdateTenantPackageCommand +) { + return request.put({ + url: `/api/admin/v1/tenant-packages/${tenantPackageId}`, + data: { + ...data, + tenantPackageId + } + }) +} + +/** + * 删除租户套餐(软删) + */ +export function fetchDeleteTenantPackage(tenantPackageId: string) { + return request.del({ + url: `/api/admin/v1/tenant-packages/${tenantPackageId}` + }) +} + +/** + * 查询套餐使用统计(订阅关联数量、使用租户数量) + */ +export function fetchTenantPackageUsages(tenantPackageIds?: string[]) { + const params = new URLSearchParams() + if (tenantPackageIds?.length) { + tenantPackageIds.forEach((id) => params.append('tenantPackageIds', id)) + } + + return request.get({ + url: '/api/admin/v1/tenant-packages/usages', + params, + showErrorMessage: false + }) +} + +/** + * 查询套餐当前使用租户列表(按有效订阅口径) + */ +export function fetchTenantPackageTenants( + tenantPackageId: string, + params: { keyword?: string; page?: number; pageSize?: number; expiringWithinDays?: number } = {} +) { + return request.get>({ + url: `/api/admin/v1/tenant-packages/${tenantPackageId}/tenants`, + params: { + keyword: params.keyword || undefined, + expiringWithinDays: params.expiringWithinDays ?? undefined, + page: params.page ?? 1, + pageSize: params.pageSize ?? 20 + } + }) +} diff --git a/src/api/tenant-role.ts b/src/api/tenant-role.ts new file mode 100644 index 0000000..d5ec2fe --- /dev/null +++ b/src/api/tenant-role.ts @@ -0,0 +1,98 @@ +import request from '@/utils/http' + +/** + * 获取租户角色列表 + * @param tenantId 租户ID + * @param params 查询参数 + */ +export function fetchGetTenantRoles( + tenantId: number | string, + params?: Api.TenantRole.RoleQueryParams +) { + return request.get({ + url: `/api/admin/v1/tenants/${tenantId}/roles`, + params + }) +} + +/** + * 获取租户角色详情 + * @param tenantId 租户ID + * @param roleId 角色ID + */ +export function fetchGetTenantRoleDetail(tenantId: number | string, roleId: number | string) { + return request.get({ + url: `/api/admin/v1/tenants/${tenantId}/roles/${roleId}` + }) +} + +/** + * 创建租户角色 + * @param tenantId 租户ID + * @param data 创建参数 + */ +export function fetchCreateTenantRole( + tenantId: number | string, + data: Api.TenantRole.CreateRoleCommand +) { + return request.post({ + url: `/api/admin/v1/tenants/${tenantId}/roles`, + data + }) +} + +/** + * 更新租户角色 + * @param tenantId 租户ID + * @param roleId 角色ID + * @param data 更新参数 + */ +export function fetchUpdateTenantRole( + tenantId: number | string, + roleId: number | string, + data: Api.TenantRole.UpdateRoleCommand +) { + return request.put({ + url: `/api/admin/v1/tenants/${tenantId}/roles/${roleId}`, + data + }) +} + +/** + * 删除租户角色 + * @param tenantId 租户ID + * @param roleId 角色ID + */ +export function fetchDeleteTenantRole(tenantId: number | string, roleId: number | string) { + return request.del({ + url: `/api/admin/v1/tenants/${tenantId}/roles/${roleId}` + }) +} + +/** + * 获取租户角色权限 + * @param tenantId 租户ID + * @param roleId 角色ID + */ +export function fetchGetTenantRolePermissions(tenantId: number | string, roleId: number | string) { + return request.get({ + url: `/api/admin/v1/tenants/${tenantId}/roles/${roleId}/permissions` + }) +} + +/** + * 更新租户角色权限 + * @param tenantId 租户ID + * @param roleId 角色ID + * @param permissions 权限代码列表 + */ +export function fetchUpdateTenantRolePermissions( + tenantId: number | string, + roleId: number | string, + permissionIds: string[] +) { + return request.put({ + url: `/api/admin/v1/tenants/${tenantId}/roles/${roleId}/permissions`, + data: { permissionIds } + }) +} diff --git a/src/api/tenant.ts b/src/api/tenant.ts new file mode 100644 index 0000000..06a5742 --- /dev/null +++ b/src/api/tenant.ts @@ -0,0 +1,263 @@ +import api from '@/utils/http' +import { fetchTenantPackageList } from './tenant-package' +import type { QuotaUsageHistoryDto, UpdateTenantCommand } from '@/types/tenant' + +/** + * 获取租户列表 + * @param params 搜索参数 + */ +export function fetchGetTenantList(params?: Partial) { + return api.get({ + url: '/api/admin/v1/tenants', + params + }) +} + +/** + * 注册租户 + */ +export function fetchRegisterTenant(data: Api.Tenant.RegisterTenantCommand) { + return api.post({ + url: '/api/admin/v1/tenants', + data + }) +} + +/** + * 后台手动新增租户并直接入驻(创建租户 + 认证 + 订阅 + 管理员账号) + */ +export function fetchCreateTenantManually(data: Api.Tenant.CreateTenantManuallyCommand) { + return api.post({ + url: '/api/admin/v1/tenants/manual', + data + }) +} + +/** + * 获取租户详情(包含认证信息、订阅信息) + */ +export function fetchGetTenantDetail(tenantId: string, options?: { showErrorMessage?: boolean }) { + return api.get({ + url: `/api/admin/v1/tenants/${tenantId}`, + showErrorMessage: options?.showErrorMessage + }) +} + +/** + * 获取租户配额使用情况 + */ +export function fetchGetTenantQuotaUsage( + tenantId: string, + options?: { showErrorMessage?: boolean } +) { + return api.get({ + url: `/api/admin/v1/tenants/${tenantId}/quota-usage`, + showErrorMessage: options?.showErrorMessage + }) +} + +/** + * 获取租户配额使用历史 + * @param tenantId 租户ID + * @param params 分页参数 + */ +export function fetchGetTenantQuotaUsageHistory( + tenantId: string, + params?: { Page?: number; PageSize?: number } +) { + return api.get>({ + url: `/api/admin/v1/tenants/${tenantId}/quota-usage-history`, + params + }) +} + +/** + * 获取订阅信息(注意:该接口为 POST) + */ +export function fetchGetTenantSubscriptions( + tenantId: string, + options?: { showErrorMessage?: boolean } +) { + return api.post({ + url: `/api/admin/v1/tenants/${tenantId}/subscriptions`, + data: {}, + showErrorMessage: options?.showErrorMessage + }) +} + +/** + * 获取账单信息(分页,最近10条可传 Page=1 PageSize=10) + */ +export function fetchGetTenantBillings( + tenantId: string, + params: { Page?: number; PageSize?: number } = {}, + options?: { showErrorMessage?: boolean } +) { + return api.get>({ + url: `/api/admin/v1/tenants/${tenantId}/billings`, + params, + showErrorMessage: options?.showErrorMessage + }) +} + +/** + * 更新租户信息(TD-001:待后端接口补充) + * @param tenantId 租户ID(Snowflake long → string) + * @param data 更新数据(tenantId 会以入参为准覆盖) + * @returns void(后端 BaseResponse 的 data 字段) + * + * 后端接口:PUT /api/admin/v1/tenants/{tenantId} + * 预期响应:BaseResponse + * 预期错误码:400(参数错误)、404(租户不存在)、409(code 冲突) + * TODO(TD-001): Swagger 中缺失该端点(当前可能返回 404/405),待后端补充后再联调验证 + */ +export function fetchUpdateTenant(tenantId: string, data: UpdateTenantCommand) { + return api.put({ + url: `/api/admin/v1/tenants/${tenantId}`, + data: { ...data, tenantId }, + showErrorMessage: false + }) +} + +/** + * 获取租户套餐列表(兼容旧调用) + */ +export const fetchGetTenantPackages = fetchTenantPackageList + +/** + * 审核租户 + */ +export function fetchReviewTenant( + tenantId: string, + data: Omit +) { + return api.post({ + url: `/api/admin/v1/tenants/${tenantId}/review`, + data: { ...data, tenantId } + }) +} + +/** + * 获取租户审核领取信息(未领取返回 null) + */ +export function fetchGetTenantReviewClaim(tenantId: string) { + return api.get({ + url: `/api/admin/v1/tenants/${tenantId}/review/claim` + }) +} + +/** + * 领取租户审核 + */ +export function fetchClaimTenantReview(tenantId: string) { + return api.post({ + url: `/api/admin/v1/tenants/${tenantId}/review/claim` + }) +} + +/** + * 强制接管租户审核(仅超级管理员) + */ +export function fetchForceClaimTenantReview(tenantId: string) { + return api.post({ + url: `/api/admin/v1/tenants/${tenantId}/review/force-claim` + }) +} + +/** + * 释放租户审核领取(仅领取人) + */ +export function fetchReleaseTenantReviewClaim(tenantId: string) { + return api.post({ + url: `/api/admin/v1/tenants/${tenantId}/review/release` + }) +} + +/** + * 查询租户审核日志 + */ +export function fetchGetTenantAuditLogs( + tenantId: string, + params: { page?: number; pageSize?: number } = {} +) { + return api.get({ + url: `/api/admin/v1/tenants/${tenantId}/audits`, + params: { + page: params.page ?? 1, + pageSize: params.pageSize ?? 20 + } + }) +} + +/** + * 冻结租户(暂停服务) + */ +export function fetchFreezeTenant( + tenantId: string, + data: Omit +) { + return api.post({ + url: `/api/admin/v1/tenants/${tenantId}/freeze`, + data: { ...data, tenantId } + }) +} + +/** + * 解冻租户(恢复服务) + */ +export function fetchUnfreezeTenant( + tenantId: string, + data: Omit = {} +) { + return api.post({ + url: `/api/admin/v1/tenants/${tenantId}/unfreeze`, + data: { ...data, tenantId } + }) +} + +/** + * 延期/赠送租户订阅时长(按当前订阅套餐续费) + */ +export function fetchExtendTenantSubscription( + tenantId: string, + data: Omit +) { + return api.post({ + url: `/api/admin/v1/tenants/${tenantId}/subscriptions/extend`, + data: { ...data, tenantId } + }) +} + +/** + * 伪装登录租户(返回租户主管理员的 Token) + */ +export function fetchImpersonateTenant(tenantId: string) { + return api.post({ + url: `/api/admin/v1/tenants/${tenantId}/impersonate` + }) +} + +/** + * 生成租户主管理员重置密码链接(仅展示一次) + */ +export function fetchCreateTenantAdminResetLink(tenantId: string) { + return api.post({ + url: `/api/admin/v1/tenants/${tenantId}/admin/reset-link` + }) +} + +/** + * 获取租户配额购买记录(分页) + */ +export function fetchGetTenantQuotaPurchases( + tenantId: string, + params: { Page?: number; PageSize?: number } = {} +) { + return api.get>({ + url: `/api/admin/v1/tenants/${tenantId}/quota-purchases`, + params: { + Page: params.Page ?? 1, + PageSize: params.PageSize ?? 10 + } + }) +} diff --git a/src/assets/images/avatar/avatar.webp b/src/assets/images/avatar/avatar.webp new file mode 100644 index 0000000..bea307b Binary files /dev/null and b/src/assets/images/avatar/avatar.webp differ diff --git a/src/assets/images/avatar/avatar1.webp b/src/assets/images/avatar/avatar1.webp new file mode 100644 index 0000000..68e256c Binary files /dev/null and b/src/assets/images/avatar/avatar1.webp differ diff --git a/src/assets/images/avatar/avatar10.webp b/src/assets/images/avatar/avatar10.webp new file mode 100644 index 0000000..a813d4c Binary files /dev/null and b/src/assets/images/avatar/avatar10.webp differ diff --git a/src/assets/images/avatar/avatar2.webp b/src/assets/images/avatar/avatar2.webp new file mode 100644 index 0000000..6716e3f Binary files /dev/null and b/src/assets/images/avatar/avatar2.webp differ diff --git a/src/assets/images/avatar/avatar3.webp b/src/assets/images/avatar/avatar3.webp new file mode 100644 index 0000000..7355ad4 Binary files /dev/null and b/src/assets/images/avatar/avatar3.webp differ diff --git a/src/assets/images/avatar/avatar4.webp b/src/assets/images/avatar/avatar4.webp new file mode 100644 index 0000000..56a9549 Binary files /dev/null and b/src/assets/images/avatar/avatar4.webp differ diff --git a/src/assets/images/avatar/avatar5.webp b/src/assets/images/avatar/avatar5.webp new file mode 100644 index 0000000..f78400c Binary files /dev/null and b/src/assets/images/avatar/avatar5.webp differ diff --git a/src/assets/images/avatar/avatar6.webp b/src/assets/images/avatar/avatar6.webp new file mode 100644 index 0000000..9771b78 Binary files /dev/null and b/src/assets/images/avatar/avatar6.webp differ diff --git a/src/assets/images/avatar/avatar7.webp b/src/assets/images/avatar/avatar7.webp new file mode 100644 index 0000000..e5ef6fe Binary files /dev/null and b/src/assets/images/avatar/avatar7.webp differ diff --git a/src/assets/images/avatar/avatar8.webp b/src/assets/images/avatar/avatar8.webp new file mode 100644 index 0000000..b66e48f Binary files /dev/null and b/src/assets/images/avatar/avatar8.webp differ diff --git a/src/assets/images/avatar/avatar9.webp b/src/assets/images/avatar/avatar9.webp new file mode 100644 index 0000000..7974139 Binary files /dev/null and b/src/assets/images/avatar/avatar9.webp differ diff --git a/src/assets/images/ceremony/hb.png b/src/assets/images/ceremony/hb.png new file mode 100644 index 0000000..4103324 Binary files /dev/null and b/src/assets/images/ceremony/hb.png differ diff --git a/src/assets/images/ceremony/sd.png b/src/assets/images/ceremony/sd.png new file mode 100644 index 0000000..75ec838 Binary files /dev/null and b/src/assets/images/ceremony/sd.png differ diff --git a/src/assets/images/ceremony/xc.png b/src/assets/images/ceremony/xc.png new file mode 100644 index 0000000..9c7ab67 Binary files /dev/null and b/src/assets/images/ceremony/xc.png differ diff --git a/src/assets/images/ceremony/yd.png b/src/assets/images/ceremony/yd.png new file mode 100644 index 0000000..426912d Binary files /dev/null and b/src/assets/images/ceremony/yd.png differ diff --git a/src/assets/images/common/logo.webp b/src/assets/images/common/logo.webp new file mode 100644 index 0000000..71542b5 Binary files /dev/null and b/src/assets/images/common/logo.webp differ diff --git a/src/assets/images/draw/draw1.png b/src/assets/images/draw/draw1.png new file mode 100644 index 0000000..da5d87a Binary files /dev/null and b/src/assets/images/draw/draw1.png differ diff --git a/src/assets/images/favicon.ico b/src/assets/images/favicon.ico new file mode 100644 index 0000000..21e7063 Binary files /dev/null and b/src/assets/images/favicon.ico differ diff --git a/src/assets/images/lock/bg_dark.webp b/src/assets/images/lock/bg_dark.webp new file mode 100644 index 0000000..1c33435 Binary files /dev/null and b/src/assets/images/lock/bg_dark.webp differ diff --git a/src/assets/images/lock/bg_light.webp b/src/assets/images/lock/bg_light.webp new file mode 100644 index 0000000..00efbd9 Binary files /dev/null and b/src/assets/images/lock/bg_light.webp differ diff --git a/src/assets/images/login/lf_icon2.webp b/src/assets/images/login/lf_icon2.webp new file mode 100644 index 0000000..5e4f3fd Binary files /dev/null and b/src/assets/images/login/lf_icon2.webp differ diff --git a/src/assets/images/settings/menu_layouts/dual_column.png b/src/assets/images/settings/menu_layouts/dual_column.png new file mode 100644 index 0000000..9b868ca Binary files /dev/null and b/src/assets/images/settings/menu_layouts/dual_column.png differ diff --git a/src/assets/images/settings/menu_layouts/horizontal.png b/src/assets/images/settings/menu_layouts/horizontal.png new file mode 100644 index 0000000..ca779bc Binary files /dev/null and b/src/assets/images/settings/menu_layouts/horizontal.png differ diff --git a/src/assets/images/settings/menu_layouts/mixed.png b/src/assets/images/settings/menu_layouts/mixed.png new file mode 100644 index 0000000..c82b580 Binary files /dev/null and b/src/assets/images/settings/menu_layouts/mixed.png differ diff --git a/src/assets/images/settings/menu_layouts/vertical.png b/src/assets/images/settings/menu_layouts/vertical.png new file mode 100644 index 0000000..16e942b Binary files /dev/null and b/src/assets/images/settings/menu_layouts/vertical.png differ diff --git a/src/assets/images/settings/menu_styles/dark.png b/src/assets/images/settings/menu_styles/dark.png new file mode 100644 index 0000000..e1653b7 Binary files /dev/null and b/src/assets/images/settings/menu_styles/dark.png differ diff --git a/src/assets/images/settings/menu_styles/design.png b/src/assets/images/settings/menu_styles/design.png new file mode 100644 index 0000000..7681aa8 Binary files /dev/null and b/src/assets/images/settings/menu_styles/design.png differ diff --git a/src/assets/images/settings/menu_styles/light.png b/src/assets/images/settings/menu_styles/light.png new file mode 100644 index 0000000..3007b99 Binary files /dev/null and b/src/assets/images/settings/menu_styles/light.png differ diff --git a/src/assets/images/settings/theme_styles/dark.png b/src/assets/images/settings/theme_styles/dark.png new file mode 100644 index 0000000..e8c6e44 Binary files /dev/null and b/src/assets/images/settings/theme_styles/dark.png differ diff --git a/src/assets/images/settings/theme_styles/light.png b/src/assets/images/settings/theme_styles/light.png new file mode 100644 index 0000000..6754238 Binary files /dev/null and b/src/assets/images/settings/theme_styles/light.png differ diff --git a/src/assets/images/settings/theme_styles/system.png b/src/assets/images/settings/theme_styles/system.png new file mode 100644 index 0000000..6a6baa9 Binary files /dev/null and b/src/assets/images/settings/theme_styles/system.png differ diff --git a/src/assets/images/svg/403.svg b/src/assets/images/svg/403.svg new file mode 100644 index 0000000..68790ad --- /dev/null +++ b/src/assets/images/svg/403.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/svg/404.svg b/src/assets/images/svg/404.svg new file mode 100644 index 0000000..48e1ca3 --- /dev/null +++ b/src/assets/images/svg/404.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/svg/500.svg b/src/assets/images/svg/500.svg new file mode 100644 index 0000000..512429f --- /dev/null +++ b/src/assets/images/svg/500.svg @@ -0,0 +1,5 @@ + diff --git a/src/assets/images/svg/login_icon.svg b/src/assets/images/svg/login_icon.svg new file mode 100644 index 0000000..4beb3ab --- /dev/null +++ b/src/assets/images/svg/login_icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/user/avatar.webp b/src/assets/images/user/avatar.webp new file mode 100644 index 0000000..6d7234b Binary files /dev/null and b/src/assets/images/user/avatar.webp differ diff --git a/src/assets/images/user/bg.webp b/src/assets/images/user/bg.webp new file mode 100644 index 0000000..762b22d Binary files /dev/null and b/src/assets/images/user/bg.webp differ diff --git a/src/assets/styles/components/_action-btn.scss b/src/assets/styles/components/_action-btn.scss new file mode 100644 index 0000000..cf0841d --- /dev/null +++ b/src/assets/styles/components/_action-btn.scss @@ -0,0 +1,71 @@ +@charset "UTF-8"; + +/** + * 统一的操作按钮样式 + * 用于表格操作列等场景 + */ + +.art-action-btn { + display: inline-flex; + gap: 6px; + align-items: center; + justify-content: center; /* 增加居中 */ + min-width: 64px; + padding: 6px 12px; + font-size: 13px; + line-height: 1; + color: var(--el-text-color-primary); + cursor: pointer; + background: var(--el-fill-color-light); + border: 1px solid var(--el-border-color-lighter); + border-radius: 10px; + transition: all 0.15s ease; /* 移动 transition 到这里 */ + + .art-action-icon { + display: flex; + font-size: 16px; + } + + &:hover { + filter: brightness(0.97); + } + + /* 样式变体 */ + &.primary { + color: var(--el-color-primary); + background: var(--el-color-primary-light-9); + border-color: var(--el-color-primary-light-8); + } + + &.success { + color: var(--el-color-success); + background: var(--el-color-success-light-9); + border-color: var(--el-color-success-light-8); + } + + &.warning { + color: var(--el-color-warning); + background: var(--el-color-warning-light-9); + border-color: var(--el-color-warning-light-8); + } + + &.danger { + color: var(--el-color-danger); + background: var(--el-color-danger-light-9); + border-color: var(--el-color-danger-light-8); + } + + &.info { + color: var(--el-text-color-secondary); + background: var(--el-fill-color-lighter); + border-color: var(--el-border-color-lighter); + } +} + +/* 操作按钮容器 */ +.action-wrap { + display: flex; + flex-wrap: wrap; + gap: 8px; + align-items: center; +} diff --git a/src/assets/styles/core/app.scss b/src/assets/styles/core/app.scss new file mode 100644 index 0000000..c0efeed --- /dev/null +++ b/src/assets/styles/core/app.scss @@ -0,0 +1,292 @@ +// 全局样式 +// 顶部进度条颜色 +#nprogress .bar { + z-index: 2400; + background-color: color-mix(in srgb, var(--theme-color) 70%, white); +} + +#nprogress .peg { + box-shadow: + 0 0 10px var(--theme-color), + 0 0 5px var(--theme-color) !important; +} + +#nprogress .spinner-icon { + border-top-color: var(--theme-color) !important; + border-left-color: var(--theme-color) !important; +} + +// 处理移动端组件兼容性 +@media screen and (max-width: 640px) { + * { + cursor: default !important; + } +} + +// 背景滤镜 +*, +::before, +::after { + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; +} + +// 色弱模式 +.color-weak { + filter: invert(80%); + -webkit-filter: invert(80%); +} + +#noop { + display: none; +} + +// 语言切换选中样式 +.langDropDownStyle { + // 选中项背景颜色 + .is-selected { + background-color: var(--art-el-active-color) !important; + } + + // 语言切换按钮菜单样式优化 + .lang-btn-item { + .el-dropdown-menu__item { + padding-left: 13px !important; + padding-right: 6px !important; + margin-bottom: 3px !important; + } + + &:last-child { + .el-dropdown-menu__item { + margin-bottom: 0 !important; + } + } + + .menu-txt { + min-width: 60px; + display: block; + } + + i { + font-size: 10px; + margin-left: 10px; + } + } +} + +// 盒子默认边框 +.page-content { + border: 1px solid var(--art-card-border) !important; +} + +@mixin art-card-base($border-color, $shadow: none, $radius-diff: 4px) { + background: var(--default-box-color); + border: 1px solid #{$border-color} !important; + border-radius: calc(var(--custom-radius) + #{$radius-diff}) !important; + box-shadow: #{$shadow} !important; + + --el-card-border-color: var(--default-border) !important; +} + +.art-card, +.art-card-sm, +.art-card-xs { + border: 1px solid var(--art-card-border); +} + +// 盒子边框 +[data-box-mode='border-mode'] { + .page-content, + .art-table-card { + border: 1px solid var(--art-card-border) !important; + } + + .art-card { + @include art-card-base(var(--art-card-border), none, 4px); + } + + .art-card-sm { + @include art-card-base(var(--art-card-border), none, 0px); + } + + .art-card-xs { + @include art-card-base(var(--art-card-border), none, -4px); + } +} + +// 盒子阴影 +[data-box-mode='shadow-mode'] { + .page-content, + .art-table-card { + box-shadow: 0px 0px 4px 0px rgba(0, 0, 0, 0.04) !important; + border: 1px solid var(--art-gray-200) !important; + } + + .layout-sidebar { + border-right: 1px solid var(--art-card-border) !important; + } + + .art-card { + @include art-card-base( + var(--art-gray-200), + (0 1px 3px 0 rgba(0, 0, 0, 0.03), 0 1px 2px -1px rgba(0, 0, 0, 0.08)), + 4px + ); + } + + .art-card-sm { + @include art-card-base( + var(--art-gray-200), + (0 1px 3px 0 rgba(0, 0, 0, 0.03), 0 1px 2px -1px rgba(0, 0, 0, 0.08)), + 2px + ); + } + + .art-card-xs { + @include art-card-base( + var(--art-gray-200), + (0 1px 2px 0 rgba(0, 0, 0, 0.03), 0 1px 1px -1px rgba(0, 0, 0, 0.08)), + -4px + ); + } +} + +// 元素全屏 +.el-full-screen { + position: fixed; + top: 0; + left: 0; + right: 0; + width: 100vw !important; + height: 100% !important; + z-index: 2300; + margin-top: 0; + padding: 15px; + box-sizing: border-box; + background-color: var(--default-box-color); + display: flex; + flex-direction: column; +} + +// 表格卡片 +.art-table-card { + flex: 1; + display: flex; + flex-direction: column; + margin-top: 12px; + border-radius: calc(var(--custom-radius) / 2 + 2px) !important; + + .el-card__body { + height: 100%; + overflow: hidden; + } +} + +// 容器全高 +.art-full-height { + height: var(--art-full-height); + display: flex; + flex-direction: column; + + @media (max-width: 640px) { + height: auto; + } +} + +// 徽章样式 +.art-badge { + position: absolute; + top: 0; + right: 20px; + bottom: 0; + width: 6px; + height: 6px; + margin: auto; + background: #ff3860; + border-radius: 50%; + animation: breathe 1.5s ease-in-out infinite; + + &.art-badge-horizontal { + right: 0; + } + + &.art-badge-mixed { + right: 0; + } + + &.art-badge-dual { + right: 5px; + top: 5px; + bottom: auto; + } +} + +// 文字徽章样式 +.art-text-badge { + position: absolute; + top: 0; + right: 12px; + bottom: 0; + min-width: 20px; + height: 18px; + line-height: 17px; + padding: 0 5px; + margin: auto; + font-size: 10px; + color: #fff; + text-align: center; + background: #fd4e4e; + border-radius: 4px; +} + +@keyframes breathe { + 0% { + opacity: 0.7; + transform: scale(1); + } + + 50% { + opacity: 1; + transform: scale(1.1); + } + + 100% { + opacity: 0.7; + transform: scale(1); + } +} + +// 修复老机型 loading 定位问题 +.art-loading-fix { + position: fixed !important; + top: 0 !important; + left: 0 !important; + right: 0 !important; + bottom: 0 !important; + width: 100vw !important; + height: 100vh !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; +} + +.art-loading-fix .el-loading-spinner { + position: static !important; + top: auto !important; + left: auto !important; + transform: none !important; +} + +// 去除移动端点击背景色 +@media screen and (max-width: 1180px) { + * { + -webkit-tap-highlight-color: transparent; + } +} diff --git a/src/assets/styles/core/dark.scss b/src/assets/styles/core/dark.scss new file mode 100644 index 0000000..c52abc3 --- /dev/null +++ b/src/assets/styles/core/dark.scss @@ -0,0 +1,93 @@ +/* +* 深色主题 +* 单页面移除深色主题 document.getElementsByTagName("html")[0].removeAttribute('class') +*/ + +$font-color: rgba(#ffffff, 0.85); + +/* 覆盖element-plus默认深色背景色 */ +html.dark { + // element-plus + --el-bg-color: var(--default-box-color); + --el-text-color-regular: #{$font-color}; + + // 富文本编辑器 + // 工具栏背景颜色 + --w-e-toolbar-bg-color: #18191c; + // 输入区域背景颜色 + --w-e-textarea-bg-color: #090909; + // 工具栏文字颜色 + --w-e-toolbar-color: var(--art-gray-600); + // 选中菜单颜色 + --w-e-toolbar-active-bg-color: #25262b; + // 弹窗边框颜色 + --w-e-toolbar-border-color: var(--default-border-dashed); + // 分割线颜色 + --w-e-textarea-border-color: var(--default-border-dashed); + // 链接输入框边框颜色 + --w-e-modal-button-border-color: var(--default-border-dashed); + // 表格头颜色 + --w-e-textarea-slight-bg-color: #090909; + // 按钮背景颜色 + --w-e-modal-button-bg-color: #090909; + // hover toolbar 背景颜色 + --w-e-toolbar-active-color: var(--art-gray-800); +} + +.dark { + .page-content .article-list .item .left .outer > div { + border-right-color: var(--dark-border-color) !important; + } + + // 富文本编辑器 + .editor-wrapper { + *:not(pre code *) { + color: inherit !important; + } + } + // 分隔线 + .w-e-bar-divider { + background-color: var(--art-gray-300) !important; + } + + .w-e-select-list, + .w-e-drop-panel, + .w-e-bar-item-group .w-e-bar-item-menus-container, + .w-e-text-container [data-slate-editor] pre > code { + border: 1px solid var(--default-border) !important; + } + + // 下拉选择框 + .w-e-select-list { + background-color: var(--default-box-color) !important; + } + + /* 下拉选择框 hover 样式调整 */ + .w-e-select-list ul li:hover, + /* 工具栏 hover 按钮背景颜色 */ + .w-e-bar-item button:hover { + background-color: #090909 !important; + } + + /* 代码块 */ + .w-e-text-container [data-slate-editor] pre > code { + background-color: #25262b !important; + text-shadow: none !important; + } + + /* 引用 */ + .w-e-text-container [data-slate-editor] blockquote { + border-left: 4px solid var(--default-border-dashed) !important; + background-color: var(--art-color); + } + + .editor-wrapper { + .w-e-text-container [data-slate-editor] .table-container th:last-of-type { + border-right: 1px solid var(--default-border-dashed) !important; + } + + .w-e-modal { + background-color: var(--art-color); + } + } +} diff --git a/src/assets/styles/core/el-dark.scss b/src/assets/styles/core/el-dark.scss new file mode 100644 index 0000000..8f81cdf --- /dev/null +++ b/src/assets/styles/core/el-dark.scss @@ -0,0 +1,2 @@ +// 导入暗黑主题 +@use 'element-plus/theme-chalk/src/dark/css-vars.scss' as *; diff --git a/src/assets/styles/core/el-light.scss b/src/assets/styles/core/el-light.scss new file mode 100644 index 0000000..ddf2bc5 --- /dev/null +++ b/src/assets/styles/core/el-light.scss @@ -0,0 +1,34 @@ +// https://github.com/element-plus/element-plus/blob/dev/packages/theme-chalk/src/common/var.scss +// 自定义Element 亮色主题 + +@forward 'element-plus/theme-chalk/src/common/var.scss' with ( + $colors: ( + 'white': #ffffff, + 'black': #000000, + 'success': ( + 'base': #13deb9 + ), + 'warning': ( + 'base': #ffae1f + ), + 'danger': ( + 'base': #ff4d4f + ), + 'error': ( + 'base': #fa896b + ) + ), + $button: ( + 'hover-bg-color': var(--el-color-primary-light-9), + 'hover-border-color': var(--el-color-primary), + 'border-color': var(--el-color-primary), + 'text-color': var(--el-color-primary) + ), + $messagebox: ( + 'border-radius': '12px' + ), + $popover: ( + 'padding': '14px', + 'border-radius': '10px' + ) +); diff --git a/src/assets/styles/core/el-ui.scss b/src/assets/styles/core/el-ui.scss new file mode 100644 index 0000000..57c0786 --- /dev/null +++ b/src/assets/styles/core/el-ui.scss @@ -0,0 +1,526 @@ +// 优化 Element Plus 组件库默认样式 + +:root { + // 系统主色 + --main-color: var(--el-color-primary); + --el-color-white: white !important; + --el-color-black: white !important; + // 输入框边框颜色 + // --el-border-color: #E4E4E7 !important; // DCDFE6 + // 按钮粗度 + --el-font-weight-primary: 400 !important; + + --el-component-custom-height: 36px !important; + + --el-component-size: var(--el-component-custom-height) !important; + + // 边框、按钮圆角... + --el-border-radius-base: calc(var(--custom-radius) / 3 + 2px) !important; + + --el-border-radius-small: calc(var(--custom-radius) / 3 + 4px) !important; + --el-messagebox-border-radius: calc(var(--custom-radius) / 3 + 4px) !important; + --el-popover-border-radius: calc(var(--custom-radius) / 3 + 4px) !important; + + .region .el-radio-button__original-radio:checked + .el-radio-button__inner { + color: var(--theme-color); + } +} + +// 优化 el-form-item 标签高度 +.el-form-item__label { + height: var(--el-component-custom-height) !important; + line-height: var(--el-component-custom-height) !important; +} + +// 日期选择器 +.el-date-range-picker { + --el-datepicker-inrange-bg-color: var(--art-gray-200) !important; +} + +// el-card 背景色跟系统背景色保持一致 +html.dark .el-card { + --el-card-bg-color: var(--default-box-color) !important; +} + +// 修改 el-pagination 大小 +.el-pagination--default { + & { + --el-pagination-button-width: 32px !important; + --el-pagination-button-height: var(--el-pagination-button-width) !important; + } + + @media (max-width: 1180px) { + & { + --el-pagination-button-width: 28px !important; + } + } + + .el-select--default .el-select__wrapper { + min-height: var(--el-pagination-button-width) !important; + } + + .el-pagination__jump .el-input { + height: var(--el-pagination-button-width) !important; + } +} + +.el-pager li { + padding: 0 10px !important; + // border: 1px solid red !important; +} + +// 优化菜单折叠展开动画(提升动画流畅度) +.el-menu.el-menu--inline { + transition: max-height 0.26s cubic-bezier(0.4, 0, 0.2, 1) !important; +} + +// 优化菜单 item hover 动画(提升鼠标跟手感) +.el-sub-menu__title, +.el-menu-item { + transition: background-color 0s !important; +} + +// -------------------------------- 修改 el-size=default 组件默认高度 start -------------------------------- +// 修改 el-button 高度 +.el-button--default { + height: var(--el-component-custom-height) !important; +} + +// circle 按钮宽度优化 +.el-button--default.is-circle { + width: var(--el-component-custom-height) !important; +} + +// 修改 el-select 高度 +.el-select--default { + .el-select__wrapper { + min-height: var(--el-component-custom-height) !important; + } +} + +// 修改 el-checkbox-button 高度 +.el-checkbox-button--default .el-checkbox-button__inner, +// 修改 el-radio-button 高度 +.el-radio-button--default .el-radio-button__inner { + padding: 10px 15px !important; +} +// -------------------------------- 修改 el-size=default 组件默认高度 end -------------------------------- + +.el-pagination.is-background .btn-next, +.el-pagination.is-background .btn-prev, +.el-pagination.is-background .el-pager li { + border-radius: 6px; +} + +.el-popover { + min-width: 80px; + border-radius: var(--el-border-radius-small) !important; +} + +.el-dialog { + border-radius: 100px !important; + border-radius: calc(var(--custom-radius) / 1.2 + 2px) !important; + overflow: hidden; +} + +.el-dialog__header { + .el-dialog__title { + font-size: 16px; + } +} + +.el-dialog__body { + padding: 25px 0 !important; + position: relative; // 为了兼容 el-pagination 样式,需要设置 relative,不然会影响 el-pagination 的样式,比如 el-pagination__jump--small 会被影响,导致 el-pagination__jump--small 按钮无法点击,详见 URL_ADDRESS.com/element-plus/element-plus/issues/5684#issuecomment-1176299275; +} + +.el-dialog.el-dialog-border { + .el-dialog__body { + // 上边框 + &::before, + // 下边框 + &::after { + content: ''; + position: absolute; + left: -16px; + width: calc(100% + 32px); + height: 1px; + background-color: var(--art-gray-300); + } + + &::before { + top: 0; + } + + &::after { + bottom: 0; + } + } +} + +// el-message 样式优化 +.el-message { + background-color: var(--default-box-color) !important; + border: 0 !important; + box-shadow: + 0 6px 16px 0 rgba(0, 0, 0, 0.08), + 0 3px 6px -4px rgba(0, 0, 0, 0.12), + 0 9px 28px 8px rgba(0, 0, 0, 0.05) !important; + + p { + font-size: 13px; + } +} + +// 修改 el-dropdown 样式 +.el-dropdown-menu { + padding: 6px !important; + border-radius: 10px !important; + border: none !important; + + .el-dropdown-menu__item { + padding: 6px 16px !important; + border-radius: 6px !important; + + &:hover:not(.is-disabled) { + color: var(--art-gray-900) !important; + background-color: var(--art-el-active-color) !important; + } + + &:focus:not(.is-disabled) { + color: var(--art-gray-900) !important; + background-color: var(--art-gray-200) !important; + } + } +} + +// 隐藏 select、dropdown 的三角 +.el-select__popper, +.el-dropdown__popper { + margin-top: -6px !important; + + .el-popper__arrow { + display: none; + } +} + +.el-dropdown-selfdefine:focus { + outline: none !important; +} + +// 处理移动端组件兼容性 +@media screen and (max-width: 640px) { + .el-message-box, + .el-dialog { + width: calc(100% - 24px) !important; + } + + .el-date-picker.has-sidebar.has-time { + width: calc(100% - 24px); + left: 12px !important; + } + + .el-picker-panel *[slot='sidebar'], + .el-picker-panel__sidebar { + display: none; + } + + .el-picker-panel *[slot='sidebar'] + .el-picker-panel__body, + .el-picker-panel__sidebar + .el-picker-panel__body { + margin-left: 0; + } +} + +// 修改el-button样式 +.el-button { + &.el-button--text { + background-color: transparent !important; + padding: 0 !important; + + span { + margin-left: 0 !important; + } + } +} + +// 修改el-tag样式 +.el-tag { + font-weight: 500; + transition: all 0s !important; + + &.el-tag--default { + height: 26px !important; + } +} + +.el-checkbox-group { + &.el-table-filter__checkbox-group label.el-checkbox { + height: 17px !important; + + .el-checkbox__label { + font-weight: 400 !important; + } + } +} + +.el-radio--default { + // 优化单选按钮大小 + .el-radio__input { + .el-radio__inner { + width: 16px; + height: 16px; + + &::after { + width: 6px; + height: 6px; + } + } + } +} + +.el-checkbox { + .el-checkbox__inner { + border-radius: 2px !important; + } +} + +// 优化复选框样式 +.el-checkbox--default { + .el-checkbox__inner { + width: 16px !important; + height: 16px !important; + border-radius: 4px !important; + + &::before { + content: ''; + height: 4px !important; + top: 5px !important; + background-color: #fff !important; + transform: scale(0.6) !important; + } + } + + .is-checked { + .el-checkbox__inner { + &::after { + width: 3px; + height: 8px; + margin: auto; + border: 2px solid var(--el-checkbox-checked-icon-color); + border-left: 0; + border-top: 0; + transform: translate(-45%, -60%) rotate(45deg) scale(0.86) !important; + transform-origin: center; + } + } + } +} + +.el-notification .el-notification__icon { + font-size: 22px !important; +} + +// 修改 el-message-box 样式 +.el-message-box__headerbtn .el-message-box__close, +.el-dialog__headerbtn .el-dialog__close { + top: 7px; + right: 7px; + width: 30px; + height: 30px; + border-radius: 5px; + transition: all 0.3s; + + &:hover { + background-color: var(--art-hover-color) !important; + color: var(--art-gray-900) !important; + } +} + +.el-message-box { + padding: 25px 20px !important; +} + +.el-message-box__title { + font-weight: 500 !important; +} + +.el-table__column-filter-trigger i { + color: var(--theme-color) !important; + margin: -3px 0 0 2px; +} + +// 去除 el-dropdown 鼠标放上去出现的边框 +.el-tooltip__trigger:focus-visible { + outline: unset; +} + +// ipad 表单右侧按钮优化 +@media screen and (max-width: 1180px) { + .el-table-fixed-column--right { + padding-right: 0 !important; + } +} + +.login-out-dialog { + padding: 30px 20px !important; + border-radius: 10px !important; +} + +// 修改 dialog 动画 +.dialog-fade-enter-active { + .el-dialog:not(.is-draggable) { + animation: dialog-open 0.3s cubic-bezier(0.32, 0.14, 0.15, 0.86); + + // 修复 el-dialog 动画后宽度不自适应问题 + .el-select__selected-item { + display: inline-block; + } + } +} + +.dialog-fade-leave-active { + animation: fade-out 0.2s linear; + + .el-dialog:not(.is-draggable) { + animation: dialog-close 0.5s; + } +} + +@keyframes dialog-open { + 0% { + opacity: 0; + transform: scale(0.2); + } + + 100% { + opacity: 1; + transform: scale(1); + } +} + +@keyframes dialog-close { + 0% { + opacity: 1; + transform: scale(1); + } + + 100% { + opacity: 0; + transform: scale(0.2); + } +} + +// 遮罩层动画 +@keyframes fade-out { + 0% { + opacity: 1; + } + + 100% { + opacity: 0; + } +} + +// 修改 el-select 样式 +.el-select__popper:not(.el-tree-select__popper) { + .el-select-dropdown__list { + padding: 5px !important; + + .el-select-dropdown__item { + height: 34px !important; + line-height: 34px !important; + border-radius: 6px !important; + + &.is-selected { + color: var(--art-gray-900) !important; + font-weight: 400 !important; + background-color: var(--art-el-active-color) !important; + margin-bottom: 4px !important; + } + + &:hover { + background-color: var(--art-hover-color) !important; + } + } + + .el-select-dropdown__item:hover ~ .is-selected, + .el-select-dropdown__item.is-selected:has(~ .el-select-dropdown__item:hover) { + background-color: transparent !important; + } + } +} + +// 修改 el-tree-select 样式 +.el-tree-select__popper { + .el-select-dropdown__list { + padding: 5px !important; + + .el-tree-node { + .el-tree-node__content { + height: 36px !important; + border-radius: 6px !important; + + &:hover { + background-color: var(--art-gray-200) !important; + } + } + } + } +} + +// 实现水波纹在文字下面效果 +.el-button > span { + position: relative; + z-index: 10; +} + +// 优化颜色选择器圆角 +.el-color-picker__color { + border-radius: 2px !important; +} + +// 优化日期时间选择器底部圆角 +.el-picker-panel { + .el-picker-panel__footer { + border-radius: 0 0 var(--el-border-radius-base) var(--el-border-radius-base); + } +} + +// 优化树型菜单样式 +.el-tree-node__content { + border-radius: 4px; + margin-bottom: 4px; + padding: 1px 0; + + &:hover { + background-color: var(--art-hover-color) !important; + } +} + +.dark { + .el-tree--highlight-current .el-tree-node.is-current > .el-tree-node__content { + background-color: var(--art-gray-300) !important; + } +} + +// 隐藏折叠菜单弹窗 hover 出现的边框 +.menu-left-popper:focus-within, +.horizontal-menu-popper:focus-within { + box-shadow: none !important; + outline: none !important; +} + +// 数字输入组件右侧按钮高度跟随自定义组件高度 +.el-input-number--default.is-controls-right { + .el-input-number__decrease, + .el-input-number__increase { + height: calc((var(--el-component-size) / 2)) !important; + } +} + +// 全局输入框文本左对齐(统一表单输入体验) +.el-input__inner, +.el-textarea__inner, +.el-input-number .el-input__inner { + text-align: left !important; +} diff --git a/src/assets/styles/core/md.scss b/src/assets/styles/core/md.scss new file mode 100644 index 0000000..b22fdc2 --- /dev/null +++ b/src/assets/styles/core/md.scss @@ -0,0 +1,1036 @@ +/* 文章标题设置(h1-h6)*/ +/* ------------------------------------------------ */ +$font-color: #24292e; + +.markdown-body h1, +.markdown-body h2, +.markdown-body h3, +.markdown-body h4, +.markdown-body h5, +.markdown-body h6 { + color: var(--art-gray-800) !important; + margin: 30px 0 10px 0; + font-weight: 600; +} + +.markdown-body h1 { + font-size: 30px; +} + +@media only screen and (max-width: 550px) { + .markdown-body h1 { + font-size: 26px; + } + + .markdown-body h2 { + font-size: 22px; + } + + .markdown-body h3 { + font-size: 18px; + } +} + +/* 块引用 */ +/* ------------------------------------------------ */ +.markdown-body blockquote { + color: rgba(60, 60, 67, 0.7); + font-size: 15px !important; + border-left: 0.18em solid #e7e7e8; + background: #f8f8f8; + padding: 15px 1em; + font-weight: 400 !important; +} + +/* 详情页文章字体颜色 */ +/* ------------------------------------------------ */ +.markdown-body p { + line-height: 28px; + margin-bottom: 10px; +} + +.markdown-body li, +.markdown-body p { + color: var(--art-gray-800) !important; + font-size: 16px !important; +} + +.dark .markdown-body li span { + color: var(--art-gray-800) !important; + background-color: transparent !important; +} + +.dark .markdown-body p span { + color: var(--art-gray-800) !important; + background-color: transparent !important; +} + +.line-numbers-mode { + background-color: var(--art-code-bg); + border-radius: 8px; + position: relative; + padding-left: 32px; + box-sizing: border-box; +} + +.line-numbers-mode pre { + flex: 1; + border-radius: 0 8px 8px 0; + background-color: var(--art-code-bg); +} + +.line-numbers-mode .line-numbers-wrapper { + width: 32px; + height: 100%; + text-align: center; + padding: 16px 0; + box-sizing: border-box; + border-right: 1px solid #000000; + position: absolute; + left: 0; + top: 0; +} + +.line-numbers-mode .line-numbers-wrapper span { + height: 23.6px; + line-height: 23.6px; + display: block; + color: #72747b; + font-size: 13px; + box-sizing: border-box; +} + +.line-numbers-mode .copy-btn { + display: inline-block; + display: flex; + position: absolute; + right: 10px; + top: 10px; + cursor: pointer; + opacity: 0; + background-color: #000; + border-radius: 5px; + text-align: center; + color: rgba(255, 255, 255, 0.6); + transition: opacity 0.3s; +} + +.line-numbers-mode .copy-btn div { + width: 34px; + height: 34px; + line-height: 34px; + cursor: pointer; + text-align: center; + font-size: 20px; +} + +.line-numbers-mode:hover .copy-btn { + opacity: 1; +} + +.line-numbers-mode .copy-btn span { + height: 34px; + line-height: 34px; + font-size: 13px; + padding-left: 10px; + display: none; +} + +.line-numbers-mode .copy-btn .show-copy { + opacity: 1; + display: block; +} + +.line-numbers-mode ::-webkit-scrollbar-track { + background-color: #292b30 !important; +} + +.markdown-body .anchor { + float: left; + line-height: 1; + margin-left: -20px; + padding-right: 4px; +} + +.markdown-body .anchor:focus { + outline: none; +} + +.markdown-body h1 .octicon-link, +.markdown-body h2 .octicon-link, +.markdown-body h3 .octicon-link, +.markdown-body h4 .octicon-link, +.markdown-body h5 .octicon-link, +.markdown-body h6 .octicon-link { + color: #1b1f23; + vertical-align: middle; + visibility: hidden; +} + +.markdown-body h1:hover .anchor, +.markdown-body h2:hover .anchor, +.markdown-body h3:hover .anchor, +.markdown-body h4:hover .anchor, +.markdown-body h5:hover .anchor, +.markdown-body h6:hover .anchor { + text-decoration: none; +} + +.markdown-body h1:hover .anchor .octicon-link, +.markdown-body h2:hover .anchor .octicon-link, +.markdown-body h3:hover .anchor .octicon-link, +.markdown-body h4:hover .anchor .octicon-link, +.markdown-body h5:hover .anchor .octicon-link, +.markdown-body h6:hover .anchor .octicon-link { + visibility: visible; +} + +.markdown-body h1:hover .anchor .octicon-link:before, +.markdown-body h2:hover .anchor .octicon-link:before, +.markdown-body h3:hover .anchor .octicon-link:before, +.markdown-body h4:hover .anchor .octicon-link:before, +.markdown-body h5:hover .anchor .octicon-link:before, +.markdown-body h6:hover .anchor .octicon-link:before { + width: 16px; + height: 16px; + content: ' '; + display: inline-block; +} + +.markdown-body { + -ms-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; + line-height: 1.5; + color: $font-color; + font-size: 16px; + line-height: 1.5; + word-wrap: break-word; +} + +.markdown-body details { + display: block; +} + +.markdown-body summary { + display: list-item; +} + +.markdown-body a { + background-color: initial; +} + +.markdown-body a:active, +.markdown-body a:hover { + outline-width: 0; +} + +.markdown-body strong { + font-weight: inherit; + font-weight: bolder; +} + +.markdown-body p br { + display: inline; + line-height: 11px; +} + +.markdown-body img { + border-style: none; +} + +.markdown-body hr { + box-sizing: initial; + height: 0; + overflow: visible; +} + +.markdown-body input { + font: inherit; + margin: 0; +} + +.markdown-body input { + overflow: visible; +} + +.markdown-body [type='checkbox'] { + box-sizing: border-box; + padding: 0; +} + +.markdown-body * { + box-sizing: border-box; +} + +.markdown-body input { + font-size: inherit; + line-height: inherit; +} + +.markdown-body a { + color: #0366d6; + text-decoration: none; +} + +.markdown-body a:hover { + text-decoration: underline; +} + +.markdown-body strong { + font-weight: 600; +} + +.markdown-body hr { + height: 0; + margin: 15px 0; + overflow: hidden; + background: transparent; + border: 0; + border-bottom: 1px solid #dfe2e5; +} + +.markdown-body hr:after, +.markdown-body hr:before { + display: table; + content: ''; +} + +.markdown-body hr:after { + clear: both; +} + +.markdown-body table { + border-spacing: 0; + border-collapse: collapse; +} + +.markdown-body td, +.markdown-body th { + padding: 0; +} + +.markdown-body details summary { + cursor: pointer; +} + +.markdown-body kbd { + display: inline-block; + padding: 3px 5px; + font: + 11px SFMono-Regular, + Consolas, + Liberation Mono, + Menlo, + monospace; + line-height: 10px; + color: #444d56; + vertical-align: middle; + background-color: #fafbfc; + border: 1px solid #d1d5da; + border-radius: 3px; + box-shadow: inset 0 -1px 0 #d1d5da; +} + +.markdown-body blockquote { + margin: 0; +} + +.markdown-body ol, +.markdown-body ul { + padding-left: 0; + margin-top: 0; + margin-bottom: 0; +} + +.markdown-body ol ol, +.markdown-body ul ol { + list-style-type: lower-roman; +} + +.markdown-body ol ol ol, +.markdown-body ol ul ol, +.markdown-body ul ol ol, +.markdown-body ul ul ol { + list-style-type: lower-alpha; +} + +.markdown-body dd { + margin-left: 0; +} + +.markdown-body code, +.markdown-body pre, +.markdown-body .line-number { + font-size: 14px !important; + border-radius: 8px; + background-color: #282c34; +} + +.dark { + .markdown-body code, + .markdown-body pre, + .markdown-body .line-number { + background-color: #252525; + } +} + +.markdown-body pre { + margin-top: 0; + margin-bottom: 0; +} + +.markdown-body input::-webkit-inner-spin-button, +.markdown-body input::-webkit-outer-spin-button { + margin: 0; + -webkit-appearance: none; + appearance: none; +} + +.markdown-body :checked + .radio-label { + position: relative; + z-index: 1; + border-color: #0366d6; +} + +.markdown-body .border { + border: 1px solid #e1e4e8 !important; +} + +.markdown-body .border-0 { + border: 0 !important; +} + +.markdown-body .border-bottom { + border-bottom: 1px solid #e1e4e8 !important; +} + +.markdown-body .rounded-1 { + border-radius: 3px !important; +} + +.markdown-body .bg-white { + background-color: #fff !important; +} + +.markdown-body .bg-gray-light { + background-color: #fafbfc !important; +} + +.markdown-body .text-gray-light { + color: #6a737d !important; +} + +.markdown-body .mb-0 { + margin-bottom: 0 !important; +} + +.markdown-body .my-2 { + margin-top: 8px !important; + margin-bottom: 8px !important; +} + +.markdown-body .pl-0 { + padding-left: 0 !important; +} + +.markdown-body .py-0 { + padding-top: 0 !important; + padding-bottom: 0 !important; +} + +.markdown-body .pl-1 { + padding-left: 4px !important; +} + +.markdown-body .pl-2 { + padding-left: 8px !important; +} + +.markdown-body .py-2 { + padding-top: 8px !important; + padding-bottom: 8px !important; +} + +.markdown-body .pl-3, +.markdown-body .px-3 { + padding-left: 16px !important; +} + +.markdown-body .px-3 { + padding-right: 16px !important; +} + +.markdown-body .pl-4 { + padding-left: 24px !important; +} + +.markdown-body .pl-5 { + padding-left: 32px !important; +} + +.markdown-body .pl-6 { + padding-left: 40px !important; +} + +.markdown-body .f6 { + font-size: 12px !important; +} + +.markdown-body .lh-condensed { + line-height: 1.25 !important; +} + +.markdown-body .text-bold { + font-weight: 600 !important; +} + +.markdown-body .pl-c { + color: #6a737d; +} + +.markdown-body .pl-c1, +.markdown-body .pl-s .pl-v { + color: #005cc5; +} + +.markdown-body .pl-e, +.markdown-body .pl-en { + color: #6f42c1; +} + +.markdown-body .pl-s .pl-s1, +.markdown-body .pl-smi { + color: $font-color; +} + +.markdown-body .pl-ent { + color: #22863a; +} + +.markdown-body .pl-k { + color: #d73a49; +} + +.markdown-body .pl-pds, +.markdown-body .pl-s, +.markdown-body .pl-s .pl-pse .pl-s1, +.markdown-body .pl-sr, +.markdown-body .pl-sr .pl-cce, +.markdown-body .pl-sr .pl-sra, +.markdown-body .pl-sr .pl-sre { + color: #032f62; +} + +.markdown-body .pl-smw, +.markdown-body .pl-v { + color: #e36209; +} + +.markdown-body .pl-bu { + color: #b31d28; +} + +.markdown-body .pl-ii { + color: #fafbfc; + background-color: #b31d28; +} + +.markdown-body .pl-c2 { + color: #fafbfc; + background-color: #d73a49; +} + +.markdown-body .pl-c2:before { + content: '^M'; +} + +.markdown-body .pl-sr .pl-cce { + font-weight: 700; + color: #22863a; +} + +.markdown-body .pl-ml { + color: #735c0f; +} + +.markdown-body .pl-mh, +.markdown-body .pl-mh .pl-en, +.markdown-body .pl-ms { + font-weight: 700; + color: #005cc5; +} + +.markdown-body .pl-mi { + font-style: italic; + color: $font-color; +} + +.markdown-body .pl-mb { + font-weight: 700; + color: $font-color; +} + +.markdown-body .pl-md { + color: #b31d28; + background-color: #ffeef0; +} + +.markdown-body .pl-mi1 { + color: #22863a; + background-color: #f0fff4; +} + +.markdown-body .pl-mc { + color: #e36209; + background-color: #ffebda; +} + +.markdown-body .pl-mi2 { + color: #f6f8fa; + background-color: #005cc5; +} + +.markdown-body .pl-mdr { + font-weight: 700; + color: #6f42c1; +} + +.markdown-body .pl-ba { + color: #586069; +} + +.markdown-body .pl-sg { + color: #959da5; +} + +.markdown-body .pl-corl { + text-decoration: underline; + color: #032f62; +} + +.markdown-body .mb-0 { + margin-bottom: 0 !important; +} + +.markdown-body .my-2 { + margin-bottom: 8px !important; +} + +.markdown-body .my-2 { + margin-top: 8px !important; +} + +.markdown-body .pl-0 { + padding-left: 0 !important; +} + +.markdown-body .py-0 { + padding-top: 0 !important; + padding-bottom: 0 !important; +} + +.markdown-body .pl-1 { + padding-left: 4px !important; +} + +.markdown-body .pl-2 { + padding-left: 8px !important; +} + +.markdown-body .py-2 { + padding-top: 8px !important; + padding-bottom: 8px !important; +} + +.markdown-body .pl-3 { + padding-left: 16px !important; +} + +.markdown-body .pl-4 { + padding-left: 24px !important; +} + +.markdown-body .pl-5 { + padding-left: 32px !important; +} + +.markdown-body .pl-6 { + padding-left: 40px !important; +} + +.markdown-body .pl-7 { + padding-left: 48px !important; +} + +.markdown-body .pl-8 { + padding-left: 64px !important; +} + +.markdown-body .pl-9 { + padding-left: 80px !important; +} + +.markdown-body .pl-10 { + padding-left: 96px !important; +} + +.markdown-body .pl-11 { + padding-left: 112px !important; +} + +.markdown-body .pl-12 { + padding-left: 128px !important; +} + +.markdown-body hr { + border-bottom-color: #eee; +} + +.markdown-body kbd { + display: inline-block; + padding: 3px 5px; + font: + 11px SFMono-Regular, + Consolas, + Liberation Mono, + Menlo, + monospace; + line-height: 10px; + color: #444d56; + vertical-align: middle; + background-color: #fafbfc; + border: 1px solid #d1d5da; + border-radius: 3px; + box-shadow: inset 0 -1px 0 #d1d5da; +} + +.markdown-body:after, +.markdown-body:before { + display: table; + content: ''; +} + +.markdown-body:after { + clear: both; +} + +.markdown-body > :first-child { + margin-top: 0 !important; +} + +.markdown-body > :last-child { + margin-bottom: 0 !important; +} + +.markdown-body a:not([href]) { + color: inherit; + text-decoration: none; +} + +.markdown-body blockquote, +.markdown-body details, +.markdown-body dl, +.markdown-body ol, +.markdown-body pre, +.markdown-body table, +.markdown-body ul { + margin-top: 0; + margin-bottom: 16px; +} + +.markdown-body hr { + height: 0.25em; + padding: 0; + margin: 24px 0; + background-color: #e1e4e8; + border: 0; +} + +.markdown-body blockquote > :first-child { + margin-top: 0; +} + +.markdown-body blockquote > :last-child { + margin-bottom: 0; +} + +.markdown-body ol, +.markdown-body ul { + padding-left: 1em; +} + +.markdown-body ol ol, +.markdown-body ol ul, +.markdown-body ul ol, +.markdown-body ul ul { + margin-top: 0; + margin-bottom: 0; +} + +.markdown-body li { + line-height: 28px; + font-size: 14px; + word-wrap: break-all; + list-style: disc; + margin-left: 10px; +} + +.markdown-body li > p { + margin-top: 16px; +} + +.markdown-body li + li { + margin-top: 0.25em; +} + +.markdown-body dl { + padding: 0; +} + +.markdown-body dl dt { + padding: 0; + margin-top: 16px; + font-size: 1em; + font-style: italic; + font-weight: 600; +} + +.markdown-body dl dd { + padding: 0 16px; + margin-bottom: 16px; +} + +.markdown-body table { + display: block; + width: 100%; + overflow: auto; +} + +.markdown-body table th { + font-weight: 600; +} + +.markdown-body table td, +.markdown-body table th { + padding: 6px 13px; + border: 1px solid #dfe2e5; +} + +.markdown-body table tr { + background-color: #fff; + border-top: 1px solid #c6cbd1; +} + +.markdown-body table tr:nth-child(2n) { + background-color: #f6f8fa; +} + +.markdown-body img { + max-width: 100%; + box-sizing: initial; + background-color: #fff; + border: 1px solid #eee; + border: 1px solid var(--art-c-border-2); + cursor: zoom-in; +} + +.markdown-body img[align='right'] { + padding-left: 20px; +} + +.markdown-body img[align='left'] { + padding-right: 20px; +} + +.markdown-body code { + padding: 0.2em 0.4em; + margin: 0; + font-size: 85%; + background-color: rgba(27, 31, 35, 0.05); + border-radius: 3px; +} + +.markdown-body pre { + word-wrap: normal; +} + +.markdown-body pre > code { + padding: 0; + margin: 0; + font-size: 100%; + word-break: normal; + white-space: pre; + background: transparent; + border: 0; +} + +.markdown-body .highlight { + margin-bottom: 16px; +} + +.markdown-body .highlight pre { + margin-bottom: 0; + word-break: normal; +} + +.markdown-body .highlight pre, +.markdown-body pre { + padding: 15px 20px 15px 0; + overflow: auto; + font-size: 92%; + line-height: 1.6; +} + +.markdown-body pre code { + display: inline; + max-width: auto; + padding: 0; + margin: 0; + overflow: visible; + line-height: inherit; + word-wrap: normal; + background-color: initial; + border: 0; +} + +.markdown-body .commit-tease-sha { + display: inline-block; + font-size: 90%; + color: #444d56; +} + +.markdown-body .full-commit .btn-outline:not(:disabled):hover { + color: #005cc5; + border-color: #005cc5; +} + +.markdown-body .blob-wrapper { + overflow-x: auto; + overflow-y: hidden; +} + +.markdown-body .blob-wrapper-embedded { + max-height: 240px; + overflow-y: auto; +} + +.markdown-body .blob-num { + width: 1%; + min-width: 50px; + padding-right: 10px; + padding-left: 10px; + font-size: 12px; + line-height: 20px; + color: rgba(27, 31, 35, 0.3); + text-align: right; + white-space: nowrap; + vertical-align: top; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.markdown-body .blob-num:hover { + color: rgba(27, 31, 35, 0.6); +} + +.markdown-body .blob-num:before { + content: attr(data-line-number); +} + +.markdown-body .blob-code { + position: relative; + padding-right: 10px; + padding-left: 10px; + line-height: 20px; + vertical-align: top; +} + +.markdown-body .blob-code-inner { + overflow: visible; + font-size: 12px; + color: $font-color; + word-wrap: normal; + white-space: pre; +} + +.markdown-body .pl-token.active, +.markdown-body .pl-token:hover { + cursor: pointer; + background: #ffea7f; +} + +.markdown-body .tab-size[data-tab-size='1'] { + -moz-tab-size: 1; + tab-size: 1; +} + +.markdown-body .tab-size[data-tab-size='2'] { + -moz-tab-size: 2; + tab-size: 2; +} + +.markdown-body .tab-size[data-tab-size='3'] { + -moz-tab-size: 3; + tab-size: 3; +} + +.markdown-body .tab-size[data-tab-size='4'] { + -moz-tab-size: 4; + tab-size: 4; +} + +.markdown-body .tab-size[data-tab-size='5'] { + -moz-tab-size: 5; + tab-size: 5; +} + +.markdown-body .tab-size[data-tab-size='6'] { + -moz-tab-size: 6; + tab-size: 6; +} + +.markdown-body .tab-size[data-tab-size='7'] { + -moz-tab-size: 7; + tab-size: 7; +} + +.markdown-body .tab-size[data-tab-size='8'] { + -moz-tab-size: 8; + tab-size: 8; +} + +.markdown-body .tab-size[data-tab-size='9'] { + -moz-tab-size: 9; + tab-size: 9; +} + +.markdown-body .tab-size[data-tab-size='10'] { + -moz-tab-size: 10; + tab-size: 10; +} + +.markdown-body .tab-size[data-tab-size='11'] { + -moz-tab-size: 11; + tab-size: 11; +} + +.markdown-body .tab-size[data-tab-size='12'] { + -moz-tab-size: 12; + tab-size: 12; +} + +.markdown-body .task-list-item { + list-style-type: none; +} + +.markdown-body .task-list-item + .task-list-item { + margin-top: 3px; +} + +.markdown-body .task-list-item input { + margin: 0 0.2em 0.25em -1.6em; + vertical-align: middle; +} diff --git a/src/assets/styles/core/mixin.scss b/src/assets/styles/core/mixin.scss new file mode 100644 index 0000000..db36888 --- /dev/null +++ b/src/assets/styles/core/mixin.scss @@ -0,0 +1,157 @@ +// sass 混合宏(函数) + +/** +* 溢出省略号 +* @param {Number} 行数 +*/ +@mixin ellipsis($rowCount: 1) { + @if $rowCount <=1 { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } @else { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: $rowCount; + -webkit-box-orient: vertical; + } +} + +/** +* 控制用户能否选中文本 +* @param {String} 类型 +*/ +@mixin userSelect($value: none) { + user-select: $value; + -moz-user-select: $value; + -ms-user-select: $value; + -webkit-user-select: $value; +} + +// 绝对定位居中 +@mixin absoluteCenter() { + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + margin: auto; +} + +/** +* css3动画 +* +*/ +@mixin animation( + $from: ( + width: 0px + ), + $to: ( + width: 100px + ), + $name: mymove, + $animate: mymove 2s 1 linear infinite +) { + -webkit-animation: $animate; + -o-animation: $animate; + animation: $animate; + + @keyframes #{$name} { + from { + @each $key, $value in $from { + #{$key}: #{$value}; + } + } + + to { + @each $key, $value in $to { + #{$key}: #{$value}; + } + } + } + + @-webkit-keyframes #{$name} { + from { + @each $key, $value in $from { + $key: $value; + } + } + + to { + @each $key, $value in $to { + $key: $value; + } + } + } +} + +// 圆形盒子 +@mixin circle($size: 11px, $bg: #fff) { + border-radius: 50%; + width: $size; + height: $size; + line-height: $size; + text-align: center; + background: $bg; +} + +// placeholder +@mixin placeholder($color: #bbb) { + // Firefox + &::-moz-placeholder { + color: $color; + opacity: 1; + } + + // Internet Explorer 10+ + &:-ms-input-placeholder { + color: $color; + } + + // Safari and Chrome + &::-webkit-input-placeholder { + color: $color; + } + + &:placeholder-shown { + text-overflow: ellipsis; + } +} + +//背景透明,文字不透明。兼容IE8 +@mixin betterTransparentize($color, $alpha) { + $c: rgba($color, $alpha); + $ie_c: ie_hex_str($c); + background: rgba($color, 1); + background: $c; + background: transparent \9; + zoom: 1; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#{$ie_c}, endColorstr=#{$ie_c}); + -ms-filter: 'progid:DXImageTransform.Microsoft.gradient(startColorstr=#{$ie_c}, endColorstr=#{$ie_c})'; +} + +//添加浏览器前缀 +@mixin browserPrefix($propertyName, $value) { + @each $prefix in -webkit-, -moz-, -ms-, -o-, '' { + #{$prefix}#{$propertyName}: $value; + } +} + +// 边框 +@mixin border($color: red) { + border: 1px solid $color; +} + +// 背景滤镜 +@mixin backdropBlur() { + --tw-backdrop-blur: blur(30px); + -webkit-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) + var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) + var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) + var(--tw-backdrop-sepia); + backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) + var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) + var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia); +} diff --git a/src/assets/styles/core/reset.scss b/src/assets/styles/core/reset.scss new file mode 100644 index 0000000..17a3bcf --- /dev/null +++ b/src/assets/styles/core/reset.scss @@ -0,0 +1,41 @@ +@charset "UTF-8"; + +/*滚动条*/ +/*滚动条整体部分,必须要设置*/ +::-webkit-scrollbar { + width: 8px !important; + height: 0 !important; +} + +/*滚动条的轨道*/ +::-webkit-scrollbar-track { + background-color: var(--art-gray-200); +} + +/*滚动条的滑块按钮*/ +::-webkit-scrollbar-thumb { + border-radius: 5px; + background-color: #cccccc !important; + transition: all 0.2s; + -webkit-transition: all 0.2s; +} + +::-webkit-scrollbar-thumb:hover { + background-color: #b0abab !important; +} + +/*滚动条的上下两端的按钮*/ +::-webkit-scrollbar-button { + height: 0px; + width: 0; +} + +.dark { + ::-webkit-scrollbar-track { + background-color: var(--default-bg-color); + } + + ::-webkit-scrollbar-thumb { + background-color: var(--art-gray-300) !important; + } +} diff --git a/src/assets/styles/core/router-transition.scss b/src/assets/styles/core/router-transition.scss new file mode 100644 index 0000000..f47c741 --- /dev/null +++ b/src/assets/styles/core/router-transition.scss @@ -0,0 +1,104 @@ +@use 'sass:map'; + +// === 变量区域 === +$transition: ( + // 动画持续时间 + duration: 0.25s, + // 滑动动画的移动距离 + distance: 15px, + // 默认缓动函数 + easing: cubic-bezier(0.25, 0.1, 0.25, 1), + // 淡入淡出专用的缓动函数 + fade-easing: cubic-bezier(0.4, 0, 0.6, 1) +); + +// 抽取配置值函数,提高可复用性 +@function transition-config($key) { + @return map.get($transition, $key); +} + +// 变量简写 +$duration: transition-config('duration'); +$distance: transition-config('distance'); +$easing: transition-config('easing'); +$fade-easing: transition-config('fade-easing'); + +// === 动画类 === + +// 淡入淡出动画 +.fade { + &-enter-active, + &-leave-active { + transition: opacity $duration $fade-easing; + will-change: opacity; + } + + &-enter-from, + &-leave-to { + opacity: 0; + } + + &-enter-to, + &-leave-from { + opacity: 1; + } +} + +// 滑动动画通用样式 +@mixin slide-transition($direction) { + $distance-x: 0; + $distance-y: 0; + + @if $direction == 'left' { + $distance-x: -$distance; + } @else if $direction == 'right' { + $distance-x: $distance; + } @else if $direction == 'top' { + $distance-y: -$distance; + } @else if $direction == 'bottom' { + $distance-y: $distance; + } + + &-enter-active { + transition: + opacity $duration $easing, + transform $duration $easing; + will-change: opacity, transform; + } + + &-leave-active { + transition: + opacity calc($duration * 0.7) $easing, + transform calc($duration * 0.7) $easing; + will-change: opacity, transform; + } + + &-enter-from { + opacity: 0; + transform: translate3d($distance-x, $distance-y, 0); + } + + &-enter-to { + opacity: 1; + transform: translate3d(0, 0, 0); + } + + &-leave-to { + opacity: 0; + transform: translate3d(-$distance-x, -$distance-y, 0); + } +} + +// 滑动动画方向类 +.slide-left { + @include slide-transition('left'); +} +.slide-right { + @include slide-transition('right'); +} +.slide-top { + @include slide-transition('top'); +} +.slide-bottom { + @include slide-transition('bottom'); +} diff --git a/src/assets/styles/core/tailwind.css b/src/assets/styles/core/tailwind.css new file mode 100644 index 0000000..1a9e22c --- /dev/null +++ b/src/assets/styles/core/tailwind.css @@ -0,0 +1,208 @@ +@import 'tailwindcss'; +@custom-variant dark (&:where(.dark, .dark *)); + +/* ==================== Light Mode Variables ==================== */ +:root { + /* Base Colors */ + --art-color: #ffffff; + --theme-color: var(--main-color); + + /* Theme Colors - OKLCH Format */ + --art-primary: oklch(0.7 0.23 260); + --art-secondary: oklch(0.72 0.19 231.6); + --art-error: oklch(0.73 0.15 25.3); + --art-info: oklch(0.58 0.03 254.1); + --art-success: oklch(0.78 0.17 166.1); + --art-warning: oklch(0.78 0.14 75.5); + --art-danger: oklch(0.68 0.22 25.3); + + /* Gray Scale - Light Mode */ + --art-gray-100: #f9fafb; + --art-gray-200: #f2f4f5; + --art-gray-300: #e6eaeb; + --art-gray-400: #dbdfe1; + --art-gray-500: #949eb7; + --art-gray-600: #7987a1; + --art-gray-700: #4d5875; + --art-gray-800: #383853; + --art-gray-900: #323251; + + /* Border Colors */ + --art-card-border: rgba(0, 0, 0, 0.08); + + --default-border: #e2e8ee; + --default-border-dashed: #dbdfe9; + + /* Background Colors */ + --default-bg-color: #fafbfc; + --default-box-color: #ffffff; + + /* Hover Color */ + --art-hover-color: #edeff0; + + /* Active Color */ + --art-active-color: #f2f4f5; + + /* Element Component Active Color */ + --art-el-active-color: #f2f4f5; +} + +/* ==================== Dark Mode Variables ==================== */ +.dark { + /* Base Colors */ + --art-color: #000000; + + /* Gray Scale - Dark Mode */ + --art-gray-100: #110f0f; + --art-gray-200: #17171c; + --art-gray-300: #393946; + --art-gray-400: #505062; + --art-gray-500: #73738c; + --art-gray-600: #8f8fa3; + --art-gray-700: #ababba; + --art-gray-800: #c7c7d1; + --art-gray-900: #e3e3e8; + + /* Border Colors */ + --art-card-border: rgba(255, 255, 255, 0.08); + + --default-border: rgba(255, 255, 255, 0.1); + --default-border-dashed: #363843; + + /* Background Colors */ + --default-bg-color: #070707; + --default-box-color: #161618; + + /* Hover Color */ + --art-hover-color: #252530; + + /* Active Color */ + --art-active-color: #202226; + + /* Element Component Active Color */ + --art-el-active-color: #2e2e38; +} + +/* ==================== Tailwind Theme Configuration ==================== */ +@theme { + /* Box Color (Light: white / Dark: black) */ + --color-box: var(--default-box-color); + + /* System Theme Color */ + --color-theme: var(--theme-color); + + /* Hover Color */ + --color-hover-color: var(--art-hover-color); + + /* Active Color */ + --color-active-color: var(--art-active-color); + + /* Active Color */ + --color-el-active-color: var(--art-active-color); + + /* ElementPlus Theme Colors */ + --color-primary: var(--art-primary); + --color-secondary: var(--art-secondary); + --color-error: var(--art-error); + --color-info: var(--art-info); + --color-success: var(--art-success); + --color-warning: var(--art-warning); + --color-danger: var(--art-danger); + + /* Gray Scale Colors (Auto-adapts to dark mode) */ + --color-g-100: var(--art-gray-100); + --color-g-200: var(--art-gray-200); + --color-g-300: var(--art-gray-300); + --color-g-400: var(--art-gray-400); + --color-g-500: var(--art-gray-500); + --color-g-600: var(--art-gray-600); + --color-g-700: var(--art-gray-700); + --color-g-800: var(--art-gray-800); + --color-g-900: var(--art-gray-900); +} + +/* ==================== Custom Border Radius Utilities ==================== */ +@utility rounded-custom-xs { + border-radius: calc(var(--custom-radius) / 2); +} + +@utility rounded-custom-sm { + border-radius: calc(var(--custom-radius) / 2 + 2px); +} + +/* ==================== Custom Utility Classes ==================== */ +@layer utilities { + /* Flexbox Layout Utilities */ + .flex-c { + @apply flex items-center; + } + + .flex-b { + @apply flex justify-between; + } + + .flex-cc { + @apply flex items-center justify-center; + } + + .flex-cb { + @apply flex items-center justify-between; + } + + /* Transition Utilities */ + .tad-200 { + @apply transition-all duration-200; + } + + .tad-300 { + @apply transition-all duration-300; + } + + /* Border Utilities */ + .border-full-d { + @apply border border-[var(--default-border)]; + } + + .border-b-d { + @apply border-b border-[var(--default-border)]; + } + + .border-t-d { + @apply border-t border-[var(--default-border)]; + } + + .border-l-d { + @apply border-l border-[var(--default-border)]; + } + + .border-r-d { + @apply border-r border-[var(--default-border)]; + } + + /* Cursor Utilities */ + .c-p { + @apply cursor-pointer; + } +} + +/* ==================== Custom Component Classes ==================== */ +@layer components { + /* Art Card Header Component */ + .art-card-header { + @apply flex justify-between pr-6 pb-1; + + .title { + h4 { + @apply text-lg font-medium text-g-900; + } + + p { + @apply mt-1 text-sm text-g-600; + + span { + @apply ml-2 font-medium; + } + } + } + } +} diff --git a/src/assets/styles/core/theme-animation.scss b/src/assets/styles/core/theme-animation.scss new file mode 100644 index 0000000..377b945 --- /dev/null +++ b/src/assets/styles/core/theme-animation.scss @@ -0,0 +1,63 @@ +// 定义基础变量 +$bg-animation-color-light: #000; +$bg-animation-color-dark: #fff; +$bg-animation-duration: 0.5s; + +html { + --bg-animation-color: $bg-animation-color-light; + + &.dark { + --bg-animation-color: $bg-animation-color-dark; + } + + // View transition styles + &::view-transition-old(*) { + animation: none; + } + + &::view-transition-new(*) { + animation: clip $bg-animation-duration ease-in both; + } + + &::view-transition-old(root) { + z-index: 1; + } + + &::view-transition-new(root) { + z-index: 9999; + } + + &.dark { + &::view-transition-old(*) { + animation: clip $bg-animation-duration ease-in reverse both; + } + + &::view-transition-new(*) { + animation: none; + } + + &::view-transition-old(root) { + z-index: 9999; + } + + &::view-transition-new(root) { + z-index: 1; + } + } +} + +// 定义动画 +@keyframes clip { + from { + clip-path: circle(0% at var(--x) var(--y)); + } + + to { + clip-path: circle(var(--r) at var(--x) var(--y)); + } +} + +// body 相关样式 +body { + background-color: var(--bg-animation-color); +} diff --git a/src/assets/styles/core/theme-change.scss b/src/assets/styles/core/theme-change.scss new file mode 100644 index 0000000..5b640d2 --- /dev/null +++ b/src/assets/styles/core/theme-change.scss @@ -0,0 +1,11 @@ +// 主题切换过渡优化,优化除视觉上的不适感 +.theme-change { + * { + transition: 0s !important; + } + + .el-switch__core, + .el-switch__action { + transition: all 0.3s !important; + } +} diff --git a/src/assets/styles/custom/one-dark-pro.scss b/src/assets/styles/custom/one-dark-pro.scss new file mode 100644 index 0000000..36bdf63 --- /dev/null +++ b/src/assets/styles/custom/one-dark-pro.scss @@ -0,0 +1,98 @@ +.hljs { + display: block; + overflow-x: auto; + padding: 0.5em; + + color: #a6accd; +} + +.hljs-string, +.hljs-section, +.hljs-selector-class, +.hljs-template-variable, +.hljs-deletion { + color: #aed07e !important; +} + +.hljs-comment, +.hljs-quote { + color: #6f747d; +} + +.hljs-doctag, +.hljs-keyword, +.hljs-formula { + color: #c792ea; +} + +.hljs-section, +.hljs-name, +.hljs-selector-tag, +.hljs-deletion, +.hljs-subst { + color: #c86068; +} + +.hljs-literal { + color: #56b6c2; +} + +.hljs-string, +.hljs-regexp, +.hljs-addition, +.hljs-attribute, +.hljs-meta-string { + color: #abb2bf; +} + +.hljs-attribute { + color: #c792ea; +} + +.hljs-function { + color: #c792ea; +} + +.hljs-type { + color: #f07178; +} + +.hljs-title { + color: #82aaff !important; +} + +.hljs-built_in, +.hljs-class { + color: #82aaff; +} + +// 括号 +.hljs-params { + color: #a6accd; +} + +.hljs-attr, +.hljs-variable, +.hljs-template-variable, +.hljs-selector-class, +.hljs-selector-attr, +.hljs-selector-pseudo, +.hljs-number { + color: #de7e61; +} + +.hljs-symbol, +.hljs-bullet, +.hljs-link, +.hljs-meta, +.hljs-selector-id { + color: #61aeee; +} + +.hljs-strong { + font-weight: bold; +} + +.hljs-link { + text-decoration: underline; +} diff --git a/src/assets/styles/index.scss b/src/assets/styles/index.scss new file mode 100644 index 0000000..0267276 --- /dev/null +++ b/src/assets/styles/index.scss @@ -0,0 +1,26 @@ +// 重置默认样式 +@use './core/reset.scss'; + +// 应用全局样式 +@use './core/app.scss'; + +// Element Plus 样式优化 +@use './core/el-ui.scss'; + +// Element Plus 暗黑主题 +@use './core/el-dark.scss'; + +// 暗黑主题样式优化 +@use './core/dark.scss'; + +// 路由切换动画 +@use './core/router-transition'; + +// 主题切换过渡优化 +@use './core/theme-change.scss'; + +// 主题切换圆形扩散动画 +@use './core/theme-animation.scss'; + +// 统一操作按钮组件 +@use './components/action-btn.scss'; diff --git a/src/assets/svg/loading.ts b/src/assets/svg/loading.ts new file mode 100644 index 0000000..fdfb078 --- /dev/null +++ b/src/assets/svg/loading.ts @@ -0,0 +1,32 @@ +// 自定义四点旋转SVG +export const fourDotsSpinnerSvg = ` + + + + + + + + + +` diff --git a/src/components/announcement/AnnouncementPreview.vue b/src/components/announcement/AnnouncementPreview.vue new file mode 100644 index 0000000..4626f89 --- /dev/null +++ b/src/components/announcement/AnnouncementPreview.vue @@ -0,0 +1,413 @@ + + + + + diff --git a/src/components/announcement/AudienceSelector.vue b/src/components/announcement/AudienceSelector.vue new file mode 100644 index 0000000..8bcdd4c --- /dev/null +++ b/src/components/announcement/AudienceSelector.vue @@ -0,0 +1,766 @@ + + + diff --git a/src/components/billing/BillingAmountDisplay.vue b/src/components/billing/BillingAmountDisplay.vue new file mode 100644 index 0000000..4c417cf --- /dev/null +++ b/src/components/billing/BillingAmountDisplay.vue @@ -0,0 +1,51 @@ + + + diff --git a/src/components/billing/BillingStatusTag.vue b/src/components/billing/BillingStatusTag.vue new file mode 100644 index 0000000..5cd85af --- /dev/null +++ b/src/components/billing/BillingStatusTag.vue @@ -0,0 +1,30 @@ + + + diff --git a/src/components/billing/BillingTimeline.vue b/src/components/billing/BillingTimeline.vue new file mode 100644 index 0000000..f344f20 --- /dev/null +++ b/src/components/billing/BillingTimeline.vue @@ -0,0 +1,132 @@ + + + diff --git a/src/components/billing/PaymentMethodIcon.vue b/src/components/billing/PaymentMethodIcon.vue new file mode 100644 index 0000000..60de7b8 --- /dev/null +++ b/src/components/billing/PaymentMethodIcon.vue @@ -0,0 +1,42 @@ + + + diff --git a/src/components/business/contact_modal/ContactModal.vue b/src/components/business/contact_modal/ContactModal.vue new file mode 100644 index 0000000..03b2370 --- /dev/null +++ b/src/components/business/contact_modal/ContactModal.vue @@ -0,0 +1,186 @@ + + + diff --git a/src/components/common/CategorySelect.vue b/src/components/common/CategorySelect.vue new file mode 100644 index 0000000..5e2cc25 --- /dev/null +++ b/src/components/common/CategorySelect.vue @@ -0,0 +1,82 @@ + + + diff --git a/src/components/common/ImageUpload.vue b/src/components/common/ImageUpload.vue new file mode 100644 index 0000000..6034e00 --- /dev/null +++ b/src/components/common/ImageUpload.vue @@ -0,0 +1,147 @@ + + + + + diff --git a/src/components/common/MerchantSelect.vue b/src/components/common/MerchantSelect.vue new file mode 100644 index 0000000..fd2424d --- /dev/null +++ b/src/components/common/MerchantSelect.vue @@ -0,0 +1,118 @@ + + + diff --git a/src/components/common/RichTextEditor.example.ts b/src/components/common/RichTextEditor.example.ts new file mode 100644 index 0000000..ac03c19 --- /dev/null +++ b/src/components/common/RichTextEditor.example.ts @@ -0,0 +1,134 @@ +/** + * RichTextEditor 组件使用示例和验收标准验证 + * + * 任务:FE-ANNOUNCE-07 创建 RichTextEditor 组件 + * + * ✅ 验收标准检查清单: + * + * 1. ✅ 组件可通过 v-model 绑定 HTML 内容 + * - 实现:使用 defineModel 和 v-model 实现双向绑定 + * - 代码:line 59, 85-92, 100-105 + * + * 2. ✅ 工具栏功能正常(加粗、斜体等) + * - 实现:配置简化工具栏包含 bold、italic、underline、headerSelect、list、link + * - 代码:line 63-76 toolbarConfig + * + * 3. ✅ disabled 状态下编辑器只读 + * - 实现:通过 readOnly 配置和动态监听 disabled 变化调用 enable/disable + * - 代码:line 80 (readOnly), line 94-103 (watch disabled) + * + * 4. ✅ 组件销毁时正确清理编辑器实例 + * - 实现:onBeforeUnmount 钩子中调用 editor.destroy() + * - 代码:line 116-121 + * + * 5. ✅ TypeScript 类型完整无 any + * - 实现:所有 props、emits、变量都有明确类型定义 + * - 代码:Props interface (line 17-30), emits (line 34-37), + * IDomEditor (line 40), computed types (line 43-48, 63-76, 79-83) + * + * 📋 实现的额外功能: + * - 支持自定义高度(height prop) + * - 支持自定义占位符(placeholder prop) + * - 支持编辑器模式切换(mode prop: default/simple) + * - 暴露编辑器实例方法(getEditor, setHtml, getHtml, clear, focus, disable, enable) + * - 正确处理外部 modelValue 变化(避免循环更新) + * + * 🔧 技术栈: + * - @wangeditor/editor 5.1.23 + * - @wangeditor/editor-for-vue (next) + * - Vue 3.5 Composition API (script setup) + * - TypeScript 5.6 + * + * 📝 使用示例: + */ + +// 示例 1: 基础使用 +/* + + + +*/ + +// 示例 2: 禁用状态 +/* + + + +*/ + +// 示例 3: 自定义高度和监听变化 +/* + + + +*/ + +// 示例 4: 使用 ref 访问编辑器实例方法 +/* + + + +*/ + +export default {} diff --git a/src/components/common/RichTextEditor.vue b/src/components/common/RichTextEditor.vue new file mode 100644 index 0000000..110fe09 --- /dev/null +++ b/src/components/common/RichTextEditor.vue @@ -0,0 +1,182 @@ + + + + + + diff --git a/src/components/core/banners/art-basic-banner/index.vue b/src/components/core/banners/art-basic-banner/index.vue new file mode 100644 index 0000000..65b47e4 --- /dev/null +++ b/src/components/core/banners/art-basic-banner/index.vue @@ -0,0 +1,343 @@ + + + + + + diff --git a/src/components/core/banners/art-card-banner/index.vue b/src/components/core/banners/art-card-banner/index.vue new file mode 100644 index 0000000..8a5f9d4 --- /dev/null +++ b/src/components/core/banners/art-card-banner/index.vue @@ -0,0 +1,114 @@ + + + + diff --git a/src/components/core/base/art-back-to-top/index.vue b/src/components/core/base/art-back-to-top/index.vue new file mode 100644 index 0000000..6f8da61 --- /dev/null +++ b/src/components/core/base/art-back-to-top/index.vue @@ -0,0 +1,40 @@ + + + + diff --git a/src/components/core/base/art-logo/index.vue b/src/components/core/base/art-logo/index.vue new file mode 100644 index 0000000..8bc8309 --- /dev/null +++ b/src/components/core/base/art-logo/index.vue @@ -0,0 +1,21 @@ + + + + diff --git a/src/components/core/base/art-svg-icon/index.vue b/src/components/core/base/art-svg-icon/index.vue new file mode 100644 index 0000000..0bfcd0c --- /dev/null +++ b/src/components/core/base/art-svg-icon/index.vue @@ -0,0 +1,24 @@ + + + + diff --git a/src/components/core/cards/art-bar-chart-card/index.vue b/src/components/core/cards/art-bar-chart-card/index.vue new file mode 100644 index 0000000..6815c2b --- /dev/null +++ b/src/components/core/cards/art-bar-chart-card/index.vue @@ -0,0 +1,103 @@ + + + + diff --git a/src/components/core/cards/art-data-list-card/index.vue b/src/components/core/cards/art-data-list-card/index.vue new file mode 100644 index 0000000..fc43323 --- /dev/null +++ b/src/components/core/cards/art-data-list-card/index.vue @@ -0,0 +1,74 @@ + + + + diff --git a/src/components/core/cards/art-donut-chart-card/index.vue b/src/components/core/cards/art-donut-chart-card/index.vue new file mode 100644 index 0000000..df2dcbb --- /dev/null +++ b/src/components/core/cards/art-donut-chart-card/index.vue @@ -0,0 +1,124 @@ + + + + diff --git a/src/components/core/cards/art-image-card/index.vue b/src/components/core/cards/art-image-card/index.vue new file mode 100644 index 0000000..d27fe00 --- /dev/null +++ b/src/components/core/cards/art-image-card/index.vue @@ -0,0 +1,89 @@ + + + + diff --git a/src/components/core/cards/art-line-chart-card/index.vue b/src/components/core/cards/art-line-chart-card/index.vue new file mode 100644 index 0000000..e58c9b2 --- /dev/null +++ b/src/components/core/cards/art-line-chart-card/index.vue @@ -0,0 +1,126 @@ + + + + diff --git a/src/components/core/cards/art-progress-card/index.vue b/src/components/core/cards/art-progress-card/index.vue new file mode 100644 index 0000000..048a836 --- /dev/null +++ b/src/components/core/cards/art-progress-card/index.vue @@ -0,0 +1,86 @@ + + + + diff --git a/src/components/core/cards/art-stats-card/index.vue b/src/components/core/cards/art-stats-card/index.vue new file mode 100644 index 0000000..8e0341b --- /dev/null +++ b/src/components/core/cards/art-stats-card/index.vue @@ -0,0 +1,67 @@ + + + + diff --git a/src/components/core/cards/art-timeline-list-card/index.vue b/src/components/core/cards/art-timeline-list-card/index.vue new file mode 100644 index 0000000..fbb2c78 --- /dev/null +++ b/src/components/core/cards/art-timeline-list-card/index.vue @@ -0,0 +1,69 @@ + + + + diff --git a/src/components/core/charts/art-bar-chart/index.vue b/src/components/core/charts/art-bar-chart/index.vue new file mode 100644 index 0000000..d677196 --- /dev/null +++ b/src/components/core/charts/art-bar-chart/index.vue @@ -0,0 +1,203 @@ + + + + diff --git a/src/components/core/charts/art-dual-bar-compare-chart/index.vue b/src/components/core/charts/art-dual-bar-compare-chart/index.vue new file mode 100644 index 0000000..32aa60f --- /dev/null +++ b/src/components/core/charts/art-dual-bar-compare-chart/index.vue @@ -0,0 +1,195 @@ + + + + diff --git a/src/components/core/charts/art-h-bar-chart/index.vue b/src/components/core/charts/art-h-bar-chart/index.vue new file mode 100644 index 0000000..2e34759 --- /dev/null +++ b/src/components/core/charts/art-h-bar-chart/index.vue @@ -0,0 +1,208 @@ + + + + diff --git a/src/components/core/charts/art-k-line-chart/index.vue b/src/components/core/charts/art-k-line-chart/index.vue new file mode 100644 index 0000000..0061b51 --- /dev/null +++ b/src/components/core/charts/art-k-line-chart/index.vue @@ -0,0 +1,152 @@ + + + + diff --git a/src/components/core/charts/art-line-chart/index.vue b/src/components/core/charts/art-line-chart/index.vue new file mode 100644 index 0000000..b70c2c3 --- /dev/null +++ b/src/components/core/charts/art-line-chart/index.vue @@ -0,0 +1,371 @@ + + + + diff --git a/src/components/core/charts/art-radar-chart/index.vue b/src/components/core/charts/art-radar-chart/index.vue new file mode 100644 index 0000000..e99fff6 --- /dev/null +++ b/src/components/core/charts/art-radar-chart/index.vue @@ -0,0 +1,105 @@ + + + + diff --git a/src/components/core/charts/art-ring-chart/index.vue b/src/components/core/charts/art-ring-chart/index.vue new file mode 100644 index 0000000..79115f7 --- /dev/null +++ b/src/components/core/charts/art-ring-chart/index.vue @@ -0,0 +1,133 @@ + + + + diff --git a/src/components/core/charts/art-scatter-chart/index.vue b/src/components/core/charts/art-scatter-chart/index.vue new file mode 100644 index 0000000..995b56a --- /dev/null +++ b/src/components/core/charts/art-scatter-chart/index.vue @@ -0,0 +1,115 @@ + + + + diff --git a/src/components/core/forms/art-button-more/index.vue b/src/components/core/forms/art-button-more/index.vue new file mode 100644 index 0000000..858d305 --- /dev/null +++ b/src/components/core/forms/art-button-more/index.vue @@ -0,0 +1,71 @@ + + + + diff --git a/src/components/core/forms/art-button-table/index.vue b/src/components/core/forms/art-button-table/index.vue new file mode 100644 index 0000000..c849901 --- /dev/null +++ b/src/components/core/forms/art-button-table/index.vue @@ -0,0 +1,59 @@ + + + + diff --git a/src/components/core/forms/art-drag-verify/index.vue b/src/components/core/forms/art-drag-verify/index.vue new file mode 100644 index 0000000..5306e04 --- /dev/null +++ b/src/components/core/forms/art-drag-verify/index.vue @@ -0,0 +1,430 @@ + + + + + + + + diff --git a/src/components/core/forms/art-excel-export/index.vue b/src/components/core/forms/art-excel-export/index.vue new file mode 100644 index 0000000..08207c2 --- /dev/null +++ b/src/components/core/forms/art-excel-export/index.vue @@ -0,0 +1,389 @@ + + + + + + diff --git a/src/components/core/forms/art-excel-import/index.vue b/src/components/core/forms/art-excel-import/index.vue new file mode 100644 index 0000000..8aa82fe --- /dev/null +++ b/src/components/core/forms/art-excel-import/index.vue @@ -0,0 +1,62 @@ + + + + diff --git a/src/components/core/forms/art-form/index.vue b/src/components/core/forms/art-form/index.vue new file mode 100644 index 0000000..1e76f14 --- /dev/null +++ b/src/components/core/forms/art-form/index.vue @@ -0,0 +1,311 @@ + + + + + + diff --git a/src/components/core/forms/art-search-bar/index.vue b/src/components/core/forms/art-search-bar/index.vue new file mode 100644 index 0000000..b25b5bb --- /dev/null +++ b/src/components/core/forms/art-search-bar/index.vue @@ -0,0 +1,437 @@ + + + + + + + + diff --git a/src/components/core/forms/art-wang-editor/index.vue b/src/components/core/forms/art-wang-editor/index.vue new file mode 100644 index 0000000..38741fe --- /dev/null +++ b/src/components/core/forms/art-wang-editor/index.vue @@ -0,0 +1,223 @@ + + + + + + diff --git a/src/components/core/forms/art-wang-editor/style.scss b/src/components/core/forms/art-wang-editor/style.scss new file mode 100644 index 0000000..fd5dbca --- /dev/null +++ b/src/components/core/forms/art-wang-editor/style.scss @@ -0,0 +1,210 @@ +$box-radius: calc(var(--custom-radius) / 3 + 2px); + +// 全屏容器 z-index 调整 +.w-e-full-screen-container { + z-index: 100 !important; +} + +/* 编辑器容器 */ +.editor-wrapper { + width: 100%; + height: 100%; + border: 1px solid var(--art-gray-300); + border-radius: $box-radius !important; + + .w-e-bar { + border-radius: $box-radius $box-radius 0 0 !important; + } + + .menu-item { + display: flex; + flex-direction: row; + align-items: center; + + i { + margin-right: 5px; + } + } + + /* 工具栏 */ + .editor-toolbar { + border-bottom: 1px solid var(--default-border); + } + + /* 下拉选择框配置 */ + .w-e-select-list { + min-width: 140px; + padding: 5px 10px 10px; + border: none; + border-radius: $box-radius; + } + + /* 下拉选择框元素配置 */ + .w-e-select-list ul li { + margin-top: 5px; + font-size: 15px !important; + border-radius: $box-radius; + } + + /* 下拉选择框 正文文字大小调整 */ + .w-e-select-list ul li:last-of-type { + font-size: 16px !important; + } + + /* 下拉选择框 hover 样式调整 */ + .w-e-select-list ul li:hover { + background-color: var(--art-gray-200); + } + + :root { + /* 激活颜色 */ + --w-e-toolbar-active-bg-color: var(--art-gray-200); + + /* toolbar 图标和文字颜色 */ + --w-e-toolbar-color: #000; + + /* 表格选中时候的边框颜色 */ + --w-e-textarea-selected-border-color: #ddd; + + /* 表格头背景颜色 */ + --w-e-textarea-slight-bg-color: var(--art-gray-200); + } + + /* 工具栏按钮样式 */ + .w-e-bar-item svg { + fill: var(--art-gray-800); + } + + .w-e-bar-item button { + color: var(--art-gray-800); + border-radius: $box-radius; + } + + /* 工具栏 hover 按钮背景颜色 */ + .w-e-bar-item button:hover { + background-color: var(--art-gray-200); + } + + /* 工具栏分割线 */ + .w-e-bar-divider { + height: 20px; + margin-top: 10px; + background-color: #ccc; + } + + /* 工具栏菜单 */ + .w-e-bar-item-group .w-e-bar-item-menus-container { + min-width: 120px; + padding: 10px 0; + border: none; + border-radius: $box-radius; + + .w-e-bar-item { + button { + width: 100%; + margin: 0 5px; + } + } + } + + /* 代码块 */ + .w-e-text-container [data-slate-editor] pre > code { + padding: 0.6rem 1rem; + background-color: var(--art-gray-50); + border-radius: $box-radius; + } + + /* 弹出框 */ + .w-e-drop-panel { + border: 0; + border-radius: $box-radius; + } + + a { + color: #318ef4; + } + + .w-e-text-container { + strong, + b { + font-weight: 500; + } + + i, + em { + font-style: italic; + } + } + + /* 表格样式优化 */ + .w-e-text-container [data-slate-editor] .table-container th { + border-right: none; + } + + .w-e-text-container [data-slate-editor] .table-container th:last-of-type { + border-right: 1px solid #ccc !important; + } + + /* 引用 */ + .w-e-text-container [data-slate-editor] blockquote { + background-color: var(--art-gray-200); + border-left: 4px solid var(--art-gray-300); + } + + /* 输入区域弹出 bar */ + .w-e-hover-bar { + border-radius: $box-radius; + } + + /* 超链接弹窗 */ + .w-e-modal { + border: none; + border-radius: $box-radius; + } + + /* 图片样式调整 */ + .w-e-text-container [data-slate-editor] .w-e-selected-image-container { + overflow: inherit; + + &:hover { + border: 0; + } + + img { + border: 1px solid transparent; + transition: border 0.3s; + + &:hover { + border: 1px solid #318ef4 !important; + } + } + + .w-e-image-dragger { + width: 12px; + height: 12px; + background-color: #318ef4; + border: 2px solid #fff; + border-radius: $box-radius; + } + + .left-top { + top: -6px; + left: -6px; + } + + .right-top { + top: -6px; + right: -6px; + } + + .left-bottom { + bottom: -6px; + left: -6px; + } + + .right-bottom { + right: -6px; + bottom: -6px; + } + } +} diff --git a/src/components/core/layouts/art-breadcrumb/index.vue b/src/components/core/layouts/art-breadcrumb/index.vue new file mode 100644 index 0000000..4b54859 --- /dev/null +++ b/src/components/core/layouts/art-breadcrumb/index.vue @@ -0,0 +1,142 @@ + + + + diff --git a/src/components/core/layouts/art-chat-window/index.vue b/src/components/core/layouts/art-chat-window/index.vue new file mode 100644 index 0000000..f3d9471 --- /dev/null +++ b/src/components/core/layouts/art-chat-window/index.vue @@ -0,0 +1,262 @@ + + + + diff --git a/src/components/core/layouts/art-fast-enter/index.vue b/src/components/core/layouts/art-fast-enter/index.vue new file mode 100644 index 0000000..fdde222 --- /dev/null +++ b/src/components/core/layouts/art-fast-enter/index.vue @@ -0,0 +1,113 @@ + + + + diff --git a/src/components/core/layouts/art-fireworks-effect/index.vue b/src/components/core/layouts/art-fireworks-effect/index.vue new file mode 100644 index 0000000..be85274 --- /dev/null +++ b/src/components/core/layouts/art-fireworks-effect/index.vue @@ -0,0 +1,633 @@ + + + + diff --git a/src/components/core/layouts/art-global-component/index.vue b/src/components/core/layouts/art-global-component/index.vue new file mode 100644 index 0000000..6908f94 --- /dev/null +++ b/src/components/core/layouts/art-global-component/index.vue @@ -0,0 +1,14 @@ + + + + diff --git a/src/components/core/layouts/art-global-search/index.vue b/src/components/core/layouts/art-global-search/index.vue new file mode 100644 index 0000000..a7d88df --- /dev/null +++ b/src/components/core/layouts/art-global-search/index.vue @@ -0,0 +1,426 @@ + + + + + + + diff --git a/src/components/core/layouts/art-header-bar/index.vue b/src/components/core/layouts/art-header-bar/index.vue new file mode 100644 index 0000000..2db0db8 --- /dev/null +++ b/src/components/core/layouts/art-header-bar/index.vue @@ -0,0 +1,521 @@ + + + + + + diff --git a/src/components/core/layouts/art-header-bar/widget/ArtUserMenu.vue b/src/components/core/layouts/art-header-bar/widget/ArtUserMenu.vue new file mode 100644 index 0000000..ab0ea0c --- /dev/null +++ b/src/components/core/layouts/art-header-bar/widget/ArtUserMenu.vue @@ -0,0 +1,172 @@ + + + + + + diff --git a/src/components/core/layouts/art-menus/art-horizontal-menu/index.vue b/src/components/core/layouts/art-menus/art-horizontal-menu/index.vue new file mode 100644 index 0000000..edd1473 --- /dev/null +++ b/src/components/core/layouts/art-menus/art-horizontal-menu/index.vue @@ -0,0 +1,110 @@ + + + + + + diff --git a/src/components/core/layouts/art-menus/art-horizontal-menu/widget/HorizontalSubmenu.vue b/src/components/core/layouts/art-menus/art-horizontal-menu/widget/HorizontalSubmenu.vue new file mode 100644 index 0000000..ff32c1e --- /dev/null +++ b/src/components/core/layouts/art-menus/art-horizontal-menu/widget/HorizontalSubmenu.vue @@ -0,0 +1,95 @@ + + + + + diff --git a/src/components/core/layouts/art-menus/art-mixed-menu/index.vue b/src/components/core/layouts/art-menus/art-mixed-menu/index.vue new file mode 100644 index 0000000..4e98246 --- /dev/null +++ b/src/components/core/layouts/art-menus/art-mixed-menu/index.vue @@ -0,0 +1,279 @@ + + + + + + + + diff --git a/src/components/core/layouts/art-menus/art-sidebar-menu/index.vue b/src/components/core/layouts/art-menus/art-sidebar-menu/index.vue new file mode 100644 index 0000000..39387dc --- /dev/null +++ b/src/components/core/layouts/art-menus/art-sidebar-menu/index.vue @@ -0,0 +1,355 @@ + + + + + + + + diff --git a/src/components/core/layouts/art-menus/art-sidebar-menu/style.scss b/src/components/core/layouts/art-menus/art-sidebar-menu/style.scss new file mode 100644 index 0000000..b98011c --- /dev/null +++ b/src/components/core/layouts/art-menus/art-sidebar-menu/style.scss @@ -0,0 +1,253 @@ +.layout-sidebar { + display: flex; + height: 100vh; + user-select: none; + scrollbar-width: none; + border-right: 1px solid var(--art-card-border); + + &.no-border { + border-right: none !important; + } + + // 自定义滚动条宽度 + :deep(.el-scrollbar__bar.is-vertical) { + width: 4px; + } + + :deep(.el-scrollbar__thumb) { + right: -2px; + background-color: #ccc; + border-radius: 2px; + } + + .dual-menu-left { + position: relative; + width: 80px; + height: 100%; + border-right: 1px solid var(--art-card-border) !important; + transition: width 0.25s; + + .logo { + margin: auto; + margin-top: 12px; + margin-bottom: 3px; + cursor: pointer; + } + + ul { + li { + > div { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + margin: 8px; + overflow: hidden; + text-align: center; + cursor: pointer; + border-radius: 5px; + + .art-svg-icon { + display: block; + margin: 0 auto; + font-size: 20px; + } + + span { + display: -webkit-box; + width: 100%; + overflow: hidden; + font-size: 12px; + text-overflow: ellipsis; + -webkit-line-clamp: 1; + line-clamp: 1; + -webkit-box-orient: vertical; + } + + &.is-active { + background: var(--el-color-primary-light-9); + + .art-svg-icon, + span { + color: var(--theme-color) !important; + } + } + } + } + } + + .switch-btn { + position: absolute; + right: 0; + bottom: 15px; + left: 0; + margin: auto; + } + } + + .menu-left { + position: relative; + box-sizing: border-box; + height: 100vh; + + @media only screen and (width <= 640px) { + height: 100dvh; + } + + .el-menu { + height: 100%; + } + + &:hover { + .dual-menu-collapse-btn { + opacity: 1 !important; + } + } + + .dual-menu-collapse-btn { + position: absolute; + top: 50%; + right: -11px; + z-index: 10; + width: 11px; + height: 50px; + cursor: pointer; + background-color: var(--default-box-color); + border: 1px solid var(--art-card-border); + border-radius: 0 15px 15px 0; + opacity: 0; + transition: opacity 0.2s; + transform: translateY(-50%); + + &:hover { + .art-svg-icon { + color: var(--art-gray-800) !important; + } + } + + .art-svg-icon { + position: absolute; + top: 0; + bottom: 0; + left: -4px; + margin: auto; + transition: all 0.3s; + } + } + } + + .header { + position: relative; + box-sizing: border-box; + display: flex; + align-items: center; + width: 100%; + height: 60px; + overflow: hidden; + line-height: 60px; + cursor: pointer; + + .logo { + margin-left: 22px; + } + + p { + position: absolute; + top: 0; + bottom: 0; + left: 58px; + box-sizing: border-box; + margin-left: 10px; + font-size: 18px; + + &.is-dual-menu-name { + left: 25px; + margin: auto; + } + } + } + + .el-menu { + box-sizing: border-box; + height: calc(100vh - 60px); + overflow-y: auto; + // 防止菜单内的滚动影响整个页面滚动 + overscroll-behavior: contain; + border-right: 0; + scrollbar-width: none; + -ms-scroll-chaining: contain; + + &::-webkit-scrollbar { + width: 0 !important; + } + } + + .menu-model { + display: none; + } +} + +@media only screen and (width <= 800px) { + .layout-sidebar { + width: 0; + + .header { + height: 50px; + line-height: 50px; + } + + .el-menu { + height: calc(100vh - 60px); + } + + .el-menu--collapse { + width: 0; + } + + // 折叠状态下的header样式 + .menu-left-close .header { + .logo { + display: none; + } + + p { + left: 16px; + font-size: 0; + opacity: 0 !important; + } + } + + .menu-model { + position: fixed; + top: 0; + left: 0; + z-index: -1; + display: block; + width: 100%; + height: 100vh; + background: rgba($color: #000, $alpha: 50%); + transition: opacity 0.2s ease-in-out; + } + } +} + +@media only screen and (width <= 640px) { + .layout-sidebar { + border-right: 0 !important; + } +} + +.dark { + .layout-sidebar { + border-right: 1px solid rgb(255 255 255 / 13%); + + :deep(.el-scrollbar__thumb) { + background-color: #777; + } + + .dual-menu-left { + border-right: 1px solid rgb(255 255 255 / 9%) !important; + } + } +} diff --git a/src/components/core/layouts/art-menus/art-sidebar-menu/theme.scss b/src/components/core/layouts/art-menus/art-sidebar-menu/theme.scss new file mode 100644 index 0000000..7626c42 --- /dev/null +++ b/src/components/core/layouts/art-menus/art-sidebar-menu/theme.scss @@ -0,0 +1,258 @@ +@use '@styles/core/mixin.scss' as *; + +// 菜单样式变量 +$menu-height: 42px; +$menu-icon-size: 20px; +$menu-font-size: 14px; +$hover-bg-color: var(--art-gray-200); +$popup-menu-height: 40px; +$popup-menu-padding: 8px; +$popup-menu-margin: 5px; +$popup-menu-radius: 6px; + +// 通用菜单项样式 +@mixin menu-item-base { + width: calc(100% - 16px); + margin-left: 8px; + border-radius: 6px; + + .menu-icon { + margin-left: -7px; + } +} + +// 通用 hover 样式 +@mixin menu-hover($bg-color) { + .el-sub-menu__title:hover, + .el-menu-item:not(.is-active):hover { + background: $bg-color !important; + } +} + +// 通用选中样式 +@mixin menu-active($color, $bg-color, $icon-color: var(--theme-color)) { + .el-menu-item.is-active { + color: $color !important; + background-color: $bg-color; + + .menu-icon { + .art-svg-icon { + color: $icon-color !important; + } + } + } +} + +// 弹窗菜单项样式 +@mixin popup-menu-item { + height: $popup-menu-height; + margin-bottom: $popup-menu-margin; + border-radius: $popup-menu-radius; + + .menu-icon { + margin-right: 5px; + } + + &:last-of-type { + margin-bottom: 0; + } +} + +// 主题菜单通用样式(合并 design 和 dark 主题的共同逻辑) +@mixin theme-menu-base { + .el-sub-menu__title, + .el-menu-item { + @include menu-item-base; + } +} + +// 弹窗菜单通用样式 +@mixin popup-menu-base($hover-bg, $active-color, $active-bg) { + .el-menu--popup { + padding: $popup-menu-padding; + + .el-sub-menu__title:hover, + .el-menu-item:hover { + background-color: $hover-bg !important; + border-radius: $popup-menu-radius; + } + + .el-menu-item { + @include popup-menu-item; + + &.is-active { + color: $active-color !important; + background-color: $active-bg !important; + } + } + + .el-sub-menu { + @include popup-menu-item; + + height: $popup-menu-height !important; + + .el-sub-menu__title { + height: $popup-menu-height !important; + border-radius: $popup-menu-radius; + } + } + } +} + +.layout-sidebar { + // ---------------------- Modify default style ---------------------- + + // 菜单折叠样式 + .menu-left-close { + .header { + .logo { + margin: 0 auto; + } + } + } + + // 菜单图标 + .menu-icon { + margin-right: 8px; + font-size: $menu-icon-size; + } + + // 菜单高度 + .el-sub-menu__title, + .el-menu-item { + height: $menu-height !important; + margin-bottom: 4px; + line-height: $menu-height !important; + + span { + font-size: $menu-font-size !important; + + @include ellipsis(); + } + } + + // 右侧箭头 + .el-sub-menu__icon-arrow { + width: 13px !important; + font-size: 13px !important; + } + + // 菜单折叠 + .el-menu--collapse { + .el-sub-menu.is-active { + .el-sub-menu__title { + .menu-icon { + .art-svg-icon { + // 选中菜单图标颜色 + color: var(--theme-color) !important; + } + } + } + } + } + + // ---------------------- Design theme menu ---------------------- + .el-menu-design { + @include theme-menu-base; + @include menu-active(var(--theme-color), var(--el-color-primary-light-9)); + @include menu-hover($hover-bg-color); + + .el-sub-menu__icon-arrow { + color: var(--art-gray-600); + } + } + + // ---------------------- Dark theme menu ---------------------- + .el-menu-dark { + @include theme-menu-base; + @include menu-active(#fff, #27282d, #fff); + @include menu-hover(#0f1015); + + .el-sub-menu__icon-arrow { + color: var(--art-gray-400); + } + } + + // ---------------------- Light theme menu ---------------------- + .el-menu-light { + .el-sub-menu__title, + .el-menu-item { + .menu-icon { + margin-left: 1px; + } + } + + .el-menu-item.is-active { + background-color: var(--el-color-primary-light-9); + + .art-svg-icon { + color: var(--theme-color) !important; + } + + &::before { + position: absolute; + top: 0; + left: 0; + width: 4px; + height: 100%; + content: ''; + background: var(--theme-color); + } + } + + @include menu-hover($hover-bg-color); + + .el-sub-menu__icon-arrow { + color: var(--art-gray-600); + } + } +} + +@media only screen and (width <= 640px) { + .layout-sidebar { + .el-menu-design { + > .el-sub-menu { + margin-left: 0; + } + + .el-sub-menu { + width: 100% !important; + } + } + } +} + +// 菜单折叠 hover 弹窗样式(浅色主题) +.el-menu--vertical, +.el-menu--popup-container { + @include popup-menu-base(var(--art-gray-200), var(--art-gray-900), var(--art-gray-200)); +} + +// 暗黑模式菜单样式 +.dark { + .el-menu--vertical, + .el-menu--popup-container { + @include popup-menu-base(var(--art-gray-200), var(--art-gray-900), #292a2e); + } + + .layout-sidebar { + // 图标颜色、文字颜色 + .menu-icon .art-svg-icon, + .menu-name { + color: var(--art-gray-800) !important; + } + + // 选中的文字颜色跟图标颜色 + .el-menu-item.is-active { + span, + .menu-icon .art-svg-icon { + color: var(--theme-color) !important; + } + } + + // 右侧箭头颜色 + .el-sub-menu__icon-arrow { + color: #fff; + } + } +} diff --git a/src/components/core/layouts/art-menus/art-sidebar-menu/widget/SidebarSubmenu.vue b/src/components/core/layouts/art-menus/art-sidebar-menu/widget/SidebarSubmenu.vue new file mode 100644 index 0000000..a7ac6a9 --- /dev/null +++ b/src/components/core/layouts/art-menus/art-sidebar-menu/widget/SidebarSubmenu.vue @@ -0,0 +1,188 @@ + + + diff --git a/src/components/core/layouts/art-notification/index.vue b/src/components/core/layouts/art-notification/index.vue new file mode 100644 index 0000000..a58853c --- /dev/null +++ b/src/components/core/layouts/art-notification/index.vue @@ -0,0 +1,456 @@ + + + + + + diff --git a/src/components/core/layouts/art-page-content/index.vue b/src/components/core/layouts/art-page-content/index.vue new file mode 100644 index 0000000..a862df1 --- /dev/null +++ b/src/components/core/layouts/art-page-content/index.vue @@ -0,0 +1,136 @@ + + + diff --git a/src/components/core/layouts/art-screen-lock/index.vue b/src/components/core/layouts/art-screen-lock/index.vue new file mode 100644 index 0000000..a3bf58b --- /dev/null +++ b/src/components/core/layouts/art-screen-lock/index.vue @@ -0,0 +1,522 @@ + + + + + + diff --git a/src/components/core/layouts/art-settings-panel/composables/useSettingsConfig.ts b/src/components/core/layouts/art-settings-panel/composables/useSettingsConfig.ts new file mode 100644 index 0000000..35e8066 --- /dev/null +++ b/src/components/core/layouts/art-settings-panel/composables/useSettingsConfig.ts @@ -0,0 +1,248 @@ +import { computed } from 'vue' +import { useI18n } from 'vue-i18n' +import { ContainerWidthEnum } from '@/enums/appEnum' +import AppConfig from '@/config' +import { headerBarConfig } from '@/config/modules/headerBar' + +/** + * 设置项配置选项管理 + */ +export function useSettingsConfig() { + const { t } = useI18n() + + // 标签页风格选项 + const tabStyleOptions = computed(() => [ + { + value: 'tab-default', + label: t('setting.tabStyle.default') + }, + { + value: 'tab-card', + label: t('setting.tabStyle.card') + }, + { + value: 'tab-google', + label: t('setting.tabStyle.google') + } + ]) + + // 页面切换动画选项 + const pageTransitionOptions = computed(() => [ + { + value: '', + label: t('setting.transition.list.none') + }, + { + value: 'fade', + label: t('setting.transition.list.fade') + }, + { + value: 'slide-left', + label: t('setting.transition.list.slideLeft') + }, + { + value: 'slide-bottom', + label: t('setting.transition.list.slideBottom') + }, + { + value: 'slide-top', + label: t('setting.transition.list.slideTop') + } + ]) + + // 圆角大小选项 + const customRadiusOptions = [ + { value: '0', label: '0' }, + { value: '0.25', label: '0.25' }, + { value: '0.5', label: '0.5' }, + { value: '0.75', label: '0.75' }, + { value: '1', label: '1' } + ] + + // 容器宽度选项 + const containerWidthOptions = computed(() => [ + { + value: ContainerWidthEnum.FULL, + label: t('setting.container.list[0]'), + icon: 'icon-park-outline:auto-width' + }, + { + value: ContainerWidthEnum.BOXED, + label: t('setting.container.list[1]'), + icon: 'ix:width' + } + ]) + + // 盒子样式选项 + const boxStyleOptions = computed(() => [ + { + value: 'border-mode', + label: t('setting.box.list[0]'), + type: 'border-mode' as const + }, + { + value: 'shadow-mode', + label: t('setting.box.list[1]'), + type: 'shadow-mode' as const + } + ]) + + // 从配置文件获取的选项 + const configOptions = { + // 主题色彩选项 + mainColors: AppConfig.systemMainColor, + + // 主题风格选项 + themeList: AppConfig.settingThemeList, + + // 菜单布局选项 + menuLayoutList: AppConfig.menuLayoutList + } + + // 基础设置项配置 + const basicSettingsConfig = computed(() => { + // 定义所有基础设置项 + const allSettings = [ + { + key: 'showWorkTab', + label: t('setting.basics.list.multiTab'), + type: 'switch' as const, + handler: 'workTab', + headerBarKey: null // 不依赖headerBar配置 + }, + { + key: 'uniqueOpened', + label: t('setting.basics.list.accordion'), + type: 'switch' as const, + handler: 'uniqueOpened', + headerBarKey: null // 不依赖headerBar配置 + }, + { + key: 'showMenuButton', + label: t('setting.basics.list.collapseSidebar'), + type: 'switch' as const, + handler: 'menuButton', + headerBarKey: 'menuButton' as const + }, + { + key: 'showFastEnter', + label: t('setting.basics.list.fastEnter'), + type: 'switch' as const, + handler: 'fastEnter', + headerBarKey: 'fastEnter' as const + }, + { + key: 'showRefreshButton', + label: t('setting.basics.list.reloadPage'), + type: 'switch' as const, + handler: 'refreshButton', + headerBarKey: 'refreshButton' as const + }, + { + key: 'showCrumbs', + label: t('setting.basics.list.breadcrumb'), + type: 'switch' as const, + handler: 'crumbs', + mobileHide: true, + headerBarKey: 'breadcrumb' as const + }, + { + key: 'showLanguage', + label: t('setting.basics.list.language'), + type: 'switch' as const, + handler: 'language', + headerBarKey: 'language' as const + }, + { + key: 'showNprogress', + label: t('setting.basics.list.progressBar'), + type: 'switch' as const, + handler: 'nprogress', + headerBarKey: null // 不依赖headerBar配置 + }, + { + key: 'colorWeak', + label: t('setting.basics.list.weakMode'), + type: 'switch' as const, + handler: 'colorWeak', + headerBarKey: null // 不依赖headerBar配置 + }, + { + key: 'watermarkVisible', + label: t('setting.basics.list.watermark'), + type: 'switch' as const, + handler: 'watermark', + headerBarKey: null // 不依赖headerBar配置 + }, + { + key: 'menuOpenWidth', + label: t('setting.basics.list.menuWidth'), + type: 'input-number' as const, + handler: 'menuOpenWidth', + min: 180, + max: 320, + step: 10, + style: { width: '120px' }, + controlsPosition: 'right' as const, + headerBarKey: null // 不依赖headerBar配置 + }, + { + key: 'tabStyle', + label: t('setting.basics.list.tabStyle'), + type: 'select' as const, + handler: 'tabStyle', + options: tabStyleOptions.value, + style: { width: '120px' }, + headerBarKey: null // 不依赖headerBar配置 + }, + { + key: 'pageTransition', + label: t('setting.basics.list.pageTransition'), + type: 'select' as const, + handler: 'pageTransition', + options: pageTransitionOptions.value, + style: { width: '120px' }, + headerBarKey: null // 不依赖headerBar配置 + }, + { + key: 'customRadius', + label: t('setting.basics.list.borderRadius'), + type: 'select' as const, + handler: 'customRadius', + options: customRadiusOptions, + style: { width: '120px' }, + headerBarKey: null // 不依赖headerBar配置 + } + ] + + // 根据 headerBarConfig 过滤设置项 + return ( + allSettings + .filter((setting) => { + // 如果设置项不依赖headerBar配置,则始终显示 + if (setting.headerBarKey === null) { + return true + } + + // 如果依赖headerBar配置,检查对应的功能是否启用 + const headerBarFeature = headerBarConfig[setting.headerBarKey] + return headerBarFeature?.enabled !== false + }) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + .map(({ headerBarKey: _headerBarKey, ...setting }) => setting) + ) + }) + + return { + // 选项配置 + tabStyleOptions, + pageTransitionOptions, + customRadiusOptions, + containerWidthOptions, + boxStyleOptions, + configOptions, + + // 设置项配置 + basicSettingsConfig + } +} diff --git a/src/components/core/layouts/art-settings-panel/composables/useSettingsHandlers.ts b/src/components/core/layouts/art-settings-panel/composables/useSettingsHandlers.ts new file mode 100644 index 0000000..392c690 --- /dev/null +++ b/src/components/core/layouts/art-settings-panel/composables/useSettingsHandlers.ts @@ -0,0 +1,167 @@ +import { useSettingStore } from '@/store/modules/setting' +import { storeToRefs } from 'pinia' +import type { ContainerWidthEnum } from '@/enums/appEnum' + +/** + * 设置项通用处理逻辑 + */ +export function useSettingsHandlers() { + const settingStore = useSettingStore() + + // DOM 操作相关 + const domOperations = { + // 设置HTML类名 + setHtmlClass: (className: string, add: boolean) => { + const el = document.getElementsByTagName('html')[0] + if (add) { + el.classList.add(className) + } else { + el.classList.remove(className) + } + }, + + // 设置根元素属性 + setRootAttribute: (attribute: string, value: string) => { + const el = document.documentElement + el.setAttribute(attribute, value) + }, + + // 设置body类名 + setBodyClass: (className: string, add: boolean) => { + const el = document.getElementsByTagName('body')[0] + if (add) { + el.classList.add(className) + } else { + el.classList.remove(className) + } + } + } + + // 通用切换处理器 + const createToggleHandler = (storeMethod: () => void, callback?: () => void) => { + return () => { + storeMethod() + callback?.() + } + } + + // 通用值变更处理器 + const createValueHandler = ( + storeMethod: (value: T) => void, + callback?: (value: T) => void + ) => { + return (value: T) => { + if (value !== undefined && value !== null) { + storeMethod(value) + callback?.(value) + } + } + } + + // 基础设置处理器 + const basicHandlers = { + // 工作台标签页 + workTab: createToggleHandler(() => settingStore.setWorkTab(!settingStore.showWorkTab)), + + // 菜单手风琴 + uniqueOpened: createToggleHandler(() => settingStore.setUniqueOpened()), + + // 显示菜单按钮 + menuButton: createToggleHandler(() => settingStore.setButton()), + + // 显示快速入口 + fastEnter: createToggleHandler(() => settingStore.setFastEnter()), + + // 显示刷新按钮 + refreshButton: createToggleHandler(() => settingStore.setShowRefreshButton()), + + // 显示面包屑 + crumbs: createToggleHandler(() => settingStore.setCrumbs()), + + // 显示语言切换 + language: createToggleHandler(() => settingStore.setLanguage()), + + // 显示进度条 + nprogress: createToggleHandler(() => settingStore.setNprogress()), + + // 色弱模式 + colorWeak: createToggleHandler( + () => settingStore.setColorWeak(), + () => { + domOperations.setHtmlClass('color-weak', settingStore.colorWeak) + } + ), + + // 水印显示 + watermark: createToggleHandler(() => + settingStore.setWatermarkVisible(!settingStore.watermarkVisible) + ), + + // 菜单展开宽度 + menuOpenWidth: createValueHandler((width: number) => + settingStore.setMenuOpenWidth(width) + ), + + // 标签页风格 + tabStyle: createValueHandler((style: string) => settingStore.setTabStyle(style)), + + // 页面切换动画 + pageTransition: createValueHandler((transition: string) => + settingStore.setPageTransition(transition) + ), + + // 圆角大小 + customRadius: createValueHandler((radius: string) => + settingStore.setCustomRadius(radius) + ) + } + + // 盒子样式处理器 + const boxStyleHandlers = { + // 设置盒子模式 + setBoxMode: (type: 'border-mode' | 'shadow-mode') => { + const { boxBorderMode } = storeToRefs(settingStore) + + // 防止重复设置 + if ( + (type === 'shadow-mode' && boxBorderMode.value === false) || + (type === 'border-mode' && boxBorderMode.value === true) + ) { + return + } + + setTimeout(() => { + domOperations.setRootAttribute('data-box-mode', type) + settingStore.setBorderMode() + }, 50) + } + } + + // 颜色设置处理器 + const colorHandlers = { + // 选择主题色 + selectColor: (theme: string) => { + settingStore.setElementTheme(theme) + settingStore.reload() + } + } + + // 容器设置处理器 + const containerHandlers = { + // 设置容器宽度 + setWidth: (type: ContainerWidthEnum) => { + settingStore.setContainerWidth(type) + settingStore.reload() + } + } + + return { + domOperations, + basicHandlers, + boxStyleHandlers, + colorHandlers, + containerHandlers, + createToggleHandler, + createValueHandler + } +} diff --git a/src/components/core/layouts/art-settings-panel/composables/useSettingsPanel.ts b/src/components/core/layouts/art-settings-panel/composables/useSettingsPanel.ts new file mode 100644 index 0000000..358ef57 --- /dev/null +++ b/src/components/core/layouts/art-settings-panel/composables/useSettingsPanel.ts @@ -0,0 +1,207 @@ +import { ref, computed, watch } from 'vue' +import { useSettingStore } from '@/store/modules/setting' +import { storeToRefs } from 'pinia' +import { useBreakpoints } from '@vueuse/core' +import AppConfig from '@/config' +import { SystemThemeEnum, MenuTypeEnum } from '@/enums/appEnum' +import { mittBus } from '@/utils/sys' +import { useTheme } from '@/hooks/core/useTheme' +import { useCeremony } from '@/hooks/core/useCeremony' +import { useSettingsState } from './useSettingsState' +import { useSettingsHandlers } from './useSettingsHandlers' + +/** + * 设置面板核心逻辑管理 + */ +export function useSettingsPanel() { + const settingStore = useSettingStore() + const { systemThemeType, systemThemeMode, menuType } = storeToRefs(settingStore) + + // Composables + const { openFestival, cleanup } = useCeremony() + const { setSystemTheme, setSystemAutoTheme } = useTheme() + const { initColorWeak } = useSettingsState() + const { domOperations } = useSettingsHandlers() + + // 响应式状态 + const showDrawer = ref(false) + + // 使用 VueUse breakpoints 优化性能 + const breakpoints = useBreakpoints({ tablet: 1000 }) + const isMobile = breakpoints.smaller('tablet') + + // 记录窗口宽度变化前的菜单类型 + const beforeMenuType = ref() + const hasChangedMenu = ref(false) + + // 计算属性 + const systemThemeColor = computed(() => settingStore.systemThemeColor as string) + + // 主题相关处理 + const useThemeHandlers = () => { + // 初始化系统颜色 + const initSystemColor = () => { + if (!AppConfig.systemMainColor.includes(systemThemeColor.value)) { + settingStore.setElementTheme(AppConfig.systemMainColor[0]) + settingStore.reload() + } + } + + // 初始化系统主题 + const initSystemTheme = () => { + if (systemThemeMode.value === SystemThemeEnum.AUTO) { + setSystemAutoTheme() + } else { + setSystemTheme(systemThemeType.value) + } + } + + // 监听系统主题变化 + const listenerSystemTheme = () => { + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)') + mediaQuery.addEventListener('change', initSystemTheme) + return () => { + mediaQuery.removeEventListener('change', initSystemTheme) + } + } + + return { + initSystemColor, + initSystemTheme, + listenerSystemTheme + } + } + + // 响应式布局处理 + const useResponsiveLayout = () => { + // 使用 watch 监听断点变化,性能更优 + const stopWatch = watch( + isMobile, + (mobile: boolean) => { + if (mobile) { + // 切换到移动端布局 + if (!hasChangedMenu.value) { + beforeMenuType.value = menuType.value + useSettingsState().switchMenuLayouts(MenuTypeEnum.LEFT) + settingStore.setMenuOpen(false) + hasChangedMenu.value = true + } + } else { + // 恢复桌面端布局 + if (hasChangedMenu.value && beforeMenuType.value) { + useSettingsState().switchMenuLayouts(beforeMenuType.value) + settingStore.setMenuOpen(true) + hasChangedMenu.value = false + } + } + }, + { immediate: true } + ) + + return { stopWatch } + } + + // 抽屉控制 + const useDrawerControl = () => { + // 用于存储 setTimeout 的 ID,以便在需要时清除 + let themeChangeTimer: ReturnType | null = null + + // 打开抽屉 + const handleOpen = () => { + // 清除可能存在的旧定时器 + if (themeChangeTimer) { + clearTimeout(themeChangeTimer) + } + // 延迟添加 theme-change class,避免抽屉打开动画受影响 + themeChangeTimer = setTimeout(() => { + domOperations.setBodyClass('theme-change', true) + themeChangeTimer = null + }, 500) + } + + // 关闭抽屉 + const handleClose = () => { + // 清除未执行的定时器,防止关闭后才添加 class + if (themeChangeTimer) { + clearTimeout(themeChangeTimer) + themeChangeTimer = null + } + // 立即移除 theme-change class + domOperations.setBodyClass('theme-change', false) + } + + // 打开设置 + const openSetting = () => { + showDrawer.value = true + } + + // 关闭设置 + const closeDrawer = () => { + showDrawer.value = false + } + + return { + handleOpen, + handleClose, + openSetting, + closeDrawer + } + } + + // Props 变化监听 + const usePropsWatcher = (props: { open?: boolean }) => { + watch( + () => props.open, + (val: boolean | undefined) => { + if (val !== undefined) { + showDrawer.value = val + } + } + ) + } + + // 初始化设置 + const useSettingsInitializer = () => { + const themeHandlers = useThemeHandlers() + const { openSetting } = useDrawerControl() + const { stopWatch } = useResponsiveLayout() + let themeCleanup: (() => void) | null = null + + const initializeSettings = () => { + mittBus.on('openSetting', openSetting) + themeHandlers.initSystemColor() + themeCleanup = themeHandlers.listenerSystemTheme() + initColorWeak() + + // 设置盒子模式 + const boxMode = settingStore.boxBorderMode ? 'border-mode' : 'shadow-mode' + domOperations.setRootAttribute('data-box-mode', boxMode) + + themeHandlers.initSystemTheme() + openFestival() + } + + const cleanupSettings = () => { + stopWatch() + themeCleanup?.() + cleanup() + } + + return { + initializeSettings, + cleanupSettings + } + } + + return { + // 状态 + showDrawer, + + // 方法组合 + useThemeHandlers, + useResponsiveLayout, + useDrawerControl, + usePropsWatcher, + useSettingsInitializer + } +} diff --git a/src/components/core/layouts/art-settings-panel/composables/useSettingsState.ts b/src/components/core/layouts/art-settings-panel/composables/useSettingsState.ts new file mode 100644 index 0000000..65352d2 --- /dev/null +++ b/src/components/core/layouts/art-settings-panel/composables/useSettingsState.ts @@ -0,0 +1,37 @@ +import { useSettingStore } from '@/store/modules/setting' +import { MenuThemeEnum, MenuTypeEnum } from '@/enums/appEnum' + +/** + * 设置状态管理 + */ +export function useSettingsState() { + const settingStore = useSettingStore() + + // 色弱模式初始化 + const initColorWeak = () => { + if (settingStore.colorWeak) { + const el = document.getElementsByTagName('html')[0] + setTimeout(() => { + el.classList.add('color-weak') + }, 100) + } + } + + // 菜单布局切换 + const switchMenuLayouts = (type: MenuTypeEnum) => { + if (type === MenuTypeEnum.LEFT || type === MenuTypeEnum.TOP_LEFT) { + settingStore.setMenuOpen(true) + } + settingStore.switchMenuLayouts(type) + if (type === MenuTypeEnum.DUAL_MENU) { + settingStore.switchMenuStyles(MenuThemeEnum.DESIGN) + settingStore.setMenuOpen(true) + } + } + + return { + // 方法 + initColorWeak, + switchMenuLayouts + } +} diff --git a/src/components/core/layouts/art-settings-panel/index.vue b/src/components/core/layouts/art-settings-panel/index.vue new file mode 100644 index 0000000..0cbf344 --- /dev/null +++ b/src/components/core/layouts/art-settings-panel/index.vue @@ -0,0 +1,72 @@ + + + + + + diff --git a/src/components/core/layouts/art-settings-panel/style.scss b/src/components/core/layouts/art-settings-panel/style.scss new file mode 100644 index 0000000..e863074 --- /dev/null +++ b/src/components/core/layouts/art-settings-panel/style.scss @@ -0,0 +1,92 @@ +@use '@styles/core/mixin.scss' as *; + +// 设置抽屉模态框样式 +.setting-modal { + background: transparent !important; + + .el-drawer { + // 背景滤镜效果 + background: rgba($color: #fff, $alpha: 50%) !important; + box-shadow: 0 0 30px rgb(0 0 0 / 10%) !important; + + @include backdropBlur(); + + .setting-box-wrap { + display: flex; + flex-wrap: wrap; + align-items: center; + width: calc(100% + 15px); + margin-bottom: 10px; + + .setting-item { + box-sizing: border-box; + width: calc(33.333% - 15px); + margin-right: 15px; + text-align: center; + + .box { + position: relative; + box-sizing: border-box; + display: flex; + height: 52px; + overflow: hidden; + cursor: pointer; + border: 2px solid var(--default-border); + border-radius: 8px; + box-shadow: 0 0 8px 0 rgb(0 0 0 / 10%); + transition: box-shadow 0.1s; + + &.mt-16 { + margin-top: 16px; + } + + &.is-active { + border: 2px solid var(--theme-color); + } + + img { + width: 100%; + height: 100%; + } + } + + .name { + margin-top: 6px; + font-size: 14px; + text-align: center; + } + } + } + } + + // 去除滚动条 + .el-drawer__body::-webkit-scrollbar { + width: 0 !important; + } +} + +.dark { + .setting-modal { + .el-drawer { + background: rgba($color: #000, $alpha: 50%) !important; + + .setting-item { + .box { + border: 2px solid transparent; + } + } + } + } +} + +// 去除火狐浏览器滚动条 +:deep(.el-drawer__body) { + scrollbar-width: none; +} + +// 移动端隐藏 +@media screen and (width <= 800px) { + .mobile-hide { + display: none !important; + } +} diff --git a/src/components/core/layouts/art-settings-panel/widget/BasicSettings.vue b/src/components/core/layouts/art-settings-panel/widget/BasicSettings.vue new file mode 100644 index 0000000..b6dc9d3 --- /dev/null +++ b/src/components/core/layouts/art-settings-panel/widget/BasicSettings.vue @@ -0,0 +1,77 @@ + + + diff --git a/src/components/core/layouts/art-settings-panel/widget/BoxStyleSettings.vue b/src/components/core/layouts/art-settings-panel/widget/BoxStyleSettings.vue new file mode 100644 index 0000000..86c7a9e --- /dev/null +++ b/src/components/core/layouts/art-settings-panel/widget/BoxStyleSettings.vue @@ -0,0 +1,38 @@ + + + diff --git a/src/components/core/layouts/art-settings-panel/widget/ColorSettings.vue b/src/components/core/layouts/art-settings-panel/widget/ColorSettings.vue new file mode 100644 index 0000000..05a4b41 --- /dev/null +++ b/src/components/core/layouts/art-settings-panel/widget/ColorSettings.vue @@ -0,0 +1,35 @@ + + + diff --git a/src/components/core/layouts/art-settings-panel/widget/ContainerSettings.vue b/src/components/core/layouts/art-settings-panel/widget/ContainerSettings.vue new file mode 100644 index 0000000..1f5be72 --- /dev/null +++ b/src/components/core/layouts/art-settings-panel/widget/ContainerSettings.vue @@ -0,0 +1,33 @@ + + + diff --git a/src/components/core/layouts/art-settings-panel/widget/MenuLayoutSettings.vue b/src/components/core/layouts/art-settings-panel/widget/MenuLayoutSettings.vue new file mode 100644 index 0000000..dbcae46 --- /dev/null +++ b/src/components/core/layouts/art-settings-panel/widget/MenuLayoutSettings.vue @@ -0,0 +1,31 @@ + + + diff --git a/src/components/core/layouts/art-settings-panel/widget/MenuStyleSettings.vue b/src/components/core/layouts/art-settings-panel/widget/MenuStyleSettings.vue new file mode 100644 index 0000000..61237eb --- /dev/null +++ b/src/components/core/layouts/art-settings-panel/widget/MenuStyleSettings.vue @@ -0,0 +1,44 @@ + + + diff --git a/src/components/core/layouts/art-settings-panel/widget/SectionTitle.vue b/src/components/core/layouts/art-settings-panel/widget/SectionTitle.vue new file mode 100644 index 0000000..31ef00c --- /dev/null +++ b/src/components/core/layouts/art-settings-panel/widget/SectionTitle.vue @@ -0,0 +1,17 @@ + + + diff --git a/src/components/core/layouts/art-settings-panel/widget/SettingActions.vue b/src/components/core/layouts/art-settings-panel/widget/SettingActions.vue new file mode 100644 index 0000000..7b47d1a --- /dev/null +++ b/src/components/core/layouts/art-settings-panel/widget/SettingActions.vue @@ -0,0 +1,235 @@ + + + + diff --git a/src/components/core/layouts/art-settings-panel/widget/SettingDrawer.vue b/src/components/core/layouts/art-settings-panel/widget/SettingDrawer.vue new file mode 100644 index 0000000..85372be --- /dev/null +++ b/src/components/core/layouts/art-settings-panel/widget/SettingDrawer.vue @@ -0,0 +1,51 @@ + + + diff --git a/src/components/core/layouts/art-settings-panel/widget/SettingHeader.vue b/src/components/core/layouts/art-settings-panel/widget/SettingHeader.vue new file mode 100644 index 0000000..e3ead9e --- /dev/null +++ b/src/components/core/layouts/art-settings-panel/widget/SettingHeader.vue @@ -0,0 +1,18 @@ + + + diff --git a/src/components/core/layouts/art-settings-panel/widget/SettingItem.vue b/src/components/core/layouts/art-settings-panel/widget/SettingItem.vue new file mode 100644 index 0000000..5721027 --- /dev/null +++ b/src/components/core/layouts/art-settings-panel/widget/SettingItem.vue @@ -0,0 +1,101 @@ + + + + + diff --git a/src/components/core/layouts/art-settings-panel/widget/ThemeSettings.vue b/src/components/core/layouts/art-settings-panel/widget/ThemeSettings.vue new file mode 100644 index 0000000..4b46fcd --- /dev/null +++ b/src/components/core/layouts/art-settings-panel/widget/ThemeSettings.vue @@ -0,0 +1,28 @@ + + + diff --git a/src/components/core/layouts/art-work-tab/index.vue b/src/components/core/layouts/art-work-tab/index.vue new file mode 100644 index 0000000..152ff63 --- /dev/null +++ b/src/components/core/layouts/art-work-tab/index.vue @@ -0,0 +1,584 @@ + + + + + + diff --git a/src/components/core/media/art-cutter-img/index.vue b/src/components/core/media/art-cutter-img/index.vue new file mode 100644 index 0000000..191ceed --- /dev/null +++ b/src/components/core/media/art-cutter-img/index.vue @@ -0,0 +1,350 @@ + + + + + + diff --git a/src/components/core/media/art-video-player/index.vue b/src/components/core/media/art-video-player/index.vue new file mode 100644 index 0000000..4f681ea --- /dev/null +++ b/src/components/core/media/art-video-player/index.vue @@ -0,0 +1,111 @@ + + + + diff --git a/src/components/core/others/art-menu-right/index.vue b/src/components/core/others/art-menu-right/index.vue new file mode 100644 index 0000000..1cc92ab --- /dev/null +++ b/src/components/core/others/art-menu-right/index.vue @@ -0,0 +1,415 @@ + + + + + + diff --git a/src/components/core/others/art-watermark/index.vue b/src/components/core/others/art-watermark/index.vue new file mode 100644 index 0000000..1d7f06b --- /dev/null +++ b/src/components/core/others/art-watermark/index.vue @@ -0,0 +1,64 @@ + + + + diff --git a/src/components/core/tables/art-table-header/index.vue b/src/components/core/tables/art-table-header/index.vue new file mode 100644 index 0000000..281dc51 --- /dev/null +++ b/src/components/core/tables/art-table-header/index.vue @@ -0,0 +1,340 @@ + + + + + + diff --git a/src/components/core/tables/art-table/index.vue b/src/components/core/tables/art-table/index.vue new file mode 100644 index 0000000..2392d96 --- /dev/null +++ b/src/components/core/tables/art-table/index.vue @@ -0,0 +1,342 @@ + + + + + + + + + diff --git a/src/components/core/tables/art-table/style.scss b/src/components/core/tables/art-table/style.scss new file mode 100644 index 0000000..67459e8 --- /dev/null +++ b/src/components/core/tables/art-table/style.scss @@ -0,0 +1,99 @@ +.art-table { + position: relative; + height: 100%; + + .el-table { + height: 100%; + margin-top: 10px; + } + + :deep(.el-loading-mask) { + z-index: 100; + background-color: var(--default-box-color) !important; + } + + // Loading 过渡动画 - 消失时淡出 + .loading-fade-leave-active { + transition: opacity 0.3s ease-out; + } + + .loading-fade-leave-to { + opacity: 0; + } + + // 空状态垂直居中 + &.is-empty { + :deep(.el-scrollbar__wrap) { + display: flex; + } + } + + .pagination { + display: flex; + margin-top: 13px; + + :deep(.el-select) { + width: 102px !important; + } + + // 分页对齐方式 + &.left { + justify-content: flex-start; + } + + &.center { + justify-content: center; + } + + &.right { + justify-content: flex-end; + } + + // 自定义分页组件样式 + &.custom-pagination { + :deep(.el-pagination) { + .btn-prev, + .btn-next { + background-color: transparent; + border: 1px solid var(--art-gray-300); + transition: border-color 0.15s; + + &:hover:not(.is-disabled) { + color: var(--theme-color); + border-color: var(--theme-color); + } + } + + li { + box-sizing: border-box; + font-weight: 400 !important; + background-color: transparent; + border: 1px solid var(--art-gray-300); + transition: border-color 0.15s; + + &.is-active { + font-weight: 400; + color: #fff; + background-color: var(--theme-color); + border: 1px solid var(--theme-color); + } + + &:hover:not(.is-disabled) { + border-color: var(--theme-color); + } + } + } + } + } +} + +// 移动端分页 +@media (width <= 640px) { + :deep(.el-pagination) { + display: flex; + flex-wrap: wrap; + gap: 15px 0; + align-items: center; + justify-content: center; + } +} diff --git a/src/components/core/text-effect/art-count-to/index.vue b/src/components/core/text-effect/art-count-to/index.vue new file mode 100644 index 0000000..7fb104b --- /dev/null +++ b/src/components/core/text-effect/art-count-to/index.vue @@ -0,0 +1,310 @@ + + + + diff --git a/src/components/core/text-effect/art-festival-text-scroll/index.vue b/src/components/core/text-effect/art-festival-text-scroll/index.vue new file mode 100644 index 0000000..770b457 --- /dev/null +++ b/src/components/core/text-effect/art-festival-text-scroll/index.vue @@ -0,0 +1,32 @@ + + + + diff --git a/src/components/core/text-effect/art-text-scroll/index.vue b/src/components/core/text-effect/art-text-scroll/index.vue new file mode 100644 index 0000000..90be30f --- /dev/null +++ b/src/components/core/text-effect/art-text-scroll/index.vue @@ -0,0 +1,285 @@ + + + + diff --git a/src/components/core/theme/theme-svg/index.vue b/src/components/core/theme/theme-svg/index.vue new file mode 100644 index 0000000..0b565a9 --- /dev/null +++ b/src/components/core/theme/theme-svg/index.vue @@ -0,0 +1,100 @@ + + + + + + + diff --git a/src/components/core/views/exception/ArtException.vue b/src/components/core/views/exception/ArtException.vue new file mode 100644 index 0000000..699228f --- /dev/null +++ b/src/components/core/views/exception/ArtException.vue @@ -0,0 +1,43 @@ + + + diff --git a/src/components/core/views/login/AuthTopBar.vue b/src/components/core/views/login/AuthTopBar.vue new file mode 100644 index 0000000..9455253 --- /dev/null +++ b/src/components/core/views/login/AuthTopBar.vue @@ -0,0 +1,149 @@ + + + + + + diff --git a/src/components/core/views/login/LoginLeftView.vue b/src/components/core/views/login/LoginLeftView.vue new file mode 100644 index 0000000..aac488e --- /dev/null +++ b/src/components/core/views/login/LoginLeftView.vue @@ -0,0 +1,120 @@ + + + + + diff --git a/src/components/core/views/result/ArtResultPage.vue b/src/components/core/views/result/ArtResultPage.vue new file mode 100644 index 0000000..b2eca48 --- /dev/null +++ b/src/components/core/views/result/ArtResultPage.vue @@ -0,0 +1,43 @@ + + + diff --git a/src/components/core/widget/art-icon-button/index.vue b/src/components/core/widget/art-icon-button/index.vue new file mode 100644 index 0000000..760888b --- /dev/null +++ b/src/components/core/widget/art-icon-button/index.vue @@ -0,0 +1,23 @@ + + + + diff --git a/src/components/merchant/AuditTimeline.vue b/src/components/merchant/AuditTimeline.vue new file mode 100644 index 0000000..98eec5a --- /dev/null +++ b/src/components/merchant/AuditTimeline.vue @@ -0,0 +1,57 @@ + + + diff --git a/src/config/assets/images.ts b/src/config/assets/images.ts new file mode 100644 index 0000000..f3e89dd --- /dev/null +++ b/src/config/assets/images.ts @@ -0,0 +1,61 @@ +/** + * 配置图片资源 + * + * 统一管理设置中心使用的预览图片资源。 + * 包含主题样式、菜单布局、菜单风格的预览图。 + * + * ## 图片分类 + * + * - themeStyles: 系统主题预览图(亮色/暗色/自动) + * - menuLayouts: 菜单布局预览图(左侧/顶部/混合/双栏) + * - menuStyles: 菜单风格预览图(设计/暗色/亮色) + * + * @module config/assets/images + * @author Art Design Pro Team + */ + +import lightTheme from '@imgs/settings/theme_styles/light.png' +import darkTheme from '@imgs/settings/theme_styles/dark.png' +import systemTheme from '@imgs/settings/theme_styles/system.png' +import verticalLayout from '@imgs/settings/menu_layouts/vertical.png' +import horizontalLayout from '@imgs/settings/menu_layouts/horizontal.png' +import mixedLayout from '@imgs/settings/menu_layouts/mixed.png' +import dualColumnLayout from '@imgs/settings/menu_layouts/dual_column.png' +import designStyle from '@imgs/settings/menu_styles/design.png' +import darkStyle from '@imgs/settings/menu_styles/dark.png' +import lightStyle from '@imgs/settings/menu_styles/light.png' + +/** + * 配置中心图片资源对象 + */ +export const configImages = { + /** 系统主题预览图 */ + themeStyles: { + /** 亮色主题 */ + light: lightTheme, + /** 暗色主题 */ + dark: darkTheme, + /** 自动主题(跟随系统) */ + system: systemTheme + }, + /** 菜单布局预览图 */ + menuLayouts: { + /** 左侧菜单 */ + vertical: verticalLayout, + /** 顶部菜单 */ + horizontal: horizontalLayout, + /** 混合菜单 */ + mixed: mixedLayout, + /** 双栏菜单 */ + dualColumn: dualColumnLayout + }, + /** 菜单风格预览图 */ + menuStyles: { + /** 设计风格 */ + design: designStyle, + /** 暗色风格 */ + dark: darkStyle, + /** 亮色风格 */ + light: lightStyle + } +} diff --git a/src/config/fastEnter.ts b/src/config/fastEnter.ts new file mode 100644 index 0000000..ccade16 --- /dev/null +++ b/src/config/fastEnter.ts @@ -0,0 +1,79 @@ +/** + * 快速入口配置 + * 包含:应用列表、快速链接等配置 + */ +import { WEB_LINKS } from '@/utils/constants' +import type { FastEnterConfig } from '@/types/config' + +const fastEnterConfig: FastEnterConfig = { + // 显示条件(屏幕宽度) + minWidth: 1200, + // 应用列表 + applications: [ + { + name: '工作台', + description: '系统概览与数据统计', + icon: 'ri:pie-chart-line', + iconColor: '#377dff', + enabled: true, + order: 1, + routeName: 'Console' + }, + { + name: '官方文档', + description: '使用指南与开发文档', + icon: 'ri:bill-line', + iconColor: '#ffb100', + enabled: true, + order: 2, + link: WEB_LINKS.DOCS + }, + { + name: '技术支持', + description: '技术支持与问题反馈', + icon: 'ri:user-location-line', + iconColor: '#ff6b6b', + enabled: true, + order: 3, + link: WEB_LINKS.COMMUNITY + }, + { + name: '哔哩哔哩', + description: '技术分享与交流', + icon: 'ri:bilibili-line', + iconColor: '#FB7299', + enabled: true, + order: 4, + link: WEB_LINKS.BILIBILI + } + ], + // 快速链接 + quickLinks: [ + { + name: '登录', + enabled: true, + order: 1, + routeName: 'Login' + }, + { + name: '注册', + enabled: true, + order: 2, + routeName: 'Register' + }, + { + name: '忘记密码', + enabled: true, + order: 3, + routeName: 'ForgetPassword' + }, + { + name: '个人中心', + enabled: true, + order: 4, + routeName: 'UserCenter' + } + ] +} + +export default Object.freeze(fastEnterConfig) diff --git a/src/config/index.ts b/src/config/index.ts new file mode 100644 index 0000000..e6466bc --- /dev/null +++ b/src/config/index.ts @@ -0,0 +1,135 @@ +/** + * 系统全局配置 + * + * 这是系统的核心配置文件,集中管理所有全局配置项。 + * 包含系统信息、主题样式、菜单布局、颜色方案等所有可配置项。 + * + * ## 主要功能 + * + * - 系统信息 - 系统名称等基础信息 + * - 主题配置 - 亮色/暗色/自动主题的样式配置 + * - 菜单配置 - 菜单布局、主题、宽度等配置 + * - 颜色方案 - 系统主色和预设颜色列表 + * - 快速入口 - 快速入口应用和链接配置 + * - 顶部栏配置 - 顶部栏功能模块配置 + * + * ## 配置项说明 + * + * - systemInfo: 系统基础信息(名称等) + * - systemThemeStyles: 系统主题样式映射 + * - settingThemeList: 可选的系统主题列表 + * - menuLayoutList: 可选的菜单布局列表 + * - themeList: 菜单主题样式列表 + * - darkMenuStyles: 暗黑模式下的菜单样式 + * - systemMainColor: 预设的系统主色列表 + * - fastEnter: 快速入口配置 + * - headerBar: 顶部栏功能配置 + * + * @module config + * @author Art Design Pro Team + */ + +import { MenuThemeEnum, MenuTypeEnum, SystemThemeEnum } from '@/enums/appEnum' +import { SystemConfig } from '@/types/config' +import { configImages } from './assets/images' +import fastEnterConfig from './modules/fastEnter' +import { headerBarConfig } from './modules/headerBar' + +const appConfig: SystemConfig = { + // 系统信息 + systemInfo: { + name: 'TakeoutSaaS AdminUI' // 系统名称 + }, + // 系统主题 + systemThemeStyles: { + [SystemThemeEnum.LIGHT]: { className: '' }, + [SystemThemeEnum.DARK]: { className: SystemThemeEnum.DARK } + }, + // 系统主题列表 + settingThemeList: [ + { + name: 'Light', + theme: SystemThemeEnum.LIGHT, + color: ['#fff', '#fff'], + leftLineColor: '#EDEEF0', + rightLineColor: '#EDEEF0', + img: configImages.themeStyles.light + }, + { + name: 'Dark', + theme: SystemThemeEnum.DARK, + color: ['#22252A'], + leftLineColor: '#3F4257', + rightLineColor: '#3F4257', + img: configImages.themeStyles.dark + }, + { + name: 'System', + theme: SystemThemeEnum.AUTO, + color: ['#fff', '#22252A'], + leftLineColor: '#EDEEF0', + rightLineColor: '#3F4257', + img: configImages.themeStyles.system + } + ], + // 菜单布局列表 + menuLayoutList: [ + { name: 'Left', value: MenuTypeEnum.LEFT, img: configImages.menuLayouts.vertical }, + { name: 'Top', value: MenuTypeEnum.TOP, img: configImages.menuLayouts.horizontal }, + { name: 'Mixed', value: MenuTypeEnum.TOP_LEFT, img: configImages.menuLayouts.mixed }, + { name: 'Dual Column', value: MenuTypeEnum.DUAL_MENU, img: configImages.menuLayouts.dualColumn } + ], + // 菜单主题列表 + themeList: [ + { + theme: MenuThemeEnum.DESIGN, + background: '#FFFFFF', + systemNameColor: 'var(--art-gray-800)', + iconColor: '#6B6B6B', + textColor: '#29343D', + img: configImages.menuStyles.design + }, + { + theme: MenuThemeEnum.DARK, + background: '#191A23', + systemNameColor: '#D9DADB', + iconColor: '#BABBBD', + textColor: '#BABBBD', + img: configImages.menuStyles.dark + }, + { + theme: MenuThemeEnum.LIGHT, + background: '#ffffff', + systemNameColor: 'var(--art-gray-800)', + iconColor: '#6B6B6B', + textColor: '#29343D', + img: configImages.menuStyles.light + } + ], + // 暗黑模式菜单样式 + darkMenuStyles: [ + { + theme: MenuThemeEnum.DARK, + background: 'var(--default-box-color)', + systemNameColor: '#DDDDDD', + iconColor: '#BABBBD', + textColor: 'rgba(#FFFFFF, 0.7)' + } + ], + // 系统主色 + systemMainColor: [ + '#5D87FF', + '#B48DF3', + '#1D84FF', + '#60C041', + '#38C0FC', + '#F9901F', + '#FF80C8' + ] as const, + // 快速入口配置 + fastEnter: fastEnterConfig, + // 顶部栏功能配置 + headerBar: headerBarConfig +} + +export default Object.freeze(appConfig) diff --git a/src/config/modules/component.ts b/src/config/modules/component.ts new file mode 100644 index 0000000..bc709e0 --- /dev/null +++ b/src/config/modules/component.ts @@ -0,0 +1,105 @@ +/** + * 全局组件配置 + * + * 统一管理系统级全局组件的注册。 + * 这些组件会在应用启动时全局注册,可在任何地方使用。 + * + * ## 主要功能 + * + * - 组件配置 - 集中管理全局组件的配置信息 + * - 异步加载 - 使用 defineAsyncComponent 实现按需加载 + * - 开关控制 - 支持通过 enabled 字段启用/禁用组件 + * - 配置查询 - 提供工具函数快速查询组件配置 + * + * @module config/component + * @author Art Design Pro Team + */ + +import { defineAsyncComponent } from 'vue' + +/** + * 全局组件配置列表 + */ +export const globalComponentsConfig: GlobalComponentConfig[] = [ + { + name: '设置面板', + key: 'settings-panel', + component: defineAsyncComponent( + () => import('@/components/core/layouts/art-settings-panel/index.vue') + ), + enabled: true + }, + { + name: '全局搜索', + key: 'global-search', + component: defineAsyncComponent( + () => import('@/components/core/layouts/art-global-search/index.vue') + ), + enabled: true + }, + { + name: '锁屏', + key: 'screen-lock', + component: defineAsyncComponent( + () => import('@/components/core/layouts/art-screen-lock/index.vue') + ), + enabled: true + }, + { + name: '聊天窗口', + key: 'chat-window', + component: defineAsyncComponent( + () => import('@/components/core/layouts/art-chat-window/index.vue') + ), + enabled: true + }, + { + name: '礼花效果', + key: 'fireworks-effect', + component: defineAsyncComponent( + () => import('@/components/core/layouts/art-fireworks-effect/index.vue') + ), + enabled: true + }, + { + name: '水印效果', + key: 'watermark', + component: defineAsyncComponent( + () => import('@/components/core/others/art-watermark/index.vue') + ), + enabled: true + } +] + +/** + * 全局组件配置接口 + */ +export interface GlobalComponentConfig { + /** 组件名称 */ + name: string + /** 组件标识 */ + key: string + /** 组件 */ + component: any + /** 是否启用 */ + enabled?: boolean + /** 组件描述 */ + description?: string +} + +/** + * 获取启用的全局组件 + * @returns 已启用的组件配置列表 + */ +export const getEnabledGlobalComponents = () => { + return globalComponentsConfig.filter((config) => config.enabled !== false) +} + +/** + * 根据 key 获取组件配置 + * @param key 组件标识 + * @returns 组件配置对象 + */ +export const getGlobalComponentByKey = (key: string) => { + return globalComponentsConfig.find((config) => config.key === key) +} diff --git a/src/config/modules/fastEnter.ts b/src/config/modules/fastEnter.ts new file mode 100644 index 0000000..6b9740c --- /dev/null +++ b/src/config/modules/fastEnter.ts @@ -0,0 +1,127 @@ +/** + * 快速入口配置 + * 包含:应用列表、快速链接等配置 + */ +import { WEB_LINKS } from '@/utils/constants' +import type { FastEnterConfig } from '@/types/config' + +const fastEnterConfig: FastEnterConfig = { + // 显示条件(屏幕宽度) + minWidth: 1200, + // 应用列表 + applications: [ + { + name: '工作台', + description: '系统概览与数据统计', + icon: 'ri:pie-chart-line', + iconColor: '#377dff', + enabled: true, + order: 1, + routeName: 'Console' + }, + { + name: '分析页', + description: '数据分析与可视化', + icon: 'ri:game-line', + iconColor: '#ff3b30', + enabled: true, + order: 2, + routeName: 'Analysis' + }, + { + name: '礼花效果', + description: '动画特效展示', + icon: 'ri:loader-line', + iconColor: '#7A7FFF', + enabled: true, + order: 3, + routeName: 'Fireworks' + }, + { + name: '聊天', + description: '即时通讯功能', + icon: 'ri:user-line', + iconColor: '#13DEB9', + enabled: true, + order: 4, + routeName: 'Chat' + }, + { + name: '官方文档', + description: '使用指南与开发文档', + icon: 'ri:bill-line', + iconColor: '#ffb100', + enabled: true, + order: 5, + link: WEB_LINKS.DOCS + }, + { + name: '技术支持', + description: '技术支持与问题反馈', + icon: 'ri:user-location-line', + iconColor: '#ff6b6b', + enabled: true, + order: 6, + link: WEB_LINKS.COMMUNITY + }, + { + name: '更新日志', + description: '版本更新与变更记录', + icon: 'ri:gamepad-line', + iconColor: '#38C0FC', + enabled: true, + order: 7, + routeName: 'ChangeLog' + }, + { + name: '哔哩哔哩', + description: '技术分享与交流', + icon: 'ri:bilibili-line', + iconColor: '#FB7299', + enabled: true, + order: 8, + link: WEB_LINKS.BILIBILI + } + ], + // 快速链接 + quickLinks: [ + { + name: '登录', + enabled: true, + order: 1, + routeName: 'Login' + }, + { + name: '注册', + enabled: true, + order: 2, + routeName: 'Register' + }, + { + name: '忘记密码', + enabled: true, + order: 3, + routeName: 'ForgetPassword' + }, + { + name: '定价', + enabled: true, + order: 4, + routeName: 'Pricing' + }, + { + name: '个人中心', + enabled: true, + order: 5, + routeName: 'UserCenter' + }, + { + name: '留言管理', + enabled: true, + order: 6, + routeName: 'ArticleComment' + } + ] +} + +export default Object.freeze(fastEnterConfig) diff --git a/src/config/modules/festival.ts b/src/config/modules/festival.ts new file mode 100644 index 0000000..39cd790 --- /dev/null +++ b/src/config/modules/festival.ts @@ -0,0 +1,51 @@ +/** + * 节日庆祝配置 + * + * 配置系统的节日烟花效果和祝福文本。 + * 支持单日节日和跨日期节日,可自定义烟花播放次数。 + * + * ## 配置说明 + * + * - name: 节日名称 + * - date: 节日开始日期(格式:YYYY-MM-DD) + * - endDate: 节日结束日期(可选,用于跨日期节日) + * - image: 烟花图片(需要预先导入) + * - scrollText: 滚动显示的祝福文本 + * - count: 烟花播放次数(可选,默认为 3 次) + * + * ## 注意事项 + * + * - 图片需要预先导入并在配置中引用 + * - 跨日期节日会在整个日期范围内生效 + * - 每个用户每天只会播放一次烟花效果 + * + * @module config/modules/festival + * @author Art Design Pro Team + */ + +import { FestivalConfig } from '@/types/config' + +// 导入烟花图片(根据需要取消注释) +// import sd from '@imgs/ceremony/sd.png' +// import yd from '@imgs/ceremony/yd.png' + +export const festivalConfigList: FestivalConfig[] = [ + // 跨日期示例 + // { + // name: 'v3.0 Sass 升级至 TailwindCSS', + // date: '2025-11-03', + // endDate: '2025-11-09', + // image: '', + // count: 3, + // scrollText: + // '🚀 系统 v3.0 测试阶段正式开启!测试周期为 11 月 3 日 - 11 月 16 日,通过 TailwindCSS 重构样式体系、统一 Iconify 图标方案,带来更高效现代的开发体验,正式发布敬请期待~' + // } + // 单日示例:圣诞节 + // { + // name: '圣诞节', + // date: '2024-12-25', + // image: sd, + // count: 3 // 可选,不设置则使用默认值 3 次 + // scrollText: 'Merry Christmas!Art Design Pro 祝您圣诞快乐,愿节日的欢乐与祝福如雪花般纷至沓来!', + // } +] diff --git a/src/config/modules/headerBar.ts b/src/config/modules/headerBar.ts new file mode 100644 index 0000000..a420e82 --- /dev/null +++ b/src/config/modules/headerBar.ts @@ -0,0 +1,63 @@ +/** + * 顶部栏功能配置 + * + * 统一管理顶部栏各个功能模块的启用状态。 + * 通过修改此配置文件可以快速启用或禁用顶部栏的功能按钮。 + * + * @module config/headerBar + * @author Art Design Pro Team + */ + +import { HeaderBarFeatureConfig } from '@/types' + +/** + * 顶部栏功能配置对象 + */ +export const headerBarConfig: HeaderBarFeatureConfig = { + menuButton: { + enabled: true, + description: '控制左侧菜单的展开/收起按钮' + }, + refreshButton: { + enabled: true, + description: '页面刷新按钮' + }, + fastEnter: { + enabled: true, + description: '快速入口功能,提供常用应用和链接的快速访问' + }, + breadcrumb: { + enabled: true, + description: '面包屑导航,显示当前页面路径' + }, + globalSearch: { + enabled: true, + description: '全局搜索功能,支持快捷键 Ctrl+K 或 Cmd+K' + }, + fullscreen: { + enabled: true, + description: '全屏切换功能' + }, + notification: { + enabled: true, + description: '通知中心,显示系统通知和消息' + }, + chat: { + enabled: true, + description: '聊天功能,提供实时沟通' + }, + language: { + enabled: true, + description: '多语言切换功能' + }, + settings: { + enabled: true, + description: '系统设置面板' + }, + themeToggle: { + enabled: true, + description: '主题切换功能(明暗主题)' + } +} + +export default headerBarConfig diff --git a/src/config/setting.ts b/src/config/setting.ts new file mode 100644 index 0000000..94f2d2c --- /dev/null +++ b/src/config/setting.ts @@ -0,0 +1,109 @@ +/** + * 系统设置默认值配置 + * + * 统一管理系统设置的所有默认值 + * + * ## 主要功能 + * + * - 菜单相关默认配置 + * - 主题相关默认配置 + * - 界面显示默认配置 + * - 功能开关默认配置 + * - 样式相关默认配置 + * + * ## 注意事项 + * + * 1. 修改此文件的配置项时,需要同步更新以下文件: + * - src/components/core/layouts/art-settings-panel/widget/SettingActions.vue(复制配置和重置配置逻辑) + * - src/store/modules/setting.ts(Store 状态定义) + * 2. 可以通过设置面板的"复制配置"按钮快速生成配置代码 + * 3. 枚举类型的值需要与 src/enums/appEnum.ts 中的定义保持一致 + */ + +import AppConfig from '@/config' +import { SystemThemeEnum, MenuThemeEnum, MenuTypeEnum, ContainerWidthEnum } from '@/enums/appEnum' + +/** + * 系统设置默认值配置 + */ +export const SETTING_DEFAULT_CONFIG = { + /** 菜单类型 */ + menuType: MenuTypeEnum.LEFT, + /** 菜单展开宽度 */ + menuOpenWidth: 230, + /** 菜单是否展开 */ + menuOpen: true, + /** 双菜单是否显示文本 */ + dualMenuShowText: false, + /** 系统主题类型 */ + systemThemeType: SystemThemeEnum.AUTO, + /** 系统主题模式 */ + systemThemeMode: SystemThemeEnum.AUTO, + /** 菜单风格 */ + menuThemeType: MenuThemeEnum.DESIGN, + /** 系统主题颜色 */ + systemThemeColor: AppConfig.systemMainColor[0], + /** 是否显示菜单按钮 */ + showMenuButton: true, + /** 是否显示快速入口 */ + showFastEnter: true, + /** 是否显示刷新按钮 */ + showRefreshButton: true, + /** 是否显示面包屑 */ + showCrumbs: true, + /** 是否显示工作台标签 */ + showWorkTab: true, + /** 是否显示语言切换 */ + showLanguage: true, + /** 是否显示进度条 */ + showNprogress: false, + /** 是否显示设置引导 */ + showSettingGuide: true, + /** 是否显示节日文本 */ + showFestivalText: false, + /** 是否显示水印 */ + watermarkVisible: false, + /** 是否自动关闭 */ + autoClose: false, + /** 是否唯一展开 */ + uniqueOpened: true, + /** 是否色弱模式 */ + colorWeak: false, + /** 是否刷新 */ + refresh: false, + /** 是否加载节日烟花 */ + holidayFireworksLoaded: false, + /** 边框模式 */ + boxBorderMode: true, + /** 页面过渡效果 */ + pageTransition: 'slide-left', + /** 标签页样式 */ + tabStyle: 'tab-default', + /** 自定义圆角 */ + customRadius: '0.75', + /** 容器宽度 */ + containerWidth: ContainerWidthEnum.FULL, + /** 节日日期 */ + festivalDate: '' +} + +/** + * 获取设置默认值 + * @returns 设置默认值对象 + */ +export function getSettingDefaults() { + return { ...SETTING_DEFAULT_CONFIG } +} + +/** + * 重置为默认设置 + * @param currentSettings 当前设置对象 + */ +export function resetToDefaults(currentSettings: Record) { + const defaults = getSettingDefaults() + Object.keys(defaults).forEach((key) => { + if (key in currentSettings) { + currentSettings[key] = defaults[key as keyof typeof defaults] + } + }) +} diff --git a/src/directives/business/highlight.ts b/src/directives/business/highlight.ts new file mode 100644 index 0000000..13af225 --- /dev/null +++ b/src/directives/business/highlight.ts @@ -0,0 +1,248 @@ +/** + * v-highlight 代码高亮指令 + * + * 为代码块提供语法高亮、行号显示和一键复制功能。 + * 基于 highlight.js 实现,支持多种编程语言的语法高亮。 + * + * ## 主要功能 + * + * - 语法高亮 - 使用 highlight.js 自动识别并高亮代码 + * - 行号显示 - 自动为每行代码添加行号 + * - 一键复制 - 提供复制按钮,点击即可复制代码(自动过滤行号) + * - 性能优化 - 批量处理代码块,避免阻塞渲染 + * - 动态监听 - 使用 MutationObserver 监听新增代码块 + * - 防重复处理 - 自动标记已处理的代码块,避免重复处理 + * + * ## 使用示例 + * + * ```vue + * + * ``` + * + * ## 性能优化 + * + * - 批量处理:每次处理 10 个代码块,避免长时间阻塞 + * - 延迟处理:使用 requestAnimationFrame 分批处理 + * - 重试机制:自动重试处理失败的代码块 + * - 智能监听:只在有新代码块时才触发处理 + * + * @module directives/highlight + * @author Art Design Pro Team + */ + +import { App, Directive } from 'vue' +import hljs from 'highlight.js' + +// 高亮代码 +function highlightCode(block: HTMLElement) { + hljs.highlightElement(block) +} + +// 插入行号 +function insertLineNumbers(block: HTMLElement) { + const lines = block.innerHTML.split('\n') + const numberedLines = lines + .map((line, index) => { + return `${index + 1} ${line}` + }) + .join('\n') + block.innerHTML = numberedLines +} + +// 添加复制按钮:调整 DOM 结构,将代码部分包裹在 .code-wrapper 内 +function addCopyButton(block: HTMLElement) { + const copyButton = document.createElement('i') + copyButton.className = 'copy-button' + copyButton.innerHTML = + '' + copyButton.onclick = () => { + // 过滤掉行号,只复制代码内容 + const codeContent = block.innerText.replace(/^\d+\s+/gm, '') + navigator.clipboard.writeText(codeContent).then(() => { + ElMessage.success('复制成功') + }) + } + + const preElement = block.parentElement + if (preElement) { + let codeWrapper: HTMLElement + // 如果代码块还没有被包裹,则创建包裹容器 + if (!block.parentElement.classList.contains('code-wrapper')) { + codeWrapper = document.createElement('div') + codeWrapper.className = 'code-wrapper' + preElement.replaceChild(codeWrapper, block) + codeWrapper.appendChild(block) + } else { + codeWrapper = block.parentElement + } + // 将复制按钮添加到 pre 元素(而非 codeWrapper 内),这样它不会随滚动条滚动 + preElement.appendChild(copyButton) + } +} + +// 检查代码块是否已经被处理过 +function isBlockProcessed(block: HTMLElement): boolean { + return ( + block.hasAttribute('data-highlighted') || + !!block.querySelector('.line-number') || + !!block.parentElement?.querySelector('.copy-button') + ) +} + +// 标记代码块为已处理 +function markBlockAsProcessed(block: HTMLElement) { + block.setAttribute('data-highlighted', 'true') +} + +// 处理单个代码块 +function processBlock(block: HTMLElement) { + if (isBlockProcessed(block)) { + return + } + + try { + highlightCode(block) + insertLineNumbers(block) + addCopyButton(block) + markBlockAsProcessed(block) + } catch (error) { + console.warn('处理代码块时出错:', error) + } +} + +// 查找并处理所有代码块 +function processAllCodeBlocks(el: HTMLElement) { + const blocks = Array.from(el.querySelectorAll('pre code')) + const unprocessedBlocks = blocks.filter((block) => !isBlockProcessed(block)) + + if (unprocessedBlocks.length === 0) { + return + } + + if (unprocessedBlocks.length <= 10) { + // 如果代码块数量少于等于10,直接处理所有代码块 + unprocessedBlocks.forEach((block) => processBlock(block)) + } else { + // 定义每次处理的代码块数 + const batchSize = 10 + let currentIndex = 0 + + const processBatch = () => { + const batch = unprocessedBlocks.slice(currentIndex, currentIndex + batchSize) + + batch.forEach((block) => { + processBlock(block) + }) + + // 更新索引并继续处理下一批 + currentIndex += batchSize + if (currentIndex < unprocessedBlocks.length) { + // 使用 requestAnimationFrame 确保下一帧再处理 + requestAnimationFrame(processBatch) + } + } + + // 开始处理第一批代码块 + processBatch() + } +} + +// 重试处理函数 +function retryProcessing(el: HTMLElement, maxRetries: number = 3, delay: number = 200) { + let retryCount = 0 + + const tryProcess = () => { + processAllCodeBlocks(el) + + // 检查是否还有未处理的代码块 + const remainingBlocks = Array.from(el.querySelectorAll('pre code')).filter( + (block) => !isBlockProcessed(block) + ) + + if (remainingBlocks.length > 0 && retryCount < maxRetries) { + retryCount++ + setTimeout(tryProcess, delay * retryCount) // 递增延迟 + } + } + + tryProcess() +} + +// 代码高亮、插入行号、复制按钮 +const highlightDirective: Directive = { + mounted(el: HTMLElement) { + // 立即尝试处理一次 + processAllCodeBlocks(el) + + // 延迟处理,确保 v-html 内容已经渲染 + setTimeout(() => { + retryProcessing(el) + }, 100) + + // 使用 MutationObserver 监听 DOM 变化 + const observer = new MutationObserver((mutations) => { + let hasNewCodeBlocks = false + + mutations.forEach((mutation) => { + if (mutation.type === 'childList') { + mutation.addedNodes.forEach((node) => { + if (node.nodeType === Node.ELEMENT_NODE) { + const element = node as HTMLElement + // 检查新添加的节点是否包含代码块 + if (element.tagName === 'PRE' || element.querySelector('pre code')) { + hasNewCodeBlocks = true + } + } + }) + } + }) + + if (hasNewCodeBlocks) { + // 延迟处理新添加的代码块 + setTimeout(() => { + processAllCodeBlocks(el) + }, 50) + } + }) + + // 开始观察 + observer.observe(el, { + childList: true, + subtree: true + }) + + // 将 observer 存储到元素上,以便在 unmounted 时清理 + ;(el as any)._highlightObserver = observer + }, + + updated(el: HTMLElement) { + // 当组件更新时,重新处理代码块 + setTimeout(() => { + processAllCodeBlocks(el) + }, 50) + }, + + unmounted(el: HTMLElement) { + // 清理 MutationObserver + const observer = (el as any)._highlightObserver + if (observer) { + observer.disconnect() + delete (el as any)._highlightObserver + } + } +} + +export function setupHighlightDirective(app: App) { + app.directive('highlight', highlightDirective) +} diff --git a/src/directives/business/ripple.ts b/src/directives/business/ripple.ts new file mode 100644 index 0000000..8d7d8f9 --- /dev/null +++ b/src/directives/business/ripple.ts @@ -0,0 +1,114 @@ +/** + * v-ripple 水波纹效果指令 + * + * 为元素添加 Material Design 风格的水波纹点击效果。 + * 点击时从点击位置扩散出圆形水波纹动画,提升交互体验。 + * + * ## 主要功能 + * + * - 水波纹动画 - 点击时从点击位置扩散圆形波纹 + * - 自适应大小 - 根据元素尺寸自动调整波纹大小和动画时长 + * - 智能配色 - 自动识别按钮类型,使用合适的波纹颜色 + * - 自定义颜色 - 支持通过参数自定义波纹颜色 + * - 性能优化 - 使用 requestAnimationFrame 和自动清理机制 + * + * ## 使用示例 + * + * ```vue + * + * ``` + * + * ## 颜色规则 + * + * - 有色按钮(primary、success、warning 等):使用白色半透明波纹 + * - 默认按钮:使用主题色半透明波纹 + * - 自定义:通过 color 参数指定任意颜色 + * + * @module directives/ripple + * @author Art Design Pro Team + */ + +import type { App, Directive, DirectiveBinding } from 'vue' + +export interface RippleOptions { + /** 水波纹颜色 */ + color?: string +} + +export const vRipple: Directive = { + mounted(el: HTMLElement, binding: DirectiveBinding) { + // 获取指令的配置参数 + const options: RippleOptions = binding.value || {} + + // 设置元素为相对定位,并隐藏溢出部分 + el.style.position = 'relative' + el.style.overflow = 'hidden' + + // 点击事件处理 + el.addEventListener('mousedown', (e: MouseEvent) => { + const rect = el.getBoundingClientRect() + const left = e.clientX - rect.left + const top = e.clientY - rect.top + + // 创建水波纹元素 + const ripple = document.createElement('div') + const diameter = Math.max(el.clientWidth, el.clientHeight) + const radius = diameter / 2 + + // 根据直径计算动画时间(直径越大,动画时间越长) + const baseTime = 600 // 基础动画时间(毫秒) + const scaleFactor = 0.5 // 缩放因子 + const animationDuration = baseTime + diameter * scaleFactor + + // 设置水波纹的尺寸和位置 + ripple.style.width = ripple.style.height = `${diameter}px` + ripple.style.left = `${left - radius}px` + ripple.style.top = `${top - radius}px` + ripple.style.position = 'absolute' + ripple.style.borderRadius = '50%' + ripple.style.pointerEvents = 'none' + + // 判断是否为有色按钮(Element Plus 按钮类型) + const buttonTypes = ['primary', 'info', 'warning', 'danger', 'success'].map( + (type) => `el-button--${type}` + ) + const isColoredButton = buttonTypes.some((type) => el.classList.contains(type)) + const defaultColor = isColoredButton + ? 'rgba(255, 255, 255, 0.25)' // 有色按钮使用白色水波纹 + : 'var(--el-color-primary-light-7)' // 默认按钮使用主题色水波纹 + + // 设置水波纹颜色、初始状态和过渡效果 + ripple.style.backgroundColor = options.color || defaultColor + ripple.style.transform = 'scale(0)' + ripple.style.transition = `transform ${animationDuration}ms cubic-bezier(0.3, 0, 0.2, 1), opacity ${animationDuration}ms cubic-bezier(0.3, 0, 0.5, 1)` + ripple.style.zIndex = '1' + + // 添加水波纹元素到DOM中 + el.appendChild(ripple) + + // 触发动画 + requestAnimationFrame(() => { + ripple.style.transform = 'scale(2)' + ripple.style.opacity = '0' + }) + + // 动画结束后移除水波纹元素 + setTimeout(() => { + ripple.remove() + }, animationDuration + 500) // 增加500ms缓冲时间 + }) + } +} + +export function setupRippleDirective(app: App) { + app.directive('ripple', vRipple) +} diff --git a/src/directives/core/auth.ts b/src/directives/core/auth.ts new file mode 100644 index 0000000..1829a0e --- /dev/null +++ b/src/directives/core/auth.ts @@ -0,0 +1,92 @@ +/** + * v-auth 权限指令 + * + * 适用于后端权限控制模式,基于权限标识控制 DOM 元素的显示和隐藏。 + * 如果用户没有对应权限,元素将从 DOM 中移除。 + * + * ## 主要功能 + * + * - 权限验证 - 根据路由 meta 中的权限列表验证用户权限 + * - DOM 控制 - 无权限时自动移除元素,而非隐藏 + * - 响应式更新 - 权限变化时自动更新元素状态 + * + * ## 使用示例 + * + * ```vue + * + * 新增 + * + * + * 编辑 + * + * + * 删除 + * ``` + * + * ## 注意事项 + * + * - 该指令会直接移除 DOM 元素,而不是使用 v-if 隐藏 + * - 权限列表从当前路由的 meta.authList 中获取 + * + * @module directives/auth + * @author Art Design Pro Team + */ + +import { App, Directive, DirectiveBinding } from 'vue' + +import { useUserStore } from '@/store/modules/user' + +interface AuthBinding extends DirectiveBinding { + value: string | string[] +} + +function checkAuthPermission(el: HTMLElement, binding: AuthBinding): void { + const { value } = binding + const userStore = useUserStore() + const permissions = userStore.permissions + + if (value && value instanceof Array && value.length > 0) { + const hasPermission = permissions.some((permission) => { + return value.includes(permission) + }) + + if (!hasPermission) { + disableElement(el) + } + } else if (value && typeof value === 'string') { + const hasPermission = permissions.includes(value) + + if (!hasPermission) { + disableElement(el) + } + } +} + +function disableElement(el: HTMLElement): void { + // 添加置灰样式 + el.style.filter = 'grayscale(100%)' + el.style.opacity = '0.5' + el.style.pointerEvents = 'none' + el.style.cursor = 'not-allowed' + // 如果是按钮或输入框,添加 disabled 属性 + if (el instanceof HTMLButtonElement || el instanceof HTMLInputElement) { + el.disabled = true + } + + // 阻止点击事件 + el.addEventListener('click', stopEvent, true) +} + +function stopEvent(e: Event) { + e.preventDefault() + e.stopPropagation() +} + +const authDirective: Directive = { + mounted: checkAuthPermission, + updated: checkAuthPermission +} + +export function setupAuthDirective(app: App): void { + app.directive('auth', authDirective) +} diff --git a/src/directives/core/roles.ts b/src/directives/core/roles.ts new file mode 100644 index 0000000..2ab1029 --- /dev/null +++ b/src/directives/core/roles.ts @@ -0,0 +1,89 @@ +/** + * v-roles 角色权限指令 + * + * 基于用户角色控制 DOM 元素的显示和隐藏。 + * 只要用户拥有指定角色中的任意一个,元素就会显示,否则从 DOM 中移除。 + * + * ## 主要功能 + * + * - 角色验证 - 检查用户是否拥有指定角色 + * - 多角色支持 - 支持单个角色或多个角色(满足其一即可) + * - DOM 控制 - 无权限时自动移除元素,而非隐藏 + * - 响应式更新 - 角色变化时自动更新元素状态 + * + * ## 使用示例 + * + * ```vue + * + * ``` + * + * ## 权限逻辑 + * + * - 用户角色从 userStore.getUserInfo.roles 获取 + * - 只要用户拥有指定角色中的任意一个,元素就会显示 + * - 如果用户没有任何角色或不满足条件,元素将被移除 + * + * ## 注意事项 + * + * - 该指令会直接移除 DOM 元素,而不是使用 v-if 隐藏 + * - 适用于基于角色的粗粒度权限控制 + * - 如需基于具体操作的细粒度权限控制,请使用 v-auth 指令 + * + * @module directives/roles + * @author Art Design Pro Team + */ + +import { useUserStore } from '@/store/modules/user' +import { App, Directive, DirectiveBinding } from 'vue' + +interface RolesBinding extends DirectiveBinding { + value: string | string[] +} + +function checkRolePermission(el: HTMLElement, binding: RolesBinding): void { + const userStore = useUserStore() + const userRoles = userStore.getUserInfo.roles + + // 如果用户角色为空或未定义,移除元素 + if (!userRoles?.length) { + removeElement(el) + return + } + + // 确保指令值为数组格式 + const requiredRoles = Array.isArray(binding.value) ? binding.value : [binding.value] + + // 检查用户是否具有所需角色之一 + const hasPermission = requiredRoles.some((role: string) => userRoles.includes(role)) + + // 如果没有权限,安全地移除元素 + if (!hasPermission) { + removeElement(el) + } +} + +function removeElement(el: HTMLElement): void { + if (el.parentNode) { + el.parentNode.removeChild(el) + } +} + +const rolesDirective: Directive = { + mounted: checkRolePermission, + updated: checkRolePermission +} + +export function setupRolesDirective(app: App): void { + app.directive('roles', rolesDirective) +} diff --git a/src/directives/index.ts b/src/directives/index.ts new file mode 100644 index 0000000..780464b --- /dev/null +++ b/src/directives/index.ts @@ -0,0 +1,12 @@ +import type { App } from 'vue' +import { setupAuthDirective } from './core/auth' +import { setupHighlightDirective } from './business/highlight' +import { setupRippleDirective } from './business/ripple' +import { setupRolesDirective } from './core/roles' + +export function setupGlobDirectives(app: App) { + setupAuthDirective(app) // 权限指令 + setupRolesDirective(app) // 角色权限指令 + setupHighlightDirective(app) // 高亮指令 + setupRippleDirective(app) // 水波纹指令 +} diff --git a/src/enums/Billing.ts b/src/enums/Billing.ts new file mode 100644 index 0000000..9c53835 --- /dev/null +++ b/src/enums/Billing.ts @@ -0,0 +1,95 @@ +/** 账单状态枚举(后端:TakeoutSaaS.Domain.Tenants.Enums.TenantBillingStatus) */ +export enum TenantBillingStatus { + /** 待支付 */ + Pending = 0, + + /** 已支付 */ + Paid = 1, + + /** 已逾期 */ + Overdue = 2, + + /** 已取消 */ + Cancelled = 3 +} + +/** 账单类型枚举(后端:TakeoutSaaS.Domain.Tenants.Enums.BillingType) */ +export enum BillingType { + /** 订阅账单 */ + Subscription = 0, + + /** 配额包购买 */ + QuotaPurchase = 1, + + /** 手动创建 */ + Manual = 2, + + /** 续费账单 */ + Renewal = 3 +} + +/** 租户账单支付方式枚举(后端:TakeoutSaaS.Domain.Tenants.Enums.TenantPaymentMethod) */ +export enum TenantPaymentMethod { + /** 在线支付 */ + Online = 0, + + /** 银行转账 */ + BankTransfer = 1, + + /** 其他方式 */ + Other = 2 +} + +/** 支付流水支付方式枚举(后端:TakeoutSaaS.Domain.Payments.Enums.PaymentMethod) */ +export enum PaymentMethod { + /** 在线支付 */ + Online = 0, + + /** 银行转账 */ + BankTransfer = 1, + + /** 其他方式 */ + Other = 2 +} + +/** 租户账单支付状态枚举(后端:TakeoutSaaS.Domain.Tenants.Enums.TenantPaymentStatus) */ +export enum TenantPaymentStatus { + /** 待处理 */ + Pending = 0, + + /** 成功 */ + Success = 1, + + /** 失败 */ + Failed = 2, + + /** 已退款 */ + Refunded = 3 +} + +/** 支付流水状态枚举(后端:TakeoutSaaS.Domain.Payments.Enums.PaymentStatus) */ +export enum PaymentStatus { + /** 待处理 */ + Pending = 0, + + /** 成功 */ + Success = 1, + + /** 失败 */ + Failed = 2, + + /** 已退款 */ + Refunded = 3 +} + +/** 导出格式枚举(后端:TakeoutSaaS.Domain.Tenants.Enums.ExportFormat) */ +export enum ExportFormat { + /** Excel 格式 */ + Excel = 0, + + /** PDF 格式 */ + Pdf = 1, + + /** CSV 格式 */ + Csv = 2 +} diff --git a/src/enums/BusinessHourType.ts b/src/enums/BusinessHourType.ts new file mode 100644 index 0000000..ac030a6 --- /dev/null +++ b/src/enums/BusinessHourType.ts @@ -0,0 +1,11 @@ +/** 营业时段类型枚举(后端:TakeoutSaaS.Domain.Stores.Enums.BusinessHourType) */ +export enum BusinessHourType { + /** 正常营业时段。 */ + Normal = 0, + /** 仅预约时段。 */ + ReservationOnly = 1, + /** 自提/配送时段。 */ + PickupOrDelivery = 2, + /** 休息时段。 */ + Closed = 3 +} diff --git a/src/enums/Dictionary.ts b/src/enums/Dictionary.ts new file mode 100644 index 0000000..0c06eef --- /dev/null +++ b/src/enums/Dictionary.ts @@ -0,0 +1,32 @@ +/** 字典作用域枚举(后端:TakeoutSaaS.Domain.Dictionary.Enums.DictionaryScope) */ +export enum DictionaryScope { + /** 系统级字典 */ + System = 1, + + /** 业务级字典 */ + Business = 2 +} + +/** 导入冲突处理模式枚举 */ +export enum ConflictResolutionMode { + /** 跳过冲突项 */ + Skip = 1, + + /** 覆盖已有项 */ + Overwrite = 2, + + /** 追加新项 */ + Append = 3 +} + +/** 缓存失效操作类型枚举 */ +export enum CacheInvalidationOperation { + /** 创建操作 */ + Create = 1, + + /** 更新操作 */ + Update = 2, + + /** 删除操作 */ + Delete = 3 +} diff --git a/src/enums/MerchantStatus.ts b/src/enums/MerchantStatus.ts new file mode 100644 index 0000000..9f559d3 --- /dev/null +++ b/src/enums/MerchantStatus.ts @@ -0,0 +1,14 @@ +/** 商户状态枚举(后端:TakeoutSaaS.Domain.Merchants.Enums.MerchantStatus) */ +export enum MerchantStatus { + /** 待审核。 */ + Pending = 0, + + /** 审核通过。 */ + Approved = 1, + + /** 审核驳回。 */ + Rejected = 2, + + /** 业务冻结。 */ + Frozen = 3 +} diff --git a/src/enums/OperatingMode.ts b/src/enums/OperatingMode.ts new file mode 100644 index 0000000..f2ff6b9 --- /dev/null +++ b/src/enums/OperatingMode.ts @@ -0,0 +1,8 @@ +/** 经营模式枚举(后端:TakeoutSaaS.Domain.Common.Enums.OperatingMode) */ +export enum OperatingMode { + /** 同一主体。 */ + SameEntity = 1, + + /** 不同主体。 */ + DifferentEntity = 2 +} diff --git a/src/enums/OverrideType.ts b/src/enums/OverrideType.ts new file mode 100644 index 0000000..5427a30 --- /dev/null +++ b/src/enums/OverrideType.ts @@ -0,0 +1,9 @@ +/** 临时时段覆盖类型枚举(后端:TakeoutSaaS.Domain.Stores.Enums.OverrideType) */ +export enum OverrideType { + /** 闭店(歇业) */ + Closed = 0, + /** 临时营业 */ + TemporaryOpen = 1, + /** 调整营业时间 */ + ModifiedHours = 2 +} diff --git a/src/enums/PackagingFeeMode.ts b/src/enums/PackagingFeeMode.ts new file mode 100644 index 0000000..d0d957c --- /dev/null +++ b/src/enums/PackagingFeeMode.ts @@ -0,0 +1,7 @@ +/** 打包费模式枚举(后端:TakeoutSaaS.Domain.Stores.Enums.PackagingFeeMode) */ +export enum PackagingFeeMode { + /** 固定打包费。 */ + Fixed = 0, + /** 商品计费打包费。 */ + PerItem = 1 +} diff --git a/src/enums/ReviewAction.ts b/src/enums/ReviewAction.ts new file mode 100644 index 0000000..5fc876d --- /dev/null +++ b/src/enums/ReviewAction.ts @@ -0,0 +1,41 @@ +/** 商户审核动作枚举(后端:TakeoutSaaS.Domain.Merchants.Enums.MerchantAuditAction) */ +export enum ReviewAction { + /** 提交入驻申请或资料。 */ + ApplicationSubmitted = 0, + + /** 上传/更新证照。 */ + DocumentUploaded = 1, + + /** 证照审核。 */ + DocumentReviewed = 2, + + /** 合同创建或更新。 */ + ContractUpdated = 3, + + /** 合同状态变更。 */ + ContractStatusChanged = 4, + + /** 商户审核结果。 */ + MerchantReviewed = 5, + + /** 领取审核。 */ + ReviewClaimed = 6, + + /** 释放审核。 */ + ReviewReleased = 7, + + /** 审核通过。 */ + ReviewApproved = 8, + + /** 审核驳回。 */ + ReviewRejected = 9, + + /** 撤销审核。 */ + ReviewRevoked = 10, + + /** 关键信息变更进入待审核。 */ + ReviewPendingReApproval = 11, + + /** 强制接管审核。 */ + ReviewForceClaimed = 12 +} diff --git a/src/enums/StoreAuditAction.ts b/src/enums/StoreAuditAction.ts new file mode 100644 index 0000000..1419325 --- /dev/null +++ b/src/enums/StoreAuditAction.ts @@ -0,0 +1,17 @@ +/** 门店审核动作枚举(后端:TakeoutSaaS.Domain.Stores.Enums.StoreAuditAction) */ +export enum StoreAuditAction { + /** 提交审核。 */ + Submit = 0, + /** 重新提交。 */ + Resubmit = 1, + /** 审核通过。 */ + Approve = 2, + /** 审核驳回。 */ + Reject = 3, + /** 强制关闭。 */ + ForceClose = 4, + /** 解除关闭。 */ + Reopen = 5, + /** 自动激活。 */ + AutoActivate = 6 +} diff --git a/src/enums/StoreAuditStatus.ts b/src/enums/StoreAuditStatus.ts new file mode 100644 index 0000000..eb2d013 --- /dev/null +++ b/src/enums/StoreAuditStatus.ts @@ -0,0 +1,11 @@ +/** 门店审核状态枚举(后端:TakeoutSaaS.Domain.Stores.Enums.StoreAuditStatus) */ +export enum StoreAuditStatus { + /** 草稿/待提交。 */ + Draft = 0, + /** 审核中。 */ + Pending = 1, + /** 已激活。 */ + Activated = 2, + /** 已驳回。 */ + Rejected = 3 +} diff --git a/src/enums/StoreBusinessStatus.ts b/src/enums/StoreBusinessStatus.ts new file mode 100644 index 0000000..7545440 --- /dev/null +++ b/src/enums/StoreBusinessStatus.ts @@ -0,0 +1,9 @@ +/** 门店经营状态枚举(后端:TakeoutSaaS.Domain.Stores.Enums.StoreBusinessStatus) */ +export enum StoreBusinessStatus { + /** 营业中。 */ + Open = 0, + /** 休息中。 */ + Resting = 1, + /** 强制关闭。 */ + ForceClosed = 2 +} diff --git a/src/enums/StoreClosureReason.ts b/src/enums/StoreClosureReason.ts new file mode 100644 index 0000000..1d13e5f --- /dev/null +++ b/src/enums/StoreClosureReason.ts @@ -0,0 +1,17 @@ +/** 门店歇业原因枚举(后端:TakeoutSaaS.Domain.Stores.Enums.StoreClosureReason) */ +export enum StoreClosureReason { + /** 非营业时间。 */ + OutOfBusinessHours = 0, + /** 设备检修。 */ + EquipmentMaintenance = 1, + /** 老板休假。 */ + OwnerVacation = 2, + /** 食材告罄。 */ + OutOfStock = 3, + /** 暂停营业。 */ + TemporarilyClosed = 4, + /** 证照过期。 */ + LicenseExpired = 5, + /** 其他原因。 */ + Other = 99 +} diff --git a/src/enums/StoreOwnershipType.ts b/src/enums/StoreOwnershipType.ts new file mode 100644 index 0000000..3e75c1f --- /dev/null +++ b/src/enums/StoreOwnershipType.ts @@ -0,0 +1,7 @@ +/** 门店主体类型枚举(后端:TakeoutSaaS.Domain.Stores.Enums.StoreOwnershipType) */ +export enum StoreOwnershipType { + /** 同一主体(自营)。 */ + SameEntity = 0, + /** 不同主体(外部入驻)。 */ + DifferentEntity = 1 +} diff --git a/src/enums/StoreQualificationType.ts b/src/enums/StoreQualificationType.ts new file mode 100644 index 0000000..d12be55 --- /dev/null +++ b/src/enums/StoreQualificationType.ts @@ -0,0 +1,11 @@ +/** 门店资质类型枚举(后端:TakeoutSaaS.Domain.Stores.Enums.StoreQualificationType) */ +export enum StoreQualificationType { + /** 营业执照。 */ + BusinessLicense = 0, + /** 食品经营许可证。 */ + FoodServiceLicense = 1, + /** 门头实景照。 */ + StorefrontPhoto = 2, + /** 店内环境照。 */ + InteriorPhoto = 3 +} diff --git a/src/enums/StoreStatus.ts b/src/enums/StoreStatus.ts new file mode 100644 index 0000000..cf624eb --- /dev/null +++ b/src/enums/StoreStatus.ts @@ -0,0 +1,14 @@ +/** 门店状态枚举(后端:TakeoutSaaS.Domain.Stores.Enums.StoreStatus) */ +export enum StoreStatus { + /** 未开业或休眠。 */ + Closed = 0, + + /** 准备营业。 */ + Preparing = 1, + + /** 正常营业中。 */ + Operating = 2, + + /** 暂停营业。 */ + Suspended = 3 +} diff --git a/src/enums/SubscriptionStatus.ts b/src/enums/SubscriptionStatus.ts new file mode 100644 index 0000000..645c14c --- /dev/null +++ b/src/enums/SubscriptionStatus.ts @@ -0,0 +1,17 @@ +/** 订阅状态枚举(后端:TakeoutSaaS.Domain.Subscriptions.Enums.SubscriptionStatus) */ +export enum SubscriptionStatus { + /** 待激活 */ + Pending = 0, + + /** 生效中 */ + Active = 1, + + /** 宽限期 */ + GracePeriod = 2, + + /** 已取消 */ + Cancelled = 3, + + /** 已暂停 */ + Suspended = 4 +} diff --git a/src/enums/TenantPackageType.ts b/src/enums/TenantPackageType.ts new file mode 100644 index 0000000..2ad6b2a --- /dev/null +++ b/src/enums/TenantPackageType.ts @@ -0,0 +1,7 @@ +/** 租户套餐类型枚举(运行时常量,避免直接引用全局 Api 命名空间) */ +export enum TenantPackageTypeEnum { + Free = 0, + Standard = 1, + Professional = 2, + Enterprise = 3 +} diff --git a/src/enums/TenantStatus.ts b/src/enums/TenantStatus.ts new file mode 100644 index 0000000..eb6a3f9 --- /dev/null +++ b/src/enums/TenantStatus.ts @@ -0,0 +1,17 @@ +/** 租户服务状态枚举(后端:TakeoutSaaS.Domain.Tenants.Enums.TenantStatus) */ +export enum TenantStatus { + /** 已提交信息,等待审核。 */ + PendingReview = 0, + + /** 审核通过并正常运营。 */ + Active = 1, + + /** 因欠费或违规被暂时停用。 */ + Suspended = 2, + + /** 服务到期尚未续费。 */ + Expired = 3, + + /** 主动或被动注销,数据进入归档状态。 */ + Closed = 4 +} diff --git a/src/enums/TenantVerificationStatus.ts b/src/enums/TenantVerificationStatus.ts new file mode 100644 index 0000000..53c49bd --- /dev/null +++ b/src/enums/TenantVerificationStatus.ts @@ -0,0 +1,14 @@ +/** 租户实名认证状态枚举(后端:TakeoutSaaS.Domain.Tenants.Enums.TenantVerificationStatus) */ +export enum TenantVerificationStatus { + /** 草稿,未提交审核。 */ + Draft = 0, + + /** 已提交审核,等待运营处理。 */ + Pending = 1, + + /** 审核通过。 */ + Approved = 2, + + /** 审核驳回。 */ + Rejected = 3 +} diff --git a/src/enums/appEnum.ts b/src/enums/appEnum.ts new file mode 100644 index 0000000..a39c278 --- /dev/null +++ b/src/enums/appEnum.ts @@ -0,0 +1,81 @@ +/** + * 系统级别枚举定义模块 + * + * ## 主要功能 + * + * - 菜单类型枚举(左侧、顶部、混合、双栏) + * - 主题类型枚举(亮色、暗色、自动) + * - 菜单主题枚举(设计、亮色、暗色) + * - 语言类型枚举(中文、英文) + * - 容器宽度枚举(全屏、固定) + * - 菜单宽度枚举(收起宽度) + * + * @module enums/appEnum + * @author Art Design Pro Team + */ + +/** + * 菜单类型 + */ +export enum MenuTypeEnum { + /** 左侧菜单 */ + LEFT = 'left', + /** 顶部菜单 */ + TOP = 'top', + /** 顶部+左侧菜单 */ + TOP_LEFT = 'top-left', + /** 双栏菜单 */ + DUAL_MENU = 'dual-menu' +} + +/** + * 系统主题 + */ +export enum SystemThemeEnum { + /** 暗色主题 */ + DARK = 'dark', + /** 亮色主题 */ + LIGHT = 'light', + /** 自动主题(跟随系统) */ + AUTO = 'auto' +} + +/** + * 菜单主题 + */ +export enum MenuThemeEnum { + /** 暗色主题 */ + DARK = 'dark', + /** 亮色主题 */ + LIGHT = 'light', + /** 设计主题 */ + DESIGN = 'design' +} + +/** + * 菜单宽度 + */ +export enum MenuWidth { + /** 收起宽度 */ + CLOSE = '64px' +} + +/** + * 语言类型 + */ +export enum LanguageEnum { + /** 中文 */ + ZH = 'zh', + /** 英文 */ + EN = 'en' +} + +/** + * 容器宽度 + */ +export enum ContainerWidthEnum { + /** 全屏宽度 */ + FULL = '100%', + /** 固定宽度 */ + BOXED = '1200px' +} diff --git a/src/enums/formEnum.ts b/src/enums/formEnum.ts new file mode 100644 index 0000000..8e9b3b4 --- /dev/null +++ b/src/enums/formEnum.ts @@ -0,0 +1,24 @@ +/** + * 表单相关枚举定义模块 + * + * ## 主要功能 + * + * - 页面模式枚举(新增、编辑) + * - 表格尺寸枚举(默认、紧凑、宽松) + * + * @module enums/formEnum + * @author Art Design Pro Team + */ + +// 页面类型 +export enum PageModeEnum { + Add, // 新增 + Edit // 编辑 +} + +// 表格大小 +export enum TableSizeEnum { + DEFAULT = 'default', + SMALL = 'small', + LARGE = 'large' +} diff --git a/src/env.d.ts b/src/env.d.ts new file mode 100644 index 0000000..4401f21 --- /dev/null +++ b/src/env.d.ts @@ -0,0 +1,34 @@ +/// + +declare module 'nprogress' + +declare module 'crypto-js' + +declare module 'vue-img-cutter' + +declare module 'file-saver' + +declare module 'qrcode.vue' { + export type Level = 'L' | 'M' | 'Q' | 'H' + export type RenderAs = 'canvas' | 'svg' + export type GradientType = 'linear' | 'radial' + export interface ImageSettings { + src: string + height: number + width: number + excavate: boolean + } + export interface QRCodeProps { + value: string + size?: number + level?: Level + background?: string + foreground?: string + renderAs?: RenderAs + } + const QrcodeVue: any + export default QrcodeVue +} + +// 全局变量声明 +declare const __APP_VERSION__: string // 版本号 diff --git a/src/hooks/core/useAppMode.ts b/src/hooks/core/useAppMode.ts new file mode 100644 index 0000000..c39cd9e --- /dev/null +++ b/src/hooks/core/useAppMode.ts @@ -0,0 +1,45 @@ +/** + * useAppMode - 应用模式管理 + * + * 提供应用访问模式的判断和管理功能,支持前端和后端两种权限控制模式。 + * 根据环境变量 VITE_ACCESS_MODE 自动识别当前运行模式。 + * + * ## 主要功能 + * + * 1. 模式识别 - 自动识别前端模式或后端模式 + * 2. 前端模式 - 权限由前端路由配置控制,适合小型项目或演示环境 + * 3. 后端模式 - 权限由后端接口返回的菜单数据控制,适合企业级应用 + * 4. 响应式状态 - 提供响应式的模式判断,方便在组件中使用 + * + * @module useAppMode + * @author Art Design Pro Team + */ + +import { computed } from 'vue' + +export function useAppMode() { + // 获取访问模式配置 + const accessMode = import.meta.env.VITE_ACCESS_MODE + + /** + * 是否为前端控制模式 + * 前端模式:权限由前端路由配置控制 + */ + const isFrontendMode = computed(() => accessMode === 'frontend') + /** + * 是否为后端控制模式 + * 后端模式:权限由后端接口返回的菜单数据控制 + */ + const isBackendMode = computed(() => accessMode === 'backend') + + /** + * 当前应用模式 + */ + const currentMode = computed(() => accessMode) + + return { + isFrontendMode, + isBackendMode, + currentMode + } +} diff --git a/src/hooks/core/useAuth.ts b/src/hooks/core/useAuth.ts new file mode 100644 index 0000000..6e49415 --- /dev/null +++ b/src/hooks/core/useAuth.ts @@ -0,0 +1,78 @@ +/** + * useAuth - 权限验证管理 + * + * 提供统一的权限验证功能,支持前端和后端两种权限模式。 + * 用于控制页面按钮、操作等功能的显示和访问权限。 + * + * ## 主要功能 + * + * 1. 权限检查 - 检查用户是否拥有指定的权限标识 + * 2. 双模式支持 - 自动适配前端模式和后端模式的权限验证 + * 3. 前端模式 - 从用户信息中获取按钮权限列表(如 ['add', 'edit', 'delete']) + * 4. 后端模式 - 从路由 meta 配置中获取权限列表(如 [{ authMark: 'add' }]) + * + * ## 使用示例 + * + * ```typescript + * const { hasAuth } = useAuth() + * + * // 检查是否有新增权限 + * if (hasAuth('add')) { + * // 显示新增按钮 + * } + * + * // 在模板中使用 + * 编辑 + * 删除 + * ``` + * + * @module useAuth + * @author Art Design Pro Team + */ + +import { useRoute } from 'vue-router' +import { storeToRefs } from 'pinia' +import { useUserStore } from '@/store/modules/user' +import { useAppMode } from '@/hooks/core/useAppMode' +import type { AppRouteRecord } from '@/types/router' + +type AuthItem = NonNullable[number] + +const userStore = useUserStore() + +export const useAuth = () => { + const route = useRoute() + const { isFrontendMode } = useAppMode() + const { info } = storeToRefs(userStore) + + // 前端权限列表(使用后端返回的 permissions) + const frontendAuthList = info.value?.permissions ?? [] + + // 后端路由 meta 配置的权限列表(例如:[{ authMark: 'add' }]) + const backendAuthList: AuthItem[] = Array.isArray(route.meta.authList) + ? (route.meta.authList as AuthItem[]) + : [] + + /** + * 检查是否拥有某权限标识(前后端模式通用) + * @param auth 权限标识 + * @returns 是否有权限 + */ + const hasAuth = (auth: string): boolean => { + // 1. 前端模式:直接按用户权限列表判断 + if (isFrontendMode.value) { + return frontendAuthList.includes(auth) + } + + // 2. 后端模式:同时校验路由 meta 声明和用户真实权限 + const declaredInRoute = backendAuthList.some((item) => item?.authMark === auth) + const ownedByUser = frontendAuthList.includes(auth) + + // 路由未声明时,退化为只校验用户权限;声明了则需用户权限+声明同时满足 + return declaredInRoute ? ownedByUser : ownedByUser + } + + return { + hasAuth + } +} diff --git a/src/hooks/core/useCeremony.ts b/src/hooks/core/useCeremony.ts new file mode 100644 index 0000000..ead2630 --- /dev/null +++ b/src/hooks/core/useCeremony.ts @@ -0,0 +1,184 @@ +/** + * useCeremony - 节日庆祝管理 + * + * 提供节日烟花效果和祝福文本展示功能,为系统增添节日氛围。 + * 自动检测当前日期是否为节日,并在首次进入时播放烟花动画和显示祝福语。 + * + * ## 主要功能 + * + * 1. 节日检测 - 自动匹配当前日期与节日配置列表,支持单日和跨日期节日 + * 2. 烟花动画 - 播放节日烟花特效,支持自定义图片和触发次数 + * 3. 祝福文本 - 烟花结束后显示节日祝福文本 + * 4. 状态管理 - 记录烟花播放状态,避免重复播放 + * 5. 清理机制 - 提供清理方法,支持手动停止和重置 + * + * ## 使用示例 + * + * ```typescript + * // 在配置文件中定义节日 + * // 单日节日 + * { + * date: '2024-12-25', + * name: '圣诞节', + * image: christmasImage, + * count: 3 // 可选,不设置则使用默认值 3 次 + * scrollText: 'Merry Christmas!', + * } + * + * // 跨日期节日 + * { + * date: '2025-11-07', + * endDate: '2025-11-10', + * name: 'v3.0 测试阶段', + * image: '', + * count: 5 // 自定义烟花播放次数 + * scrollText: '系统 v3.0 测试阶段正式开启!', + * } + * ``` + * + * @module useCeremony + * @author Art Design Pro Team + */ + +import { useTimeoutFn, useIntervalFn, useDateFormat } from '@vueuse/core' +import { storeToRefs } from 'pinia' +import { computed } from 'vue' +import { useSettingStore } from '@/store/modules/setting' +import { mittBus } from '@/utils/sys' +import { festivalConfigList } from '@/config/modules/festival' + +/** + * 节日庆祝配置常量 + */ +const FESTIVAL_CONFIG = { + /** 初始延迟(毫秒) */ + INITIAL_DELAY: 300, + /** 烟花播放间隔(毫秒) */ + FIREWORK_INTERVAL: 1000, + /** 文本显示延迟(毫秒) */ + TEXT_DELAY: 2000, + /** 默认烟花播放次数 */ + DEFAULT_FIREWORKS_COUNT: 3 +} as const + +/** + * 节日庆祝功能 + * 提供节日烟花效果和祝福文本展示 + */ +export function useCeremony() { + const settingStore = useSettingStore() + const { holidayFireworksLoaded, isShowFireworks } = storeToRefs(settingStore) + + let fireworksInterval: { pause: () => void } | null = null + + /** + * 检查日期是否在节日范围内 + * @param currentDate 当前日期 + * @param festivalDate 节日开始日期 + * @param festivalEndDate 节日结束日期(可选) + */ + const isDateInRange = ( + currentDate: string, + festivalDate: string, + festivalEndDate?: string + ): boolean => { + if (!festivalEndDate) { + // 单日节日 + return currentDate === festivalDate + } + + // 跨日期节日 + const current = new Date(currentDate) + const start = new Date(festivalDate) + const end = new Date(festivalEndDate) + + return current >= start && current <= end + } + + /** + * 获取当前日期对应的节日数据 + */ + const currentFestivalData = computed(() => { + const currentDate = useDateFormat(new Date(), 'YYYY-MM-DD').value + return festivalConfigList.find((item) => isDateInRange(currentDate, item.date, item.endDate)) + }) + + /** + * 更新节日日期到 store + */ + const updateFestivalDate = () => { + settingStore.setFestivalDate(currentFestivalData.value?.date || '') + } + + /** + * 触发烟花效果 + */ + const triggerFirework = () => { + mittBus.emit('triggerFireworks', currentFestivalData.value?.image) + } + + /** + * 完成烟花效果后显示文本 + */ + const showFestivalText = () => { + settingStore.setholidayFireworksLoaded(true) + + useTimeoutFn(() => { + settingStore.setShowFestivalText(true) + updateFestivalDate() + }, FESTIVAL_CONFIG.TEXT_DELAY) + } + + /** + * 启动烟花循环 + */ + const startFireworksLoop = () => { + let playedCount = 0 + // 使用节日配置的播放次数,如果没有则使用默认值 + const count = currentFestivalData.value?.count ?? FESTIVAL_CONFIG.DEFAULT_FIREWORKS_COUNT + + const { pause } = useIntervalFn(() => { + triggerFirework() + playedCount++ + + if (playedCount >= count) { + pause() + showFestivalText() + } + }, FESTIVAL_CONFIG.FIREWORK_INTERVAL) + + fireworksInterval = { pause } + } + + /** + * 开启节日庆祝 + */ + const openFestival = () => { + if (!currentFestivalData.value || !isShowFireworks.value) { + return + } + + const { start } = useTimeoutFn(startFireworksLoop, FESTIVAL_CONFIG.INITIAL_DELAY) + start() + } + + /** + * 清理烟花效果 + */ + const cleanup = () => { + if (fireworksInterval) { + fireworksInterval.pause() + fireworksInterval = null + } + settingStore.setShowFestivalText(false) + updateFestivalDate() + } + + return { + openFestival, + cleanup, + holidayFireworksLoaded, + currentFestivalData, + isShowFireworks + } +} diff --git a/src/hooks/core/useChart.ts b/src/hooks/core/useChart.ts new file mode 100644 index 0000000..29ba1d1 --- /dev/null +++ b/src/hooks/core/useChart.ts @@ -0,0 +1,745 @@ +/** + * useChart - ECharts 图表管理 + * + * 提供完整的 ECharts 图表生命周期管理和配置能力,简化图表开发流程。 + * 自动处理图表初始化、更新、销毁、主题切换、响应式调整等复杂逻辑。 + * + * ## 核心功能 + * + * 1. 图表生命周期管理 - 自动处理初始化、更新、销毁,支持延迟加载和可见性检测 + * 2. 主题自动适配 - 响应系统主题变化,自动更新图表样式和配色 + * 3. 响应式调整 - 监听窗口大小、菜单展开等变化,自动调整图表尺寸 + * 4. 空状态处理 - 优雅的空数据展示,自动显示"暂无数据"提示 + * 5. 样式配置统一 - 提供坐标轴、图例、提示框等统一的样式配置方法 + * 6. 性能优化 - 防抖处理、样式缓存、requestAnimationFrame 优化 + * 7. 高级组件抽象 - useChartComponent 提供更高层次的图表组件封装 + * + * ## 使用示例 + * + * ```typescript + * // 基础用法 + * const { + * chartRef, + * initChart, + * updateChart, + * getAxisLineStyle, + * getTooltipStyle + * } = useChart() + * + * onMounted(() => { + * initChart({ + * xAxis: { type: 'category', data: ['Mon', 'Tue', 'Wed'] }, + * yAxis: { type: 'value' }, + * series: [{ data: [120, 200, 150], type: 'bar' }] + * }) + * }) + * + * // 高级用法 - 组件抽象 + * const chart = useChartComponent({ + * props, + * generateOptions: () => ({ + * // ECharts 配置 + * }), + * checkEmpty: () => data.value.length === 0, + * watchSources: [() => props.data] + * }) + * ``` + * + * @module useChart + * @author Art Design Pro Team + */ + +import { echarts, type EChartsOption } from '@/plugins/echarts' +import { storeToRefs } from 'pinia' +import { useSettingStore } from '@/store/modules/setting' +import { getCssVar } from '@/utils/ui' +import type { BaseChartProps, ChartThemeConfig, UseChartOptions } from '@/types/component/chart' + +// 图表主题配置 +export const useChartOps = (): ChartThemeConfig => ({ + /** */ + chartHeight: '16rem', + /** 字体大小 */ + fontSize: 13, + /** 字体颜色 */ + fontColor: '#999', + /** 主题颜色 */ + themeColor: getCssVar('--el-color-primary-light-1'), + /** 颜色组 */ + colors: [ + getCssVar('--el-color-primary-light-1'), + '#4ABEFF', + '#EDF2FF', + '#14DEBA', + '#FFAF20', + '#FA8A6C', + '#FFAF20' + ] +}) + +// 常量定义 +const RESIZE_DELAYS = [50, 100, 200, 350] as const +const MENU_RESIZE_DELAYS = [50, 100, 200] as const +const RESIZE_DEBOUNCE_DELAY = 100 + +export function useChart(options: UseChartOptions = {}) { + const { initOptions, initDelay = 0, threshold = 0.1, autoTheme = true } = options + + const settingStore = useSettingStore() + const { isDark, menuOpen, menuType } = storeToRefs(settingStore) + + const chartRef = ref() + let chart: echarts.ECharts | null = null + let intersectionObserver: IntersectionObserver | null = null + let pendingOptions: EChartsOption | null = null + let resizeTimeoutId: number | null = null + let resizeFrameId: number | null = null + let isDestroyed = false + let emptyStateDiv: HTMLElement | null = null + + // 清理定时器的统一方法 + const clearTimers = () => { + if (resizeTimeoutId) { + clearTimeout(resizeTimeoutId) + resizeTimeoutId = null + } + if (resizeFrameId) { + cancelAnimationFrame(resizeFrameId) + resizeFrameId = null + } + } + + // 使用 requestAnimationFrame 优化 resize 处理 + const requestAnimationResize = () => { + if (resizeFrameId) { + cancelAnimationFrame(resizeFrameId) + } + resizeFrameId = requestAnimationFrame(() => { + handleResize() + resizeFrameId = null + }) + } + + // 防抖的resize处理(用于窗口resize事件) + const debouncedResize = () => { + if (resizeTimeoutId) { + clearTimeout(resizeTimeoutId) + } + resizeTimeoutId = window.setTimeout(() => { + requestAnimationResize() + resizeTimeoutId = null + }, RESIZE_DEBOUNCE_DELAY) + } + + // 多延迟resize处理 - 统一方法 + const multiDelayResize = (delays: readonly number[]) => { + // 立即调用一次,快速响应 + nextTick(requestAnimationResize) + + // 使用延迟时间,确保图表正确适应变化 + delays.forEach((delay) => { + setTimeout(requestAnimationResize, delay) + }) + } + + // 收缩菜单时,重新计算图表大小(仅在图表存在时监听) + let menuOpenStopHandle: (() => void) | null = null + let menuTypeStopHandle: (() => void) | null = null + + const setupMenuWatchers = () => { + menuOpenStopHandle = watch(menuOpen, () => multiDelayResize(RESIZE_DELAYS)) + menuTypeStopHandle = watch(menuType, () => { + nextTick(requestAnimationResize) + setTimeout(() => multiDelayResize(MENU_RESIZE_DELAYS), 0) + }) + } + + const cleanupMenuWatchers = () => { + menuOpenStopHandle?.() + menuTypeStopHandle?.() + menuOpenStopHandle = null + menuTypeStopHandle = null + } + + // 主题变化时重新设置图表选项 + let themeStopHandle: (() => void) | null = null + + const setupThemeWatcher = () => { + if (autoTheme) { + themeStopHandle = watch(isDark, () => { + // 更新空状态样式 + emptyStateManager.updateStyle() + + if (chart && !isDestroyed) { + // 使用 requestAnimationFrame 优化主题更新 + requestAnimationFrame(() => { + if (chart && !isDestroyed) { + const currentOptions = chart.getOption() + if (currentOptions) { + updateChart(currentOptions as EChartsOption) + } + } + }) + } + }) + } + } + + const cleanupThemeWatcher = () => { + themeStopHandle?.() + themeStopHandle = null + } + + // 样式生成器 - 统一的样式配置 + const createLineStyle = (color: string, width = 1, type?: 'solid' | 'dashed') => ({ + color, + width, + ...(type && { type }) + }) + + // 缓存样式配置以减少重复计算 + const styleCache = { + axisLine: null as any, + splitLine: null as any, + axisLabel: null as any, + lastDarkValue: isDark.value + } + + const clearStyleCache = () => { + styleCache.axisLine = null + styleCache.splitLine = null + styleCache.axisLabel = null + styleCache.lastDarkValue = isDark.value + } + + // 坐标轴线样式 + const getAxisLineStyle = (show: boolean = true) => { + if (styleCache.lastDarkValue !== isDark.value) { + clearStyleCache() + } + if (!styleCache.axisLine) { + styleCache.axisLine = { + show, + lineStyle: createLineStyle(isDark.value ? '#444' : '#EDEDED') + } + } + return styleCache.axisLine + } + + // 分割线样式 + const getSplitLineStyle = (show: boolean = true) => { + if (styleCache.lastDarkValue !== isDark.value) { + clearStyleCache() + } + if (!styleCache.splitLine) { + styleCache.splitLine = { + show, + lineStyle: createLineStyle(isDark.value ? '#444' : '#EDEDED', 1, 'dashed') + } + } + return styleCache.splitLine + } + + // 坐标轴标签样式 + const getAxisLabelStyle = (show: boolean = true) => { + if (styleCache.lastDarkValue !== isDark.value) { + clearStyleCache() + } + if (!styleCache.axisLabel) { + const { fontColor, fontSize } = useChartOps() + styleCache.axisLabel = { + show, + color: fontColor, + fontSize + } + } + return styleCache.axisLabel + } + + // 坐标轴刻度样式(静态配置,无需缓存) + const getAxisTickStyle = () => ({ + show: false + }) + + // 获取动画配置 + const getAnimationConfig = (animationDelay: number = 50, animationDuration: number = 1500) => ({ + animationDelay: (idx: number) => idx * animationDelay + 200, + animationDuration: (idx: number) => animationDuration - idx * 50, + animationEasing: 'quarticOut' as const + }) + + // 获取统一的 tooltip 配置 + const getTooltipStyle = (trigger: 'item' | 'axis' = 'axis', customOptions: any = {}) => ({ + trigger, + backgroundColor: isDark.value ? 'rgba(0, 0, 0, 0.8)' : 'rgba(255, 255, 255, 0.9)', + borderColor: isDark.value ? '#333' : '#ddd', + borderWidth: 1, + textStyle: { + color: isDark.value ? '#fff' : '#333' + }, + ...customOptions + }) + + // 获取统一的图例配置 + const getLegendStyle = ( + position: 'bottom' | 'top' | 'left' | 'right' = 'bottom', + customOptions: any = {} + ) => { + const baseConfig = { + textStyle: { + color: isDark.value ? '#fff' : '#333' + }, + itemWidth: 12, + itemHeight: 12, + itemGap: 20, + ...customOptions + } + + // 根据位置设置不同的配置 + switch (position) { + case 'bottom': + return { + ...baseConfig, + bottom: 0, + left: 'center', + orient: 'horizontal', + icon: 'roundRect' + } + case 'top': + return { + ...baseConfig, + top: 0, + left: 'center', + orient: 'horizontal', + icon: 'roundRect' + } + case 'left': + return { + ...baseConfig, + left: 0, + top: 'center', + orient: 'vertical', + icon: 'roundRect' + } + case 'right': + return { + ...baseConfig, + right: 0, + top: 'center', + orient: 'vertical', + icon: 'roundRect' + } + default: + return baseConfig + } + } + + // 根据图例位置计算 grid 配置 + const getGridWithLegend = ( + showLegend: boolean, + legendPosition: 'bottom' | 'top' | 'left' | 'right' = 'bottom', + baseGrid: any = {} + ) => { + const defaultGrid = { + top: 15, + right: 15, + bottom: 8, + left: 0, + containLabel: true, + ...baseGrid + } + + if (!showLegend) { + return defaultGrid + } + + // 根据图例位置调整 grid + switch (legendPosition) { + case 'bottom': + return { + ...defaultGrid, + bottom: 40 + } + case 'top': + return { + ...defaultGrid, + top: 40 + } + case 'left': + return { + ...defaultGrid, + left: 120 + } + case 'right': + return { + ...defaultGrid, + right: 120 + } + default: + return defaultGrid + } + } + + // 创建IntersectionObserver + const createIntersectionObserver = () => { + if (intersectionObserver || !chartRef.value) return + + intersectionObserver = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting && pendingOptions && !isDestroyed) { + // 使用 requestAnimationFrame 确保在下一帧初始化图表 + requestAnimationFrame(() => { + if (!isDestroyed && pendingOptions) { + try { + // 元素变为可见,初始化图表 + if (!chart) { + chart = echarts.init(entry.target as HTMLElement) + } + + // 触发自定义事件,让组件处理动画逻辑 + const event = new CustomEvent('chartVisible', { + detail: { options: pendingOptions } + }) + entry.target.dispatchEvent(event) + + pendingOptions = null + cleanupIntersectionObserver() + } catch (error) { + console.error('图表初始化失败:', error) + } + } + }) + } + }) + }, + { threshold } + ) + + intersectionObserver.observe(chartRef.value) + } + + // 清理IntersectionObserver + const cleanupIntersectionObserver = () => { + if (intersectionObserver) { + intersectionObserver.disconnect() + intersectionObserver = null + } + } + + // 检查容器是否可见 + const isContainerVisible = (element: HTMLElement): boolean => { + const rect = element.getBoundingClientRect() + return rect.width > 0 && rect.height > 0 && rect.top < window.innerHeight && rect.bottom > 0 + } + + // 图表初始化核心逻辑 + const performChartInit = (options: EChartsOption) => { + if (!chart && chartRef.value && !isDestroyed) { + chart = echarts.init(chartRef.value) + // 图表创建后立即设置监听器 + setupMenuWatchers() + setupThemeWatcher() + } + if (chart && !isDestroyed) { + chart.setOption(options) + pendingOptions = null + } + } + + // 空状态管理器 + const emptyStateManager = { + create: () => { + if (!chartRef.value || emptyStateDiv) return + + emptyStateDiv = document.createElement('div') + emptyStateDiv.style.cssText = ` + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-size: 12px; + color: ${isDark.value ? '#555555' : '#B3B2B2'}; + background: transparent; + z-index: 10; + ` + emptyStateDiv.innerHTML = `暂无数据` + + // 确保父容器有相对定位 + if ( + chartRef.value.style.position !== 'relative' && + chartRef.value.style.position !== 'absolute' + ) { + chartRef.value.style.position = 'relative' + } + + chartRef.value.appendChild(emptyStateDiv) + }, + + remove: () => { + if (emptyStateDiv && chartRef.value) { + chartRef.value.removeChild(emptyStateDiv) + emptyStateDiv = null + } + }, + + updateStyle: () => { + if (emptyStateDiv) { + emptyStateDiv.style.color = isDark.value ? '#666' : '#999' + } + } + } + + // 初始化图表 + const initChart = (options: EChartsOption = {}, isEmpty: boolean = false) => { + if (!chartRef.value || isDestroyed) return + + const mergedOptions = { ...initOptions, ...options } + + try { + if (isEmpty) { + // 处理空数据情况 - 显示自定义空状态div + if (chart) { + chart.clear() + } + emptyStateManager.create() + return + } else { + // 有数据时移除空状态div + emptyStateManager.remove() + } + + if (isContainerVisible(chartRef.value)) { + // 容器可见,正常初始化 + if (initDelay > 0) { + setTimeout(() => performChartInit(mergedOptions), initDelay) + } else { + performChartInit(mergedOptions) + } + } else { + // 容器不可见,保存选项并设置监听器 + pendingOptions = mergedOptions + createIntersectionObserver() + } + } catch (error) { + console.error('图表初始化失败:', error) + } + } + + // 更新图表 + const updateChart = (options: EChartsOption) => { + if (isDestroyed) return + + try { + if (!chart) { + // 如果图表不存在,先初始化 + initChart(options) + return + } + chart.setOption(options) + } catch (error) { + console.error('图表更新失败:', error) + } + } + + // 处理窗口大小变化 + const handleResize = () => { + if (chart && !isDestroyed) { + try { + chart.resize() + } catch (error) { + console.error('图表resize失败:', error) + } + } + } + + // 销毁图表 + const destroyChart = () => { + isDestroyed = true + + if (chart) { + try { + chart.dispose() + } catch (error) { + console.error('图表销毁失败:', error) + } finally { + chart = null + } + } + + // 清理所有监听器和资源 + cleanupMenuWatchers() + cleanupThemeWatcher() + emptyStateManager.remove() + cleanupIntersectionObserver() + clearTimers() + clearStyleCache() + pendingOptions = null + } + + // 获取图表实例 + const getChartInstance = () => chart + + // 获取图表是否已初始化 + const isChartInitialized = () => chart !== null + + onMounted(() => { + window.addEventListener('resize', debouncedResize) + }) + + onBeforeUnmount(() => { + window.removeEventListener('resize', debouncedResize) + }) + + onUnmounted(() => { + destroyChart() + }) + + return { + isDark, + chartRef, + initChart, + updateChart, + handleResize, + destroyChart, + getChartInstance, + isChartInitialized, + emptyStateManager, + getAxisLineStyle, + getSplitLineStyle, + getAxisLabelStyle, + getAxisTickStyle, + getAnimationConfig, + getTooltipStyle, + getLegendStyle, + useChartOps, + getGridWithLegend + } +} + +// 高级图表组件抽象 +interface UseChartComponentOptions { + /** Props响应式对象 */ + props: T + /** 图表配置生成函数 */ + generateOptions: () => EChartsOption + /** 空数据检查函数 */ + checkEmpty?: () => boolean + /** 自定义监听的响应式数据 */ + watchSources?: (() => any)[] + /** 自定义可视事件处理 */ + onVisible?: () => void + /** useChart选项 */ + chartOptions?: UseChartOptions +} + +export function useChartComponent(options: UseChartComponentOptions) { + const { + props, + generateOptions, + checkEmpty, + watchSources = [], + onVisible, + chartOptions = {} + } = options + + const chart = useChart(chartOptions) + const { chartRef, initChart, isDark, emptyStateManager } = chart + + // 检查是否为空数据 + const isEmpty = computed(() => { + if (props.isEmpty) return true + if (checkEmpty) return checkEmpty() + return false + }) + + // 更新图表 + const updateChart = () => { + nextTick(() => { + if (isEmpty.value) { + // 处理空数据情况 - 显示自定义空状态div + if (chart.getChartInstance()) { + chart.getChartInstance()?.clear() + } + emptyStateManager.create() + } else { + // 有数据时移除空状态div并初始化图表 + emptyStateManager.remove() + initChart(generateOptions()) + } + }) + } + + // 处理图表进入可视区域时的逻辑 + const handleChartVisible = () => { + if (onVisible) { + onVisible() + } else { + updateChart() + } + } + + // 存储监听器停止函数 + const stopHandles: (() => void)[] = [] + + // 设置数据监听 + const setupWatchers = () => { + // 监听自定义数据源 + if (watchSources.length > 0) { + const stopHandle = watch(watchSources, updateChart, { deep: true }) + stopHandles.push(stopHandle) + } + + // 监听主题变化 + const themeStopHandle = watch(isDark, () => { + emptyStateManager.updateStyle() + updateChart() + }) + stopHandles.push(themeStopHandle) + } + + // 清理所有监听器 + const cleanupWatchers = () => { + stopHandles.forEach((stop) => stop()) + stopHandles.length = 0 + } + + // 设置生命周期 + const setupLifecycle = () => { + onMounted(() => { + updateChart() + + // 监听图表可见事件 + if (chartRef.value) { + chartRef.value.addEventListener('chartVisible', handleChartVisible) + } + }) + + onBeforeUnmount(() => { + // 清理事件监听器 + if (chartRef.value) { + chartRef.value.removeEventListener('chartVisible', handleChartVisible) + } + // 清理所有监听器 + cleanupWatchers() + // 清理空状态div + emptyStateManager.remove() + }) + } + + // 初始化 + setupWatchers() + setupLifecycle() + + return { + ...chart, + isEmpty, + updateChart, + handleChartVisible + } +} diff --git a/src/hooks/core/useCommon.ts b/src/hooks/core/useCommon.ts new file mode 100644 index 0000000..c936854 --- /dev/null +++ b/src/hooks/core/useCommon.ts @@ -0,0 +1,87 @@ +/** + * useCommon - 通用功能集合 + * + * 提供常用的页面操作功能,包括页面刷新、滚动控制、路径获取等。 + * 这些功能在多个页面和组件中都会用到,统一封装便于复用。 + * + * ## 主要功能 + * + * 1. 首页路径 - 获取系统配置的首页路径 + * 2. 页面刷新 - 刷新当前页面内容 + * 3. 滚动控制 - 提供多种滚动到顶部和指定位置的方法 + * 4. 平滑滚动 - 支持平滑滚动动画效果 + * + * @module useCommon + * @author Art Design Pro Team + */ + +import { computed } from 'vue' +import { useMenuStore } from '@/store/modules/menu' +import { useSettingStore } from '@/store/modules/setting' + +export function useCommon() { + const menuStore = useMenuStore() + const settingStore = useSettingStore() + + /** + * 首页路径 + * 从菜单 store 中获取配置的首页路径 + */ + const homePath = computed(() => menuStore.getHomePath()) + + /** + * 刷新当前页面 + * 通过切换 setting store 中的 refresh 状态触发页面重新渲染 + */ + const refresh = () => { + settingStore.reload() + } + + /** + * 滚动到页面顶部 + * 查找主内容区域并将其滚动位置重置为顶部 + */ + const scrollToTop = () => { + const scrollContainer = document.getElementById('app-main') + if (scrollContainer) { + scrollContainer.scrollTop = 0 + } + } + + /** + * 平滑滚动到页面顶部 + * 使用 smooth 行为实现平滑滚动效果 + */ + const smoothScrollToTop = () => { + const scrollContainer = document.getElementById('app-main') + if (scrollContainer) { + scrollContainer.scrollTo({ + top: 0, + behavior: 'smooth' + }) + } + } + + /** + * 滚动到指定位置 + * @param top 目标滚动位置(像素) + * @param smooth 是否使用平滑滚动 + */ + const scrollTo = (top: number, smooth: boolean = false) => { + const scrollContainer = document.getElementById('app-main') + if (scrollContainer) { + scrollContainer.scrollTo({ + top, + behavior: smooth ? 'smooth' : 'auto' + }) + } + } + + return { + homePath, + refresh, + scrollTo, + scrollToTop, + smoothScrollToTop + } +} diff --git a/src/hooks/core/useFastEnter.ts b/src/hooks/core/useFastEnter.ts new file mode 100644 index 0000000..555eb65 --- /dev/null +++ b/src/hooks/core/useFastEnter.ts @@ -0,0 +1,55 @@ +/** + * useFastEnter - 快速入口管理 + * + * 管理顶部栏的快速入口功能,提供应用列表和快速链接的配置和过滤。 + * 支持动态启用/禁用、自定义排序、响应式宽度控制等功能。 + * + * ## 主要功能 + * + * 1. 应用列表管理 - 获取启用的应用列表,自动按排序权重排序 + * 2. 快速链接管理 - 获取启用的快速链接,支持自定义排序 + * 3. 响应式配置 - 所有配置自动响应变化,无需手动更新 + * 4. 宽度控制 - 提供最小显示宽度配置,支持响应式布局 + * + * @module useFastEnter + * @author Art Design Pro Team + */ + +import { computed } from 'vue' +import appConfig from '@/config' +import type { FastEnterApplication, FastEnterQuickLink } from '@/types/config' + +export function useFastEnter() { + // 获取快速入口配置 + const fastEnterConfig = computed(() => appConfig.fastEnter) + + // 获取启用的应用列表(按排序权重排序) + const enabledApplications = computed(() => { + if (!fastEnterConfig.value?.applications) return [] + + return fastEnterConfig.value.applications + .filter((app) => app.enabled !== false) + .sort((a, b) => (a.order || 0) - (b.order || 0)) + }) + + // 获取启用的快速链接(按排序权重排序) + const enabledQuickLinks = computed(() => { + if (!fastEnterConfig.value?.quickLinks) return [] + + return fastEnterConfig.value.quickLinks + .filter((link) => link.enabled !== false) + .sort((a, b) => (a.order || 0) - (b.order || 0)) + }) + + // 获取最小显示宽度 + const minWidth = computed(() => { + return fastEnterConfig.value?.minWidth || 1200 + }) + + return { + fastEnterConfig, + enabledApplications, + enabledQuickLinks, + minWidth + } +} diff --git a/src/hooks/core/useHeaderBar.ts b/src/hooks/core/useHeaderBar.ts new file mode 100644 index 0000000..be10712 --- /dev/null +++ b/src/hooks/core/useHeaderBar.ts @@ -0,0 +1,201 @@ +/** + * useHeaderBar - 顶部栏功能管理 + * + * 统一管理顶部栏各个功能模块的显示状态和配置信息。 + * 提供灵活的功能开关控制,支持动态显示/隐藏顶部栏的各个功能按钮。 + * + * ## 主要功能 + * + * 1. 功能开关控制 - 统一管理菜单按钮、刷新按钮、快速入口等功能的显示状态 + * 2. 配置信息获取 - 获取各个功能模块的详细配置信息 + * 3. 功能列表查询 - 快速获取所有启用或禁用的功能列表 + * 4. 响应式状态 - 所有状态自动响应配置和 store 变化 + * + * @module useHeaderBar + * @author Art Design Pro Team + */ + +import { computed } from 'vue' +import { storeToRefs } from 'pinia' +import { useSettingStore } from '@/store/modules/setting' +import { headerBarConfig } from '@/config/modules/headerBar' +import { HeaderBarFeatureConfig } from '@/types' + +/** + * 顶部栏功能管理 + * @returns 顶部栏功能相关的状态和方法 + */ +export function useHeaderBar() { + const settingStore = useSettingStore() + + // 获取顶部栏配置 + const headerBarConfigRef = computed(() => headerBarConfig) + + // 从store中获取相关状态 + const { showMenuButton, showFastEnter, showRefreshButton, showCrumbs, showLanguage } = + storeToRefs(settingStore) + + /** + * 检查特定功能是否启用 + * @param feature 功能名称 + * @returns 是否启用 + */ + const isFeatureEnabled = (feature: keyof HeaderBarFeatureConfig): boolean => { + return headerBarConfigRef.value[feature]?.enabled ?? false + } + + /** + * 获取功能配置信息 + * @param feature 功能名称 + * @returns 功能配置信息 + */ + const getFeatureConfig = (feature: keyof HeaderBarFeatureConfig) => { + return headerBarConfigRef.value[feature] + } + + // 检查菜单按钮是否显示 + const shouldShowMenuButton = computed(() => { + return isFeatureEnabled('menuButton') && showMenuButton.value + }) + + // 检查刷新按钮是否显示 + const shouldShowRefreshButton = computed(() => { + return isFeatureEnabled('refreshButton') && showRefreshButton.value + }) + + // 检查快速入口是否显示 + const shouldShowFastEnter = computed(() => { + return isFeatureEnabled('fastEnter') && showFastEnter.value + }) + + // 检查面包屑是否显示 + const shouldShowBreadcrumb = computed(() => { + return isFeatureEnabled('breadcrumb') && showCrumbs.value + }) + + // 检查全局搜索是否显示 + const shouldShowGlobalSearch = computed(() => { + return isFeatureEnabled('globalSearch') + }) + + // 检查全屏按钮是否显示 + const shouldShowFullscreen = computed(() => { + return isFeatureEnabled('fullscreen') + }) + + // 检查通知中心是否显示 + const shouldShowNotification = computed(() => { + return isFeatureEnabled('notification') + }) + + // 检查聊天功能是否显示 + const shouldShowChat = computed(() => { + return isFeatureEnabled('chat') + }) + + // 检查语言切换是否显示 + const shouldShowLanguage = computed(() => { + return isFeatureEnabled('language') && showLanguage.value + }) + + // 检查设置面板是否显示 + const shouldShowSettings = computed(() => { + return isFeatureEnabled('settings') + }) + + // 检查主题切换是否显示 + const shouldShowThemeToggle = computed(() => { + return isFeatureEnabled('themeToggle') + }) + + // 获取快速入口的最小宽度 + const fastEnterMinWidth = computed(() => { + const config = getFeatureConfig('fastEnter') + return (config as any)?.minWidth || 1200 + }) + + /** + * 检查功能是否启用(别名) + * @param feature 功能名称 + * @returns 是否启用 + */ + const isFeatureActive = (feature: keyof HeaderBarFeatureConfig): boolean => { + return isFeatureEnabled(feature) + } + + /** + * 获取功能配置(别名) + * @param feature 功能名称 + * @returns 功能配置 + */ + const getFeatureInfo = (feature: keyof HeaderBarFeatureConfig) => { + return getFeatureConfig(feature) + } + + /** + * 获取所有启用的功能列表 + * @returns 启用的功能名称数组 + */ + const getEnabledFeatures = (): (keyof HeaderBarFeatureConfig)[] => { + return Object.keys(headerBarConfigRef.value).filter( + (key) => headerBarConfigRef.value[key as keyof HeaderBarFeatureConfig]?.enabled + ) as (keyof HeaderBarFeatureConfig)[] + } + + /** + * 获取所有禁用的功能列表 + * @returns 禁用的功能名称数组 + */ + const getDisabledFeatures = (): (keyof HeaderBarFeatureConfig)[] => { + return Object.keys(headerBarConfigRef.value).filter( + (key) => !headerBarConfigRef.value[key as keyof HeaderBarFeatureConfig]?.enabled + ) as (keyof HeaderBarFeatureConfig)[] + } + + /** + * 获取所有启用的功能(别名) + * @returns 启用的功能列表 + */ + const getActiveFeatures = () => { + return getEnabledFeatures() + } + + /** + * 获取所有禁用的功能(别名) + * @returns 禁用的功能列表 + */ + const getInactiveFeatures = () => { + return getDisabledFeatures() + } + + return { + // 配置 + headerBarConfig: headerBarConfigRef, + + // 显示状态计算属性 + shouldShowMenuButton, // 是否显示菜单按钮 + shouldShowRefreshButton, // 是否显示刷新按钮 + shouldShowFastEnter, // 是否显示快速入口 + shouldShowBreadcrumb, // 是否显示面包屑 + shouldShowGlobalSearch, // 是否显示全局搜索 + shouldShowFullscreen, // 是否显示全屏按钮 + shouldShowNotification, // 是否显示通知中心 + shouldShowChat, // 是否显示聊天功能 + shouldShowLanguage, // 是否显示语言切换 + shouldShowSettings, // 是否显示设置面板 + shouldShowThemeToggle, // 是否显示主题切换 + + // 配置相关 + fastEnterMinWidth, // 快速入口最小宽度 + + // 方法 + isFeatureEnabled, // 检查功能是否启用 + isFeatureActive, // 检查功能是否启用(别名) + getFeatureConfig, // 获取功能配置 + getFeatureInfo, // 获取功能配置(别名) + getEnabledFeatures, // 获取所有启用的功能 + getDisabledFeatures, // 获取所有禁用的功能 + getActiveFeatures, // 获取所有启用的功能(别名) + getInactiveFeatures // 获取所有禁用的功能(别名) + } +} diff --git a/src/hooks/core/useLayoutHeight.ts b/src/hooks/core/useLayoutHeight.ts new file mode 100644 index 0000000..4b1171a --- /dev/null +++ b/src/hooks/core/useLayoutHeight.ts @@ -0,0 +1,148 @@ +/** + * useLayoutHeight - 页面布局高度管理 + * + * 自动计算和管理页面内容区域的高度,确保内容区域能够正确填充剩余空间。 + * 监听头部元素高度变化,动态调整内容区域高度,避免出现滚动条或布局错乱。 + * + * ## 主要功能 + * + * 1. 动态高度计算 - 根据头部元素高度自动计算内容区域高度 + * 2. 响应式监听 - 自动监听元素尺寸变化并更新高度 + * 3. CSS 变量同步 - 自动更新 CSS 变量,方便全局使用 + * 4. 灵活配置 - 支持自定义间距、CSS 变量名等 + * 5. 自动查找模式 - 提供通过 ID 自动查找元素的便捷方式 + * + * @module useLayoutHeight + * @author Art Design Pro Team + */ + +import { ref, computed, watch, onMounted } from 'vue' +import { useElementSize } from '@vueuse/core' + +/** + * 页面容器高度配置 + */ +interface LayoutHeightOptions { + /** 额外的间距(默认 15px) */ + extraSpacing?: number + /** 是否自动更新 CSS 变量(默认 true) */ + updateCssVar?: boolean + /** CSS 变量名称(默认 '--art-full-height') */ + cssVarName?: string +} + +export function useLayoutHeight(options: LayoutHeightOptions = {}) { + const { extraSpacing = 15, updateCssVar = true, cssVarName = '--art-full-height' } = options + + // 元素引用 + const headerRef = ref() + const contentHeaderRef = ref() + + // 使用 VueUse 自动监听元素尺寸变化 + const { height: headerHeight } = useElementSize(headerRef) + const { height: contentHeaderHeight } = useElementSize(contentHeaderRef) + + // 计算容器最小高度(响应式) + const containerMinHeight = computed(() => { + const totalHeight = headerHeight.value + contentHeaderHeight.value + extraSpacing + return `calc(100vh - ${totalHeight}px)` + }) + + if (updateCssVar) { + watch( + containerMinHeight, + (newHeight) => { + requestAnimationFrame(() => { + document.documentElement.style.setProperty(cssVarName, newHeight) + }) + }, + { immediate: true } + ) + } + + return { + /** 容器最小高度(响应式) */ + containerMinHeight, + /** 头部元素引用 */ + headerRef, + /** 内容头部元素引用 */ + contentHeaderRef, + /** 头部高度(响应式) */ + headerHeight, + /** 内容头部高度(响应式) */ + contentHeaderHeight + } +} + +/** + * 通过 ID 自动查找元素的布局高度管理 + * 适用于无法直接获取元素引用的场景 + * + * @param headerIds 头部元素的 ID 数组 + * @param options 配置选项 + * + * ``` + */ +export function useAutoLayoutHeight( + headerIds: string[] = ['app-header', 'app-content-header'], + options: LayoutHeightOptions = {} +) { + const { extraSpacing = 15, updateCssVar = true, cssVarName = '--art-full-height' } = options + + // 创建元素引用 + const headerRef = ref() + const contentHeaderRef = ref() + + // 使用 VueUse 自动监听元素尺寸变化 + const { height: headerHeight } = useElementSize(headerRef) + const { height: contentHeaderHeight } = useElementSize(contentHeaderRef) + + // 计算容器最小高度(响应式) + const containerMinHeight = computed(() => { + const totalHeight = headerHeight.value + contentHeaderHeight.value + extraSpacing + return `calc(100vh - ${totalHeight}px)` + }) + + if (updateCssVar) { + watch( + containerMinHeight, + (newHeight) => { + requestAnimationFrame(() => { + document.documentElement.style.setProperty(cssVarName, newHeight) + }) + }, + { immediate: true } + ) + } + + // 在 DOM 挂载后查找元素 + onMounted(() => { + if (typeof document !== 'undefined') { + // 使用 nextTick 确保 DOM 完全渲染 + requestAnimationFrame(() => { + const header = document.getElementById(headerIds[0]) + const contentHeader = document.getElementById(headerIds[1]) + + if (header) { + headerRef.value = header + } + if (contentHeader) { + contentHeaderRef.value = contentHeader + } + }) + } + }) + + return { + /** 容器最小高度(响应式) */ + containerMinHeight, + /** 头部元素引用 */ + headerRef, + /** 内容头部元素引用 */ + contentHeaderRef, + /** 头部高度(响应式) */ + headerHeight, + /** 内容头部高度(响应式) */ + contentHeaderHeight + } +} diff --git a/src/hooks/core/useTable.ts b/src/hooks/core/useTable.ts new file mode 100644 index 0000000..bf06c4d --- /dev/null +++ b/src/hooks/core/useTable.ts @@ -0,0 +1,737 @@ +/** + * useTable - 企业级表格数据管理方案 + * + * 功能完整的表格数据管理解决方案,专为后台管理系统设计。 + * 封装了表格开发中的所有常见需求,让你专注于业务逻辑。 + * + * ## 主要功能 + * + * 1. 数据管理 - 自动处理 API 请求、响应转换、加载状态和错误处理 + * 2. 分页控制 - 自动同步分页状态、移动端适配、智能页码边界处理 + * 3. 搜索功能 - 防抖搜索优化、参数管理、一键重置、参数过滤 + * 4. 缓存系统 - 智能请求缓存、多种清理策略、自动过期管理、统计信息 + * 5. 刷新策略 - 提供 5 种刷新方法适配不同业务场景(新增/更新/删除/手动/定时) + * 6. 列配置管理 - 动态显示/隐藏列、列排序、配置持久化、批量操作(可选) + * + * @module useTable + * @author Art Design Pro Team + */ + +import { ref, reactive, computed, onMounted, onUnmounted, nextTick, readonly } from 'vue' +import { useWindowSize } from '@vueuse/core' +import { useTableColumns } from './useTableColumns' +import type { ColumnOption } from '@/types/component' +import { + TableCache, + CacheInvalidationStrategy, + type ApiResponse +} from '../../utils/table/tableCache' +import { + type TableError, + defaultResponseAdapter, + extractTableData, + updatePaginationFromResponse, + createSmartDebounce, + createErrorHandler +} from '../../utils/table/tableUtils' +import { tableConfig } from '../../utils/table/tableConfig' + +// 类型推导工具类型 +type InferApiParams = T extends (params: infer P) => any ? P : never +type InferApiResponse = T extends (params: any) => Promise ? R : never +type InferRecordType = T extends Api.Common.PaginatedResponse ? U : never + +// 优化的配置接口 - 支持自动类型推导 +export interface UseTableConfig< + TApiFn extends (params: any) => Promise = (params: any) => Promise, + TRecord = InferRecordType>, + TParams = InferApiParams, + TResponse = InferApiResponse +> { + // 核心配置 + core: { + /** API 请求函数 */ + apiFn: TApiFn + /** 默认请求参数 */ + apiParams?: Partial + /** 排除 apiParams 中的属性 */ + excludeParams?: string[] + /** 是否立即加载数据 */ + immediate?: boolean + /** 列配置工厂函数 */ + columnsFactory?: () => ColumnOption[] + /** 自定义分页字段映射 */ + paginationKey?: { + /** 当前页码字段名,默认为 'current' */ + current?: string + /** 每页条数字段名,默认为 'size' */ + size?: string + } + } + + // 数据处理 + transform?: { + /** 数据转换函数 */ + dataTransformer?: (data: TRecord[]) => TRecord[] + /** 响应数据适配器 */ + responseAdapter?: (response: TResponse) => ApiResponse + } + + // 性能优化 + performance?: { + /** 是否启用缓存 */ + enableCache?: boolean + /** 缓存时间(毫秒) */ + cacheTime?: number + /** 防抖延迟时间(毫秒) */ + debounceTime?: number + /** 最大缓存条数限制 */ + maxCacheSize?: number + } + + // 生命周期钩子 + hooks?: { + /** 数据加载成功回调(仅网络请求成功时触发) */ + onSuccess?: (data: TRecord[], response: ApiResponse) => void + /** 错误处理回调 */ + onError?: (error: TableError) => void + /** 缓存命中回调(从缓存获取数据时触发) */ + onCacheHit?: (data: TRecord[], response: ApiResponse) => void + /** 加载状态变化回调 */ + onLoading?: (loading: boolean) => void + /** 重置表单回调函数 */ + resetFormCallback?: () => void + } + + // 调试配置 + debug?: { + /** 是否启用日志输出 */ + enableLog?: boolean + /** 日志级别 */ + logLevel?: 'info' | 'warn' | 'error' + } +} + +export function useTable< + TApiFn extends (params: any) => Promise, + TRecord = InferRecordType> +>(config: UseTableConfig) { + return useTableImpl(config) +} + +/** + * useTable 的核心实现 - 强大的表格数据管理 Hook + * + * 提供完整的表格解决方案,包括: + * - 数据获取与缓存 + * - 分页控制 + * - 搜索功能 + * - 智能刷新策略 + * - 错误处理 + * - 列配置管理 + */ +function useTableImpl< + TApiFn extends (params: any) => Promise, + TRecord = InferRecordType> +>(config: UseTableConfig) { + type TParams = InferApiParams + const { + core: { + apiFn, + apiParams = {} as Partial, + excludeParams = [], + immediate = true, + columnsFactory, + paginationKey + }, + transform: { dataTransformer, responseAdapter = defaultResponseAdapter } = {}, + performance: { + enableCache = false, + cacheTime = 5 * 60 * 1000, + debounceTime = 300, + maxCacheSize = 50 + } = {}, + hooks: { onSuccess, onError, onCacheHit, resetFormCallback } = {}, + debug: { enableLog = false } = {} + } = config + + // 分页字段名配置:优先使用传入的配置,否则使用全局配置 + const pageKey = paginationKey?.current || tableConfig.paginationKey.current + const sizeKey = paginationKey?.size || tableConfig.paginationKey.size + + // 响应式触发器,用于手动更新缓存统计信息 + const cacheUpdateTrigger = ref(0) + + // 日志工具函数 + const logger = { + log: (message: string, ...args: unknown[]) => { + if (enableLog) { + console.log(`[useTable] ${message}`, ...args) + } + }, + warn: (message: string, ...args: unknown[]) => { + if (enableLog) { + console.warn(`[useTable] ${message}`, ...args) + } + }, + error: (message: string, ...args: unknown[]) => { + if (enableLog) { + console.error(`[useTable] ${message}`, ...args) + } + } + } + + // 缓存实例 + const cache = enableCache ? new TableCache(cacheTime, maxCacheSize, enableLog) : null + + // 加载状态机 + type LoadingState = 'idle' | 'loading' | 'success' | 'error' + const loadingState = ref('idle') + const loading = computed(() => loadingState.value === 'loading') + + // 错误状态 + const error = ref(null) + + // 表格数据 + const data = ref([]) + + // 请求取消控制器 + let abortController: AbortController | null = null + + // 缓存清理定时器 + let cacheCleanupTimer: NodeJS.Timeout | null = null + + // 搜索参数 + const searchParams = reactive( + Object.assign( + { + [pageKey]: 1, + [sizeKey]: 10 + }, + apiParams || {} + ) as TParams + ) + + // 分页配置 + const pagination = reactive({ + current: ((searchParams as Record)[pageKey] as number) || 1, + size: ((searchParams as Record)[sizeKey] as number) || 10, + total: 0 + }) + + // 移动端分页 (响应式) + const { width } = useWindowSize() + const mobilePagination = computed(() => ({ + ...pagination, + small: width.value < 768 + })) + + // 列配置 + const columnConfig = columnsFactory ? useTableColumns(columnsFactory) : null + const columns = columnConfig?.columns + const columnChecks = columnConfig?.columnChecks + + // 是否有数据 + const hasData = computed(() => data.value.length > 0) + + // 缓存统计信息 + const cacheInfo = computed(() => { + // 依赖触发器,确保缓存变化时重新计算 + void cacheUpdateTrigger.value + if (!cache) return { total: 0, size: '0KB', hitRate: '0 avg hits' } + return cache.getStats() + }) + + // 错误处理函数 + const handleError = createErrorHandler(onError, enableLog) + + // 清理缓存,根据不同的业务场景选择性地清理缓存 + const clearCache = (strategy: CacheInvalidationStrategy, context?: string): void => { + if (!cache) return + + let clearedCount = 0 + + switch (strategy) { + case CacheInvalidationStrategy.CLEAR_ALL: + cache.clear() + logger.log(`清空所有缓存 - ${context || ''}`) + break + + case CacheInvalidationStrategy.CLEAR_CURRENT: + clearedCount = cache.clearCurrentSearch(searchParams) + logger.log(`清空当前搜索缓存 ${clearedCount} 条 - ${context || ''}`) + break + + case CacheInvalidationStrategy.CLEAR_PAGINATION: + clearedCount = cache.clearPagination() + logger.log(`清空分页缓存 ${clearedCount} 条 - ${context || ''}`) + break + + case CacheInvalidationStrategy.KEEP_ALL: + default: + logger.log(`保持缓存不变 - ${context || ''}`) + break + } + // 手动触发缓存状态更新 + cacheUpdateTrigger.value++ + } + + // 获取数据的核心方法 + const fetchData = async ( + params?: Partial, + useCache = enableCache + ): Promise> => { + // 取消上一个请求 + if (abortController) { + abortController.abort() + } + + // 创建新的取消控制器 + const currentController = new AbortController() + abortController = currentController + + // 状态机:进入 loading 状态 + loadingState.value = 'loading' + error.value = null + + try { + let requestParams = Object.assign( + {}, + searchParams, + { + [pageKey]: pagination.current, + [sizeKey]: pagination.size + }, + params || {} + ) as TParams + + // 剔除不需要的参数 + if (excludeParams.length > 0) { + const filteredParams = { ...requestParams } + excludeParams.forEach((key) => { + delete (filteredParams as Record)[key] + }) + requestParams = filteredParams as TParams + } + + // 检查缓存 + if (useCache && cache) { + const cachedItem = cache.get(requestParams) + if (cachedItem) { + data.value = cachedItem.data + updatePaginationFromResponse(pagination, cachedItem.response) + + // 修复:避免重复设置相同的值,防止响应式循环更新 + const paramsRecord = searchParams as Record + if (paramsRecord[pageKey] !== pagination.current) { + paramsRecord[pageKey] = pagination.current + } + if (paramsRecord[sizeKey] !== pagination.size) { + paramsRecord[sizeKey] = pagination.size + } + + // 状态机:缓存命中,进入 success 状态 + loadingState.value = 'success' + + // 缓存命中时触发专门的回调,而不是 onSuccess + if (onCacheHit) { + onCacheHit(cachedItem.data, cachedItem.response) + } + + logger.log(`缓存命中`) + return cachedItem.response + } + } + + const response = await apiFn(requestParams) + + // 检查请求是否被取消 + if (currentController.signal.aborted) { + throw new Error('请求已取消') + } + + // 使用响应适配器转换为标准格式 + const standardResponse = responseAdapter(response) + + // 处理响应数据 + let tableData = extractTableData(standardResponse) + + // 应用数据转换函数 + if (dataTransformer) { + tableData = dataTransformer(tableData) + } + + // 更新状态 + data.value = tableData + updatePaginationFromResponse(pagination, standardResponse) + + // 修复:避免重复设置相同的值,防止响应式循环更新 + const paramsRecord = searchParams as Record + if (paramsRecord[pageKey] !== pagination.current) { + paramsRecord[pageKey] = pagination.current + } + if (paramsRecord[sizeKey] !== pagination.size) { + paramsRecord[sizeKey] = pagination.size + } + + // 缓存数据 + if (useCache && cache) { + cache.set(requestParams, tableData, standardResponse) + // 手动触发缓存状态更新 + cacheUpdateTrigger.value++ + logger.log(`数据已缓存`) + } + + // 状态机:请求成功,进入 success 状态 + loadingState.value = 'success' + + // 成功回调 + if (onSuccess) { + onSuccess(tableData, standardResponse) + } + + return standardResponse + } catch (err) { + if (err instanceof Error && err.message === '请求已取消') { + // 请求被取消,回到 idle 状态 + loadingState.value = 'idle' + return { records: [], total: 0, current: 1, size: 10 } + } + + // 状态机:请求失败,进入 error 状态 + loadingState.value = 'error' + data.value = [] + const tableError = handleError(err, '获取表格数据失败') + throw tableError + } finally { + // 只有当前控制器是活跃的才清空 + if (abortController === currentController) { + abortController = null + } + } + } + + // 获取数据 (保持当前页) + const getData = async (params?: Partial): Promise | void> => { + try { + return await fetchData(params) + } catch { + // 错误已在 fetchData 中处理 + return Promise.resolve() + } + } + + // 分页获取数据 (重置到第一页) - 专门用于搜索场景 + const getDataByPage = async (params?: Partial): Promise | void> => { + pagination.current = 1 + ;(searchParams as Record)[pageKey] = 1 + + // 搜索时清空当前搜索条件的缓存,确保获取最新数据 + clearCache(CacheInvalidationStrategy.CLEAR_CURRENT, '搜索数据') + + try { + return await fetchData(params, false) // 搜索时不使用缓存 + } catch { + // 错误已在 fetchData 中处理 + return Promise.resolve() + } + } + + // 智能防抖搜索函数 + const debouncedGetDataByPage = createSmartDebounce(getDataByPage, debounceTime) + + // 重置搜索参数 + const resetSearchParams = async (): Promise => { + // 取消防抖的搜索 + debouncedGetDataByPage.cancel() + + // 保存分页相关的默认值 + const paramsRecord = searchParams as Record + const defaultPagination = { + [pageKey]: 1, + [sizeKey]: (paramsRecord[sizeKey] as number) || 10 + } + + // 清空所有搜索参数 + Object.keys(searchParams).forEach((key) => { + delete paramsRecord[key] + }) + + // 重新设置默认参数 + Object.assign(searchParams, apiParams || {}, defaultPagination) + + // 重置分页 + pagination.current = 1 + pagination.size = defaultPagination[sizeKey] as number + + // 清空错误状态 + error.value = null + + // 清空缓存 + clearCache(CacheInvalidationStrategy.CLEAR_ALL, '重置搜索') + + // 重新获取数据 + await getData() + + // 执行重置回调 + if (resetFormCallback) { + await nextTick() + resetFormCallback() + } + } + + // 防重复调用的标志 + let isCurrentChanging = false + + // 处理分页大小变化 + const handleSizeChange = async (newSize: number): Promise => { + if (newSize <= 0) return + + debouncedGetDataByPage.cancel() + + const paramsRecord = searchParams as Record + pagination.size = newSize + pagination.current = 1 + paramsRecord[sizeKey] = newSize + paramsRecord[pageKey] = 1 + + clearCache(CacheInvalidationStrategy.CLEAR_CURRENT, '分页大小变化') + + await getData() + } + + // 处理当前页变化 + const handleCurrentChange = async (newCurrent: number): Promise => { + if (newCurrent <= 0) return + + // 修复:防止重复调用 + if (isCurrentChanging) { + return + } + + // 修复:如果当前页没有变化,不需要重新请求 + if (pagination.current === newCurrent) { + logger.log('分页页码未变化,跳过请求') + return + } + + try { + isCurrentChanging = true + + // 修复:只更新必要的状态 + const paramsRecord = searchParams as Record + pagination.current = newCurrent + // 只有当 searchParams 的分页字段与新值不同时才更新 + if (paramsRecord[pageKey] !== newCurrent) { + paramsRecord[pageKey] = newCurrent + } + + await getData() + } finally { + isCurrentChanging = false + } + } + + // 针对不同业务场景的刷新方法 + + // 新增后刷新:回到第一页并清空分页缓存(适用于新增数据后) + const refreshCreate = async (): Promise => { + debouncedGetDataByPage.cancel() + pagination.current = 1 + ;(searchParams as Record)[pageKey] = 1 + clearCache(CacheInvalidationStrategy.CLEAR_PAGINATION, '新增数据') + await getData() + } + + // 更新后刷新:保持当前页,仅清空当前搜索缓存(适用于更新数据后) + const refreshUpdate = async (): Promise => { + clearCache(CacheInvalidationStrategy.CLEAR_CURRENT, '编辑数据') + await getData() + } + + // 删除后刷新:智能处理页码,避免空页面(适用于删除数据后) + const refreshRemove = async (): Promise => { + const { current } = pagination + + // 清除缓存并获取最新数据 + clearCache(CacheInvalidationStrategy.CLEAR_CURRENT, '删除数据') + await getData() + + // 如果当前页为空且不是第一页,回到上一页 + if (data.value.length === 0 && current > 1) { + pagination.current = current - 1 + ;(searchParams as Record)[pageKey] = current - 1 + await getData() + } + } + + // 全量刷新:清空所有缓存,重新获取数据(适用于手动刷新按钮) + const refreshData = async (): Promise => { + debouncedGetDataByPage.cancel() + clearCache(CacheInvalidationStrategy.CLEAR_ALL, '手动刷新') + await getData() + } + + // 轻量刷新:仅清空当前搜索条件的缓存,保持分页状态(适用于定时刷新) + const refreshSoft = async (): Promise => { + clearCache(CacheInvalidationStrategy.CLEAR_CURRENT, '软刷新') + await getData() + } + + // 取消当前请求 + const cancelRequest = (): void => { + if (abortController) { + abortController.abort() + } + debouncedGetDataByPage.cancel() + } + + // 清空数据 + const clearData = (): void => { + data.value = [] + error.value = null + clearCache(CacheInvalidationStrategy.CLEAR_ALL, '清空数据') + } + + // 清理已过期的缓存条目,释放内存空间 + const clearExpiredCache = (): number => { + if (!cache) return 0 + const cleanedCount = cache.cleanupExpired() + if (cleanedCount > 0) { + // 手动触发缓存状态更新 + cacheUpdateTrigger.value++ + } + return cleanedCount + } + + // 设置定期清理过期缓存 + if (enableCache && cache) { + cacheCleanupTimer = setInterval(() => { + const cleanedCount = cache.cleanupExpired() + if (cleanedCount > 0) { + logger.log(`自动清理 ${cleanedCount} 条过期缓存`) + // 手动触发缓存状态更新 + cacheUpdateTrigger.value++ + } + }, cacheTime / 2) // 每半个缓存周期清理一次 + } + + // 挂载时自动加载数据 + if (immediate) { + onMounted(async () => { + await getData() + }) + } + + // 组件卸载时彻底清理 + onUnmounted(() => { + cancelRequest() + if (cache) { + cache.clear() + } + if (cacheCleanupTimer) { + clearInterval(cacheCleanupTimer) + } + }) + + // 优化的返回值结构 + return { + // 数据相关 + /** 表格数据 */ + data, + /** 数据加载状态 */ + loading: readonly(loading), + /** 错误状态 */ + error: readonly(error), + /** 数据是否为空 */ + isEmpty: computed(() => data.value.length === 0), + /** 是否有数据 */ + hasData, + + // 分页相关 + /** 分页状态信息 */ + pagination: readonly(pagination), + /** 移动端分页配置 */ + paginationMobile: mobilePagination, + /** 页面大小变化处理 */ + handleSizeChange, + /** 当前页变化处理 */ + handleCurrentChange, + + // 搜索相关 - 统一前缀 + /** 搜索参数 */ + searchParams, + /** 重置搜索参数 */ + resetSearchParams, + + // 数据操作 - 更明确的操作意图 + /** 加载数据 */ + fetchData: getData, + /** 获取数据 */ + getData: getDataByPage, + /** 获取数据(防抖) */ + getDataDebounced: debouncedGetDataByPage, + /** 清空数据 */ + clearData, + + // 刷新策略 + /** 全量刷新:清空所有缓存,重新获取数据(适用于手动刷新按钮) */ + refreshData, + /** 轻量刷新:仅清空当前搜索条件的缓存,保持分页状态(适用于定时刷新) */ + refreshSoft, + /** 新增后刷新:回到第一页并清空分页缓存(适用于新增数据后) */ + refreshCreate, + /** 更新后刷新:保持当前页,仅清空当前搜索缓存(适用于更新数据后) */ + refreshUpdate, + /** 删除后刷新:智能处理页码,避免空页面(适用于删除数据后) */ + refreshRemove, + + // 缓存控制 + /** 缓存统计信息 */ + cacheInfo, + /** 清除缓存,根据不同的业务场景选择性地清理缓存: */ + clearCache, + // 支持4种清理策略 + // clearCache(CacheInvalidationStrategy.CLEAR_ALL, '手动刷新') // 清空所有缓存 + // clearCache(CacheInvalidationStrategy.CLEAR_CURRENT, '搜索数据') // 只清空当前搜索条件的缓存 + // clearCache(CacheInvalidationStrategy.CLEAR_PAGINATION, '新增数据') // 清空分页相关缓存 + // clearCache(CacheInvalidationStrategy.KEEP_ALL, '保持缓存') // 不清理任何缓存 + /** 清理已过期的缓存条目,释放内存空间 */ + clearExpiredCache, + + // 请求控制 + /** 取消当前请求 */ + cancelRequest, + + // 列配置 (如果提供了 columnsFactory) + ...(columnConfig && { + /** 表格列配置 */ + columns, + /** 列显示控制 */ + columnChecks, + /** 新增列 */ + addColumn: columnConfig.addColumn, + /** 删除列 */ + removeColumn: columnConfig.removeColumn, + /** 切换列显示状态 */ + toggleColumn: columnConfig.toggleColumn, + /** 更新列配置 */ + updateColumn: columnConfig.updateColumn, + /** 批量更新列配置 */ + batchUpdateColumns: columnConfig.batchUpdateColumns, + /** 重新排序列 */ + reorderColumns: columnConfig.reorderColumns, + /** 获取指定列配置 */ + getColumnConfig: columnConfig.getColumnConfig, + /** 获取所有列配置 */ + getAllColumns: columnConfig.getAllColumns, + /** 重置所有列配置到默认状态 */ + resetColumns: columnConfig.resetColumns + }) + } +} + +// 重新导出类型和枚举,方便使用 +export { CacheInvalidationStrategy } from '../../utils/table/tableCache' +export type { ApiResponse, CacheItem } from '../../utils/table/tableCache' +export type { BaseRequestParams, TableError } from '../../utils/table/tableUtils' diff --git a/src/hooks/core/useTableColumns.ts b/src/hooks/core/useTableColumns.ts new file mode 100644 index 0000000..84b6e13 --- /dev/null +++ b/src/hooks/core/useTableColumns.ts @@ -0,0 +1,312 @@ +/** + * useTableColumns - 表格列配置管理 + * + * 提供动态的表格列配置管理能力,支持运行时灵活控制列的显示、隐藏、排序等操作。 + * 通常与 useTable 配合使用,为表格提供完整的列管理功能。 + * + * ## 主要功能 + * + * 1. 列显示控制 - 动态显示/隐藏列,支持批量操作 + * 2. 列排序 - 拖拽或编程方式重新排列列顺序 + * 3. 列配置管理 - 新增、删除、更新列配置 + * 4. 特殊列支持 - 自动处理 selection、expand、index 等特殊列 + * 5. 状态持久化 - 保持列的显示状态,支持重置到初始状态 + * + * ## 使用示例 + * + * ```typescript + * const { columns, columnChecks, toggleColumn, reorderColumns } = useTableColumns(() => [ + * { prop: 'name', label: '姓名', visible: true }, + * { prop: 'email', label: '邮箱', visible: true }, + * { prop: 'status', label: '状态', visible: false } + * ]) + * + * // 切换列显示 + * toggleColumn('email', false) + * + * // 重新排序 + * reorderColumns(0, 2) + * ``` + * + * @module useTableColumns + * @author Art Design Pro Team + */ + +import { ref, computed, watch } from 'vue' +import { $t } from '@/locales' +import type { ColumnOption } from '@/types/component' + +/** + * 特殊列类型 + */ +const SPECIAL_COLUMNS: Record = { + selection: { prop: '__selection__', label: $t('table.column.selection') }, + expand: { prop: '__expand__', label: $t('table.column.expand') }, + index: { prop: '__index__', label: $t('table.column.index') } +} + +/** + * 获取列的唯一标识 + */ +export const getColumnKey = (col: ColumnOption) => + SPECIAL_COLUMNS[col.type as keyof typeof SPECIAL_COLUMNS]?.prop ?? (col.prop as string) + +/** + * 获取列的显示状态 + * 优先使用 visible 字段,如果不存在则使用 checked 字段 + */ +export const getColumnVisibility = (col: ColumnOption): boolean => { + // visible 优先级高于 checked + if (col.visible !== undefined) { + return col.visible + } + // 如果 visible 未定义,使用 checked,默认为 true + return col.checked ?? true +} + +/** + * 获取列的检查状态 + */ +export const getColumnChecks = (columns: ColumnOption[]) => + columns.map((col) => { + const special = col.type && SPECIAL_COLUMNS[col.type] + const visibility = getColumnVisibility(col) + + if (special) { + return { ...col, prop: special.prop, label: special.label, checked: true, visible: true } + } + return { ...col, checked: visibility, visible: visibility } + }) + +/** + * 动态列配置接口 + */ +export interface DynamicColumnConfig { + /** + * 新增列(支持单个或批量) + * @param column 列配置或列配置数组 + * @param index 可选的插入位置,默认末尾(批量时为第一个列的位置) + */ + addColumn: (column: ColumnOption | ColumnOption[], index?: number) => void + /** + * 删除列(支持单个或批量) + * @param prop 列的唯一标识或标识数组 + */ + removeColumn: (prop: string | string[]) => void + /** + * 切换列显示状态(支持单个或批量) + * @param prop 列的唯一标识或标识数组 + * @param visible 可选的显示状态,默认取反 + */ + toggleColumn: (prop: string | string[], visible?: boolean) => void + + /** + * 更新列(支持单个或批量) + * @param prop 列的唯一标识或更新配置数组 + * @param updates 列配置更新(当 prop 为字符串时使用) + */ + updateColumn: ( + prop: string | Array<{ prop: string; updates: Partial> }>, + updates?: Partial> + ) => void + /** + * 批量更新列(兼容旧版本,推荐使用 updateColumn 的数组模式) + * @param updates 列更新配置 + * @deprecated 推荐使用 updateColumn 的数组模式 + */ + batchUpdateColumns: (updates: Array<{ prop: string; updates: Partial> }>) => void + /** + * 重新排序列 + * @param fromIndex 源索引 + * @param toIndex 目标索引 + */ + reorderColumns: (fromIndex: number, toIndex: number) => void + /** + * 获取列配置 + * @param prop 列的唯一标识 + * @returns 列配置 + */ + getColumnConfig: (prop: string) => ColumnOption | undefined + /** + * 获取所有列配置 + * @returns 所有列配置 + */ + getAllColumns: () => ColumnOption[] + /** + * 重置所有列 + */ + resetColumns: () => void +} + +export function useTableColumns( + columnsFactory: () => ColumnOption[] +): { + columns: any + columnChecks: any +} & DynamicColumnConfig { + const dynamicColumns = ref[]>(columnsFactory()) + const columnChecks = ref[]>(getColumnChecks(dynamicColumns.value)) + + // 当 dynamicColumns 变动时,重新生成 columnChecks 且保留已存在的显示状态 + watch( + dynamicColumns, + (newCols) => { + const visibilityMap = new Map( + columnChecks.value.map((c) => [getColumnKey(c), getColumnVisibility(c)]) + ) + const newChecks = getColumnChecks(newCols).map((c) => { + const key = getColumnKey(c) + const visibility = visibilityMap.has(key) ? visibilityMap.get(key) : getColumnVisibility(c) + return { + ...c, + checked: visibility, + visible: visibility + } + }) + columnChecks.value = newChecks + }, + { deep: true } + ) + + // 当前显示列(基于 columnChecks 的 checked 或 visible) + const columns = computed(() => { + const colMap = new Map(dynamicColumns.value.map((c) => [getColumnKey(c), c])) + return columnChecks.value + .filter((c) => getColumnVisibility(c)) + .map((c) => colMap.get(getColumnKey(c))) + .filter(Boolean) as ColumnOption[] + }) + + // 支持 updater 返回新数组或直接在传入数组上 mutate + const setDynamicColumns = (updater: (cols: ColumnOption[]) => void | ColumnOption[]) => { + const copy = [...dynamicColumns.value] + const result = updater(copy) + dynamicColumns.value = Array.isArray(result) ? result : copy + } + + return { + columns, + columnChecks, + + /** + * 新增列(支持单个或批量) + */ + addColumn: (column: ColumnOption | ColumnOption[], index?: number) => + setDynamicColumns((cols) => { + const next = [...cols] + const columnsToAdd = Array.isArray(column) ? column : [column] + const insertIndex = + typeof index === 'number' && index >= 0 && index <= next.length ? index : next.length + + // 批量插入 + next.splice(insertIndex, 0, ...columnsToAdd) + return next + }), + + /** + * 删除列(支持单个或批量) + */ + removeColumn: (prop: string | string[]) => + setDynamicColumns((cols) => { + const propsToRemove = Array.isArray(prop) ? prop : [prop] + return cols.filter((c) => !propsToRemove.includes(getColumnKey(c))) + }), + + /** + * 更新列(支持单个或批量) + */ + updateColumn: ( + prop: string | Array<{ prop: string; updates: Partial> }>, + updates?: Partial> + ) => { + // 批量模式:prop 是数组 + if (Array.isArray(prop)) { + setDynamicColumns((cols) => { + const map = new Map(prop.map((u) => [u.prop, u.updates])) + return cols.map((c) => { + const key = getColumnKey(c) + const upd = map.get(key) + return upd ? { ...c, ...upd } : c + }) + }) + } + // 单个模式:prop 是字符串 + else if (updates) { + setDynamicColumns((cols) => + cols.map((c) => (getColumnKey(c) === prop ? { ...c, ...updates } : c)) + ) + } + }, + + /** + * 切换列显示状态(支持单个或批量) + */ + toggleColumn: (prop: string | string[], visible?: boolean) => { + const propsToToggle = Array.isArray(prop) ? prop : [prop] + const next = [...columnChecks.value] + + propsToToggle.forEach((p) => { + const i = next.findIndex((c) => getColumnKey(c) === p) + if (i > -1) { + const currentVisibility = getColumnVisibility(next[i]) + const newVisibility = visible ?? !currentVisibility + // 同时更新 checked 和 visible 以保持兼容性 + next[i] = { ...next[i], checked: newVisibility, visible: newVisibility } + } + }) + + columnChecks.value = next + }, + + /** + * 重置所有列 + */ + resetColumns: () => { + dynamicColumns.value = columnsFactory() + }, + + /** + * 批量更新列(兼容旧版本) + * @deprecated 推荐使用 updateColumn 的数组模式 + */ + batchUpdateColumns: (updates) => + setDynamicColumns((cols) => { + const map = new Map(updates.map((u) => [u.prop, u.updates])) + return cols.map((c) => { + const key = getColumnKey(c) + const upd = map.get(key) + return upd ? { ...c, ...upd } : c + }) + }), + + /** + * 重新排序列 + */ + reorderColumns: (fromIndex: number, toIndex: number) => + setDynamicColumns((cols) => { + if ( + fromIndex < 0 || + fromIndex >= cols.length || + toIndex < 0 || + toIndex >= cols.length || + fromIndex === toIndex + ) { + return cols + } + const next = [...cols] + const [moved] = next.splice(fromIndex, 1) + next.splice(toIndex, 0, moved) + return next + }), + + /** + * 获取列配置 + */ + getColumnConfig: (prop: string) => dynamicColumns.value.find((c) => getColumnKey(c) === prop), + + /** + * 获取所有列配置 + */ + getAllColumns: () => [...dynamicColumns.value] + } +} diff --git a/src/hooks/core/useTableHeight.ts b/src/hooks/core/useTableHeight.ts new file mode 100644 index 0000000..8fdf6da --- /dev/null +++ b/src/hooks/core/useTableHeight.ts @@ -0,0 +1,105 @@ +/** + * useTableHeight - 表格高度自动计算 + * + * 自动计算表格容器的最佳高度,确保表格在不同布局场景下都能正确显示。 + * 根据表格头部、分页器等元素的高度动态调整容器高度,避免出现滚动条或布局错乱。 + * + * ## 主要功能 + * + * 1. 动态高度计算 - 根据表格头部、分页器高度自动计算容器高度 + * 2. 响应式更新 - 配置变化时自动重新计算高度 + * 3. 灵活配置 - 支持自定义各部分高度和间距 + * 4. 智能适配 - 无额外元素时自动使用 100% 高度 + * + * @module useTableHeight + * @author Art Design Pro Team + */ + +import { computed, type Ref } from 'vue' + +/** + * 表格高度计算器配置接口 + */ +interface TableHeightOptions { + /** 是否显示表格头部 */ + showTableHeader: Ref + /** 分页器高度 */ + paginationHeight: Ref + /** 表格头部高度 */ + tableHeaderHeight: Ref + /** 分页器间距 */ + paginationSpacing: Ref +} + +/** + * 表格高度计算器类 + */ +class TableHeightCalculator { + // 常量配置 + private static readonly DEFAULT_TABLE_HEADER_HEIGHT = 44 + private static readonly TABLE_HEADER_SPACING = 12 + + constructor(private options: TableHeightOptions) {} + + /** + * 计算容器高度 + */ + calculate(): { height: string } { + const offset = this.calculateOffset() + return { + height: offset === 0 ? '100%' : `calc(100% - ${offset}px)` + } + } + + /** + * 计算偏移量 + */ + private calculateOffset(): number { + if (!this.options.showTableHeader.value) { + return this.calculatePaginationOffset() + } + + const headerHeight = this.getHeaderHeight() + const paginationOffset = this.calculatePaginationOffset() + + return headerHeight + paginationOffset + TableHeightCalculator.TABLE_HEADER_SPACING + } + + /** + * 获取表格头部高度 + */ + private getHeaderHeight(): number { + return this.options.tableHeaderHeight.value || TableHeightCalculator.DEFAULT_TABLE_HEADER_HEIGHT + } + + /** + * 计算分页器偏移量 + */ + private calculatePaginationOffset(): number { + const { paginationHeight, paginationSpacing } = this.options + return paginationHeight.value === 0 ? 0 : paginationHeight.value + paginationSpacing.value + } +} + +/** + * 表格高度计算 Hook + * + * 提供表格容器高度的自动计算功能,支持: + * - 表格头部高度 + * - 分页器高度 + * - 动态间距计算 + * + * @param options 配置选项 + * @returns 容器高度计算结果 + */ +export function useTableHeight(options: TableHeightOptions) { + const containerHeight = computed(() => { + const calculator = new TableHeightCalculator(options) + return calculator.calculate() + }) + + return { + /** 容器高度样式对象 */ + containerHeight + } +} diff --git a/src/hooks/core/useTheme.ts b/src/hooks/core/useTheme.ts new file mode 100644 index 0000000..187c3e0 --- /dev/null +++ b/src/hooks/core/useTheme.ts @@ -0,0 +1,174 @@ +/** + * useTheme - 系统主题管理 + * + * 提供完整的主题切换和管理功能,支持亮色、暗色和自动模式。 + * 自动处理主题切换时的过渡效果,确保切换流畅无闪烁。 + * + * ## 主要功能 + * + * 1. 主题切换 - 支持亮色、暗色、自动三种主题模式 + * 2. 自动模式 - 根据系统偏好自动切换主题 + * 3. 颜色适配 - 自动调整主题色的明暗变体(9 个层级) + * 4. 过渡优化 - 切换时临时禁用过渡效果,避免闪烁 + * 5. 状态持久化 - 主题设置自动保存到 store + * + * ## 使用示例 + * + * ```typescript + * const { switchThemeStyles } = useTheme() + * + * // 切换到暗色主题 + * switchThemeStyles(SystemThemeEnum.DARK) + * + * // 切换到亮色主题 + * switchThemeStyles(SystemThemeEnum.LIGHT) + * + * // 切换到自动模式(跟随系统) + * switchThemeStyles(SystemThemeEnum.AUTO) + * ``` + * + * @module useTheme + * @author Art Design Pro Team + */ + +import { useSettingStore } from '@/store/modules/setting' +import { SystemThemeEnum } from '@/enums/appEnum' +import AppConfig from '@/config' +import { SystemThemeTypes } from '@/types/store' +import { getDarkColor, getLightColor, setElementThemeColor } from '@/utils/ui' +import { usePreferredDark } from '@vueuse/core' +import { watch } from 'vue' + +export function useTheme() { + const settingStore = useSettingStore() + + // 禁用过渡效果 + const disableTransitions = () => { + const style = document.createElement('style') + style.setAttribute('id', 'disable-transitions') + style.textContent = '* { transition: none !important; }' + document.head.appendChild(style) + } + + // 启用过渡效果 + const enableTransitions = () => { + const style = document.getElementById('disable-transitions') + if (style) { + style.remove() + } + } + + // 设置系统主题 + const setSystemTheme = (theme: SystemThemeEnum, themeMode?: SystemThemeEnum) => { + // 临时禁用过渡效果 + disableTransitions() + + const el = document.getElementsByTagName('html')[0] + const isDark = theme === SystemThemeEnum.DARK + + if (!themeMode) { + themeMode = theme + } + + const currentTheme = AppConfig.systemThemeStyles[theme as keyof SystemThemeTypes] + + if (currentTheme) { + el.setAttribute('class', currentTheme.className) + } + + // 设置按钮颜色加深或变浅 + const primary = settingStore.systemThemeColor + + for (let i = 1; i <= 9; i++) { + document.documentElement.style.setProperty( + `--el-color-primary-light-${i}`, + isDark ? `${getDarkColor(primary, i / 10)}` : `${getLightColor(primary, i / 10)}` + ) + } + + // 更新store中的主题设置 + settingStore.setGlopTheme(theme, themeMode) + + // 使用 requestAnimationFrame 确保在下一帧恢复过渡效果 + requestAnimationFrame(() => { + requestAnimationFrame(() => { + enableTransitions() + }) + }) + } + + // 使用 VueUse 的 usePreferredDark 检测系统主题偏好 + const prefersDark = usePreferredDark() + + // 自动设置系统主题 + const setSystemAutoTheme = () => { + const theme = prefersDark.value ? SystemThemeEnum.DARK : SystemThemeEnum.LIGHT + setSystemTheme(theme, SystemThemeEnum.AUTO) + } + + // 切换主题 + const switchThemeStyles = (theme: SystemThemeEnum) => { + if (theme === SystemThemeEnum.AUTO) { + setSystemAutoTheme() + } else { + setSystemTheme(theme) + } + } + + return { + setSystemTheme, + setSystemAutoTheme, + switchThemeStyles, + prefersDark + } +} + +/** + * 初始化主题系统 + */ +export function initializeTheme() { + const settingStore = useSettingStore() + const prefersDark = usePreferredDark() + + // 根据系统偏好应用主题 + const applyThemeByMode = () => { + const el = document.getElementsByTagName('html')[0] + let actualTheme = settingStore.systemThemeType + + // 如果是 AUTO 模式,检测系统偏好 + if (settingStore.systemThemeMode === SystemThemeEnum.AUTO) { + actualTheme = prefersDark.value ? SystemThemeEnum.DARK : SystemThemeEnum.LIGHT + // 更新实际应用的主题类型 + settingStore.systemThemeType = actualTheme + } + + // 设置主题 class + const currentTheme = AppConfig.systemThemeStyles[actualTheme as keyof SystemThemeTypes] + if (currentTheme) { + el.setAttribute('class', currentTheme.className) + } + + // 设置主题颜色 + setElementThemeColor(settingStore.systemThemeColor) + + // 设置圆角 + document.documentElement.style.setProperty('--custom-radius', `${settingStore.customRadius}rem`) + } + + // 应用主题 + applyThemeByMode() + + // 如果是 AUTO 模式,监听系统主题变化(使用 VueUse 的响应式特性) + if (settingStore.systemThemeMode === SystemThemeEnum.AUTO) { + watch( + prefersDark, + () => { + // 只有在 AUTO 模式下才响应系统主题变化 + if (settingStore.systemThemeMode === SystemThemeEnum.AUTO) { + applyThemeByMode() + } + }, + { immediate: false } + ) + } +} diff --git a/src/hooks/index.ts b/src/hooks/index.ts new file mode 100644 index 0000000..472b09c --- /dev/null +++ b/src/hooks/index.ts @@ -0,0 +1,32 @@ +// 通用功能集合 +export { useCommon } from './core/useCommon' + +// 应用模式 +export { useAppMode } from './core/useAppMode' + +// 权限控制 +export { useAuth } from './core/useAuth' + +// 表格数据管理方案 +export { useTable } from './core/useTable' + +// 表格列配置管理 +export { useTableColumns } from './core/useTableColumns' + +// 主题相关 +export { useTheme } from './core/useTheme' + +// 礼花+文字滚动 +export { useCeremony } from './core/useCeremony' + +// 顶栏快速入口 +export { useFastEnter } from './core/useFastEnter' + +// 顶栏功能管理 +export { useHeaderBar } from './core/useHeaderBar' + +// 图表相关 +export { useChart, useChartComponent, useChartOps } from './core/useChart' + +// 布局高度 +export { useLayoutHeight, useAutoLayoutHeight } from './core/useLayoutHeight' diff --git a/src/locales/en-US/billing.json b/src/locales/en-US/billing.json new file mode 100644 index 0000000..1545b9e --- /dev/null +++ b/src/locales/en-US/billing.json @@ -0,0 +1,244 @@ +{ + "table": { + "column": { + "action": "Action" + } + }, + "billing": { + "quickFilter": { + "title": "Quick Filters", + "all": "All", + "pending": "Pending", + "paid": "Paid", + "overdue": "Overdue", + "cancelled": "Cancelled" + }, + "search": { + "tenant": "Tenant", + "tenantPlaceholder": "Select tenant (optional)", + "billingType": "Bill Type", + "billingTypePlaceholder": "Select bill type", + "status": "Status", + "statusPlaceholder": "Select status", + "dateRange": "Date range", + "dateRangePlaceholder": "Select date range", + "amountRange": "Amount Range", + "minAmountPlaceholder": "Min amount", + "maxAmountPlaceholder": "Max amount", + "keyword": "Keyword", + "keywordPlaceholder": "Statement no / tenant name" + }, + "action": { + "export": "Export", + "exportPdf": "Export PDF", + "create": "Create", + "detail": "Detail", + "cancel": "Cancel", + "recordPayment": "Confirm Payment", + "verifyPayment": "Verify Payment", + "addLineItem": "Add line item", + "uploadProof": "Upload proof" + }, + "dialog": { + "createTitle": "Create Bill", + "recordPaymentTitle": "Confirm Payment", + "proofPreviewTitle": "Proof Preview" + }, + "field": { + "statementNo": "Statement No", + "tenantName": "Tenant", + "billingType": "Bill Type", + "amount": "Amount", + "amountDue": "Amount Due", + "amountPaid": "Amount Paid", + "status": "Status", + "dueDate": "Due date", + "createdAt": "Created at", + "periodStart": "Period start", + "periodEnd": "Period end", + "notes": "Notes" + }, + "drawer": { + "tabs": { + "basic": "Basic", + "lineItems": "Line Items", + "payments": "Payments", + "statusFlow": "Status Flow" + }, + "title": "Bill Detail", + "basicInfo": "Basic info", + "paymentList": "Payment records", + "openProof": "Open proof", + "noPayments": "No payment records", + "noStatusFlow": "No status flow records" + }, + "view": { + "timeline": "Timeline", + "table": "Table" + }, + "status": { + "draft": "Draft", + "pending": "Pending", + "paid": "Paid", + "overdue": "Overdue", + "cancelled": "Cancelled" + }, + "statusFlow": { + "created": "Created", + "due": "Due", + "paid": "Paid", + "overdue": "Overdue", + "cancelled": "Cancelled" + }, + "lineItem": { + "itemType": "Type", + "description": "Description", + "quantity": "Qty", + "unitPrice": "Unit Price", + "amount": "Amount" + }, + "billingType": { + "subscription": "Subscription", + "quotaPurchase": "Quota Purchase", + "manual": "Manual", + "renewal": "Renewal" + }, + "placeholder": { + "enterItemType": "Enter type", + "enterDescription": "Enter description", + "selectTenant": "Select tenant", + "selectBillingType": "Select bill type", + "selectDueDate": "Select due date", + "enterNotes": "Enter notes (optional)", + "selectPaymentMethod": "Select payment method", + "enterPaymentAmount": "Enter payment amount", + "enterTransactionNo": "Enter transaction no (optional)", + "enterPaymentNotes": "Enter notes (optional)" + }, + "hint": { + "amountAutoSum": "Amount is auto-calculated from line items" + }, + "validation": { + "tenantRequired": "Please select tenant", + "billingTypeRequired": "Please select bill type", + "dueDateRequired": "Please select due date", + "lineItemsRequired": "Please add at least one line item", + "lineItemDescriptionRequired": "Line item #{index}: description is required", + "lineItemQuantityInvalid": "Line item #{index}: quantity is invalid", + "lineItemUnitPriceInvalid": "Line item #{index}: unit price is invalid", + "amountMin": "Amount due must be greater than 0", + "paymentAmountRequired": "Please enter payment amount", + "paymentAmountMin": "Payment amount must be greater than 0", + "paymentAmountExceedRemain": "Payment amount cannot exceed remaining amount", + "paymentMethodRequired": "Please select payment method" + }, + "message": { + "exportFailed": "Export failed, please retry", + "exportNoData": "No data to export", + "exportSuccess": "Export successful", + "exportExcelSuccess": "Exported {count} rows", + "exportPdfNeedSingleSelection": "Export PDF supports single selection only", + "exportPdfOpened": "PDF opened in a new window", + "exportPdfPopupBlocked": "Popup blocked by browser, please allow popups and retry", + "batchMarkPaidSuccess": "Batch marked as paid", + "batchMarkPaidNotes": "Batch marked as paid", + "batchNoPending": "No pending bills selected", + "batchCancelNotes": "Batch cancelled", + "batchCancelSuccess": "Batch cancelled successfully", + "cancelByAdmin": "Cancelled by admin", + "cancelConfirm": "Cancel this bill?", + "cancelSuccess": "Cancelled", + "createSuccess": "Created successfully", + "loadDetailFailed": "Failed to load bill detail", + "recordPaymentSuccess": "Payment confirmed", + "verifyPaymentSuccess": "Payment verified", + "verifyPaymentConfirm": "Are you sure to verify this payment? After verification, the paid amount of the bill will be updated accordingly.", + "proofTypeNotAllowed": "Only JPG/PNG images are allowed", + "proofTooLarge": "File is too large (max {size}MB)", + "proofUploadSuccess": "Proof uploaded", + "sortFieldNotSupported": "Sort field is not supported", + "generatingPdf": "Generating PDF, please wait...", + "exportCancelled": "Export cancelled" + }, + "batch": { + "confirmPayment": "Confirm Payment", + "confirmPaymentTip": "Mark selected pending bills as paid", + "cancel": "Batch Cancel", + "cancelTip": "Cancel selected bills (irreversible)", + "export": "Batch Export", + "selectedCount": "Selected {count}" + }, + "payment": { + "amount": "Amount", + "remainAmount": "Remaining", + "method": "Method", + "transactionNo": "Transaction No", + "proofUrl": "Proof", + "proofUploadHint": "JPG/PNG, single file ≤ 10MB", + "paidAt": "Paid at", + "notes": "Notes", + "status": "Status" + }, + "paymentMethod": { + "online": "Online", + "bankTransfer": "Bank transfer", + "other": "Other" + }, + "paymentStatus": { + "pending": "Pending", + "success": "Success", + "failed": "Failed", + "refunded": "Refunded" + }, + "export": { + "title": "Export Bills", + "format": "Format", + "formatExcel": "Excel", + "formatPdf": "PDF", + "formatCsv": "CSV", + "scope": "Scope", + "currentPage": "Current page", + "selected": "Selected", + "all": "All", + "fields": "Fields", + "dateRange": "Date range", + "dateRangePlaceholder": "Select date range", + "confirm": "Export", + "sheetName": "Bills", + "fileName": "billing_export", + "noPayments": "No payment records", + "formatRequired": "Please select format", + "scopeRequired": "Please select scope", + "fieldsRequired": "Please select at least one field" + }, + "statistics": { + "title": "Billing Statistics", + "startDate": "Start date", + "endDate": "End date", + "groupBy": { + "day": "Daily", + "week": "Weekly", + "month": "Monthly" + }, + "totalRevenue": "Collected", + "pendingAmount": "Uncollected", + "overdueAmount": "Overdue", + "totalAmountDue": "Amount Due", + "statusDistribution": "Status Distribution", + "paymentMethodDistribution": "Payment Method Share", + "paymentMethodNoData": "No payment method data", + "revenueTrend": "Revenue Trend", + "topDebtors": "Top Debtors", + "overdueDays": "Overdue Days" + }, + "timeline": { + "created": "Created", + "dueDate": "Due date", + "overdueByDays": "Overdue by {days} days", + "payment": "Payment", + "paymentDesc": "{method} ${amount}", + "currentStatus": "Current status: {status}", + "currentStatusDesc": "Current status is authoritative" + } + } +} diff --git a/src/locales/en/announcement.ts b/src/locales/en/announcement.ts new file mode 100644 index 0000000..3a336cf --- /dev/null +++ b/src/locales/en/announcement.ts @@ -0,0 +1,155 @@ +/** + * Announcement module i18n (en) + * + * Note: This file is merged into `announcement.*` namespace at i18n bootstrap. + */ +export default { + common: { + empty: '-' + }, + create: { + title: 'Create Platform Announcement' + }, + edit: { + title: 'Edit Platform Announcement', + notEditable: 'This announcement is published or revoked and cannot be edited.', + readonlyFieldsHint: + 'Current API only supports editing title, content and audience. Other fields are read-only.' + }, + list: { + total: '{total} announcement(s)' + }, + action: { + create: 'Create', + refresh: 'Refresh', + detail: 'Detail', + edit: 'Edit', + publish: 'Publish', + revoke: 'Revoke', + delete: 'Delete', + save: 'Save', + back: 'Back' + }, + search: { + status: 'Status', + statusPlaceholder: 'All statuses', + keyword: 'Keyword', + keywordPlaceholder: 'Title/Content', + dateRange: 'Date Range', + dateRangeStart: 'Start date', + dateRangeEnd: 'End date' + }, + table: { + title: 'Title', + type: 'Type', + priority: 'Priority', + status: 'Status', + effectiveRange: 'Effective Range', + publishedAt: 'Published At', + actions: 'Actions', + effectiveOpenEnded: 'Open-ended' + }, + form: { + title: 'Title', + titlePlaceholder: 'Enter announcement title', + type: 'Type', + typePlaceholder: 'Select announcement type', + priority: 'Priority', + effectiveFrom: 'Effective From', + effectiveFromPlaceholder: 'Select start time', + effectiveTo: 'Effective To', + effectiveToPlaceholder: 'Select end time (optional)', + target: 'Audience', + targetTypePlaceholder: 'Select audience type', + content: 'Content' + }, + status: { + draft: 'Draft', + published: 'Published', + revoked: 'Revoked', + unknown: 'Unknown' + }, + type: { + system: 'System', + billing: 'Billing/Subscription', + operation: 'Operations', + platformUpdate: 'Platform Update', + security: 'Security Notice', + compliance: 'Compliance Notice', + tenantInternal: 'Tenant Internal', + tenantFinance: 'Tenant Finance', + tenantOperation: 'Tenant Operations', + unknown: 'Unknown Type' + }, + targetType: { + all: 'All Users', + rules: 'Rules', + roles: 'Roles', + users: 'Users', + manual: 'Manual' + }, + audience: { + placeholder: 'Audience selector will be integrated in a later release.', + rulesPlaceholder: 'Enter rules JSON (e.g. {"roles":["roleId"]})', + usersPlaceholder: 'Enter user IDs separated by commas' + }, + draft: { + notSaved: 'Not saved', + savedAt: 'Saved · {time}', + readOnly: 'Read-only state, auto-save stopped' + }, + validation: { + titleRequired: 'Please enter a title', + titleMax: 'Title cannot exceed 128 characters', + contentRequired: 'Please enter content', + typeRequired: 'Please select a type', + priorityRequired: 'Please enter priority', + priorityRange: 'Priority must be between 1 and 5', + effectiveFromRequired: 'Please select effective start time', + effectiveToAfter: 'End time must be later than start time', + targetTypeRequired: 'Please select audience type', + targetRulesRequired: 'Please complete rule conditions', + targetUsersRequired: 'Please select at least one user', + targetRulesInvalid: 'Invalid rules JSON format' + }, + message: { + loadFailed: 'Failed to load announcement', + createSuccess: 'Created successfully', + createFailed: 'Create failed, please retry', + updateSuccess: 'Updated successfully', + updateFailed: 'Update failed, please retry', + publishConfirm: 'Publish this announcement?', + publishSuccess: 'Published successfully', + publishFailed: 'Publish failed, please retry', + revokeConfirm: 'Revoke this announcement?', + revokeSuccess: 'Revoked successfully', + revokeFailed: 'Revoke failed, please retry', + deleteNotSupported: 'Delete is not supported for platform announcements', + rowVersionMissing: 'Missing row version, operation cancelled', + validationFailed: 'Please complete required fields', + missingId: 'Missing announcement ID', + targetParseFailed: 'Failed to parse audience parameters', + concurrencyConflict: 'Concurrency conflict: {message}', + refreshAndRetry: 'Data has been modified by another user, please refresh and retry', + cannotEditPublished: 'Cannot edit announcement with status {status}', + invalidId: 'Invalid announcement ID' + }, + detail: { + id: 'Announcement ID', + status: 'Status', + type: 'Type', + priority: 'Priority', + effectiveFrom: 'Effective From', + effectiveTo: 'Effective To', + publishedAt: 'Published At', + revokedAt: 'Revoked At', + targetType: 'Audience', + targetParameters: 'Audience Parameters', + publisherScope: 'Publisher Scope', + content: 'Content', + contentPlaceholder: 'No content', + contentHint: 'Rich text preview will be integrated with a safe renderer (DOMPurify).', + readStats: 'Read Stats', + readStatsPlaceholder: 'Read stats API is not integrated yet.' + } +} diff --git a/src/locales/en/dictionary.json b/src/locales/en/dictionary.json new file mode 100644 index 0000000..28b4d16 --- /dev/null +++ b/src/locales/en/dictionary.json @@ -0,0 +1,196 @@ +{ + "dictionary": { + "common": { + "system": "System", + "business": "Business", + "refresh": "Refresh", + "warning": "Warning", + "confirm": "Confirm", + "cancel": "Cancel", + "edit": "Edit", + "delete": "Delete", + "actions": "Actions", + "key": "Key", + "value": "Value", + "enabled": "Enabled", + "description": "Description", + "default": "Default", + "sortOrder": "Sort Order", + "code": "Code", + "name": "Name", + "scope": "Scope", + "order": "Order", + "hidden": "Hidden", + "source": "Source", + "tenant": "Tenant", + "systemLabel": "System", + "selectGroupFirst": "Select a group first" + }, + "group": { + "searchPlaceholder": "Search by code or name", + "new": "New Group", + "createTitle": "Create Dictionary Group", + "editTitle": "Edit Dictionary Group", + "codePlaceholder": "e.g. ORDER_STATUS", + "namePlaceholder": "Dictionary group name", + "scopePlaceholder": "Scope", + "allowOverride": "Allow Override", + "enabled": "Enabled", + "description": "Description", + "empty": "No groups found.", + "deleteConfirm": "Deleting a group also removes all its items. Continue?", + "deleteSuccess": "Deleted", + "rowVersionMissing": "Row version is missing. Please refresh.", + "codeRequired": "Code is required", + "codeLength": "Length {min}-{max}", + "codePattern": "Only letters, numbers, and underscore are allowed", + "nameRequired": "Name is required", + "nameTooLong": "Name is too long", + "descriptionTooLong": "Description is too long" + }, + "item": { + "new": "New Item", + "createTitle": "Create Dictionary Item", + "editTitle": "Edit Dictionary Item", + "keyPlaceholder": "e.g. PENDING", + "value": "Value", + "default": "Default", + "enabled": "Enabled", + "sortOrder": "Sort Order", + "deleteConfirm": "Delete this item?", + "deleteSuccess": "Deleted", + "import": "Import", + "export": "Export", + "importCompleted": "Import completed", + "rowVersionMissing": "Row version is missing. Please refresh.", + "groupNotSelected": "Group is not selected", + "keyRequired": "Key is required", + "keyTooLong": "Key is too long", + "valueRequired": "At least one language is required", + "descriptionTooLong": "Description is too long", + "sortUpdated": "Sort order updated" + }, + "i18n": { + "zh": "中文", + "en": "English", + "zhPlaceholder": "请输入中文内容", + "enPlaceholder": "Enter English content", + "hint": "Fill at least one language. Both recommended for full coverage." + }, + "import": { + "title": "Batch Import Dictionary Items", + "dropHere": "Drop file here or click to upload", + "tip": "Max file size: 10MB. CSV or JSON only.", + "conflictMode": "Conflict Mode", + "skip": "Skip", + "overwrite": "Overwrite", + "append": "Append", + "successSummary": "Success: {success}, Skipped: {skip}, Errors: {error}", + "row": "Row", + "field": "Field", + "message": "Message", + "start": "Start Import", + "fileTooLarge": "File size exceeds 10MB", + "selectFile": "Please select a file", + "unsupportedFormat": "Unsupported file format" + }, + "override": { + "toggleEnabled": "Override Enabled", + "toggleDisabled": "Override Disabled", + "systemItems": "System Items", + "customView": "Custom View", + "newCustomItem": "New Custom Item", + "saveSortOrder": "Save Sort Order", + "tenantGroupNotReady": "Tenant group is not ready", + "hiddenSaved": "Hidden items saved", + "sortSaved": "Sort order saved", + "selectSystemDictionary": "Select a system dictionary", + "selectGroupHint": "Select a dictionary group to continue.", + "unsavedSortConfirm": "You have unsaved sort changes. Leave without saving?" + }, + "metrics": { + "cacheHitRatio": "Cache Hit Ratio", + "totalQueries": "Total Queries", + "hits": "Hits", + "misses": "Misses", + "avgResponse": "Avg Response", + "last1h": "Last 1 hour", + "hitRatioTrend": "Hit Ratio Trend (L1 vs L2)", + "timeRange1h": "1 Hour", + "timeRange24h": "24 Hours", + "timeRange7d": "7 Days", + "invalidationEvents": "Invalidation Events", + "rangeTo": "To", + "start": "Start", + "end": "End", + "timestamp": "Timestamp", + "dictionary": "Dictionary", + "operation": "Operation", + "keys": "Keys", + "operator": "Operator", + "create": "Create", + "update": "Update", + "delete": "Delete", + "l1HitRatio": "L1 Hit Ratio", + "l2HitRatio": "L2 Hit Ratio", + "target": "Target" + }, + "labelOverride": { + "title": "Label Override Management", + "tenantTitle": "Tenant Label Override", + "platformTitle": "Platform Label Override", + "selectTenant": "Select Tenant", + "selectTenantHint": "Select a tenant to view their label overrides", + "dictionaryItem": "Dictionary Item", + "dictionaryItemKey": "Dictionary Item Key", + "originalValue": "Original Value", + "overrideValue": "Override Value", + "overrideType": "Override Type", + "tenantCustomization": "Tenant Customization", + "platformEnforcement": "Platform Enforcement", + "reason": "Override Reason", + "reasonPlaceholder": "Enter override reason (optional)", + "reasonHint": "Recommended for audit purposes", + "createdAt": "Created At", + "updatedAt": "Updated At", + "operator": "Operator", + "newOverride": "New Override", + "editOverride": "Edit Override", + "deleteOverride": "Delete Override", + "deleteConfirm": "Are you sure to delete this label override? The original value will be restored.", + "noOverrides": "No label overrides found", + "selectDictionaryItem": "Please select a dictionary item", + "overrideValueRequired": "Override value is required", + "onlySystemDictionary": "Tenant can only override system dictionary items", + "filterAll": "All", + "filterTenantCustomization": "Tenant Customization", + "filterPlatformEnforcement": "Platform Enforcement" + }, + "errors": { + "loadGroups": "Failed to load dictionary groups.", + "loadGroup": "Failed to load dictionary group.", + "createGroup": "Failed to create dictionary group.", + "updateGroup": "Failed to update dictionary group.", + "deleteGroup": "Failed to delete dictionary group.", + "loadItems": "Failed to load dictionary items.", + "createItem": "Failed to create dictionary item.", + "updateItem": "Failed to update dictionary item.", + "deleteItem": "Failed to delete dictionary item.", + "loadOverrides": "Failed to load overrides.", + "loadOverride": "Failed to load override.", + "enableOverride": "Failed to enable override.", + "disableOverride": "Failed to disable override.", + "updateHidden": "Failed to update hidden items.", + "updateSort": "Failed to update sort order.", + "dataConflict": "Data has been updated by someone else. Refresh and retry.", + "loadLabelOverrides": "Failed to load label overrides.", + "saveLabelOverride": "Failed to save label override.", + "deleteLabelOverride": "Failed to delete label override." + }, + "messages": { + "deleted": "Deleted", + "importCompleted": "Import completed", + "sortUpdated": "Sort order updated" + } + } +} diff --git a/src/locales/en/merchant.ts b/src/locales/en/merchant.ts new file mode 100644 index 0000000..5e24412 --- /dev/null +++ b/src/locales/en/merchant.ts @@ -0,0 +1,135 @@ +/** + * Merchant module i18n (en) + * + * NOTE: This file is merged into `merchant.*` namespace. + */ +export default { + list: { + title: 'Merchants', + search: { + keyword: 'Keyword', + keywordPlaceholder: 'Merchant name or license number', + status: 'Status', + operatingMode: 'Operating mode', + tenant: 'Tenant', + tenantPlaceholder: 'Enter tenant ID' + }, + table: { + name: 'Merchant', + tenantName: 'Tenant', + operatingMode: 'Operating mode', + status: 'Status', + frozen: 'Frozen', + storeCount: 'Stores', + createdAt: 'Created at' + } + }, + review: { + title: 'Merchant Review', + claim: 'Claim', + release: 'Release', + approve: 'Approve', + reject: 'Reject', + revoke: 'Revoke', + reason: 'Reason', + comment: 'Comment', + result: 'Result', + claimed: 'Claimed', + unclaimed: 'Unclaimed', + readonlyTip: 'This review is claimed by another operator', + reasonPlaceholder: 'Enter rejection reason', + selectResult: 'Select review result', + needClaim: 'Please claim before reviewing', + success: 'Review submitted', + pendingReApproval: 'Pending re-approval', + empty: 'No audit records', + section: { + action: 'Review Action', + history: 'Audit History' + } + }, + detail: { + title: 'Merchant Detail', + basicInfo: 'Basic info', + subjectInfo: 'Entity info', + stores: 'Stores', + auditHistory: 'Audit history', + changeHistory: 'Change history' + }, + fields: { + name: 'Merchant name', + tenant: 'Tenant', + status: 'Status', + operatingMode: 'Operating mode', + licenseNumber: 'License number', + legalRepresentative: 'Legal representative', + registeredAddress: 'Registered address', + contactPhone: 'Contact phone', + contactEmail: 'Contact email', + isFrozen: 'Frozen', + frozenReason: 'Frozen reason', + approvedAt: 'Approved at', + approvedBy: 'Approved by', + createdAt: 'Created at', + updatedAt: 'Updated at' + }, + status: { + pending: 'Pending', + approved: 'Approved', + rejected: 'Rejected', + frozen: 'Frozen', + unknown: 'Unknown' + }, + operatingMode: { + same: 'Same entity', + different: 'Different entity' + }, + frozen: { + yes: 'Frozen', + no: 'Normal' + }, + action: { + edit: 'Edit', + detail: 'Detail', + export: 'Export PDF' + }, + placeholder: { + name: 'Enter merchant name', + licenseNumber: 'Enter license number', + legalRepresentative: 'Enter legal representative', + registeredAddress: 'Enter registered address', + contactPhone: 'Enter contact phone', + contactEmail: 'Enter contact email' + }, + rules: { + nameRequired: 'Merchant name is required', + contactPhoneRequired: 'Contact phone is required', + contactEmailInvalid: 'Invalid email format' + }, + message: { + rowVersionMissing: 'Missing row version, please refresh', + updateSuccess: 'Update successful', + updateRequiresReview: 'Critical changes require re-approval' + }, + store: { + name: 'Store name', + status: 'Store status', + address: 'Address', + contactPhone: 'Contact phone', + licenseNumber: 'License number', + empty: 'No stores', + statusOperating: 'Operating', + statusPreparing: 'Preparing', + statusClosed: 'Closed', + statusSuspended: 'Suspended', + statusUnknown: 'Unknown' + }, + change: { + field: 'Field', + oldValue: 'Old value', + newValue: 'New value', + changedBy: 'Changed by', + changedAt: 'Changed at', + empty: 'No change records' + } +} diff --git a/src/locales/en/store.ts b/src/locales/en/store.ts new file mode 100644 index 0000000..f703256 --- /dev/null +++ b/src/locales/en/store.ts @@ -0,0 +1,506 @@ +/** + * Store module i18n (en) + * + * Note: merged into `store.*` namespace during i18n init. + */ +export default { + list: { + search: { + keyword: 'Keyword', + keywordPlaceholder: 'Store name/code', + merchantId: 'Merchant ID', + merchantIdPlaceholder: 'Enter merchant ID', + auditStatus: 'Audit Status', + businessStatus: 'Business Status', + ownershipType: 'Ownership Type' + }, + table: { + name: 'Store Name', + code: 'Store Code', + ownershipType: 'Ownership Type', + auditStatus: 'Audit Status', + businessStatus: 'Business Status', + phone: 'Phone', + createdAt: 'Created At' + }, + action: { + create: 'Create Store', + detail: 'View Detail', + edit: 'Edit', + toggleStatus: 'Toggle Status', + submitAudit: 'Submit Audit' + } + }, + form: { + createTitle: 'Create Store', + editTitle: 'Edit Store', + tabs: { + basic: 'Basic Info', + location: 'Location', + settings: 'Settings', + services: 'Services' + }, + merchantId: 'Merchant ID', + merchantIdPlaceholder: 'Enter merchant ID', + code: 'Store Code', + codePlaceholder: 'Enter store code', + name: 'Store Name', + namePlaceholder: 'Enter store name', + phone: 'Phone', + phonePlaceholder: 'Enter phone number', + managerName: 'Manager Name', + managerNamePlaceholder: 'Enter manager name', + status: 'Store Status', + signboardImageUrl: 'Signboard Image', + signboardImageUrlPlaceholder: 'Enter signboard image URL', + ownershipType: 'Ownership Type', + categoryId: 'Category ID', + categoryIdPlaceholder: 'Enter category ID', + deliveryRadiusKm: 'Delivery Radius (km)', + locationTitle: 'Location', + province: 'Province', + provincePlaceholder: 'Enter province', + city: 'City', + cityPlaceholder: 'Enter city', + district: 'District', + districtPlaceholder: 'Enter district', + address: 'Address', + addressPlaceholder: 'Enter address', + coordinate: 'Coordinates', + coordinatePlaceholder: 'e.g. 39.695954,116.074058', + coordinatePasteAction: 'Paste', + coordinatePickerAction: 'Open coordinate picker', + coordinateHint: 'Supports lat,lng or lng,lat; copy from Tencent picker', + coordinatePasteUnavailable: 'Clipboard access is unavailable', + coordinatePasteEmpty: 'Clipboard is empty, copy coordinates first', + coordinateInvalid: 'Invalid coordinate format', + settingsTitle: 'Service Settings', + announcement: 'Announcement', + announcementPlaceholder: 'Enter announcement', + tags: 'Tags', + tagsPlaceholder: 'Comma-separated tags', + supportsDineIn: 'Supports Dine-in', + supportsPickup: 'Supports Pickup', + supportsDelivery: 'Supports Delivery', + supportsReservation: 'Supports Reservation', + supportsQueueing: 'Supports Queueing' + }, + rules: { + merchantId: 'Merchant ID is required', + code: 'Store code is required', + name: 'Store name is required', + signboardImageUrl: 'Signboard image is required', + ownershipType: 'Select ownership type', + coordinateText: 'Coordinates are required', + coordinateTextFormat: 'Use lat,lng or lng,lat format' + }, + detail: { + title: 'Store Detail', + empty: 'No store data', + back: 'Back to List', + fields: { + name: 'Store Name', + code: 'Store Code', + auditStatus: 'Audit Status', + businessStatus: 'Business Status', + ownershipType: 'Ownership Type', + phone: 'Phone', + address: 'Address', + createdAt: 'Created At' + }, + tabs: { + basic: 'Basic Info', + qualification: 'Qualifications', + businessHours: 'Business Hours', + temporaryHours: 'Temporary Adjustments', + deliveryZone: 'Delivery Zones', + fee: 'Fee Config' + } + }, + qualification: { + warningTitle: 'Qualification Alerts', + complete: 'Complete', + incomplete: 'Incomplete', + expiringSoon: 'Expiring soon {count}', + expired: 'Expired {count}', + action: { + add: 'Add Qualification', + edit: 'Edit Qualification' + }, + table: { + type: 'Type', + documentNumber: 'Document No.', + expiresAt: 'Expires At', + status: 'Status' + }, + form: { + type: 'Type', + fileUrl: 'File URL', + fileUrlPlaceholder: 'Enter file URL', + documentNumber: 'Document No.', + documentNumberPlaceholder: 'Enter document number', + issuedAt: 'Issued At', + expiresAt: 'Expires At', + sortOrder: 'Sort Order' + }, + rules: { + type: 'Select qualification type', + fileUrl: 'File URL is required', + documentNumber: 'Document number is required', + expiresAt: 'Select expiry date' + }, + status: { + valid: 'Valid', + expiringSoon: 'Expiring Soon', + expired: 'Expired' + }, + type: { + businessLicense: 'Business License', + foodService: 'Food Service License', + storefront: 'Storefront Photo', + interior: 'Interior Photo' + }, + deleteTitle: 'Delete Qualification', + deleteConfirm: 'Are you sure to delete this qualification?' + }, + businessHours: { + tip: 'Cross-day hours are supported and overlap validation will run on save.', + action: { + add: 'Add Slot', + save: 'Save' + }, + table: { + dayOfWeek: 'Day', + hourType: 'Type', + startTime: 'Start Time', + endTime: 'End Time', + capacityLimit: 'Capacity', + notes: 'Notes', + notesPlaceholder: 'Notes' + }, + days: { + sunday: 'Sunday', + monday: 'Monday', + tuesday: 'Tuesday', + wednesday: 'Wednesday', + thursday: 'Thursday', + friday: 'Friday', + saturday: 'Saturday' + }, + hourType: { + normal: 'Normal', + reservation: 'Reservation Only', + pickup: 'Pickup/Delivery', + closed: 'Closed' + } + }, + temporaryHours: { + tip: 'Set closures, temporary openings, or adjusted hours for specific dates', + allDay: 'All Day', + action: { + add: 'Add Adjustment' + }, + table: { + dateRange: 'Date Range', + timeRange: 'Time Range', + overrideType: 'Type', + reason: 'Note' + }, + overrideType: { + closed: 'Closed', + temporaryOpen: 'Temporary Open', + modifiedHours: 'Modified Hours' + }, + dialog: { + addTitle: 'Add Temporary Adjustment', + editTitle: 'Edit Temporary Adjustment' + }, + form: { + dateRange: 'Date Range', + isAllDay: 'All Day', + timeRange: 'Time Range', + overrideType: 'Type', + reason: 'Note', + reasonPlaceholder: "e.g., Chinese New Year closure, Valentine's extended hours" + }, + rules: { + dateRequired: 'Please select date range', + typeRequired: 'Please select adjustment type', + timeRequired: 'Time range is required when not all-day' + }, + deleteConfirm: 'Are you sure you want to delete this adjustment?' + }, + deliveryZone: { + tip: 'Use GeoJSON polygons to define delivery zones and test delivery checks.', + action: { + add: 'Add Zone', + edit: 'Edit Zone' + }, + table: { + zoneName: 'Zone Name', + minimumOrderAmount: 'Minimum Order', + deliveryFee: 'Delivery Fee', + estimatedMinutes: 'ETA (min)' + }, + form: { + zoneName: 'Zone Name', + zoneNamePlaceholder: 'Enter zone name', + polygonGeoJson: 'GeoJSON', + polygonPlaceholder: 'Enter GeoJSON polygon', + drawPolygon: 'Draw Area', + drawPolygonTitle: 'Draw Delivery Zone', + editPolygon: 'Edit', + clearPolygon: 'Clear', + drawHint: 'Finish drawing and confirm to apply to GeoJSON', + mapKeyMissing: 'Tencent map key is not configured, cannot load the drawing tool.', + mapKeyMissingHint: 'Set VITE_TENCENT_MAP_KEY in your environment variables.', + minimumOrderAmount: 'Minimum Order', + deliveryFee: 'Delivery Fee', + estimatedMinutes: 'ETA (min)', + sortOrder: 'Sort Order' + }, + rules: { + zoneName: 'Zone name is required', + polygonGeoJson: 'GeoJSON is required' + }, + deleteTitle: 'Delete Delivery Zone', + deleteConfirm: 'Are you sure to delete this zone?', + check: { + title: 'Delivery Check', + action: 'Check', + longitude: 'Longitude', + latitude: 'Latitude', + inRange: 'In range', + outOfRange: 'Out of range', + distance: 'Distance', + zoneName: 'Matched Zone' + } + }, + fee: { + title: 'Fee Config', + minimumOrderAmount: 'Minimum Order', + deliveryFee: 'Delivery Fee', + packagingFeeMode: 'Packaging Fee Mode', + fixedPackagingFee: 'Fixed Packaging Fee', + freeDeliveryThreshold: 'Free Delivery Threshold', + packagingMode: { + fixed: 'Fixed', + perItem: 'Per Item' + }, + preview: { + title: 'Fee Preview', + action: 'Preview', + orderAmount: 'Order Amount', + itemCount: 'Item Count', + itemsTitle: 'Items', + addItem: 'Add Item', + skuId: 'SKU', + quantity: 'Qty', + packagingFee: 'Packaging Fee', + result: { + totalAmount: 'Total Amount', + totalFee: 'Total Fee', + deliveryFee: 'Delivery Fee', + packagingFee: 'Packaging Fee', + meetsMinimum: 'Meets Minimum', + shortfall: 'Shortfall' + } + } + }, + auditStatus: { + draft: 'Draft', + pending: 'Pending', + activated: 'Activated', + rejected: 'Rejected', + unknown: 'Unknown' + }, + businessStatus: { + title: 'Business Status', + targetStatus: 'Target Status', + closureReason: 'Closure Reason', + closureReasonText: 'Details', + closureReasonTextPlaceholder: 'Enter details', + forceClosedTip: 'This store is force closed and cannot be changed by tenant.', + rules: { + status: 'Select status', + reason: 'Select a reason' + }, + open: 'Open', + resting: 'Resting', + forceClosed: 'Force Closed', + unknown: 'Unknown' + }, + ownership: { + same: 'Same Entity', + different: 'Different Entity' + }, + closureReason: { + equipment: 'Equipment Maintenance', + vacation: 'Owner Vacation', + outOfStock: 'Out of Stock', + temporarilyClosed: 'Temporarily Closed', + other: 'Other' + }, + status: { + closed: 'Closed', + preparing: 'Preparing', + operating: 'Operating', + suspended: 'Suspended' + }, + action: { + save: 'Save', + edit: 'Edit', + delete: 'Delete', + refresh: 'Refresh' + }, + message: { + createSuccess: 'Created successfully', + updateSuccess: 'Updated successfully', + deleteSuccess: 'Deleted successfully', + statusUpdated: 'Status updated', + submitAuditSuccess: 'Audit submitted' + }, + audit: { + stats: { + pending: 'Pending', + overdue: 'Overdue', + approved: 'Approved', + rejected: 'Rejected', + avgProcessing: 'Avg Processing (hours)' + }, + search: { + keyword: 'Keyword', + keywordPlaceholder: 'Store name/merchant name', + tenantId: 'Tenant ID', + tenantIdPlaceholder: 'Enter tenant ID', + dateRange: 'Submitted Range', + dateRangeStart: 'Start', + dateRangeEnd: 'End', + overdueOnly: 'Overdue Only' + }, + table: { + storeName: 'Store Name', + storeCode: 'Store Code', + tenantName: 'Tenant', + merchantName: 'Merchant', + ownershipType: 'Ownership', + submittedAt: 'Submitted At', + waitingDays: 'Waiting Days', + qualificationCount: 'Qualifications', + overdue: 'Overdue' + }, + action: { + detail: 'View Detail', + approve: 'Approve', + reject: 'Reject', + riskControl: 'Risk Control' + }, + overdue: 'Overdue', + notOverdue: 'Normal', + detail: { + title: 'Audit Detail', + empty: 'No data', + loadFailed: 'Failed to load detail', + viewFile: 'View File', + tabs: { + basic: 'Basic', + qualification: 'Qualifications', + history: 'History' + }, + sections: { + tenant: 'Tenant', + merchant: 'Merchant' + }, + fields: { + storeName: 'Store Name', + storeCode: 'Store Code', + auditStatus: 'Audit Status', + ownershipType: 'Ownership Type', + phone: 'Phone', + address: 'Address', + submittedAt: 'Submitted At', + signboard: 'Signboard', + tenantName: 'Tenant', + tenantContact: 'Contact', + tenantPhone: 'Phone', + merchantName: 'Merchant', + merchantLegal: 'Legal Name', + merchantCredit: 'Credit Code', + file: 'File' + } + }, + history: { + action: 'Action', + operator: 'Operator', + remark: 'Remark', + createdAt: 'Time' + }, + approve: { + prompt: 'Optional remark for approval', + placeholder: 'Enter remark', + success: 'Approved successfully' + }, + reject: { + title: 'Reject Audit', + reason: 'Reason', + reasonText: 'Details', + reasonTextPlaceholder: 'Enter details', + remark: 'Remark', + remarkPlaceholder: 'Enter remark', + success: 'Rejected successfully', + rules: { + reason: 'Select a reason', + reasonText: 'Enter reason details' + }, + reasonOptions: { + licenseMissing: 'Missing documents', + photoBlur: 'Blurry photos', + inconsistent: 'Inconsistent information', + other: 'Other' + } + }, + riskControl: { + title: 'Risk Control', + storeName: 'Current store: {name}', + action: 'Action', + forceClose: 'Force Close', + reopen: 'Reopen', + reason: 'Reason', + reasonPlaceholder: 'Enter reason', + remark: 'Remark', + remarkPlaceholder: 'Enter remark', + forceCloseSuccess: 'Store force closed', + reopenSuccess: 'Store reopened', + rules: { + action: 'Select action', + reason: 'Reason is required' + } + } + }, + alerts: { + summary: { + expiringSoon: 'Expiring Soon', + expired: 'Expired' + }, + search: { + tenantId: 'Tenant ID', + tenantIdPlaceholder: 'Enter tenant ID', + daysThreshold: 'Threshold (days)', + expired: 'Expired' + }, + table: { + storeName: 'Store Name', + storeCode: 'Store Code', + tenantName: 'Tenant', + qualificationType: 'Qualification', + expiresAt: 'Expires At', + daysUntilExpiry: 'Days Left', + status: 'Status', + businessStatus: 'Business Status' + }, + status: { + expiring: 'Expiring Soon', + expired: 'Expired' + } + } +} diff --git a/src/locales/en/tenant.ts b/src/locales/en/tenant.ts new file mode 100644 index 0000000..3c4552a --- /dev/null +++ b/src/locales/en/tenant.ts @@ -0,0 +1,267 @@ +/** + * Tenant module i18n (en) + * + * Note: This file is merged into `tenant.*` namespace at i18n bootstrap. + */ +export default { + // Detail Drawer + detail: { + title: 'Tenant Detail', + basicInfo: 'Basic Info', + statusInfo: 'Status Info', + subscriptionInfo: 'Subscription', + quotaInfo: 'Quota', + billingInfo: 'Billing', + + tenantId: 'Tenant ID', + name: 'Tenant Name', + code: 'Tenant Code', + shortName: 'Short Name', + industry: 'Industry', + contactName: 'Contact Name', + contactPhone: 'Contact Phone', + contactEmail: 'Contact Email', + + status: 'Tenant Status', + verificationStatus: 'Verification Status', + autoRenew: 'Auto Renew', + effectiveFrom: 'Effective From', + effectiveTo: 'Effective To', + currentPackage: 'Current Plan' + }, + + // Tabs + tabs: { + basicInfo: 'Basic Info', + statusInfo: 'Status Info', + subscription: 'Subscription', + quotaOverview: 'Quota Overview', + billing: 'Billing' + }, + + // Quota + quota: { + title: 'Quota Overview', + usageSummary: 'Quota Usage', + usageHistory: 'Quota Usage Details', + purchaseHistory: 'Quota Purchase History', + + recordedAt: 'Recorded At', + changeType: 'Change Type', + snapshot: 'Snapshot', + historyNotice: 'Current data is real-time snapshot. History feature pending backend API.', + fullOrderId: 'Full Order ID', + + orderNo: 'Order No', + quotaType: 'Quota Type', + quotaPackage: 'Quota Package', + limitValue: 'Limit', + usedValue: 'Used', + remainingValue: 'Remaining', + usagePercentage: 'Usage Rate', + resetCycle: 'Reset Cycle', + lastResetAt: 'Last Reset At', + + purchaseValue: 'Quantity', + price: 'Price', + purchasedAt: 'Purchased At', + expiredAt: 'Expired At', + notes: 'Notes', + + type: { + store: 'Stores', + account: 'Accounts', + storageGb: 'Storage (GB)', + smsCredits: 'SMS Credits', + deliveryOrders: 'Delivery Orders', + unknown: 'Unknown ({value})' + } + }, + + // 公告管理 + announcement: { + title: 'Tenant Announcements', + listTitle: 'Announcement List', + createTitle: 'Create Announcement', + editTitle: 'Edit Announcement', + detailTitle: 'Announcement Detail', + + search: { + status: 'Status', + statusPlaceholder: 'All Statuses', + keyword: 'Keyword', + keywordPlaceholder: 'Title/Content keyword', + dateRange: 'Date Range', + dateRangePlaceholder: 'Select date range' + }, + + field: { + title: 'Title', + content: 'Content', + announcementType: 'Type', + priority: 'Priority', + status: 'Status', + effectiveRange: 'Effective Range', + effectiveFrom: 'Effective From', + effectiveTo: 'Effective To', + targetType: 'Audience', + targetParameters: 'Target Parameters', + departmentIds: 'Department IDs', + roleIds: 'Role IDs', + tagIds: 'Tag IDs', + publishedAt: 'Published At', + revokedAt: 'Revoked At', + isActive: 'Active', + rowVersion: 'Row Version' + }, + + action: { + create: 'Create', + edit: 'Edit', + detail: 'Detail', + publish: 'Publish', + revoke: 'Revoke', + delete: 'Delete', + save: 'Save', + back: 'Back', + more: 'More' + }, + + status: { + draft: 'Draft', + published: 'Published', + revoked: 'Revoked' + }, + + type: { + system: 'System', + billing: 'Billing/Subscription', + operation: 'Operation', + systemPlatformUpdate: 'Platform Update', + systemSecurityNotice: 'Security Notice', + systemCompliance: 'Compliance', + tenantInternal: 'Tenant Internal', + tenantFinance: 'Tenant Finance', + tenantOperation: 'Tenant Operation' + }, + + targetType: { + all: 'All', + roles: 'Roles', + users: 'Users', + rules: 'Rules', + manual: 'Manual' + }, + + placeholder: { + title: 'Enter announcement title', + content: 'Enter announcement content', + type: 'Select announcement type', + priority: 'Enter priority', + effectiveRange: 'Select effective range', + targetType: 'Select audience', + roleIds: 'Enter role IDs, separated by commas', + userIds: 'Enter user IDs, separated by commas', + departmentIds: 'Enter department IDs, separated by commas', + tagIds: 'Enter tag IDs, separated by commas' + }, + + validation: { + titleRequired: 'Please enter title', + titleLength: 'Title must be 2-128 characters', + contentRequired: 'Please enter content', + typeRequired: 'Please select type', + priorityRequired: 'Please enter priority', + effectiveRangeRequired: 'Please select effective range', + targetTypeRequired: 'Please select audience', + roleIdsRequired: 'Please enter at least one role ID', + userIdsRequired: 'Please enter at least one user ID', + targetRuleRequired: 'Please fill at least one rule field' + }, + + message: { + loadListFailed: 'Failed to load list', + loadDetailFailed: 'Failed to load detail', + detailNotFound: 'Announcement not found. Please return to the list.', + tenantIdMissing: 'Tenant ID not found', + publishConfirm: 'Publish this announcement?', + revokeConfirm: 'Revoke this announcement?', + deleteConfirm: 'Delete this announcement?', + publishSuccess: 'Announcement published', + revokeSuccess: 'Announcement revoked', + deleteSuccess: 'Announcement deleted', + createSuccess: 'Announcement created', + updateSuccess: 'Announcement updated', + actionNotAllowed: 'Action not allowed for current status', + empty: 'No announcements' + }, + + tip: { + tenantScope: 'This is a tenant-level announcement and will be sent to this tenant only.', + draftAutoSave: 'Auto-saving draft', + draftSaved: 'Draft saved at {time}', + draftLoaded: 'Draft loaded', + draftNotEditable: 'Only draft announcements can be edited' + }, + + section: { + content: 'Content', + audience: 'Audience' + } + }, + + // Edit/Create Dialog + edit: { + titleEdit: 'Edit Tenant', + titleCreate: 'Add Tenant', + + placeholder: { + code: 'Enter tenant code', + name: 'Enter tenant name', + shortName: 'Enter short name', + industry: 'Enter industry', + contactName: 'Enter contact name', + contactPhone: 'Enter contact phone', + contactEmail: 'Enter contact email', + tenantPackageId: 'Select plan', + effectiveFrom: 'Select effective date' + }, + + package: { + title: 'Plan Info', + select: 'Plan', + durationMonths: 'Duration' + }, + + packageType: { + free: 'Free', + paid: 'Paid' + }, + + unit: { + month: 'month(s)' + }, + + validation: { + codeRequired: 'Please enter tenant code', + nameRequired: 'Please enter tenant name', + tenantPackageIdRequired: 'Please select plan' + } + }, + + // Messages + warning: { + updateApiNotReady: 'Tenant update API is under development. Please try again later.' + }, + error: { + loadDetailFailed: 'Failed to load tenant detail', + loadQuotaFailed: 'Failed to load quota info', + loadBillingFailed: 'Failed to load billing info', + loadSubscriptionFailed: 'Failed to load subscription info', + updateFailed: 'Update failed, please retry' + }, + success: { + registerSuccess: 'Registered successfully', + updateSuccess: 'Updated successfully' + } +} diff --git a/src/locales/en/tenantPackage.ts b/src/locales/en/tenantPackage.ts new file mode 100644 index 0000000..f7bc6b6 --- /dev/null +++ b/src/locales/en/tenantPackage.ts @@ -0,0 +1,137 @@ +/** + * Tenant Package Module Locale (en) + */ +export default { + actions: { + more: 'More', + copy: 'Copy', + toggleActive: 'Enable/Disable', + toggleVisible: 'Show/Hide', + togglePurchasable: 'List/Unlist', + publish: 'Publish', + rollbackDraft: 'Rollback Draft', + quotaEdit: 'Edit Quota', + viewTenants: 'View Tenants', + delete: 'Delete', + view: 'View', + edit: 'Edit' + }, + drawer: { + title: 'Associated Tenants - {name}', + expiringTitle: 'Associated Tenants - {name} (Expiring in {days} days)', + searchPlaceholder: 'Search Tenant Name/Code', + tenantName: 'Tenant Name', + tenantCode: 'Tenant Code', + tenantStatus: 'Tenant Status', + contact: 'Contact', + phone: 'Phone', + effectiveTo: 'Effective To' + }, + search: { + keyword: 'Keyword', + keywordPlaceholder: 'Search Package Name', + status: 'Status', + statusAll: 'All', + statusEnabled: 'Enabled', + statusDisabled: 'Disabled' + }, + form: { + name: 'Package Name', + namePlaceholder: 'Enter package name', + packageType: 'Package Type', + packageTypePlaceholder: 'Select package type', + monthlyPrice: 'Monthly Price', + yearlyPrice: 'Yearly Price', + pricePlaceholder: 'Enter price', + maxStoreCount: 'Max Stores', + maxAccountCount: 'Max Accounts', + maxStorageGb: 'Max Storage (GB)', + maxSmsCredits: 'SMS Credits', + maxDeliveryOrders: 'Max Delivery Orders', + quotaPlaceholder: 'Enter quantity (-1 for unlimited)', + featurePoliciesJson: 'Feature Policies', + featurePlaceholder: 'Enter JSON configuration', + isActive: 'Active', + publicVisible: 'Public Visible', + allowNewPurchase: 'Allow New Purchase', + publishStatus: 'Publish Status', + isRecommended: 'Recommended', + sortOrder: 'Sort Order', + submit: 'Submit' + }, + status: { + enabled: 'Enabled', + disabled: 'Disabled' + }, + publishStatus: { + published: 'Published', + draft: 'Draft' + }, + type: { + free: 'Free', + standard: 'Standard', + professional: 'Professional', + enterprise: 'Enterprise' + }, + tags: { + recommended: 'Recommended', + bestValue: 'Best Value', + flagship: 'Flagship' + }, + table: { + name: 'Package Name', + packageType: 'Type', + publishStatus: 'Status', + publicVisible: 'Public', + allowNewPurchase: 'Buyable', + monthlyPrice: 'Monthly', + yearlyPrice: 'Yearly', + quota: 'Quota', + usage: 'Usage', + status: 'Active', + actions: 'Actions', + quotaEmpty: 'Unlimited' + }, + message: { + updateSuccess: 'Update Successful', + + toggleConfirmEnable: 'Enable this package?', + toggleConfirmDisable: 'Disable this package? New purchases will be blocked.', + toggleTitleEnable: 'Enable', + toggleTitleDisable: 'Disable', + toggleSuccessEnable: 'Enabled', + toggleSuccessDisable: 'Disabled', + + toggleVisibleConfirmEnable: 'Make public visible?', + toggleVisibleConfirmDisable: 'Hide package?', + toggleVisibleTitleEnable: 'Show', + toggleVisibleTitleDisable: 'Hide', + toggleVisibleSuccessEnable: 'Package is now visible', + toggleVisibleSuccessDisable: 'Package is now hidden', + + togglePurchasableConfirmEnable: 'Allow new purchases?', + togglePurchasableConfirmDisable: 'Disallow new purchases?', + togglePurchasableTitleEnable: 'Allow Purchase', + togglePurchasableTitleDisable: 'Disallow Purchase', + togglePurchasableSuccessEnable: 'Purchases Allowed', + togglePurchasableSuccessDisable: 'Purchases Disabled', + + publishConfirm: 'Publish this package? It will be visible to tenants.', + publishTitle: 'Publish', + publishSuccess: 'Published Successfully', + + rollbackDraftConfirm: 'Rollback to draft? New users cannot see it.', + rollbackDraftTitle: 'Rollback', + rollbackDraftSuccess: 'Rolled back to draft', + + deleteConfirm: 'Delete this package? This cannot be undone.', + deleteTitle: 'Delete', + deleteSuccess: 'Deleted Successfully' + }, + dialog: { + quotaTitle: 'Quota Management - {name}' + }, + featurePolicy: { + invalidJson: 'Invalid JSON format' + } +} diff --git a/src/locales/index.ts b/src/locales/index.ts new file mode 100644 index 0000000..668f1b0 --- /dev/null +++ b/src/locales/index.ts @@ -0,0 +1,193 @@ +/** + * 国际化配置 + * + * 基于 vue-i18n 实现的多语言国际化解决方案。 + * 支持中文和英文切换,自动从本地存储恢复用户的语言偏好。 + * + * ## 主要功能 + * + * - 多语言支持 - 支持中文(简体)和英文两种语言 + * - 语言切换 - 运行时动态切换语言,无需刷新页面 + * - 持久化存储 - 自动保存和恢复用户的语言偏好 + * - 全局注入 - 在任何组件中都可以使用 $t 函数进行翻译 + * - 类型安全 - 提供 TypeScript 类型支持 + * + * ## 支持的语言 + * + * - zh: 简体中文 + * - en: English + * + * @module locales + * @author Art Design Pro Team + */ + +import { createI18n } from 'vue-i18n' +import type { I18n, I18nOptions } from 'vue-i18n' +import { LanguageEnum } from '@/enums/appEnum' +import { getSystemStorage } from '@/utils/storage' +import { StorageKeyManager } from '@/utils/storage/storage-key-manager' + +// 同步导入语言文件 +import enMessages from './langs/en.json' +import zhMessages from './langs/zh.json' + +// 业务模块语言包(按模块拆分,避免主语言文件过大) +import enBillingMessages from './en-US/billing.json' +import zhBillingMessages from './zh-CN/billing.json' +import enDictionaryMessages from './en/dictionary.json' +import zhDictionaryMessages from './zh-CN/dictionary.json' +import enTenantMessages from './en/tenant' +import zhTenantMessages from './zh-CN/tenant' +import enAnnouncementMessages from './en/announcement' +import zhAnnouncementMessages from './zh-CN/announcement' +import enMerchantMessages from './en/merchant' +import zhMerchantMessages from './zh-CN/merchant' +import enStoreMessages from './en/store' +import zhStoreMessages from './zh-CN/store' +import enTenantPackageMessages from './en/tenantPackage' +import zhTenantPackageMessages from './zh-CN/tenantPackage' + +/** + * 存储键管理器实例 + */ +const storageKeyManager = new StorageKeyManager() + +/** + * 语言消息对象 + */ +type JsonObject = Record + +const isPlainObject = (value: unknown): value is JsonObject => { + return typeof value === 'object' && value !== null && !Array.isArray(value) +} + +/** + * 深度合并语言包(用于按模块拆分语言文件) + * - 数组:直接覆盖 + * - 对象:递归合并 + * - 其他:直接覆盖 + */ +const deepMerge = (target: T, source: U): T & U => { + const result: JsonObject = { ...target } + + Object.keys(source).forEach((key) => { + const sourceValue = source[key] + const targetValue = result[key] + + if (isPlainObject(targetValue) && isPlainObject(sourceValue)) { + result[key] = deepMerge(targetValue, sourceValue) + return + } + + result[key] = sourceValue + }) + + return result as T & U +} + +const messages = { + [LanguageEnum.EN]: deepMerge( + deepMerge( + deepMerge(enMessages as unknown as JsonObject, enBillingMessages as unknown as JsonObject), + enDictionaryMessages as unknown as JsonObject + ), + { + tenant: enTenantMessages, + tenantPackage: enTenantPackageMessages, + announcement: enAnnouncementMessages, + merchant: enMerchantMessages, + store: enStoreMessages + } as unknown as JsonObject + ), + [LanguageEnum.ZH]: deepMerge( + deepMerge( + deepMerge(zhMessages as unknown as JsonObject, zhBillingMessages as unknown as JsonObject), + zhDictionaryMessages as unknown as JsonObject + ), + { + tenant: zhTenantMessages, + tenantPackage: zhTenantPackageMessages, + announcement: zhAnnouncementMessages, + merchant: zhMerchantMessages, + store: zhStoreMessages + } as unknown as JsonObject + ) +} as unknown as NonNullable + +/** + * 语言选项列表 + * 用于语言切换下拉框 + */ +export const languageOptions = [ + { value: LanguageEnum.ZH, label: '简体中文' }, + { value: LanguageEnum.EN, label: 'English' } +] + +/** + * 从存储中获取语言设置 + * @returns 语言设置,如果获取失败则返回默认语言 + */ +const getDefaultLanguage = (): LanguageEnum => { + // 尝试从版本化的存储中获取语言设置 + try { + const storageKey = storageKeyManager.getStorageKey('user') + const userStore = localStorage.getItem(storageKey) + + if (userStore) { + const { language } = JSON.parse(userStore) + if (language && Object.values(LanguageEnum).includes(language)) { + return language + } + } + } catch (error) { + console.warn('[i18n] 从版本化存储获取语言设置失败:', error) + } + + // 尝试从系统存储中获取语言设置 + try { + const sys = getSystemStorage() + if (sys) { + const { user } = JSON.parse(sys) + if (user?.language && Object.values(LanguageEnum).includes(user.language)) { + return user.language + } + } + } catch (error) { + console.warn('[i18n] 从系统存储获取语言设置失败:', error) + } + + // 返回默认语言 + console.debug('[i18n] 使用默认语言:', LanguageEnum.ZH) + return LanguageEnum.ZH +} + +/** + * i18n 配置选项 + */ +const i18nOptions: I18nOptions = { + locale: getDefaultLanguage(), + legacy: false, + globalInjection: true, + fallbackLocale: LanguageEnum.ZH, + messages +} + +/** + * i18n 实例 + */ +const i18n: I18n = createI18n(i18nOptions) + +/** + * 翻译函数类型 + */ +interface Translation { + (key: string): string +} + +/** + * 全局翻译函数 + * 可在任何地方使用,无需导入 useI18n + */ +export const $t = i18n.global.t as Translation + +export default i18n diff --git a/src/locales/lang/en/announcement.ts b/src/locales/lang/en/announcement.ts new file mode 100644 index 0000000..7084b68 --- /dev/null +++ b/src/locales/lang/en/announcement.ts @@ -0,0 +1,227 @@ +/** + * 公告模块国际化(en) + * + * 注意:该文件会在 i18n 初始化时合并到 `announcement.*` 命名空间下 + */ +export default { + title: 'Announcement Management', + + platform: { + title: 'Platform Announcements', + list: 'Platform Announcement List', + create: 'Create Platform Announcement', + edit: 'Edit Platform Announcement', + detail: 'Platform Announcement Detail' + }, + + tenant: { + title: 'Tenant Announcements', + list: 'Tenant Announcement List', + create: 'Create Tenant Announcement', + edit: 'Edit Tenant Announcement', + detail: 'Tenant Announcement Detail' + }, + + app: { + title: 'App Announcements', + list: 'Announcements', + detail: 'Announcement Detail', + center: 'Announcement Center', + unread: 'Unread', + all: 'All', + markAllRead: 'Mark All as Read', + emptyUnread: 'No unread announcements', + emptyAll: 'No announcements' + }, + + drafts: { + title: 'Draft Center', + list: 'Draft List', + empty: 'No drafts', + save: 'Save Draft', + saving: 'Saving', + savedAt: 'Saved · {time}', + restore: 'Restore Draft', + discard: 'Discard Draft', + autoSaveOn: 'Auto-save enabled', + autoSaveOff: 'Auto-save disabled' + }, + + list: { + title: 'Announcement List', + total: '{count} items', + selected: '{count} selected', + empty: 'No announcements' + }, + + table: { + title: 'Title', + type: 'Type', + priority: 'Priority', + status: 'Status', + effectiveRange: 'Effective Range', + publishedAt: 'Published At', + actions: 'Actions' + }, + + detail: { + title: 'Announcement Detail', + readStatus: 'Read Status', + unreadTip: 'Unread', + readTip: 'Read' + }, + + form: { + title: 'Announcement Info', + base: 'Basic Info', + audience: 'Audience Settings', + schedule: 'Publish Settings', + preview: 'Preview Settings' + }, + + fields: { + title: 'Title', + summary: 'Summary', + content: 'Content', + type: 'Type', + priority: 'Priority', + status: 'Status', + publisher: 'Publisher', + publisherScope: 'Publisher Scope', + targetType: 'Audience', + effectiveFrom: 'Effective From', + effectiveTo: 'Effective To', + publishedAt: 'Published At', + revokedAt: 'Revoked At', + scheduledPublishAt: 'Scheduled Publish At', + createdAt: 'Created At', + updatedAt: 'Updated At', + readStatus: 'Read Status', + readAt: 'Read At', + tenantId: 'Tenant ID' + }, + + search: { + keyword: 'Keyword', + keywordPlaceholder: 'Search title/content', + status: 'Status', + statusPlaceholder: 'All statuses', + dateRange: 'Date Range', + dateFrom: 'Start Date', + dateTo: 'End Date', + targetType: 'Audience', + publisherScope: 'Publisher Scope', + announcementType: 'Announcement Type', + priority: 'Priority' + }, + + status: { + draft: 'Draft', + published: 'Published', + revoked: 'Revoked', + scheduled: 'Scheduled', + active: 'Active', + inactive: 'Inactive', + read: 'Read', + unread: 'Unread' + }, + + priority: { + low: 'Low', + normal: 'Normal', + high: 'High', + urgent: 'Urgent' + }, + + targetType: { + all: 'All Users', + roles: 'Roles', + users: 'Users', + rules: 'Rules', + manual: 'Manual' + }, + + publish: { + mode: 'Publish Mode', + immediate: 'Publish Now', + scheduled: 'Schedule', + scheduleTime: 'Scheduled Time' + }, + + action: { + create: 'Create', + edit: 'Edit', + view: 'View', + delete: 'Delete', + publish: 'Publish', + revoke: 'Revoke', + preview: 'Preview', + save: 'Save', + saveDraft: 'Save Draft', + markRead: 'Mark as Read', + markAllRead: 'Mark All as Read', + refresh: 'Refresh', + reset: 'Reset', + back: 'Back', + close: 'Close' + }, + + messages: { + createSuccess: 'Created successfully', + updateSuccess: 'Updated successfully', + deleteSuccess: 'Deleted successfully', + publishSuccess: 'Published successfully', + revokeSuccess: 'Revoked successfully', + saveDraftSuccess: 'Draft saved', + loadFailed: 'Failed to load', + empty: 'No data', + deleteConfirm: 'Are you sure you want to delete this announcement?', + revokeConfirm: 'Are you sure you want to revoke this announcement?', + publishConfirm: 'Are you sure you want to publish this announcement?', + discardDraftConfirm: 'Discard this draft?', + audienceEstimateFailed: 'Failed to estimate audience. Please try again.', + permissionDenied: 'You do not have permission to perform this action', + deleteNotSupported: 'Platform announcements cannot be deleted directly' + }, + + validation: { + titleRequired: 'Please enter a title', + contentRequired: 'Please enter content', + effectiveFromRequired: 'Please select effective start time', + targetTypeRequired: 'Please select audience', + announcementTypeRequired: 'Please select announcement type', + priorityRequired: 'Please select priority' + }, + + tabs: { + base: 'Basic Info', + audience: 'Audience', + schedule: 'Publish', + preview: 'Preview' + }, + + preview: { + openPreview: 'Preview', + drawerTitle: 'Announcement Preview', + titlePlaceholder: 'Enter announcement title', + emptyContent: 'No announcement content', + publishAt: 'Publish Time', + publishAtUnknown: 'Pending', + priorityTag: 'Priority P{level}', + priorityUnknown: 'Not set', + resizeHandle: 'Resize preview width' + }, + + type: { + system: 'System Announcement', + billing: 'Billing Notice', + operation: 'Operations Notice', + systemPlatformUpdate: 'Platform System Update', + systemSecurityNotice: 'System Security Notice', + systemCompliance: 'System Compliance Notice', + tenantInternal: 'Tenant Internal Notice', + tenantFinance: 'Tenant Finance Notice', + tenantOperation: 'Tenant Operations Notice', + unknown: 'Unknown Type' + } +} diff --git a/src/locales/lang/zh-CN/announcement.ts b/src/locales/lang/zh-CN/announcement.ts new file mode 100644 index 0000000..df5c7c8 --- /dev/null +++ b/src/locales/lang/zh-CN/announcement.ts @@ -0,0 +1,227 @@ +/** + * 公告模块国际化(zh-CN) + * + * 注意:该文件会在 i18n 初始化时合并到 `announcement.*` 命名空间下 + */ +export default { + title: '公告管理', + + platform: { + title: '平台公告', + list: '平台公告列表', + create: '创建平台公告', + edit: '编辑平台公告', + detail: '平台公告详情' + }, + + tenant: { + title: '租户公告', + list: '租户公告列表', + create: '创建租户公告', + edit: '编辑租户公告', + detail: '租户公告详情' + }, + + app: { + title: '应用端公告', + list: '公告列表', + detail: '公告详情', + center: '公告中心', + unread: '未读公告', + all: '全部公告', + markAllRead: '全部标记已读', + emptyUnread: '暂无未读公告', + emptyAll: '暂无公告' + }, + + drafts: { + title: '草稿中心', + list: '草稿列表', + empty: '暂无草稿', + save: '保存草稿', + saving: '保存中', + savedAt: '已保存 · {time}', + restore: '恢复草稿', + discard: '丢弃草稿', + autoSaveOn: '自动保存已开启', + autoSaveOff: '自动保存已关闭' + }, + + list: { + title: '公告列表', + total: '共 {count} 条', + selected: '已选 {count} 条', + empty: '暂无公告' + }, + + table: { + title: '标题', + type: '类型', + priority: '优先级', + status: '状态', + effectiveRange: '生效时间', + publishedAt: '发布时间', + actions: '操作' + }, + + detail: { + title: '公告详情', + readStatus: '阅读状态', + unreadTip: '未读', + readTip: '已读' + }, + + form: { + title: '公告信息', + base: '基础信息', + audience: '受众设置', + schedule: '发布设置', + preview: '预览设置' + }, + + fields: { + title: '标题', + summary: '摘要', + content: '内容', + type: '公告类型', + priority: '优先级', + status: '状态', + publisher: '发布人', + publisherScope: '发布范围', + targetType: '目标受众', + effectiveFrom: '生效开始时间', + effectiveTo: '生效结束时间', + publishedAt: '发布时间', + revokedAt: '撤销时间', + scheduledPublishAt: '计划发布时间', + createdAt: '创建时间', + updatedAt: '更新时间', + readStatus: '已读状态', + readAt: '已读时间', + tenantId: '租户ID' + }, + + search: { + keyword: '关键词', + keywordPlaceholder: '标题/内容关键词', + status: '状态', + statusPlaceholder: '全部状态', + dateRange: '日期范围', + dateFrom: '开始日期', + dateTo: '结束日期', + targetType: '受众类型', + publisherScope: '发布范围', + announcementType: '公告类型', + priority: '优先级' + }, + + status: { + draft: '草稿', + published: '已发布', + revoked: '已撤销', + scheduled: '待发布', + active: '生效中', + inactive: '未生效', + read: '已读', + unread: '未读' + }, + + priority: { + low: '低', + normal: '普通', + high: '高', + urgent: '紧急' + }, + + targetType: { + all: '全部用户', + roles: '指定角色', + users: '指定用户', + rules: '规则模式', + manual: '手动选择' + }, + + publish: { + mode: '发布方式', + immediate: '立即发布', + scheduled: '定时发布', + scheduleTime: '定时发布时间' + }, + + action: { + create: '新建', + edit: '编辑', + view: '查看', + delete: '删除', + publish: '发布', + revoke: '撤销', + preview: '预览', + save: '保存', + saveDraft: '保存草稿', + markRead: '标记已读', + markAllRead: '全部已读', + refresh: '刷新', + reset: '重置', + back: '返回', + close: '关闭' + }, + + messages: { + createSuccess: '创建成功', + updateSuccess: '更新成功', + deleteSuccess: '删除成功', + publishSuccess: '发布成功', + revokeSuccess: '撤销成功', + saveDraftSuccess: '草稿已保存', + loadFailed: '加载失败', + empty: '暂无数据', + deleteConfirm: '确认删除该公告?', + revokeConfirm: '确认撤销该公告?', + publishConfirm: '确认发布该公告?', + discardDraftConfirm: '确认丢弃该草稿?', + audienceEstimateFailed: '受众预估失败,请稍后重试', + permissionDenied: '暂无权限执行该操作', + deleteNotSupported: '平台公告不支持直接删除' + }, + + validation: { + titleRequired: '请输入标题', + contentRequired: '请输入内容', + effectiveFromRequired: '请选择生效开始时间', + targetTypeRequired: '请选择目标受众', + announcementTypeRequired: '请选择公告类型', + priorityRequired: '请选择优先级' + }, + + tabs: { + base: '基本信息', + audience: '受众设置', + schedule: '发布设置', + preview: '预览' + }, + + preview: { + openPreview: '预览', + drawerTitle: '公告预览', + titlePlaceholder: '请输入公告标题', + emptyContent: '暂无公告内容', + publishAt: '发布时间', + publishAtUnknown: '待发布', + priorityTag: '优先级 P{level}', + priorityUnknown: '未设置', + resizeHandle: '拖动调整预览宽度' + }, + + type: { + system: '系统公告', + billing: '账单提醒', + operation: '运营通知', + systemPlatformUpdate: '平台系统更新公告', + systemSecurityNotice: '系统安全公告', + systemCompliance: '系统合规公告', + tenantInternal: '租户内部公告', + tenantFinance: '租户财务公告', + tenantOperation: '租户运营公告', + unknown: '未知类型' + } +} diff --git a/src/locales/langs/en.json b/src/locales/langs/en.json new file mode 100644 index 0000000..571f780 --- /dev/null +++ b/src/locales/langs/en.json @@ -0,0 +1,2114 @@ +{ + "httpMsg": { + "unauthorized": "Unauthorized access, please login again", + "forbidden": "Access to this resource is forbidden", + "notFound": "The requested resource does not exist", + "methodNotAllowed": "Request method not allowed", + "conflict": "Data has been updated by someone else. Refresh and retry.", + "validationFailed": "Request validation failed", + "requestTimeout": "Request timeout, please try again later", + "internalServerError": "Internal server error, please try again later", + "badGateway": "Bad gateway error, please try again later", + "serviceUnavailable": "Service temporarily unavailable, please try again later", + "gatewayTimeout": "Gateway timeout, please try again later", + "requestCancelled": "Request cancelled", + "networkError": "Network connection error, please check your connection", + "requestFailed": "Request failed", + "requestConfigError": "Request configuration error" + }, + "topBar": { + "search": { + "title": "Search" + }, + "user": { + "userCenter": "User center", + "docs": "Document", + "github": "Github", + "lockScreen": "Lock screen", + "logout": "Log out" + }, + "guide": { + "title": "Click here to view", + "theme": "Theme style", + "menu": "Open top menu", + "description": "More configurations" + }, + "impersonation": { + "active": "Impersonating (Tenant {tenantId})", + "exit": "Exit", + "confirm": "Exit impersonation and return to the platform console?" + } + }, + "common": { + "id": "ID", + "tips": "Prompt", + "cancel": "Cancel", + "confirm": "Confirm", + "yes": "Yes", + "no": "No", + "days": "days", + "delete": "Delete", + "edit": "Edit", + "action": "Action", + "uploadImage": "Upload Image", + "fileTooLarge": "File too large, max {size}MB allowed", + "uploadFailed": "Upload failed", + "logOutTips": "Do you want to log out?", + "prevStep": "Previous Step", + "nextStep": "Next Step", + "unlimited": "Unlimited", + "startDate": "Start Date", + "endDate": "End Date", + "startTime": "Start Time", + "endTime": "End Time" + }, + "search": { + "placeholder": "Search page", + "historyTitle": "Search history", + "switchKeydown": "Navigate", + "selectKeydown": "Select", + "exitKeydown": "Close" + }, + "setting": { + "menuType": { + "title": "Menu Layout", + "list": [ + "Vertical", + "Horizontal", + "Mixed", + "Dual" + ] + }, + "theme": { + "title": "Theme Style", + "list": [ + "Light", + "Dark", + "System" + ] + }, + "menu": { + "title": "Menu Style" + }, + "color": { + "title": "Theme Color" + }, + "box": { + "title": "Box Style", + "list": [ + "Border", + "Shadow" + ] + }, + "container": { + "title": "Container Width", + "list": [ + "Full", + "Boxed" + ] + }, + "basics": { + "title": "Basic Config", + "list": { + "multiTab": "Show work tab", + "accordion": "Sidebar opens accordion", + "collapseSidebar": "Show sidebar button", + "reloadPage": "Show reload page button", + "fastEnter": "Show fast enter", + "breadcrumb": "Show crumb navigation", + "language": "Show multilingual selection", + "progressBar": "Show top progress bar", + "weakMode": "Color Weakness Mode", + "watermark": "Global watermark", + "menuWidth": "Menu width", + "tabStyle": "Tab style", + "pageTransition": "Page animation", + "borderRadius": "Custom radius" + } + }, + "tabStyle": { + "default": "Default", + "card": "Card", + "google": "Chrome" + }, + "transition": { + "list": { + "none": "None", + "fade": "Fade", + "slideLeft": "Slide Left", + "slideBottom": "Slide Bottom", + "slideTop": "Slide Top" + } + }, + "actions": { + "resetConfig": "Reset Config", + "copyConfig": "Copy Config", + "copySuccess": "Configuration copied to clipboard, paste it into src/config/setting.ts file", + "copyFailed": "Copy failed, please try again", + "resetFailed": "Reset failed, please refresh the page and try again" + } + }, + "notice": { + "title": "Notice", + "btnRead": "Mark as read", + "bar": [ + "Notice", + "Message", + "Todo" + ], + "text": [ + "No" + ], + "viewAll": "View all" + }, + "appAnnouncement": { + "list": { + "title": "Announcements", + "unreadCount": "Unread {count}", + "typePlaceholder": "Select announcement type", + "refresh": "Refresh", + "viewDetail": "View Detail", + "empty": "No announcements", + "publisher": "Publisher: ", + "publishedAt": "Published at: ", + "effective": "Effective: " + }, + "detail": { + "title": "Announcement Detail", + "back": "Back to List", + "publisher": "Publisher: ", + "publishedAt": "Published at: ", + "effective": "Effective: ", + "content": "Content" + }, + "type": { + "all": "All Types", + "system": "System Announcement", + "billing": "Billing Reminder", + "operation": "Operation Notice", + "platformUpdate": "Platform Update", + "security": "Security Notice", + "compliance": "Compliance Notice", + "tenantInternal": "Tenant Internal", + "tenantFinance": "Tenant Finance", + "tenantOperation": "Tenant Operation" + }, + "publisher": { + "platform": "Platform", + "tenant": "Tenant", + "unknown": "Unknown" + }, + "common": { + "notAvailable": "N/A", + "dateRange": "{start} ~ {end}" + }, + "message": { + "loadFailed": "Failed to load announcements, please try again.", + "missingId": "Missing announcement ID.", + "detailNotFound": "Announcement not found, please return to the list." + } + }, + "worktab": { + "btn": { + "refresh": "Refresh", + "fixed": "Fixed", + "unfixed": "Unfixed", + "closeLeft": "Close left", + "closeRight": "Close right", + "closeOther": "Close other", + "closeAll": "Close all" + } + }, + "login": { + "leftView": { + "title": "A backend system of beauty and efficiency", + "subTitle": "A sleek and practical interface for a great user experience" + }, + "title": "Welcome back", + "subTitle": "Please enter your account and password to login", + "roles": { + "super": "Super Admin", + "admin": "Admin", + "user": "User" + }, + "placeholder": { + "account": "Enter account", + "phone": "Enter phone number", + "password": "Please enter your password", + "slider": "Please slide to verify" + }, + "sliderText": "Please slide to verify", + "sliderSuccessText": "Verification successful", + "rememberPwd": "Remember password", + "forgetPwd": "Forgot password", + "btnText": "Login", + "noAccount": "No account yet?", + "register": "Register", + "success": { + "title": "Login successful", + "message": "Welcome back" + } + }, + "forgetPassword": { + "title": "Forgot password?", + "subTitle": "Enter your email to reset your password", + "placeholder": "Please enter your email", + "submitBtnText": "Submit", + "backBtnText": "Back" + }, + "resetPassword": { + "title": "Reset Password", + "subTitle": "Set a new login password", + "invalidToken": "The reset link is invalid or expired. Please ask the platform admin to generate a new link.", + "newPassword": "New Password", + "newPasswordPlaceholder": "Enter new password (6-32 chars)", + "confirmPassword": "Confirm Password", + "confirmPasswordPlaceholder": "Re-enter new password", + "submitBtnText": "Reset", + "backBtnText": "Back to Login", + "success": "Password reset succeeded. Please log in with the new password.", + "rules": { + "newPasswordRequired": "Please enter a new password", + "confirmPasswordRequired": "Please confirm the new password", + "passwordLength": "Password length must be 6-32 characters", + "passwordNotMatch": "Passwords do not match" + } + }, + "register": { + "title": "Self-service onboarding", + "subTitle": "Provide admin info to spin up your space quickly", + "layout": { + "brand": "CloudSaaS", + "badge": "Self-Service Onboarding", + "badgeDesc": "Password is only for login and will not return", + "title": "Start Your", + "titleHighlight": "Digital Management Journey", + "desc": "Open your dedicated workspace in just a few steps. Enjoy secure, stable, and efficient enterprise-grade management.", + "pillSecurity": "Enterprise Security", + "pillData": "Data Connectivity", + "pillDeploy": "Rapid Deployment", + "footer": "© CloudSaaS Inc. All rights reserved.", + "formTitle": "Fill in administrator information", + "formDesc": "Get your tenant ready fast. The password is only for login and will not be returned.", + "optional": "Optional" + }, + "alert": { + "title": "Save your admin account and password", + "desc": "Password is only used for login and will not be returned. You can fill verification and subscription later on the progress page." + }, + "field": { + "code": "Tenant code", + "name": "Tenant name", + "shortName": "Short name", + "industry": "Industry", + "contactName": "Contact name", + "contactPhone": "Contact phone", + "contactEmail": "Contact email", + "tenantPackageId": "Package ID", + "durationMonths": "Duration (months)", + "autoRenew": "Auto renew on expiry", + "adminAccount": "Admin account", + "adminDisplayName": "Admin display name", + "adminEmail": "Admin email", + "adminPhone": "Admin phone", + "adminPassword": "Admin password", + "confirmPassword": "Confirm password" + }, + "placeholder": { + "code": "Enter tenant code, e.g. your-tenant", + "name": "Enter tenant name", + "shortName": "Optional short name", + "industry": "Optional industry", + "contactName": "Enter contact name", + "contactPhone": "Enter contact phone", + "contactEmail": "Optional contact email", + "tenantPackageId": "Enter package ID", + "durationMonths": "Subscription duration, default 12 months", + "adminAccount": "Enter admin account", + "adminDisplayName": "Optional admin nickname", + "adminEmail": "Optional admin email", + "adminPhone": "Enter admin phone", + "adminPassword": "Enter admin password, at least 8 chars", + "confirmPassword": "Re-enter admin password" + }, + "rule": { + "adminPhoneRequired": "Please enter admin phone", + "adminPhoneFormat": "Please enter a valid phone number", + "adminAccountRequired": "Please enter admin account", + "adminAccountFormat": "Admin account can only contain letters and numbers", + "adminPasswordRequired": "Please enter admin password", + "adminPasswordLength": "Admin password must be at least 8 chars", + "adminEmailFormat": "Please enter a valid email", + "adminEmailRequired": "Please enter admin email", + "adminDisplayNameRequired": "Please enter admin display name", + "confirmPasswordRequired": "Please confirm admin password", + "passwordMismatch": "The two admin passwords do not match", + "agreementRequired": "Please agree to the privacy policy first" + }, + "agreeText": "I have read and agree", + "privacyPolicy": "Privacy policy", + "submitBtnText": "Submit Registration Now", + "hasAccount": "Already have an account?", + "toLogin": "Go to login", + "success": "Registered successfully, redirecting to progress", + "failed": "Registration failed, please try again later", + "rateLimit": "Too many requests, please try again later" + }, + "onboarding": { + "breadcrumb": "Self-service onboarding", + "title": "Onboarding progress", + "waiting": { + "title": "Waiting for review", + "tip": "Your information has been submitted. Please wait for review." + }, + "error": { + "title": "Status error", + "tip": "The tenant status is abnormal. Please contact an administrator.", + "loading": "Loading status...", + "suspended": { + "title": "Service suspended", + "desc": "Your account has been suspended due to unpaid bills or policy violations. Please contact support.", + "primaryAction": "Contact support", + "secondaryAction": "View billing" + }, + "expired": { + "title": "Subscription expired", + "desc": "Your subscription has expired. Please renew to continue using the service.", + "primaryAction": "Renew now", + "secondaryAction": "Contact sales" + }, + "closed": { + "title": "Account closed", + "desc": "This account has been closed or archived and cannot be used. Please register again if needed.", + "primaryAction": "Register again", + "secondaryAction": "Back to home" + }, + "rejected": { + "title": "Verification rejected", + "desc": "Your onboarding information was rejected. Please update and resubmit.", + "reasonTitle": "Rejection reason", + "defaultReason": "No detailed reason provided. Please contact support.", + "primaryAction": "Update and resubmit", + "secondaryAction": "Contact support" + }, + "fallback": { + "title": "Status", + "desc": "This status cannot be displayed here. Please refresh and try again.", + "primaryAction": "Refresh" + } + }, + "placeholder": { + "tenantId": "Enter tenant ID", + "businessLicenseNumber": "Enter business license number", + "businessLicenseUrl": "Enter business license URL", + "legalPersonName": "Enter legal name", + "legalPersonIdNumber": "Enter legal ID number", + "legalPersonIdFrontUrl": "Enter ID front URL", + "legalPersonIdBackUrl": "Enter ID back URL", + "bankAccountName": "Enter bank account name", + "bankAccountNumber": "Enter bank account number", + "bankName": "Enter bank name", + "additionalDataJson": "Optional extra data JSON", + "tenantPackageId": "Enter package ID", + "durationMonths": "Enter duration (months)", + "notes": "Optional notes" + }, + "actions": { + "load": "Query", + "refresh": "Refresh", + "submitVerification": "Submit verification", + "goConsole": "Go to console", + "goLogin": "Back to login" + }, + "messages": { + "missingTenantInfo": "Missing tenant or package info, please go back and try again", + "submitSuccess": "Submitted for review and package bound successfully" + }, + "card": { + "tenantId": "Tenant ID", + "verificationStatus": "Verification status", + "tenantStatus": "Tenant status" + }, + "status": { + "verification": { + "draft": "Draft", + "pending": "Pending", + "rejected": "Rejected", + "approved": "Approved" + }, + "tenant": { + "active": "Active", + "pendingReview": "Pending review", + "suspended": "Suspended", + "expired": "Expired", + "closed": "Closed" + }, + "hintDraft": "Please submit verification", + "hintPending": "Waiting for review", + "hintRejected": "Review rejected, please resubmit", + "hintApproved": "Approved, you can enter console", + "unknown": "Unknown" + }, + "form": { + "title": "Submit verification", + "tip": "Fill business license and legal info to speed up review", + "rejected": "Review rejected, please update and resubmit", + "businessLicenseNumber": "Business license number", + "businessLicenseUrl": "Business license URL", + "legalPersonName": "Legal name", + "legalPersonIdNumber": "Legal ID number", + "legalPersonIdFrontUrl": "ID front URL", + "legalPersonIdBackUrl": "ID back URL", + "bankAccountName": "Bank account name", + "bankAccountNumber": "Bank account number", + "bankName": "Bank name", + "additionalDataJson": "Additional data JSON", + "tenantPackageId": "Package ID", + "durationMonths": "Duration (months)", + "notes": "Notes", + "autoRenew": "Auto renew", + "securityTip": "Data is only used for review and will not be leaked." + }, + "subscription": { + "title": "Plan subscription", + "tip": "Sign in to submit subscription and lock quota", + "loginTip": "Please log in to submit subscription", + "submit": "Submit subscription", + "failed": "Create subscription failed, please try again later" + }, + "pricing": { + "badge": "Self-service onboarding", + "freeTag": "Free for commercial use", + "title": "Trusted by 53,476+ developers", + "subtitle": "And chosen by many tech giants", + "defaultDesc": "For cloud products, billed annually by user.", + "perTime": "one-time payment", + "selectCta": "Choose this plan", + "empty": "No plans available, please try again later", + "features": { + "code": "Full source code", + "docs": "Technical docs", + "saasAuth": "SaaS license", + "singleProject": "Single project use", + "unlimitedProjects": "Unlimited projects", + "support": "1-year support", + "update": "1-year updates", + "limitStore": "Store limit: {value}", + "limitAccount": "Account limit: {value}", + "limitStorage": "Storage: {value}GB", + "unlimited": "Unlimited" + } + }, + "pending": { + "title": "Under review", + "subtitle": "We received your info, we will notify when done", + "polling": "Auto refresh in {seconds}s", + "tipsTitle": "Tips", + "tip1": "Avoid repeated submissions; update after a short wait.", + "tip2": "If you see 429, wait 1 minute before retry." + }, + "ready": { + "title": "Verification approved", + "subtitle": "You can enter the console now", + "tipsTitle": "Next steps", + "tip1": "Check roles and permissions after login.", + "tip2": "Adjust plan in console or contact support if needed." + }, + "fallback": { + "title": "Cannot enter console", + "tipsTitle": "Possible reasons", + "tip1": "Tenant is suspended or closed, please contact admin.", + "tip2": "Renew the plan after login if expired." + }, + "message": { + "noTenantId": "Please enter tenant ID first", + "rateLimit": "Too many requests, please try again later", + "loadFailed": "Failed to load progress, please retry", + "verificationSubmitted": "Verification submitted", + "submitFailed": "Submit failed, please retry later", + "subscriptionCreated": "Subscription created" + }, + "rule": { + "businessLicenseNumber": "Please enter business license number", + "legalPersonName": "Please enter legal name", + "legalPersonIdNumber": "Please enter valid ID number", + "bankAccountNumber": "Please enter valid bank account number", + "packageRequired": "Please select a package", + "durationRequired": "Please enter duration", + "durationRange": "Duration range 1-120 months" + } + }, + "lockScreen": { + "pwdError": "Password error", + "lock": { + "inputPlaceholder": "Please input lock screen password", + "btnText": "Lock" + }, + "unlock": { + "inputPlaceholder": "Please input unlock password", + "btnText": "Unlock", + "backBtnText": "Back to login" + } + }, + "greeting": { + "dawn": "Good morning!", + "morning": "Good morning!", + "afternoon": "Good afternoon!", + "evening": "Good evening!" + }, + "exceptionPage": { + "403": "Sorry, you do not have permission to access this page", + "404": "Sorry, the page you are trying to access does not exist", + "500": "Sorry, there was an error on the server", + "gohome": "Go Home" + }, + "termsOfService": { + "title": "Terms of Service" + }, + "menus": { + "login": { + "title": "Login" + }, + "register": { + "title": "Register" + }, + "termsOfService": { + "title": "Terms of Service" + }, + "onboarding": { + "status": "Onboarding progress", + "pricing": "Plan selection", + "waiting": "Waiting for review", + "error": "Status error" + }, + "forgetPassword": { + "title": "Forget Password" + }, + "resetPassword": { + "title": "Reset Password" + }, + "outside": { + "title": "Outside" + }, + "dashboard": { + "title": "Dashboard", + "console": "Console" + }, + "result": { + "title": "Result Page", + "success": "Success", + "fail": "Fail" + }, + "exception": { + "title": "Exception", + "forbidden": "403", + "notFound": "404", + "serverError": "500" + }, + "system": { + "title": "System Settings", + "user": "User Manage", + "role": "Role Manage", + "tenantRole": "Tenant Role", + "userCenter": "User Center", + "menu": "Menu Manage", + "tenant": "Tenant Manage", + "tenantPackage": "Package Management" + }, + "dictionary": { + "title": "Dictionary", + "system": "System Dictionary", + "tenant": "Tenant Dictionary", + "override": "Dictionary Overrides", + "labelOverride": "Label Override Management", + "metrics": "Cache Metrics" + }, + "tenant": { + "title": "Tenant Management", + "subscription": "Subscription Management", + "billing": "Billing Management", + "billingStatistics": "Billing Statistics" + }, + "announcement": { + "title": "Announcement Management", + "platform": { + "title": "Platform Announcements", + "list": "Platform Announcement List", + "create": "Create Platform Announcement", + "edit": "Edit Platform Announcement", + "detail": "Announcement Detail" + }, + "tenant": { + "title": "Tenant Announcements", + "list": "Tenant Announcement List", + "create": "Create Tenant Announcement", + "edit": "Edit Tenant Announcement", + "detail": "Announcement Detail" + }, + "app": { + "title": "App Announcements", + "list": "Announcement List", + "detail": "Announcement Detail" + }, + "drafts": { + "title": "Draft Center", + "list": "Draft List" + } + }, + "merchant": { + "title": "Merchant Management", + "list": "Merchants", + "review": "Merchant Review", + "detail": "Merchant Detail" + }, + "store": { + "title": "Store Management", + "list": "Store List", + "detail": "Store Detail" + }, + "platform": { + "title": "Platform Ops", + "storeAudits": "Store Audits", + "qualificationAlerts": "Qualification Alerts" + } + }, + "tenant": { + "search": { + "keyword": "Keyword", + "keywordPlaceholder": "Tenant name/code", + "status": "Status", + "statusPlaceholder": "All", + "tenantName": "Tenant Name", + "tenantNamePlaceholder": "Enter tenant name", + "contactName": "Contact Name", + "contactNamePlaceholder": "Enter contact name", + "contactPhone": "Contact Phone", + "contactPhonePlaceholder": "Enter contact phone", + "verificationStatus": "Verification Status" + }, + "action": { + "addTenant": "Add Tenant", + "edit": "Edit", + "detail": "Details", + "more": "More", + "impersonateLogin": "Impersonate Login", + "quotaOverview": "Quota Overview", + "toggleFreeze": "Freeze/Unfreeze", + "freeze": "Freeze", + "unfreeze": "Unfreeze", + "extendOrGift": "Extend/Gift Time", + "resetAdmin": "Reset Admin" + }, + "more": { + "impersonate": { + "title": "Impersonate Login", + "tip": "This will switch to the tenant console and log in as the tenant primary admin.", + "confirm": "Confirm", + "success": "Switched to tenant console", + "alreadyImpersonating": "You are already impersonating. Please exit first." + }, + "freeze": { + "freezeTitle": "Freeze Tenant", + "unfreezeTitle": "Unfreeze Tenant", + "freezeTip": "After freezing, the tenant will not be able to use the system. Please proceed with caution.", + "unfreezeTip": "After unfreezing, the tenant service will be restored (expired subscription will still show as expired).", + "reason": "Reason", + "reasonRequired": "Please enter the freeze reason", + "freezePlaceholder": "Enter freeze reason (required)", + "unfreezePlaceholder": "Enter unfreeze note (optional)", + "success": "Success" + }, + "extend": { + "title": "Extend/Gift Time", + "months": "Months", + "monthUnit": "months", + "monthsRequired": "Please enter months", + "notes": "Notes", + "notesPlaceholder": "Enter notes (optional)", + "expireUnknown": "Current expiration time is unknown. The extension will start from now.", + "currentExpireAt": "Current expiration time: {date}", + "success": "Success" + }, + "resetAdmin": { + "title": "Reset Admin", + "tip": "Generate a password reset link for the primary admin (shown only once).", + "generate": "Generate Link", + "resetUrl": "Reset Link", + "copy": "Copy", + "copied": "Copied", + "empty": "Please generate the link first", + "success": "Generated", + "failed": "Failed to generate. Please try again." + }, + "todo": { + "impersonateLogin": "Impersonate login is not implemented yet", + "quotaOverview": "Quota overview is not implemented yet", + "resetAdmin": "Reset admin is not implemented yet" + } + }, + "status": { + "pendingReview": "Pending review", + "active": "Active", + "suspended": "Suspended", + "expired": "Expired", + "closed": "Closed", + "unknown": "Unknown" + }, + "verificationStatus": { + "draft": "Draft", + "pending": "Pending", + "approved": "Approved", + "rejected": "Rejected", + "unknown": "Unknown" + }, + "subscriptionStatus": { + "active": "Active", + "expired": "Expired", + "cancelled": "Cancelled", + "suspended": "Suspended", + "unknown": "Unknown" + }, + "review": { + "title": "Tenant Review", + "dialogTitle": "Tenant Review", + "tenantName": "Tenant Name", + "result": "Review Result", + "approve": "Approve", + "reject": "Reject", + "rejectReason": "Reject Reason", + "rejectReasonPlaceholder": "Please enter reject reason", + "renewMonths": "Renew Months", + "renewMonthsPlaceholder": "Please enter renew months", + "monthUnit": "months", + "rejectReasonCustom": "Custom Reason", + "rejectReasons": { + "materialsIncomplete": "Incomplete materials / Missing documents", + "licenseInvalid": "Invalid or unclear business license", + "identityMismatch": "Legal person info mismatch", + "contactUnreachable": "Unreachable contact / Invalid info", + "duplicateRegistration": "Possible duplicate registration / Risk related", + "other": "Other (custom)" + }, + "claim": { + "label": "Claim", + "unclaimed": "Unclaimed", + "claimed": "Claimed", + "claimedBy": "Claimed by: {name}", + "needClaimTip": "Please claim before reviewing", + "claimButton": "Claim", + "releaseButton": "Release", + "forceButton": "Force Takeover", + "readonlyTip": "This review is claimed by {name}. You can view only.", + "needClaimFirst": "Please claim this review first", + "claimedByTip": "This review has been claimed by {name}", + "forceDenied": "No permission to force takeover" + }, + "selectResult": "Please select review result", + "success": "Review completed", + "action": "Review", + "openMaterial": "Open materials", + "section": { + "basic": "Application Info", + "package": "Package Info", + "subscription": "Subscription Info", + "verification": "Verification Info", + "action": "Review Action", + "history": "Review History" + }, + "history": { + "empty": "No records" + }, + "field": { + "name": "Tenant Name", + "code": "Tenant Code", + "packageId": "Package ID", + "packageName": "Selected Package", + "packageType": "Package Type", + "packageDescription": "Description", + "monthlyPrice": "Monthly Price", + "yearlyPrice": "Yearly Price", + "packageActive": "Enabled", + "packageQuota": "Quota", + "featurePoliciesJson": "Feature Policies (JSON)", + "subscriptionId": "Subscription ID", + "subscriptionStatus": "Subscription Status", + "subscriptionPackageId": "Subscription Package ID", + "subscriptionEffectiveFrom": "Effective From", + "subscriptionEffectiveTo": "Effective To", + "nextBillingDate": "Next Billing Date", + "autoRenew": "Auto Renew", + "contactName": "Contact Name", + "contactPhone": "Contact Phone", + "contactEmail": "Contact Email", + "effectiveFrom": "Submitted At", + "verificationStatus": "Verification Status", + "verificationSubmittedAt": "Verification Submitted At", + "businessLicenseNumber": "Business License No.", + "businessLicenseUrl": "Materials", + "legalPersonName": "Legal Person Name", + "legalPersonIdNumber": "Legal Person ID", + "legalPersonIdFrontUrl": "Legal Person ID (Front)", + "legalPersonIdBackUrl": "Legal Person ID (Back)", + "bankName": "Bank Name", + "bankAccountName": "Account Name", + "bankAccountNumber": "Bank Account", + "additionalDataJson": "Additional Info (JSON)", + "reviewRemarks": "Review Remarks", + "reviewedBy": "Reviewer ID", + "reviewedByName": "Reviewed By", + "reviewedAt": "Reviewed At" + }, + "operatingMode": "Operating mode", + "operatingModePlaceholder": "Select operating mode", + "operatingModeOptions": { + "same": "Same entity", + "different": "Different entity" + } + }, + "manualCreate": { + "title": "Create Tenant (Manual, Active)", + "success": "Created successfully", + "unit": { + "month": "month(s)" + }, + "section": { + "tenant": "Tenant Info", + "subscription": "Plan & Subscription", + "verification": "Verification Info", + "admin": "Admin Account", + "system": "System Fields (Read-only)" + }, + "field": { + "region": "Province/City/District", + "code": "Tenant Code", + "name": "Tenant Name", + "shortName": "Short Name", + "legalEntityName": "Legal Entity Name", + "industry": "Industry", + "status": "Tenant Status", + "contactName": "Contact Name", + "contactPhone": "Contact Phone", + "contactEmail": "Contact Email", + "website": "Website", + "logoUrl": "LogoUrl", + "coverImageUrl": "CoverImageUrl", + "country": "Country/Region", + "province": "Province/State", + "city": "City", + "district": "District", + "address": "Address", + "tags": "Tags", + "remarks": "Remarks", + "suspendedAt": "Suspended At", + "suspensionReason": "Suspension Reason", + "tenantPackageId": "Package", + "durationMonths": "Duration", + "subscriptionEffectiveFrom": "Effective From", + "subscriptionEffectiveToPreview": "Effective To", + "nextBillingDate": "Next Billing Date", + "autoRenew": "Auto Renew", + "subscriptionStatus": "Subscription Status", + "scheduledPackageId": "Scheduled Package ID", + "subscriptionNotes": "Subscription Notes", + "verificationStatus": "Verification Status", + "businessLicenseNumber": "Business License No.", + "businessLicenseUrl": "Business License URL", + "legalPersonName": "Legal Person Name", + "legalPersonIdNumber": "Legal Person ID No.", + "legalPersonIdFrontUrl": "ID Front URL", + "legalPersonIdBackUrl": "ID Back URL", + "bankAccountName": "Bank Account Name", + "bankAccountNumber": "Bank Account Number", + "bankName": "Bank Name", + "additionalDataJson": "Additional Data (JSON)", + "submittedAt": "Submitted At", + "reviewedAt": "Reviewed At", + "reviewedBy": "Reviewer ID", + "reviewedByName": "Reviewer Name", + "reviewRemarks": "Review Remarks", + "adminAccount": "Admin Account", + "adminDisplayName": "Admin Name", + "adminPassword": "Initial Password", + "adminAvatar": "Admin Avatar", + "adminMerchantId": "Merchant ID", + "primaryOwnerUserId": "Primary Owner UserId", + "tenantId": "TenantId", + "tenantCreatedAt": "Tenant.CreatedAt", + "tenantUpdatedAt": "Tenant.UpdatedAt", + "tenantDeletedAt": "Tenant.DeletedAt", + "subscriptionId": "SubscriptionId", + "verificationId": "VerificationId", + "tenantCreatedBy": "Tenant.CreatedBy", + "tenantUpdatedBy": "Tenant.UpdatedBy", + "tenantDeletedBy": "Tenant.DeletedBy", + "subscriptionDeletedAt": "Subscription.DeletedAt" + }, + "placeholder": { + "region": "Select province/city/district", + "code": "Enter tenant code (unique)", + "name": "Enter tenant name", + "shortName": "Enter short name", + "legalEntityName": "Enter legal entity name", + "industry": "Enter industry", + "contactName": "Enter contact name", + "contactPhone": "Enter contact phone (unique)", + "contactEmail": "Enter contact email", + "website": "Enter website", + "logoUrl": "Enter logo URL", + "coverImageUrl": "Enter cover URL", + "country": "Enter country/region", + "province": "Enter province/state", + "city": "Enter city", + "address": "Enter address", + "tags": "Comma-separated", + "remarks": "Enter remarks", + "suspensionReason": "Enter suspension reason", + "scheduledPackageId": "Optional", + "subscriptionNotes": "Optional", + "businessLicenseNumber": "Enter business license no.", + "businessLicenseUrl": "Enter business license URL", + "legalPersonName": "Enter legal person name", + "legalPersonIdNumber": "Enter legal person ID no.", + "legalPersonIdFrontUrl": "Enter ID front URL", + "legalPersonIdBackUrl": "Enter ID back URL", + "bankAccountName": "Enter bank account name", + "bankAccountNumber": "Enter bank account number", + "bankName": "Enter bank name", + "additionalDataJson": "Optional", + "reviewedByName": "Defaults to current user if empty", + "reviewRemarks": "Optional", + "adminAccount": "Enter admin account (unique)", + "adminDisplayName": "Enter admin name", + "adminPassword": "Enter initial password", + "adminAvatar": "Optional", + "adminMerchantId": "Optional", + "systemGenerated": "Generated by system", + "systemAutoFill": "Auto-filled by system" + }, + "action": { + "upload": "Upload", + "reupload": "Re-upload", + "remove": "Remove", + "noImage": "No image", + "tip": "jpg/png/webp supported, auto-fill after upload" + }, + "rule": { + "code": "Tenant code is required", + "name": "Tenant name is required", + "tenantStatus": "Please select tenant status", + "tenantPackageId": "Please select package", + "durationMonths": "Please enter duration (months)", + "subscriptionEffectiveFrom": "Please select effective from", + "subscriptionStatus": "Please select subscription status", + "verificationStatus": "Please select verification status", + "adminAccount": "Admin account is required", + "adminDisplayName": "Admin name is required", + "adminPassword": "Initial password is required" + }, + "subscriptionStatus": { + "pending": "Pending", + "active": "Active", + "gracePeriod": "Grace Period", + "cancelled": "Cancelled", + "suspended": "Suspended" + } + }, + "table": { + "index": "No.", + "name": "Tenant Name", + "code": "Tenant Code", + "contactName": "Contact Name", + "contactPhone": "Contact Phone", + "status": "Status", + "verificationStatus": "Verification Status", + "effectiveFrom": "Effective From", + "effectiveTo": "Effective To", + "action": "Actions" + } + }, + "tenantPackage": { + "actions": { + "create": "New Package", + "close": "Close", + "view": "View", + "edit": "Edit", + "more": "More", + "copy": "Copy", + "saveDraft": "Save Draft", + "publish": "Publish", + "rollbackDraft": "Revert to Draft", + "toggleActive": "Enable/Disable", + "toggleVisible": "Public Visible", + "togglePurchasable": "Allow Purchase", + "quotaEdit": "Quota", + "viewTenants": "Tenants", + "delete": "Delete" + }, + "usage": { + "activeSubscription": "Active Subscriptions", + "totalSubscription": "Total Subscriptions", + "activeTenant": "Active Tenants", + "mrr": "MRR", + "arr": "ARR", + "expiringIn": "Expiring" + }, + "search": { + "keyword": "Keyword", + "keywordPlaceholder": "Enter package name or description", + "status": "Status", + "statusAll": "All", + "statusEnabled": "Enabled", + "statusDisabled": "Disabled" + }, + "table": { + "name": "Name", + "packageType": "Package Type", + "publishStatus": "Publish Status", + "publicVisible": "Visible", + "allowNewPurchase": "Purchasable", + "monthlyPrice": "Monthly Price", + "yearlyPrice": "Yearly Price", + "quota": "Quota", + "usage": "Usage", + "status": "Status", + "description": "Description", + "actions": "Actions", + "quotaEmpty": "Not set" + }, + "featurePolicy": { + "open": "Visual Config", + "clear": "Clear", + "configured": "Configured", + "notConfigured": "Not configured", + "titleEdit": "Feature Policy", + "titleView": "View Feature Policy", + "hint": "Configure feature toggles and quotas visually. The result will be saved to featurePoliciesJson for future feature control by package.", + "saved": "Feature policy saved", + "invalidJson": "Invalid feature policy JSON", + "applyPreset": "Apply Preset", + "applyPresetTitle": "Confirm Apply", + "applyPresetConfirm": "Applying a preset will overwrite current feature toggles and quotas. Continue?", + "presetApplied": "Preset applied", + "validationPassed": "Validation passed", + "validationFailed": "Validation failed ({count})", + "tabs": { + "config": "Config", + "custom": "Custom", + "preview": "JSON Preview" + }, + "presets": { + "blank": "Blank", + "standard": "Standard", + "pro": "Pro" + }, + "customHint": "Add custom extension items for future expansion (e.g. hardware gifts, special services). Keys should use English/underscore and will be saved to extra.customItems.", + "custom": { + "add": "Add Item", + "key": "Key", + "label": "Label", + "type": "Type", + "value": "Value", + "typeBoolean": "Boolean", + "typeNumber": "Number", + "typeString": "Text" + }, + "sections": { + "features": "Features", + "quotas": "Quotas", + "preview": "JSON Preview (Read-only)" + }, + "features": { + "reportsExport": "Reports / Export", + "printing": "Printing / Receipt", + "apiAccess": "API Access", + "marketing": "Marketing", + "coupon": "Coupon", + "fullReduction": "Full Reduction", + "member": "Membership", + "points": "Points" + }, + "quotas": { + "maxProducts": "Max Products", + "maxMenus": "Max Menus", + "maxApiCallsPerDay": "API Calls / Day", + "unlimitedPlaceholder": "Empty means unlimited" + } + }, + "detail": { + "featureStrategy": { + "title": "Feature Strategy", + "invalidJson": "Invalid feature strategy JSON", + "empty": "No strategies to display", + "columns": { + "name": "Strategy", + "value": "Value", + "status": "Status", + "conditions": "Constraints" + }, + "status": { + "enabled": "Enabled", + "disabled": "Disabled" + }, + "value": { + "unlimited": "Unlimited" + }, + "noConditions": "None", + "conditions": { + "dependsOnEnabled": "Depends on: {name}=Enabled", + "min": "Min: {min}", + "max": "Max: {max}", + "unknownDependency": "Unknown dependency ({key})" + } + } + }, + "form": { + "createTitle": "Create Package", + "editTitle": "Edit Package", + "viewTitle": "Package Details", + "copyTitle": "Copy Package", + "publishStatus": "Publish Status", + "publicVisible": "Public Visible", + "allowNewPurchase": "Allow Purchase", + "isRecommended": "Recommended", + "tags": "Tags", + "tagsPlaceholder": "Select tags (multiple)", + "name": "Package Name", + "namePlaceholder": "Enter package name", + "description": "Description", + "descriptionPlaceholder": "Enter package description", + "packageType": "Package Type", + "packageTypePlaceholder": "Select package type", + "monthlyPrice": "Monthly Price (CNY)", + "yearlyPrice": "Yearly Price (CNY)", + "pricePlaceholder": "Enter amount", + "maxStoreCount": "Store Limit (count)", + "maxAccountCount": "Employee Limit (count)", + "maxStorageGb": "Storage (GB)", + "maxSmsCredits": "SMS (messages)", + "maxDeliveryOrders": "Delivery Order Limit", + "quotaPlaceholder": "Enter limit or leave blank for unlimited", + "featurePoliciesJson": "Feature Policies (JSON)", + "featurePlaceholder": "Optional custom feature policy JSON", + "isActive": "Active", + "sortOrder": "Sort Order", + "sortOrderPlaceholder": "Smaller values come first", + "submit": "Submit", + "submitDraft": "Save Draft", + "submitPublish": "Publish", + "validation": { + "nameRequired": "Please enter package name", + "packageTypeRequired": "Please select package type" + } + }, + "tags": { + "recommended": "Recommended", + "bestValue": "Best Value", + "flagship": "Flagship" + }, + "drawer": { + "title": "Using Tenants ({name})", + "expiringTitle": "Expiring Tenants ({name} · within {days} days)", + "detailTitle": "Package Details ({name})", + "searchPlaceholder": "Tenant name/code/contact/phone", + "tenantName": "Tenant Name", + "tenantCode": "Tenant Code", + "tenantStatus": "Status", + "contact": "Contact", + "phone": "Phone", + "effectiveTo": "Expires At" + }, + "dialog": { + "quotaTitle": "Quota ({name})" + }, + "status": { + "enabled": "Enabled", + "disabled": "Disabled" + }, + "publishStatus": { + "draft": "Draft", + "published": "Published" + }, + "message": { + "createSuccess": "Created successfully", + "saveDraftSuccess": "Draft saved", + "updateSuccess": "Updated successfully", + "publishConfirm": "Publish this package? After publishing (and visible/purchasable/enabled), new tenants can choose/purchase it.", + "publishTitle": "Publish Confirmation", + "publishSuccess": "Published", + "rollbackDraftConfirm": "Revert this package to draft? Draft packages are not publicly visible/sellable.", + "rollbackDraftTitle": "Revert Confirmation", + "rollbackDraftSuccess": "Reverted to draft", + "toggleVisibleConfirmEnable": "Set this package to publicly visible?", + "toggleVisibleConfirmDisable": "Set this package to not publicly visible?", + "toggleVisibleTitleEnable": "Visible Confirmation", + "toggleVisibleTitleDisable": "Hidden Confirmation", + "toggleVisibleSuccessEnable": "Now publicly visible", + "toggleVisibleSuccessDisable": "Now hidden", + "togglePurchasableConfirmEnable": "Allow new tenants to purchase/choose this package?", + "togglePurchasableConfirmDisable": "Disallow new tenants to purchase/choose this package?", + "togglePurchasableTitleEnable": "Allow Purchase Confirmation", + "togglePurchasableTitleDisable": "Disallow Purchase Confirmation", + "togglePurchasableSuccessEnable": "Purchase allowed", + "togglePurchasableSuccessDisable": "Purchase disallowed", + "toggleConfirmEnable": "Enable this package? It will be available for new tenants to choose/purchase.", + "toggleConfirmDisable": "Disable this package? New tenants will not be able to choose it (existing subscriptions are not affected).", + "toggleTitleEnable": "Enable Confirmation", + "toggleTitleDisable": "Disable Confirmation", + "toggleSuccessEnable": "Enabled", + "toggleSuccessDisable": "Disabled", + "deleteSuccess": "Deleted successfully", + "deleteConfirm": "Are you sure to delete this package? It will be soft deleted.", + "deleteTitle": "Delete Confirmation" + }, + "type": { + "free": "Free", + "standard": "Standard", + "professional": "Professional", + "enterprise": "Enterprise", + "unknown": "Unknown" + } + }, + "subscription": { + "title": "Subscription Management", + "search": { + "status": "Status", + "statusPlaceholder": "All Status", + "package": "Package", + "packagePlaceholder": "All Packages", + "tenantKeyword": "Tenant Keyword", + "tenantKeywordPlaceholder": "Tenant name / code", + "expireDateRange": "Expire Date", + "expireDateRangePlaceholder": "Select date range" + }, + "table": { + "index": "No.", + "tenantName": "Tenant Name", + "currentPackage": "Current Package", + "status": "Status", + "effectiveDate": "Effective Date", + "expireDate": "Expire Date", + "createdAt": "Created At", + "autoRenew": "Auto Renew", + "action": "Actions" + }, + "action": { + "detail": "View Details", + "more": "More", + "extend": "Extend", + "changePlan": "Change Plan", + "changeStatus": "Change Status" + }, + "status": { + "pending": "Pending", + "active": "Active", + "gracePeriod": "Grace Period", + "cancelled": "Cancelled", + "suspended": "Suspended" + }, + "detail": { + "title": "Subscription Details", + "basicInfo": "Basic Information", + "tenantName": "Tenant Name", + "currentPackage": "Current Package", + "status": "Status", + "effectiveDate": "Effective Date", + "expireDate": "Expire Date", + "autoRenew": "Auto Renew", + "notes": "Notes", + "notesPlaceholder": "Enter notes (optional)", + "saveNotes": "Save Notes", + "saveNotesSuccess": "Notes saved", + "autoRenewUpdateSuccess": "Auto renew updated", + "autoRenewUpdateFailed": "Failed to update auto renew", + "quotaUsage": "Quota Usage", + "noQuota": "No quota data", + "stores": "Stores", + "accounts": "Accounts", + "storage": "Storage", + "sms": "SMS Credits", + "deliveryOrders": "Delivery Orders", + "promotionSlots": "Promotion Slots", + "unlimited": "Unlimited", + "changeHistory": "Change History", + "noHistory": "No history records", + "changeType": { + "new": "New", + "renew": "Renew", + "upgrade": "Upgrade", + "downgrade": "Downgrade", + "unknown": "Change" + } + }, + "extend": { + "title": "Extend Subscription", + "duration": "Duration", + "durationPlaceholder": "Enter duration", + "unit": "Unit", + "unitDay": "Day(s)", + "unitMonth": "Month(s)", + "notes": "Notes", + "notesPlaceholder": "Enter notes (optional)", + "preview": "New Expire Date", + "success": "Extended successfully", + "validation": { + "durationRequired": "Please enter duration", + "durationMin": "Duration must be at least 1" + } + }, + "changePlan": { + "title": "Change Plan", + "currentPackage": "Current Package", + "newPackage": "New Package", + "newPackagePlaceholder": "Select new package", + "effectiveTime": "Effective Time", + "effectiveNow": "Immediately", + "effectiveNextCycle": "Next Cycle", + "notes": "Notes", + "notesPlaceholder": "Enter notes (optional)", + "success": "Plan changed successfully", + "validation": { + "packageRequired": "Please select new package" + } + }, + "changeStatus": { + "title": "Change Status", + "currentStatus": "Current Status", + "newStatus": "New Status", + "newStatusPlaceholder": "Select new status", + "reason": "Reason", + "reasonPlaceholder": "Enter reason", + "confirm": "Change subscription status to {status}?", + "success": "Status changed successfully", + "validation": { + "statusRequired": "Please select new status", + "reasonRequired": "Please enter reason" + } + } + }, + "quotaPackage": { + "title": "Quota Package Management", + "tabs": { + "packages": "Packages", + "purchases": "Purchase Records", + "dashboard": "Quota Dashboard", + "alertConfig": "Alert Settings" + }, + "search": { + "quotaType": "Quota Type", + "quotaTypePlaceholder": "All Types", + "status": "Status", + "statusPlaceholder": "All Status" + }, + "table": { + "index": "No.", + "name": "Package Name", + "quotaType": "Quota Type", + "quotaValue": "Quota Value", + "price": "Price", + "status": "Status", + "sortOrder": "Sort Order", + "action": "Actions" + }, + "action": { + "create": "Create Package" + }, + "quotaType": { + "storeCount": "Store Count", + "accountCount": "Account Count", + "storage": "Storage", + "smsCredits": "SMS Credits", + "deliveryOrders": "Delivery Orders", + "promotionSlots": "Promotion Slots" + }, + "status": { + "active": "Active", + "inactive": "Inactive" + }, + "unit": { + "sms": "messages", + "orders": "orders" + }, + "benefit": { + "featureStrategy": { + "title": "Feature policy", + "invalidJson": "Invalid feature policy JSON", + "notConfigured": "Feature policy not configured", + "empty": "No policy items to display", + "columns": { + "name": "Policy", + "value": "Value", + "status": "Status", + "conditions": "Conditions" + }, + "status": { + "enabled": "Enabled", + "disabled": "Disabled" + }, + "value": { + "unlimited": "Unlimited" + }, + "noConditions": "None", + "conditions": { + "dependsOnEnabled": "Depends on: {name}=enabled", + "min": "Min: {min}", + "max": "Max: {max}", + "unknownDependency": "Unknown dependency ({key})" + } + } + }, + "form": { + "createTitle": "Create Quota Package", + "editTitle": "Edit Quota Package", + "name": "Package Name", + "namePlaceholder": "Enter package name", + "quotaType": "Quota Type", + "quotaTypePlaceholder": "Select quota type", + "quotaValue": "Quota Value", + "quotaValuePlaceholder": "Enter quota value", + "price": "Price", + "pricePlaceholder": "Enter price", + "description": "Description", + "descriptionPlaceholder": "Enter description (optional)", + "sortOrder": "Sort Order", + "isActive": "Active Status" + }, + "purchase": { + "title": "Purchase Quota Package", + "tenant": "Tenant", + "quotaPackage": "Quota Package", + "quotaPackagePlaceholder": "Select quota package", + "price": "Price", + "notes": "Notes", + "notesPlaceholder": "Enter notes (optional)", + "confirm": "Confirm Purchase", + "success": "Purchased successfully" + }, + "purchases": { + "search": { + "tenant": "Tenant", + "tenantPlaceholder": "Select tenant" + }, + "table": { + "index": "No.", + "tenantName": "Tenant Name", + "quotaPackageName": "Quota Package", + "quotaType": "Quota Type", + "quotaValue": "Quota Value", + "price": "Price", + "purchasedAt": "Purchased At", + "expiredAt": "Expired At" + }, + "message": { + "selectTenantFirst": "Please select a tenant first" + } + }, + "dashboard": { + "search": { + "tenant": "Tenant", + "tenantPlaceholder": "Select tenant" + }, + "empty": { + "selectTenant": "Please select a tenant to view quota usage" + }, + "meta": { + "used": "Used", + "total": "Total" + }, + "threshold": { + "reached": "Reached {threshold}%" + }, + "message": { + "selectTenantFirst": "Please select a tenant first" + } + }, + "alertConfig": { + "tip": { + "title": "Alert Threshold", + "description": "When quota usage reaches or exceeds the threshold, the dashboard will show an alert indicator." + }, + "action": { + "reset": "Reset", + "resetDefault": "Restore Default", + "save": "Save" + }, + "message": { + "saveSuccess": "Saved successfully", + "resetSuccess": "Reset successfully", + "resetDefaultSuccess": "Default thresholds restored" + } + }, + "validation": { + "nameRequired": "Please enter package name", + "nameMaxLength": "Name cannot exceed 50 characters", + "quotaTypeRequired": "Please select quota type", + "quotaValueRequired": "Please enter quota value", + "priceRequired": "Please enter price", + "packageRequired": "Please select quota package" + }, + "message": { + "createSuccess": "Created successfully", + "updateSuccess": "Updated successfully", + "deleteConfirm": "Are you sure you want to delete this quota package?", + "deleteSuccess": "Deleted successfully", + "statusUpdateSuccess": "Status updated successfully" + } + }, + "announcementDrafts": { + "title": "Draft Center", + "filter": { + "type": "Type", + "typePlaceholder": "All Types", + "date": "Date", + "dateStart": "Start date", + "dateEnd": "End date" + }, + "table": { + "title": "Title", + "type": "Type", + "lastSaved": "Last Saved", + "author": "Author", + "empty": "No drafts" + }, + "action": { + "continueEdit": "Continue Editing", + "publish": "Publish" + }, + "message": { + "loadFailed": "Failed to load drafts, please try again later", + "deleteConfirm": "Delete this draft?", + "deleteTitle": "Delete Draft", + "deleteSuccess": "Draft deleted", + "publishConfirm": "Publish this draft?", + "publishTitle": "Publish Draft", + "publishSuccess": "Draft published", + "missingRowVersion": "Missing version info, cannot publish", + "localPublishHint": "Local drafts must be published from the editor", + "draftNotFound": "Draft not found or expired", + "routeNotFound": "Editor route not found, please check routing" + }, + "pagination": { + "total": "Total {count} drafts" + }, + "source": { + "local": "Local Draft", + "server": "Server Draft" + }, + "type": { + "all": "All", + "system": "System Announcement", + "billing": "Billing/Subscription Reminder", + "operation": "Operations Notice", + "systemPlatformUpdate": "Platform System Update", + "systemSecurityNotice": "System Security Notice", + "systemCompliance": "System Compliance Notice", + "tenantInternal": "Tenant Internal Notice", + "tenantFinance": "Tenant Finance Notice", + "tenantOperation": "Tenant Operations Notice", + "unknown": "Unknown Type" + }, + "field": { + "untitled": "Untitled Draft", + "unknownAuthor": "Unknown", + "unknown": "Unknown" + } + }, + "table": { + "form": { + "reset": "Reset", + "submit": "Submit" + }, + "searchBar": { + "reset": "Reset", + "search": "Search", + "expand": "Expand", + "collapse": "Collapse", + "searchInputPlaceholder": "Please enter", + "searchSelectPlaceholder": "Please select" + }, + "selection": "Select", + "sizeOptions": { + "small": "Compact", + "default": "Default", + "large": "Loose" + }, + "column": { + "selection": "Select", + "expand": "Expand", + "index": "Index" + }, + "zebra": "Zebra", + "border": "Border", + "headerBackground": "Header BG" + }, + "billing": { + "title": "Billing Management", + "quickFilter": { + "title": "Quick Filter", + "all": "All" + }, + "search": { + "tenant": "Tenant", + "tenantPlaceholder": "Search tenant name", + "billingType": "Billing Type", + "billingTypePlaceholder": "Select billing type", + "status": "Status", + "statusPlaceholder": "Select status", + "dateRange": "Date Range", + "dateRangePlaceholder": "Select date range", + "amountRange": "Amount Range", + "minAmountPlaceholder": "Min amount", + "maxAmountPlaceholder": "Max amount", + "keyword": "Keyword", + "keywordPlaceholder": "Statement No. or Tenant Name" + }, + "field": { + "statementNo": "Statement No.", + "tenantName": "Tenant Name", + "billingType": "Billing Type", + "amount": "Amount", + "amountDue": "Amount Due", + "amountPaid": "Amount Paid", + "status": "Status", + "dueDate": "Due Date", + "periodStart": "Period Start", + "periodEnd": "Period End", + "createdAt": "Created At", + "notes": "Notes" + }, + "billingType": { + "subscription": "Subscription", + "quotaPurchase": "Quota Purchase", + "manual": "Manual", + "renewal": "Renewal" + }, + "status": { + "pending": "Pending", + "paid": "Paid", + "overdue": "Overdue", + "cancelled": "Cancelled" + }, + "action": { + "export": "Export", + "create": "Create Bill", + "detail": "View Detail", + "markPaid": "Mark Paid", + "cancel": "Cancel", + "recordPayment": "Record Payment", + "exportExcel": "Export Excel", + "exportPdf": "Export PDF", + "addLineItem": "Add Line Item", + "batch": "Batch Actions", + "batchMarkPaid": "Batch Mark Paid", + "batchExportExcel": "Batch Export Excel", + "batchExportPdf": "Batch Export PDF", + "uploadProof": "Upload Proof" + }, + "dialog": { + "createTitle": "Create Bill Manually", + "recordPaymentTitle": "Confirm Payment", + "proofPreviewTitle": "Proof Preview" + }, + "drawer": { + "title": "Bill Detail", + "basicInfo": "Basic Information", + "paymentList": "Payment Records", + "tabs": { + "basic": "Basic", + "lineItems": "Line Items", + "payments": "Payments", + "statusFlow": "Status Flow" + }, + "openProof": "Open Proof", + "noPayments": "No payment records", + "noStatusFlow": "No status flow records" + }, + "view": { + "timeline": "Timeline", + "table": "Table" + }, + "lineItem": { + "itemType": "Item Type", + "description": "Description", + "quantity": "Quantity", + "unitPrice": "Unit Price", + "amount": "Amount" + }, + "statusFlow": { + "created": "Created", + "due": "Due", + "paid": "Paid", + "overdue": "Overdue", + "cancelled": "Cancelled" + }, + "hint": { + "amountAutoSum": "Amount is summed from line items" + }, + "placeholder": { + "selectTenant": "Select tenant", + "selectBillingType": "Select billing type", + "enterAmount": "Enter amount", + "selectDueDate": "Select due date", + "enterNotes": "Enter notes", + "enterItemType": "e.g., Subscription/Quota/Manual", + "enterDescription": "Enter description", + "enterPaymentAmount": "Enter payment amount", + "selectPaymentMethod": "Select payment method", + "enterTransactionNo": "Enter transaction no. (optional)", + "enterProofUrl": "Enter proof URL (optional)", + "enterPaymentNotes": "Enter payment notes (optional)" + }, + "validation": { + "tenantRequired": "Please select tenant", + "billingTypeRequired": "Please select billing type", + "amountRequired": "Please enter amount", + "amountMin": "Amount must be greater than 0", + "dueDateRequired": "Please select due date", + "lineItemsRequired": "Please add at least 1 line item", + "lineItemDescriptionRequired": "Line item #{index}: description is required", + "lineItemQuantityInvalid": "Line item #{index}: quantity must be greater than 0", + "lineItemUnitPriceInvalid": "Line item #{index}: unit price cannot be negative", + "paymentAmountRequired": "Please enter payment amount", + "paymentAmountMin": "Payment amount must be greater than 0", + "paymentAmountExceedRemain": "Payment amount cannot exceed remaining amount", + "paymentMethodRequired": "Please select payment method" + }, + "message": { + "createSuccess": "Bill created successfully", + "loadDetailFailed": "Failed to load billing detail", + "sortFieldNotSupported": "This sort field is not supported", + "cancelConfirm": "Are you sure to cancel this bill?", + "cancelByAdmin": "Cancelled by admin", + "cancelSuccess": "Bill cancelled", + "recordPaymentSuccess": "Payment confirmed", + "exportNoData": "No data to export", + "exportExcelSuccess": "Exported {count} records", + "exportSuccess": "Exported {count} records", + "exportFailed": "Export failed, please retry later", + "exportPdfNeedDetail": "Please open bill detail or select exactly 1 bill before exporting PDF", + "exportPdfNeedSingleSelection": "Please select exactly 1 bill to export PDF", + "exportPdfPopupBlocked": "Popup blocked by the browser. Please allow popups and retry.", + "exportPdfOpened": "Print window opened. Choose “Save as PDF” in the print dialog.", + "noSelection": "Please select bills first", + "batchNoPending": "No pending bills in selection", + "batchMarkPaidNotes": "Batch marked as paid", + "batchMarkPaidSuccess": "Confirmed payment for {count} bills", + "batchCancelNotes": "Batch cancelled", + "batchCancelSuccess": "Cancelled {count} bills", + "proofTypeNotAllowed": "Only JPG/PNG files are allowed", + "proofTooLarge": "File too large. Max {size}MB", + "proofUploadSuccess": "Proof uploaded successfully" + }, + "export": { + "title": "Export Settings", + "format": "Format", + "scope": "Scope", + "fields": "Fields", + "currentPage": "Current Page", + "selected": "Selected", + "all": "All", + "confirm": "Export", + "dateRange": "Date Range", + "dateRangePlaceholder": "Select date range", + "formatRequired": "Please select format", + "scopeRequired": "Please select scope", + "fieldsRequired": "Please select at least one field", + "formatExcel": "Excel", + "formatPdf": "PDF", + "formatCsv": "CSV", + "sheetName": "Bills", + "fileName": "bills_export", + "selectedSuffix": "selected", + "noPayments": "No payment records" + }, + "batch": { + "confirmPayment": "Batch Confirm Payment", + "cancel": "Batch Cancel", + "export": "Batch Export", + "confirmPaymentTip": "Confirm payment for {count} bills in batch?", + "cancelTip": "Cancel {count} bills in batch?", + "selectedCount": "Selected {count} items" + }, + "payment": { + "amount": "Amount", + "method": "Method", + "status": "Status", + "transactionNo": "Transaction No.", + "paidAt": "Paid At", + "notes": "Notes", + "remainAmount": "Remain Amount", + "proofUrl": "Proof URL", + "proofUrlHint": "URL link to payment proof image", + "proofUploadHint": "JPG/PNG only, max 10MB. URL will be filled after upload." + }, + "paymentMethod": { + "online": "Online", + "bankTransfer": "Bank Transfer", + "other": "Other" + }, + "paymentStatus": { + "pending": "Pending", + "success": "Success", + "failed": "Failed", + "refunded": "Refunded" + } + }, + "dashboard": { + "title": "Operations Dashboard", + "overview": { + "title": "Subscription Overview", + "totalActive": "Active Subscriptions", + "expiringIn7Days": "Expiring in 7 Days", + "expiringIn3Days": "Expiring in 3 Days", + "expiringIn1Day": "Expiring Tomorrow", + "expired": "Expired", + "pending": "Pending", + "suspended": "Suspended" + }, + "expiring": { + "title": "Expiring Subscriptions", + "viewAll": "View All", + "noData": "No expiring subscriptions", + "daysLeft": "Days Left" + }, + "revenue": { + "title": "Revenue Statistics", + "total": "Total Revenue", + "monthly": "Monthly Revenue", + "quarterly": "Quarterly Revenue" + }, + "quotaRanking": { + "title": "Quota Usage Ranking", + "tenant": "Tenant", + "usage": "Usage", + "limit": "Limit", + "percentage": "Usage Rate", + "rank": "Rank", + "quotaType": "Quota Type", + "selectType": "Select Quota Type", + "type": { + "orders": "Orders", + "storage": "Storage", + "users": "Users" + } + } + }, + "batch": { + "selectedCount": "Selected {count} items", + "extend": { + "title": "Batch Extend", + "selectedCount": "Selected {count} subscriptions", + "duration": "Duration", + "notes": "Notes", + "notesPlaceholder": "Enter notes (optional)", + "success": "Batch extend successful", + "partialSuccess": "Batch extend partially successful, {success} succeeded, {failed} failed", + "result": "{success} succeeded, {failed} failed", + "noSelection": "Please select subscriptions to extend", + "failedItems": "Failed Items" + }, + "remind": { + "title": "Batch Remind", + "selectedCount": "Selected {count} subscriptions", + "content": "Reminder Content", + "contentPlaceholder": "Enter reminder content", + "success": "Batch reminders sent successfully", + "partialSuccess": "Batch reminders partially successful, {success} succeeded, {failed} failed", + "result": "{success} sent, {failed} failed", + "noSelection": "Please select subscriptions to remind", + "failedItems": "Failed Items", + "validation": { + "contentRequired": "Please enter reminder content", + "contentMin": "Reminder content must be at least 10 characters" + } + } + }, + "user": { + "action": { + "create": "Create User", + "view": "View", + "edit": "Edit", + "more": "More", + "enable": "Enable", + "disable": "Disable", + "unlock": "Unlock", + "resetPassword": "Reset Password", + "restore": "Restore", + "delete": "Delete", + "cancel": "Cancel", + "confirm": "Confirm" + }, + "avatar": { + "upload": "Upload", + "reupload": "Reupload", + "remove": "Remove", + "noImage": "No Image", + "tip": "Supports jpg/png/webp, auto-filled after upload" + }, + "batch": { + "title": "Batch Actions", + "selected": "{count} selected", + "enable": "Batch Enable", + "disable": "Batch Disable", + "delete": "Batch Delete", + "restore": "Batch Restore", + "export": "Batch Export" + }, + "detail": { + "title": "User Details", + "rolesTitle": "Roles", + "permissionsTitle": "Permissions" + }, + "empty": { + "roles": "No roles", + "permissions": "No permissions" + }, + "field": { + "userId": "User ID", + "tenant": "Tenant", + "account": "Account", + "displayName": "Display Name", + "phone": "Phone", + "email": "Email", + "status": "Status", + "createdAt": "Created At", + "lastLoginAt": "Last Login", + "password": "Password", + "roles": "Roles", + "avatar": "Avatar" + }, + "status": { + "active": "Active", + "disabled": "Disabled", + "locked": "Locked", + "deleted": "Deleted", + "unknown": "Unknown" + }, + "table": { + "avatar": "Avatar", + "account": "Account", + "displayName": "Display Name", + "phone": "Phone", + "email": "Email", + "roles": "Roles", + "status": "Status", + "createdAt": "Created At", + "lastLoginAt": "Last Login", + "actions": "Actions", + "tenant": "Tenant" + }, + "search": { + "keyword": "Keyword", + "keywordPlaceholder": "Account/Name/Phone/Email", + "status": "Status", + "statusPlaceholder": "All Statuses", + "role": "Role", + "rolePlaceholder": "Select role", + "createdRange": "Created At", + "createdRangeStart": "Start", + "createdRangeEnd": "End", + "lastLoginRange": "Last Login", + "lastLoginRangeStart": "Start", + "lastLoginRangeEnd": "End", + "includeDeleted": "Include Deleted", + "includeDeletedOn": "Include", + "includeDeletedOff": "Exclude", + "tenant": "Tenant", + "tenantPlaceholder": "Select tenant" + }, + "placeholder": { + "tenant": "Select tenant", + "account": "Enter account", + "displayName": "Enter display name", + "password": "Enter password", + "phone": "Enter phone", + "email": "Enter email", + "avatar": "Enter avatar URL", + "roles": "Select roles", + "status": "Select status" + }, + "dialog": { + "createTitle": "Create User", + "editTitle": "Edit User" + }, + "validation": { + "tenantRequired": "Please select a tenant", + "accountRequired": "Please enter an account", + "displayNameRequired": "Please enter a display name", + "passwordRequired": "Please enter a password", + "passwordLength": "Password length must be 6-32 characters", + "phoneInvalid": "Please enter a valid phone number", + "emailInvalid": "Please enter a valid email address" + }, + "message": { + "createSuccess": "User created successfully", + "missingUserId": "Missing user ID, cannot submit", + "rowVersionMissing": "Missing row version, please refresh and retry", + "updateSuccess": "User updated successfully", + "concurrencyConflict": "Data has been updated. Please refresh and retry: {message}", + "statusConfirm": "Confirm change status for {name}?", + "statusSuccess": "Status updated successfully", + "deleteConfirm": "Confirm delete {name}?", + "deleteSuccess": "User deleted", + "restoreConfirm": "Confirm restore {name}?", + "restoreSuccess": "User restored", + "resetConfirm": "Confirm reset password for {name}?", + "resetToken": "Reset token: {token}\nExpires at: {expiresAt}", + "batchEmpty": "Please select users first", + "batchLimit": "Up to 100 users can be processed at once", + "batchConfirm": "Confirm batch action for {count} users?", + "batchResult": "Succeeded {success}, failed {failure}.\nFailure details:\n{details}", + "batchSuccess": "Batch action succeeded for {count} users", + "exportSuccess": "Exported {count} user records" + }, + "export": { + "fileName": "users" + } + }, + "announcement": { + "audience": { + "title": "Audience Selector", + "targetType": "Target Type", + "targetTypeHint": "Choose the audience for this announcement", + "type": { + "all": "All Tenants/Users", + "roles": "Specific Roles", + "users": "Specific Users", + "rules": "Rule-Based", + "manual": "Manual Selection" + }, + "helper": { + "all": "Announcement will be sent to all tenants or users", + "roles": "Filter recipients by role", + "users": "Add specific users by search", + "rules": "Filter by department, role, and tags", + "manual": "Select users via search and transfer list" + }, + "rules": { + "departments": "Department", + "roles": "Role", + "tags": "Tags", + "departmentsPlaceholder": "Select departments", + "tagsPlaceholder": "Select or type tags", + "emptyDepartments": "No departments available", + "emptyRoles": "No roles available", + "estimateLabel": "Estimated Audience", + "estimateLoading": "Estimating..." + }, + "users": { + "placeholder": "Search users by name/phone/email", + "unknownUser": "User {id}" + }, + "manual": { + "searchPlaceholder": "Search by name/phone/email", + "transferLeft": "Candidates", + "transferRight": "Selected", + "filterPlaceholder": "Type to filter", + "estimateLabel": "Selected Count" + }, + "errors": { + "loadRolesFailed": "Failed to load roles", + "loadUsersFailed": "Failed to load users" + } + } + } +} diff --git a/src/locales/langs/zh.json b/src/locales/langs/zh.json new file mode 100644 index 0000000..9bad553 --- /dev/null +++ b/src/locales/langs/zh.json @@ -0,0 +1,2211 @@ +{ + "httpMsg": { + "unauthorized": "未授权访问,请重新登录", + "forbidden": "禁止访问该资源", + "notFound": "请求的资源不存在", + "methodNotAllowed": "请求方法不允许", + "conflict": "数据已被他人修改,请刷新后重试", + "validationFailed": "请求参数验证失败", + "requestTimeout": "请求超时,请稍后重试", + "internalServerError": "服务器内部错误,请稍后重试", + "badGateway": "网关错误,请稍后重试", + "serviceUnavailable": "服务暂时不可用,请稍后重试", + "gatewayTimeout": "网关超时,请稍后重试", + "requestCancelled": "请求已取消", + "networkError": "网络连接异常,请检查网络连接", + "requestFailed": "请求失败", + "requestConfigError": "请求配置错误" + }, + "topBar": { + "search": { + "title": "搜索" + }, + "user": { + "userCenter": "个人中心", + "docs": "使用文档", + "github": "Github", + "lockScreen": "锁定屏幕", + "logout": "退出登录" + }, + "guide": { + "title": "点击这里查看", + "theme": "主题风格", + "menu": "开启顶栏菜单", + "description": "等更多配置" + }, + "impersonation": { + "active": "伪装中(租户 {tenantId})", + "exit": "退出伪装", + "confirm": "确认退出伪装并返回平台后台?" + } + }, + "common": { + "id": "ID", + "tips": "提示", + "cancel": "取消", + "confirm": "确定", + "yes": "是", + "no": "否", + "days": "天", + "delete": "删除", + "edit": "编辑", + "action": "操作", + "uploadImage": "上传图片", + "fileTooLarge": "文件体积过大,最大支持 {size}MB", + "uploadFailed": "上传失败", + "logOutTips": "您是否要退出登录?", + "prevStep": "上一步", + "nextStep": "下一步", + "unlimited": "不限", + "startDate": "开始日期", + "endDate": "结束日期", + "startTime": "开始时间", + "endTime": "结束时间", + "tips": "提示", + "error": { + "operationFailed": "操作失败,请重试" + } + }, + "search": { + "placeholder": "搜索页面", + "historyTitle": "搜索历史", + "switchKeydown": "切换", + "selectKeydown": "选择", + "exitKeydown": "关闭" + }, + "setting": { + "menuType": { + "title": "菜单布局", + "list": ["垂直", "水平", "混合", "双列"] + }, + "theme": { + "title": "主题风格", + "list": ["浅色", "深色", "系统"] + }, + "menu": { + "title": "菜单风格" + }, + "color": { + "title": "系统主题色" + }, + "box": { + "title": "盒子样式", + "list": ["边框", "阴影"] + }, + "container": { + "title": "容器宽度", + "list": ["铺满", "定宽"] + }, + "basics": { + "title": "基础配置", + "list": { + "multiTab": "开启多标签栏", + "accordion": "侧边栏开启手风琴模式", + "collapseSidebar": "显示折叠侧边栏按钮", + "fastEnter": "显示快速入口", + "reloadPage": "显示重载页面按钮", + "breadcrumb": "显示全局面包屑导航", + "language": "显示多语言选择", + "progressBar": "显示顶部进度条", + "weakMode": "色弱模式", + "watermark": "全局水印", + "menuWidth": "菜单宽度", + "tabStyle": "标签页风格", + "pageTransition": "页面切换动画", + "borderRadius": "自定义圆角" + } + }, + "tabStyle": { + "default": "默认", + "card": "卡片", + "google": "谷歌" + }, + "transition": { + "list": { + "none": "无动画", + "fade": "淡入淡出", + "slideLeft": "左侧滑入", + "slideBottom": "下方滑入", + "slideTop": "上方滑入" + } + }, + "actions": { + "resetConfig": "重置配置", + "copyConfig": "复制配置", + "copySuccess": "配置已复制到剪贴板,可粘贴到 src/config/setting.ts 文件中", + "copyFailed": "复制失败,请重试", + "resetFailed": "重置失败,请刷新页面后重试" + } + }, + "notice": { + "title": "通知", + "btnRead": "标为已读", + "bar": ["通知", "消息", "代办"], + "text": ["暂无"], + "viewAll": "查看全部" + }, + "appAnnouncement": { + "list": { + "title": "公告中心", + "unreadCount": "未读 {count} 条", + "typePlaceholder": "请选择公告类型", + "refresh": "刷新列表", + "viewDetail": "查看详情", + "empty": "暂无可读公告", + "publisher": "发布者:", + "publishedAt": "发布时间:", + "effective": "有效期:" + }, + "detail": { + "title": "公告详情", + "back": "返回列表", + "publisher": "发布者:", + "publishedAt": "发布时间:", + "effective": "有效期:", + "content": "公告内容" + }, + "type": { + "all": "全部类型", + "system": "系统公告", + "billing": "账单提醒", + "operation": "运营通知", + "platformUpdate": "平台系统更新", + "security": "安全公告", + "compliance": "合规公告", + "tenantInternal": "租户内部公告", + "tenantFinance": "租户财务公告", + "tenantOperation": "租户运营公告" + }, + "publisher": { + "platform": "平台", + "tenant": "租户", + "unknown": "未知" + }, + "common": { + "notAvailable": "暂无", + "dateRange": "{start} ~ {end}" + }, + "message": { + "loadFailed": "加载公告失败,请稍后重试", + "missingId": "缺少公告ID,无法加载详情", + "detailNotFound": "未获取到公告详情,请返回列表重试" + } + }, + "worktab": { + "btn": { + "refresh": "刷新", + "fixed": "固定", + "unfixed": "取消固定", + "closeLeft": "关闭左侧", + "closeRight": "关闭右侧", + "closeOther": "关闭其他", + "closeAll": "关闭全部" + } + }, + "login": { + "leftView": { + "title": "一款兼具设计美学与高效开发的后台系统", + "subTitle": "美观实用的界面,经过视觉优化,确保卓越的用户体验" + }, + "title": "欢迎回来", + "subTitle": "输入您的账号和密码登录", + "roles": { + "super": "超级管理员", + "admin": "管理员", + "user": "普通用户" + }, + "placeholder": { + "account": "请输入账号名", + "phone": "请输入手机号", + "password": "请输入密码", + "slider": "请拖动滑块完成验证" + }, + "sliderText": "按住滑块拖动", + "sliderSuccessText": "验证成功", + "rememberPwd": "记住密码", + "forgetPwd": "忘记密码", + "btnText": "登录", + "noAccount": "还没有账号?", + "register": "注册", + "success": { + "title": "登录成功", + "message": "欢迎回来" + } + }, + "forgetPassword": { + "title": "忘记密码?", + "subTitle": "输入您的电子邮件来重置您的密码", + "placeholder": "请输入您的电子邮件", + "submitBtnText": "提交", + "backBtnText": "返回" + }, + "resetPassword": { + "title": "重置密码", + "subTitle": "请设置一个新的登录密码", + "invalidToken": "重置链接无效或已过期,请联系平台管理员重新生成链接", + "newPassword": "新密码", + "newPasswordPlaceholder": "请输入新密码(6~32 位)", + "confirmPassword": "确认新密码", + "confirmPasswordPlaceholder": "请再次输入新密码", + "submitBtnText": "确认重置", + "backBtnText": "返回登录", + "success": "密码重置成功,请使用新密码登录", + "rules": { + "newPasswordRequired": "请输入新密码", + "confirmPasswordRequired": "请再次输入新密码", + "passwordLength": "密码长度需为 6~32 位", + "passwordNotMatch": "两次输入的密码不一致" + } + }, + "register": { + "title": "自助入驻", + "subTitle": "填写管理员信息,快速开通独立空间", + "layout": { + "brand": "CloudSaaS", + "badge": "自助入驻", + "badgeDesc": "密码仅用于登录不会返回", + "title": "开启您的", + "titleHighlight": "数字化管理 新篇章", + "desc": "仅需几步,即可快速开通您的独立空间。为您提供安全、稳定、高效的企业级管理体验。", + "pillSecurity": "企业级安全", + "pillData": "数据联接", + "pillDeploy": "极速部署", + "footer": "© CloudSaaS Inc. All rights reserved.", + "formTitle": "填写管理员信息", + "formDesc": "快速开启独立空间,密码仅用于登录不会返回。", + "optional": "可选" + }, + "alert": { + "title": "请保存管理员账号与密码", + "desc": "密码仅用于登录不会返回,注册后可前往进度页补充实名与订阅信息。" + }, + "field": { + "code": "租户编码", + "name": "租户名称", + "shortName": "租户简称", + "industry": "所属行业", + "contactName": "联系人姓名", + "contactPhone": "联系电话", + "contactEmail": "联系邮箱", + "tenantPackageId": "套餐ID", + "durationMonths": "订阅时长(月)", + "autoRenew": "到期自动续费", + "adminAccount": "管理员账号", + "adminDisplayName": "管理员名称", + "adminEmail": "管理员邮箱", + "adminPhone": "管理员手机号", + "adminPassword": "管理员密码", + "confirmPassword": "确认密码" + }, + "placeholder": { + "code": "请输入租户编码,例如 your-tenant", + "name": "请输入租户名称", + "shortName": "可选,方便展示的简称", + "industry": "可选,所属行业", + "contactName": "请输入联系人姓名", + "contactPhone": "请输入联系电话", + "contactEmail": "可选,接收通知的邮箱", + "tenantPackageId": "请输入套餐ID", + "durationMonths": "订阅时长,默认12个月", + "adminAccount": "请输入管理员账号", + "adminDisplayName": "可选,管理员昵称", + "adminEmail": "可选,管理员邮箱", + "adminPhone": "请输入管理员手机号", + "adminPassword": "请输入管理员密码,至少8位", + "confirmPassword": "请再次输入管理员密码" + }, + "rule": { + "adminPhoneRequired": "请输入管理员手机号", + "adminPhoneFormat": "请输入正确的手机号", + "adminAccountRequired": "请输入管理员账号", + "adminAccountFormat": "管理员账号仅允许大小写字母与数字", + "adminPasswordRequired": "请输入管理员密码", + "adminPasswordLength": "管理员密码至少8位", + "adminEmailFormat": "请输入正确的邮箱地址", + "adminEmailRequired": "请输入管理员邮箱", + "adminDisplayNameRequired": "请输入管理员名称", + "confirmPasswordRequired": "请再次输入管理员密码", + "passwordMismatch": "两次输入的管理员密码不一致", + "agreementRequired": "请先同意隐私政策" + }, + "agreeText": "我已阅读并同意", + "privacyPolicy": "《隐私政策》", + "submitBtnText": "立即提交注册", + "hasAccount": "已有账号?", + "toLogin": "去登录", + "success": "注册成功,正在跳转进度页", + "failed": "注册失败,请稍后再试", + "rateLimit": "请求过于频繁,请稍后再试" + }, + "onboarding": { + "breadcrumb": "自助入驻", + "title": "租户入驻进度", + "waiting": { + "title": "等待审核", + "tip": "资料已提交,请等待管理员审核" + }, + "error": { + "title": "状态异常", + "tip": "当前租户状态异常,请联系管理员处理", + "loading": "正在加载状态...", + "suspended": { + "title": "账号服务已暂停", + "desc": "您的账号因欠费或违反平台规则已被暂停服务。如有疑问,请联系客服处理。", + "primaryAction": "联系客服", + "secondaryAction": "查看账单" + }, + "expired": { + "title": "订阅服务已到期", + "desc": "您的订阅服务已到期。为不影响正常使用,请尽快续费。", + "primaryAction": "立即续费", + "secondaryAction": "联系销售" + }, + "closed": { + "title": "账号已注销", + "desc": "该账号已被注销或归档,无法继续使用。如需使用本平台服务,请重新注册。", + "primaryAction": "重新注册", + "secondaryAction": "返回首页" + }, + "rejected": { + "title": "实名资质审核未通过", + "desc": "很抱歉,您提交的入驻资料未通过审核。请根据原因修改后重新提交。", + "reasonTitle": "驳回原因", + "defaultReason": "未提供详细原因,请联系客服。", + "primaryAction": "修改并重新提交", + "secondaryAction": "联系人工客服" + }, + "fallback": { + "title": "状态信息", + "desc": "当前状态不支持在此页面展示,请点击刷新重试。", + "primaryAction": "刷新" + } + }, + "placeholder": { + "tenantId": "请输入租户ID", + "businessLicenseNumber": "请输入营业执照编号", + "businessLicenseUrl": "请输入营业执照链接", + "legalPersonName": "请输入法人姓名", + "legalPersonIdNumber": "请输入法人证件号", + "legalPersonIdFrontUrl": "请输入身份证人像面链接", + "legalPersonIdBackUrl": "请输入身份证国徽面链接", + "bankAccountName": "请输入开户名称", + "bankAccountNumber": "请输入对公账号", + "bankName": "请输入开户银行", + "additionalDataJson": "可选,附加信息 JSON", + "tenantPackageId": "请输入套餐ID", + "durationMonths": "请输入订阅时长(月)", + "notes": "可选,订阅备注" + }, + "actions": { + "load": "查询", + "refresh": "刷新", + "submitVerification": "提交实名资料", + "goConsole": "进入控制台", + "goLogin": "返回登录" + }, + "messages": { + "missingTenantInfo": "缺少租户或套餐信息,请返回上一步重试", + "submitSuccess": "资料已提交审核,套餐已成功绑定" + }, + "card": { + "tenantId": "租户ID", + "verificationStatus": "实名状态", + "tenantStatus": "租户状态" + }, + "status": { + "verification": { + "draft": "草稿未提交", + "pending": "审核中", + "rejected": "已驳回", + "approved": "已通过" + }, + "tenant": { + "active": "服务可用", + "pendingReview": "待审核", + "suspended": "已暂停", + "expired": "已过期", + "closed": "已关闭" + }, + "hintDraft": "请补充实名资料后提交", + "hintPending": "正在审核,请耐心等待", + "hintRejected": "审核未通过,请修改后重提", + "hintApproved": "已通过审核,可进入控制台", + "unknown": "状态未知" + }, + "form": { + "title": "提交实名资料", + "tip": "填写营业执照与法人信息,加速审核", + "rejected": "审核未通过,请重新提交", + "businessLicenseNumber": "营业执照编号", + "businessLicenseUrl": "营业执照链接", + "legalPersonName": "法人姓名", + "legalPersonIdNumber": "法人证件号", + "legalPersonIdFrontUrl": "身份证人像面链接", + "legalPersonIdBackUrl": "身份证国徽面链接", + "bankAccountName": "开户名称", + "bankAccountNumber": "对公账号", + "bankName": "开户银行", + "additionalDataJson": "附加信息 JSON", + "tenantPackageId": "套餐ID", + "durationMonths": "订阅时长(月)", + "notes": "备注", + "autoRenew": "到期自动续费", + "securityTip": "资料仅用于审核,不会泄露。" + }, + "subscription": { + "title": "套餐订阅", + "tip": "登录后可提交套餐订阅,提前锁定额度", + "loginTip": "请登录后提交套餐订阅", + "submit": "提交订阅", + "failed": "创建订阅失败,请稍后再试" + }, + "pricing": { + "badge": "自助入驻", + "freeTag": "免费商用", + "title": "超过 53,476 位信赖的开发者", + "subtitle": "以及众多科技巨头的选择", + "defaultDesc": "适用于云端产品,能够用户需按年付费。", + "perTime": "一次性付款", + "selectCta": "立即购买", + "empty": "暂无可选套餐,请稍后重试", + "features": { + "code": "完整源代码", + "docs": "技术文档", + "saasAuth": "SaaS应用授权", + "singleProject": "单个项目使用", + "unlimitedProjects": "无限项目使用", + "support": "一年技术支持", + "update": "一年免费更新", + "limitStore": "门店数上限:{value}", + "limitAccount": "账号数上限:{value}", + "limitStorage": "存储空间:{value}GB", + "unlimited": "不限" + } + }, + "pending": { + "title": "资料审核中", + "subtitle": "我们已收到您的资料,审核通过后可立即使用", + "polling": "将于 {seconds}s 后自动刷新进度", + "tipsTitle": "温馨提示", + "tip1": "避免重复提交,若需补充请稍后再试。", + "tip2": "如遇 429,请稍等 1 分钟后再刷新。" + }, + "ready": { + "title": "审核已通过", + "subtitle": "您可以进入控制台开始使用", + "tipsTitle": "下一步建议", + "tip1": "在控制台检查账号与权限是否符合预期。", + "tip2": "如需调整套餐,可在控制台续费或联系管理员。" + }, + "fallback": { + "title": "无法进入控制台", + "tipsTitle": "可能原因", + "tip1": "租户已暂停或关闭,请联系管理员恢复。", + "tip2": "如需续费,请先登录后续订。" + }, + "message": { + "noTenantId": "请先输入租户ID", + "rateLimit": "请求过于频繁,请稍后再试", + "loadFailed": "获取进度失败,请稍后再试", + "verificationSubmitted": "实名资料已提交", + "submitFailed": "提交失败,请稍后再试", + "subscriptionCreated": "订阅创建成功" + }, + "rule": { + "businessLicenseNumber": "请输入营业执照编号", + "legalPersonName": "请输入法人姓名", + "legalPersonIdNumber": "请输入合法的证件号", + "bankAccountNumber": "请输入正确的对公账号", + "packageRequired": "请选择套餐", + "durationRequired": "请输入订阅时长", + "durationRange": "订阅时长范围 1-120 个月" + } + }, + "lockScreen": { + "pwdError": "密码错误", + "lock": { + "inputPlaceholder": "请输入锁屏密码", + "btnText": "锁定" + }, + "unlock": { + "inputPlaceholder": "请输入解锁密码", + "btnText": "解锁", + "backBtnText": "返回登录" + } + }, + "greeting": { + "dawn": "凌晨了!", + "morning": "上午好!", + "afternoon": "下午好!", + "evening": "晚上好!" + }, + "exceptionPage": { + "403": "抱歉,您无权访问该页面", + "404": "抱歉,您访问的页面不存在", + "500": "抱歉,服务器出错了", + "gohome": "返回首页" + }, + "termsOfService": { + "title": "服务条款" + }, + "menus": { + "login": { + "title": "登录" + }, + "register": { + "title": "注册" + }, + "termsOfService": { + "title": "服务条款" + }, + "onboarding": { + "status": "入驻进度", + "pricing": "套餐选择", + "waiting": "等待审核", + "error": "状态异常" + }, + "forgetPassword": { + "title": "忘记密码" + }, + "resetPassword": { + "title": "重置密码" + }, + "outside": { + "title": "内嵌页面" + }, + "dashboard": { + "title": "仪表盘", + "console": "工作台" + }, + "result": { + "title": "结果页面", + "success": "成功页", + "fail": "失败页" + }, + "exception": { + "title": "异常页面", + "forbidden": "403", + "notFound": "404", + "serverError": "500" + }, + "system": { + "title": "系统管理", + "user": "用户管理", + "role": "角色管理", + "tenantRole": "租户角色", + "userCenter": "个人中心", + "menu": "菜单管理", + "tenant": "租户管理", + "tenantPackage": "套餐管理" + }, + "dictionary": { + "title": "字典管理", + "system": "系统字典", + "tenant": "租户字典", + "override": "字典覆盖", + "labelOverride": "标签覆盖管理", + "metrics": "缓存监控" + }, + "tenant": { + "title": "租户管理", + "subscription": "订阅管理", + "billing": "账单管理", + "billingStatistics": "账单统计" + }, + "announcement": { + "title": "公告管理", + "platform": { + "title": "平台公告", + "list": "平台公告列表", + "create": "创建平台公告", + "edit": "编辑平台公告", + "detail": "公告详情" + }, + "tenant": { + "title": "租户公告", + "list": "租户公告列表", + "create": "创建租户公告", + "edit": "编辑租户公告", + "detail": "公告详情" + }, + "app": { + "title": "应用端公告", + "list": "公告列表", + "detail": "公告详情" + }, + "drafts": { + "title": "草稿中心", + "list": "草稿列表" + } + }, + "merchant": { + "title": "商户管理", + "list": "商户列表", + "review": "商户审核", + "detail": "商户详情" + }, + "store": { + "title": "门店管理", + "list": "门店列表" + } + }, + "tenant": { + "search": { + "keyword": "关键词", + "keywordPlaceholder": "租户名称/代码", + "status": "状态", + "statusPlaceholder": "全部", + "tenantName": "租户名称", + "tenantNamePlaceholder": "请输入租户名称", + "contactName": "联系人", + "contactNamePlaceholder": "请输入联系人", + "contactPhone": "联系电话", + "contactPhonePlaceholder": "请输入联系电话", + "verificationStatus": "认证状态" + }, + "action": { + "addTenant": "新增租户", + "edit": "编辑", + "detail": "详情", + "more": "更多", + "impersonateLogin": "伪装登录", + "quotaOverview": "资源配额概览", + "toggleFreeze": "一键冻结/解冻", + "freeze": "冻结", + "unfreeze": "解冻", + "extendOrGift": "延期/赠送时长", + "resetAdmin": "重置管理员" + }, + "more": { + "impersonate": { + "title": "伪装登录", + "tip": "将切换到该租户后台,并以租户主管理员身份登录。", + "confirm": "确认伪装", + "success": "已切换到租户后台", + "alreadyImpersonating": "当前已处于伪装态,请先退出后再继续" + }, + "freeze": { + "freezeTitle": "冻结租户", + "unfreezeTitle": "解冻租户", + "freezeTip": "冻结后该租户将无法正常使用系统,请谨慎操作。", + "unfreezeTip": "解冻后将恢复租户服务状态(若订阅已到期仍会显示到期)。", + "reason": "原因", + "reasonRequired": "请输入冻结原因", + "freezePlaceholder": "请输入冻结原因(必填)", + "unfreezePlaceholder": "请输入解冻备注(可选)", + "success": "操作成功" + }, + "extend": { + "title": "延期/赠送时长", + "months": "续费时长", + "monthUnit": "月", + "monthsRequired": "请输入续费时长", + "notes": "备注", + "notesPlaceholder": "请输入备注(可选)", + "expireUnknown": "当前订阅到期时间未知,将从当前时间开始顺延。", + "currentExpireAt": "当前订阅到期时间:{date}", + "success": "操作成功" + }, + "resetAdmin": { + "title": "重置管理员", + "tip": "将生成主管理员的重置密码链接(仅展示一次)。", + "generate": "生成重置链接", + "resetUrl": "重置链接", + "copy": "复制", + "copied": "已复制", + "empty": "请先生成链接", + "success": "生成成功", + "failed": "生成失败,请重试" + }, + "todo": { + "impersonateLogin": "伪装登录功能待实现", + "quotaOverview": "资源配额概览功能待实现", + "resetAdmin": "重置管理员功能待实现" + } + }, + "status": { + "pendingReview": "待审核", + "active": "正常运营", + "suspended": "已停用", + "expired": "已过期", + "closed": "已注销", + "unknown": "未知" + }, + "verificationStatus": { + "draft": "草稿未提交", + "pending": "审核中", + "approved": "已通过", + "rejected": "已驳回", + "unknown": "未知" + }, + "subscriptionStatus": { + "active": "生效中", + "expired": "已过期", + "cancelled": "已取消", + "suspended": "已暂停", + "unknown": "未知" + }, + "review": { + "title": "租户审核", + "dialogTitle": "租户审核", + "tenantName": "租户名称", + "result": "审核结果", + "approve": "通过", + "reject": "拒绝", + "rejectReason": "拒绝原因", + "rejectReasonPlaceholder": "请输入拒绝原因", + "renewMonths": "续费时长", + "renewMonthsPlaceholder": "请输入续费时长(月)", + "monthUnit": "月", + "rejectReasonCustom": "自定义原因", + "rejectReasons": { + "materialsIncomplete": "资料不完整/缺少材料", + "licenseInvalid": "营业执照信息无效/不清晰", + "identityMismatch": "法人信息不一致/证件不匹配", + "contactUnreachable": "联系电话无法联系/信息不真实", + "duplicateRegistration": "疑似重复入驻/关联风险", + "other": "其他(自定义)" + }, + "claim": { + "label": "领单", + "unclaimed": "未领取", + "claimed": "已领取", + "claimedBy": "领取人:{name}", + "needClaimTip": "请先领取后再进行审核操作", + "claimButton": "领取审核", + "releaseButton": "释放领取", + "forceButton": "强制接管", + "readonlyTip": "该审核已被 {name} 领取,你当前仅可查看", + "needClaimFirst": "请先领取该审核", + "claimedByTip": "该审核已被 {name} 领取", + "forceDenied": "无强制接管权限" + }, + "selectResult": "请选择审核结果", + "success": "审核完成", + "action": "审核", + "openMaterial": "查看材料", + "section": { + "basic": "申请信息", + "package": "套餐信息", + "subscription": "订阅信息", + "verification": "认证信息", + "action": "审核操作", + "history": "审核历史" + }, + "history": { + "empty": "暂无记录" + }, + "field": { + "name": "租户名称", + "code": "租户代码", + "packageId": "套餐ID", + "packageName": "选择套餐", + "packageType": "套餐类型", + "packageDescription": "套餐描述", + "monthlyPrice": "月费", + "yearlyPrice": "年费", + "packageActive": "是否启用", + "packageQuota": "套餐配额", + "featurePoliciesJson": "功能策略(JSON)", + "subscriptionId": "订阅ID", + "subscriptionStatus": "订阅状态", + "subscriptionPackageId": "订阅套餐ID", + "subscriptionEffectiveFrom": "订阅生效时间", + "subscriptionEffectiveTo": "订阅到期时间", + "nextBillingDate": "下次扣费时间", + "autoRenew": "自动续费", + "contactName": "联系人", + "contactPhone": "联系电话", + "contactEmail": "联系邮箱", + "effectiveFrom": "提交时间", + "verificationStatus": "认证状态", + "verificationSubmittedAt": "认证提交时间", + "businessLicenseNumber": "营业执照号", + "businessLicenseUrl": "资质材料", + "legalPersonName": "法人姓名", + "legalPersonIdNumber": "法人证件号", + "legalPersonIdFrontUrl": "法人证件正面", + "legalPersonIdBackUrl": "法人证件反面", + "bankName": "开户行", + "bankAccountName": "开户名", + "bankAccountNumber": "银行账号", + "additionalDataJson": "补充信息(JSON)", + "reviewRemarks": "审核备注", + "reviewedBy": "审核人ID", + "reviewedByName": "审核人", + "reviewedAt": "审核时间" + }, + "operatingMode": "经营模式", + "operatingModePlaceholder": "请选择经营模式", + "operatingModeOptions": { + "same": "同一主体", + "different": "不同主体" + } + }, + "manualCreate": { + "title": "手动新增租户(直接入驻)", + "success": "创建成功", + "unit": { + "month": "月" + }, + "section": { + "tenant": "租户信息", + "subscription": "套餐与订阅", + "verification": "认证信息", + "admin": "管理员账号", + "system": "系统字段(只读)" + }, + "field": { + "region": "省市县", + "code": "租户代码", + "name": "租户名称", + "shortName": "租户简称", + "legalEntityName": "主体名称", + "industry": "所属行业", + "status": "租户状态", + "contactName": "联系人", + "contactPhone": "联系电话", + "contactEmail": "联系邮箱", + "website": "官网", + "logoUrl": "LogoUrl", + "coverImageUrl": "CoverImageUrl", + "country": "国家/地区", + "province": "省份/州", + "city": "城市", + "district": "区/县", + "address": "详细地址", + "tags": "标签", + "remarks": "备注", + "suspendedAt": "暂停时间", + "suspensionReason": "暂停原因", + "tenantPackageId": "选择套餐", + "durationMonths": "订阅时长", + "subscriptionEffectiveFrom": "订阅生效时间", + "subscriptionEffectiveToPreview": "订阅到期时间", + "nextBillingDate": "下次计费时间", + "autoRenew": "自动续费", + "subscriptionStatus": "订阅状态", + "scheduledPackageId": "预定套餐ID", + "subscriptionNotes": "订阅备注", + "verificationStatus": "认证状态", + "businessLicenseNumber": "营业执照编号", + "businessLicenseUrl": "营业执照URL", + "legalPersonName": "法人姓名", + "legalPersonIdNumber": "法人身份证号", + "legalPersonIdFrontUrl": "身份证正面URL", + "legalPersonIdBackUrl": "身份证反面URL", + "bankAccountName": "对公账户户名", + "bankAccountNumber": "对公账号", + "bankName": "开户行", + "additionalDataJson": "补充资料(JSON)", + "submittedAt": "提交时间", + "reviewedAt": "审核时间", + "reviewedBy": "审核人ID", + "reviewedByName": "审核人姓名", + "reviewRemarks": "审核备注", + "adminAccount": "管理员账号", + "adminDisplayName": "管理员姓名", + "adminPassword": "初始密码", + "adminAvatar": "管理员头像", + "adminMerchantId": "关联商户ID", + "primaryOwnerUserId": "租户所有者UserId", + "tenantId": "TenantId", + "tenantCreatedAt": "Tenant.CreatedAt", + "tenantUpdatedAt": "Tenant.UpdatedAt", + "tenantDeletedAt": "Tenant.DeletedAt", + "subscriptionId": "SubscriptionId", + "verificationId": "VerificationId", + "tenantCreatedBy": "Tenant.CreatedBy", + "tenantUpdatedBy": "Tenant.UpdatedBy", + "tenantDeletedBy": "Tenant.DeletedBy", + "subscriptionDeletedAt": "Subscription.DeletedAt" + }, + "placeholder": { + "region": "请选择省/市/区(县)", + "code": "请输入租户代码(唯一)", + "name": "请输入租户名称", + "shortName": "请输入租户简称", + "legalEntityName": "请输入主体名称", + "industry": "请输入所属行业", + "contactName": "请输入联系人姓名", + "contactPhone": "请输入联系电话(唯一)", + "contactEmail": "请输入联系邮箱", + "website": "请输入官网地址", + "logoUrl": "请输入 Logo URL", + "coverImageUrl": "请输入封面图 URL", + "country": "请输入国家/地区", + "province": "请输入省份/州", + "city": "请输入城市", + "address": "请输入详细地址", + "tags": "多个标签用逗号分隔", + "remarks": "请输入备注", + "suspensionReason": "请输入暂停原因", + "scheduledPackageId": "请输入预定套餐ID(可选)", + "subscriptionNotes": "请输入订阅备注(可选)", + "businessLicenseNumber": "请输入营业执照编号", + "businessLicenseUrl": "请输入营业执照 URL", + "legalPersonName": "请输入法人姓名", + "legalPersonIdNumber": "请输入法人身份证号", + "legalPersonIdFrontUrl": "请输入身份证正面 URL", + "legalPersonIdBackUrl": "请输入身份证反面 URL", + "bankAccountName": "请输入对公账户户名", + "bankAccountNumber": "请输入对公账号", + "bankName": "请输入开户行", + "additionalDataJson": "请输入补充资料 JSON(可选)", + "reviewedByName": "为空则默认当前用户", + "reviewRemarks": "可选", + "adminAccount": "请输入管理员账号(唯一)", + "adminDisplayName": "请输入管理员姓名", + "adminPassword": "请输入初始密码", + "adminAvatar": "请输入头像 URL(可选)", + "adminMerchantId": "请输入关联商户ID(可选)", + "systemGenerated": "系统生成", + "systemAutoFill": "系统自动填充" + }, + "action": { + "upload": "上传", + "reupload": "重新上传", + "remove": "移除", + "noImage": "暂无图片", + "tip": "支持 jpg/png/webp,上传后自动填充" + }, + "rule": { + "code": "请输入租户代码", + "name": "请输入租户名称", + "tenantStatus": "请选择租户状态", + "tenantPackageId": "请选择套餐", + "durationMonths": "请输入订阅时长(月)", + "subscriptionEffectiveFrom": "请选择订阅生效时间", + "subscriptionStatus": "请选择订阅状态", + "verificationStatus": "请选择认证状态", + "adminAccount": "请输入管理员账号", + "adminDisplayName": "请输入管理员姓名", + "adminPassword": "请输入初始密码" + }, + "subscriptionStatus": { + "pending": "待生效", + "active": "生效中", + "gracePeriod": "宽限期", + "cancelled": "已取消", + "suspended": "已暂停" + } + }, + "table": { + "index": "序号", + "name": "租户名称", + "code": "租户代码", + "contactName": "联系人", + "contactPhone": "联系电话", + "status": "状态", + "verificationStatus": "认证状态", + "effectiveFrom": "申请生效时间", + "effectiveTo": "到期时间", + "action": "操作" + } + }, + "tenantPackage": { + "actions": { + "create": "新增套餐", + "close": "关闭", + "view": "查看", + "edit": "编辑", + "more": "更多", + "copy": "复制", + "saveDraft": "保存草稿", + "publish": "发布", + "rollbackDraft": "回滚草稿", + "toggleActive": "上架/下架", + "toggleVisible": "对外可见", + "togglePurchasable": "允许新购", + "quotaEdit": "权益配额", + "viewTenants": "使用租户", + "delete": "删除" + }, + "usage": { + "activeSubscription": "活跃订阅", + "totalSubscription": "总订阅", + "activeTenant": "使用租户", + "mrr": "MRR", + "arr": "ARR", + "expiringIn": "到期分布" + }, + "search": { + "keyword": "关键词", + "keywordPlaceholder": "请输入套餐名称或描述", + "status": "启用状态", + "statusAll": "全部状态", + "statusEnabled": "启用", + "statusDisabled": "停用" + }, + "table": { + "name": "套餐名称", + "packageType": "套餐类型", + "publishStatus": "发布状态", + "publicVisible": "对外可见", + "allowNewPurchase": "允许新购", + "monthlyPrice": "月费", + "yearlyPrice": "年费", + "quota": "配额", + "usage": "使用情况", + "status": "状态", + "description": "描述", + "actions": "操作", + "quotaEmpty": "未配置" + }, + "featurePolicy": { + "open": "可视化配置", + "clear": "清空", + "configured": "已配置", + "notConfigured": "未配置", + "titleEdit": "功能策略配置", + "titleView": "功能策略查看", + "hint": "建议使用可视化方式配置功能开关与配额;保存后将写入 featurePoliciesJson(用于后续按套餐控制功能)。", + "saved": "已保存功能策略", + "invalidJson": "功能策略 JSON 格式不正确", + "applyPreset": "应用模板", + "applyPresetTitle": "确认应用模板", + "applyPresetConfirm": "应用模板会覆盖当前已编辑的功能开关与配额,是否继续?", + "presetApplied": "已应用模板", + "validationPassed": "校验通过", + "validationFailed": "校验失败({count}项)", + "tabs": { + "config": "配置", + "custom": "自定义扩展", + "preview": "JSON 预览" + }, + "presets": { + "blank": "空白模板", + "standard": "标准版模板", + "pro": "专业版模板" + }, + "customHint": "可在此添加自定义扩展项,用于后续扩展(如:赠送硬件、专属服务等)。key 建议使用英文与下划线,保存后将写入 extra.customItems。", + "custom": { + "add": "新增扩展项", + "key": "Key", + "label": "名称", + "type": "类型", + "value": "值", + "typeBoolean": "布尔", + "typeNumber": "数字", + "typeString": "文本" + }, + "sections": { + "features": "功能开关", + "quotas": "功能配额", + "preview": "JSON 预览(只读)" + }, + "features": { + "reportsExport": "报表/导出权限", + "printing": "打印/小票能力", + "apiAccess": "API 调用能力", + "marketing": "营销功能", + "coupon": "优惠券", + "fullReduction": "满减", + "member": "会员", + "points": "积分" + }, + "quotas": { + "maxProducts": "商品上限", + "maxMenus": "菜单上限", + "maxApiCallsPerDay": "API 调用/天", + "unlimitedPlaceholder": "留空表示不限" + } + }, + "detail": { + "featureStrategy": { + "title": "功能策略", + "invalidJson": "功能策略 JSON 格式不正确", + "empty": "暂无可展示的策略项", + "columns": { + "name": "策略名称", + "value": "策略值", + "status": "启用状态", + "conditions": "限制条件" + }, + "status": { + "enabled": "启用", + "disabled": "停用" + }, + "value": { + "unlimited": "不限" + }, + "noConditions": "无", + "conditions": { + "dependsOnEnabled": "依赖:{name}=启用", + "min": "最小值:{min}", + "max": "最大值:{max}", + "unknownDependency": "未知依赖({key})" + } + } + }, + "form": { + "createTitle": "新增套餐", + "editTitle": "编辑套餐", + "viewTitle": "套餐详情", + "copyTitle": "复制套餐", + "publishStatus": "发布状态", + "publicVisible": "对外可见", + "allowNewPurchase": "允许新购", + "isRecommended": "推荐标识", + "tags": "标签", + "tagsPlaceholder": "请选择标签(可多选)", + "name": "套餐名称", + "namePlaceholder": "请输入套餐名称", + "description": "套餐描述", + "descriptionPlaceholder": "请输入套餐描述", + "packageType": "套餐类型", + "packageTypePlaceholder": "请选择套餐类型", + "monthlyPrice": "月费金额(¥)", + "yearlyPrice": "年费金额(¥)", + "pricePlaceholder": "请输入金额", + "maxStoreCount": "门店数(个)", + "maxAccountCount": "员工数(个)", + "maxStorageGb": "存储空间(GB)", + "maxSmsCredits": "短信条数(条)", + "maxDeliveryOrders": "外卖订单数", + "quotaPlaceholder": "请输入配额上限,留空表示不限", + "featurePoliciesJson": "功能策略(JSON)", + "featurePlaceholder": "可选:自定义功能策略 JSON 字符串", + "isActive": "启用状态", + "sortOrder": "排序", + "sortOrderPlaceholder": "数值越小越靠前", + "submit": "提交", + "submitDraft": "保存草稿", + "submitPublish": "发布", + "validation": { + "nameRequired": "请输入套餐名称", + "packageTypeRequired": "请选择套餐类型" + } + }, + "tags": { + "recommended": "推荐", + "bestValue": "性价比", + "flagship": "旗舰" + }, + "drawer": { + "title": "使用租户({name})", + "expiringTitle": "即将到期租户({name} · {days}天内)", + "detailTitle": "套餐详情({name})", + "searchPlaceholder": "租户名称/编码/联系人/电话", + "tenantName": "租户名称", + "tenantCode": "租户代码", + "tenantStatus": "状态", + "contact": "联系人", + "phone": "电话", + "effectiveTo": "到期时间" + }, + "dialog": { + "quotaTitle": "权益配额({name})" + }, + "status": { + "enabled": "启用", + "disabled": "停用" + }, + "publishStatus": { + "draft": "草稿", + "published": "已发布" + }, + "message": { + "createSuccess": "创建成功", + "saveDraftSuccess": "草稿保存成功", + "updateSuccess": "更新成功", + "publishConfirm": "确定要发布该套餐吗?发布后(且对外可见/允许新购/启用)才能被新租户选择/购买。", + "publishTitle": "发布确认", + "publishSuccess": "已发布", + "rollbackDraftConfirm": "确定要将该套餐回滚为草稿吗?草稿状态将不会对外展示/出售。", + "rollbackDraftTitle": "回滚确认", + "rollbackDraftSuccess": "已回滚为草稿", + "toggleVisibleConfirmEnable": "确定要设置为对外可见吗?", + "toggleVisibleConfirmDisable": "确定要设置为对外不可见吗?", + "toggleVisibleTitleEnable": "可见确认", + "toggleVisibleTitleDisable": "不可见确认", + "toggleVisibleSuccessEnable": "已设为对外可见", + "toggleVisibleSuccessDisable": "已设为对外不可见", + "togglePurchasableConfirmEnable": "确定要允许新租户购买/选择该套餐吗?", + "togglePurchasableConfirmDisable": "确定要禁止新租户购买/选择该套餐吗?", + "togglePurchasableTitleEnable": "允许新购确认", + "togglePurchasableTitleDisable": "禁止新购确认", + "togglePurchasableSuccessEnable": "已允许新购", + "togglePurchasableSuccessDisable": "已禁止新购", + "toggleConfirmEnable": "确定要上架该套餐吗?上架后可被新租户选择/购买。", + "toggleConfirmDisable": "确定要下架该套餐吗?下架后新租户将无法选择该套餐(已订阅租户不受影响)。", + "toggleTitleEnable": "上架确认", + "toggleTitleDisable": "下架确认", + "toggleSuccessEnable": "已上架", + "toggleSuccessDisable": "已下架", + "deleteSuccess": "删除成功", + "deleteConfirm": "确定要删除该套餐吗?删除后可在后台恢复。", + "deleteTitle": "删除确认" + }, + "type": { + "free": "免费版", + "standard": "标准版", + "professional": "专业版", + "enterprise": "企业版", + "unknown": "未知" + } + }, + "subscription": { + "title": "订阅管理", + "search": { + "status": "订阅状态", + "statusPlaceholder": "全部状态", + "package": "套餐", + "packagePlaceholder": "全部套餐", + "tenantKeyword": "租户关键词", + "tenantKeywordPlaceholder": "租户名/租户编码", + "expireDateRange": "到期时间", + "expireDateRangePlaceholder": "选择日期范围" + }, + "table": { + "index": "序号", + "tenantName": "租户名称", + "currentPackage": "当前套餐", + "status": "状态", + "effectiveDate": "生效期", + "expireDate": "到期时间", + "createdAt": "创建时间", + "autoRenew": "自动续费", + "action": "操作" + }, + "action": { + "detail": "查看详情", + "more": "更多", + "extend": "延期", + "changePlan": "变更套餐", + "changeStatus": "变更状态" + }, + "status": { + "pending": "待激活", + "active": "生效中", + "gracePeriod": "宽限期", + "cancelled": "已取消", + "suspended": "已暂停" + }, + "detail": { + "title": "订阅详情", + "basicInfo": "基本信息", + "tenantName": "租户名称", + "currentPackage": "当前套餐", + "status": "订阅状态", + "effectiveDate": "生效时间", + "expireDate": "到期时间", + "autoRenew": "自动续费", + "notes": "备注", + "notesPlaceholder": "请输入备注(可选)", + "saveNotes": "保存备注", + "saveNotesSuccess": "备注已保存", + "autoRenewUpdateSuccess": "自动续费设置已更新", + "autoRenewUpdateFailed": "自动续费设置更新失败", + "quotaUsage": "配额使用情况", + "noQuota": "暂无配额数据", + "stores": "门店数", + "accounts": "员工数", + "storage": "存储空间", + "sms": "短信额度", + "deliveryOrders": "外卖订单", + "promotionSlots": "营销活动", + "unlimited": "不限", + "changeHistory": "变更历史", + "noHistory": "暂无变更记录", + "changeType": { + "new": "新订阅", + "renew": "续费", + "upgrade": "升级", + "downgrade": "降级", + "unknown": "变更" + } + }, + "extend": { + "title": "延期订阅", + "duration": "延期时长", + "durationPlaceholder": "请输入延期时长", + "unit": "单位", + "unitDay": "天", + "unitMonth": "月", + "notes": "备注", + "notesPlaceholder": "请输入延期备注(可选)", + "preview": "延期后到期时间", + "success": "延期成功", + "validation": { + "durationRequired": "请输入延期时长", + "durationMin": "延期时长至少为1" + } + }, + "changePlan": { + "title": "变更套餐", + "currentPackage": "当前套餐", + "newPackage": "新套餐", + "newPackagePlaceholder": "请选择新套餐", + "effectiveTime": "生效时间", + "effectiveNow": "立即生效", + "effectiveNextCycle": "下周期生效", + "notes": "备注", + "notesPlaceholder": "请输入变更备注(可选)", + "success": "套餐变更成功", + "validation": { + "packageRequired": "请选择新套餐" + } + }, + "changeStatus": { + "title": "变更状态", + "currentStatus": "当前状态", + "newStatus": "新状态", + "newStatusPlaceholder": "请选择新状态", + "reason": "变更原因", + "reasonPlaceholder": "请输入变更原因", + "confirm": "确定要将订阅状态变更为 {status} 吗?", + "success": "状态变更成功", + "validation": { + "statusRequired": "请选择新状态", + "reasonRequired": "请输入变更原因" + } + } + }, + "quotaPackage": { + "title": "配额包管理", + "tabs": { + "packages": "配额包列表", + "purchases": "购买记录", + "dashboard": "配额仪表盘", + "alertConfig": "告警配置" + }, + "search": { + "quotaType": "配额类型", + "quotaTypePlaceholder": "全部类型", + "status": "状态", + "statusPlaceholder": "全部状态" + }, + "table": { + "index": "序号", + "name": "配额包名称", + "quotaType": "配额类型", + "quotaValue": "配额值", + "price": "价格", + "status": "状态", + "sortOrder": "排序", + "action": "操作" + }, + "action": { + "create": "新增配额包" + }, + "quotaType": { + "storeCount": "门店数量", + "accountCount": "账号数量", + "storage": "存储空间", + "smsCredits": "短信额度", + "deliveryOrders": "配送订单", + "promotionSlots": "促销位" + }, + "status": { + "active": "已上架", + "inactive": "已下架" + }, + "unit": { + "sms": "条", + "orders": "单" + }, + "benefit": { + "featureStrategy": { + "title": "功能策略", + "invalidJson": "功能策略 JSON 格式不正确", + "notConfigured": "未配置功能策略", + "empty": "暂无可展示的策略项", + "columns": { + "name": "策略名称", + "value": "策略值", + "status": "启用状态", + "conditions": "限制条件" + }, + "status": { + "enabled": "启用", + "disabled": "停用" + }, + "value": { + "unlimited": "不限" + }, + "noConditions": "无", + "conditions": { + "dependsOnEnabled": "依赖:{name}=启用", + "min": "最小值:{min}", + "max": "最大值:{max}", + "unknownDependency": "未知依赖({key})" + } + } + }, + "form": { + "createTitle": "新增配额包", + "editTitle": "编辑配额包", + "name": "配额包名称", + "namePlaceholder": "请输入配额包名称", + "quotaType": "配额类型", + "quotaTypePlaceholder": "请选择配额类型", + "quotaValue": "配额值", + "quotaValuePlaceholder": "请输入配额值", + "price": "价格", + "pricePlaceholder": "请输入价格", + "description": "描述", + "descriptionPlaceholder": "请输入描述(可选)", + "sortOrder": "排序", + "isActive": "上架状态" + }, + "purchase": { + "title": "购买配额包", + "tenant": "租户", + "quotaPackage": "配额包", + "quotaPackagePlaceholder": "请选择配额包", + "price": "价格", + "notes": "备注", + "notesPlaceholder": "请输入备注(可选)", + "confirm": "确认购买", + "success": "购买成功" + }, + "purchases": { + "search": { + "tenant": "租户", + "tenantPlaceholder": "请选择租户" + }, + "table": { + "index": "序号", + "tenantName": "租户名称", + "quotaPackageName": "配额包名称", + "quotaType": "配额类型", + "quotaValue": "配额值", + "price": "价格", + "purchasedAt": "购买时间", + "expiredAt": "过期时间" + }, + "message": { + "selectTenantFirst": "请先选择租户" + } + }, + "dashboard": { + "search": { + "tenant": "租户", + "tenantPlaceholder": "请选择租户" + }, + "empty": { + "selectTenant": "请选择租户查看配额使用情况" + }, + "meta": { + "used": "已用", + "total": "总量" + }, + "threshold": { + "reached": "已达 {threshold}%" + }, + "message": { + "selectTenantFirst": "请先选择租户" + } + }, + "alertConfig": { + "tip": { + "title": "告警阈值说明", + "description": "当某类配额使用率达到或超过阈值时,将在仪表盘中显示告警标识。" + }, + "action": { + "reset": "重置", + "resetDefault": "恢复默认", + "save": "保存" + }, + "message": { + "saveSuccess": "保存成功", + "resetSuccess": "已重置", + "resetDefaultSuccess": "已恢复默认阈值" + } + }, + "validation": { + "nameRequired": "请输入配额包名称", + "nameMaxLength": "名称不能超过50个字符", + "quotaTypeRequired": "请选择配额类型", + "quotaValueRequired": "请输入配额值", + "priceRequired": "请输入价格", + "packageRequired": "请选择配额包" + }, + "message": { + "createSuccess": "创建成功", + "updateSuccess": "更新成功", + "deleteConfirm": "确定要删除该配额包吗?", + "deleteSuccess": "删除成功", + "statusUpdateSuccess": "状态更新成功" + } + }, + "announcementDrafts": { + "title": "草稿中心", + "filter": { + "type": "类型", + "typePlaceholder": "全部类型", + "date": "日期", + "dateStart": "开始日期", + "dateEnd": "结束日期" + }, + "table": { + "title": "标题", + "type": "类型", + "lastSaved": "最后保存时间", + "author": "作者", + "empty": "暂无草稿" + }, + "action": { + "continueEdit": "继续编辑", + "publish": "发布" + }, + "message": { + "loadFailed": "草稿加载失败,请稍后重试", + "deleteConfirm": "确定要删除该草稿吗?", + "deleteTitle": "删除草稿", + "deleteSuccess": "草稿已删除", + "publishConfirm": "确定要发布该草稿吗?", + "publishTitle": "发布草稿", + "publishSuccess": "草稿已发布", + "missingRowVersion": "缺少版本信息,无法发布", + "localPublishHint": "本地草稿需进入编辑页发布", + "draftNotFound": "草稿不存在或已过期", + "routeNotFound": "未找到编辑页面,请检查路由配置" + }, + "pagination": { + "total": "共 {count} 条草稿" + }, + "source": { + "local": "本地草稿", + "server": "服务端草稿" + }, + "type": { + "all": "全部", + "system": "系统公告", + "billing": "账单/订阅相关提醒", + "operation": "运营通知", + "systemPlatformUpdate": "平台系统更新公告", + "systemSecurityNotice": "系统安全公告", + "systemCompliance": "系统合规公告", + "tenantInternal": "租户内部公告", + "tenantFinance": "租户财务公告", + "tenantOperation": "租户运营公告", + "unknown": "未知类型" + }, + "field": { + "untitled": "未命名草稿", + "unknownAuthor": "未知", + "unknown": "未知" + } + }, + "table": { + "form": { + "reset": "重置", + "submit": "提交" + }, + "searchBar": { + "reset": "重置", + "search": "查询", + "expand": "展开", + "collapse": "收起", + "searchInputPlaceholder": "请输入", + "searchSelectPlaceholder": "请选择" + }, + "selection": "选择", + "sizeOptions": { + "small": "紧凑", + "default": "默认", + "large": "宽松" + }, + "column": { + "selection": "勾选", + "expand": "展开", + "index": "序号" + }, + "zebra": "斑马纹", + "border": "边框", + "headerBackground": "表头背景" + }, + "billing": { + "title": "账单管理", + "quickFilter": { + "title": "快捷筛选", + "all": "全部" + }, + "search": { + "tenant": "租户", + "tenantPlaceholder": "输入租户名搜索", + "billingType": "账单类型", + "billingTypePlaceholder": "请选择账单类型", + "status": "账单状态", + "statusPlaceholder": "请选择账单状态", + "dateRange": "创建时间", + "dateRangePlaceholder": "选择日期范围", + "amountRange": "金额区间", + "minAmountPlaceholder": "最小金额", + "maxAmountPlaceholder": "最大金额", + "keyword": "关键词", + "keywordPlaceholder": "账单号或租户名" + }, + "field": { + "statementNo": "账单编号", + "tenantName": "租户名称", + "billingType": "账单类型", + "amount": "金额", + "amountDue": "应付金额", + "amountPaid": "已付金额", + "status": "状态", + "dueDate": "到期日", + "periodStart": "计费周期开始", + "periodEnd": "计费周期结束", + "createdAt": "创建时间", + "notes": "备注" + }, + "billingType": { + "subscription": "订阅账单", + "quotaPurchase": "配额包购买", + "manual": "手动创建", + "renewal": "续费账单" + }, + "status": { + "pending": "待支付", + "paid": "已支付", + "overdue": "已逾期", + "cancelled": "已作废" + }, + "action": { + "export": "导出", + "create": "创建账单", + "detail": "查看详情", + "markPaid": "标记已付", + "cancel": "作废", + "recordPayment": "记录支付", + "exportExcel": "导出 Excel", + "exportPdf": "导出 PDF", + "addLineItem": "新增明细项", + "batch": "批量操作", + "batchMarkPaid": "批量标记已支付", + "batchExportExcel": "批量导出 Excel", + "batchExportPdf": "批量导出 PDF", + "uploadProof": "上传凭证" + }, + "dialog": { + "createTitle": "手动创建账单", + "recordPaymentTitle": "确认收款", + "proofPreviewTitle": "凭证预览" + }, + "drawer": { + "title": "账单详情", + "basicInfo": "基本信息", + "paymentList": "支付记录", + "tabs": { + "basic": "基本信息", + "lineItems": "账单明细", + "payments": "支付记录", + "statusFlow": "状态流转" + }, + "openProof": "打开凭证", + "noPayments": "暂无支付记录", + "noStatusFlow": "暂无状态流转记录" + }, + "view": { + "timeline": "时间线", + "table": "表格" + }, + "lineItem": { + "itemType": "项目类型", + "description": "描述", + "quantity": "数量", + "unitPrice": "单价", + "amount": "小计" + }, + "statusFlow": { + "created": "创建", + "due": "到期", + "paid": "已支付", + "overdue": "已逾期", + "cancelled": "已取消" + }, + "hint": { + "amountAutoSum": "金额自动根据明细项汇总" + }, + "placeholder": { + "selectTenant": "请选择租户", + "selectBillingType": "请选择账单类型", + "enterAmount": "请输入金额", + "selectDueDate": "请选择到期日", + "enterNotes": "请输入备注", + "enterItemType": "例如:Subscription/Quota/Manual", + "enterDescription": "请输入明细描述", + "enterPaymentAmount": "请输入支付金额", + "selectPaymentMethod": "请选择支付方式", + "enterTransactionNo": "请输入交易号(可选)", + "enterProofUrl": "请输入支付凭证链接(可选)", + "enterPaymentNotes": "请输入支付备注(可选)" + }, + "validation": { + "tenantRequired": "请选择租户", + "billingTypeRequired": "请选择账单类型", + "amountRequired": "请输入金额", + "amountMin": "金额必须大于0", + "dueDateRequired": "请选择到期日", + "lineItemsRequired": "请至少添加 1 条明细项", + "lineItemDescriptionRequired": "第 {index} 条明细:请填写描述", + "lineItemQuantityInvalid": "第 {index} 条明细:数量必须大于 0", + "lineItemUnitPriceInvalid": "第 {index} 条明细:单价不能小于 0", + "paymentAmountRequired": "请输入支付金额", + "paymentAmountMin": "支付金额必须大于0", + "paymentAmountExceedRemain": "支付金额不能超过剩余应付", + "paymentMethodRequired": "请选择支付方式" + }, + "message": { + "createSuccess": "账单创建成功", + "loadDetailFailed": "账单详情加载失败", + "sortFieldNotSupported": "当前排序字段暂不支持", + "cancelConfirm": "确定要作废该账单吗?", + "cancelByAdmin": "管理员作废", + "cancelSuccess": "账单已作废", + "recordPaymentSuccess": "确认收款成功", + "exportNoData": "暂无可导出的数据", + "exportExcelSuccess": "已导出 {count} 条数据", + "exportSuccess": "已导出 {count} 条数据", + "exportFailed": "导出失败,请稍后重试", + "exportPdfNeedDetail": "请先打开账单详情或选中 1 条账单再导出 PDF", + "exportPdfNeedSingleSelection": "请仅选择 1 条账单用于导出 PDF", + "exportPdfPopupBlocked": "弹窗被浏览器拦截,请允许弹窗后重试", + "exportPdfOpened": "已打开打印窗口,请在打印对话框选择“另存为 PDF”", + "noSelection": "请先选择要操作的账单", + "batchNoPending": "所选账单中没有待支付项", + "batchMarkPaidNotes": "批量标记已支付", + "batchMarkPaidSuccess": "已批量确认支付 {count} 条账单", + "batchCancelNotes": "批量取消", + "batchCancelSuccess": "已批量取消 {count} 条账单", + "proofTypeNotAllowed": "仅支持 JPG/PNG 文件", + "proofTooLarge": "文件过大,最大 {size}MB", + "proofUploadSuccess": "凭证上传成功" + }, + "export": { + "title": "导出配置", + "format": "导出格式", + "scope": "导出范围", + "fields": "字段选择", + "currentPage": "当前页", + "selected": "已选择", + "all": "全部", + "confirm": "开始导出", + "dateRange": "日期范围", + "dateRangePlaceholder": "选择日期范围", + "formatRequired": "请选择导出格式", + "scopeRequired": "请选择导出范围", + "fieldsRequired": "请至少选择一个字段", + "formatExcel": "Excel", + "formatPdf": "PDF", + "formatCsv": "CSV", + "sheetName": "账单列表", + "fileName": "账单导出", + "selectedSuffix": "已选", + "noPayments": "暂无支付记录" + }, + "batch": { + "confirmPayment": "批量确认支付", + "cancel": "批量取消", + "export": "批量导出", + "confirmPaymentTip": "确定要批量确认支付 {count} 条账单吗?", + "cancelTip": "确定要批量取消 {count} 条账单吗?", + "selectedCount": "已选择 {count} 项" + }, + "payment": { + "amount": "金额", + "method": "支付方式", + "status": "状态", + "transactionNo": "交易号", + "paidAt": "支付时间", + "notes": "备注", + "remainAmount": "剩余应付", + "proofUrl": "支付凭证", + "proofUrlHint": "可填写支付凭证图片的URL链接", + "proofUploadHint": "支持 JPG/PNG,最多 10MB,上传后自动写入凭证链接" + }, + "paymentMethod": { + "online": "线上支付", + "bankTransfer": "银行转账", + "other": "其他" + }, + "paymentStatus": { + "pending": "待支付", + "success": "支付成功", + "failed": "支付失败", + "refunded": "已退款" + } + }, + "dashboard": { + "title": "运营仪表盘", + "overview": { + "title": "订阅概览", + "totalActive": "活跃订阅", + "expiringIn7Days": "7天内到期", + "expiringIn3Days": "3天内到期", + "expiringIn1Day": "明天到期", + "expired": "已过期", + "pending": "待激活", + "suspended": "已暂停" + }, + "expiring": { + "title": "即将到期订阅", + "viewAll": "查看全部", + "noData": "暂无即将到期的订阅", + "daysLeft": "剩余天数" + }, + "revenue": { + "title": "收入统计", + "total": "总收入", + "monthly": "本月收入", + "quarterly": "本季度收入" + }, + "quotaRanking": { + "title": "配额使用率排行", + "tenant": "租户", + "usage": "使用量", + "limit": "配额上限", + "percentage": "使用率", + "rank": "排名", + "quotaType": "配额类型", + "selectType": "选择配额类型", + "type": { + "orders": "订单数", + "storage": "存储空间", + "users": "用户数" + } + } + }, + "batch": { + "selectedCount": "已选择 {count} 项", + "extend": { + "title": "批量延期", + "selectedCount": "已选择 {count} 个订阅", + "duration": "延期时长", + "notes": "备注", + "notesPlaceholder": "请输入备注(可选)", + "success": "批量延期成功", + "partialSuccess": "批量延期部分成功,成功 {success} 个,失败 {failed} 个", + "result": "成功 {success} 个,失败 {failed} 个", + "noSelection": "请先选择要延期的订阅", + "failedItems": "失败项" + }, + "remind": { + "title": "批量提醒", + "selectedCount": "已选择 {count} 个订阅", + "content": "提醒内容", + "contentPlaceholder": "请输入提醒内容", + "success": "批量提醒发送成功", + "partialSuccess": "批量提醒部分成功,成功 {success} 个,失败 {failed} 个", + "result": "成功发送 {success} 条,失败 {failed} 条", + "noSelection": "请先选择要提醒的订阅", + "failedItems": "失败项", + "validation": { + "contentRequired": "请输入提醒内容", + "contentMin": "提醒内容至少10个字符" + } + } + }, + "user": { + "action": { + "create": "新增用户", + "view": "详情", + "edit": "编辑", + "more": "更多", + "enable": "启用", + "disable": "禁用", + "unlock": "解锁", + "resetPassword": "重置密码", + "restore": "恢复", + "delete": "删除", + "cancel": "取消", + "confirm": "确定" + }, + "avatar": { + "upload": "上传", + "reupload": "重新上传", + "remove": "移除", + "noImage": "暂无图片", + "tip": "支持 jpg/png/webp,上传后自动填充" + }, + "batch": { + "title": "批量操作", + "selected": "已选 {count} 项", + "enable": "批量启用", + "disable": "批量禁用", + "delete": "批量删除", + "restore": "批量恢复", + "export": "批量导出" + }, + "detail": { + "title": "用户详情", + "rolesTitle": "角色", + "permissionsTitle": "权限" + }, + "empty": { + "roles": "暂无角色", + "permissions": "暂无权限" + }, + "field": { + "userId": "用户ID", + "tenant": "租户", + "account": "账号", + "displayName": "昵称", + "phone": "手机号", + "email": "邮箱", + "status": "状态", + "createdAt": "创建时间", + "lastLoginAt": "最后登录", + "password": "密码", + "roles": "角色", + "avatar": "头像" + }, + "status": { + "active": "启用", + "disabled": "禁用", + "locked": "锁定", + "deleted": "已删除", + "unknown": "未知" + }, + "table": { + "avatar": "头像", + "account": "账号", + "displayName": "昵称", + "phone": "手机号", + "email": "邮箱", + "roles": "角色", + "status": "状态", + "createdAt": "创建时间", + "lastLoginAt": "最后登录", + "actions": "操作", + "tenant": "租户" + }, + "search": { + "keyword": "关键词", + "keywordPlaceholder": "账号/昵称/手机号/邮箱", + "status": "状态", + "statusPlaceholder": "全部状态", + "role": "角色", + "rolePlaceholder": "请选择角色", + "createdRange": "创建时间", + "createdRangeStart": "开始时间", + "createdRangeEnd": "结束时间", + "lastLoginRange": "最后登录", + "lastLoginRangeStart": "开始时间", + "lastLoginRangeEnd": "结束时间", + "includeDeleted": "包含已删除", + "includeDeletedOn": "包含", + "includeDeletedOff": "不包含", + "tenant": "租户", + "tenantPlaceholder": "请选择租户" + }, + "placeholder": { + "tenant": "请选择租户", + "account": "请输入账号", + "displayName": "请输入昵称", + "password": "请输入密码", + "phone": "请输入手机号", + "email": "请输入邮箱", + "avatar": "请输入头像链接", + "roles": "请选择角色", + "status": "请选择状态" + }, + "dialog": { + "createTitle": "新增用户", + "editTitle": "编辑用户" + }, + "validation": { + "tenantRequired": "请选择租户", + "accountRequired": "请输入账号", + "displayNameRequired": "请输入昵称", + "passwordRequired": "请输入密码", + "passwordLength": "密码长度需为 6~32 位", + "phoneInvalid": "请输入正确的手机号", + "emailInvalid": "请输入正确的邮箱" + }, + "message": { + "createSuccess": "用户创建成功", + "missingUserId": "缺少用户ID,无法提交", + "rowVersionMissing": "缺少版本信息,请刷新后重试", + "updateSuccess": "用户更新成功", + "concurrencyConflict": "数据已被更新,请刷新后重试:{message}", + "statusConfirm": "确认要调整 {name} 的状态吗?", + "statusSuccess": "状态更新成功", + "deleteConfirm": "确认删除 {name} 吗?", + "deleteSuccess": "用户已删除", + "restoreConfirm": "确认恢复 {name} 吗?", + "restoreSuccess": "用户已恢复", + "resetConfirm": "确认重置 {name} 的密码吗?", + "resetToken": "重置令牌:{token}\n有效期至:{expiresAt}", + "batchEmpty": "请先选择需要操作的用户", + "batchLimit": "单次最多操作 100 个用户", + "batchConfirm": "确认对 {count} 个用户执行该批量操作吗?", + "batchResult": "成功 {success} 个,失败 {failure} 个。\n失败明细:\n{details}", + "batchSuccess": "批量操作成功,共 {count} 个用户", + "exportSuccess": "已导出 {count} 条用户记录" + }, + "export": { + "fileName": "用户列表" + } + }, + "announcement": { + "audience": { + "title": "受众选择", + "targetType": "目标类型", + "targetTypeHint": "选择公告发布的受众范围", + "type": { + "all": "全部租户/用户", + "roles": "指定角色", + "users": "指定用户", + "rules": "规则模式", + "manual": "手动选择" + }, + "helper": { + "all": "将向所有租户或用户发送公告", + "roles": "按角色筛选发送对象", + "users": "通过搜索添加指定用户", + "rules": "通过部门、角色与标签组合筛选", + "manual": "通过搜索与穿梭框选择用户" + }, + "rules": { + "departments": "部门", + "roles": "角色", + "tags": "标签", + "departmentsPlaceholder": "请选择部门", + "tagsPlaceholder": "请选择或输入标签", + "emptyDepartments": "暂无部门数据", + "emptyRoles": "暂无角色数据", + "estimateLabel": "预估人数", + "estimateLoading": "正在预估..." + }, + "users": { + "placeholder": "请输入姓名/手机号/邮箱搜索用户", + "unknownUser": "用户 {id}" + }, + "manual": { + "searchPlaceholder": "输入姓名/手机号/邮箱搜索", + "transferLeft": "候选用户", + "transferRight": "已选用户", + "filterPlaceholder": "输入关键词筛选", + "estimateLabel": "已选人数" + }, + "errors": { + "loadRolesFailed": "加载角色失败", + "loadUsersFailed": "加载用户失败" + } + } + }, + "store": { + "title": "门店管理", + "list": "门店列表", + "detail": "门店详情", + "fee": { + "title": "费用配置", + "minimumOrderAmount": "起送金额", + "deliveryFee": "基础配送费", + "packagingFeeMode": "打包费模式", + "fixedPackagingFee": "固定打包费", + "freeDeliveryThreshold": "免配送费门槛 (满免)", + "packagingMode": { + "title": "收取方式", + "fixed": "固定按单", + "perItem": "按商品累加", + "byItem": "按商品收费", + "byOrder": "按订单收费" + }, + "packagingHelp": { + "title": "收费说明", + "item1": "按订单统一设置打包费", + "item2": "一口价模式:每单按照一口价进行收取", + "item3": "阶梯价模式:每单按照订单内商品折后价总和收取对应的打包费 (折后价总和=折扣商品折后价+非折扣商品原价,不计算满减、红包、商品券等优惠)" + }, + "packagingRule": { + "title": "收费规则", + "tiered": "阶梯价", + "fixed": "一口价" + }, + "tier": { + "label": "折后价阶梯{index}", + "inputPlaceholder": "请输入", + "unit": "元", + "unitInclude": "元(含)", + "fee": "打包费", + "limitInfinity": "阶梯上限 - 无限", + "add": "新增阶梯{current}/{max}" + }, + "preview": { + "title": "费用计算预览", + "action": "计算", + "orderAmount": "订单金额", + "itemCount": "商品数量", + "itemsTitle": "商品明细 (模拟)", + "addItem": "添加商品", + "skuId": "SKU ID", + "quantity": "数量", + "packagingFee": "单品打包费", + "result": { + "totalAmount": "总金额", + "totalFee": "总费用", + "deliveryFee": "配送费", + "packagingFee": "打包费", + "meetsMinimum": "满足起送", + "shortfall": "起送差额" + } + } + }, + "deliveryZone": { + "tip": "配置门店的配送区域及其对应的起送价、配送费和预计送达时间。", + "action": { + "add": "新增区域", + "edit": "编辑区域", + "setup": "去设置" + }, + "table": { + "zoneName": "区域名称", + "minimumOrderAmount": "起送价", + "deliveryFee": "配送费", + "estimatedMinutes": "预计送达(分)" + }, + "status": { + "configured": "已配置配送区域", + "notConfigured": "未配置配送区域" + }, + "form": { + "basicInfo": "基础信息", + "costSettings": "费用与时效", + "zoneName": "区域名称", + "zoneNamePlaceholder": "给此区域起个名字,如:核心配送区", + "polygonGeoJson": "配送范围", + "polygonPlaceholder": "请点击下方按钮绘制", + "minimumOrderAmount": "起送金额", + "deliveryFee": "配送费", + "estimatedMinutes": "预计送达(分钟)", + "sortOrder": "排序 (越小越前)", + "drawPolygonTitle": "绘制配送范围", + "mapKeyMissing": "未配置地图密钥", + "mapKeyMissingHint": "请联系管理员配置 VITE_TENCENT_MAP_KEY", + "drawPolygon": "绘制范围", + "editPolygon": "编辑图形", + "clearPolygon": "清空图形", + "drawHint": "在地图上点击添加点,双击结束绘制", + "editHint": "拖动图形顶点进行调整", + "clickToEdit": "点击修改范围", + "clickToDraw": "点击设置范围" + }, + "check": { + "title": "配送范围校验", + "action": "检测坐标", + "longitude": "经度", + "latitude": "纬度", + "inRange": "在配送范围内", + "outOfRange": "超出配送范围", + "distance": "距离门店", + "zoneName": "所在区域" + }, + "rules": { + "zoneName": "请输入区域名称", + "polygonGeoJson": "请设置配送范围" + }, + "deleteConfirm": "确定要删除该配送区域配置吗?", + "deleteTitle": "删除确认" + } + }, + "platform": { + "title": "平台运营", + "storeAudits": "门店审核", + "qualificationAlerts": "资质预警" + } +} diff --git a/src/locales/zh-CN/announcement.ts b/src/locales/zh-CN/announcement.ts new file mode 100644 index 0000000..e343acc --- /dev/null +++ b/src/locales/zh-CN/announcement.ts @@ -0,0 +1,154 @@ +/** + * 公告模块国际化(zh-CN) + * + * 注意:该文件会在 i18n 初始化时合并到 `announcement.*` 命名空间下 + */ +export default { + common: { + empty: '-' + }, + create: { + title: '新建平台公告' + }, + edit: { + title: '编辑平台公告', + notEditable: '当前公告已发布或撤销,无法编辑', + readonlyFieldsHint: '当前接口仅支持编辑标题、内容与目标受众,其余字段仅展示' + }, + list: { + total: '共 {total} 条公告' + }, + action: { + create: '新建', + refresh: '刷新', + detail: '详情', + edit: '编辑', + publish: '发布', + revoke: '撤销', + delete: '删除', + save: '保存', + back: '返回' + }, + search: { + status: '状态', + statusPlaceholder: '全部状态', + keyword: '关键词', + keywordPlaceholder: '标题/内容', + dateRange: '日期范围', + dateRangeStart: '开始日期', + dateRangeEnd: '结束日期' + }, + table: { + title: '标题', + type: '类型', + priority: '优先级', + status: '状态', + effectiveRange: '有效期', + publishedAt: '发布时间', + actions: '操作', + effectiveOpenEnded: '长期有效' + }, + form: { + title: '标题', + titlePlaceholder: '请输入公告标题', + type: '类型', + typePlaceholder: '请选择公告类型', + priority: '优先级', + effectiveFrom: '生效开始时间', + effectiveFromPlaceholder: '请选择开始时间', + effectiveTo: '生效结束时间', + effectiveToPlaceholder: '请选择结束时间(可选)', + target: '目标受众', + targetTypePlaceholder: '请选择受众类型', + content: '内容' + }, + status: { + draft: '草稿', + published: '已发布', + revoked: '已撤销', + unknown: '未知' + }, + type: { + system: '系统公告', + billing: '账单/订阅相关提醒', + operation: '运营通知', + platformUpdate: '平台系统更新公告', + security: '系统安全公告', + compliance: '系统合规公告', + tenantInternal: '租户内部公告', + tenantFinance: '租户财务公告', + tenantOperation: '租户运营公告', + unknown: '未知类型' + }, + targetType: { + all: '全部用户', + rules: '规则筛选', + roles: '指定角色', + users: '指定用户', + manual: '手动选择' + }, + audience: { + placeholder: '受众选择器将在后续版本接入完整能力', + rulesPlaceholder: '请输入规则 JSON(示例:{"roles":["roleId"]})', + usersPlaceholder: '请输入用户ID,使用英文逗号分隔' + }, + draft: { + notSaved: '未保存', + savedAt: '已保存 · {time}', + readOnly: '只读状态,已停止自动保存' + }, + validation: { + titleRequired: '请输入公告标题', + titleMax: '标题不能超过 128 个字符', + contentRequired: '请输入公告内容', + typeRequired: '请选择公告类型', + priorityRequired: '请输入优先级', + priorityRange: '优先级范围为 1-5', + effectiveFromRequired: '请选择生效开始时间', + effectiveToAfter: '结束时间必须晚于开始时间', + targetTypeRequired: '请选择目标受众类型', + targetRulesRequired: '请完善规则受众条件', + targetUsersRequired: '请至少选择一个用户', + targetRulesInvalid: '规则 JSON 格式不正确' + }, + message: { + loadFailed: '加载公告失败', + createSuccess: '创建成功', + createFailed: '创建失败,请稍后重试', + updateSuccess: '更新成功', + updateFailed: '更新失败,请稍后重试', + publishConfirm: '确认发布该公告?', + publishSuccess: '发布成功', + publishFailed: '发布失败,请稍后重试', + revokeConfirm: '确认撤销该公告?', + revokeSuccess: '撤销成功', + revokeFailed: '撤销失败,请稍后重试', + deleteNotSupported: '平台公告暂不支持删除', + rowVersionMissing: '缺少并发控制版本,操作已取消', + validationFailed: '请完善表单必填项', + missingId: '缺少公告ID,无法加载详情', + targetParseFailed: '目标受众参数解析失败', + concurrencyConflict: '并发冲突: {message}', + refreshAndRetry: '数据已被其他用户修改,请刷新页面后重试', + cannotEditPublished: '当前公告状态为 {status},无法编辑', + invalidId: '无效的公告ID' + }, + detail: { + id: '公告ID', + status: '状态', + type: '类型', + priority: '优先级', + effectiveFrom: '生效开始时间', + effectiveTo: '生效结束时间', + publishedAt: '发布时间', + revokedAt: '撤销时间', + targetType: '目标受众', + targetParameters: '受众参数', + publisherScope: '发布范围', + content: '公告内容', + contentPlaceholder: '暂无公告内容', + contentHint: '富文本预览将接入安全渲染组件(DOMPurify)', + readStats: '已读统计', + readStatsPlaceholder: '已读统计接口尚未接入' + } +} diff --git a/src/locales/zh-CN/billing.json b/src/locales/zh-CN/billing.json new file mode 100644 index 0000000..000ad05 --- /dev/null +++ b/src/locales/zh-CN/billing.json @@ -0,0 +1,244 @@ +{ + "table": { + "column": { + "action": "操作" + } + }, + "billing": { + "quickFilter": { + "title": "快捷筛选", + "all": "全部", + "pending": "待支付", + "paid": "已支付", + "overdue": "已逾期", + "cancelled": "已取消" + }, + "search": { + "tenant": "租户", + "tenantPlaceholder": "请选择租户(可选)", + "billingType": "账单类型", + "billingTypePlaceholder": "请选择账单类型", + "status": "状态", + "statusPlaceholder": "请选择状态", + "dateRange": "日期范围", + "dateRangePlaceholder": "请选择日期范围", + "amountRange": "金额范围", + "minAmountPlaceholder": "最小金额", + "maxAmountPlaceholder": "最大金额", + "keyword": "关键词", + "keywordPlaceholder": "账单号/租户名" + }, + "action": { + "export": "导出", + "exportPdf": "导出 PDF", + "create": "新建", + "detail": "详情", + "cancel": "作废", + "recordPayment": "确认收款", + "verifyPayment": "审核收款", + "addLineItem": "新增明细", + "uploadProof": "上传凭证" + }, + "dialog": { + "createTitle": "创建账单", + "recordPaymentTitle": "确认收款", + "proofPreviewTitle": "凭证预览" + }, + "field": { + "statementNo": "账单号", + "tenantName": "租户", + "billingType": "账单类型", + "amount": "金额", + "amountDue": "应收金额", + "amountPaid": "实收金额", + "status": "状态", + "dueDate": "到期日", + "createdAt": "创建时间", + "periodStart": "周期开始", + "periodEnd": "周期结束", + "notes": "备注" + }, + "drawer": { + "tabs": { + "basic": "基本信息", + "lineItems": "账单明细", + "payments": "支付记录", + "statusFlow": "状态流转" + }, + "title": "账单详情", + "basicInfo": "基本信息", + "paymentList": "支付记录", + "openProof": "打开凭证", + "noPayments": "暂无支付记录", + "noStatusFlow": "暂无状态流转记录" + }, + "view": { + "timeline": "时间线", + "table": "表格" + }, + "status": { + "draft": "草稿", + "pending": "待支付", + "paid": "已支付", + "overdue": "已逾期", + "cancelled": "已取消" + }, + "statusFlow": { + "created": "创建", + "due": "到期", + "paid": "已支付", + "overdue": "已逾期", + "cancelled": "已取消" + }, + "lineItem": { + "itemType": "类型", + "description": "描述", + "quantity": "数量", + "unitPrice": "单价", + "amount": "金额" + }, + "billingType": { + "subscription": "订阅账单", + "quotaPurchase": "配额包购买", + "manual": "手动创建", + "renewal": "续费账单" + }, + "placeholder": { + "enterItemType": "请输入类型", + "enterDescription": "请输入描述", + "selectTenant": "请选择租户", + "selectBillingType": "请选择账单类型", + "selectDueDate": "请选择到期日", + "enterNotes": "请输入备注(可选)", + "selectPaymentMethod": "请选择支付方式", + "enterPaymentAmount": "请输入支付金额", + "enterTransactionNo": "请输入交易号(可选)", + "enterPaymentNotes": "请输入备注(可选)" + }, + "hint": { + "amountAutoSum": "金额由明细自动汇总" + }, + "validation": { + "tenantRequired": "请选择租户", + "billingTypeRequired": "请选择账单类型", + "dueDateRequired": "请选择到期日", + "lineItemsRequired": "请至少添加 1 条明细", + "lineItemDescriptionRequired": "第 {index} 条明细请填写描述", + "lineItemQuantityInvalid": "第 {index} 条明细数量不合法", + "lineItemUnitPriceInvalid": "第 {index} 条明细单价不合法", + "amountMin": "应收金额需大于 0", + "paymentAmountRequired": "请输入支付金额", + "paymentAmountMin": "支付金额需大于 0", + "paymentAmountExceedRemain": "支付金额不能超过剩余应付", + "paymentMethodRequired": "请选择支付方式" + }, + "message": { + "exportFailed": "导出失败,请重试", + "exportNoData": "暂无可导出的数据", + "exportSuccess": "导出成功", + "exportExcelSuccess": "已导出 {count} 条", + "exportPdfNeedSingleSelection": "导出 PDF 仅支持单条选择", + "exportPdfOpened": "已在新窗口打开 PDF", + "exportPdfPopupBlocked": "弹窗被浏览器拦截,请允许弹窗后重试", + "generatingPdf": "正在生成 PDF,请稍候...", + "exportCancelled": "导出已取消", + "batchMarkPaidSuccess": "批量标记已支付成功", + "batchMarkPaidNotes": "批量标记已支付", + "batchNoPending": "所选账单中没有待支付项", + "batchCancelNotes": "批量作废", + "batchCancelSuccess": "批量作废成功", + "cancelByAdmin": "管理员作废", + "cancelConfirm": "确定要作废该账单吗?", + "cancelSuccess": "作废成功", + "createSuccess": "创建成功", + "loadDetailFailed": "加载账单详情失败", + "recordPaymentSuccess": "确认收款成功", + "verifyPaymentSuccess": "审核收款成功", + "verifyPaymentConfirm": "确定要审核通过该笔收款吗?审核后将同步更新账单已收金额。", + "proofTypeNotAllowed": "仅支持 JPG/PNG 图片", + "proofTooLarge": "文件过大(最大 {size}MB)", + "proofUploadSuccess": "凭证上传成功", + "sortFieldNotSupported": "暂不支持该排序字段" + }, + "batch": { + "confirmPayment": "确认收款", + "confirmPaymentTip": "将所选待支付账单标记为已支付", + "cancel": "批量作废", + "cancelTip": "将所选账单作废(不可恢复)", + "export": "批量导出", + "selectedCount": "已选 {count} 条" + }, + "payment": { + "amount": "收款金额", + "remainAmount": "剩余应收", + "method": "支付方式", + "transactionNo": "交易号", + "proofUrl": "支付凭证", + "proofUploadHint": "支持 JPG/PNG,单张 ≤ 10MB", + "paidAt": "支付时间", + "notes": "备注", + "status": "状态" + }, + "paymentMethod": { + "online": "在线支付", + "bankTransfer": "银行转账", + "other": "其他" + }, + "paymentStatus": { + "pending": "待处理", + "success": "成功", + "failed": "失败", + "refunded": "已退款" + }, + "export": { + "title": "导出账单", + "format": "导出格式", + "formatExcel": "Excel", + "formatPdf": "PDF", + "formatCsv": "CSV", + "scope": "导出范围", + "currentPage": "当前页", + "selected": "已选", + "all": "全部", + "fields": "导出字段", + "dateRange": "日期范围", + "dateRangePlaceholder": "请选择日期范围", + "confirm": "开始导出", + "sheetName": "账单", + "fileName": "账单导出", + "noPayments": "暂无支付记录", + "formatRequired": "请选择导出格式", + "scopeRequired": "请选择导出范围", + "fieldsRequired": "请至少选择 1 个字段" + }, + "statistics": { + "title": "账单统计", + "startDate": "开始日期", + "endDate": "结束日期", + "groupBy": { + "day": "按天", + "week": "按周", + "month": "按月" + }, + "totalRevenue": "实收金额", + "pendingAmount": "未收金额", + "overdueAmount": "逾期金额", + "totalAmountDue": "应收金额", + "statusDistribution": "账单状态分布", + "paymentMethodDistribution": "支付方式占比", + "paymentMethodNoData": "暂无支付方式统计数据", + "revenueTrend": "收入趋势", + "topDebtors": "租户欠款排行榜", + "overdueDays": "逾期天数" + }, + "timeline": { + "created": "创建账单", + "dueDate": "到期日", + "overdueByDays": "已逾期 {days} 天", + "payment": "支付记录", + "paymentDesc": "{method} ¥{amount}", + "currentStatus": "当前状态:{status}", + "currentStatusDesc": "以当前状态为准" + } + } +} diff --git a/src/locales/zh-CN/dictionary.json b/src/locales/zh-CN/dictionary.json new file mode 100644 index 0000000..269aaf0 --- /dev/null +++ b/src/locales/zh-CN/dictionary.json @@ -0,0 +1,197 @@ +{ + "dictionary": { + "common": { + "system": "系统", + "business": "业务", + "refresh": "刷新", + "warning": "提示", + "confirm": "确认", + "cancel": "取消", + "edit": "编辑", + "detail": "详情", + "delete": "删除", + "actions": "操作", + "key": "键", + "value": "值", + "enabled": "启用", + "description": "描述", + "default": "默认", + "sortOrder": "排序", + "code": "编码", + "name": "名称", + "scope": "作用域", + "order": "顺序", + "hidden": "隐藏", + "source": "来源", + "tenant": "租户", + "systemLabel": "系统", + "selectGroupFirst": "请先选择分组" + }, + "group": { + "searchPlaceholder": "按编码或名称搜索", + "new": "新增分组", + "createTitle": "新增字典分组", + "editTitle": "编辑字典分组", + "codePlaceholder": "例如 ORDER_STATUS", + "namePlaceholder": "字典分组名称", + "scopePlaceholder": "选择作用域", + "allowOverride": "允许覆盖", + "enabled": "启用", + "description": "描述", + "empty": "暂无分组", + "deleteConfirm": "删除分组会同时删除其字典项,是否继续?", + "deleteSuccess": "已删除", + "rowVersionMissing": "行版本缺失,请刷新后重试。", + "codeRequired": "编码不能为空", + "codeLength": "长度 {min}-{max}", + "codePattern": "仅支持字母、数字和下划线", + "nameRequired": "名称不能为空", + "nameTooLong": "名称过长", + "descriptionTooLong": "描述过长" + }, + "item": { + "new": "新增字典项", + "createTitle": "新增字典项", + "editTitle": "编辑字典项", + "keyPlaceholder": "例如 PENDING", + "value": "字典值", + "default": "默认", + "enabled": "启用", + "sortOrder": "排序", + "deleteConfirm": "确定删除该字典项?", + "deleteSuccess": "已删除", + "import": "导入", + "export": "导出", + "importCompleted": "导入完成", + "rowVersionMissing": "行版本缺失,请刷新后重试。", + "groupNotSelected": "请先选择分组", + "keyRequired": "键不能为空", + "keyTooLong": "键过长", + "valueRequired": "至少填写一种语言", + "descriptionTooLong": "描述过长", + "sortUpdated": "排序已更新" + }, + "i18n": { + "zh": "中文", + "en": "英文", + "zhPlaceholder": "请输入中文内容", + "enPlaceholder": "请输入英文内容", + "hint": "至少填写一种语言,建议同时填写中英文。" + }, + "import": { + "title": "批量导入字典项", + "dropHere": "拖拽文件到此或点击上传", + "tip": "最大 10MB,仅支持 CSV 或 JSON", + "conflictMode": "冲突处理", + "skip": "跳过", + "overwrite": "覆盖", + "append": "追加", + "successSummary": "成功: {success},跳过: {skip},错误: {error}", + "row": "行号", + "field": "字段", + "message": "错误信息", + "start": "开始导入", + "fileTooLarge": "文件大小超过 10MB", + "selectFile": "请选择文件", + "unsupportedFormat": "不支持的文件格式" + }, + "override": { + "toggleEnabled": "覆盖已启用", + "toggleDisabled": "覆盖未启用", + "systemItems": "系统字典项", + "customView": "自定义视图", + "newCustomItem": "新增自定义项", + "saveSortOrder": "保存排序", + "tenantGroupNotReady": "租户分组未就绪", + "hiddenSaved": "隐藏项已保存", + "sortSaved": "排序已保存", + "selectSystemDictionary": "请选择系统字典", + "selectGroupHint": "请选择分组后继续", + "unsavedSortConfirm": "排序未保存,确定离开?" + }, + "metrics": { + "cacheHitRatio": "缓存命中率", + "totalQueries": "总查询数", + "hits": "命中", + "misses": "未命中", + "avgResponse": "平均响应", + "last1h": "最近 1 小时", + "hitRatioTrend": "命中率趋势(L1 vs L2)", + "timeRange1h": "1 小时", + "timeRange24h": "24 小时", + "timeRange7d": "7 天", + "invalidationEvents": "失效事件", + "rangeTo": "至", + "start": "开始", + "end": "结束", + "timestamp": "时间", + "dictionary": "字典", + "operation": "操作", + "keys": "键数量", + "operator": "操作人", + "create": "新增", + "update": "更新", + "delete": "删除", + "l1HitRatio": "L1 命中率", + "l2HitRatio": "L2 命中率", + "target": "目标" + }, + "labelOverride": { + "title": "标签覆盖管理", + "tenantTitle": "租户标签覆盖", + "platformTitle": "平台标签覆盖", + "selectTenant": "选择租户", + "selectTenantHint": "选择租户查看其标签覆盖配置", + "dictionaryItem": "字典项", + "dictionaryItemKey": "字典项键", + "originalValue": "原始值", + "overrideValue": "覆盖值", + "overrideType": "覆盖类型", + "tenantCustomization": "租户定制", + "platformEnforcement": "平台强制", + "reason": "覆盖原因", + "reasonPlaceholder": "请输入覆盖原因(可选)", + "reasonHint": "建议说明覆盖原因,便于后续审计", + "createdAt": "创建时间", + "updatedAt": "更新时间", + "operator": "操作人", + "newOverride": "新增覆盖", + "editOverride": "编辑覆盖", + "deleteOverride": "删除覆盖", + "deleteConfirm": "确定删除该标签覆盖?删除后将恢复原始显示值。", + "noOverrides": "暂无标签覆盖配置", + "selectDictionaryItem": "请选择字典项", + "overrideValueRequired": "请输入覆盖值", + "onlySystemDictionary": "租户只能覆盖系统字典项", + "filterAll": "全部", + "filterTenantCustomization": "租户定制", + "filterPlatformEnforcement": "平台强制" + }, + "errors": { + "loadGroups": "加载字典分组失败。", + "loadGroup": "加载字典分组失败。", + "createGroup": "创建字典分组失败。", + "updateGroup": "更新字典分组失败。", + "deleteGroup": "删除字典分组失败。", + "loadItems": "加载字典项失败。", + "createItem": "创建字典项失败。", + "updateItem": "更新字典项失败。", + "deleteItem": "删除字典项失败。", + "loadOverrides": "加载覆盖配置失败。", + "loadOverride": "加载覆盖配置失败。", + "enableOverride": "启用覆盖失败。", + "disableOverride": "关闭覆盖失败。", + "updateHidden": "更新隐藏项失败。", + "updateSort": "更新排序失败。", + "dataConflict": "数据已被他人修改,请刷新后重试。", + "loadLabelOverrides": "加载标签覆盖失败。", + "saveLabelOverride": "保存标签覆盖失败。", + "deleteLabelOverride": "删除标签覆盖失败。" + }, + "messages": { + "deleted": "已删除", + "importCompleted": "导入完成", + "sortUpdated": "排序已更新" + } + } +} diff --git a/src/locales/zh-CN/merchant.ts b/src/locales/zh-CN/merchant.ts new file mode 100644 index 0000000..6c8369d --- /dev/null +++ b/src/locales/zh-CN/merchant.ts @@ -0,0 +1,135 @@ +/** + * 商户模块国际化(zh-CN) + * + * 注意:该文件会在 i18n 初始化时合并到 `merchant.*` 命名空间下 + */ +export default { + list: { + title: '商户列表', + search: { + keyword: '关键词', + keywordPlaceholder: '商户名称/营业执照号', + status: '审核状态', + operatingMode: '经营模式', + tenant: '租户', + tenantPlaceholder: '请输入租户ID' + }, + table: { + name: '商户名称', + tenantName: '所属租户', + operatingMode: '经营模式', + status: '状态', + frozen: '冻结状态', + storeCount: '门店数量', + createdAt: '创建时间' + } + }, + review: { + title: '商户审核', + claim: '领取', + release: '释放', + approve: '通过', + reject: '驳回', + revoke: '撤销审核', + reason: '原因', + comment: '备注', + result: '审核结果', + claimed: '已领取', + unclaimed: '未领取', + readonlyTip: '当前审核已被他人领取,您只能查看', + reasonPlaceholder: '请输入驳回原因', + selectResult: '请选择审核结果', + needClaim: '请先领取审核', + success: '审核提交成功', + pendingReApproval: '关键信息变更待审核', + empty: '暂无审核记录', + section: { + action: '审核操作', + history: '审核历史' + } + }, + detail: { + title: '商户详情', + basicInfo: '基本信息', + subjectInfo: '主体信息', + stores: '门店列表', + auditHistory: '审核历史', + changeHistory: '变更历史' + }, + fields: { + name: '商户名称', + tenant: '租户', + status: '审核状态', + operatingMode: '经营模式', + licenseNumber: '营业执照号', + legalRepresentative: '法人', + registeredAddress: '注册地址', + contactPhone: '联系电话', + contactEmail: '联系邮箱', + isFrozen: '冻结状态', + frozenReason: '冻结原因', + approvedAt: '审核通过时间', + approvedBy: '审核通过人', + createdAt: '创建时间', + updatedAt: '更新时间' + }, + status: { + pending: '待审核', + approved: '审核通过', + rejected: '已拒绝', + frozen: '冻结', + unknown: '未知' + }, + operatingMode: { + same: '同一主体', + different: '不同主体' + }, + frozen: { + yes: '已冻结', + no: '正常' + }, + action: { + edit: '编辑', + detail: '查看详情', + export: '导出PDF' + }, + placeholder: { + name: '请输入商户名称', + licenseNumber: '请输入营业执照号', + legalRepresentative: '请输入法人姓名', + registeredAddress: '请输入注册地址', + contactPhone: '请输入联系电话', + contactEmail: '请输入联系邮箱' + }, + rules: { + nameRequired: '请输入商户名称', + contactPhoneRequired: '请输入联系电话', + contactEmailInvalid: '邮箱格式不正确' + }, + message: { + rowVersionMissing: '缺少版本信息,请刷新后重试', + updateSuccess: '更新成功', + updateRequiresReview: '关键信息修改,商户已进入待审核状态' + }, + store: { + name: '门店名称', + status: '门店状态', + address: '门店地址', + contactPhone: '联系电话', + licenseNumber: '营业执照号', + empty: '暂无门店', + statusOperating: '营业中', + statusPreparing: '筹备中', + statusClosed: '已关闭', + statusSuspended: '已停业', + statusUnknown: '未知' + }, + change: { + field: '字段', + oldValue: '修改前', + newValue: '修改后', + changedBy: '操作人', + changedAt: '变更时间', + empty: '暂无变更记录' + } +} diff --git a/src/locales/zh-CN/store.ts b/src/locales/zh-CN/store.ts new file mode 100644 index 0000000..e521fd7 --- /dev/null +++ b/src/locales/zh-CN/store.ts @@ -0,0 +1,506 @@ +/** + * 门店模块国际化(zh-CN) + * + * 注意:该文件会在 i18n 初始化时合并到 `store.*` 命名空间下 + */ +export default { + list: { + search: { + keyword: '关键词', + keywordPlaceholder: '门店名称/门店编码', + merchantId: '商户ID', + merchantIdPlaceholder: '请输入商户ID', + auditStatus: '审核状态', + businessStatus: '经营状态', + ownershipType: '主体类型' + }, + table: { + name: '门店名称', + code: '门店编码', + ownershipType: '主体类型', + auditStatus: '审核状态', + businessStatus: '经营状态', + phone: '联系电话', + createdAt: '创建时间' + }, + action: { + create: '新增门店', + detail: '查看详情', + edit: '编辑', + toggleStatus: '切换状态', + submitAudit: '提交审核' + } + }, + form: { + createTitle: '新增门店', + editTitle: '编辑门店', + tabs: { + basic: '基础信息', + location: '位置信息', + settings: '经营设置', + services: '服务设施' + }, + merchantId: '商户ID', + merchantIdPlaceholder: '请输入商户ID', + code: '门店编码', + codePlaceholder: '请输入门店编码', + name: '门店名称', + namePlaceholder: '请输入门店名称', + phone: '联系电话', + phonePlaceholder: '请输入联系电话', + managerName: '负责人姓名', + managerNamePlaceholder: '请输入负责人姓名', + status: '门店状态', + signboardImageUrl: '门头招牌图', + signboardImageUrlPlaceholder: '请输入门头招牌图链接', + ownershipType: '主体类型', + categoryId: '类目ID', + categoryIdPlaceholder: '请输入类目ID', + deliveryRadiusKm: '配送半径(公里)', + locationTitle: '地址信息', + province: '省份', + provincePlaceholder: '请输入省份', + city: '城市', + cityPlaceholder: '请输入城市', + district: '区县', + districtPlaceholder: '请输入区县', + address: '详细地址', + addressPlaceholder: '请输入详细地址', + coordinate: '经纬度', + coordinatePlaceholder: '例如 39.695954,116.074058', + coordinatePasteAction: '一键粘贴', + coordinatePickerAction: '打开坐标拾取', + coordinateHint: '支持纬度,经度或经度,纬度;可使用腾讯坐标拾取工具复制', + coordinatePasteUnavailable: '当前环境不支持读取剪贴板', + coordinatePasteEmpty: '剪贴板为空,请先复制坐标', + coordinateInvalid: '坐标格式不正确,请检查', + settingsTitle: '服务设置', + announcement: '门店公告', + announcementPlaceholder: '请输入门店公告', + tags: '标签', + tagsPlaceholder: '多个标签用逗号分隔', + supportsDineIn: '支持堂食', + supportsPickup: '支持自取', + supportsDelivery: '支持外卖', + supportsReservation: '支持预订', + supportsQueueing: '支持排队' + }, + rules: { + merchantId: '请输入商户ID', + code: '请输入门店编码', + name: '请输入门店名称', + signboardImageUrl: '请填写门头招牌图', + ownershipType: '请选择主体类型', + coordinateText: '请输入经纬度', + coordinateTextFormat: '坐标格式应为 纬度,经度 或 经度,纬度' + }, + detail: { + title: '门店详情', + empty: '暂无门店信息', + back: '返回列表', + fields: { + name: '门店名称', + code: '门店编码', + auditStatus: '审核状态', + businessStatus: '经营状态', + ownershipType: '主体类型', + phone: '联系电话', + address: '地址', + createdAt: '创建时间' + }, + tabs: { + basic: '基本信息', + qualification: '资质管理', + businessHours: '营业时段', + temporaryHours: '临时调整', + deliveryZone: '配送区域', + fee: '费用配置' + } + }, + qualification: { + warningTitle: '资质预警', + complete: '资质齐全', + incomplete: '资质不完整', + expiringSoon: '即将过期 {count} 项', + expired: '已过期 {count} 项', + action: { + add: '新增资质', + edit: '编辑资质' + }, + table: { + type: '资质类型', + documentNumber: '证件编号', + expiresAt: '到期时间', + status: '状态' + }, + form: { + type: '资质类型', + fileUrl: '文件链接', + fileUrlPlaceholder: '请输入资质文件链接', + documentNumber: '证件编号', + documentNumberPlaceholder: '请输入证件编号', + issuedAt: '签发日期', + expiresAt: '到期日期', + sortOrder: '排序' + }, + rules: { + type: '请选择资质类型', + fileUrl: '请输入资质文件链接', + documentNumber: '请输入证件编号', + expiresAt: '请选择到期日期' + }, + status: { + valid: '有效', + expiringSoon: '即将过期', + expired: '已过期' + }, + type: { + businessLicense: '营业执照', + foodService: '食品经营许可证', + storefront: '门头实景照', + interior: '店内环境照' + }, + deleteTitle: '删除资质', + deleteConfirm: '确认删除该资质吗?' + }, + businessHours: { + tip: '支持跨天营业时间段,保存时会自动校验重叠与跨天情况。', + action: { + add: '新增时段', + save: '保存设置' + }, + table: { + dayOfWeek: '星期', + hourType: '营业类型', + startTime: '开始时间', + endTime: '结束时间', + capacityLimit: '接单上限', + notes: '备注', + notesPlaceholder: '备注信息' + }, + days: { + sunday: '周日', + monday: '周一', + tuesday: '周二', + wednesday: '周三', + thursday: '周四', + friday: '周五', + saturday: '周六' + }, + hourType: { + normal: '正常营业', + reservation: '仅预订', + pickup: '自取/配送', + closed: '暂停营业' + } + }, + temporaryHours: { + tip: '设置特定日期的歇业、临时营业或调整营业时间', + allDay: '全天', + action: { + add: '添加临时调整' + }, + table: { + dateRange: '日期范围', + timeRange: '时间段', + overrideType: '调整类型', + reason: '说明' + }, + overrideType: { + closed: '歇业', + temporaryOpen: '临时营业', + modifiedHours: '调整时间' + }, + dialog: { + addTitle: '添加临时调整', + editTitle: '编辑临时调整' + }, + form: { + dateRange: '日期范围', + isAllDay: '全天生效', + timeRange: '时间段', + overrideType: '调整类型', + reason: '说明', + reasonPlaceholder: '如:春节放假、情人节延长营业等' + }, + rules: { + dateRequired: '请选择日期范围', + typeRequired: '请选择调整类型', + timeRequired: '非全天模式下请选择时间段' + }, + deleteConfirm: '确定要删除这条临时调整吗?' + }, + deliveryZone: { + tip: '配送区域使用 GeoJSON 描述,可先维护多边形后再进行配送检测。', + action: { + add: '新增区域', + edit: '编辑区域' + }, + table: { + zoneName: '区域名称', + minimumOrderAmount: '起送金额', + deliveryFee: '配送费', + estimatedMinutes: '预计送达(分钟)' + }, + form: { + zoneName: '区域名称', + zoneNamePlaceholder: '请输入区域名称', + polygonGeoJson: 'GeoJSON', + polygonPlaceholder: '请输入 GeoJSON 多边形', + drawPolygon: '绘制区域', + drawPolygonTitle: '绘制配送区域', + editPolygon: '编辑', + clearPolygon: '清空', + drawHint: '绘制完成后点击确定应用到 GeoJSON', + mapKeyMissing: '未配置腾讯地图 Key,无法加载绘图组件', + mapKeyMissingHint: '请在环境变量 VITE_TENCENT_MAP_KEY 中配置腾讯地图 Key', + minimumOrderAmount: '起送金额', + deliveryFee: '配送费', + estimatedMinutes: '预计送达(分钟)', + sortOrder: '排序' + }, + rules: { + zoneName: '请输入区域名称', + polygonGeoJson: '请输入 GeoJSON' + }, + deleteTitle: '删除配送区域', + deleteConfirm: '确认删除该配送区域吗?', + check: { + title: '配送检测', + action: '检测', + longitude: '经度', + latitude: '纬度', + inRange: '配送范围内', + outOfRange: '配送范围外', + distance: '距离', + zoneName: '命中区域' + } + }, + fee: { + title: '费用配置', + minimumOrderAmount: '起送费', + deliveryFee: '配送费', + packagingFeeMode: '打包费模式', + fixedPackagingFee: '固定打包费', + freeDeliveryThreshold: '免配送门槛', + packagingMode: { + fixed: '固定金额', + perItem: '按商品' + }, + preview: { + title: '费用预览', + action: '计算预览', + orderAmount: '订单金额', + itemCount: '商品数量', + itemsTitle: '商品明细', + addItem: '新增商品', + skuId: 'SKU', + quantity: '数量', + packagingFee: '打包费', + result: { + totalAmount: '订单总额', + totalFee: '费用合计', + deliveryFee: '配送费', + packagingFee: '打包费', + meetsMinimum: '满足起送', + shortfall: '差额' + } + } + }, + auditStatus: { + draft: '草稿', + pending: '待审核', + activated: '已激活', + rejected: '已驳回', + unknown: '未知' + }, + businessStatus: { + title: '经营状态调整', + targetStatus: '目标状态', + closureReason: '歇业原因', + closureReasonText: '补充说明', + closureReasonTextPlaceholder: '请输入补充说明', + forceClosedTip: '当前门店已被平台强制关闭,无法由租户自行调整。', + rules: { + status: '请选择目标状态', + reason: '请选择歇业原因' + }, + open: '营业中', + resting: '休息中', + forceClosed: '强制关闭', + unknown: '未知' + }, + ownership: { + same: '同一主体', + different: '不同主体' + }, + closureReason: { + equipment: '设备维护', + vacation: '负责人休假', + outOfStock: '缺货', + temporarilyClosed: '临时歇业', + other: '其他' + }, + status: { + closed: '已关闭', + preparing: '筹备中', + operating: '营业中', + suspended: '已停业' + }, + action: { + save: '保存', + edit: '编辑', + delete: '删除', + refresh: '刷新' + }, + message: { + createSuccess: '创建成功', + updateSuccess: '更新成功', + deleteSuccess: '删除成功', + statusUpdated: '经营状态已更新', + submitAuditSuccess: '审核已提交' + }, + audit: { + stats: { + pending: '待审核', + overdue: '超时待审', + approved: '已通过', + rejected: '已驳回', + avgProcessing: '平均处理时长(小时)' + }, + search: { + keyword: '关键词', + keywordPlaceholder: '门店名称/商户名称', + tenantId: '租户ID', + tenantIdPlaceholder: '请输入租户ID', + dateRange: '提交时间', + dateRangeStart: '开始日期', + dateRangeEnd: '结束日期', + overdueOnly: '仅显示超时' + }, + table: { + storeName: '门店名称', + storeCode: '门店编码', + tenantName: '所属租户', + merchantName: '商户名称', + ownershipType: '主体类型', + submittedAt: '提交时间', + waitingDays: '等待天数', + qualificationCount: '资质数量', + overdue: '超时状态' + }, + action: { + detail: '查看详情', + approve: '审核通过', + reject: '审核驳回', + riskControl: '风控操作' + }, + overdue: '超时', + notOverdue: '正常', + detail: { + title: '审核详情', + empty: '暂无数据', + loadFailed: '加载审核详情失败', + viewFile: '查看文件', + tabs: { + basic: '基本信息', + qualification: '资质信息', + history: '审核记录' + }, + sections: { + tenant: '租户信息', + merchant: '商户信息' + }, + fields: { + storeName: '门店名称', + storeCode: '门店编码', + auditStatus: '审核状态', + ownershipType: '主体类型', + phone: '联系电话', + address: '门店地址', + submittedAt: '提交时间', + signboard: '门头招牌', + tenantName: '租户名称', + tenantContact: '联系人', + tenantPhone: '联系电话', + merchantName: '商户名称', + merchantLegal: '法人名称', + merchantCredit: '社会信用代码', + file: '资质文件' + } + }, + history: { + action: '动作', + operator: '操作人', + remark: '备注', + createdAt: '时间' + }, + approve: { + prompt: '可填写审核备注(可选)', + placeholder: '请输入审核备注', + success: '审核已通过' + }, + reject: { + title: '审核驳回', + reason: '驳回原因', + reasonText: '补充说明', + reasonTextPlaceholder: '请输入补充说明', + remark: '审核备注', + remarkPlaceholder: '请输入审核备注', + success: '审核已驳回', + rules: { + reason: '请选择驳回原因', + reasonText: '请输入驳回原因补充说明' + }, + reasonOptions: { + licenseMissing: '证照缺失', + photoBlur: '证照模糊', + inconsistent: '信息不一致', + other: '其他' + } + }, + riskControl: { + title: '风控操作', + storeName: '当前门店:{name}', + action: '操作类型', + forceClose: '强制关闭', + reopen: '解除关闭', + reason: '关闭原因', + reasonPlaceholder: '请输入关闭原因', + remark: '备注', + remarkPlaceholder: '请输入备注', + forceCloseSuccess: '门店已强制关闭', + reopenSuccess: '门店已解除强制关闭', + rules: { + action: '请选择操作类型', + reason: '请输入关闭原因' + } + } + }, + alerts: { + summary: { + expiringSoon: '即将过期', + expired: '已过期' + }, + search: { + tenantId: '租户ID', + tenantIdPlaceholder: '请输入租户ID', + daysThreshold: '预警阈值(天)', + expired: '过期状态' + }, + table: { + storeName: '门店名称', + storeCode: '门店编码', + tenantName: '所属租户', + qualificationType: '资质类型', + expiresAt: '到期时间', + daysUntilExpiry: '剩余天数', + status: '状态', + businessStatus: '经营状态' + }, + status: { + expiring: '即将过期', + expired: '已过期' + } + } +} diff --git a/src/locales/zh-CN/tenant.ts b/src/locales/zh-CN/tenant.ts new file mode 100644 index 0000000..d7e90d6 --- /dev/null +++ b/src/locales/zh-CN/tenant.ts @@ -0,0 +1,267 @@ +/** + * 租户模块国际化(zh-CN) + * + * 注意:该文件会在 i18n 初始化时合并到 `tenant.*` 命名空间下 + */ +export default { + // 详情 Drawer + detail: { + title: '租户详情', + basicInfo: '基本信息', + statusInfo: '状态信息', + subscriptionInfo: '订阅信息', + quotaInfo: '配额信息', + billingInfo: '账单信息', + + tenantId: '租户ID', + name: '租户名称', + code: '租户代码', + shortName: '租户简称', + industry: '所属行业', + contactName: '联系人', + contactPhone: '联系电话', + contactEmail: '联系邮箱', + + status: '租户状态', + verificationStatus: '认证状态', + autoRenew: '自动续费', + effectiveFrom: '生效时间', + effectiveTo: '到期时间', + currentPackage: '当前套餐' + }, + + // Tab 标签 + tabs: { + basicInfo: '基本信息', + statusInfo: '状态信息', + subscription: '订阅信息', + quotaOverview: '配额概览', + billing: '账单信息' + }, + + // 配额相关 + quota: { + title: '配额资源概览', + usageSummary: '配额使用情况', + usageHistory: '配额使用明细', + purchaseHistory: '配额购买记录', + + recordedAt: '记录时间', + changeType: '变更类型', + snapshot: '快照', + historyNotice: '当前为实时数据快照,历史记录功能待后端接口补充', + fullOrderId: '完整订单ID', + + orderNo: '订单号', + quotaType: '配额类型', + quotaPackage: '配额包', + limitValue: '配额上限', + usedValue: '已使用', + remainingValue: '剩余', + usagePercentage: '使用率', + resetCycle: '重置周期', + lastResetAt: '上次重置时间', + + purchaseValue: '购买数量', + price: '价格', + purchasedAt: '购买时间', + expiredAt: '过期时间', + notes: '备注', + + type: { + store: '门店数', + account: '账号数', + storageGb: '存储空间(GB)', + smsCredits: '短信额度', + deliveryOrders: '外卖订单数', + unknown: '未知类型({value})' + } + }, + + // 公告管理 + announcement: { + title: '租户公告', + listTitle: '租户公告列表', + createTitle: '新建公告', + editTitle: '编辑公告', + detailTitle: '公告详情', + + search: { + status: '状态', + statusPlaceholder: '全部状态', + keyword: '关键词', + keywordPlaceholder: '标题/内容关键词', + dateRange: '日期范围', + dateRangePlaceholder: '选择日期范围' + }, + + field: { + title: '标题', + content: '内容', + announcementType: '公告类型', + priority: '优先级', + status: '状态', + effectiveRange: '有效期', + effectiveFrom: '生效开始', + effectiveTo: '生效结束', + targetType: '目标受众', + targetParameters: '目标参数', + departmentIds: '部门ID', + roleIds: '角色ID', + tagIds: '标签ID', + publishedAt: '发布时间', + revokedAt: '撤销时间', + isActive: '是否生效', + rowVersion: '并发版本' + }, + + action: { + create: '新建公告', + edit: '编辑', + detail: '详情', + publish: '发布', + revoke: '撤销', + delete: '删除', + save: '保存', + back: '返回', + more: '更多' + }, + + status: { + draft: '草稿', + published: '已发布', + revoked: '已撤销' + }, + + type: { + system: '系统公告', + billing: '账单/订阅相关提醒', + operation: '运营通知', + systemPlatformUpdate: '平台系统更新公告', + systemSecurityNotice: '系统安全公告', + systemCompliance: '系统合规公告', + tenantInternal: '租户内部公告', + tenantFinance: '租户财务公告', + tenantOperation: '租户运营公告' + }, + + targetType: { + all: '全员', + roles: '角色', + users: '指定用户', + rules: '规则匹配', + manual: '手动选择' + }, + + placeholder: { + title: '请输入公告标题', + content: '请输入公告内容', + type: '请选择公告类型', + priority: '请输入优先级', + effectiveRange: '选择生效时间范围', + targetType: '请选择目标受众', + roleIds: '请输入角色ID,逗号分隔', + userIds: '请输入用户ID,逗号分隔', + departmentIds: '请输入部门ID,逗号分隔', + tagIds: '请输入标签ID,逗号分隔' + }, + + validation: { + titleRequired: '请输入公告标题', + titleLength: '标题长度需为 2-128 字符', + contentRequired: '请输入公告内容', + typeRequired: '请选择公告类型', + priorityRequired: '请输入优先级', + effectiveRangeRequired: '请选择有效期', + targetTypeRequired: '请选择目标受众', + roleIdsRequired: '请输入至少一个角色ID', + userIdsRequired: '请输入至少一个用户ID', + targetRuleRequired: '请至少填写一个规则字段' + }, + + message: { + loadListFailed: '加载公告列表失败', + loadDetailFailed: '加载公告详情失败', + detailNotFound: '未获取到公告详情,请返回列表重试', + tenantIdMissing: '未获取到租户ID,无法继续操作', + publishConfirm: '确认发布该公告吗?', + revokeConfirm: '确认撤销该公告吗?', + deleteConfirm: '确认删除该公告吗?', + publishSuccess: '公告发布成功', + revokeSuccess: '公告已撤销', + deleteSuccess: '公告已删除', + createSuccess: '公告创建成功', + updateSuccess: '公告更新成功', + actionNotAllowed: '当前状态不支持该操作', + empty: '暂无公告数据' + }, + + tip: { + tenantScope: '当前为租户范围公告,将仅发送给本租户用户。', + draftAutoSave: '草稿自动保存中', + draftSaved: '草稿已保存于 {time}', + draftLoaded: '已加载草稿', + draftNotEditable: '仅草稿状态可编辑' + }, + + section: { + content: '公告内容', + audience: '目标受众' + } + }, + + // 编辑/新增弹窗 + edit: { + titleEdit: '编辑租户', + titleCreate: '新增租户', + + placeholder: { + code: '请输入租户代码', + name: '请输入租户名称', + shortName: '请输入租户简称', + industry: '请输入所属行业', + contactName: '请输入联系人', + contactPhone: '请输入联系电话', + contactEmail: '请输入联系邮箱', + tenantPackageId: '请选择套餐', + effectiveFrom: '选择生效日期' + }, + + package: { + title: '套餐信息', + select: '选择套餐', + durationMonths: '购买时长' + }, + + packageType: { + free: '免费', + paid: '付费' + }, + + unit: { + month: '月' + }, + + validation: { + codeRequired: '请输入租户代码', + nameRequired: '请输入租户名称', + tenantPackageIdRequired: '请选择套餐' + } + }, + + // 提示信息 + warning: { + updateApiNotReady: '租户编辑接口开发中,请稍后再试' + }, + error: { + loadDetailFailed: '加载租户详情失败', + loadQuotaFailed: '加载配额信息失败', + loadBillingFailed: '加载账单信息失败', + loadSubscriptionFailed: '加载订阅信息失败', + updateFailed: '更新失败,请重试' + }, + success: { + registerSuccess: '注册成功', + updateSuccess: '更新成功' + } +} diff --git a/src/locales/zh-CN/tenantPackage.ts b/src/locales/zh-CN/tenantPackage.ts new file mode 100644 index 0000000..e0fd6fe --- /dev/null +++ b/src/locales/zh-CN/tenantPackage.ts @@ -0,0 +1,137 @@ +/** + * 租户套餐模块国际化(zh-CN) + */ +export default { + actions: { + more: '更多', + copy: '复制', + toggleActive: '启用/禁用', + toggleVisible: '显示/隐藏', + togglePurchasable: '上架/下架', + publish: '发布', + rollbackDraft: '撤回草稿', + quotaEdit: '调整配额', + viewTenants: '查看关联租户', + delete: '删除', + view: '查看', + edit: '编辑' + }, + drawer: { + title: '关联租户 - {name}', + expiringTitle: '关联租户 - {name} ({days}天内到期)', + searchPlaceholder: '搜索租户名称/代码', + tenantName: '租户名称', + tenantCode: '租户代码', + tenantStatus: '租户状态', + contact: '联系人', + phone: '联系电话', + effectiveTo: '到期时间' + }, + search: { + keyword: '关键词', + keywordPlaceholder: '搜索套餐名称', + status: '状态', + statusAll: '全部', + statusEnabled: '启用', + statusDisabled: '禁用' + }, + form: { + name: '套餐名称', + namePlaceholder: '请输入套餐名称', + packageType: '套餐类型', + packageTypePlaceholder: '请选择套餐类型', + monthlyPrice: '月付价格', + yearlyPrice: '年付价格', + pricePlaceholder: '请输入价格', + maxStoreCount: '最大门店数', + maxAccountCount: '最大账号数', + maxStorageGb: '存储空间(GB)', + maxSmsCredits: '短信额度', + maxDeliveryOrders: '外卖订单数', + quotaPlaceholder: '请输入数量 (-1为无限)', + featurePoliciesJson: '功能策略', + featurePlaceholder: '请输入JSON格式的策略配置', + isActive: '是否启用', + publicVisible: '公开可见', + allowNewPurchase: '允许新购', + publishStatus: '发布状态', + isRecommended: '是否推荐', + sortOrder: '排序值', + submit: '提交' + }, + status: { + enabled: '启用', + disabled: '禁用' + }, + publishStatus: { + published: '已发布', + draft: '草稿' + }, + type: { + free: '免费版', + standard: '标准版', + professional: '专业版', + enterprise: '企业版' + }, + tags: { + recommended: '推荐', + bestValue: '超值', + flagship: '旗舰' + }, + table: { + name: '套餐名称', + packageType: '类型', + publishStatus: '发布状态', + publicVisible: '公开', + allowNewPurchase: '可购', + monthlyPrice: '月付', + yearlyPrice: '年付', + quota: '配额概览', + usage: '使用情况', + status: '状态', + actions: '操作', + quotaEmpty: '无限制' + }, + message: { + updateSuccess: '更新成功', + + toggleConfirmEnable: '确认启用该套餐吗?', + toggleConfirmDisable: '确认禁用该套餐吗?禁用后不可新购。', + toggleTitleEnable: '启用确认', + toggleTitleDisable: '禁用确认', + toggleSuccessEnable: '已启用', + toggleSuccessDisable: '已禁用', + + toggleVisibleConfirmEnable: '确认设为公开可见吗?', + toggleVisibleConfirmDisable: '确认设为隐藏吗?', + toggleVisibleTitleEnable: '公开确认', + toggleVisibleTitleDisable: '隐藏确认', + toggleVisibleSuccessEnable: '已设为公开', + toggleVisibleSuccessDisable: '已设为隐藏', + + togglePurchasableConfirmEnable: '确认允许新用户购买吗?', + togglePurchasableConfirmDisable: '确认禁止新用户购买吗?', + togglePurchasableTitleEnable: '允许购买确认', + togglePurchasableTitleDisable: '禁止购买确认', + togglePurchasableSuccessEnable: '已允许购买', + togglePurchasableSuccessDisable: '已禁止购买', + + publishConfirm: '确认发布该套餐吗?发布后租户可见。', + publishTitle: '发布确认', + publishSuccess: '发布成功', + + rollbackDraftConfirm: '确认撤回为草稿吗?撤回后新用户不可见。', + rollbackDraftTitle: '撤回确认', + rollbackDraftSuccess: '已撤回为草稿', + + deleteConfirm: '确认删除该套餐吗?此操作不可恢复。', + deleteTitle: '删除确认', + deleteSuccess: '删除成功' + }, + dialog: { + quotaTitle: '配额管理 - {name}' + }, + featurePolicy: { + invalidJson: 'JSON格式不正确' + } +} diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..b257649 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,34 @@ +import App from './App.vue' +import { createApp } from 'vue' +import { initStore } from './store' // Store +import { initRouter } from './router' // Router +import language from './locales' // 国际化 +import '@styles/core/tailwind.css' // tailwind +import '@styles/index.scss' // 样式 +import '@utils/sys/console.ts' // 控制台输出内容 +import { setupGlobDirectives } from './directives' +import { setupErrorHandle } from './utils/sys/error-handle' + +document.addEventListener( + 'touchstart', + function () {}, + { passive: false } +) + +const app = createApp(App) +const warnIgnores = [ + 'onUnmounted is called when there is no active component instance to be associated with.' +] +app.config.warnHandler = (msg, instance, trace) => { + if (warnIgnores.some((item) => msg.includes(item))) { + return + } + console.warn(`[Vue warn]: ${msg}${trace}`) +} +initStore(app) +initRouter(app) +setupGlobDirectives(app) +setupErrorHandle(app) + +app.use(language) +app.mount('#app') diff --git a/src/mock/temp/formData.ts b/src/mock/temp/formData.ts new file mode 100644 index 0000000..8aa7ae5 --- /dev/null +++ b/src/mock/temp/formData.ts @@ -0,0 +1,273 @@ +import avatar1 from '@/assets/images/avatar/avatar1.webp' +import avatar2 from '@/assets/images/avatar/avatar2.webp' +import avatar3 from '@/assets/images/avatar/avatar3.webp' +import avatar4 from '@/assets/images/avatar/avatar4.webp' +import avatar5 from '@/assets/images/avatar/avatar5.webp' +import avatar6 from '@/assets/images/avatar/avatar6.webp' +import avatar7 from '@/assets/images/avatar/avatar7.webp' +import avatar8 from '@/assets/images/avatar/avatar8.webp' +import avatar9 from '@/assets/images/avatar/avatar9.webp' +import avatar10 from '@/assets/images/avatar/avatar10.webp' + +export interface User { + id: number + username: string + gender: 1 | 0 + mobile: string + email: string + dep: string + status: string + create_time: string + avatar: string +} + +// 用户列表 +export const ACCOUNT_TABLE_DATA: User[] = [ + { + id: 1, + username: 'alexmorgan', + gender: 1, + mobile: '18670001591', + email: 'alexmorgan@company.com', + dep: '研发部', + status: '1', + create_time: '2020-09-09 10:01:10', + avatar: avatar1 + }, + { + id: 2, + username: 'sophiabaker', + gender: 1, + mobile: '17766664444', + email: 'sophiabaker@company.com', + dep: '电商部', + status: '1', + create_time: '2020-10-10 13:01:12', + avatar: avatar2 + }, + { + id: 3, + username: 'liampark', + gender: 1, + mobile: '18670001597', + email: 'liampark@company.com', + dep: '人事部', + status: '1', + create_time: '2020-11-14 12:01:45', + avatar: avatar3 + }, + { + id: 4, + username: 'oliviagrant', + gender: 0, + mobile: '18670001596', + email: 'oliviagrant@company.com', + dep: '产品部', + status: '1', + create_time: '2020-11-14 09:01:20', + avatar: avatar4 + }, + { + id: 5, + username: 'emmawilson', + gender: 0, + mobile: '18670001595', + email: 'emmawilson@company.com', + dep: '财务部', + status: '1', + create_time: '2020-11-13 11:01:05', + avatar: avatar5 + }, + { + id: 6, + username: 'noahevan', + gender: 1, + mobile: '18670001594', + email: 'noahevan@company.com', + dep: '运营部', + status: '1', + create_time: '2020-10-11 13:10:26', + avatar: avatar6 + }, + { + id: 7, + username: 'avamartin', + gender: 1, + mobile: '18123820191', + email: 'avamartin@company.com', + dep: '客服部', + status: '2', + create_time: '2020-05-14 12:05:10', + avatar: avatar7 + }, + { + id: 8, + username: 'jacoblee', + gender: 1, + mobile: '18670001592', + email: 'jacoblee@company.com', + dep: '总经办', + status: '3', + create_time: '2020-11-12 07:22:25', + avatar: avatar8 + }, + { + id: 9, + username: 'miaclark', + gender: 0, + mobile: '18670001581', + email: 'miaclark@company.com', + dep: '研发部', + status: '4', + create_time: '2020-06-12 05:04:20', + avatar: avatar9 + }, + { + id: 10, + username: 'ethanharris', + gender: 1, + mobile: '13755554444', + email: 'ethanharris@company.com', + dep: '研发部', + status: '1', + create_time: '2020-11-12 16:01:10', + avatar: avatar10 + }, + { + id: 11, + username: 'isabellamoore', + gender: 1, + mobile: '13766660000', + email: 'isabellamoore@company.com', + dep: '研发部', + status: '1', + create_time: '2020-11-14 12:01:20', + avatar: avatar6 + }, + { + id: 12, + username: 'masonwhite', + gender: 1, + mobile: '18670001502', + email: 'masonwhite@company.com', + dep: '研发部', + status: '1', + create_time: '2020-11-14 12:01:20', + avatar: avatar7 + }, + { + id: 13, + username: 'charlottehall', + gender: 1, + mobile: '13006644977', + email: 'charlottehall@company.com', + dep: '研发部', + status: '1', + create_time: '2020-11-14 12:01:20', + avatar: avatar8 + }, + { + id: 14, + username: 'benjaminscott', + gender: 0, + mobile: '13599998888', + email: 'benjaminscott@company.com', + dep: '研发部', + status: '1', + create_time: '2020-11-14 12:01:20', + avatar: avatar9 + }, + { + id: 15, + username: 'ameliaking', + gender: 1, + mobile: '13799998888', + email: 'ameliaking@company.com', + dep: '研发部', + status: '1', + create_time: '2020-11-14 12:01:20', + avatar: avatar10 + } +] + +export interface Role { + roleName: string + roleCode: string + des: string + date: string + enable: boolean +} + +// 角色列表 +export const ROLE_LIST_DATA: Role[] = [ + { + roleName: '超级管理员', + roleCode: 'R_SUPER', + des: '拥有系统全部权限', + date: '2025-05-15 12:30:45', + enable: true + }, + { + roleName: '管理员', + roleCode: 'R_ADMIN', + des: '拥有系统管理权限', + date: '2025-05-15 12:30:45', + enable: true + }, + { + roleName: '普通用户', + roleCode: 'R_USER', + des: '拥有系统普通权限', + date: '2025-05-15 12:30:45', + enable: true + }, + { + roleName: '财务管理员', + roleCode: 'R_FINANCE', + des: '管理财务相关权限', + date: '2025-05-16 09:15:30', + enable: true + }, + { + roleName: '数据分析师', + roleCode: 'R_ANALYST', + des: '拥有数据分析权限', + date: '2025-05-16 11:45:00', + enable: false + }, + { + roleName: '客服专员', + roleCode: 'R_SUPPORT', + des: '处理客户支持请求', + date: '2025-05-17 14:30:22', + enable: true + }, + { + roleName: '营销经理', + roleCode: 'R_MARKETING', + des: '管理营销活动权限', + date: '2025-05-17 15:10:50', + enable: true + }, + { + roleName: '访客用户', + roleCode: 'R_GUEST', + des: '仅限浏览权限', + date: '2025-05-18 08:25:40', + enable: false + }, + { + roleName: '系统维护员', + roleCode: 'R_MAINTAINER', + des: '负责系统维护和更新', + date: '2025-05-18 09:50:12', + enable: true + }, + { + roleName: '项目经理', + roleCode: 'R_PM', + des: '管理项目相关权限', + date: '2025-05-19 13:40:35', + enable: true + } +] diff --git a/src/mock/upgrade/changeLog.ts b/src/mock/upgrade/changeLog.ts new file mode 100644 index 0000000..dd6b772 --- /dev/null +++ b/src/mock/upgrade/changeLog.ts @@ -0,0 +1,12 @@ +import { ref } from 'vue' + +interface UpgradeLog { + version: string // 版本号 + title: string // 更新标题 + date: string // 更新日期 + detail?: string[] // 更新内容 + requireReLogin?: boolean // 是否需要重新登录 + remark?: string // 备注 +} + +export const upgradeLogList = ref([]) diff --git a/src/plugins/echarts.ts b/src/plugins/echarts.ts new file mode 100644 index 0000000..4f56d89 --- /dev/null +++ b/src/plugins/echarts.ts @@ -0,0 +1,76 @@ +/** + * ECharts 插件配置 + * + * 按需导入 ECharts 图表和组件,减小打包体积。 + * 只注册项目中实际使用的图表类型和组件。 + * + * @module plugins/echarts + * @author Art Design Pro Team + */ + +// ECharts 按需导入配置 +import * as echarts from 'echarts/core' + +// 导入图表类型 +import { + BarChart, + LineChart, + PieChart, + ScatterChart, + RadarChart, + MapChart, + CandlestickChart +} from 'echarts/charts' + +// 导入组件 +import { + TitleComponent, + TooltipComponent, + GridComponent, + LegendComponent, + DataZoomComponent, + MarkPointComponent, + MarkLineComponent, + ToolboxComponent, + BrushComponent, + GeoComponent, + VisualMapComponent +} from 'echarts/components' + +// 导入渲染器 +import { CanvasRenderer } from 'echarts/renderers' + +// 注册必要的组件 +echarts.use([ + // 图表类型 + BarChart, + LineChart, + PieChart, + ScatterChart, + RadarChart, + MapChart, + CandlestickChart, + + // 组件 + TitleComponent, + TooltipComponent, + GridComponent, + LegendComponent, + DataZoomComponent, + MarkPointComponent, + MarkLineComponent, + ToolboxComponent, + BrushComponent, + GeoComponent, + VisualMapComponent, + + // 渲染器 + CanvasRenderer +]) + +// 导出 echarts 实例和类型 +export { echarts } +export type { EChartsOption, BarSeriesOption } from 'echarts' + +// 导出常用的图形工具 +export const graphic = echarts.graphic diff --git a/src/plugins/index.ts b/src/plugins/index.ts new file mode 100644 index 0000000..4536a86 --- /dev/null +++ b/src/plugins/index.ts @@ -0,0 +1,6 @@ +/** + * 插件统一导出 + * 集中管理第三方库的封装和配置 + */ + +export * from './echarts' diff --git a/src/router/core/ComponentLoader.ts b/src/router/core/ComponentLoader.ts new file mode 100644 index 0000000..9467934 --- /dev/null +++ b/src/router/core/ComponentLoader.ts @@ -0,0 +1,85 @@ +/** + * 组件加载器 + * + * 负责动态加载 Vue 组件 + * + * @module router/core/ComponentLoader + * @author Art Design Pro Team + */ + +import { h } from 'vue' + +export class ComponentLoader { + private modules: Record Promise> + + constructor() { + // 动态导入 views 目录下所有 .vue 组件 + this.modules = import.meta.glob('../../views/**/*.vue') + } + + /** + * 加载组件 + */ + load(componentPath: string): () => Promise { + if (!componentPath) { + return this.createEmptyComponent() + } + + // 规范化路径,确保以 / 开头 + const normalizedPath = componentPath.startsWith('/') ? componentPath : `/${componentPath}` + + // 构建可能的路径 + const fullPath = `../../views${normalizedPath}.vue` + const fullPathWithIndex = `../../views${normalizedPath}/index.vue` + + // 先尝试直接路径,再尝试添加/index的路径 + const module = this.modules[fullPath] || this.modules[fullPathWithIndex] + + if (!module) { + console.error( + `[ComponentLoader] 未找到组件: ${componentPath},尝试过的路径: ${fullPath} 和 ${fullPathWithIndex}` + ) + return this.createErrorComponent(componentPath) + } + + return module + } + + /** + * 加载布局组件 + */ + loadLayout(): () => Promise { + return () => import('@/views/index/index.vue') + } + + /** + * 加载 iframe 组件 + */ + loadIframe(): () => Promise { + return () => import('@/views/outside/Iframe.vue') + } + + /** + * 创建空组件 + */ + private createEmptyComponent(): () => Promise { + return () => + Promise.resolve({ + render() { + return h('div', {}) + } + }) + } + + /** + * 创建错误提示组件 + */ + private createErrorComponent(componentPath: string): () => Promise { + return () => + Promise.resolve({ + render() { + return h('div', { class: 'route-error' }, `组件未找到: ${componentPath}`) + } + }) + } +} diff --git a/src/router/core/IframeRouteManager.ts b/src/router/core/IframeRouteManager.ts new file mode 100644 index 0000000..c054ca1 --- /dev/null +++ b/src/router/core/IframeRouteManager.ts @@ -0,0 +1,78 @@ +/** + * Iframe 路由管理器 + * + * 负责管理 iframe 类型的路由 + * + * @module router/core/IframeRouteManager + * @author Art Design Pro Team + */ + +import type { AppRouteRecord } from '@/types/router' + +export class IframeRouteManager { + private static instance: IframeRouteManager + private iframeRoutes: AppRouteRecord[] = [] + + private constructor() {} + + static getInstance(): IframeRouteManager { + if (!IframeRouteManager.instance) { + IframeRouteManager.instance = new IframeRouteManager() + } + return IframeRouteManager.instance + } + + /** + * 添加 iframe 路由 + */ + add(route: AppRouteRecord): void { + if (!this.iframeRoutes.find((r) => r.path === route.path)) { + this.iframeRoutes.push(route) + } + } + + /** + * 获取所有 iframe 路由 + */ + getAll(): AppRouteRecord[] { + return this.iframeRoutes + } + + /** + * 根据路径查找 iframe 路由 + */ + findByPath(path: string): AppRouteRecord | undefined { + return this.iframeRoutes.find((route) => route.path === path) + } + + /** + * 清空所有 iframe 路由 + */ + clear(): void { + this.iframeRoutes = [] + } + + /** + * 保存到 sessionStorage + */ + save(): void { + if (this.iframeRoutes.length > 0) { + sessionStorage.setItem('iframeRoutes', JSON.stringify(this.iframeRoutes)) + } + } + + /** + * 从 sessionStorage 加载 + */ + load(): void { + try { + const data = sessionStorage.getItem('iframeRoutes') + if (data) { + this.iframeRoutes = JSON.parse(data) + } + } catch (error) { + console.error('[IframeRouteManager] 加载 iframe 路由失败:', error) + this.iframeRoutes = [] + } + } +} diff --git a/src/router/core/MenuProcessor.ts b/src/router/core/MenuProcessor.ts new file mode 100644 index 0000000..c7b55cc --- /dev/null +++ b/src/router/core/MenuProcessor.ts @@ -0,0 +1,261 @@ +/** + * 菜单处理器 + * + * 负责菜单数据的获取、过滤和处理 + * + * @module router/core/MenuProcessor + * @author Art Design Pro Team + */ + +import type { AppRouteRecord } from '@/types/router' +import { useUserStore } from '@/store/modules/user' +import { useAppMode } from '@/hooks/core/useAppMode' +import { fetchGetMenu } from '@/api/auth' +import { asyncRoutes } from '../routes/asyncRoutes' +import { RoutesAlias } from '../routesAlias' +import { formatMenuTitle } from '@/utils' + +export class MenuProcessor { + /** + * 获取菜单数据 + */ + async getMenuList(): Promise { + const { isFrontendMode } = useAppMode() + + let menuList: AppRouteRecord[] + if (isFrontendMode.value) { + menuList = await this.processFrontendMenu() + } else { + menuList = await this.processBackendMenu() + } + + // 在规范化路径之前,验证原始路径配置 + this.validateMenuPaths(menuList) + + // 规范化路径(将相对路径转换为完整路径) + const normalizedMenuList = this.normalizeMenuPaths(menuList) + + return normalizedMenuList + } + + /** + * 处理前端控制模式的菜单 + */ + private async processFrontendMenu(): Promise { + const userStore = useUserStore() + const roles = userStore.info?.roles + + let menuList = [...asyncRoutes] + + // 根据角色过滤菜单 + if (roles && roles.length > 0) { + menuList = this.filterMenuByRoles(menuList, roles) + } + + return this.filterEmptyMenus(menuList) + } + + /** + * 处理后端控制模式的菜单 + */ + private async processBackendMenu(): Promise { + const list = await fetchGetMenu() + return this.filterEmptyMenus(list as unknown as AppRouteRecord[]) + } + + /** + * 通过 name/path 匹配路由索引 + */ + private findRouteIndex(list: AppRouteRecord[], target: AppRouteRecord): number { + // 1. 优先按 name 匹配 + if (target.name) { + const index = list.findIndex((item) => item.name === target.name) + if (index >= 0) return index + } + + // 2. 回退按 path 匹配 + if (target.path) { + return list.findIndex((item) => item.path === target.path) + } + + return -1 + } + + /** + * 根据角色过滤菜单 + */ + private filterMenuByRoles(menu: AppRouteRecord[], roles: string[]): AppRouteRecord[] { + return menu.reduce((acc: AppRouteRecord[], item) => { + const itemRoles = item.meta?.roles + const hasPermission = !itemRoles || itemRoles.some((role) => roles?.includes(role)) + + if (hasPermission) { + const filteredItem = { ...item } + if (filteredItem.children?.length) { + filteredItem.children = this.filterMenuByRoles(filteredItem.children, roles) + } + acc.push(filteredItem) + } + + return acc + }, []) + } + + /** + * 递归过滤空菜单项 + */ + private filterEmptyMenus(menuList: AppRouteRecord[]): AppRouteRecord[] { + return menuList + .map((item) => { + // 如果有子菜单,先递归过滤子菜单 + if (item.children && item.children.length > 0) { + const filteredChildren = this.filterEmptyMenus(item.children) + return { + ...item, + children: filteredChildren + } + } + return item + }) + .filter((item) => { + // 如果定义了 children 属性(即使是空数组),说明这是一个目录菜单,应该保留 + if ('children' in item) { + return true + } + + // 如果有外链或 iframe,保留 + if (item.meta?.isIframe === true || item.meta?.link) { + return true + } + + // 如果有有效的 component,保留 + if (item.component && item.component !== '' && item.component !== RoutesAlias.Layout) { + return true + } + + // 其他情况过滤掉 + return false + }) + } + + /** + * 验证菜单列表是否有效 + */ + validateMenuList(menuList: AppRouteRecord[]): boolean { + return Array.isArray(menuList) && menuList.length > 0 + } + + /** + * 规范化菜单路径 + * 将相对路径转换为完整路径,确保菜单跳转正确 + */ + private normalizeMenuPaths(menuList: AppRouteRecord[], parentPath = ''): AppRouteRecord[] { + return menuList.map((item) => { + // 构建完整路径 + const fullPath = this.buildFullPath(item.path || '', parentPath) + + // 递归处理子菜单 + const children = item.children?.length + ? this.normalizeMenuPaths(item.children, fullPath) + : item.children + + return { + ...item, + path: fullPath, + children + } + }) + } + + /** + * 验证菜单路径配置 + * 检测非一级菜单是否错误使用了 / 开头的路径 + */ + /** + * 验证菜单路径配置 + * 检测非一级菜单是否错误使用了 / 开头的路径 + */ + private validateMenuPaths(menuList: AppRouteRecord[], level = 1): void { + menuList.forEach((route) => { + if (!route.children?.length) return + + const parentName = String(route.name || route.path || '未知路由') + + route.children.forEach((child) => { + const childPath = child.path || '' + + // 跳过合法的绝对路径:外部链接和 iframe 路由 + if (this.isValidAbsolutePath(childPath)) return + + // 检测非法的绝对路径 + if (childPath.startsWith('/')) { + this.logPathError(child, childPath, parentName, level) + } + }) + + // 递归检查更深层级的子路由 + this.validateMenuPaths(route.children, level + 1) + }) + } + + /** + * 判断是否为合法的绝对路径 + */ + private isValidAbsolutePath(path: string): boolean { + return ( + path.startsWith('http://') || + path.startsWith('https://') || + path.startsWith('/outside/iframe/') + ) + } + + /** + * 输出路径配置错误日志 + */ + private logPathError( + route: AppRouteRecord, + path: string, + parentName: string, + level: number + ): void { + const routeName = String(route.name || path || '未知路由') + const menuTitle = route.meta?.title || routeName + const suggestedPath = path.split('/').pop() || path.slice(1) + + console.error( + `[路由配置错误] 菜单 "${formatMenuTitle(menuTitle)}" (name: ${routeName}, path: ${path}) 配置错误\n` + + ` 位置: ${parentName} > ${routeName}\n` + + ` 问题: ${level + 1}级菜单的 path 不能以 / 开头\n` + + ` 当前配置: path: '${path}'\n` + + ` 应该改为: path: '${suggestedPath}'` + ) + } + + /** + * 构建完整路径 + */ + private buildFullPath(path: string, parentPath: string): string { + if (!path) return '' + + // 外部链接直接返回 + if (path.startsWith('http://') || path.startsWith('https://')) { + return path + } + + // 如果已经是绝对路径,直接返回 + if (path.startsWith('/')) { + return path + } + + // 拼接父路径和当前路径 + if (parentPath) { + // 移除父路径末尾的斜杠,移除子路径开头的斜杠,然后拼接 + const cleanParent = parentPath.replace(/\/$/, '') + const cleanChild = path.replace(/^\//, '') + return `${cleanParent}/${cleanChild}` + } + + // 没有父路径,添加前导斜杠 + return `/${path}` + } +} diff --git a/src/router/core/RoutePermissionValidator.ts b/src/router/core/RoutePermissionValidator.ts new file mode 100644 index 0000000..72b9912 --- /dev/null +++ b/src/router/core/RoutePermissionValidator.ts @@ -0,0 +1,144 @@ +/** + * 路由权限验证模块 + * + * 提供路由权限验证和路径检查功能 + * + * ## 主要功能 + * + * - 验证路径是否在用户菜单权限中 + * - 构建菜单路径集合(扁平化处理) + * - 支持动态路由参数匹配 + * - 路径前缀匹配 + * + * ## 使用场景 + * + * - 路由守卫中验证用户权限 + * - 动态路由注册后的权限检查 + * - 防止用户访问无权限的页面 + * + * @module router/core/RoutePermissionValidator + * @author Art Design Pro Team + */ + +import type { AppRouteRecord } from '@/types/router' + +/** + * 路由权限验证器 + */ +export class RoutePermissionValidator { + /** + * 验证路径是否在用户菜单权限中 + * @param targetPath 目标路径 + * @param menuList 菜单列表 + * @returns 是否有权限访问 + */ + static hasPermission(targetPath: string, menuList: AppRouteRecord[]): boolean { + // 根路径始终允许访问 + if (targetPath === '/') { + return true + } + + // 构建路径集合 + const pathSet = this.buildMenuPathSet(menuList) + + // 检查路径是否在集合中(精确匹配或前缀匹配) + return pathSet.has(targetPath) || this.checkPathPrefix(targetPath, pathSet) + } + + /** + * 构建菜单路径集合(扁平化处理) + * @param menuList 菜单列表 + * @param pathSet 路径集合 + * @returns 路径集合 + */ + static buildMenuPathSet( + menuList: AppRouteRecord[], + pathSet: Set = new Set() + ): Set { + if (!Array.isArray(menuList) || menuList.length === 0) { + return pathSet + } + + for (const menuItem of menuList) { + // 跳过隐藏的菜单项 + if (menuItem.meta?.isHide || !menuItem.path) { + continue + } + + // 标准化路径并添加到集合 + const menuPath = menuItem.path.startsWith('/') ? menuItem.path : `/${menuItem.path}` + pathSet.add(menuPath) + + // 递归处理子菜单 + if (menuItem.children?.length) { + this.buildMenuPathSet(menuItem.children, pathSet) + } + } + + return pathSet + } + + /** + * 检查目标路径是否匹配集合中的某个路径前缀 + * 用于支持动态路由参数匹配,如 /user/123 匹配 /user + * @param targetPath 目标路径 + * @param pathSet 路径集合 + * @returns 是否匹配 + */ + static checkPathPrefix(targetPath: string, pathSet: Set): boolean { + // 遍历路径集合,检查是否有前缀匹配 + for (const menuPath of pathSet) { + // 1. 动态路由参数匹配(:id → [^/]+) + if (this.isDynamicPath(menuPath)) { + const regex = new RegExp(`^${this.normalizeDynamicPath(menuPath)}(/|$)`) + if (regex.test(targetPath)) { + return true + } + continue + } + + // 2. 普通前缀匹配 + if (targetPath.startsWith(`${menuPath}/`)) { + return true + } + } + return false + } + + /** + * 判断是否包含动态路由参数 + */ + private static isDynamicPath(path: string): boolean { + return path.includes(':') + } + + /** + * 将动态路由转换为正则表达式片段 + * 例如:/tenant/:tenantId/announcements → /tenant/[^/]+/announcements + */ + private static normalizeDynamicPath(path: string): string { + return path.replace(/:[^/]+/g, '[^/]+') + } + + /** + * 验证并返回有效的路径 + * 如果目标路径无权限,返回首页路径 + * @param targetPath 目标路径 + * @param menuList 菜单列表 + * @param homePath 首页路径 + * @returns 验证后的路径 + */ + static validatePath( + targetPath: string, + menuList: AppRouteRecord[], + homePath: string = '/' + ): { path: string; hasPermission: boolean } { + const hasPermission = this.hasPermission(targetPath, menuList) + + if (hasPermission) { + return { path: targetPath, hasPermission: true } + } + + return { path: homePath, hasPermission: false } + } +} diff --git a/src/router/core/RouteRegistry.ts b/src/router/core/RouteRegistry.ts new file mode 100644 index 0000000..e1acb9e --- /dev/null +++ b/src/router/core/RouteRegistry.ts @@ -0,0 +1,90 @@ +/** + * 路由注册核心类 + * + * 负责动态路由的注册、验证和管理 + * + * @module router/core/RouteRegistry + * @author Art Design Pro Team + */ + +import type { Router, RouteRecordRaw } from 'vue-router' +import type { AppRouteRecord } from '@/types/router' +import { ComponentLoader } from './ComponentLoader' +import { RouteValidator } from './RouteValidator' +import { RouteTransformer } from './RouteTransformer' + +export class RouteRegistry { + private router: Router + private componentLoader: ComponentLoader + private validator: RouteValidator + private transformer: RouteTransformer + private removeRouteFns: (() => void)[] = [] + private registered = false + + constructor(router: Router) { + this.router = router + this.componentLoader = new ComponentLoader() + this.validator = new RouteValidator() + this.transformer = new RouteTransformer(this.componentLoader) + } + + /** + * 注册动态路由 + */ + register(menuList: AppRouteRecord[]): void { + if (this.registered) { + console.warn('[RouteRegistry] 路由已注册,跳过重复注册') + return + } + + // 验证路由配置 + const validationResult = this.validator.validate(menuList) + if (!validationResult.valid) { + throw new Error(`路由配置验证失败: ${validationResult.errors.join(', ')}`) + } + + // 转换并注册路由 + const removeRouteFns: (() => void)[] = [] + + menuList.forEach((route) => { + if (route.name && !this.router.hasRoute(route.name)) { + const routeConfig = this.transformer.transform(route) + const removeRouteFn = this.router.addRoute(routeConfig as RouteRecordRaw) + removeRouteFns.push(removeRouteFn) + } + }) + + this.removeRouteFns = removeRouteFns + this.registered = true + } + + /** + * 移除所有动态路由 + */ + unregister(): void { + this.removeRouteFns.forEach((fn) => fn()) + this.removeRouteFns = [] + this.registered = false + } + + /** + * 检查是否已注册 + */ + isRegistered(): boolean { + return this.registered + } + + /** + * 获取移除函数列表(用于 store 管理) + */ + getRemoveRouteFns(): (() => void)[] { + return this.removeRouteFns + } + + /** + * 标记为已注册(用于错误处理场景,避免重复请求) + */ + markAsRegistered(): void { + this.registered = true + } +} diff --git a/src/router/core/RouteTransformer.ts b/src/router/core/RouteTransformer.ts new file mode 100644 index 0000000..f00f27c --- /dev/null +++ b/src/router/core/RouteTransformer.ts @@ -0,0 +1,148 @@ +/** + * 路由转换器 + * + * 负责将菜单数据转换为 Vue Router 路由配置 + * + * @module router/core/RouteTransformer + * @author Art Design Pro Team + */ + +import type { RouteRecordRaw } from 'vue-router' +import type { AppRouteRecord } from '@/types/router' +import { ComponentLoader } from './ComponentLoader' +import { IframeRouteManager } from './IframeRouteManager' + +interface ConvertedRoute extends Omit { + id?: number + children?: ConvertedRoute[] + component?: RouteRecordRaw['component'] | (() => Promise) +} + +export class RouteTransformer { + private componentLoader: ComponentLoader + private iframeManager: IframeRouteManager + + constructor(componentLoader: ComponentLoader) { + this.componentLoader = componentLoader + this.iframeManager = IframeRouteManager.getInstance() + } + + /** + * 转换路由配置 + */ + transform(route: AppRouteRecord, depth = 0): ConvertedRoute { + const { component, children, ...routeConfig } = route + + // 基础路由配置 + const converted: ConvertedRoute = { + ...routeConfig, + component: undefined + } + + // 处理不同类型的路由 + if (route.meta.isIframe) { + this.handleIframeRoute(converted, route, depth) + } else if (this.isFirstLevelRoute(route, depth)) { + this.handleFirstLevelRoute(converted, route, component) + } else { + this.handleNormalRoute(converted, component) + } + + // 递归处理子路由 + if (children?.length) { + converted.children = children.map((child) => this.transform(child, depth + 1)) + } + + return converted + } + + /** + * 判断是否为一级路由(需要 Layout 包裹) + */ + private isFirstLevelRoute(route: AppRouteRecord, depth: number): boolean { + return depth === 0 && (!route.children || route.children.length === 0) + } + + /** + * 处理 iframe 类型路由 + */ + private handleIframeRoute( + targetRoute: ConvertedRoute, + sourceRoute: AppRouteRecord, + depth: number + ): void { + if (depth === 0) { + // 顶级 iframe:用 Layout 包裹 + targetRoute.component = this.componentLoader.loadLayout() + targetRoute.path = this.extractFirstSegment(sourceRoute.path || '') + targetRoute.name = '' + + targetRoute.children = [ + { + ...sourceRoute, + component: this.componentLoader.loadIframe() + } as ConvertedRoute + ] + } else { + // 非顶级(嵌套)iframe:直接使用 Iframe.vue + targetRoute.component = this.componentLoader.loadIframe() + } + + // 记录 iframe 路由 + this.iframeManager.add(sourceRoute) + } + + /** + * 处理一级菜单路由 + */ + private handleFirstLevelRoute( + converted: ConvertedRoute, + route: AppRouteRecord, + component: AppRouteRecord['component'] | undefined + ): void { + converted.component = this.componentLoader.loadLayout() + converted.path = this.extractFirstSegment(route.path || '') + converted.name = '' + route.meta.isFirstLevel = true + + converted.children = [ + { + ...route, + component: this.resolveComponent(component) + } as ConvertedRoute + ] + } + + /** + * 处理普通路由 + */ + private handleNormalRoute( + converted: ConvertedRoute, + component: AppRouteRecord['component'] | undefined + ): void { + converted.component = this.resolveComponent(component) + } + + /** + * 解析组件配置(支持字符串路径与动态导入函数) + */ + private resolveComponent( + component: AppRouteRecord['component'] | undefined + ): RouteRecordRaw['component'] | undefined { + if (!component) return undefined + + if (typeof component === 'function') { + return component + } + + return this.componentLoader.load(component) + } + + /** + * 提取路径的第一段 + */ + private extractFirstSegment(path: string): string { + const segments = path.split('/').filter(Boolean) + return segments.length > 0 ? `/${segments[0]}` : '/' + } +} diff --git a/src/router/core/RouteValidator.ts b/src/router/core/RouteValidator.ts new file mode 100644 index 0000000..f8e58fc --- /dev/null +++ b/src/router/core/RouteValidator.ts @@ -0,0 +1,187 @@ +/** + * 路由验证器 + * + * 负责验证路由配置的合法性 + * + * @module router/core/RouteValidator + * @author Art Design Pro Team + */ + +import type { AppRouteRecord } from '@/types/router' +import { RoutesAlias } from '../routesAlias' + +export interface ValidationResult { + valid: boolean + errors: string[] + warnings: string[] +} + +export class RouteValidator { + // 用于记录已经提示过的路由,避免重复提示 + private warnedRoutes = new Set() + + /** + * 验证路由配置 + */ + validate(routes: AppRouteRecord[]): ValidationResult { + const errors: string[] = [] + const warnings: string[] = [] + + // 检测重复路由 + this.checkDuplicates(routes, errors, warnings) + + // 检测组件配置 + this.checkComponents(routes, errors, warnings) + + // 检测嵌套菜单的 /index/index 配置 + this.checkNestedIndexComponent(routes) + + return { + valid: errors.length === 0, + errors, + warnings + } + } + + /** + * 检测重复路由 + */ + private checkDuplicates( + routes: AppRouteRecord[], + errors: string[], + warnings: string[], + parentPath = '' + ): void { + const routeNameMap = new Map() + const componentPathMap = new Map() + + const checkRoutes = (routes: AppRouteRecord[], parentPath = '') => { + routes.forEach((route) => { + const currentPath = route.path || '' + const fullPath = this.resolvePath(parentPath, currentPath) + + // 名称重复检测 + if (route.name) { + const routeName = String(route.name) + if (routeNameMap.has(routeName)) { + warnings.push(`路由名称重复: "${routeName}" (${fullPath})`) + } else { + routeNameMap.set(routeName, fullPath) + } + } + + // 组件路径重复检测 + if (route.component && typeof route.component === 'string') { + const componentPath = route.component + if (componentPath !== RoutesAlias.Layout) { + const componentKey = `${parentPath}:${componentPath}` + if (componentPathMap.has(componentKey)) { + warnings.push(`组件路径重复: "${componentPath}" (${fullPath})`) + } else { + componentPathMap.set(componentKey, fullPath) + } + } + } + + // 递归处理子路由 + if (route.children?.length) { + checkRoutes(route.children, fullPath) + } + }) + } + + checkRoutes(routes, parentPath) + } + + /** + * 检测组件配置 + */ + private checkComponents( + routes: AppRouteRecord[], + errors: string[], + warnings: string[], + parentPath = '' + ): void { + routes.forEach((route) => { + const hasExternalLink = !!route.meta?.link?.trim() + const hasChildren = Array.isArray(route.children) && route.children.length > 0 + const routePath = route.path || '[未定义路径]' + const isIframe = route.meta?.isIframe + + // 如果配置了 component,则无需校验 + if (route.component) { + // 递归检查子路由 + if (route.children?.length) { + const fullPath = this.resolvePath(parentPath, route.path || '') + this.checkComponents(route.children, errors, warnings, fullPath) + } + return + } + + // 一级菜单:必须指定 Layout,除非是外链或 iframe + if (parentPath === '' && !hasExternalLink && !isIframe) { + errors.push(`一级菜单(${routePath}) 缺少 component,必须指向 ${RoutesAlias.Layout}`) + return + } + + // 非一级菜单:如果既不是外链、iframe,也没有子路由,则必须配置 component + if (!hasExternalLink && !isIframe && !hasChildren) { + errors.push(`路由(${routePath}) 缺少 component 配置`) + } + + // 递归检查子路由 + if (route.children?.length) { + const fullPath = this.resolvePath(parentPath, route.path || '') + this.checkComponents(route.children, errors, warnings, fullPath) + } + }) + } + + /** + * 检测嵌套菜单的 Layout 组件配置 + * 只有一级菜单才能使用 Layout,二级及以下菜单不能使用 + */ + private checkNestedIndexComponent(routes: AppRouteRecord[], level = 1): void { + routes.forEach((route) => { + // 检查二级及以下菜单是否错误使用了 Layout + if (level > 1 && route.component === RoutesAlias.Layout) { + this.logLayoutError(route, level) + } + + // 递归检查子路由 + if (route.children?.length) { + this.checkNestedIndexComponent(route.children, level + 1) + } + }) + } + + /** + * 输出 Layout 组件配置错误日志 + */ + private logLayoutError(route: AppRouteRecord, level: number): void { + const routeName = String(route.name || route.path || '未知路由') + const routeKey = `${routeName}_${route.path}` + + // 避免重复提示 + if (this.warnedRoutes.has(routeKey)) return + this.warnedRoutes.add(routeKey) + + const menuTitle = route.meta?.title || routeName + const routePath = route.path || '/' + + console.error( + `[路由配置错误] 菜单 "${menuTitle}" (name: ${routeName}, path: ${routePath}) 配置错误\n` + + ` 问题: ${level}级菜单不能使用 ${RoutesAlias.Layout} 作为 component\n` + + ` 说明: 只有一级菜单才能使用 ${RoutesAlias.Layout},二级及以下菜单应该指向具体的组件路径\n` + + ` 当前配置: component: '${RoutesAlias.Layout}'\n` + + ` 应该改为: component: '/your/component/path' 或留空 ''(如果是目录菜单)` + ) + } + + /** + * 路径解析 + */ + private resolvePath(parent: string, child: string): string { + return [parent.replace(/\/$/, ''), child.replace(/^\//, '')].filter(Boolean).join('/') + } +} diff --git a/src/router/core/index.ts b/src/router/core/index.ts new file mode 100644 index 0000000..fcfecfc --- /dev/null +++ b/src/router/core/index.ts @@ -0,0 +1,14 @@ +/** + * 路由核心模块导出 + * + * @module router/core + * @author Art Design Pro Team + */ + +export { RouteRegistry } from './RouteRegistry' +export { ComponentLoader } from './ComponentLoader' +export { RouteValidator } from './RouteValidator' +export { RouteTransformer } from './RouteTransformer' +export { IframeRouteManager } from './IframeRouteManager' +export { MenuProcessor } from './MenuProcessor' +export { RoutePermissionValidator } from './RoutePermissionValidator' diff --git a/src/router/guards/afterEach.ts b/src/router/guards/afterEach.ts new file mode 100644 index 0000000..d60572d --- /dev/null +++ b/src/router/guards/afterEach.ts @@ -0,0 +1,34 @@ +import { nextTick } from 'vue' +import { useSettingStore } from '@/store/modules/setting' +import { Router } from 'vue-router' +import NProgress from 'nprogress' +import { useCommon } from '@/hooks/core/useCommon' +import { loadingService } from '@/utils/ui' +import { getPendingLoading, resetPendingLoading } from './beforeEach' + +/** 路由全局后置守卫 */ +export function setupAfterEachGuard(router: Router) { + const { scrollToTop } = useCommon() + + router.afterEach(() => { + scrollToTop() + + // 关闭进度条 + const settingStore = useSettingStore() + if (settingStore.showNprogress) { + NProgress.done() + // 确保进度条完全移除,避免残影 + setTimeout(() => { + NProgress.remove() + }, 600) + } + + // 关闭 loading 效果 + if (getPendingLoading()) { + nextTick(() => { + loadingService.hideLoading() + resetPendingLoading() + }) + } + }) +} diff --git a/src/router/guards/beforeEach.ts b/src/router/guards/beforeEach.ts new file mode 100644 index 0000000..07cbc17 --- /dev/null +++ b/src/router/guards/beforeEach.ts @@ -0,0 +1,430 @@ +/** + * 路由全局前置守卫模块 + * + * 提供完整的路由导航守卫功能 + * + * ## 主要功能 + * + * - 登录状态验证和重定向 + * - 动态路由注册和权限控制 + * - 菜单数据获取和处理(前端/后端模式) + * - 用户信息获取和缓存 + * - 页面标题设置 + * - 工作标签页管理 + * - 进度条和加载动画控制 + * - 静态路由识别和处理 + * - 错误处理和异常跳转 + * + * ## 使用场景 + * + * - 路由跳转前的权限验证 + * - 动态菜单加载和路由注册 + * - 用户登录状态管理 + * - 页面访问控制 + * - 路由级别的加载状态管理 + * + * ## 工作流程 + * + * 1. 检查登录状态,未登录跳转到登录页 + * 2. 首次访问时获取用户信息和菜单数据 + * 3. 根据权限动态注册路由 + * 4. 设置页面标题和工作标签页 + * 5. 处理根路径重定向到首页 + * 6. 未匹配路由跳转到 404 页面 + * + * @module router/guards/beforeEach + * @author Art Design Pro Team + */ +import type { Router, RouteLocationNormalized, NavigationGuardNext } from 'vue-router' +import { nextTick } from 'vue' +import NProgress from 'nprogress' +import { useSettingStore } from '@/store/modules/setting' +import { useUserStore } from '@/store/modules/user' +import { useMenuStore } from '@/store/modules/menu' +import { setWorktab } from '@/utils/navigation' +import { setPageTitle } from '@/utils/router' +import { RoutesAlias } from '../routesAlias' +import { staticRoutes } from '../routes/staticRoutes' +import { loadingService } from '@/utils/ui' +import { useCommon } from '@/hooks/core/useCommon' +import { useWorktabStore } from '@/store/modules/worktab' +import { fetchGetUserInfo } from '@/api/auth' +import { ApiStatus } from '@/utils/http/status' +import { isHttpError } from '@/utils/http/error' +import { RouteRegistry, MenuProcessor, IframeRouteManager, RoutePermissionValidator } from '../core' + +// 路由注册器实例 +let routeRegistry: RouteRegistry | null = null + +// 菜单处理器实例 +const menuProcessor = new MenuProcessor() + +// 跟踪是否需要关闭 loading +let pendingLoading = false + +// 路由初始化失败标记,防止死循环 +// 一旦设置为 true,只有刷新页面或重新登录才能重置 +let routeInitFailed = false + +// 路由初始化进行中标记,防止并发请求 +let routeInitInProgress = false + +/** + * 获取 pendingLoading 状态 + */ +export function getPendingLoading(): boolean { + return pendingLoading +} + +/** + * 重置 pendingLoading 状态 + */ +export function resetPendingLoading(): void { + pendingLoading = false +} + +/** + * 获取路由初始化失败状态 + */ +export function getRouteInitFailed(): boolean { + return routeInitFailed +} + +/** + * 重置路由初始化状态(用于重新登录场景) + */ +export function resetRouteInitState(): void { + routeInitFailed = false + routeInitInProgress = false +} + +/** + * 设置路由全局前置守卫 + */ +export function setupBeforeEachGuard(router: Router): void { + // 初始化路由注册器 + routeRegistry = new RouteRegistry(router) + + router.beforeEach( + async ( + to: RouteLocationNormalized, + from: RouteLocationNormalized, + next: NavigationGuardNext + ) => { + try { + await handleRouteGuard(to, from, next, router) + } catch (error) { + console.error('[RouteGuard] 路由守卫处理失败:', error) + closeLoading() + next({ name: 'Exception500' }) + } + } + ) +} + +/** + * 关闭 loading 效果 + */ +function closeLoading(): void { + if (pendingLoading) { + nextTick(() => { + loadingService.hideLoading() + pendingLoading = false + }) + } +} + +/** + * 处理路由守卫逻辑 + */ +async function handleRouteGuard( + to: RouteLocationNormalized, + from: RouteLocationNormalized, + next: NavigationGuardNext, + router: Router +): Promise { + const settingStore = useSettingStore() + const userStore = useUserStore() + + // 启动进度条 + if (settingStore.showNprogress) { + NProgress.start() + } + + // 1. 检查登录状态 + if (!handleLoginStatus(to, userStore, next)) { + return + } + + // 2. 检查路由初始化是否已失败(防止死循环) + if (routeInitFailed) { + // 已经失败过,直接放行到错误页面,不再重试 + if (to.matched.length > 0) { + next() + } else { + // 未匹配到路由,跳转到 500 页面 + next({ name: 'Exception500', replace: true }) + } + return + } + + // 3. 处理动态路由注册 + if (!routeRegistry?.isRegistered() && userStore.isLogin && to.path !== RoutesAlias.Login) { + // 静态页面(如入驻进度)不需要动态菜单,直接放行 + if (isStaticRoute(to.path)) { + setPageTitle(to) + next() + return + } + // 防止并发请求(快速连续导航场景) + if (routeInitInProgress) { + // 正在初始化中,等待完成后重新导航 + next(false) + return + } + await handleDynamicRoutes(to, next, router) + return + } + + // 4. 处理根路径重定向 + if (handleRootPathRedirect(to, next)) { + return + } + + // 5. 处理已匹配的路由 + if (to.matched.length > 0) { + setWorktab(to) + setPageTitle(to) + next() + return + } + + // 6. 未匹配到路由,跳转到 404 + next({ name: 'Exception404' }) +} + +/** + * 处理登录状态 + * @returns true 表示可以继续,false 表示已处理跳转 + */ +function handleLoginStatus( + to: RouteLocationNormalized, + userStore: ReturnType, + next: NavigationGuardNext +): boolean { + // 已登录或访问登录页或允许未登录访问的静态路由,直接放行 + if (userStore.isLogin || to.path === RoutesAlias.Login || isPublicStaticRoute(to.path)) { + return true + } + + // 未登录且访问需要权限的页面,跳转到登录页并携带 redirect 参数 + userStore.logOut() + next({ + name: 'Login', + query: { redirect: to.fullPath } + }) + return false +} + +/** + * 检查路由是否为无需登录的静态路由 + */ +function isPublicStaticRoute(path: string): boolean { + // 0. 入驻相关页面虽然是静态路由,但要求登录访问 + const loginRequiredStaticPaths = [ + '/onboarding/status', + '/onboarding/pricing', + '/onboarding/waiting', + '/onboarding/error' + ] + return isStaticRoute(path) && !loginRequiredStaticPaths.includes(path) +} + +/** + * 检查路由是否为静态路由 + */ +function isStaticRoute(path: string): boolean { + const checkRoute = (routes: any[], targetPath: string): boolean => { + return routes.some((route) => { + // 处理动态路由参数匹配 + const routePath = route.path + const pattern = routePath.replace(/:[^/]+/g, '[^/]+').replace(/\*/g, '.*') + const regex = new RegExp(`^${pattern}$`) + + if (regex.test(targetPath)) { + return true + } + if (route.children && route.children.length > 0) { + return checkRoute(route.children, targetPath) + } + return false + }) + } + + return checkRoute(staticRoutes, path) +} + +/** + * 处理动态路由注册 + */ +async function handleDynamicRoutes( + to: RouteLocationNormalized, + next: NavigationGuardNext, + router: Router +): Promise { + // 标记初始化进行中 + routeInitInProgress = true + + // 显示 loading + pendingLoading = true + loadingService.showLoading() + + try { + // 1. 获取用户信息 + await fetchUserInfo() + + // 2. 获取菜单数据 + const menuList = await menuProcessor.getMenuList() + + // 3. 验证菜单数据 + if (!menuProcessor.validateMenuList(menuList)) { + throw new Error('获取菜单列表失败,请重新登录') + } + + // 4. 注册动态路由 + routeRegistry?.register(menuList) + + // 5. 保存菜单数据到 store + const menuStore = useMenuStore() + menuStore.setMenuList(menuList) + menuStore.addRemoveRouteFns(routeRegistry?.getRemoveRouteFns() || []) + + // 6. 保存 iframe 路由 + IframeRouteManager.getInstance().save() + + // 7. 验证工作标签页 + useWorktabStore().validateWorktabs(router) + + // 8. 验证目标路径权限 + const { homePath } = useCommon() + const { path: validatedPath, hasPermission } = RoutePermissionValidator.validatePath( + to.path, + menuList, + homePath.value || '/' + ) + + // 初始化成功,重置进行中标记 + routeInitInProgress = false + + // 9. 重新导航到目标路由 + if (!hasPermission) { + // 无权限访问,跳转到首页 + closeLoading() + + // 输出警告信息 + console.warn(`[RouteGuard] 用户无权限访问路径: ${to.path},已跳转到首页`) + + // 直接跳转到首页 + next({ + path: validatedPath, + replace: true + }) + } else { + // 有权限,正常导航 + next({ + path: to.path, + query: to.query, + hash: to.hash, + replace: true + }) + } + } catch (error) { + console.error('[RouteGuard] 动态路由注册失败:', error) + + // 关闭 loading + closeLoading() + + // 401 错误:axios 拦截器已处理退出登录,取消当前导航 + if (isUnauthorizedError(error)) { + // 重置状态,允许重新登录后再次初始化 + routeInitInProgress = false + next(false) + return + } + + // 处理菜单获取失败的情况(如接口异常或返回空数据),直接跳转登录页 + if (error instanceof Error && error.message === '获取菜单列表失败,请重新登录') { + const userStore = useUserStore() + userStore.logOut() + next({ name: 'Login', query: { redirect: to.fullPath } }) + return + } + + // 标记初始化失败,防止死循环 + routeInitFailed = true + routeInitInProgress = false + + // 输出详细错误信息,便于排查 + if (isHttpError(error)) { + console.error(`[RouteGuard] 错误码: ${error.code}, 消息: ${error.message}`) + } + + // 跳转到 500 页面,使用 replace 避免产生历史记录 + next({ name: 'Exception500', replace: true }) + } +} + +/** + * 获取用户信息 + */ +async function fetchUserInfo(): Promise { + const userStore = useUserStore() + const data = await fetchGetUserInfo() + userStore.setUserInfo(data) + // 获取用户权限 + await userStore.getUserPermissions() + // 检查并清理工作台标签页(如果是不同用户登录) + userStore.checkAndClearWorktabs() +} + +/** + * 重置路由相关状态 + */ +export function resetRouterState(delay: number): void { + setTimeout(() => { + routeRegistry?.unregister() + IframeRouteManager.getInstance().clear() + + const menuStore = useMenuStore() + menuStore.removeAllDynamicRoutes() + menuStore.setMenuList([]) + + // 重置路由初始化状态,允许重新登录后再次初始化 + resetRouteInitState() + }, delay) +} + +/** + * 处理根路径重定向到首页 + * @returns true 表示已处理跳转,false 表示无需跳转 + */ +function handleRootPathRedirect(to: RouteLocationNormalized, next: NavigationGuardNext): boolean { + if (to.path !== '/') { + return false + } + + const { homePath } = useCommon() + if (homePath.value && homePath.value !== '/') { + next({ path: homePath.value, replace: true }) + return true + } + + return false +} + +/** + * 判断是否为未授权错误(401) + */ +function isUnauthorizedError(error: unknown): boolean { + return isHttpError(error) && error.code === ApiStatus.unauthorized +} diff --git a/src/router/index.ts b/src/router/index.ts new file mode 100644 index 0000000..286ae58 --- /dev/null +++ b/src/router/index.ts @@ -0,0 +1,23 @@ +import type { App } from 'vue' +import { createRouter, createWebHashHistory } from 'vue-router' +import { staticRoutes } from './routes/staticRoutes' +import { configureNProgress } from '@/utils/router' +import { setupBeforeEachGuard } from './guards/beforeEach' +import { setupAfterEachGuard } from './guards/afterEach' + +// 创建路由实例 +export const router = createRouter({ + history: createWebHashHistory(), + routes: staticRoutes // 静态路由 +}) + +// 初始化路由 +export function initRouter(app: App): void { + configureNProgress() // 顶部进度条 + setupBeforeEachGuard(router) // 路由前置守卫 + setupAfterEachGuard(router) // 路由后置守卫 + app.use(router) +} + +// 主页路径,默认使用菜单第一个有效路径,配置后使用此路径 +export const HOME_PAGE_PATH = '' diff --git a/src/router/modules/announcement.ts b/src/router/modules/announcement.ts new file mode 100644 index 0000000..ccd1abb --- /dev/null +++ b/src/router/modules/announcement.ts @@ -0,0 +1,144 @@ +import type { AppRouteRecord } from '@/types/router' + +/** + * 公告管理路由模块 + * + * 覆盖平台、租户、应用端和草稿中心的公告管理路由 + */ +const announcementRoutes: AppRouteRecord[] = [ + /** + * 平台公告管理 + * - 列表 /platform/announcements + * - 创建 /platform/announcements/create + * - 编辑 /platform/announcements/:announcementId/edit + * - 详情 /platform/announcements/:announcementId + */ + { + path: '/platform/announcements', + name: 'PlatformAnnouncementRoot', + component: '/index/index', + meta: { + title: 'menus.announcement.platform.title', + icon: 'ri:megaphone-line', + permission: ['platform-announcement:read', 'platform-announcement:create'] + }, + children: [ + { + path: '', + name: 'PlatformAnnouncementList', + component: () => import('@views/platform/announcements/index.vue'), + meta: { + title: 'menus.announcement.platform.list', + icon: 'ri:list-check-2', + permission: ['platform-announcement:read', 'platform-announcement:create'], + authList: [ + { title: '创建平台公告', authMark: 'platform-announcement:create' }, + { title: '查看平台公告', authMark: 'platform-announcement:read' }, + { title: '编辑平台公告', authMark: 'platform-announcement:update' }, + { title: '删除平台公告', authMark: 'platform-announcement:delete' }, + { title: '发布平台公告', authMark: 'platform-announcement:publish' }, + { title: '撤销平台公告', authMark: 'platform-announcement:revoke' } + ] + } + }, + { + path: 'create', + name: 'PlatformAnnouncementCreate', + component: () => import('@views/platform/announcements/create.vue'), + meta: { + title: 'menus.announcement.platform.create', + icon: 'ri:add-circle-line', + permission: ['platform-announcement:create'] + } + }, + { + path: ':announcementId/edit', + name: 'PlatformAnnouncementEdit', + component: () => import('@views/platform/announcements/edit.vue'), + meta: { + title: 'menus.announcement.platform.edit', + icon: 'ri:edit-line', + permission: ['platform-announcement:update'] + } + }, + { + path: ':announcementId', + name: 'PlatformAnnouncementDetail', + component: () => import('@views/platform/announcements/detail.vue'), + meta: { + title: 'menus.announcement.platform.detail', + icon: 'ri:file-text-line', + permission: ['platform-announcement:read', 'platform-announcement:create'] + } + } + ] + }, + + /** + * 应用端公告(仅读) + * - 列表 /app/announcements + * - 详情 /app/announcements/:announcementId + */ + { + path: '/app/announcements', + name: 'AppAnnouncementRoot', + component: '/index/index', + meta: { + title: 'menus.announcement.app.title', + icon: 'ri:notification-badge-line', + permission: ['app-announcement:read'] + }, + children: [ + { + path: '', + name: 'AppAnnouncementList', + component: () => import('@views/app/announcements/index.vue'), + meta: { + title: 'menus.announcement.app.list', + icon: 'ri:list-check-2', + permission: ['app-announcement:read'], + authList: [{ title: '查看应用端公告', authMark: 'app-announcement:read' }] + } + }, + { + path: ':announcementId', + name: 'AppAnnouncementDetail', + component: () => import('@views/app/announcements/detail.vue'), + meta: { + title: 'menus.announcement.app.detail', + icon: 'ri:file-text-line', + permission: ['app-announcement:read'] + } + } + ] + }, + + /** + * 草稿中心 + * - 列表 /announcement-drafts + */ + { + path: '/announcement-drafts', + name: 'AnnouncementDraftRoot', + component: '/index/index', + meta: { + title: 'menus.announcement.drafts.title', + icon: 'ri:draft-line', + permission: ['platform-announcement:create', 'tenant-announcement:create'] + }, + children: [ + { + path: '', + name: 'AnnouncementDraftList', + component: () => import('@views/announcement-drafts/index.vue'), + meta: { + title: 'menus.announcement.drafts.list', + icon: 'ri:draft-line', + permission: ['platform-announcement:create', 'tenant-announcement:create'] + } + } + ] + } +] + +export default announcementRoutes diff --git a/src/router/modules/dashboard.ts b/src/router/modules/dashboard.ts new file mode 100644 index 0000000..5f9c3e9 --- /dev/null +++ b/src/router/modules/dashboard.ts @@ -0,0 +1,24 @@ +import { AppRouteRecord } from '@/types/router' + +export const dashboardRoutes: AppRouteRecord = { + name: 'Dashboard', + path: '/dashboard', + component: '/index/index', + meta: { + title: 'menus.dashboard.title', + icon: 'ri:pie-chart-line', + roles: ['R_SUPER', 'R_ADMIN'] + }, + children: [ + { + path: 'console', + name: 'Console', + component: '/dashboard/console', + meta: { + title: 'menus.dashboard.console', + keepAlive: false, + fixedTab: true + } + } + ] +} diff --git a/src/router/modules/dictionary.ts b/src/router/modules/dictionary.ts new file mode 100644 index 0000000..5209a23 --- /dev/null +++ b/src/router/modules/dictionary.ts @@ -0,0 +1,76 @@ +import type { AppRouteRecord } from '@/types/router' + +/** + * 字典管理相关路由模块 + * 包含:系统字典管理、租户字典管理、字典覆盖配置、缓存监控 + */ +const dictionaryRoutes: AppRouteRecord = { + path: '/dictionary', + name: 'Dictionary', + component: '/index/index', + redirect: '/dictionary/system', + meta: { + title: 'menus.dictionary.title', + icon: 'ri:book-2-line', + order: 5 + }, + children: [ + { + path: 'system', + name: 'SystemDictionary', + component: () => import('@views/system/dictionary/index.vue'), + meta: { + title: 'menus.dictionary.system', + icon: 'ri:book-2-line', + roles: ['PlatformAdmin'], + permission: ['dictionary:group:read'] + } + }, + { + path: 'tenant', + name: 'TenantDictionary', + component: () => import('@views/tenant/dictionary/index.vue'), + meta: { + title: 'menus.dictionary.tenant', + icon: 'ri:bookmark-3-line', + roles: ['TenantAdmin'], + permission: ['dictionary:group:read'] + } + }, + { + path: 'override', + name: 'TenantDictionaryOverride', + component: () => import('@views/tenant/dictionary-override/index.vue'), + meta: { + title: 'menus.dictionary.override', + icon: 'ri:settings-3-line', + roles: ['TenantAdmin'], + permission: ['dictionary:override:read'] + } + }, + { + path: 'label-override', + name: 'PlatformLabelOverride', + component: () => import('@views/system/dictionary-label-override/index.vue'), + meta: { + title: 'menus.dictionary.labelOverride', + icon: 'ri:exchange-line', + roles: ['PlatformAdmin'], + permission: ['dictionary:override:platform:read'] + } + }, + { + path: 'metrics', + name: 'DictionaryMetrics', + component: () => import('@views/system/dictionary-metrics/index.vue'), + meta: { + title: 'menus.dictionary.metrics', + icon: 'ri:bar-chart-box-line', + roles: ['PlatformAdmin'], + permission: ['dictionary:group:read'] + } + } + ] +} + +export default dictionaryRoutes diff --git a/src/router/modules/exception.ts b/src/router/modules/exception.ts new file mode 100644 index 0000000..07c5604 --- /dev/null +++ b/src/router/modules/exception.ts @@ -0,0 +1,46 @@ +import { AppRouteRecord } from '@/types/router' + +export const exceptionRoutes: AppRouteRecord = { + path: '/exception', + name: 'Exception', + component: '/index/index', + meta: { + title: 'menus.exception.title', + icon: 'ri:error-warning-line' + }, + children: [ + { + path: '403', + name: 'Exception403', + component: '/exception/403', + meta: { + title: 'menus.exception.forbidden', + keepAlive: true, + isHideTab: true, + isFullPage: true + } + }, + { + path: '404', + name: 'Exception404', + component: '/exception/404', + meta: { + title: 'menus.exception.notFound', + keepAlive: true, + isHideTab: true, + isFullPage: true + } + }, + { + path: '500', + name: 'Exception500', + component: '/exception/500', + meta: { + title: 'menus.exception.serverError', + keepAlive: true, + isHideTab: true, + isFullPage: true + } + } + ] +} diff --git a/src/router/modules/index.ts b/src/router/modules/index.ts new file mode 100644 index 0000000..80d74d5 --- /dev/null +++ b/src/router/modules/index.ts @@ -0,0 +1,23 @@ +import { AppRouteRecord } from '@/types/router' +import { dashboardRoutes } from './dashboard' +import { resultRoutes } from './result' +import { exceptionRoutes } from './exception' +import tenantRoutes from './tenant' +import announcementRoutes from './announcement' +import merchantRoutes from './merchant' +import dictionaryRoutes from './dictionary' +import storeRoutes from './store' + +/** + * 导出所有模块化路由 + */ +export const routeModules: AppRouteRecord[] = [ + dashboardRoutes, + tenantRoutes, + merchantRoutes, + dictionaryRoutes, + ...storeRoutes, + ...announcementRoutes, + resultRoutes, + exceptionRoutes +] diff --git a/src/router/modules/merchant.ts b/src/router/modules/merchant.ts new file mode 100644 index 0000000..7c25be7 --- /dev/null +++ b/src/router/modules/merchant.ts @@ -0,0 +1,48 @@ +import { AppRouteRecord } from '@/types/router' + +/** + * 商户管理相关路由模块 + */ +const merchantRoutes: AppRouteRecord = { + path: '/merchant', + name: 'Merchant', + component: '/index/index', + redirect: '/merchant/list', + meta: { + title: 'menus.merchant.title', + icon: 'ri:store-2-line', + order: 4 + }, + children: [ + { + path: 'list', + name: 'MerchantList', + component: '/merchant/list', + meta: { + title: 'menus.merchant.list', + icon: 'ri:list-unordered' + } + }, + { + path: 'review', + name: 'MerchantReview', + component: '/merchant/review', + meta: { + title: 'menus.merchant.review', + icon: 'ri:shield-check-line' + } + }, + { + path: 'store', + name: 'StoreList', + component: '/store/store-list/index', + meta: { + title: 'menus.store.title', + icon: 'ri:store-2-line', + permission: ['store:read'] + } + } + ] +} + +export default merchantRoutes diff --git a/src/router/modules/result.ts b/src/router/modules/result.ts new file mode 100644 index 0000000..575a2f7 --- /dev/null +++ b/src/router/modules/result.ts @@ -0,0 +1,33 @@ +import { AppRouteRecord } from '@/types/router' + +export const resultRoutes: AppRouteRecord = { + path: '/result', + name: 'Result', + component: '/index/index', + meta: { + title: 'menus.result.title', + icon: 'ri:checkbox-circle-line' + }, + children: [ + { + path: 'success', + name: 'ResultSuccess', + component: '/result/success', + meta: { + title: 'menus.result.success', + icon: 'ri:checkbox-circle-line', + keepAlive: true + } + }, + { + path: 'fail', + name: 'ResultFail', + component: '/result/fail', + meta: { + title: 'menus.result.fail', + icon: 'ri:close-circle-line', + keepAlive: true + } + } + ] +} diff --git a/src/router/modules/store.ts b/src/router/modules/store.ts new file mode 100644 index 0000000..69b746d --- /dev/null +++ b/src/router/modules/store.ts @@ -0,0 +1,47 @@ +import type { AppRouteRecord } from '@/types/router' + +/** + * 门店管理路由模块 + */ +const storeRoutes: AppRouteRecord[] = [ + { + path: '/platform', + name: 'Platform', + component: '/index/index', + redirect: '/platform/store-audits', + meta: { + title: 'menus.platform.title', + icon: 'ri:shield-star-line', + permission: ['store-audit:read', 'store-qualification:read'] + }, + children: [ + { + path: 'store-audits', + name: 'PlatformStoreAudits', + component: '/platform/store-audits', + meta: { + title: 'menus.platform.storeAudits', + icon: 'ri:checkbox-multiple-line', + permission: ['store-audit:read'], + authList: [ + { title: '审核通过', authMark: 'store-audit:approve' }, + { title: '审核驳回', authMark: 'store-audit:reject' }, + { title: '强制关闭/解除', authMark: 'store-audit:force-close' } + ] + } + }, + { + path: 'qualification-alerts', + name: 'PlatformQualificationAlerts', + component: '/platform/qualification-alerts', + meta: { + title: 'menus.platform.qualificationAlerts', + icon: 'ri:alarm-warning-line', + permission: ['store-qualification:read'] + } + } + ] + } +] + +export default storeRoutes diff --git a/src/router/modules/tenant.ts b/src/router/modules/tenant.ts new file mode 100644 index 0000000..ddb1ffa --- /dev/null +++ b/src/router/modules/tenant.ts @@ -0,0 +1,88 @@ +import { AppRouteRecord } from '@/types/router' + +/** + * 租户管理相关路由模块 + */ +const tenantRoutes: AppRouteRecord = { + path: '/tenant', + name: 'Tenant', + component: '/index/index', + redirect: '/tenant/subscription', + meta: { + title: 'menus.tenant.title', + icon: 'ri:building-2-line', + order: 3 + }, + children: [ + { + path: 'subscription', + name: 'TenantSubscription', + component: '/tenant/subscription', + meta: { + title: 'menus.tenant.subscription', + icon: 'ri:calendar-check-line' + } + }, + { + path: 'billing', + name: 'TenantBilling', + component: '/tenant/billing', + meta: { + title: 'menus.tenant.billing', + icon: 'ri:bill-line' + } + }, + { + path: 'billing/statistics', + name: 'TenantBillingStatistics', + component: '/tenant/billing/statistics', + meta: { + title: 'menus.tenant.billingStatistics', + icon: 'ri:bar-chart-box-line', + isHideMenu: false + } + }, + { + path: 'announcements', + name: 'TenantAnnouncementList', + component: () => import('@views/tenant/announcements/index.vue'), + meta: { + title: 'menus.announcement.tenant.list', + icon: 'ri:notification-3-line', + permission: ['tenant-announcement:read'], + authList: [ + { title: '创建租户公告', authMark: 'tenant-announcement:create' }, + { title: '查看租户公告', authMark: 'tenant-announcement:read' }, + { title: '编辑租户公告', authMark: 'tenant-announcement:update' }, + { title: '删除租户公告', authMark: 'tenant-announcement:delete' }, + { title: '发布租户公告', authMark: 'tenant-announcement:publish' }, + { title: '撤销租户公告', authMark: 'tenant-announcement:revoke' } + ] + } + }, + { + path: 'announcements/create', + name: 'TenantAnnouncementCreate', + component: () => import('@views/tenant/announcements/create.vue'), + meta: { + title: 'menus.announcement.tenant.create', + icon: 'ri:add-circle-line', + permission: ['tenant-announcement:create'], + hideMenu: true + } + }, + { + path: 'announcements/:announcementId/edit', + name: 'TenantAnnouncementEdit', + component: () => import('@views/tenant/announcements/create.vue'), + meta: { + title: 'menus.announcement.tenant.edit', + icon: 'ri:edit-line', + permission: ['tenant-announcement:update'], + hideMenu: true + } + } + ] +} + +export default tenantRoutes diff --git a/src/router/routes/asyncRoutes.ts b/src/router/routes/asyncRoutes.ts new file mode 100644 index 0000000..ccf1201 --- /dev/null +++ b/src/router/routes/asyncRoutes.ts @@ -0,0 +1,9 @@ +// 权限文档:https://www.artd.pro/docs/zh/guide/in-depth/permission.html +import { AppRouteRecord } from '@/types/router' +import { routeModules } from '../modules' + +/** + * 动态路由(需要权限才能访问的路由) + * 用于渲染菜单以及根据菜单权限动态加载路由,如果没有权限无法访问 + */ +export const asyncRoutes: AppRouteRecord[] = routeModules diff --git a/src/router/routes/staticRoutes.ts b/src/router/routes/staticRoutes.ts new file mode 100644 index 0000000..7af56d7 --- /dev/null +++ b/src/router/routes/staticRoutes.ts @@ -0,0 +1,108 @@ +import { AppRouteRecordRaw } from '@/utils/router' + +/** + * 静态路由配置(不需要权限就能访问的路由) + * + * 属性说明: + * isHideTab: true 表示不在标签页中显示 + * + * 注意事项: + * 1、path、name 不要和动态路由冲突,否则会导致路由冲突无法访问 + * 2、静态路由不管是否登录都可以访问 + */ +export const staticRoutes: AppRouteRecordRaw[] = [ + // 不需要登录就能访问的路由示例 + // { + // path: '/welcome', + // name: 'WelcomeStatic', + // component: () => import('@views/dashboard/console/index.vue'), + // meta: { title: 'menus.dashboard.title' } + // }, + { + path: '/auth/login', + name: 'Login', + component: () => import('@views/auth/login/index.vue'), + meta: { title: 'menus.login.title', isHideTab: true } + }, + { + path: '/auth/register', + name: 'Register', + component: () => import('@views/auth/register/index.vue'), + meta: { title: 'menus.register.title', isHideTab: true } + }, + { + path: '/onboarding/status', + name: 'TenantOnboardingStatus', + component: () => import('@views/onboarding/status/index.vue'), + meta: { title: 'menus.onboarding.status', isHideTab: true } + }, + { + path: '/onboarding/pricing', + name: 'TenantOnboardingPricing', + component: () => import('@views/onboarding/pricing/index.vue'), + meta: { title: 'menus.onboarding.pricing', isHideTab: true } + }, + { + path: '/onboarding/waiting', + name: 'TenantOnboardingWaiting', + component: () => import('@views/onboarding/waiting/index.vue'), + meta: { title: 'menus.onboarding.waiting', isHideTab: true } + }, + { + path: '/onboarding/error', + name: 'TenantOnboardingError', + component: () => import('@views/onboarding/error/index.vue'), + meta: { title: 'menus.onboarding.error', isHideTab: true } + }, + { + path: '/terms-of-service', + name: 'TermsOfService', + component: () => import('@views/onboarding/terms-of-service/index.vue'), + meta: { title: 'menus.termsOfService.title', isHideTab: true } + }, + { + path: '/auth/forget-password', + name: 'ForgetPassword', + component: () => import('@views/auth/forget-password/index.vue'), + meta: { title: 'menus.forgetPassword.title', isHideTab: true } + }, + { + path: '/auth/reset-password', + name: 'ResetPassword', + component: () => import('@views/auth/reset-password/index.vue'), + meta: { title: 'menus.resetPassword.title', isHideTab: true } + }, + { + path: '/403', + name: 'Exception403', + component: () => import('@views/exception/403/index.vue'), + meta: { title: '403', isHideTab: true } + }, + { + path: '/:pathMatch(.*)*', + name: 'Exception404', + component: () => import('@views/exception/404/index.vue'), + meta: { title: '404', isHideTab: true } + }, + { + path: '/500', + name: 'Exception500', + component: () => import('@views/exception/500/index.vue'), + meta: { title: '500', isHideTab: true } + }, + { + path: '/outside', + component: () => import('@views/index/index.vue'), + name: 'Outside', + meta: { title: 'menus.outside.title' }, + children: [ + // iframe 内嵌页面 + { + path: '/outside/iframe/:path', + name: 'Iframe', + component: () => import('@/views/outside/Iframe.vue'), + meta: { title: 'iframe' } + } + ] + } +] diff --git a/src/router/routesAlias.ts b/src/router/routesAlias.ts new file mode 100644 index 0000000..2af1c68 --- /dev/null +++ b/src/router/routesAlias.ts @@ -0,0 +1,8 @@ +/** + * 公共路由别名 + # 存放系统级公共路由路径,如布局容器、登录页等 + */ +export enum RoutesAlias { + Layout = '/index/index', // 布局容器 + Login = '/auth/login' // 登录页 +} diff --git a/src/store/index.ts b/src/store/index.ts new file mode 100644 index 0000000..b485999 --- /dev/null +++ b/src/store/index.ts @@ -0,0 +1,52 @@ +/** + * Pinia Store 配置模块 + * + * 提供全局状态管理的初始化和配置 + * + * ## 主要功能 + * + * - Pinia Store 实例创建 + * - 持久化插件配置(pinia-plugin-persistedstate) + * - 版本化存储键管理 + * - 自动数据迁移(跨版本) + * - LocalStorage 序列化配置 + * - Store 初始化函数 + * + * ## 持久化策略 + * + * - 使用 StorageKeyManager 生成版本化的存储键 + * - 格式:sys-v{version}-{storeId} + * - 自动迁移旧版本数据到当前版本 + * - 使用 localStorage 作为存储介质 + * + * @module store/index + * @author Art Design Pro Team + */ +import type { App } from 'vue' +import { createPinia } from 'pinia' +import { createPersistedState } from 'pinia-plugin-persistedstate' +import { StorageKeyManager } from '@/utils/storage/storage-key-manager' + +export const store = createPinia() + +// 创建存储键管理器实例 +const storageKeyManager = new StorageKeyManager() + +// 配置持久化插件 +store.use( + createPersistedState({ + key: (storeId: string) => storageKeyManager.getStorageKey(storeId), + storage: localStorage, + serializer: { + serialize: JSON.stringify, + deserialize: JSON.parse + } + }) +) + +/** + * 初始化 Store + */ +export function initStore(app: App): void { + app.use(store) +} diff --git a/src/store/modules/__tests__/dictionaryGroup.spec.ts b/src/store/modules/__tests__/dictionaryGroup.spec.ts new file mode 100644 index 0000000..930c7d0 --- /dev/null +++ b/src/store/modules/__tests__/dictionaryGroup.spec.ts @@ -0,0 +1,157 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest' +import { createPinia, setActivePinia } from 'pinia' +import { useDictionaryGroupStore } from '@/store/modules/dictionaryGroup' +import { DictionaryScope } from '@/enums/Dictionary' + +vi.mock('element-plus', () => ({ + ElMessage: { + error: vi.fn(), + success: vi.fn() + } +})) + +vi.mock('@/store/modules/user', () => ({ + useUserStore: () => ({ + info: { tenantId: '100' } + }) +})) + +const getGroupsMock = vi.fn() +const createGroupMock = vi.fn() +const deleteGroupMock = vi.fn() +const updateGroupMock = vi.fn() +const getGroupByIdMock = vi.fn() + +vi.mock('@/api/dictionary/group', () => ({ + getGroups: (...args: unknown[]) => getGroupsMock(...args), + createGroup: (...args: unknown[]) => createGroupMock(...args), + deleteGroup: (...args: unknown[]) => deleteGroupMock(...args), + updateGroup: (...args: unknown[]) => updateGroupMock(...args), + getGroupById: (...args: unknown[]) => getGroupByIdMock(...args) +})) + +const createDeferred = () => { + let resolve: (value: T) => void + let reject: (reason?: unknown) => void + const promise = new Promise((res, rej) => { + resolve = res + reject = rej + }) + return { promise, resolve: resolve!, reject: reject! } +} + +describe('dictionaryGroup store', () => { + beforeEach(() => { + setActivePinia(createPinia()) + vi.clearAllMocks() + }) + + it('fetchGroups updates state correctly', async () => { + const store = useDictionaryGroupStore() + const group: Api.Dictionary.DictionaryGroupDto = { + id: '1', + tenantId: '0', + code: 'order_status', + name: 'Order Status', + scope: DictionaryScope.System, + allowOverride: true, + isEnabled: true, + description: null, + createdAt: new Date().toISOString(), + updatedAt: null, + rowVersion: '', + items: [] + } + getGroupsMock.mockResolvedValue({ + items: [group], + page: 1, + pageSize: 20, + totalCount: 1, + totalPages: 1 + }) + + await store.fetchGroups(DictionaryScope.System, 1, 20) + + expect(store.groups).toEqual([group]) + expect(store.pagination.totalCount).toBe(1) + }) + + it('createGroup optimistic update and rollback on error', async () => { + const store = useDictionaryGroupStore() + const existing: Api.Dictionary.DictionaryGroupDto = { + id: '2', + tenantId: '0', + code: 'payment_method', + name: 'Payment Method', + scope: DictionaryScope.System, + allowOverride: true, + isEnabled: true, + description: null, + createdAt: new Date().toISOString(), + updatedAt: null, + rowVersion: '', + items: [] + } + store.groups = [existing] + + const deferred = createDeferred() + createGroupMock.mockReturnValue(deferred.promise) + + const actionPromise = store.createGroup({ + code: 'shipping_method', + name: 'Shipping Method', + scope: DictionaryScope.System, + allowOverride: false, + description: null + }) + + expect(store.groups.length).toBe(2) + expect(store.groups[0].code).toBe('shipping_method') + + deferred.reject(new Error('failed')) + await actionPromise + + expect(store.groups).toEqual([existing]) + }) + + it('deleteGroup removes from state', async () => { + const store = useDictionaryGroupStore() + const groupA: Api.Dictionary.DictionaryGroupDto = { + id: '3', + tenantId: '0', + code: 'order_status', + name: 'Order Status', + scope: DictionaryScope.System, + allowOverride: true, + isEnabled: true, + description: null, + createdAt: new Date().toISOString(), + updatedAt: null, + rowVersion: '', + items: [] + } + const groupB: Api.Dictionary.DictionaryGroupDto = { + id: '4', + tenantId: '0', + code: 'payment_method', + name: 'Payment Method', + scope: DictionaryScope.System, + allowOverride: true, + isEnabled: true, + description: null, + createdAt: new Date().toISOString(), + updatedAt: null, + rowVersion: '', + items: [] + } + store.groups = [groupA, groupB] + store.currentGroup = groupA + deleteGroupMock.mockResolvedValue(true) + + const result = await store.deleteGroup(groupA.id) + + expect(result).toBe(true) + expect(store.groups).toEqual([groupB]) + expect(store.currentGroup).toBeNull() + }) +}) diff --git a/src/store/modules/__tests__/dictionaryOverride.spec.ts b/src/store/modules/__tests__/dictionaryOverride.spec.ts new file mode 100644 index 0000000..55142d5 --- /dev/null +++ b/src/store/modules/__tests__/dictionaryOverride.spec.ts @@ -0,0 +1,86 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest' +import { createPinia, setActivePinia } from 'pinia' +import { useDictionaryOverrideStore } from '@/store/modules/dictionaryOverride' + +vi.mock('element-plus', () => ({ + ElMessage: { + error: vi.fn(), + success: vi.fn() + } +})) + +const getOverridesMock = vi.fn() +const getOverrideMock = vi.fn() +const enableOverrideMock = vi.fn() +const disableOverrideMock = vi.fn() +const updateHiddenItemsMock = vi.fn() +const updateSortOrderMock = vi.fn() + +vi.mock('@/api/dictionary/override', () => ({ + getOverrides: (...args: unknown[]) => getOverridesMock(...args), + getOverride: (...args: unknown[]) => getOverrideMock(...args), + enableOverride: (...args: unknown[]) => enableOverrideMock(...args), + disableOverride: (...args: unknown[]) => disableOverrideMock(...args), + updateHiddenItems: (...args: unknown[]) => updateHiddenItemsMock(...args), + updateSortOrder: (...args: unknown[]) => updateSortOrderMock(...args) +})) + +describe('dictionaryOverride store', () => { + beforeEach(() => { + setActivePinia(createPinia()) + vi.clearAllMocks() + }) + + it('enableOverride calls API and updates state', async () => { + const store = useDictionaryOverrideStore() + const config: Api.Dictionary.OverrideConfigDto = { + tenantId: '100', + systemDictionaryGroupCode: 'ORDER_STATUS', + overrideEnabled: true, + hiddenSystemItemIds: [], + customSortOrder: {} + } + enableOverrideMock.mockResolvedValue(config) + + const result = await store.enableOverride('ORDER_STATUS') + + expect(result).toEqual(config) + expect(store.overrides['order_status']).toEqual(config) + }) + + it('updateHiddenItems updates config', async () => { + const store = useDictionaryOverrideStore() + const config: Api.Dictionary.OverrideConfigDto = { + tenantId: '100', + systemDictionaryGroupCode: 'PAYMENT_METHOD', + overrideEnabled: true, + hiddenSystemItemIds: ['1', '2'], + customSortOrder: {} + } + updateHiddenItemsMock.mockResolvedValue(config) + + const result = await store.updateHiddenItems('PAYMENT_METHOD', ['1', '2']) + + expect(result).toEqual(config) + expect(store.overrides.payment_method).toEqual(config) + }) + + it('clearCache invalidates all cached overrides', async () => { + const store = useDictionaryOverrideStore() + const configs: Api.Dictionary.OverrideConfigDto[] = [ + { + tenantId: '100', + systemDictionaryGroupCode: 'ORDER_STATUS', + overrideEnabled: true, + hiddenSystemItemIds: [], + customSortOrder: {} + } + ] + getOverridesMock.mockResolvedValue(configs) + + await store.clearCache() + + expect(getOverridesMock).toHaveBeenCalled() + expect(store.overrides['order_status']).toEqual(configs[0]) + }) +}) diff --git a/src/store/modules/announcement.ts b/src/store/modules/announcement.ts new file mode 100644 index 0000000..7e4dc36 --- /dev/null +++ b/src/store/modules/announcement.ts @@ -0,0 +1,1004 @@ +/** + * 文件用途:公告模块状态管理 + * 作者:前端架构师(Codex) + * 日期:2025-12-20 + */ + +import { defineStore } from 'pinia' +import { computed, ref } from 'vue' +import { ElMessage } from 'element-plus' +import type { + AnnouncementDraft, + AnnouncementFormData, + AnnouncementQueryParams, + AnnouncementStatistics, + AudienceEstimate, + TargetRules, + TenantAnnouncementDto +} from '@/types/announcement' +import type { PagedResult } from '@/types/common/response' +import { + appAnnouncementApi, + platformAnnouncementApi, + tenantAnnouncementApi +} from '@/api/announcement' +import { normalizeAnnouncementStatus } from '@/utils/announcementStatus' +import { StorageKeyManager } from '@/utils/storage/storage-key-manager' +import { StorageConfig } from '@/utils/storage/storage-config' +import { useUserStore } from '@/store/modules/user' + +/** 公告作用域 */ +type AnnouncementScope = 'platform' | 'tenant' | 'app' + +/** 公告请求数据(允许传入 targetParameters 以便后端解析) */ +type AnnouncementRequestData = AnnouncementFormData & { targetParameters?: string | null } + +/** 公告操作可选项 */ +interface AnnouncementActionOptions { + scope?: AnnouncementScope + tenantId?: string + silent?: boolean + skipLoading?: boolean +} + +const storageKeyManager = new StorageKeyManager() +const DRAFT_STORE_ID = 'announcement-draft' +const DEFAULT_PAGE = 1 +const DEFAULT_PAGE_SIZE = 20 +const DEFAULT_DEBOUNCE_MS = 2000 +const DEFAULT_AUTO_SAVE_INTERVAL = 30000 + +/** + * 公告状态管理 Store + */ +export const useAnnouncementStore = defineStore( + 'announcementStore', + () => { + // 1. 公告列表数据 + const announcements = ref([]) + + // 2. 当前选中的公告 + const currentAnnouncement = ref(null) + + // 3. 未读公告数 + const unreadCount = ref(0) + + // 4. 统计数据 + const statistics = ref(null) + + // 5. 分页信息 + const pagination = ref({ + page: DEFAULT_PAGE, + pageSize: DEFAULT_PAGE_SIZE, + totalCount: 0, + totalPages: 0 + }) + + // 6. 加载状态 + const loading = ref(false) + + // 7. 草稿数据 + const currentDraft = ref(null) + + // 8. 草稿自动保存定时器 + let draftAutoSaveTimer: ReturnType | null = null + + // 9. 草稿防抖保存定时器 + let draftDebounceTimer: ReturnType | null = null + + /** + * 是否存在未读公告 + */ + const hasUnread = computed(() => unreadCount.value > 0) + + /** + * 草稿与已发布公告数量(合计) + */ + const draftPublishedAnnouncements = computed(() => { + // 1. 优先使用统计数据 + if (statistics.value) { + return statistics.value.draftCount + statistics.value.publishedCount + } + + // 2. 回退本地计算 + const draftCount = announcements.value.filter( + (item) => normalizeAnnouncementStatus(item.status) === 'Draft' + ).length + const publishedCount = announcements.value.filter( + (item) => normalizeAnnouncementStatus(item.status) === 'Published' + ).length + return draftCount + publishedCount + }) + + /** + * 有效期内公告(已发布且在有效期内) + */ + const effectiveAnnouncements = computed(() => { + // 1. 获取当前时间 + const now = Date.now() + + // 2. 过滤有效期公告 + return announcements.value.filter((item) => { + // 1. 解析时间范围 + const startAt = Date.parse(item.effectiveFrom) + const endAt = item.effectiveTo ? Date.parse(item.effectiveTo) : null + const startOk = !Number.isNaN(startAt) && startAt <= now + const endOk = !endAt || now <= endAt + + // 2. 判断有效状态 + return ( + item.isActive && + normalizeAnnouncementStatus(item.status) === 'Published' && + startOk && + endOk + ) + }) + }) + + /** + * 生成草稿存储键 + */ + const getDraftStorageKey = (draftId: string) => { + return `${storageKeyManager.getStorageKey(DRAFT_STORE_ID)}:${draftId}` + } + + /** + * 生成草稿ID + */ + const createDraftId = () => { + return `draft-${Date.now()}-${Math.random().toString(36).slice(2, 8)}` + } + + /** + * 解析作用域(优先使用显式 scope,其次根据参数判断) + */ + const resolveScope = ( + options?: AnnouncementActionOptions, + params?: AnnouncementQueryParams + ) => { + // 1. 优先使用显式 scope + if (options?.scope) { + return options.scope + } + + // 2. 根据租户参数推断为租户作用域 + if (params?.tenantId) { + return 'tenant' + } + + // 3. 根据当前租户信息判断 + const currentTenantId = resolveTenantId() + if (currentTenantId && currentTenantId !== '0') { + return 'tenant' + } + + // 4. 默认平台作用域 + return 'platform' + } + + /** + * 获取租户ID + */ + const resolveTenantId = (tenantId?: string) => { + // 1. 优先使用传入 tenantId + if (tenantId) { + return tenantId + } + + // 2. 回退用户信息中的 tenantId + const userStore = useUserStore() + if (userStore.info?.tenantId) { + return userStore.info.tenantId + } + + // 3. 回退本地存储的 tenantId + return localStorage.getItem(StorageConfig.TENANT_ID_KEY) || null + } + + /** + * 获取错误提示信息 + */ + const resolveErrorMessage = (error: unknown, fallback: string) => { + // 1. 使用异常信息作为提示 + if (error instanceof Error && error.message) { + return error.message + } + + // 2. 回退默认提示 + return fallback + } + + const handleActionError = ( + error: unknown, + fallback: string, + options?: AnnouncementActionOptions + ) => { + const message = resolveErrorMessage(error, fallback) + if (!options?.silent) { + ElMessage.error(message) + return null + } + throw error instanceof Error ? error : new Error(message) + } + + /** + * 构建目标受众参数 + */ + const buildTargetParameters = (data: AnnouncementRequestData): string | null => { + // 1. 直接使用调用方传入的 targetParameters + if (typeof data.targetParameters === 'string') { + return data.targetParameters + } + + // 2. 规则/角色模式使用规则参数 + if ((data.targetType === 'rules' || data.targetType === 'roles') && data.targetRules) { + return JSON.stringify(data.targetRules) + } + + // 3. 手选用户模式使用用户ID参数 + if ( + (data.targetType === 'users' || data.targetType === 'manual') && + data.targetUserIds?.length + ) { + return JSON.stringify({ userIds: data.targetUserIds }) + } + + // 4. 默认返回空 + return null + } + + /** + * 构建创建公告请求体 + */ + const buildCreatePayload = (data: AnnouncementRequestData) => { + // 1. 构建创建字段 + return { + title: data.title, + content: data.content, + announcementType: data.announcementType, + priority: data.priority, + effectiveFrom: data.effectiveFrom, + effectiveTo: data.effectiveTo ?? null, + targetType: data.targetType, + targetParameters: buildTargetParameters(data) + } + } + + /** + * 构建更新公告请求体 + */ + const buildUpdatePayload = (data: AnnouncementRequestData) => { + // 1. 构建更新字段 + return { + title: data.title, + content: data.content, + targetType: data.targetType, + targetParameters: buildTargetParameters(data), + rowVersion: data.rowVersion + } + } + + /** + * 更新分页状态 + */ + const applyPagedResult = (result: PagedResult) => { + // 1. 更新列表数据 + announcements.value = Array.isArray(result.items) ? result.items : [] + + // 2. 更新分页信息 + pagination.value = { + page: result.page, + pageSize: result.pageSize, + totalCount: result.totalCount, + totalPages: result.totalPages + } + } + + /** + * 刷新统计数据 + */ + const refreshStatistics = () => { + // 1. 统计列表中的状态数量 + const list = announcements.value + const draftCount = list.filter( + (item) => normalizeAnnouncementStatus(item.status) === 'Draft' + ).length + const publishedCount = list.filter( + (item) => normalizeAnnouncementStatus(item.status) === 'Published' + ).length + const revokedCount = list.filter( + (item) => normalizeAnnouncementStatus(item.status) === 'Revoked' + ).length + const totalCount = pagination.value.totalCount > 0 ? pagination.value.totalCount : list.length + + // 2. 生成统计结果 + statistics.value = { + totalCount, + draftCount, + publishedCount, + revokedCount, + unreadCount: unreadCount.value + } + } + + /** + * 同步公告到列表与当前详情 + */ + const syncAnnouncement = (announcement: TenantAnnouncementDto, insertIfMissing: boolean) => { + // 1. 同步公告列表 + const index = announcements.value.findIndex((item) => item.id === announcement.id) + if (index >= 0) { + announcements.value.splice(index, 1, announcement) + } else if (insertIfMissing) { + announcements.value = [announcement, ...announcements.value] + } + + // 2. 同步当前详情 + currentAnnouncement.value = announcement + } + + /** + * 获取公告列表 + */ + const fetchAnnouncements = async ( + params: AnnouncementQueryParams, + options?: AnnouncementActionOptions + ) => { + loading.value = true + + try { + // 1. 解析作用域与租户上下文 + const scope = resolveScope(options, params) + const tenantId = + scope === 'tenant' ? resolveTenantId(options?.tenantId ?? params.tenantId) : null + + if (scope === 'tenant' && !tenantId) { + ElMessage.error('未获取到租户ID,无法加载租户公告') + return null + } + + // 2. 拉取公告列表数据 + let result: PagedResult + if (scope === 'app') { + result = await appAnnouncementApi.list(params) + } else if (scope === 'tenant' && tenantId) { + result = await tenantAnnouncementApi.list(tenantId, params) + } else { + result = await platformAnnouncementApi.list(params) + } + + // 3. 更新列表与分页状态 + applyPagedResult(result) + + // 4. 刷新统计数据 + refreshStatistics() + + return result + } catch (error) { + // 1. 处理异常提示 + return handleActionError(error, '获取公告列表失败', options) + } finally { + // 1. 关闭加载状态 + loading.value = false + } + } + + /** + * 获取公告详情 + */ + const fetchAnnouncementDetail = async ( + announcementId: string, + options?: AnnouncementActionOptions + ) => { + loading.value = true + + try { + // 1. 解析作用域与租户上下文 + const scope = resolveScope(options) + const tenantId = scope === 'tenant' ? resolveTenantId(options?.tenantId) : null + + if (scope === 'tenant' && !tenantId) { + ElMessage.error('未获取到租户ID,无法加载公告详情') + return null + } + + if (scope === 'app') { + const cached = announcements.value.find((item) => item.id === announcementId) || null + currentAnnouncement.value = cached + return cached + } + + // 2. 拉取公告详情数据 + const result = + scope === 'tenant' && tenantId + ? await tenantAnnouncementApi.detail(tenantId, announcementId) + : await platformAnnouncementApi.detail(announcementId) + + // 3. 同步详情数据 + syncAnnouncement(result, true) + + return result + } catch (error) { + // 1. 处理异常提示 + return handleActionError(error, '获取公告详情失败', options) + } finally { + // 1. 关闭加载状态 + loading.value = false + } + } + + /** + * 创建公告 + */ + const createAnnouncement = async ( + data: AnnouncementFormData, + options?: AnnouncementActionOptions + ) => { + loading.value = true + + try { + // 1. 解析作用域与租户上下文 + const scope = resolveScope(options) + const tenantId = scope === 'tenant' ? resolveTenantId(options?.tenantId) : null + + if (scope === 'app') { + ElMessage.error('应用端不支持创建公告') + return null + } + + if (scope === 'tenant' && !tenantId) { + ElMessage.error('未获取到租户ID,无法创建公告') + return null + } + + // 2. 构建请求体并发起请求 + const payload = buildCreatePayload(data as AnnouncementRequestData) as AnnouncementFormData + const result = + scope === 'tenant' && tenantId + ? await tenantAnnouncementApi.create(tenantId, payload) + : await platformAnnouncementApi.create(payload) + + // 3. 同步列表与统计 + syncAnnouncement(result, true) + pagination.value.totalCount += 1 + pagination.value.totalPages = Math.max( + 1, + Math.ceil(pagination.value.totalCount / pagination.value.pageSize) + ) + refreshStatistics() + + return result + } catch (error) { + // 1. 处理异常提示 + ElMessage.error(resolveErrorMessage(error, '创建公告失败')) + return null + } finally { + // 1. 关闭加载状态 + loading.value = false + } + } + + /** + * 更新公告(仅草稿) + */ + const updateAnnouncement = async ( + announcementId: string, + data: AnnouncementFormData, + options?: AnnouncementActionOptions + ) => { + loading.value = true + + try { + // 1. 校验并发控制版本 + if (!data.rowVersion) { + ElMessage.error('缺少并发控制版本,无法更新公告') + return null + } + + // 2. 解析作用域与租户上下文 + const scope = resolveScope(options) + const tenantId = scope === 'tenant' ? resolveTenantId(options?.tenantId) : null + + if (scope === 'app') { + ElMessage.error('应用端不支持更新公告') + return null + } + + if (scope === 'tenant' && !tenantId) { + ElMessage.error('未获取到租户ID,无法更新公告') + return null + } + + // 3. 构建请求体并发起请求 + const payload = buildUpdatePayload(data as AnnouncementRequestData) as AnnouncementFormData + const result = + scope === 'tenant' && tenantId + ? await tenantAnnouncementApi.update(tenantId, announcementId, payload) + : await platformAnnouncementApi.update(announcementId, payload) + + // 4. 同步列表与统计 + syncAnnouncement(result, true) + refreshStatistics() + + return result + } catch (error) { + // 1. 处理异常提示 + ElMessage.error(resolveErrorMessage(error, '更新公告失败')) + return null + } finally { + // 1. 关闭加载状态 + loading.value = false + } + } + + /** + * 发布公告 + */ + const publishAnnouncement = async ( + announcementId: string, + rowVersion: string, + options?: AnnouncementActionOptions + ) => { + loading.value = true + + try { + // 1. 校验并发控制版本 + if (!rowVersion) { + ElMessage.error('缺少并发控制版本,无法发布公告') + return null + } + + // 2. 解析作用域与租户上下文 + const scope = resolveScope(options) + const tenantId = scope === 'tenant' ? resolveTenantId(options?.tenantId) : null + + if (scope === 'app') { + ElMessage.error('应用端不支持发布公告') + return null + } + + if (scope === 'tenant' && !tenantId) { + ElMessage.error('未获取到租户ID,无法发布公告') + return null + } + + // 3. 发起发布请求 + const result = + scope === 'tenant' && tenantId + ? await tenantAnnouncementApi.publish(tenantId, announcementId, { rowVersion }) + : await platformAnnouncementApi.publish(announcementId, { rowVersion }) + + // 4. 同步列表与统计 + syncAnnouncement(result, true) + refreshStatistics() + + return result + } catch (error) { + // 1. 处理异常提示 + ElMessage.error(resolveErrorMessage(error, '发布公告失败')) + return null + } finally { + // 1. 关闭加载状态 + loading.value = false + } + } + + /** + * 撤销公告 + */ + const revokeAnnouncement = async ( + announcementId: string, + rowVersion: string, + options?: AnnouncementActionOptions + ) => { + loading.value = true + + try { + // 1. 校验并发控制版本 + if (!rowVersion) { + ElMessage.error('缺少并发控制版本,无法撤销公告') + return null + } + + // 2. 解析作用域与租户上下文 + const scope = resolveScope(options) + const tenantId = scope === 'tenant' ? resolveTenantId(options?.tenantId) : null + + if (scope === 'app') { + ElMessage.error('应用端不支持撤销公告') + return null + } + + if (scope === 'tenant' && !tenantId) { + ElMessage.error('未获取到租户ID,无法撤销公告') + return null + } + + // 3. 发起撤销请求 + const result = + scope === 'tenant' && tenantId + ? await tenantAnnouncementApi.revoke(tenantId, announcementId, { rowVersion }) + : await platformAnnouncementApi.revoke(announcementId, { rowVersion }) + + // 4. 同步列表与统计 + syncAnnouncement(result, true) + refreshStatistics() + + return result + } catch (error) { + // 1. 处理异常提示 + ElMessage.error(resolveErrorMessage(error, '撤销公告失败')) + return null + } finally { + // 1. 关闭加载状态 + loading.value = false + } + } + + /** + * 删除公告 + */ + const deleteAnnouncement = async ( + announcementId: string, + options?: AnnouncementActionOptions + ) => { + loading.value = true + + try { + // 1. 解析作用域与租户上下文 + const scope = resolveScope(options) + const tenantId = scope === 'tenant' ? resolveTenantId(options?.tenantId) : null + + if (scope !== 'tenant') { + ElMessage.error('仅租户公告支持删除操作') + return false + } + + if (!tenantId) { + ElMessage.error('未获取到租户ID,无法删除公告') + return false + } + + // 2. 发起删除请求 + const result = await tenantAnnouncementApi.delete(tenantId, announcementId) + + // 3. 同步列表与统计 + announcements.value = announcements.value.filter((item) => item.id !== announcementId) + if (currentAnnouncement.value?.id === announcementId) { + currentAnnouncement.value = null + } + pagination.value.totalCount = Math.max(0, pagination.value.totalCount - 1) + pagination.value.totalPages = Math.max( + 0, + Math.ceil(pagination.value.totalCount / pagination.value.pageSize) + ) + refreshStatistics() + + return result + } catch (error) { + // 1. 处理异常提示 + ElMessage.error(resolveErrorMessage(error, '删除公告失败')) + return false + } finally { + // 1. 关闭加载状态 + loading.value = false + } + } + + /** + * 标记公告已读 + */ + const markAsRead = async (announcementId: string, options?: AnnouncementActionOptions) => { + if (!options?.skipLoading) { + loading.value = true + } + + try { + // 1. 解析作用域与租户上下文 + const scope = options?.scope ?? 'app' + const tenantId = scope === 'tenant' ? resolveTenantId(options?.tenantId) : null + + if (scope === 'platform') { + if (!options?.silent) { + ElMessage.error('平台公告不支持已读标记') + } + return null + } + + if (scope === 'tenant' && !tenantId) { + if (!options?.silent) { + ElMessage.error('未获取到租户ID,无法标记已读') + } + return null + } + + // 2. 发起标记请求 + const result = + scope === 'tenant' && tenantId + ? await tenantAnnouncementApi.markRead(tenantId, announcementId) + : await appAnnouncementApi.markRead(announcementId) + + // 3. 同步列表与统计 + const wasUnread = + announcements.value.find((item) => item.id === announcementId)?.isRead === false + syncAnnouncement(result, true) + if (wasUnread && unreadCount.value > 0) { + unreadCount.value -= 1 + } + refreshStatistics() + + return result + } catch (error) { + // 1. 处理异常提示 + if (!options?.silent) { + ElMessage.error(resolveErrorMessage(error, '标记已读失败')) + } + return null + } finally { + // 1. 关闭加载状态 + if (!options?.skipLoading) { + loading.value = false + } + } + } + + /** + * 批量标记已读 + */ + const batchMarkAsRead = async ( + announcementIds: string[], + options?: AnnouncementActionOptions + ) => { + loading.value = true + + try { + // 1. 校验参数 + if (!announcementIds.length) { + ElMessage.warning('未选择需要标记的公告') + return 0 + } + + // 2. 执行批量标记 + const tasks = announcementIds.map((id) => + markAsRead(id, { ...options, silent: true, skipLoading: true }) + ) + const results = await Promise.allSettled(tasks) + const successCount = results.filter( + (item) => item.status === 'fulfilled' && item.value + ).length + + // 3. 同步未读数量 + if (successCount < announcementIds.length) { + ElMessage.warning('部分公告标记失败,请检查网络或权限') + } + + return successCount + } catch (error) { + // 1. 处理异常提示 + ElMessage.error(resolveErrorMessage(error, '批量标记已读失败')) + return 0 + } finally { + // 1. 关闭加载状态 + loading.value = false + } + } + + /** + * 获取未读数量 + */ + const fetchUnreadCount = async () => { + try { + // 1. 拉取未读列表并读取总数 + const result = await appAnnouncementApi.unread({ page: 1, pageSize: 1 }) + unreadCount.value = Number.isNaN(result.totalCount) + ? result.items.length + : result.totalCount + + // 2. 刷新统计 + refreshStatistics() + + return unreadCount.value + } catch (error) { + // 1. 处理异常提示 + ElMessage.error(resolveErrorMessage(error, '获取未读数量失败')) + return 0 + } + } + + /** + * 获取统计数据 + */ + const fetchStatistics = async () => { + try { + // 1. 基于当前列表刷新统计 + refreshStatistics() + + return statistics.value + } catch (error) { + // 1. 处理异常提示 + ElMessage.error(resolveErrorMessage(error, '获取统计数据失败')) + return null + } + } + + /** + * 保存草稿 + */ + const saveDraft = (data: Partial) => { + try { + // 1. 准备草稿基础信息 + const draftId = currentDraft.value?.draftId ?? createDraftId() + const lastSaved = new Date().toISOString() + + // 2. 合并草稿数据 + const draft: AnnouncementDraft = { + ...(currentDraft.value || {}), + ...data, + draftId, + lastSaved + } + + // 3. 持久化到本地存储 + const storageKey = getDraftStorageKey(draftId) + localStorage.setItem(storageKey, JSON.stringify(draft)) + currentDraft.value = draft + + return draft + } catch (error) { + // 1. 处理异常提示 + ElMessage.error(resolveErrorMessage(error, '保存草稿失败')) + return null + } + } + + /** + * 加载草稿 + */ + const loadDraft = (draftId: string) => { + try { + // 1. 读取本地存储 + const storageKey = getDraftStorageKey(draftId) + const raw = localStorage.getItem(storageKey) + if (!raw) { + currentDraft.value = null + return null + } + + // 2. 解析草稿数据 + const draft = JSON.parse(raw) as AnnouncementDraft + currentDraft.value = draft + + return draft + } catch (error) { + // 1. 处理异常提示 + ElMessage.error(resolveErrorMessage(error, '加载草稿失败')) + return null + } + } + + /** + * 清除草稿 + */ + const clearDraft = (draftId: string) => { + try { + // 1. 删除本地存储 + const storageKey = getDraftStorageKey(draftId) + localStorage.removeItem(storageKey) + + // 2. 同步内存状态 + if (currentDraft.value?.draftId === draftId) { + currentDraft.value = null + } + } catch (error) { + // 1. 处理异常提示 + ElMessage.error(resolveErrorMessage(error, '清除草稿失败')) + } + } + + /** + * 自动保存草稿(定时 + 防抖) + */ + const autoSaveDraft = (data: Partial, interval: number) => { + // 1. 清理旧定时器 + if (draftAutoSaveTimer) { + clearInterval(draftAutoSaveTimer) + draftAutoSaveTimer = null + } + + if (draftDebounceTimer) { + clearTimeout(draftDebounceTimer) + draftDebounceTimer = null + } + + // 2. 触发防抖保存 + const scheduleSave = () => { + // 1. 清理旧防抖定时器 + if (draftDebounceTimer) { + clearTimeout(draftDebounceTimer) + } + + // 2. 设置新防抖定时器 + draftDebounceTimer = setTimeout(() => { + saveDraft(data) + }, DEFAULT_DEBOUNCE_MS) + } + + scheduleSave() + + // 3. 启动定时器 + const delay = + Number.isFinite(interval) && interval > 0 ? interval : DEFAULT_AUTO_SAVE_INTERVAL + draftAutoSaveTimer = setInterval(() => scheduleSave(), delay) + + return () => { + if (draftAutoSaveTimer) { + clearInterval(draftAutoSaveTimer) + draftAutoSaveTimer = null + } + + if (draftDebounceTimer) { + clearTimeout(draftDebounceTimer) + draftDebounceTimer = null + } + } + } + + /** + * 预估受众人数 + */ + const estimateAudience = async (rules: TargetRules): Promise => { + try { + // 1. 受众预估接口尚未对接,返回空结果 + const estimate: AudienceEstimate = { + count: 0, + preview: [] + } + + if (Object.keys(rules || {}).length > 0) { + ElMessage.warning('受众预估接口暂未接入,请联系后端补齐') + } + + return estimate + } catch (error) { + // 1. 处理异常提示 + ElMessage.error(resolveErrorMessage(error, '预估受众失败')) + return { count: 0, preview: [] } + } + } + + return { + announcements, + currentAnnouncement, + unreadCount, + statistics, + pagination, + loading, + currentDraft, + hasUnread, + draftPublishedAnnouncements, + effectiveAnnouncements, + fetchAnnouncements, + fetchAnnouncementDetail, + createAnnouncement, + updateAnnouncement, + publishAnnouncement, + revokeAnnouncement, + deleteAnnouncement, + markAsRead, + batchMarkAsRead, + fetchUnreadCount, + fetchStatistics, + saveDraft, + loadDraft, + clearDraft, + autoSaveDraft, + estimateAudience + } + }, + { + // 使用 storeId 走全局 StorageKeyManager(避免硬编码 Key) + persist: { + paths: ['unreadCount', 'statistics'] + } + } +) diff --git a/src/store/modules/billingStore.ts b/src/store/modules/billingStore.ts new file mode 100644 index 0000000..299137a --- /dev/null +++ b/src/store/modules/billingStore.ts @@ -0,0 +1,408 @@ +/** + * 账单管理状态模块 + * + * 提供账单列表/详情/统计、筛选条件、批量选择等状态管理能力。 + * + * @module store/modules/billingStore + */ +import { defineStore } from 'pinia' +import { computed, ref } from 'vue' +import { + batchUpdateStatus as apiBatchUpdateStatus, + cancelBilling as apiCancelBilling, + createBilling as apiCreateBilling, + fetchBillingDetail as apiFetchBillingDetail, + fetchBillingList as apiFetchBillingList, + fetchBillingPayments as apiFetchBillingPayments, + fetchBillingStatistics as apiFetchBillingStatistics, + fetchOverdueBillings as apiFetchOverdueBillings, + exportBillings as apiExportBillings, + recordPayment as apiRecordPayment, + updateBillingStatus as apiUpdateBillingStatus, + verifyPayment as apiVerifyPayment +} from '@/api/billing' +import { TenantBillingStatus } from '@/enums/Billing' + +interface BillingPagination { + page: number + pageSize: number + totalCount: number + totalPages: number +} + +/** + * 账单管理 Store + */ +export const useBillingStore = defineStore('billingStore', () => { + // ==================== State ==================== + + /** 账单列表 */ + const billings = ref([]) + + /** 当前账单详情 */ + const currentBilling = ref(null) + + /** 账单支付记录(可独立于 currentBilling 使用) */ + const payments = ref([]) + + /** 统计数据 */ + const statistics = ref(null) + + /** 逾期账单列表(用于逾期提醒/排行榜等,避免与 getter 同名冲突) */ + const overdueList = ref([]) + + /** 筛选条件(与后端 Query 保持一致) */ + const filters = ref>({ + PageNumber: 1, + PageSize: 20, + SortBy: 'CreatedAt', + SortDesc: true + }) + + /** 分页信息(来自后端 PageResult) */ + const pagination = ref({ + page: 1, + pageSize: 20, + totalCount: 0, + totalPages: 0 + }) + + /** 选中的账单 ID 集合 */ + const selectedIds = ref([]) + + /** 加载状态 */ + const loading = ref(false) + + // ==================== Getters ==================== + + /** 待支付账单 */ + const pendingBillings = computed(() => + billings.value.filter((b) => b.status === TenantBillingStatus.Pending) + ) + + /** 逾期账单 */ + const overdueBillings = computed(() => + billings.value.filter((b) => b.status === TenantBillingStatus.Overdue) + ) + + /** 当前列表合计金额(应付) */ + const totalAmount = computed(() => billings.value.reduce((sum, b) => sum + (b.amountDue || 0), 0)) + + /** 已选数量 */ + const selectedCount = computed(() => selectedIds.value.length) + + // ==================== Actions ==================== + + /** + * 获取账单列表 + * @param params 查询参数(可选) + */ + const fetchBillings = async (params?: Partial) => { + // 1. 进入加载态 + loading.value = true + + try { + // 2. 合并参数(以传入 params 覆盖 filters) + const query: Partial = { + ...filters.value, + ...(params || {}) + } + + // 3. 请求列表数据 + const res = await apiFetchBillingList(query) + + // 4. 写入列表与分页 + billings.value = res.items || [] + pagination.value = { + page: res.page ?? query.PageNumber ?? 1, + pageSize: res.pageSize ?? query.PageSize ?? 20, + totalCount: res.totalCount ?? 0, + totalPages: res.totalPages ?? 0 + } + + // 5. 更新筛选条件(保持 UI 与 Store 一致) + filters.value = query + } finally { + // 6. 退出加载态 + loading.value = false + } + } + + /** + * 获取账单详情 + * @param id 账单 ID + */ + const fetchBillingDetail = async (id: string) => { + // 1. 进入加载态 + loading.value = true + + try { + // 2. 请求详情 + currentBilling.value = await apiFetchBillingDetail(id) + + // 3. 同步支付记录(便于页面复用) + payments.value = currentBilling.value?.payments || [] + } finally { + // 4. 退出加载态 + loading.value = false + } + } + + /** + * 创建账单 + * @param data 创建账单参数 + */ + const createBilling = async (data: Api.Billing.CreateBillingCommand) => { + // 1. 创建 + const created = await apiCreateBilling(data) + + // 2. 刷新列表(保持 UI 一致) + await fetchBillings({ PageNumber: 1 }) + + return created + } + + /** + * 更新账单状态 + * @param id 账单 ID + * @param status 新状态 + * @param notes 备注(可选) + */ + const updateStatus = async ( + id: string, + status: Api.Billing.TenantBillingStatus, + notes?: string + ) => { + // 1. 更新状态 + const updated = await apiUpdateBillingStatus(id, { status, notes }) + + // 2. 同步列表项 + const index = billings.value.findIndex((b) => b.id === id) + if (index !== -1) { + billings.value[index] = { ...billings.value[index], ...updated } + } + + // 3. 同步详情 + if (currentBilling.value?.id === id) { + currentBilling.value = { ...currentBilling.value, ...updated } + } + + return updated + } + + /** + * 记录支付 + * @param billingId 账单 ID + * @param data 支付参数 + */ + const recordPayment = async (billingId: string, data: Api.Billing.RecordPaymentCommand) => { + // 1. 记录支付 + const payment = await apiRecordPayment(billingId, data) + + // 2. 刷新详情(保证支付列表最新) + await fetchBillingDetail(billingId) + + return payment + } + + /** + * 获取支付记录列表 + * @param billingId 账单 ID + */ + const fetchPayments = async (billingId: string) => { + // 1. 进入加载态 + loading.value = true + + try { + // 2. 拉取支付记录 + payments.value = (await apiFetchBillingPayments(billingId)) || [] + + // 3. 若当前详情一致,则同步 + if (currentBilling.value?.id === billingId) { + currentBilling.value = { ...currentBilling.value, payments: payments.value } + } + } finally { + // 4. 退出加载态 + loading.value = false + } + } + + /** + * 审核支付记录 + * @param paymentId 支付记录 ID + * @param approved 是否通过 + * @param notes 备注(可选) + */ + const verifyPayment = async (paymentId: string, approved: boolean, notes?: string) => { + // 1. 请求审核 + const updated = await apiVerifyPayment(paymentId, { approved, notes }) + + // 2. 同步本地支付记录 + const index = payments.value.findIndex((p) => p.id === paymentId) + if (index !== -1) payments.value[index] = { ...payments.value[index], ...updated } + + // 3. 若当前详情存在,则同步详情 payments + if (currentBilling.value?.payments) { + const idx = currentBilling.value.payments.findIndex((p) => p.id === paymentId) + if (idx !== -1) { + currentBilling.value = { + ...currentBilling.value, + payments: currentBilling.value.payments.map((p) => + p.id === paymentId ? { ...p, ...updated } : p + ) + } + } + } + + return updated + } + + /** + * 作废账单(若后端支持 DELETE 作废) + * @param id 账单 ID + * @param reason 作废原因 + */ + const cancelBilling = async (id: string, reason: string) => { + // 1. 发起作废 + await apiCancelBilling(id, { reason }) + + // 2. 刷新列表/详情,保证状态一致 + await fetchBillings() + if (currentBilling.value?.id === id) { + await fetchBillingDetail(id) + } + } + + /** + * 批量更新账单状态 + * @param data 批量更新命令 + */ + const batchUpdateStatus = async (data: Api.Billing.BatchUpdateStatusCommand) => { + // 1. 发起更新 + await apiBatchUpdateStatus(data) + + // 2. 刷新列表 + await fetchBillings() + } + + /** + * 拉取逾期账单列表(用于排行榜/提醒) + * @param params 查询参数 + */ + const fetchOverdueList = async (params?: Partial) => { + // 1. 进入加载态 + loading.value = true + + try { + // 2. 拉取逾期账单 + const res = await apiFetchOverdueBillings(params) + overdueList.value = res.items || [] + return res + } finally { + // 3. 退出加载态 + loading.value = false + } + } + + /** + * 导出账单(后端导出接口) + * @param data 导出参数 + */ + const exportBillings = async (data: Api.Billing.ExportParams) => { + // 1. 获取文件流 + return apiExportBillings(data) + } + + /** + * 获取统计数据 + * @param params 统计查询参数 + */ + const fetchStatistics = async (params: Api.Billing.BillingStatisticsParams) => { + // 1. 进入加载态 + loading.value = true + + try { + // 2. 请求统计数据 + statistics.value = await apiFetchBillingStatistics(params) + } finally { + // 3. 退出加载态 + loading.value = false + } + } + + /** + * 设置筛选条件(不自动请求) + * @param nextFilters 新筛选条件 + */ + const setFilters = (nextFilters: Partial) => { + // 1. 合并筛选条件 + filters.value = { ...filters.value, ...nextFilters } + } + + /** + * 选中/取消选中某条账单(切换) + * @param id 账单 ID + */ + const selectBilling = (id: string) => { + // 1. 切换选中状态 + if (selectedIds.value.includes(id)) { + selectedIds.value = selectedIds.value.filter((x) => x !== id) + return + } + + selectedIds.value = [...selectedIds.value, id] + } + + /** + * 全选当前列表 + */ + const selectAll = () => { + // 1. 全选当前页数据 + selectedIds.value = billings.value.map((b) => b.id) + } + + /** + * 清空选择 + */ + const clearSelection = () => { + // 1. 清空已选 + selectedIds.value = [] + } + + return { + // State + billings, + currentBilling, + payments, + statistics, + overdueList, + filters, + pagination, + selectedIds, + loading, + + // Getters + pendingBillings, + overdueBillings, + totalAmount, + selectedCount, + + // Actions + fetchBillings, + fetchBillingDetail, + createBilling, + updateStatus, + recordPayment, + fetchPayments, + verifyPayment, + cancelBilling, + batchUpdateStatus, + fetchOverdueList, + exportBillings, + fetchStatistics, + setFilters, + selectBilling, + selectAll, + clearSelection + } +}) diff --git a/src/store/modules/dictionaryCache.ts b/src/store/modules/dictionaryCache.ts new file mode 100644 index 0000000..5a09838 --- /dev/null +++ b/src/store/modules/dictionaryCache.ts @@ -0,0 +1,120 @@ +/** + * Dictionary cache store (client-side). + */ + +import { defineStore } from 'pinia' +import { computed, ref } from 'vue' +import { getDictionary, batchGetDictionaries } from '@/api/dictionary/query' + +const DEFAULT_TTL_MS = 30 * 60 * 1000 + +export const useDictionaryCacheStore = defineStore( + 'dictionaryCacheStore', + () => { + const cache = ref>({}) + const expiryMap = ref>({}) + const loading = ref(false) + + const normalizeCode = (code: string) => code.trim().toLowerCase() + + const isExpired = (code: string) => { + const key = normalizeCode(code) + const expiry = expiryMap.value[key] + return !expiry || Date.now() > expiry + } + + const isCached = (code: string) => { + const key = normalizeCode(code) + return Array.isArray(cache.value[key]) && !isExpired(key) + } + + const setCache = ( + code: string, + items: Api.Dictionary.DictionaryItemDto[], + ttl = DEFAULT_TTL_MS + ) => { + const key = normalizeCode(code) + cache.value[key] = items + expiryMap.value[key] = Date.now() + ttl + } + + const invalidate = (code: string) => { + const key = normalizeCode(code) + delete cache.value[key] + delete expiryMap.value[key] + } + + const invalidateAll = () => { + cache.value = {} + expiryMap.value = {} + } + + const getDictionaryAction = async (code: string, forceRefresh = false) => { + const key = normalizeCode(code) + if (!forceRefresh && isCached(key)) { + return cache.value[key] + } + + loading.value = true + try { + const result = await getDictionary(code) + setCache(code, result) + return result + } finally { + loading.value = false + } + } + + const batchGetDictionariesAction = async (codes: string[]) => { + if (!codes.length) return {} + + const cachedResult: Record = {} + const missing: string[] = [] + + codes.forEach((code) => { + const normalized = normalizeCode(code) + if (isCached(normalized)) { + cachedResult[code] = cache.value[normalized] + } else { + missing.push(code) + } + }) + + if (!missing.length) { + return cachedResult + } + + loading.value = true + try { + const fetched = await batchGetDictionaries(missing) + Object.keys(fetched).forEach((code) => { + setCache(code, fetched[code]) + cachedResult[code] = fetched[code] + }) + return cachedResult + } finally { + loading.value = false + } + } + + const cacheSize = computed(() => Object.keys(cache.value).length) + + return { + cache, + expiryMap, + loading, + cacheSize, + isCached, + isExpired, + getDictionary: getDictionaryAction, + batchGetDictionaries: batchGetDictionariesAction, + invalidate, + invalidateAll + } + }, + { + persist: { + paths: ['cache', 'expiryMap'] + } + } +) diff --git a/src/store/modules/dictionaryGroup.ts b/src/store/modules/dictionaryGroup.ts new file mode 100644 index 0000000..4771bad --- /dev/null +++ b/src/store/modules/dictionaryGroup.ts @@ -0,0 +1,227 @@ +/** + * Dictionary group state management. + */ + +import { defineStore } from 'pinia' +import { computed, ref } from 'vue' +import { ElMessage } from 'element-plus' +import { + createGroup, + deleteGroup, + getGroupById, + getGroups, + updateGroup +} from '@/api/dictionary/group' +import { useUserStore } from '@/store/modules/user' +import { $t } from '@/locales' +import { DictionaryScope } from '@/enums/Dictionary' + +const DEFAULT_PAGE = 1 +const DEFAULT_PAGE_SIZE = 20 + +export const useDictionaryGroupStore = defineStore('dictionaryGroupStore', () => { + const groups = ref([]) + const currentGroup = ref(null) + const loading = ref(false) + const error = ref(null) + const pagination = ref({ + page: DEFAULT_PAGE, + pageSize: DEFAULT_PAGE_SIZE, + totalCount: 0, + totalPages: 0 + }) + + const systemGroups = computed(() => + groups.value.filter((group) => group.scope === DictionaryScope.System) + ) + + const businessGroups = computed(() => + groups.value.filter((group) => group.scope === DictionaryScope.Business) + ) + + const resolveTenantId = () => { + const userStore = useUserStore() + return userStore.info?.tenantId || '0' + } + + const applyPageResult = (result: Api.Common.PageResult) => { + groups.value = Array.isArray(result.items) ? result.items : [] + pagination.value = { + page: result.page, + pageSize: result.pageSize, + totalCount: result.totalCount, + totalPages: result.totalPages + } + } + + const fetchGroups = async ( + scope?: Api.Dictionary.DictionaryScope, + page: number = DEFAULT_PAGE, + pageSize: number = DEFAULT_PAGE_SIZE + ) => { + loading.value = true + error.value = null + + try { + const result = await getGroups({ + Page: page, + PageSize: pageSize, + scope + }) + applyPageResult(result) + return result + } catch (err) { + error.value = err instanceof Error ? err.message : $t('dictionary.errors.loadGroups') + ElMessage.error(error.value) + return null + } finally { + loading.value = false + } + } + + const fetchGroupById = async (groupId: string) => { + loading.value = true + error.value = null + + try { + const result = await getGroupById(groupId) + currentGroup.value = result + return result + } catch (err) { + error.value = err instanceof Error ? err.message : $t('dictionary.errors.loadGroup') + ElMessage.error(error.value) + return null + } finally { + loading.value = false + } + } + + const createGroupAction = async (data: Api.Dictionary.CreateDictionaryGroupRequest) => { + loading.value = true + error.value = null + + const snapshot = [...groups.value] + const previousCurrentGroup = currentGroup.value + const tempId = `temp-${Date.now()}` + const tempGroup: Api.Dictionary.DictionaryGroupDto = { + id: tempId, + tenantId: String(resolveTenantId()), + code: data.code, + name: data.name, + scope: data.scope, + allowOverride: data.allowOverride, + isEnabled: true, + description: data.description ?? null, + createdAt: new Date().toISOString(), + updatedAt: null, + rowVersion: '', + items: [] + } + + groups.value = [tempGroup, ...groups.value] + + try { + const result = await createGroup(data) + const index = groups.value.findIndex((group) => group.id === tempId) + if (index >= 0) { + groups.value.splice(index, 1, result) + } else { + groups.value = [result, ...groups.value] + } + currentGroup.value = result + pagination.value.totalCount += 1 + pagination.value.totalPages = Math.max( + 1, + Math.ceil(pagination.value.totalCount / pagination.value.pageSize) + ) + return result + } catch (err) { + groups.value = snapshot + currentGroup.value = previousCurrentGroup + error.value = err instanceof Error ? err.message : $t('dictionary.errors.createGroup') + ElMessage.error(error.value) + return null + } finally { + loading.value = false + } + } + + const updateGroupAction = async ( + groupId: string, + data: Api.Dictionary.UpdateDictionaryGroupRequest + ) => { + loading.value = true + error.value = null + + const snapshot = [...groups.value] + const index = groups.value.findIndex((group) => group.id === groupId) + if (index >= 0) { + groups.value.splice(index, 1, { + ...groups.value[index], + ...data, + updatedAt: new Date().toISOString() + }) + } + + try { + const result = await updateGroup(groupId, data) + if (index >= 0) { + groups.value.splice(index, 1, result) + } + if (currentGroup.value?.id === groupId) { + currentGroup.value = result + } + return result + } catch (err) { + groups.value = snapshot + error.value = err instanceof Error ? err.message : $t('dictionary.errors.updateGroup') + ElMessage.error(error.value) + return null + } finally { + loading.value = false + } + } + + const deleteGroupAction = async (groupId: string) => { + loading.value = true + error.value = null + + const snapshot = [...groups.value] + groups.value = groups.value.filter((group) => group.id !== groupId) + if (currentGroup.value?.id === groupId) { + currentGroup.value = null + } + + try { + await deleteGroup(groupId) + pagination.value.totalCount = Math.max(0, pagination.value.totalCount - 1) + pagination.value.totalPages = Math.max( + 0, + Math.ceil(pagination.value.totalCount / pagination.value.pageSize) + ) + return true + } catch (err) { + groups.value = snapshot + error.value = err instanceof Error ? err.message : $t('dictionary.errors.deleteGroup') + ElMessage.error(error.value) + return false + } finally { + loading.value = false + } + } + + return { + groups, + currentGroup, + loading, + error, + pagination, + systemGroups, + businessGroups, + fetchGroups, + fetchGroupById, + createGroup: createGroupAction, + updateGroup: updateGroupAction, + deleteGroup: deleteGroupAction + } +}) diff --git a/src/store/modules/dictionaryItem.ts b/src/store/modules/dictionaryItem.ts new file mode 100644 index 0000000..8c41835 --- /dev/null +++ b/src/store/modules/dictionaryItem.ts @@ -0,0 +1,171 @@ +/** + * Dictionary item state management. + */ + +import { defineStore } from 'pinia' +import { computed, ref } from 'vue' +import { ElMessage } from 'element-plus' +import { createItem, deleteItem, getItems, updateItem } from '@/api/dictionary/item' +import { HttpError } from '@/utils/http/error' +import { $t } from '@/locales' + +export const useDictionaryItemStore = defineStore('dictionaryItemStore', () => { + const items = ref([]) + const currentGroupId = ref(null) + const loading = ref(false) + const error = ref(null) + + const enabledItems = computed(() => items.value.filter((item) => item.isEnabled)) + const defaultItem = computed(() => items.value.find((item) => item.isDefault) ?? null) + + const reset = () => { + items.value = [] + currentGroupId.value = null + loading.value = false + error.value = null + } + + const fetchItems = async (groupId: string) => { + error.value = null + currentGroupId.value = groupId + const requestGroupId = groupId + + if (requestGroupId.startsWith('temp-')) { + items.value = [] + loading.value = false + return null + } + + loading.value = true + try { + const result = await getItems(groupId) + if (currentGroupId.value !== requestGroupId) { + return null + } + items.value = Array.isArray(result) ? result : [] + return result + } catch (err) { + error.value = err instanceof Error ? err.message : $t('dictionary.errors.loadItems') + ElMessage.error(error.value) + if (currentGroupId.value === requestGroupId) { + items.value = [] + } + return null + } finally { + if (currentGroupId.value === requestGroupId) { + loading.value = false + } + } + } + + const createItemAction = async ( + groupId: string, + data: Api.Dictionary.CreateDictionaryItemRequest + ) => { + loading.value = true + error.value = null + + const snapshot = [...items.value] + const tempId = `temp-${Date.now()}` + const tempItem: Api.Dictionary.DictionaryItemDto = { + id: tempId, + groupId, + key: data.key, + value: data.value, + isDefault: data.isDefault ?? false, + isEnabled: data.isEnabled ?? true, + sortOrder: data.sortOrder ?? 0, + description: data.description ?? null, + source: 'tenant', + rowVersion: '' + } + + items.value = [tempItem, ...items.value] + + try { + const result = await createItem(groupId, data) + const index = items.value.findIndex((item) => item.id === tempId) + if (index >= 0) { + items.value.splice(index, 1, result) + } else { + items.value = [result, ...items.value] + } + return result + } catch (err) { + items.value = snapshot + error.value = err instanceof Error ? err.message : $t('dictionary.errors.createItem') + ElMessage.error(error.value) + return null + } finally { + loading.value = false + } + } + + const updateItemAction = async ( + groupId: string, + itemId: string, + data: Api.Dictionary.UpdateDictionaryItemRequest + ) => { + loading.value = true + error.value = null + + const snapshot = [...items.value] + const index = items.value.findIndex((item) => item.id === itemId) + if (index >= 0) { + items.value.splice(index, 1, { ...items.value[index], ...data }) + } + + try { + const result = await updateItem(groupId, itemId, data) + if (index >= 0) { + items.value.splice(index, 1, result) + } + return result + } catch (err) { + items.value = snapshot + if (err instanceof HttpError && err.code === 409) { + ElMessage.warning($t('dictionary.errors.dataConflict')) + } else { + error.value = err instanceof Error ? err.message : $t('dictionary.errors.updateItem') + ElMessage.error(error.value) + } + return null + } finally { + loading.value = false + } + } + + const deleteItemAction = async (groupId: string, itemId: string) => { + loading.value = true + error.value = null + + const snapshot = [...items.value] + items.value = items.value.filter((item) => item.id !== itemId) + + try { + await deleteItem(groupId, itemId) + return true + } catch (err) { + items.value = snapshot + error.value = err instanceof Error ? err.message : $t('dictionary.errors.deleteItem') + ElMessage.error(error.value) + return false + } finally { + loading.value = false + } + } + + return { + items, + currentGroupId, + loading, + error, + enabledItems, + defaultItem, + fetchItems, + reset, + createItem: createItemAction, + updateItem: updateItemAction, + deleteItem: deleteItemAction + } +}) diff --git a/src/store/modules/dictionaryOverride.ts b/src/store/modules/dictionaryOverride.ts new file mode 100644 index 0000000..e617612 --- /dev/null +++ b/src/store/modules/dictionaryOverride.ts @@ -0,0 +1,160 @@ +/** + * Dictionary override state management. + */ + +import { defineStore } from 'pinia' +import { ref } from 'vue' +import { ElMessage } from 'element-plus' +import { + disableOverride, + enableOverride, + getOverride, + getOverrides, + updateHiddenItems, + updateSortOrder +} from '@/api/dictionary/override' +import { $t } from '@/locales' + +export const useDictionaryOverrideStore = defineStore('dictionaryOverrideStore', () => { + const overrides = ref>({}) + const loading = ref(false) + const error = ref(null) + + const normalizeCode = (code: string) => code.trim().toLowerCase() + + const applyOverride = (config: Api.Dictionary.OverrideConfigDto) => { + overrides.value[normalizeCode(config.systemDictionaryGroupCode)] = config + } + + const fetchOverrides = async () => { + loading.value = true + error.value = null + + try { + const result = await getOverrides() + const nextMap: Record = {} + result.forEach((item) => { + nextMap[normalizeCode(item.systemDictionaryGroupCode)] = item + }) + overrides.value = nextMap + return result + } catch (err) { + error.value = err instanceof Error ? err.message : $t('dictionary.errors.loadOverrides') + ElMessage.error(error.value) + return null + } finally { + loading.value = false + } + } + + const fetchOverride = async (groupCode: string) => { + loading.value = true + error.value = null + + try { + const result = await getOverride(groupCode) + applyOverride(result) + return result + } catch (err) { + error.value = err instanceof Error ? err.message : $t('dictionary.errors.loadOverride') + ElMessage.error(error.value) + return null + } finally { + loading.value = false + } + } + + const enableOverrideAction = async (groupCode: string) => { + loading.value = true + error.value = null + + try { + const result = await enableOverride(groupCode) + applyOverride(result) + return result + } catch (err) { + error.value = err instanceof Error ? err.message : $t('dictionary.errors.enableOverride') + ElMessage.error(error.value) + return null + } finally { + loading.value = false + } + } + + const disableOverrideAction = async (groupCode: string) => { + loading.value = true + error.value = null + + try { + await disableOverride(groupCode) + const key = normalizeCode(groupCode) + if (overrides.value[key]) { + overrides.value[key] = { ...overrides.value[key], overrideEnabled: false } + } else { + await fetchOverride(groupCode) + } + return true + } catch (err) { + error.value = err instanceof Error ? err.message : $t('dictionary.errors.disableOverride') + ElMessage.error(error.value) + return false + } finally { + loading.value = false + } + } + + const updateHiddenItemsAction = async (groupCode: string, hiddenIds: string[]) => { + loading.value = true + error.value = null + + try { + const result = await updateHiddenItems(groupCode, hiddenIds) + applyOverride(result) + return result + } catch (err) { + error.value = err instanceof Error ? err.message : $t('dictionary.errors.updateHidden') + ElMessage.error(error.value) + return null + } finally { + loading.value = false + } + } + + const updateCustomSortOrderAction = async ( + groupCode: string, + sortOrder: Record + ) => { + loading.value = true + error.value = null + + try { + const result = await updateSortOrder(groupCode, sortOrder) + applyOverride(result) + return result + } catch (err) { + error.value = err instanceof Error ? err.message : $t('dictionary.errors.updateSort') + ElMessage.error(error.value) + return null + } finally { + loading.value = false + } + } + + const clearCache = async () => { + overrides.value = {} + return fetchOverrides() + } + + return { + overrides, + loading, + error, + fetchOverrides, + fetchOverride, + enableOverride: enableOverrideAction, + disableOverride: disableOverrideAction, + updateHiddenItems: updateHiddenItemsAction, + updateCustomSortOrder: updateCustomSortOrderAction, + clearCache + } +}) diff --git a/src/store/modules/impersonation.ts b/src/store/modules/impersonation.ts new file mode 100644 index 0000000..6b0e436 --- /dev/null +++ b/src/store/modules/impersonation.ts @@ -0,0 +1,119 @@ +/** + * 租户伪装登录状态管理模块 + * + * 负责管理“平台管理员伪装登录租户”的状态切换与退出恢复 + * + * @module store/modules/impersonation + */ +import { defineStore } from 'pinia' +import { computed, ref } from 'vue' +import { router } from '@/router' +import { resetRouterState } from '@/router/guards/beforeEach' +import { useUserStore } from '@/store/modules/user' +import { StorageConfig } from '@/utils/storage/storage-config' + +interface ImpersonationSnapshot { + originalAccessToken: string + originalRefreshToken: string + originalTenantId: string + impersonatedTenantId: string + startedAt: number +} + +/** + * 租户伪装登录 Store + */ +export const useImpersonationStore = defineStore( + 'impersonationStore', + () => { + // 伪装快照(为空表示未处于伪装态) + const snapshot = ref(null) + + // 计算属性:是否处于伪装态 + const isImpersonating = computed(() => snapshot.value !== null) + + /** + * 开始伪装登录 + */ + const start = async (token: Api.Auth.TokenResponse) => { + const userStore = useUserStore() + + // 1. 校验当前不处于伪装态 + if (isImpersonating.value) { + throw new Error('当前已处于伪装态,请先退出后再继续') + } + + // 2. 记录原始会话快照 + const originalTenantId = localStorage.getItem(StorageConfig.TENANT_ID_KEY) || '' + snapshot.value = { + originalAccessToken: userStore.accessToken, + originalRefreshToken: userStore.refreshToken, + originalTenantId, + impersonatedTenantId: token.user?.tenantId || '', + startedAt: Date.now() + } + + // 3. 切换到目标租户会话(Token + TenantId) + userStore.setToken(token.accessToken, token.refreshToken) + if (token.user) { + userStore.setUserInfo(token.user) + } + userStore.setLoginStatus(true) + + if (token.user?.tenantId) { + localStorage.setItem(StorageConfig.TENANT_ID_KEY, token.user.tenantId) + } + + // 4. 重置动态路由并重新导航(触发重新拉取用户信息与菜单) + resetRouterState(0) + await new Promise((resolve) => setTimeout(resolve, 20)) + await router.replace({ path: '/' }) + } + + /** + * 退出伪装登录 + */ + const exit = async () => { + const userStore = useUserStore() + + // 1. 校验当前处于伪装态 + if (!snapshot.value) { + return + } + + // 2. 恢复原始 Token 与 TenantId + userStore.setToken(snapshot.value.originalAccessToken, snapshot.value.originalRefreshToken) + if (snapshot.value.originalTenantId) { + localStorage.setItem(StorageConfig.TENANT_ID_KEY, snapshot.value.originalTenantId) + } + userStore.setLoginStatus(true) + + // 3. 清空快照并重置动态路由 + snapshot.value = null + resetRouterState(0) + await new Promise((resolve) => setTimeout(resolve, 20)) + await router.replace({ path: '/' }) + } + + /** + * 清空伪装快照(不做会话恢复) + */ + const clear = () => { + snapshot.value = null + } + + return { + snapshot, + isImpersonating, + start, + exit, + clear + } + }, + { + persist: { + key: 'impersonation', + storage: localStorage + } + } +) diff --git a/src/store/modules/labelOverride.ts b/src/store/modules/labelOverride.ts new file mode 100644 index 0000000..2a420bf --- /dev/null +++ b/src/store/modules/labelOverride.ts @@ -0,0 +1,301 @@ +/** + * 字典标签覆盖状态管理 + */ + +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import { ElMessage } from 'element-plus' +import { + getTenantLabelOverrides, + upsertTenantLabelOverride, + deleteTenantLabelOverride, + getPlatformLabelOverrides, + upsertPlatformLabelOverride, + deletePlatformLabelOverride +} from '@/api/dictionary/labelOverride' +import { $t } from '@/locales' + +export const useLabelOverrideStore = defineStore('labelOverrideStore', () => { + // ==================== 状态 ==================== + + // 1. 租户端覆盖列表 + const tenantOverrides = ref([]) + + // 2. 平台端覆盖列表(按租户 ID 分组) + const platformOverrides = ref([]) + + // 3. 当前选中的目标租户(平台管理用) + const selectedTenantId = ref(null) + + // 4. 加载状态 + const loading = ref(false) + + // 5. 错误信息 + const error = ref(null) + + // ==================== 计算属性 ==================== + + // 1. 按字典项 ID 索引的覆盖配置(方便快速查找) + const tenantOverrideMap = computed(() => { + const map: Record = {} + tenantOverrides.value.forEach((item) => { + map[item.dictionaryItemId] = item + }) + return map + }) + + // 2. 平台覆盖按字典项 ID 索引 + const platformOverrideMap = computed(() => { + const map: Record = {} + platformOverrides.value.forEach((item) => { + map[item.dictionaryItemId] = item + }) + return map + }) + + // ==================== 租户端操作 ==================== + + /** + * 获取当前租户的所有标签覆盖 + */ + const fetchTenantOverrides = async () => { + loading.value = true + error.value = null + + try { + const result = await getTenantLabelOverrides() + tenantOverrides.value = result ?? [] + return result + } catch (err) { + error.value = err instanceof Error ? err.message : $t('dictionary.errors.loadLabelOverrides') + ElMessage.error(error.value) + return null + } finally { + loading.value = false + } + } + + /** + * 租户创建/更新标签覆盖 + */ + const upsertTenantOverrideAction = async (data: Api.Dictionary.UpsertLabelOverrideRequest) => { + loading.value = true + error.value = null + + try { + const result = await upsertTenantLabelOverride(data) + + // 1. 更新本地状态 + const idx = tenantOverrides.value.findIndex( + (o) => o.dictionaryItemId === data.dictionaryItemId + ) + if (idx >= 0) { + tenantOverrides.value[idx] = result + } else { + tenantOverrides.value.push(result) + } + + ElMessage.success($t('common.saveSuccess')) + return result + } catch (err) { + error.value = err instanceof Error ? err.message : $t('dictionary.errors.saveLabelOverride') + ElMessage.error(error.value) + return null + } finally { + loading.value = false + } + } + + /** + * 租户删除标签覆盖 + */ + const deleteTenantOverrideAction = async (dictionaryItemId: string) => { + loading.value = true + error.value = null + + try { + await deleteTenantLabelOverride(dictionaryItemId) + + // 1. 从本地移除 + tenantOverrides.value = tenantOverrides.value.filter( + (o) => o.dictionaryItemId !== dictionaryItemId + ) + + ElMessage.success($t('common.deleteSuccess')) + return true + } catch (err) { + error.value = err instanceof Error ? err.message : $t('dictionary.errors.deleteLabelOverride') + ElMessage.error(error.value) + return false + } finally { + loading.value = false + } + } + + // ==================== 平台端操作 ==================== + + /** + * 设置当前选中的目标租户 + */ + const setSelectedTenant = (tenantId: string | null) => { + selectedTenantId.value = tenantId + if (!tenantId) { + platformOverrides.value = [] + } + } + + /** + * 获取指定租户的所有标签覆盖(平台管理员用) + */ + const fetchPlatformOverrides = async ( + targetTenantId: string, + overrideType?: Api.Dictionary.OverrideType + ) => { + loading.value = true + error.value = null + + try { + const result = await getPlatformLabelOverrides(targetTenantId, overrideType) + platformOverrides.value = result ?? [] + selectedTenantId.value = targetTenantId + return result + } catch (err) { + error.value = err instanceof Error ? err.message : $t('dictionary.errors.loadLabelOverrides') + ElMessage.error(error.value) + return null + } finally { + loading.value = false + } + } + + /** + * 平台强制覆盖租户字典项的标签 + */ + const upsertPlatformOverrideAction = async ( + targetTenantId: string, + data: Api.Dictionary.UpsertLabelOverrideRequest + ) => { + loading.value = true + error.value = null + + try { + const result = await upsertPlatformLabelOverride(targetTenantId, data) + + // 1. 更新本地状态 + const idx = platformOverrides.value.findIndex( + (o) => o.dictionaryItemId === data.dictionaryItemId + ) + if (idx >= 0) { + platformOverrides.value[idx] = result + } else { + platformOverrides.value.push(result) + } + + ElMessage.success($t('common.saveSuccess')) + return result + } catch (err) { + error.value = err instanceof Error ? err.message : $t('dictionary.errors.saveLabelOverride') + ElMessage.error(error.value) + return null + } finally { + loading.value = false + } + } + + /** + * 平台删除对租户的强制覆盖 + */ + const deletePlatformOverrideAction = async (targetTenantId: string, dictionaryItemId: string) => { + loading.value = true + error.value = null + + try { + await deletePlatformLabelOverride(targetTenantId, dictionaryItemId) + + // 1. 从本地移除 + platformOverrides.value = platformOverrides.value.filter( + (o) => o.dictionaryItemId !== dictionaryItemId + ) + + ElMessage.success($t('common.deleteSuccess')) + return true + } catch (err) { + error.value = err instanceof Error ? err.message : $t('dictionary.errors.deleteLabelOverride') + ElMessage.error(error.value) + return false + } finally { + loading.value = false + } + } + + // ==================== 辅助方法 ==================== + + /** + * 清空缓存 + */ + const clearCache = () => { + tenantOverrides.value = [] + platformOverrides.value = [] + selectedTenantId.value = null + error.value = null + } + + /** + * 检查某个字典项是否已有覆盖(租户端) + */ + const hasTenantOverride = (dictionaryItemId: string) => { + return !!tenantOverrideMap.value[dictionaryItemId] + } + + /** + * 检查某个字典项是否已有平台覆盖 + */ + const hasPlatformOverride = (dictionaryItemId: string) => { + return !!platformOverrideMap.value[dictionaryItemId] + } + + /** + * 获取字典项的覆盖配置(租户端) + */ + const getTenantOverride = (dictionaryItemId: string) => { + return tenantOverrideMap.value[dictionaryItemId] ?? null + } + + /** + * 获取字典项的平台覆盖配置 + */ + const getPlatformOverride = (dictionaryItemId: string) => { + return platformOverrideMap.value[dictionaryItemId] ?? null + } + + return { + // 状态 + tenantOverrides, + platformOverrides, + selectedTenantId, + loading, + error, + + // 计算属性 + tenantOverrideMap, + platformOverrideMap, + + // 租户端操作 + fetchTenantOverrides, + upsertTenantOverride: upsertTenantOverrideAction, + deleteTenantOverride: deleteTenantOverrideAction, + + // 平台端操作 + setSelectedTenant, + fetchPlatformOverrides, + upsertPlatformOverride: upsertPlatformOverrideAction, + deletePlatformOverride: deletePlatformOverrideAction, + + // 辅助方法 + clearCache, + hasTenantOverride, + hasPlatformOverride, + getTenantOverride, + getPlatformOverride + } +}) diff --git a/src/store/modules/menu.ts b/src/store/modules/menu.ts new file mode 100644 index 0000000..85d13da --- /dev/null +++ b/src/store/modules/menu.ts @@ -0,0 +1,109 @@ +/** + * 菜单状态管理模块 + * + * 提供菜单数据和动态路由的状态管理 + * + * ## 主要功能 + * + * - 菜单列表存储和管理 + * - 首页路径配置 + * - 动态路由注册和移除 + * - 路由移除函数管理 + * - 菜单宽度配置 + * + * ## 使用场景 + * + * - 动态菜单加载和渲染 + * - 路由权限控制 + * - 首页路径动态设置 + * - 登出时清理动态路由 + * + * ## 工作流程 + * + * 1. 获取菜单数据(前端/后端模式) + * 2. 设置菜单列表和首页路径 + * 3. 注册动态路由并保存移除函数 + * 4. 登出时调用移除函数清理路由 + * + * @module store/modules/menu + * @author Art Design Pro Team + */ +import { defineStore } from 'pinia' +import { ref } from 'vue' +import { AppRouteRecord } from '@/types/router' +import { getFirstMenuPath } from '@/utils' +import { HOME_PAGE_PATH } from '@/router' + +/** + * 菜单状态管理 + * 管理应用的菜单列表、首页路径、菜单宽度和动态路由移除函数 + */ +export const useMenuStore = defineStore('menuStore', () => { + /** 首页路径 */ + const homePath = ref(HOME_PAGE_PATH) + /** 菜单列表 */ + const menuList = ref([]) + /** 菜单宽度 */ + const menuWidth = ref('') + /** 存储路由移除函数的数组 */ + const removeRouteFns = ref<(() => void)[]>([]) + + /** + * 设置菜单列表 + * @param list 菜单路由记录数组 + */ + const setMenuList = (list: AppRouteRecord[]) => { + menuList.value = list + setHomePath(HOME_PAGE_PATH || getFirstMenuPath(list)) + } + + /** + * 获取首页路径 + * @returns 首页路径字符串 + */ + const getHomePath = () => homePath.value + + /** + * 设置主页路径 + * @param path 主页路径 + */ + const setHomePath = (path: string) => { + homePath.value = path + } + + /** + * 添加路由移除函数 + * @param fns 要添加的路由移除函数数组 + */ + const addRemoveRouteFns = (fns: (() => void)[]) => { + removeRouteFns.value.push(...fns) + } + + /** + * 移除所有动态路由 + * 执行所有存储的路由移除函数并清空数组 + */ + const removeAllDynamicRoutes = () => { + removeRouteFns.value.forEach((fn) => fn()) + removeRouteFns.value = [] + } + + /** + * 清空路由移除函数数组 + */ + const clearRemoveRouteFns = () => { + removeRouteFns.value = [] + } + + return { + menuList, + menuWidth, + removeRouteFns, + setMenuList, + getHomePath, + setHomePath, + addRemoveRouteFns, + removeAllDynamicRoutes, + clearRemoveRouteFns + } +}) diff --git a/src/store/modules/merchant.ts b/src/store/modules/merchant.ts new file mode 100644 index 0000000..fc09dc2 --- /dev/null +++ b/src/store/modules/merchant.ts @@ -0,0 +1,31 @@ +/** + * 文件用途:商户模块状态管理 + */ + +import { defineStore } from 'pinia' +import { ref } from 'vue' + +export const useMerchantStore = defineStore('merchantStore', () => { + const currentMerchant = ref(null) + const listFilters = ref>({}) + + const setCurrentMerchant = (merchant: Api.Merchant.MerchantDetail | null) => { + currentMerchant.value = merchant + } + + const setListFilters = (filters: Partial) => { + listFilters.value = { ...listFilters.value, ...filters } + } + + const resetListFilters = () => { + listFilters.value = {} + } + + return { + currentMerchant, + listFilters, + setCurrentMerchant, + setListFilters, + resetListFilters + } +}) diff --git a/src/store/modules/quota-alert.ts b/src/store/modules/quota-alert.ts new file mode 100644 index 0000000..b3b6b30 --- /dev/null +++ b/src/store/modules/quota-alert.ts @@ -0,0 +1,63 @@ +/** + * 配额告警配置状态管理模块 + * + * 用于维护“配额使用率阈值”配置,并持久化到 localStorage。 + * + * @module store/modules/quota-alert + */ +import { defineStore } from 'pinia' +import { ref } from 'vue' + +// 默认阈值(百分比) +const DEFAULT_ALERT_THRESHOLD = 80 + +// 默认各类型阈值(按后端 QuotaType 枚举值) +const DEFAULT_THRESHOLDS: Record = { + 0: DEFAULT_ALERT_THRESHOLD, // 门店 + 1: DEFAULT_ALERT_THRESHOLD, // 账号 + 2: DEFAULT_ALERT_THRESHOLD, // 存储 + 3: DEFAULT_ALERT_THRESHOLD, // 短信 + 4: DEFAULT_ALERT_THRESHOLD, // 配送订单 + 5: DEFAULT_ALERT_THRESHOLD // 促销位 +} + +/** + * 配额告警配置 Store + */ +export const useQuotaAlertStore = defineStore( + 'quotaAlertStore', + () => { + // 1. 阈值配置(百分比) + const thresholds = ref>({ ...DEFAULT_THRESHOLDS }) + + // 2. 获取指定类型阈值 + const getThreshold = (quotaType: number) => { + return thresholds.value[quotaType] ?? DEFAULT_ALERT_THRESHOLD + } + + // 3. 设置指定类型阈值(0~100) + const setThreshold = (quotaType: number, value: number) => { + const nextValue = Math.max(0, Math.min(100, Math.round(value))) + thresholds.value = { + ...thresholds.value, + [quotaType]: nextValue + } + } + + // 4. 重置为默认值 + const resetToDefault = () => { + thresholds.value = { ...DEFAULT_THRESHOLDS } + } + + return { + thresholds, + getThreshold, + setThreshold, + resetToDefault + } + }, + { + // 使用 storeId 走全局 StorageKeyManager(避免硬编码 Key) + persist: true + } +) diff --git a/src/store/modules/setting.ts b/src/store/modules/setting.ts new file mode 100644 index 0000000..2878259 --- /dev/null +++ b/src/store/modules/setting.ts @@ -0,0 +1,450 @@ +/** + * 系统设置状态管理模块 + * + * 提供完整的系统设置状态管理 + * + * ## 主要功能 + * + * - 菜单布局配置(左侧、顶部、混合、双栏) + * - 主题管理(亮色、暗色、自动) + * - 菜单主题样式配置 + * - 界面显示开关(面包屑、标签页、语言切换等) + * - 功能开关(手风琴模式、色弱模式、水印等) + * - 样式配置(边框、圆角、容器宽度、页面过渡) + * - 节日功能配置 + * - Element Plus 主题色动态设置 + * + * ## 使用场景 + * + * - 设置面板配置管理 + * - 主题切换和样式定制 + * - 界面功能开关控制 + * - 用户偏好设置持久化 + * + * ## 持久化 + * + * - 使用 localStorage 存储 + * - 存储键:sys-v{version}-setting + * - 支持跨版本数据迁移 + * + * @module store/modules/setting + * @author Art Design Pro Team + */ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import { MenuThemeType } from '@/types/store' +import AppConfig from '@/config' +import { SystemThemeEnum, MenuThemeEnum, MenuTypeEnum, ContainerWidthEnum } from '@/enums/appEnum' +import { setElementThemeColor } from '@/utils/ui' +import { useCeremony } from '@/hooks/core/useCeremony' +import { StorageConfig } from '@/utils' +import { SETTING_DEFAULT_CONFIG } from '@/config/setting' + +/** + * 系统设置状态管理 + * 管理应用的菜单、主题、界面显示等各项设置 + */ +export const useSettingStore = defineStore( + 'settingStore', + () => { + // 菜单相关设置 + /** 菜单类型 */ + const menuType = ref(SETTING_DEFAULT_CONFIG.menuType) + /** 菜单展开宽度 */ + const menuOpenWidth = ref(SETTING_DEFAULT_CONFIG.menuOpenWidth) + /** 菜单是否展开 */ + const menuOpen = ref(SETTING_DEFAULT_CONFIG.menuOpen) + /** 双菜单是否显示文本 */ + const dualMenuShowText = ref(SETTING_DEFAULT_CONFIG.dualMenuShowText) + + // 主题相关设置 + /** 系统主题类型 */ + const systemThemeType = ref(SETTING_DEFAULT_CONFIG.systemThemeType) + /** 系统主题模式 */ + const systemThemeMode = ref(SETTING_DEFAULT_CONFIG.systemThemeMode) + /** 菜单主题类型 */ + const menuThemeType = ref(SETTING_DEFAULT_CONFIG.menuThemeType) + /** 系统主题颜色 */ + const systemThemeColor = ref(SETTING_DEFAULT_CONFIG.systemThemeColor) + + // 界面显示设置 + /** 是否显示菜单按钮 */ + const showMenuButton = ref(SETTING_DEFAULT_CONFIG.showMenuButton) + /** 是否显示快速入口 */ + const showFastEnter = ref(SETTING_DEFAULT_CONFIG.showFastEnter) + /** 是否显示刷新按钮 */ + const showRefreshButton = ref(SETTING_DEFAULT_CONFIG.showRefreshButton) + /** 是否显示面包屑 */ + const showCrumbs = ref(SETTING_DEFAULT_CONFIG.showCrumbs) + /** 是否显示工作台标签 */ + const showWorkTab = ref(SETTING_DEFAULT_CONFIG.showWorkTab) + /** 是否显示语言切换 */ + const showLanguage = ref(SETTING_DEFAULT_CONFIG.showLanguage) + /** 是否显示进度条 */ + const showNprogress = ref(SETTING_DEFAULT_CONFIG.showNprogress) + /** 是否显示设置引导 */ + const showSettingGuide = ref(SETTING_DEFAULT_CONFIG.showSettingGuide) + /** 是否显示节日文本 */ + const showFestivalText = ref(SETTING_DEFAULT_CONFIG.showFestivalText) + /** 是否显示水印 */ + const watermarkVisible = ref(SETTING_DEFAULT_CONFIG.watermarkVisible) + + // 功能设置 + /** 是否自动关闭 */ + const autoClose = ref(SETTING_DEFAULT_CONFIG.autoClose) + /** 是否唯一展开 */ + const uniqueOpened = ref(SETTING_DEFAULT_CONFIG.uniqueOpened) + /** 是否色弱模式 */ + const colorWeak = ref(SETTING_DEFAULT_CONFIG.colorWeak) + /** 是否刷新 */ + const refresh = ref(SETTING_DEFAULT_CONFIG.refresh) + /** 是否加载节日烟花 */ + const holidayFireworksLoaded = ref(SETTING_DEFAULT_CONFIG.holidayFireworksLoaded) + + // 样式设置 + /** 边框模式 */ + const boxBorderMode = ref(SETTING_DEFAULT_CONFIG.boxBorderMode) + /** 页面过渡效果 */ + const pageTransition = ref(SETTING_DEFAULT_CONFIG.pageTransition) + /** 标签页样式 */ + const tabStyle = ref(SETTING_DEFAULT_CONFIG.tabStyle) + /** 自定义圆角 */ + const customRadius = ref(SETTING_DEFAULT_CONFIG.customRadius) + /** 容器宽度 */ + const containerWidth = ref(SETTING_DEFAULT_CONFIG.containerWidth) + + // 节日相关 + /** 节日日期 */ + const festivalDate = ref('') + + /** + * 获取菜单主题 + * 根据当前主题类型和暗色模式返回对应的主题配置 + */ + const getMenuTheme = computed((): MenuThemeType => { + const list = AppConfig.themeList.filter((item) => item.theme === menuThemeType.value) + if (isDark.value) { + return AppConfig.darkMenuStyles[0] + } else { + return list[0] + } + }) + + /** + * 判断是否为暗色模式 + */ + const isDark = computed((): boolean => { + return systemThemeType.value === SystemThemeEnum.DARK + }) + + /** + * 获取菜单展开宽度 + */ + const getMenuOpenWidth = computed((): string => { + return menuOpenWidth.value + 'px' || SETTING_DEFAULT_CONFIG.menuOpenWidth + 'px' + }) + + /** + * 获取自定义圆角 + */ + const getCustomRadius = computed((): string => { + return customRadius.value + 'rem' || SETTING_DEFAULT_CONFIG.customRadius + 'rem' + }) + + /** + * 是否显示烟花 + * 根据当前日期和节日日期判断是否显示烟花效果 + */ + const isShowFireworks = computed((): boolean => { + return festivalDate.value === useCeremony().currentFestivalData.value?.date ? false : true + }) + + /** + * 切换菜单布局 + * @param type 菜单类型 + */ + const switchMenuLayouts = (type: MenuTypeEnum) => { + menuType.value = type + } + + /** + * 设置菜单展开宽度 + * @param width 宽度值 + */ + const setMenuOpenWidth = (width: number) => { + menuOpenWidth.value = width + } + + /** + * 设置全局主题 + * @param theme 主题类型 + * @param themeMode 主题模式 + */ + const setGlopTheme = (theme: SystemThemeEnum, themeMode: SystemThemeEnum) => { + systemThemeType.value = theme + systemThemeMode.value = themeMode + localStorage.setItem(StorageConfig.THEME_KEY, theme) + } + + /** + * 切换菜单样式 + * @param theme 菜单主题 + */ + const switchMenuStyles = (theme: MenuThemeEnum) => { + menuThemeType.value = theme + } + + /** + * 设置Element Plus主题颜色 + * @param theme 主题颜色 + */ + const setElementTheme = (theme: string) => { + systemThemeColor.value = theme + setElementThemeColor(theme) + } + + /** + * 切换边框模式 + */ + const setBorderMode = () => { + boxBorderMode.value = !boxBorderMode.value + } + + /** + * 设置容器宽度 + * @param width 容器宽度枚举值 + */ + const setContainerWidth = (width: ContainerWidthEnum) => { + containerWidth.value = width + } + + /** + * 切换唯一展开模式 + */ + const setUniqueOpened = () => { + uniqueOpened.value = !uniqueOpened.value + } + + /** + * 切换菜单按钮显示 + */ + const setButton = () => { + showMenuButton.value = !showMenuButton.value + } + + /** + * 切换快速入口显示 + */ + const setFastEnter = () => { + showFastEnter.value = !showFastEnter.value + } + + /** + * 切换自动关闭 + */ + const setAutoClose = () => { + autoClose.value = !autoClose.value + } + + /** + * 切换刷新按钮显示 + */ + const setShowRefreshButton = () => { + showRefreshButton.value = !showRefreshButton.value + } + + /** + * 切换面包屑显示 + */ + const setCrumbs = () => { + showCrumbs.value = !showCrumbs.value + } + + /** + * 设置工作台标签显示 + * @param show 是否显示 + */ + const setWorkTab = (show: boolean) => { + showWorkTab.value = show + } + + /** + * 切换语言切换显示 + */ + const setLanguage = () => { + showLanguage.value = !showLanguage.value + } + + /** + * 切换进度条显示 + */ + const setNprogress = () => { + showNprogress.value = !showNprogress.value + } + + /** + * 切换色弱模式 + */ + const setColorWeak = () => { + colorWeak.value = !colorWeak.value + } + + /** + * 隐藏设置引导 + */ + const hideSettingGuide = () => { + showSettingGuide.value = false + } + + /** + * 显示设置引导 + */ + const openSettingGuide = () => { + showSettingGuide.value = true + } + + /** + * 设置页面过渡效果 + * @param transition 过渡效果名称 + */ + const setPageTransition = (transition: string) => { + pageTransition.value = transition + } + + /** + * 设置标签页样式 + * @param style 样式名称 + */ + const setTabStyle = (style: string) => { + tabStyle.value = style + } + + /** + * 设置菜单展开状态 + * @param open 是否展开 + */ + const setMenuOpen = (open: boolean) => { + menuOpen.value = open + } + + /** + * 刷新页面 + */ + const reload = () => { + refresh.value = !refresh.value + } + + /** + * 设置水印显示 + * @param visible 是否显示 + */ + const setWatermarkVisible = (visible: boolean) => { + watermarkVisible.value = visible + } + + /** + * 设置自定义圆角 + * @param radius 圆角值 + */ + const setCustomRadius = (radius: string) => { + customRadius.value = radius + document.documentElement.style.setProperty('--custom-radius', `${radius}rem`) + } + + /** + * 设置节日烟花加载状态 + * @param isLoad 是否已加载 + */ + const setholidayFireworksLoaded = (isLoad: boolean) => { + holidayFireworksLoaded.value = isLoad + } + + /** + * 设置节日文本显示 + * @param show 是否显示 + */ + const setShowFestivalText = (show: boolean) => { + showFestivalText.value = show + } + + const setFestivalDate = (date: string) => { + festivalDate.value = date + } + + const setDualMenuShowText = (show: boolean) => { + dualMenuShowText.value = show + } + + return { + menuType, + menuOpenWidth, + systemThemeType, + systemThemeMode, + menuThemeType, + systemThemeColor, + boxBorderMode, + uniqueOpened, + showMenuButton, + showFastEnter, + showRefreshButton, + showCrumbs, + autoClose, + showWorkTab, + showLanguage, + showNprogress, + colorWeak, + showSettingGuide, + pageTransition, + tabStyle, + menuOpen, + refresh, + watermarkVisible, + customRadius, + holidayFireworksLoaded, + showFestivalText, + festivalDate, + dualMenuShowText, + containerWidth, + getMenuTheme, + isDark, + getMenuOpenWidth, + getCustomRadius, + isShowFireworks, + switchMenuLayouts, + setMenuOpenWidth, + setGlopTheme, + switchMenuStyles, + setElementTheme, + setBorderMode, + setContainerWidth, + setUniqueOpened, + setButton, + setFastEnter, + setAutoClose, + setShowRefreshButton, + setCrumbs, + setWorkTab, + setLanguage, + setNprogress, + setColorWeak, + hideSettingGuide, + openSettingGuide, + setPageTransition, + setTabStyle, + setMenuOpen, + reload, + setWatermarkVisible, + setCustomRadius, + setholidayFireworksLoaded, + setShowFestivalText, + setFestivalDate, + setDualMenuShowText + } + }, + { + persist: { + key: 'setting', + storage: localStorage + } + } +) diff --git a/src/store/modules/table.ts b/src/store/modules/table.ts new file mode 100644 index 0000000..094c310 --- /dev/null +++ b/src/store/modules/table.ts @@ -0,0 +1,97 @@ +/** + * 表格状态管理模块 + * + * 提供表格显示配置的状态管理 + * + * ## 主要功能 + * + * - 表格尺寸配置(紧凑、默认、宽松) + * - 斑马纹显示开关 + * - 边框显示开关 + * - 表头背景显示开关 + * - 全屏模式开关 + * + * ## 使用场景 + * - 表格组件样式配置 + * - 用户表格偏好设置 + * - 表格工具栏功能控制 + * + * ## 持久化 + * + * - 使用 localStorage 存储 + * - 存储键:sys-v{version}-table + * - 用户配置跨页面保持 + * + * @module store/modules/table + * @author Art Design Pro Team + */ +import { defineStore } from 'pinia' +import { ref } from 'vue' +import { TableSizeEnum } from '@/enums/formEnum' + +// 表格 +export const useTableStore = defineStore( + 'tableStore', + () => { + // 表格大小 + const tableSize = ref(TableSizeEnum.DEFAULT) + // 斑马纹 + const isZebra = ref(false) + // 边框 + const isBorder = ref(false) + // 表头背景 + const isHeaderBackground = ref(false) + + // 是否全屏 + const isFullScreen = ref(false) + + /** + * 设置表格大小 + * @param size 表格大小枚举值 + */ + const setTableSize = (size: TableSizeEnum) => (tableSize.value = size) + + /** + * 设置斑马纹显示状态 + * @param value 是否显示斑马纹 + */ + const setIsZebra = (value: boolean) => (isZebra.value = value) + + /** + * 设置表格边框显示状态 + * @param value 是否显示边框 + */ + const setIsBorder = (value: boolean) => (isBorder.value = value) + + /** + * 设置表头背景显示状态 + * @param value 是否显示表头背景 + */ + const setIsHeaderBackground = (value: boolean) => (isHeaderBackground.value = value) + + /** + * 设置是否全屏 + * @param value 是否全屏 + */ + const setIsFullScreen = (value: boolean) => (isFullScreen.value = value) + + return { + tableSize, + isZebra, + isBorder, + isHeaderBackground, + setTableSize, + setIsZebra, + setIsBorder, + setIsHeaderBackground, + isFullScreen, + setIsFullScreen + } + }, + { + persist: { + key: 'table', + storage: localStorage + } + } +) diff --git a/src/store/modules/user.ts b/src/store/modules/user.ts new file mode 100644 index 0000000..b5adfef --- /dev/null +++ b/src/store/modules/user.ts @@ -0,0 +1,257 @@ +/** + * 用户状态管理模块 + * + * 提供用户相关的状态管理 + * + * ## 主要功能 + * + * - 用户登录状态管理 + * - 用户信息存储 + * - 访问令牌和刷新令牌管理 + * - 语言设置 + * - 搜索历史记录 + * - 锁屏状态和密码管理 + * - 登出清理逻辑 + * + * ## 使用场景 + * + * - 用户登录和认证 + * - 权限验证 + * - 个人信息展示 + * - 多语言切换 + * - 锁屏功能 + * - 搜索历史管理 + * + * ## 持久化 + * + * - 使用 localStorage 存储 + * - 存储键:sys-v{version}-user + * - 登出时自动清理 + * + * @module store/modules/user + * @author Art Design Pro Team + */ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import { LanguageEnum } from '@/enums/appEnum' +import { router } from '@/router' +import { useSettingStore } from './setting' +import { useWorktabStore } from './worktab' +import { AppRouteRecord } from '@/types/router' +import { setPageTitle } from '@/utils/router' +import { resetRouterState } from '@/router/guards/beforeEach' +import { useMenuStore } from './menu' +import { StorageConfig } from '@/utils/storage/storage-config' +import { fetchGetUserPermissions } from '@/api/auth' + +/** + * 用户状态管理 + * 管理用户登录状态、个人信息、语言设置、搜索历史、锁屏状态等 + */ +export const useUserStore = defineStore( + 'userStore', + () => { + // 语言设置 + const language = ref(LanguageEnum.ZH) + // 登录状态 + const isLogin = ref(false) + // 锁屏状态 + const isLock = ref(false) + // 锁屏密码 + const lockPassword = ref('') + // 用户信息 + const info = ref>({}) + // 搜索历史记录 + const searchHistory = ref([]) + // 访问令牌 + const accessToken = ref('') + // 刷新令牌 + const refreshToken = ref('') + // 用户权限列表 + const permissions = ref([]) + + // 计算属性:获取用户信息 + const getUserInfo = computed(() => info.value) + // 计算属性:获取设置状态 + const getSettingState = computed(() => useSettingStore().$state) + // 计算属性:获取工作台状态 + const getWorktabState = computed(() => useWorktabStore().$state) + + /** + * 设置用户信息 + * @param newInfo 新的用户信息 + */ + const setUserInfo = (newInfo: Api.Auth.UserInfo) => { + info.value = newInfo + } + + /** + * 设置登录状态 + * @param status 登录状态 + */ + const setLoginStatus = (status: boolean) => { + isLogin.value = status + } + + /** + * 设置语言 + * @param lang 语言枚举值 + */ + const setLanguage = (lang: LanguageEnum) => { + setPageTitle(router.currentRoute.value) + language.value = lang + } + + /** + * 设置搜索历史 + * @param list 搜索历史列表 + */ + const setSearchHistory = (list: AppRouteRecord[]) => { + searchHistory.value = list + } + + /** + * 设置锁屏状态 + * @param status 锁屏状态 + */ + const setLockStatus = (status: boolean) => { + isLock.value = status + } + + /** + * 设置锁屏密码 + * @param password 锁屏密码 + */ + const setLockPassword = (password: string) => { + lockPassword.value = password + } + + /** + * 设置令牌 + * @param newAccessToken 访问令牌 + * @param newRefreshToken 刷新令牌(可选) + */ + const setToken = (newAccessToken: string, newRefreshToken?: string) => { + accessToken.value = newAccessToken + if (newRefreshToken) { + refreshToken.value = newRefreshToken + } + } + + /** + * 获取用户权限 + */ + const getUserPermissions = async () => { + if (!info.value.userId) return [] + try { + const res = await fetchGetUserPermissions(String(info.value.userId)) + permissions.value = res.permissions + return res.permissions + } catch (error) { + console.error('获取用户权限失败:', error) + return [] + } + } + + /** + * 退出登录 + * 清空所有用户相关状态并跳转到登录页 + * 如果是同一账号重新登录,保留工作台标签页 + */ + const logOut = () => { + // 保存当前用户 ID,用于下次登录时判断是否为同一用户 + const currentUserId = info.value.userId + if (currentUserId) { + localStorage.setItem(StorageConfig.LAST_USER_ID_KEY, String(currentUserId)) + } + + // 清空用户信息 + info.value = {} + // 重置登录状态 + isLogin.value = false + // 重置锁屏状态 + isLock.value = false + // 清空锁屏密码 + lockPassword.value = '' + // 清空访问令牌 + accessToken.value = '' + // 清空刷新令牌 + refreshToken.value = '' + // 清空权限列表 + permissions.value = [] + // 注意:不清空工作台标签页,等下次登录时根据用户判断 + // 移除iframe路由缓存 + sessionStorage.removeItem('iframeRoutes') + // 清空主页路径 + useMenuStore().setHomePath('') + // 重置路由状态 + resetRouterState(500) + // 跳转到登录页,携带当前路由作为 redirect 参数 + const currentRoute = router.currentRoute.value + const redirect = currentRoute.path !== '/login' ? currentRoute.fullPath : undefined + router.push({ + name: 'Login', + query: redirect ? { redirect } : undefined + }) + } + + /** + * 检查并清理工作台标签页 + * 如果不是同一用户登录,清空工作台标签页 + * 应在登录成功后调用 + */ + const checkAndClearWorktabs = () => { + const lastUserId = localStorage.getItem(StorageConfig.LAST_USER_ID_KEY) + const currentUserId = info.value.userId + + // 无法获取当前用户 ID,跳过检查 + if (!currentUserId) return + + // 首次登录或缓存已清除,保留现有标签页 + if (!lastUserId) { + return + } + + // 不同用户登录,清空工作台标签页 + if (String(currentUserId) !== lastUserId) { + const worktabStore = useWorktabStore() + worktabStore.opened = [] + worktabStore.keepAliveExclude = [] + } + + // 清除临时存储 + localStorage.removeItem(StorageConfig.LAST_USER_ID_KEY) + } + + return { + language, + isLogin, + isLock, + lockPassword, + info, + searchHistory, + accessToken, + refreshToken, + permissions, + getUserInfo, + getSettingState, + getWorktabState, + setUserInfo, + setLoginStatus, + setLanguage, + setSearchHistory, + setLockStatus, + setLockPassword, + setToken, + logOut, + checkAndClearWorktabs, + getUserPermissions + } + }, + { + persist: { + key: 'user', + storage: localStorage + } + } +) diff --git a/src/store/modules/worktab.ts b/src/store/modules/worktab.ts new file mode 100644 index 0000000..caa0d90 --- /dev/null +++ b/src/store/modules/worktab.ts @@ -0,0 +1,568 @@ +/** + * 工作标签页状态管理模块 + * + * 提供多标签页功能的完整状态管理 + * + * ## 主要功能 + * + * - 标签页打开和关闭 + * - 标签页固定和取消固定 + * - 批量关闭(左侧、右侧、其他、全部) + * - 标签页缓存管理(KeepAlive) + * - 标签页标题自定义 + * - 标签页路由验证 + * - 动态路由参数处理 + * + * ## 使用场景 + * + * - 多标签页导航 + * - 页面缓存控制 + * - 标签页右键菜单 + * - 固定常用页面 + * - 批量关闭标签 + * + * ## 核心特性 + * + * - 智能标签页复用(同路由名称复用) + * - 固定标签页保护(不可关闭) + * - KeepAlive 缓存排除管理 + * - 路由有效性验证 + * - 首页自动保留 + * + * ## 持久化 + * - 使用 localStorage 存储 + * - 存储键:sys-v{version}-worktab + * - 刷新页面保持标签状态 + * + * @module store/modules/worktab + * @author Art Design Pro Team + */ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import { router } from '@/router' +import { LocationQueryRaw, Router } from 'vue-router' +import { WorkTab } from '@/types' +import { useCommon } from '@/hooks/core/useCommon' + +interface WorktabState { + current: Partial + opened: WorkTab[] + keepAliveExclude: string[] +} + +/** + * 工作台标签页管理 Store + */ +export const useWorktabStore = defineStore( + 'worktabStore', + () => { + // 状态定义 + const current = ref>({}) + const opened = ref([]) + const keepAliveExclude = ref([]) + + // 计算属性 + const hasOpenedTabs = computed(() => opened.value.length > 0) + const hasMultipleTabs = computed(() => opened.value.length > 1) + const currentTabIndex = computed(() => + current.value.path ? opened.value.findIndex((tab) => tab.path === current.value.path) : -1 + ) + + /** + * 查找标签页索引 + */ + const findTabIndex = (path: string): number => { + return opened.value.findIndex((tab) => tab.path === path) + } + + /** + * 获取标签页 + */ + const getTab = (path: string): WorkTab | undefined => { + return opened.value.find((tab) => tab.path === path) + } + + /** + * 检查标签页是否可关闭 + */ + const isTabClosable = (tab: WorkTab): boolean => { + return !tab.fixedTab + } + + /** + * 安全的路由跳转 + */ + const safeRouterPush = (tab: Partial): void => { + if (!tab.path) { + console.warn('尝试跳转到无效路径的标签页') + return + } + + try { + router.push({ + path: tab.path, + query: tab.query as LocationQueryRaw + }) + } catch (error) { + console.error('路由跳转失败:', error) + } + } + + /** + * 打开或激活一个选项卡 + */ + const openTab = (tab: WorkTab): void => { + if (!tab.path) { + console.warn('尝试打开无效的标签页') + return + } + + // 从 keepAlive 排除列表中移除 + if (tab.name) { + removeKeepAliveExclude(tab.name) + } + + // 先根据路由名称查找(应对动态路由参数导致的多开问题),找不到再根据路径查找 + let existingIndex = -1 + if (tab.name) { + existingIndex = opened.value.findIndex((t) => t.name === tab.name) + } + if (existingIndex === -1) { + existingIndex = findTabIndex(tab.path) + } + + if (existingIndex === -1) { + // 新增标签页 + const insertIndex = tab.fixedTab ? findFixedTabInsertIndex() : opened.value.length + const newTab = { ...tab } + + if (tab.fixedTab) { + opened.value.splice(insertIndex, 0, newTab) + } else { + opened.value.push(newTab) + } + + current.value = newTab + } else { + // 更新现有标签页(当动态路由参数或查询变更时,复用同一标签) + const existingTab = opened.value[existingIndex] + + opened.value[existingIndex] = { + ...existingTab, + path: tab.path, + params: tab.params, + query: tab.query, + title: tab.title || existingTab.title, + fixedTab: tab.fixedTab ?? existingTab.fixedTab, + keepAlive: tab.keepAlive ?? existingTab.keepAlive, + name: tab.name || existingTab.name, + icon: tab.icon || existingTab.icon + } + + current.value = opened.value[existingIndex] + } + } + + /** + * 查找固定标签页的插入位置 + */ + const findFixedTabInsertIndex = (): number => { + let insertIndex = 0 + for (let i = 0; i < opened.value.length; i++) { + if (opened.value[i].fixedTab) { + insertIndex = i + 1 + } else { + break + } + } + return insertIndex + } + + /** + * 关闭指定的选项卡 + */ + const removeTab = (path: string): void => { + const targetTab = getTab(path) + const targetIndex = findTabIndex(path) + + if (targetIndex === -1) { + console.warn(`尝试关闭不存在的标签页: ${path}`) + return + } + + if (targetTab && !isTabClosable(targetTab)) { + console.warn(`尝试关闭固定标签页: ${path}`) + return + } + + // 从标签页列表中移除 + opened.value.splice(targetIndex, 1) + + // 处理缓存排除 + if (targetTab?.name) { + addKeepAliveExclude(targetTab) + } + + const { homePath } = useCommon() + + // 如果关闭后无标签页,跳转首页 + if (!hasOpenedTabs.value) { + if (path !== homePath.value) { + current.value = {} + safeRouterPush({ path: homePath.value }) + } + return + } + + // 如果关闭的是当前激活标签,需要激活其他标签 + if (current.value.path === path) { + const newIndex = targetIndex >= opened.value.length ? opened.value.length - 1 : targetIndex + current.value = opened.value[newIndex] + safeRouterPush(current.value) + } + } + + /** + * 关闭左侧选项卡 + */ + const removeLeft = (path: string): void => { + const targetIndex = findTabIndex(path) + + if (targetIndex === -1) { + console.warn(`尝试关闭左侧标签页,但目标标签页不存在: ${path}`) + return + } + + // 获取左侧可关闭的标签页 + const leftTabs = opened.value.slice(0, targetIndex) + const closableLeftTabs = leftTabs.filter(isTabClosable) + + if (closableLeftTabs.length === 0) { + console.warn('左侧没有可关闭的标签页') + return + } + + // 标记为缓存排除 + markTabsToRemove(closableLeftTabs) + + // 移除左侧可关闭的标签页 + opened.value = opened.value.filter( + (tab, index) => index >= targetIndex || !isTabClosable(tab) + ) + + // 确保当前标签是激活状态 + const targetTab = getTab(path) + if (targetTab) { + current.value = targetTab + } + } + + /** + * 关闭右侧选项卡 + */ + const removeRight = (path: string): void => { + const targetIndex = findTabIndex(path) + + if (targetIndex === -1) { + console.warn(`尝试关闭右侧标签页,但目标标签页不存在: ${path}`) + return + } + + // 获取右侧可关闭的标签页 + const rightTabs = opened.value.slice(targetIndex + 1) + const closableRightTabs = rightTabs.filter(isTabClosable) + + if (closableRightTabs.length === 0) { + console.warn('右侧没有可关闭的标签页') + return + } + + // 标记为缓存排除 + markTabsToRemove(closableRightTabs) + + // 移除右侧可关闭的标签页 + opened.value = opened.value.filter( + (tab, index) => index <= targetIndex || !isTabClosable(tab) + ) + + // 确保当前标签是激活状态 + const targetTab = getTab(path) + if (targetTab) { + current.value = targetTab + } + } + + /** + * 关闭其他选项卡 + */ + const removeOthers = (path: string): void => { + const targetTab = getTab(path) + + if (!targetTab) { + console.warn(`尝试关闭其他标签页,但目标标签页不存在: ${path}`) + return + } + + // 获取其他可关闭的标签页 + const otherTabs = opened.value.filter((tab) => tab.path !== path) + const closableTabs = otherTabs.filter(isTabClosable) + + if (closableTabs.length === 0) { + console.warn('没有其他可关闭的标签页') + return + } + + // 标记为缓存排除 + markTabsToRemove(closableTabs) + + // 只保留当前标签和固定标签 + opened.value = opened.value.filter((tab) => tab.path === path || !isTabClosable(tab)) + + // 确保当前标签是激活状态 + current.value = targetTab + } + + /** + * 关闭所有可关闭的标签页 + */ + const removeAll = (): void => { + const { homePath } = useCommon() + const hasFixedTabs = opened.value.some((tab) => tab.fixedTab) + + // 获取可关闭的标签页 + const closableTabs = opened.value.filter((tab) => { + if (!isTabClosable(tab)) return false + // 如果有固定标签,则所有可关闭的都可以关闭;否则保留首页 + return hasFixedTabs || tab.path !== homePath.value + }) + + if (closableTabs.length === 0) { + console.warn('没有可关闭的标签页') + return + } + + // 标记为缓存排除 + markTabsToRemove(closableTabs) + + // 保留不可关闭的标签页和首页(当没有固定标签时) + opened.value = opened.value.filter((tab) => { + return !isTabClosable(tab) || (!hasFixedTabs && tab.path === homePath.value) + }) + + // 处理激活状态 + if (!hasOpenedTabs.value) { + current.value = {} + safeRouterPush({ path: homePath.value }) + return + } + + // 选择激活的标签页:优先首页,其次第一个可用标签 + const homeTab = opened.value.find((tab) => tab.path === homePath.value) + const targetTab = homeTab || opened.value[0] + + current.value = targetTab + safeRouterPush(targetTab) + } + + /** + * 将指定选项卡添加到 keepAlive 排除列表中 + */ + const addKeepAliveExclude = (tab: WorkTab): void => { + if (!tab.keepAlive || !tab.name) return + + if (!keepAliveExclude.value.includes(tab.name)) { + keepAliveExclude.value.push(tab.name) + } + } + + /** + * 从 keepAlive 排除列表中移除指定组件名称 + */ + const removeKeepAliveExclude = (name: string): void => { + if (!name) return + + keepAliveExclude.value = keepAliveExclude.value.filter((item) => item !== name) + } + + /** + * 将传入的一组选项卡的组件名称标记为排除缓存 + */ + const markTabsToRemove = (tabs: WorkTab[]): void => { + tabs.forEach((tab) => { + if (tab.name) { + addKeepAliveExclude(tab) + } + }) + } + + /** + * 切换指定标签页的固定状态 + */ + const toggleFixedTab = (path: string): void => { + const targetIndex = findTabIndex(path) + + if (targetIndex === -1) { + console.warn(`尝试切换不存在标签页的固定状态: ${path}`) + return + } + + const tab = { ...opened.value[targetIndex] } + tab.fixedTab = !tab.fixedTab + + // 移除原位置 + opened.value.splice(targetIndex, 1) + + if (tab.fixedTab) { + // 固定标签插入到所有固定标签的末尾 + const firstNonFixedIndex = opened.value.findIndex((t) => !t.fixedTab) + const insertIndex = firstNonFixedIndex === -1 ? opened.value.length : firstNonFixedIndex + opened.value.splice(insertIndex, 0, tab) + } else { + // 非固定标签插入到所有固定标签后 + const fixedCount = opened.value.filter((t) => t.fixedTab).length + opened.value.splice(fixedCount, 0, tab) + } + + // 更新当前标签引用 + if (current.value.path === path) { + current.value = tab + } + } + + /** + * 验证工作台标签页的路由有效性 + */ + const validateWorktabs = (routerInstance: Router): void => { + try { + // 动态路由校验:优先使用路由 name 判断有效性;否则用 resolve 匹配参数化路径 + const isTabRouteValid = (tab: Partial): boolean => { + try { + if (tab.name) { + const routes = routerInstance.getRoutes() + if (routes.some((r) => r.name === tab.name)) return true + } + if (tab.path) { + const resolved = routerInstance.resolve({ + path: tab.path, + query: (tab.query as LocationQueryRaw) || undefined + }) + return resolved.matched.length > 0 + } + return false + } catch { + return false + } + } + + // 过滤出有效的标签页 + const validTabs = opened.value.filter((tab) => isTabRouteValid(tab)) + + if (validTabs.length !== opened.value.length) { + console.warn('发现无效的标签页路由,已自动清理') + opened.value = validTabs + } + + // 验证当前激活标签的有效性 + const isCurrentValid = current.value && isTabRouteValid(current.value) + + if (!isCurrentValid && validTabs.length > 0) { + console.warn('当前激活标签无效,已自动切换') + current.value = validTabs[0] + } else if (!isCurrentValid) { + current.value = {} + } + } catch (error) { + console.error('验证工作台标签页失败:', error) + } + } + + /** + * 清空所有状态(用于登出等场景) + */ + const clearAll = (): void => { + current.value = {} + opened.value = [] + keepAliveExclude.value = [] + } + + /** + * 获取状态快照(用于持久化存储) + */ + const getStateSnapshot = (): WorktabState => { + return { + current: { ...current.value }, + opened: [...opened.value], + keepAliveExclude: [...keepAliveExclude.value] + } + } + + /** + * 获取标签页标题 + */ + const getTabTitle = (path: string): WorkTab | undefined => { + const tab = getTab(path) + return tab + } + + /** + * 更新标签页标题 + */ + const updateTabTitle = (path: string, title: string): void => { + const tab = getTab(path) + if (tab) { + tab.customTitle = title + } + } + + /** + * 重置标签页标题 + */ + const resetTabTitle = (path: string): void => { + const tab = getTab(path) + if (tab) { + tab.customTitle = '' + } + } + + return { + // 状态 + current, + opened, + keepAliveExclude, + + // 计算属性 + hasOpenedTabs, + hasMultipleTabs, + currentTabIndex, + + // 方法 + openTab, + removeTab, + removeLeft, + removeRight, + removeOthers, + removeAll, + toggleFixedTab, + validateWorktabs, + clearAll, + getStateSnapshot, + + // 工具方法 + findTabIndex, + getTab, + isTabClosable, + addKeepAliveExclude, + removeKeepAliveExclude, + markTabsToRemove, + getTabTitle, + updateTabTitle, + resetTabTitle + } + }, + { + persist: { + key: 'worktab', + storage: localStorage + } + } +) diff --git a/src/types/announcement.ts b/src/types/announcement.ts new file mode 100644 index 0000000..94a0249 --- /dev/null +++ b/src/types/announcement.ts @@ -0,0 +1,140 @@ +/** + * 文件用途:公告模块 TypeScript 类型定义 + * 作者:前端架构师(Codex) + * 日期:2025-12-20 + */ + +/** 雪花ID(后端long序列化为字符串) */ +export type SnowflakeId = string + +/** UTC ISO 8601 时间字符串 */ +export type IsoDateTimeString = string + +/** 公告状态(字符串字面量) */ +export type AnnouncementStatus = 'Draft' | 'Published' | 'Revoked' + +/** 发布者范围(字符串字面量) */ +export type PublisherScope = 'Platform' | 'Tenant' + +/** 公告类型(数字枚举 0-8) */ +export enum TenantAnnouncementType { + System = 0, // 系统公告 + Billing = 1, // 账单/订阅相关提醒 + Operation = 2, // 运营通知 + SYSTEM_PLATFORM_UPDATE = 3, // 平台系统更新公告 + SYSTEM_SECURITY_NOTICE = 4, // 系统安全公告 + SYSTEM_COMPLIANCE = 5, // 系统合规公告 + TENANT_INTERNAL = 6, // 租户内部公告 + TENANT_FINANCE = 7, // 租户财务公告 + TENANT_OPERATION = 8 // 租户运营公告 +} + +/** 目标受众类型(字符串字面量) */ +export type AnnouncementTargetType = 'All' | 'Roles' | 'Users' | 'Rules' | 'Manual' + +/** 公告DTO(对齐后端 TenantAnnouncementDto) */ +export interface TenantAnnouncementDto { + id: SnowflakeId // 公告ID(雪花ID字符串) + tenantId: SnowflakeId // 租户ID(雪花ID字符串) + title: string // 标题 + content: string // 内容(富文本) + announcementType: TenantAnnouncementType // 公告类型 + priority: number // 优先级 + effectiveFrom: IsoDateTimeString // 生效开始时间(UTC) + effectiveTo?: IsoDateTimeString | null // 生效结束时间(UTC) + publisherScope: PublisherScope // 发布者范围 + publisherUserId?: SnowflakeId | null // 发布人用户ID(雪花ID字符串) + status: AnnouncementStatus // 公告状态 + publishedAt?: IsoDateTimeString | null // 发布时间(UTC) + revokedAt?: IsoDateTimeString | null // 撤销时间(UTC) + scheduledPublishAt?: IsoDateTimeString | null // 计划发布时间(UTC) + targetType: AnnouncementTargetType // 目标受众类型 + targetParameters?: string | null // 目标参数(JSON字符串) + rowVersion: string // 并发控制版本(Base64字符串) + isActive: boolean // 是否生效 + isRead: boolean // 当前用户是否已读 + readAt?: IsoDateTimeString | null // 已读时间(UTC) +} + +/** 目标受众规则(规则模式) */ +export interface TargetRules { + departments?: string[] // 部门ID列表 + roles?: string[] // 角色ID列表 + tags?: string[] // 标签ID列表 +} + +/** 公告表单数据(创建/编辑) */ +export interface AnnouncementFormData { + title: string // 标题 + content: string // 内容(富文本) + announcementType: TenantAnnouncementType // 公告类型 + priority: number // 优先级 + effectiveFrom: IsoDateTimeString // 生效开始时间(UTC) + effectiveTo?: IsoDateTimeString | null // 生效结束时间(UTC) + targetType: AnnouncementTargetType // 目标受众类型 + targetRules?: TargetRules // 规则模式参数 + targetUserIds?: SnowflakeId[] // 手选用户ID列表(雪花ID字符串) + rowVersion?: string // 并发控制版本(更新时必传) +} + +/** 公告查询参数 */ +export interface AnnouncementQueryParams { + page?: number // 页码 + pageSize?: number // 每页数量 + status?: AnnouncementStatus // 状态筛选 + keyword?: string // 关键词(标题/内容) + effectiveFrom?: IsoDateTimeString // 生效开始时间(UTC) + effectiveTo?: IsoDateTimeString // 生效结束时间(UTC) + dateFrom?: IsoDateTimeString // 起始时间(UTC) + dateTo?: IsoDateTimeString // 结束时间(UTC) + onlyEffective?: boolean // 仅查询有效公告 + tenantId?: SnowflakeId // 租户ID(雪花ID字符串) + isRead?: boolean // 已读状态筛选 +} + +/** 公告草稿数据 */ +export interface AnnouncementDraft extends Partial { + draftId: string // 草稿ID + lastSaved: IsoDateTimeString // 上次保存时间(UTC) +} + +/** 受众预估结果 */ +export interface AudienceEstimate { + count: number // 预估人数 + preview: SnowflakeId[] // 预览用户ID(前10个) +} + +/** 公告统计数据 */ +export interface AnnouncementStatistics { + totalCount: number // 总数 + draftCount: number // 草稿数量 + publishedCount: number // 已发布数量 + revokedCount: number // 已撤销数量 + unreadCount: number // 未读数量 +} + +/** 公告操作命令(发布/撤销) */ +export interface AnnouncementCommand { + announcementId: SnowflakeId // 公告ID(雪花ID字符串) + rowVersion: string // 并发控制版本(Base64字符串) +} + +/** 批量标记已读命令 */ +export interface BatchMarkReadCommand { + announcementIds: SnowflakeId[] // 公告ID列表(雪花ID字符串) +} + +/** 公告类型选项(下拉框) */ +export interface AnnouncementTypeOption { + label: string // 显示文本 + value: TenantAnnouncementType // 类型值 + icon?: string // 图标 + color?: string // 颜色 +} + +/** 公告状态选项(下拉框) */ +export interface AnnouncementStatusOption { + label: string // 显示文本 + value: AnnouncementStatus // 状态值 + color?: string // 颜色 +} diff --git a/src/types/api/auth.d.ts b/src/types/api/auth.d.ts new file mode 100644 index 0000000..6e40df5 --- /dev/null +++ b/src/types/api/auth.d.ts @@ -0,0 +1,82 @@ +declare namespace Api { + /** 认证类型 */ + namespace Auth { + /** 登录参数 */ + interface LoginParams { + account: string + password: string + } + + /** Token 响应 */ + interface TokenResponse { + accessToken: string + accessTokenExpiresAt: string + refreshToken: string + refreshTokenExpiresAt: string + user: UserInfo + isNewUser: boolean + } + + /** 登录响应 */ + type LoginResponse = TokenResponse + + /** 用户信息 */ + interface UserInfo { + userId: string + account: string + displayName: string + tenantId: string + merchantId?: string + roles: string[] + permissions: string[] + avatar?: string + } + + /** 菜单权限项 */ + interface MenuAuthItem { + title: string + authMark: string + } + + /** 菜单元数据 */ + interface MenuMeta { + title: string + icon?: string + keepAlive?: boolean + isIframe?: boolean + link?: string + roles?: string[] + authList?: MenuAuthItem[] + } + + /** 菜单节点 */ + interface MenuNode { + name: string + path: string + component: string + meta: MenuMeta + children?: MenuNode[] + } + + /** 菜单列表响应 */ + type MenuListResponse = MenuNode[] + + /** 用户权限响应 */ + interface UserPermissionsResponse { + userId: string + tenantId: string + merchantId: string + account: string + displayName: string + roles: string[] + permissions: string[] + createdAt: string + } + + /** 通过重置链接令牌重置管理员密码请求 */ + interface ResetAdminPasswordRequest { + token: string + newPassword: string + } + } +} diff --git a/src/types/api/billing.d.ts b/src/types/api/billing.d.ts new file mode 100644 index 0000000..fae9f99 --- /dev/null +++ b/src/types/api/billing.d.ts @@ -0,0 +1,369 @@ +declare namespace Api { + /** 账单管理类型 */ + namespace Billing { + // ==================== 枚举定义 ==================== + + /** 账单状态枚举 */ + enum TenantBillingStatus { + /** 待支付 */ + /** 已支付 */ + Pending = 0, + /** 已支付 */ + Paid = 1, + /** 已逾期 */ + Overdue = 2, + /** 已取消 */ + Cancelled = 3 + } + + /** 账单类型枚举 */ + enum BillingType { + /** 订阅账单 */ + Subscription = 0, + /** 配额包购买 */ + QuotaPurchase = 1, + /** 手动创建 */ + Manual = 2, + /** 续费账单 */ + Renewal = 3 + } + + /** 租户账单支付方式枚举(后端已从 PaymentMethod 重命名为 TenantPaymentMethod,以解决 Swagger schemaId 冲突) */ + enum TenantPaymentMethod { + /** 在线支付 */ + Online = 0, + /** 银行转账 */ + BankTransfer = 1, + /** 其他方式 */ + Other = 2 + } + + /** 租户账单支付状态枚举(后端已从 PaymentStatus 重命名为 TenantPaymentStatus,以解决 Swagger schemaId 冲突) */ + enum TenantPaymentStatus { + /** 待处理 */ + Pending = 0, + /** 成功 */ + Success = 1, + /** 失败 */ + Failed = 2, + /** 已退款 */ + Refunded = 3 + } + + /** 导出格式枚举 */ + enum ExportFormat { + /** Excel 格式 */ + Excel = 0, + /** PDF 格式 */ + Pdf = 1, + /** CSV 格式 */ + Csv = 2 + } + + // ==================== DTOs 定义 ==================== + + /** 账单列表项 DTO */ + interface BillingListDto { + id: string + tenantId: string + tenantName?: string + statementNo: string + /** 账单类型(部分接口可能不返回) */ + billingType?: BillingType + periodStart: string + periodEnd: string + amountDue: number + amountPaid: number + discountAmount?: number + taxAmount?: number + currency?: string + status: TenantBillingStatus + dueDate: string + overdueNotifiedAt?: string + createdAt: string + updatedAt?: string + } + + /** 账单详情 DTO */ + interface BillingDetailDto extends BillingListDto { + /** 账单明细 JSON(后端可能直接返回字符串) */ + lineItemsJson?: string + /** 账单明细列表(后端可能直接返回数组,或由前端从 lineItemsJson 解析) */ + lineItems?: BillingLineItemDto[] + /** 支付记录列表 */ + payments: PaymentRecordDto[] + notes?: string + reminderSentAt?: string + cancelledAt?: string + cancelReason?: string + } + + /** 账单明细项 DTO */ + interface BillingLineItemDto { + itemType: string + description: string + quantity: number + unitPrice: number + amount: number + discountRate?: number + } + + /** 支付记录 DTO */ + interface PaymentRecordDto { + id: string + /** 账单 ID(兼容不同接口字段命名) */ + billingStatementId?: string + /** 账单 ID(兼容不同接口字段命名) */ + billingId?: string + amount: number + method: TenantPaymentMethod + status: TenantPaymentStatus + transactionNo?: string + proofUrl?: string + paidAt?: string + notes?: string + /** 兼容部分后端返回的支付方式文本字段 */ + paymentMethod?: string + /** 兼容部分后端返回的审核状态字段 */ + verificationStatus?: string + verifiedBy?: string + verifiedAt?: string + refundReason?: string + refundedAt?: string + createdAt: string + } + + /** 账单统计数据 DTO */ + interface BillingStatisticsDto { + // 兼容“基础统计”返回(当前后端实现) + totalCount?: number + pendingCount?: number + paidCount?: number + overdueCount?: number + cancelledCount?: number + totalAmountDue?: number + totalAmountPaid?: number + totalAmountUnpaid?: number + totalOverdueAmount?: number + averageAmount?: number + paymentSuccessRate?: number + dailyStats?: Array<{ + date: string + count: number + totalAmount: number + paidAmount: number + }> + + // 兼容“增强统计”返回(用于仪表盘展示) + /** 总收入 */ + totalRevenue?: number + /** 本月收入 */ + monthlyRevenue?: number + /** 待收款金额 */ + pendingAmount?: number + /** 逾期金额 */ + overdueAmount?: number + /** 已支付账单数 */ + paidBillCount?: number + /** 待支付账单数 */ + pendingBillCount?: number + /** 已逾期账单数 */ + overdueBillCount?: number + /** 按状态分布 */ + statusDistribution?: Array<{ + status: TenantBillingStatus + count: number + amount: number + }> + /** 收入趋势(按日期) */ + revenueTrend?: Array<{ + date: string + amount: number + count: number + }> + /** 支付方式占比 */ + paymentMethodDistribution?: Array<{ + method: TenantPaymentMethod + count: number + amount: number + }> + /** 租户欠款排行(Top 10) */ + topDebtors?: Array<{ + tenantId: string + tenantName: string + overdueAmount: number + overdueDays: number + }> + } + + /** 账单导出数据 DTO */ + interface BillingExportDto { + statementNo: string + tenantName: string + billingType: string + periodStart: string + periodEnd: string + amountDue: number + amountPaid: number + status: string + dueDate: string + createdAt: string + } + + // ==================== 查询参数 ==================== + + /** 账单列表查询参数 */ + interface BillingListParams { + /** 页码(从 1 开始) */ + PageNumber?: number + /** 页大小 */ + PageSize?: number + /** 租户 ID */ + TenantId?: string + /** 账单状态 */ + Status?: TenantBillingStatus + /** 账单类型 */ + BillingType?: BillingType + /** 开始日期(账单创建时间或到期日) */ + StartDate?: string + /** 结束日期 */ + EndDate?: string + /** 最小金额 */ + MinAmount?: number + /** 最大金额 */ + MaxAmount?: number + /** 搜索关键词(账单号、租户名) */ + Keyword?: string + /** 排序字段(CreatedAt, DueDate, AmountDue) */ + SortBy?: string + /** 是否降序 */ + SortDesc?: boolean + } + + /** 账单统计查询参数 */ + interface BillingStatisticsParams { + /** 租户 ID(可选,不传则统计全部) */ + TenantId?: string + /** 开始日期 */ + StartDate: string + /** 结束日期 */ + EndDate: string + /** 分组方式(Day, Week, Month) */ + GroupBy?: 'Day' | 'Week' | 'Month' + } + + /** 导出参数 */ + interface ExportParams { + /** 导出格式 */ + Format: ExportFormat + /** 账单 ID 列表(可选:按选中导出) */ + BillingIds?: string[] + /** 租户 ID(可选:按筛选条件导出) */ + TenantId?: string + /** 状态(可选) */ + Status?: TenantBillingStatus + /** 开始日期(可选) */ + StartDate?: string + /** 结束日期(可选) */ + EndDate?: string + /** 关键词(可选) */ + Keyword?: string + } + + // ==================== 命令参数 ==================== + + /** 创建账单命令 */ + interface CreateBillingCommand { + /** 租户 ID */ + tenantId: string + /** 账单类型 */ + billingType: BillingType + /** 应付金额 */ + amountDue: number + /** 到期日期 */ + dueDate: string + /** 账单明细 */ + lineItems: Array<{ + itemType: string + description: string + quantity: number + unitPrice: number + amount: number + discountRate?: number + }> + /** 备注 */ + notes?: string + } + + /** 更新账单状态命令 */ + interface UpdateStatusCommand { + /** 新状态 */ + status: TenantBillingStatus + /** 备注 */ + notes?: string + } + + /** 记录支付命令 */ + interface RecordPaymentCommand { + /** 支付金额 */ + amount: number + /** 支付方式 */ + method: TenantPaymentMethod + /** 交易号 */ + transactionNo?: string + /** 支付凭证 URL */ + proofUrl?: string + /** 备注 */ + notes?: string + } + + /** 审核支付命令 */ + interface VerifyPaymentCommand { + /** 审核通过 */ + approved: boolean + /** 审核备注 */ + notes?: string + } + + /** 取消账单命令 */ + interface CancelBillingCommand { + /** 取消原因 */ + reason: string + } + + /** 批量更新状态命令 */ + interface BatchUpdateStatusCommand { + /** 账单 ID 列表 */ + billingIds: string[] + /** 新状态 */ + newStatus: TenantBillingStatus + /** 备注 */ + notes?: string + } + + // ==================== 响应类型 ==================== + + /** 账单列表响应 */ + type BillingListResponse = Common.PageResult + + /** 支付记录列表响应 */ + type PaymentRecordListResponse = PaymentRecordDto[] + + // ==================== 兼容别名(适配现有页面/历史命名) ==================== + + /** 账单列表项(兼容旧命名) */ + type BillDto = BillingListDto + /** 账单详情(兼容旧命名) */ + type BillDetailDto = BillingDetailDto + /** 账单列表查询参数(兼容旧命名) */ + type BillListParams = BillingListParams + /** 账单列表响应(兼容旧命名) */ + type BillListResponse = BillingListResponse + /** 支付记录(兼容旧命名) */ + type PaymentDto = PaymentRecordDto + /** 创建账单命令(兼容旧命名) */ + type CreateBillCommand = CreateBillingCommand + /** 更新账单状态命令(兼容旧命名) */ + type UpdateBillStatusCommand = UpdateStatusCommand + } +} diff --git a/src/types/api/common.d.ts b/src/types/api/common.d.ts new file mode 100644 index 0000000..d156d96 --- /dev/null +++ b/src/types/api/common.d.ts @@ -0,0 +1,43 @@ +declare namespace Api { + /** 通用类型 */ + namespace Common { + /** 分页参数 */ + interface PaginationParams { + /** 当前页码 */ + current: number + /** 每页条数 */ + size: number + /** 总条数 */ + total: number + } + + /** 分页查询参数 */ + interface PageParams { + Page?: number + PageSize?: number + } + + /** 通用搜索参数 */ + type CommonSearchParams = Pick + + /** 分页响应基础结构 */ + interface PaginatedResponse { + records: T[] + current: number + size: number + total: number + } + + /** 后端标准分页结果 */ + interface PageResult { + items: T[] + page: number + pageSize: number + totalCount: number + totalPages: number + } + + /** 启用状态 */ + type EnableStatus = '1' | '2' + } +} diff --git a/src/types/api/dictionary.d.ts b/src/types/api/dictionary.d.ts new file mode 100644 index 0000000..62d863d --- /dev/null +++ b/src/types/api/dictionary.d.ts @@ -0,0 +1,215 @@ +declare namespace Api { + /** 字典管理类型 */ + namespace Dictionary { + /** 字典作用域 */ + enum DictionaryScope { + System = 1, + Business = 2 + } + + /** 导入冲突处理模式 */ + enum ConflictResolutionMode { + Skip = 1, + Overwrite = 2, + Append = 3 + } + + /** 缓存失效操作类型 */ + enum CacheInvalidationOperation { + Create = 1, + Update = 2, + Delete = 3 + } + + /** 字典分组 */ + interface DictionaryGroupDto { + id: string + tenantId: string + code: string + name: string + scope: DictionaryScope + allowOverride: boolean + isEnabled: boolean + description?: string | null + createdAt: string + updatedAt?: string | null + rowVersion: string + items?: DictionaryItemDto[] + } + + /** 字典项 */ + interface DictionaryItemDto { + id: string + groupId: string + key: string + value: Record + isDefault: boolean + isEnabled: boolean + sortOrder: number + description?: string | null + source: 'system' | 'tenant' + rowVersion: string + } + + /** 覆盖配置 */ + interface OverrideConfigDto { + tenantId: string + systemDictionaryGroupCode: string + overrideEnabled: boolean + hiddenSystemItemIds: string[] + customSortOrder: Record + } + + /** 字典导入错误 */ + interface DictionaryImportError { + rowNumber: number + field: string + message: string + } + + /** 字典导入结果 */ + interface DictionaryImportResultDto { + successCount: number + skipCount: number + errorCount: number + errors: DictionaryImportError[] + duration: string + } + + /** 缓存失效日志 */ + interface CacheInvalidationLogDto { + id: string + tenantId: string + timestamp: string + dictionaryCode: string + scope: DictionaryScope + affectedCacheKeyCount: number + operatorId: string + operation: CacheInvalidationOperation + } + + /** 字典分组查询参数 */ + interface DictionaryGroupQueryParams extends Api.Common.PageParams { + scope?: DictionaryScope + keyword?: string + isEnabled?: boolean + sortBy?: string + sortOrder?: 'asc' | 'desc' + includeItems?: boolean + } + + /** 创建字典分组 */ + interface CreateDictionaryGroupRequest { + code: string + name: string + scope: DictionaryScope + allowOverride: boolean + description?: string | null + } + + /** 更新字典分组 */ + interface UpdateDictionaryGroupRequest { + name: string + description?: string | null + allowOverride: boolean + isEnabled: boolean + rowVersion: string + } + + /** 创建字典项 */ + interface CreateDictionaryItemRequest { + key: string + value: Record + isDefault?: boolean + isEnabled?: boolean + sortOrder?: number + description?: string | null + } + + /** 更新字典项 */ + interface UpdateDictionaryItemRequest { + key: string + value: Record + isDefault?: boolean + isEnabled?: boolean + sortOrder?: number + description?: string | null + rowVersion: string + } + + /** 导出请求 */ + interface DictionaryExportRequest { + format?: 'csv' | 'json' + } + + /** 覆盖隐藏项请求 */ + interface DictionaryOverrideHiddenItemsRequest { + hiddenItemIds: string[] + } + + /** 覆盖排序请求 */ + interface DictionaryOverrideSortOrderRequest { + sortOrder: Record + } + + /** 批量查询请求 */ + interface DictionaryBatchQueryRequest { + codes: string[] + } + + // ==================== 标签覆盖相关类型 ==================== + + /** 覆盖类型枚举 */ + enum OverrideType { + /** 租户定制(租户覆盖系统字典) */ + TenantCustomization = 1, + /** 平台强制(平台覆盖租户字典) */ + PlatformEnforcement = 2 + } + + /** 标签覆盖 DTO */ + interface LabelOverrideDto { + /** 覆盖记录 ID */ + id: string + /** 租户 ID */ + tenantId: string + /** 被覆盖的字典项 ID */ + dictionaryItemId: string + /** 字典项 Key */ + dictionaryItemKey: string + /** 原始显示值(多语言) */ + originalValue: Record + /** 覆盖后的显示值(多语言) */ + overrideValue: Record + /** 覆盖类型 */ + overrideType: OverrideType + /** 覆盖类型名称 */ + overrideTypeName: string + /** 覆盖原因/备注 */ + reason?: string | null + /** 创建时间 */ + createdAt: string + /** 更新时间 */ + updatedAt?: string | null + /** 创建人 ID */ + createdBy?: string | null + /** 更新人 ID */ + updatedBy?: string | null + } + + /** 创建/更新标签覆盖请求 */ + interface UpsertLabelOverrideRequest { + /** 被覆盖的字典项 ID */ + dictionaryItemId: string + /** 覆盖后的显示值(多语言) */ + overrideValue: Record + /** 覆盖原因/备注(平台强制覆盖时建议填写) */ + reason?: string | null + } + + /** 批量标签覆盖请求 */ + interface BatchLabelOverrideRequest { + items: UpsertLabelOverrideRequest[] + } + } +} diff --git a/src/types/api/files.d.ts b/src/types/api/files.d.ts new file mode 100644 index 0000000..f7a10ea --- /dev/null +++ b/src/types/api/files.d.ts @@ -0,0 +1,23 @@ +declare namespace Api { + /** 文件相关类型 */ + namespace Files { + /** 上传类型(后端 UploadFileType 枚举字符串) */ + type UploadFileType = + | 'dish_image' + | 'merchant_logo' + | 'user_avatar' + | 'review_image' + | 'business_license' + | 'other' + + /** 上传文件结果 */ + interface FileUploadResponse { + /** 上传后的可访问地址 */ + url?: string | null + /** 文件名 */ + fileName?: string | null + /** 文件大小(字节) */ + fileSize: number + } + } +} diff --git a/src/types/api/merchant.d.ts b/src/types/api/merchant.d.ts new file mode 100644 index 0000000..2829d60 --- /dev/null +++ b/src/types/api/merchant.d.ts @@ -0,0 +1,187 @@ +declare namespace Api { + /** 商户管理类型 */ + namespace Merchant { + /** 商户状态枚举 */ + enum MerchantStatus { + Pending = 0, + Approved = 1, + Rejected = 2, + Frozen = 3 + } + + /** 经营模式枚举 */ + enum OperatingMode { + SameEntity = 1, + DifferentEntity = 2 + } + + /** 门店状态枚举 */ + enum StoreStatus { + Closed = 0, + Preparing = 1, + Operating = 2, + Suspended = 3 + } + + /** 审核动作枚举 */ + enum ReviewAction { + ApplicationSubmitted = 0, + DocumentUploaded = 1, + DocumentReviewed = 2, + ContractUpdated = 3, + ContractStatusChanged = 4, + MerchantReviewed = 5, + ReviewClaimed = 6, + ReviewReleased = 7, + ReviewApproved = 8, + ReviewRejected = 9, + ReviewRevoked = 10, + ReviewPendingReApproval = 11, + ReviewForceClaimed = 12 + } + + /** 商户列表项 */ + interface MerchantListItem { + id: string // long -> string + tenantId: string // long -> string + tenantName?: string + name: string + operatingMode?: OperatingMode + licenseNumber?: string + status: MerchantStatus + isFrozen: boolean + storeCount?: number + createdAt: string + updatedAt?: string + } + + /** 商户审核列表项 */ + interface MerchantReviewListItem { + id: string // long -> string + tenantId: string // long -> string + tenantName?: string + name: string + operatingMode?: OperatingMode + licenseNumber?: string + status: MerchantStatus + claimedByName?: string + claimedAt?: string | null + claimExpiresAt?: string | null + createdAt: string + } + + /** 商户列表查询参数 */ + interface MerchantListParams extends Common.PageParams { + keyword?: string + status?: MerchantStatus + operatingMode?: OperatingMode + tenantId?: string // long -> string + sortBy?: string + sortOrder?: 'asc' | 'desc' + } + + /** 商户列表响应 */ + type MerchantListResponse = Common.PageResult + + /** 商户审核列表响应 */ + type MerchantReviewListResponse = Common.PageResult + + /** 门店列表项 */ + interface StoreListItem { + id: string // long -> string + name: string + licenseNumber?: string | null + contactPhone?: string + address?: string + status: StoreStatus + } + + /** 商户详情 */ + interface MerchantDetail { + id: string // long -> string + tenantId: string // long -> string + tenantName?: string + name: string + operatingMode?: OperatingMode + licenseNumber?: string + legalRepresentative?: string + registeredAddress?: string + contactPhone?: string + contactEmail?: string + status: MerchantStatus + isFrozen: boolean + frozenReason?: string | null + frozenAt?: string | null + approvedBy?: string | null // long -> string + approvedAt?: string | null + stores?: StoreListItem[] + rowVersion?: string + createdAt?: string + createdBy?: string | null // long -> string + updatedAt?: string | null + updatedBy?: string | null // long -> string + } + + /** 更新商户请求 */ + interface UpdateMerchantRequest { + name?: string + licenseNumber?: string + legalRepresentative?: string + registeredAddress?: string + contactPhone?: string + contactEmail?: string + rowVersion: string + } + + /** 更新商户响应 */ + interface UpdateMerchantResult { + merchant: MerchantDetail + requiresReview: boolean + } + + /** 审核领取信息 */ + interface ClaimInfo { + merchantId: string // long -> string + claimedBy?: string // long -> string + claimedByName?: string + claimedAt?: string | null + claimExpiresAt?: string | null + } + + /** 审核记录 */ + interface MerchantAuditRecord { + id: string // long -> string + merchantId?: string // long -> string + action: ReviewAction + operatorId?: string | null // long -> string + operatorName?: string | null + ipAddress?: string | null + title?: string + description?: string | null + createdAt: string + } + + /** 变更记录 */ + interface MerchantChangeLogRecord { + id: string // long -> string + fieldName: string + oldValue?: string | null + newValue?: string | null + changedBy?: string | null // long -> string + changedByName?: string | null + changedAt: string + changeReason?: string | null + } + + /** 审核请求 */ + interface ReviewMerchantRequest { + approve: boolean + remarks?: string + } + + /** 撤销审核请求 */ + interface RevokeMerchantRequest { + reason: string + } + } +} diff --git a/src/types/api/permission.d.ts b/src/types/api/permission.d.ts new file mode 100644 index 0000000..061ba02 --- /dev/null +++ b/src/types/api/permission.d.ts @@ -0,0 +1,25 @@ +declare namespace Api { + namespace Permission { + /** 权限 DTO */ + interface PermissionDto { + id: string + parentId?: string | null + tenantId: string + name: string | null + code: string | null + type?: 'group' | 'leaf' | string + description: string | null + children?: PermissionDto[] + } + + /** 查询参数 */ + interface PermissionQueryParams extends Api.Common.PageParams { + Keyword?: string + SortBy?: string + SortDescending?: boolean + } + + /** 分页响应 */ + type PermissionPageResult = Api.Common.PageResult + } +} diff --git a/src/types/api/role-template.d.ts b/src/types/api/role-template.d.ts new file mode 100644 index 0000000..d0c70e8 --- /dev/null +++ b/src/types/api/role-template.d.ts @@ -0,0 +1,53 @@ +declare namespace Api { + /** 角色模板类型 */ + namespace RoleTemplate { + /** 权限模板 DTO */ + interface PermissionTemplateDto { + code: string + name: string + description?: string + } + + /** 角色模板 DTO */ + interface RoleTemplateDto { + templateCode: string + name: string + description?: string + isActive: boolean + permissions: PermissionTemplateDto[] + } + + /** 创建角色模板参数 */ + interface CreateRoleTemplateCommand { + templateCode: string + name: string + description?: string + isActive: boolean + permissionCodes?: string[] + } + + /** 更新角色模板参数 */ + interface UpdateRoleTemplateCommand { + templateCode?: string + name?: string + description?: string + isActive?: boolean + permissionCodes?: string[] + } + + /** 克隆角色模板参数 */ + interface CloneRoleTemplateCommand { + sourceTemplateCode?: string + newTemplateCode?: string + name?: string + description?: string + isActive?: boolean + permissionCodes?: string[] + } + + /** 初始化角色模板参数 */ + interface InitializeRoleTemplatesCommand { + templateCodes?: string[] + } + } +} diff --git a/src/types/api/statistics.d.ts b/src/types/api/statistics.d.ts new file mode 100644 index 0000000..28e5c52 --- /dev/null +++ b/src/types/api/statistics.d.ts @@ -0,0 +1,144 @@ +/** + * 统计相关类型定义 + */ +declare namespace Api.Statistics { + /** + * 订阅概览统计 + */ + interface SubscriptionOverview { + /** 总订阅数 */ + totalSubscriptions: number + /** 活跃订阅数 */ + activeSubscriptions: number + /** 7天内到期 */ + expiringIn7Days: number + /** 3天内到期 */ + expiringIn3Days: number + /** 明天到期 */ + expiringIn1Day: number + /** 已过期 */ + expiredSubscriptions: number + /** 待激活 */ + pendingSubscriptions: number + /** 已暂停 */ + suspendedSubscriptions: number + } + + /** + * 配额使用排行项 + */ + interface QuotaUsageRankingItem { + /** 租户ID */ + tenantId: string + /** 租户名称 */ + tenantName: string + /** 配额类型 */ + quotaType: number + /** 配额名称 */ + quotaName: string + /** 当前使用量 */ + currentUsage: number + /** 配额上限 */ + quotaLimit: number + /** 使用率(百分比) */ + usagePercentage: number + } + + /** + * 配额使用排行请求参数 + */ + interface QuotaUsageRankingParams { + /** 配额类型(可选) */ + quotaType?: number + /** 返回数量限制 */ + limit?: number + } + + /** + * 配额使用排行响应 + */ + interface QuotaUsageRankingResponse { + /** 排行列表 */ + items: QuotaUsageRankingItem[] + } + + /** + * 月度收入统计项 + */ + interface MonthlyRevenueItem { + /** 年份 */ + year: number + /** 月份 */ + month: number + /** 月度收入 */ + revenue: number + } + + /** + * 收入统计请求参数 + */ + interface RevenueStatisticsParams { + /** 年份(可选,默认当前年) */ + year?: number + } + + /** + * 收入统计响应 + */ + interface RevenueStatisticsResponse { + /** 总收入 */ + totalRevenue: number + /** 本月收入 */ + currentMonthRevenue: number + /** 本季度收入 */ + currentQuarterRevenue: number + /** 月度收入列表(最近6个月) */ + monthlyRevenues: MonthlyRevenueItem[] + } + + /** + * 即将到期订阅项 + */ + interface ExpiringSubscriptionItem { + /** 订阅ID */ + subscriptionId: string + /** 租户ID */ + tenantId: string + /** 租户名称 */ + tenantName: string + /** 套餐名称 */ + packageName: string + /** 到期日期 */ + expireDate: string + /** 距离到期天数 */ + daysUntilExpiry: number + /** 订阅状态 */ + status: number + } + + /** + * 即将到期订阅请求参数 + */ + interface ExpiringSubscriptionsParams { + /** 天数范围(默认7天) */ + days?: number + /** 页码 */ + page?: number + /** 每页数量 */ + pageSize?: number + } + + /** + * 即将到期订阅响应 + */ + interface ExpiringSubscriptionsResponse { + /** 订阅列表 */ + items: ExpiringSubscriptionItem[] + /** 总数 */ + totalCount: number + /** 当前页 */ + page: number + /** 每页数量 */ + pageSize: number + } +} diff --git a/src/types/api/store.d.ts b/src/types/api/store.d.ts new file mode 100644 index 0000000..0bfcb8a --- /dev/null +++ b/src/types/api/store.d.ts @@ -0,0 +1,609 @@ +declare namespace Api { + /** 门店管理类型 */ + namespace Store { + /** 门店状态枚举 */ + enum StoreStatus { + Closed = 0, + Preparing = 1, + Operating = 2, + Suspended = 3 + } + + /** 门店审核状态枚举 */ + enum StoreAuditStatus { + Draft = 0, + Pending = 1, + Activated = 2, + Rejected = 3 + } + + /** 门店经营状态枚举 */ + enum StoreBusinessStatus { + Open = 0, + Resting = 1, + ForceClosed = 2 + } + + /** 门店主体类型枚举 */ + enum StoreOwnershipType { + SameEntity = 0, + DifferentEntity = 1 + } + + /** 门店歇业原因枚举 */ + enum StoreClosureReason { + OutOfBusinessHours = 0, + EquipmentMaintenance = 1, + OwnerVacation = 2, + OutOfStock = 3, + TemporarilyClosed = 4, + LicenseExpired = 5, + Other = 99 + } + + /** 打包费模式枚举 */ + enum PackagingFeeMode { + Fixed = 0, + PerItem = 1 + } + + /** 门店资质类型枚举 */ + enum StoreQualificationType { + BusinessLicense = 0, + FoodServiceLicense = 1, + StorefrontPhoto = 2, + InteriorPhoto = 3 + } + + /** 门店审核动作枚举 */ + enum StoreAuditAction { + Submit = 0, + Resubmit = 1, + Approve = 2, + Reject = 3, + ForceClose = 4, + Reopen = 5, + AutoActivate = 6 + } + + /** 营业时段类型枚举 */ + enum BusinessHourType { + Normal = 0, + ReservationOnly = 1, + PickupOrDelivery = 2, + Closed = 3 + } + + /** 临时时段覆盖类型枚举 */ + enum OverrideType { + Closed = 0, + TemporaryOpen = 1, + ModifiedHours = 2 + } + + /** 门店信息 */ + interface StoreDto { + id: string + tenantId: string + merchantId: string + code: string + name: string + phone?: string | null + managerName?: string | null + status: StoreStatus + signboardImageUrl?: string | null + ownershipType: StoreOwnershipType + auditStatus: StoreAuditStatus + businessStatus: StoreBusinessStatus + closureReason?: StoreClosureReason | null + closureReasonText?: string | null + categoryId?: string | null + rejectionReason?: string | null + submittedAt?: string | null + activatedAt?: string | null + forceClosedAt?: string | null + forceCloseReason?: string | null + province?: string | null + city?: string | null + district?: string | null + address?: string | null + longitude?: number | null + latitude?: number | null + announcement?: string | null + tags?: string | null + deliveryRadiusKm: number + supportsDineIn: boolean + supportsPickup: boolean + supportsDelivery: boolean + supportsReservation: boolean + supportsQueueing: boolean + createdAt: string + } + + /** 门店列表查询参数 */ + interface StoreListParams extends Api.Common.PageParams { + merchantId?: string + status?: StoreStatus + auditStatus?: StoreAuditStatus + businessStatus?: StoreBusinessStatus + ownershipType?: StoreOwnershipType + keyword?: string + sortBy?: string + sortDesc?: boolean + } + + /** 门店列表响应 */ + type StoreListResponse = Api.Common.PageResult + + /** 创建门店请求 */ + interface CreateStoreRequest { + merchantId: string + code: string + name: string + phone?: string | null + managerName?: string | null + status?: StoreStatus + signboardImageUrl?: string | null + ownershipType: StoreOwnershipType + categoryId?: string | null + province?: string | null + city?: string | null + district?: string | null + address?: string | null + longitude?: number | null + latitude?: number | null + announcement?: string | null + tags?: string | null + deliveryRadiusKm: number + supportsDineIn?: boolean + supportsPickup?: boolean + supportsDelivery?: boolean + supportsReservation?: boolean + supportsQueueing?: boolean + } + + /** 更新门店请求 */ + interface UpdateStoreRequest { + merchantId: string + code: string + name: string + phone?: string | null + managerName?: string | null + status?: StoreStatus + signboardImageUrl?: string | null + categoryId?: string | null + province?: string | null + city?: string | null + district?: string | null + address?: string | null + longitude?: number | null + latitude?: number | null + announcement?: string | null + tags?: string | null + deliveryRadiusKm: number + supportsDineIn?: boolean + supportsPickup?: boolean + supportsDelivery?: boolean + supportsReservation?: boolean + supportsQueueing?: boolean + } + + /** 切换经营状态请求 */ + interface ToggleBusinessStatusRequest { + businessStatus: StoreBusinessStatus + closureReason?: StoreClosureReason | null + closureReasonText?: string | null + } + + /** 门店资质 */ + interface StoreQualificationDto { + id: string + storeId: string + qualificationType: StoreQualificationType + fileUrl: string + documentNumber?: string | null + issuedAt?: string | null + expiresAt?: string | null + isExpired: boolean + isExpiringSoon: boolean + daysUntilExpiry?: number | null + sortOrder: number + createdAt: string + updatedAt?: string | null + } + + /** 资质完整性项 */ + interface StoreQualificationRequirementDto { + qualificationType: StoreQualificationType + isRequired: boolean + isUploaded: boolean + isValid: boolean + uploadedCount: number + } + + /** 资质完整性检查结果 */ + interface StoreQualificationCheckResultDto { + isComplete: boolean + canSubmitAudit: boolean + requiredTypes: StoreQualificationRequirementDto[] + expiringSoonCount: number + expiredCount: number + missingTypes: string[] + warnings: string[] + } + + /** 创建资质请求 */ + interface CreateStoreQualificationRequest { + qualificationType: StoreQualificationType + fileUrl: string + documentNumber?: string | null + issuedAt?: string | null + expiresAt?: string | null + sortOrder?: number + } + + /** 更新资质请求 */ + interface UpdateStoreQualificationRequest { + fileUrl?: string | null + documentNumber?: string | null + issuedAt?: string | null + expiresAt?: string | null + sortOrder?: number | null + } + + /** 营业时段 */ + interface StoreBusinessHourDto { + id: string + tenantId: string + storeId: string + dayOfWeek: number + hourType: BusinessHourType + startTime: string + endTime: string + capacityLimit?: number | null + notes?: string | null + createdAt: string + } + + /** 批量更新营业时段输入项 */ + interface StoreBusinessHourInputDto { + dayOfWeek: number + hourType: BusinessHourType + startTime: string + endTime: string + capacityLimit?: number | null + notes?: string | null + } + + /** 批量更新营业时段请求 */ + interface BatchUpdateBusinessHoursRequest { + items: StoreBusinessHourInputDto[] + } + + /** 门店临时时段 */ + interface StoreHolidayDto { + id: string + tenantId: string + storeId: string + date: string + endDate?: string + isAllDay: boolean + startTime?: string + endTime?: string + overrideType: OverrideType + isClosed: boolean + reason?: string + createdAt: string + } + + /** 创建临时时段请求 */ + interface CreateStoreHolidayRequest { + date: string + endDate?: string + isAllDay: boolean + startTime?: string + endTime?: string + overrideType: OverrideType + reason?: string + } + + /** 更新临时时段请求 */ + type UpdateStoreHolidayRequest = CreateStoreHolidayRequest + + /** 配送区域 */ + interface StoreDeliveryZoneDto { + id: string + tenantId: string + storeId: string + zoneName: string + polygonGeoJson: string + minimumOrderAmount?: number | null + deliveryFee?: number | null + estimatedMinutes?: number | null + sortOrder: number + createdAt: string + } + + /** 创建配送区域请求 */ + interface CreateStoreDeliveryZoneRequest { + zoneName: string + polygonGeoJson: string + minimumOrderAmount?: number | null + deliveryFee?: number | null + estimatedMinutes?: number | null + sortOrder?: number + } + + /** 更新配送区域请求 */ + interface UpdateStoreDeliveryZoneRequest { + zoneName: string + polygonGeoJson: string + minimumOrderAmount?: number | null + deliveryFee?: number | null + estimatedMinutes?: number | null + sortOrder?: number + } + + /** 配送范围检测请求 */ + interface StoreDeliveryCheckRequest { + longitude: number + latitude: number + } + + /** 配送范围检测结果 */ + interface StoreDeliveryCheckResultDto { + inRange: boolean + distance?: number | null + deliveryZoneId?: string | null + deliveryZoneName?: string | null + } + + /** 门店费用配置 */ + interface StoreFeeDto { + id: string + storeId: string + minimumOrderAmount: number + deliveryFee: number + packagingFeeMode: PackagingFeeMode + fixedPackagingFee: number + freeDeliveryThreshold?: number | null + createdAt: string + updatedAt?: string | null + } + + /** 更新费用请求 */ + interface UpdateStoreFeeRequest { + minimumOrderAmount: number + deliveryFee: number + packagingFeeMode: PackagingFeeMode + fixedPackagingFee?: number | null + freeDeliveryThreshold?: number | null + } + + /** 费用计算请求 */ + interface CalculateStoreFeeRequest { + orderAmount: number + itemCount?: number | null + items?: StoreFeeCalculationItemDto[] + } + + /** 费用计算商品项 */ + interface StoreFeeCalculationItemDto { + skuId: string + quantity: number + packagingFee: number + } + + /** 打包费拆分明细 */ + interface StoreFeeCalculationBreakdownDto { + skuId: string + quantity: number + unitFee: number + subtotal: number + } + + /** 费用计算结果 */ + interface StoreFeeCalculationResultDto { + orderAmount: number + minimumOrderAmount: number + meetsMinimum: boolean + shortfall?: number | null + deliveryFee: number + packagingFee: number + packagingFeeMode: PackagingFeeMode + packagingFeeBreakdown?: StoreFeeCalculationBreakdownDto[] | null + totalFee: number + totalAmount: number + message?: string | null + } + + /** 资质预警查询参数 */ + interface StoreQualificationAlertQueryParams extends Api.Common.PageParams { + daysThreshold?: number + tenantId?: string + expired?: boolean + } + + /** 资质预警项 */ + interface StoreQualificationAlertDto { + qualificationId: string + storeId: string + storeName: string + storeCode: string + tenantId: string + tenantName: string + qualificationType: StoreQualificationType + expiresAt?: string | null + daysUntilExpiry?: number | null + isExpired: boolean + storeBusinessStatus: StoreBusinessStatus + } + + /** 资质预警汇总 */ + interface StoreQualificationAlertSummaryDto { + expiringSoonCount: number + expiredCount: number + } + + /** 资质预警分页结果 */ + interface StoreQualificationAlertResultDto + extends Api.Common.PageResult { + summary: StoreQualificationAlertSummaryDto + } + } + + /** 门店审核管理类型 */ + namespace StoreAudit { + /** 待审核门店 */ + interface PendingStoreAuditDto { + storeId: string + storeName: string + storeCode: string + tenantId: string + tenantName: string + merchantId: string + merchantName: string + signboardImageUrl?: string | null + fullAddress: string + ownershipType: Api.Store.StoreOwnershipType + submittedAt?: string | null + waitingDays: number + isOverdue: boolean + qualificationCount: number + } + + /** 门店审核详情 - 门店信息 */ + interface StoreAuditStoreDto { + id: string + name: string + code: string + phone?: string | null + signboardImageUrl?: string | null + province?: string | null + city?: string | null + district?: string | null + address?: string | null + longitude?: number | null + latitude?: number | null + ownershipType: Api.Store.StoreOwnershipType + auditStatus: Api.Store.StoreAuditStatus + submittedAt?: string | null + } + + /** 门店审核详情 - 租户信息 */ + interface StoreAuditTenantDto { + id: string + name: string + contactName?: string | null + contactPhone?: string | null + } + + /** 门店审核详情 - 商户信息 */ + interface StoreAuditMerchantDto { + id: string + name: string + legalName?: string | null + creditCode?: string | null + } + + /** 审核记录 */ + interface StoreAuditRecordDto { + id: string + action: Api.Store.StoreAuditAction + actionName: string + operatorId?: string | null + operatorName: string + previousStatus?: Api.Store.StoreAuditStatus | null + newStatus: Api.Store.StoreAuditStatus + rejectionReasonId?: string | null + rejectionReasonText?: string | null + remark?: string | null + createdAt: string + } + + /** 门店审核详情 */ + interface StoreAuditDetailDto { + store: StoreAuditStoreDto + tenant: StoreAuditTenantDto + merchant: StoreAuditMerchantDto + qualifications: Api.Store.StoreQualificationDto[] + auditHistory: StoreAuditRecordDto[] + } + + /** 审核统计趋势项 */ + interface StoreAuditDailyTrendDto { + date: string + submitted: number + approved: number + rejected: number + } + + /** 审核统计 */ + interface StoreAuditStatisticsDto { + pendingCount: number + overdueCount: number + approvedCount: number + rejectedCount: number + avgProcessingHours: number + dailyTrend: StoreAuditDailyTrendDto[] + } + + /** 审核/风控操作结果 */ + interface StoreAuditActionResultDto { + storeId: string + auditStatus: Api.Store.StoreAuditStatus + businessStatus: Api.Store.StoreBusinessStatus + rejectionReason?: string | null + message?: string | null + } + + /** 待审核列表查询参数 */ + interface ListPendingStoreAuditsParams extends Api.Common.PageParams { + tenantId?: string + keyword?: string + submittedFrom?: string + submittedTo?: string + overdueOnly?: boolean + sortBy?: string + sortDesc?: boolean + } + + /** 待审核门店分页响应 */ + type PendingStoreAuditResponse = Api.Common.PageResult + + /** 审核记录查询参数 */ + interface ListStoreAuditRecordsParams { + page?: number + pageSize?: number + } + + /** 审核统计查询参数 */ + interface StoreAuditStatisticsParams { + dateFrom?: string + dateTo?: string + } + + /** 审核通过请求 */ + interface ApproveStoreAuditRequest { + remark?: string | null + } + + /** 审核驳回请求 */ + interface RejectStoreAuditRequest { + rejectionReasonId: number + rejectionReasonText?: string | null + remark?: string | null + } + + /** 强制关闭请求 */ + interface ForceCloseStoreRequest { + reason: string + remark?: string | null + } + + /** 解除关闭请求 */ + interface ReopenStoreRequest { + remark?: string | null + } + } +} diff --git a/src/types/api/subscription.d.ts b/src/types/api/subscription.d.ts new file mode 100644 index 0000000..7f857ab --- /dev/null +++ b/src/types/api/subscription.d.ts @@ -0,0 +1,194 @@ +/** + * 订阅管理相关类型定义 + */ +declare namespace Api { + namespace Subscription { + /** 订阅状态枚举 */ + enum SubscriptionStatus { + /** 待激活 */ + Pending = 0, + /** 生效中 */ + Active = 1, + /** 宽限期 */ + GracePeriod = 2, + /** 已取消 */ + Cancelled = 3, + /** 已暂停 */ + Suspended = 4 + } + + /** 订阅列表查询参数 */ + interface SubscriptionListParams extends Common.PageParams { + /** 订阅状态 */ + Status?: SubscriptionStatus + /** 套餐 ID */ + TenantPackageId?: string + /** 租户 ID */ + TenantId?: string + /** 租户关键词(名称或编码模糊匹配) */ + TenantKeyword?: string + /** 到期时间筛选:未来 N 天内到期 */ + ExpiringWithinDays?: number + /** 是否自动续费筛选 */ + AutoRenew?: boolean + /** 到期时间范围开始 */ + ExpireFrom?: string + /** 到期时间范围结束 */ + ExpireTo?: string + } + + /** 订阅列表响应 */ + type SubscriptionListResponse = Common.PageResult + + /** 订阅列表项 DTO */ + interface SubscriptionListDto { + /** 订阅 ID */ + id: string + /** 租户 ID */ + tenantId: string + /** 租户名称 */ + tenantName: string + /** 租户编码 */ + tenantCode: string + /** 当前套餐 ID */ + tenantPackageId: string + /** 当前套餐名称 */ + packageName: string + /** 当前套餐名称(别名,兼容旧代码) */ + tenantPackageName?: string + /** 排期套餐 ID(下周期生效) */ + scheduledPackageId?: string + /** 排期套餐名称 */ + scheduledPackageName?: string + /** 订阅状态 */ + status: SubscriptionStatus + /** 生效时间(UTC) */ + effectiveFrom: string + /** 到期时间(UTC) */ + effectiveTo: string + /** 下次计费时间 */ + nextBillingDate?: string + /** 是否自动续费 */ + autoRenew: boolean + /** 备注信息 */ + notes?: string + /** 创建时间 */ + createdAt: string + /** 更新时间 */ + updatedAt?: string + } + + /** 简化订阅 DTO(用于操作后返回) */ + type SubscriptionDto = SubscriptionListDto + + /** 订阅详情 DTO */ + interface SubscriptionDetailDto extends SubscriptionListDto { + /** 当前套餐信息 */ + package?: TenantPackageDto + /** 排期套餐信息 */ + scheduledPackage?: TenantPackageDto + /** 配额使用情况列表 */ + quotaUsages: QuotaUsageDto[] + /** 配额使用情况列表(别名,兼容旧代码) */ + quotaUsage?: QuotaUsageDto[] + /** 订阅变更历史列表 */ + changeHistory: SubscriptionHistoryDto[] + } + + /** 租户套餐 DTO */ + interface TenantPackageDto { + id: string + name: string + description?: string + packageType: number + monthlyPrice?: number + yearlyPrice?: number + maxStoreCount?: number + maxAccountCount?: number + maxStorageGb?: number + maxSmsCredits?: number + maxDeliveryOrders?: number + featurePoliciesJson?: string + isActive: boolean + isPublicVisible: boolean + isAllowNewTenantPurchase: boolean + publishStatus: number + isRecommended: boolean + tags: string[] + sortOrder: number + } + + /** 配额使用情况 DTO */ + interface QuotaUsageDto { + quotaType: number + quotaName: string + limit?: number + used: number + remaining?: number + usagePercentage?: number + } + + /** 订阅历史 DTO */ + interface SubscriptionHistoryDto { + id: string + subscriptionId: string + changeType: string + previousPackageId?: string + previousPackageName?: string + newPackageId?: string + newPackageName?: string + previousEffectiveTo?: string + newEffectiveTo?: string + notes?: string + /** 描述(兼容旧代码) */ + description?: string + createdAt: string + /** 时间戳(兼容旧代码,等同于 createdAt) */ + timestamp?: string + createdBy?: string + } + + /** 更新订阅命令 */ + interface UpdateSubscriptionCommand { + subscriptionId: string + autoRenew?: boolean + notes?: string + } + + /** 延期订阅命令 */ + interface ExtendSubscriptionCommand { + subscriptionId: string + durationMonths: number + notes?: string + } + + /** 变更套餐命令 */ + interface ChangePlanCommand { + subscriptionId: string + targetPackageId: string + immediate: boolean + notes?: string + } + + /** 更新订阅状态命令 */ + interface UpdateStatusCommand { + subscriptionId: string + status: SubscriptionStatus + notes?: string + } + + /** 批量操作结果 */ + interface BatchOperationResult { + successCount: number + failedCount: number + results: BatchOperationItem[] + } + + /** 批量操作项结果 */ + interface BatchOperationItem { + subscriptionId: string + success: boolean + message?: string + } + } +} diff --git a/src/types/api/system-manage.d.ts b/src/types/api/system-manage.d.ts new file mode 100644 index 0000000..cde880b --- /dev/null +++ b/src/types/api/system-manage.d.ts @@ -0,0 +1,112 @@ +declare namespace Api { + /** 系统管理类型 */ + namespace SystemManage { + /** 用户状态 */ + type IdentityUserStatus = 1 | 2 | 3 + /** 批量用户操作类型 */ + type IdentityUserBatchOperation = 1 | 2 | 3 | 4 | 5 + /** 用户列表项 */ + interface UserListItem { + userId: string + tenantId: string + account: string + displayName: string + avatar?: string + phone?: string + email?: string + status: IdentityUserStatus + isLocked: boolean + isDeleted: boolean + roles: string[] + createdAt: string + lastLoginAt?: string + } + /** 用户详情 */ + interface UserDetailDto { + userId: string + tenantId: string + merchantId?: string + account: string + displayName: string + phone?: string + email?: string + status: IdentityUserStatus + isLocked: boolean + roles: string[] + roleIds: string[] + permissions: string[] + createdAt: string + lastLoginAt?: string + avatar?: string + rowVersion: string + } + /** 用户列表响应 */ + type UserListResponse = Api.Common.PageResult + /** 用户搜索参数 */ + interface UserSearchParams extends Api.Common.PageParams { + TenantId?: string + Keyword?: string + Status?: IdentityUserStatus + RoleId?: string + CreatedAtFrom?: string + CreatedAtTo?: string + LastLoginFrom?: string + LastLoginTo?: string + IncludeDeleted?: boolean + SortBy?: string + SortDescending?: boolean + } + /** 创建用户命令 */ + interface CreateIdentityUserCommand { + tenantId?: string + account: string + displayName: string + password: string + phone?: string + email?: string + avatar?: string + roleIds?: string[] + status?: IdentityUserStatus + } + /** 更新用户命令 */ + interface UpdateIdentityUserCommand { + userId?: string + tenantId?: string + displayName: string + phone?: string + email?: string + avatar?: string + roleIds?: string[] + rowVersion: string + } + /** 更新用户状态命令 */ + interface ChangeIdentityUserStatusCommand { + userId?: string + tenantId?: string + status: IdentityUserStatus + } + /** 重置密码结果 */ + interface ResetIdentityUserPasswordResult { + token: string + expiresAt: string + } + /** 批量操作失败项 */ + interface BatchIdentityUserFailureItem { + userId: string + reason: string + } + /** 批量用户操作命令 */ + interface BatchIdentityUserOperationCommand { + tenantId?: string + operation: IdentityUserBatchOperation + userIds: string[] + } + /** 批量用户操作结果 */ + interface BatchIdentityUserOperationResult { + successCount: number + failureCount: number + failures: BatchIdentityUserFailureItem[] + exportItems: UserListItem[] + } + } +} diff --git a/src/types/api/tenant-role.d.ts b/src/types/api/tenant-role.d.ts new file mode 100644 index 0000000..3e6538c --- /dev/null +++ b/src/types/api/tenant-role.d.ts @@ -0,0 +1,44 @@ +declare namespace Api { + /** 租户角色类型 */ + namespace TenantRole { + /** 角色 DTO */ + interface RoleDto { + id: string // long -> string + tenantId: string // long -> string + name: string + code: string + description?: string + } + + /** 角色分页结果 */ + interface RoleDtoPagedResult { + items: RoleDto[] + totalCount: number // Swagger usually returns totalCount or total? Need to check PagedResult schema + } + + /** 角色分页查询参数 */ + interface RoleQueryParams { + TenantId?: string + Keyword?: string + Page?: number + PageSize?: number + SortBy?: string + SortDescending?: boolean + } + + /** 创建角色参数 */ + interface CreateRoleCommand { + name: string + code: string + description?: string + copyFromTemplateId?: string + } + + /** 更新角色参数 */ + interface UpdateRoleCommand { + roleId: string // long -> string + name?: string + description?: string + } + } +} diff --git a/src/types/api/tenant.d.ts b/src/types/api/tenant.d.ts new file mode 100644 index 0000000..da0d8e6 --- /dev/null +++ b/src/types/api/tenant.d.ts @@ -0,0 +1,412 @@ +declare namespace Api { + /** 租户管理类型 */ + namespace Tenant { + /** 租户状态枚举 */ + enum TenantStatus { + PendingReview = 0, + Active = 1, + Suspended = 2, + Expired = 3, + Closed = 4 + } + + /** 租户认证状态枚举 */ + enum TenantVerificationStatus { + Draft = 0, + Pending = 1, + Approved = 2, + Rejected = 3 + } + + /** 经营模式枚举 */ + enum OperatingMode { + SameEntity = 1, + DifferentEntity = 2 + } + + /** 租户信息 */ + interface TenantDto { + id: string // long -> string + code: string + name: string + shortName: string + industry?: string + contactName: string + contactPhone: string + contactEmail: string + status: TenantStatus + verificationStatus: TenantVerificationStatus + operatingMode?: OperatingMode + currentPackageId?: string // long -> string + effectiveFrom?: string + effectiveTo?: string + autoRenew: boolean + } + + /** 租户列表查询参数 */ + interface TenantListParams extends Common.PageParams { + Status?: TenantStatus + VerificationStatus?: TenantVerificationStatus + Name?: string + ContactName?: string + ContactPhone?: string + Keyword?: string + } + + /** 租户列表响应 */ + type TenantListResponse = Common.PageResult + + /** 注册租户命令 */ + interface RegisterTenantCommand { + code: string + name: string + shortName?: string + industry?: string + contactName?: string + contactPhone?: string + contactEmail?: string + tenantPackageId: string // long -> string + durationMonths: number + autoRenew: boolean + effectiveFrom?: string + } + + /** 后台手动新增租户命令(直接入驻) */ + interface CreateTenantManuallyCommand { + // 1. 租户信息(public.tenants) + code: string + name: string + shortName?: string + legalEntityName?: string + industry?: string + logoUrl?: string + coverImageUrl?: string + website?: string + country?: string + province?: string + city?: string + address?: string + contactName?: string + contactPhone?: string + contactEmail?: string + tags?: string + remarks?: string + suspendedAt?: string | Date + suspensionReason?: string + tenantStatus: TenantStatus + + // 2. 订阅信息(public.tenant_subscriptions) + tenantPackageId: string // long -> string + durationMonths: number + autoRenew: boolean + subscriptionEffectiveFrom?: string | Date + nextBillingDate?: string | Date + subscriptionStatus?: number + scheduledPackageId?: string // long -> string + subscriptionNotes?: string + + // 3. 认证信息(public.tenant_verification_profiles) + verificationStatus?: TenantVerificationStatus + businessLicenseNumber?: string + businessLicenseUrl?: string + legalPersonName?: string + legalPersonIdNumber?: string + legalPersonIdFrontUrl?: string + legalPersonIdBackUrl?: string + bankAccountName?: string + bankAccountNumber?: string + bankName?: string + additionalDataJson?: string + submittedAt?: string | Date + reviewedAt?: string | Date + reviewedByName?: string + reviewRemarks?: string + + // 4. 管理员账号(identity.identity_users) + adminAccount: string + adminDisplayName: string + adminPassword: string + adminAvatar?: string + adminMerchantId?: string // long -> string + } + + /** 自助注册租户命令 */ + interface SelfRegisterTenantCommand { + adminAccount: string + adminDisplayName?: string + adminEmail?: string + adminPhone: string + adminPassword: string + } + + /** 自助注册结果 */ + interface SelfRegisterResult { + tenantId: string // long -> string + code?: string + status: TenantStatus + verificationStatus: TenantVerificationStatus + effectiveFrom?: string + effectiveTo?: string + adminAccount?: string + } + + /** 入驻进度信息 */ + interface TenantProgress { + tenantId: string // long -> string + code?: string + status: TenantStatus + verificationStatus: TenantVerificationStatus + effectiveFrom?: string + effectiveTo?: string + } + + /** 自助实名提交命令 */ + interface SubmitTenantVerificationCommand { + tenantId: string // long -> string + businessLicenseNumber?: string + businessLicenseUrl?: string + legalPersonName?: string + legalPersonIdNumber?: string + legalPersonIdFrontUrl?: string + legalPersonIdBackUrl?: string + bankAccountName?: string + bankAccountNumber?: string + bankName?: string + additionalDataJson?: string + } + + /** 租户套餐类型枚举 */ + enum TenantPackageType { + Free = 0, + Standard = 1, + Professional = 2, + Enterprise = 3 + } + + /** 套餐发布状态 */ + enum TenantPackagePublishStatus { + Draft = 0, + Published = 1 + } + + /** 租户套餐信息 */ + interface TenantPackageDto { + id: string // long -> string + name: string + description?: string + packageType: TenantPackageType + monthlyPrice?: number + yearlyPrice?: number + maxStoreCount?: number + maxAccountCount?: number + maxStorageGb?: number + maxSmsCredits?: number + maxDeliveryOrders?: number + featurePoliciesJson?: string + isActive: boolean + isPublicVisible: boolean + isAllowNewTenantPurchase: boolean + publishStatus: TenantPackagePublishStatus + isRecommended: boolean + tags: string[] + sortOrder: number + } + + /** 租户套餐列表查询参数 */ + interface TenantPackageQueryParams extends Common.PageParams { + Keyword?: string + IsActive?: boolean + } + + /** 租户套餐列表响应 */ + type TenantPackageListResponse = Common.PageResult + + /** 公共租户套餐列表响应(匿名) */ + interface PublicTenantPackageListResponse { + pageIndex: number + pageSize: number + totalCount: number + items: TenantPackageDto[] + } + + /** 创建租户套餐参数 */ + interface CreateTenantPackageCommand { + name: string + description?: string + packageType: TenantPackageType + monthlyPrice?: number + yearlyPrice?: number + maxStoreCount?: number + maxAccountCount?: number + maxStorageGb?: number + maxSmsCredits?: number + maxDeliveryOrders?: number + featurePoliciesJson?: string + isActive: boolean + isPublicVisible: boolean + isAllowNewTenantPurchase: boolean + publishStatus: TenantPackagePublishStatus + isRecommended: boolean + tags: string[] + sortOrder: number + } + + /** 更新租户套餐参数 */ + interface UpdateTenantPackageCommand extends CreateTenantPackageCommand { + tenantPackageId: string // long -> string + } + + /** 套餐使用统计 */ + interface TenantPackageUsageDto { + tenantPackageId: string // long -> string + activeSubscriptionCount: number + activeTenantCount: number + totalSubscriptionCount: number + mrr: number + arr: number + expiringTenantCount7Days: number + expiringTenantCount15Days: number + expiringTenantCount30Days: number + } + + /** 套餐当前使用租户 */ + interface TenantPackageTenantDto { + tenantId: string // long -> string + code: string + name: string + status: TenantStatus + contactName?: string + contactPhone?: string + subscriptionEffectiveFrom: string + subscriptionEffectiveTo: string + } + + /** 订阅状态枚举 */ + enum SubscriptionStatus { + Active = 0, + Expired = 1, + Cancelled = 2, + Suspended = 3 + } + + /** 租户订阅信息 */ + interface TenantSubscriptionDto { + id: string // long -> string + tenantId: string // long -> string + tenantPackageId: string // long -> string + status: SubscriptionStatus + effectiveFrom: string + effectiveTo: string + nextBillingDate?: string + autoRenew: boolean + } + + /** 创建订阅命令 */ + interface CreateTenantSubscriptionCommand { + tenantId: string // long -> string + tenantPackageId: string // long -> string + durationMonths?: number + autoRenew?: boolean + notes?: string + } + + /** 延期/赠送订阅命令(按当前订阅续费) */ + interface ExtendTenantSubscriptionCommand { + tenantId: string // long -> string + durationMonths: number + notes?: string + } + + /** 初次绑定订阅命令(自助入驻) */ + interface BindInitialTenantSubscriptionCommand { + tenantId: string // long -> string + tenantPackageId: string // long -> string + autoRenew?: boolean + } + + /** 订阅升降配命令 */ + interface ChangeTenantSubscriptionPlanCommand { + tenantId: string // long -> string + tenantSubscriptionId: string // long -> string + targetPackageId: string // long -> string + immediate?: boolean + notes?: string + } + + /** 租户认证信息 */ + interface TenantVerificationDto { + id: string // long -> string + tenantId: string // long -> string + status: TenantVerificationStatus + businessLicenseNumber?: string + businessLicenseUrl?: string + legalPersonName?: string + legalPersonIdNumber?: string + legalPersonIdFrontUrl?: string + legalPersonIdBackUrl?: string + bankAccountName?: string + bankAccountNumber?: string + bankName?: string + additionalDataJson?: string + submittedAt?: string + reviewedBy?: string // long -> string + reviewRemarks?: string + reviewedByName?: string + reviewedAt?: string + } + + /** 租户详情信息 */ + interface TenantDetailDto { + tenant: TenantDto + verification: TenantVerificationDto + subscription: TenantSubscriptionDto + package: TenantPackageDto + } + + /** 租户审核日志 */ + interface TenantAuditLogDto { + id: string // long -> string + tenantId: string // long -> string + action: number + title: string + description?: string + operatorName?: string + previousStatus?: TenantStatus + currentStatus?: TenantStatus + createdAt: string + } + + /** 租户审核日志分页 */ + type TenantAuditLogListResponse = Common.PageResult + + /** 审核租户命令 */ + interface ReviewTenantCommand { + tenantId: string // long -> string + approve: boolean + reason?: string + renewMonths?: number + operatingMode?: OperatingMode + } + + /** 冻结租户命令 */ + interface FreezeTenantCommand { + tenantId: string // long -> string + reason: string + } + + /** 解冻租户命令 */ + interface UnfreezeTenantCommand { + tenantId: string // long -> string + reason?: string + } + + /** 租户审核领取信息 */ + interface TenantReviewClaimDto { + id: string // long -> string + tenantId: string // long -> string + claimedBy: string // long -> string + claimedByName: string + claimedAt: string + } + } +} diff --git a/src/types/common/index.ts b/src/types/common/index.ts new file mode 100644 index 0000000..7e751d1 --- /dev/null +++ b/src/types/common/index.ts @@ -0,0 +1,95 @@ +/** + * 通用类型定义模块 + * + * 提供项目中常用的通用类型定义 + * + * ## 主要功能 + * + * - 状态类型(启用/禁用) + * - 性别类型 + * - 排序方向类型 + * - 操作类型(增删改查) + * - 记录类型(键值对) + * - 时间范围类型 + * - 文件信息类型 + * - 坐标和尺寸类型 + * - 响应式断点类型 + * - 主题和语言类型 + * - 环境和弹窗类型 + * + * ## 使用场景 + * + * - 通用数据结构定义 + * - 类型约束和提示 + * - 减少重复类型定义 + * + * @module types/common/index + * @author Art Design Pro Team + */ + +// 导出响应类型 +export * from './response' + +// 状态类型 +export type Status = 0 | 1 // 0: 禁用, 1: 启用 + +// 性别类型 +export type Gender = 'male' | 'female' | 'unknown' + +// 排序方向 +export type SortOrder = 'ascending' | 'descending' + +// 操作类型 +export type ActionType = 'create' | 'update' | 'delete' | 'view' + +// 可选的记录类型 +export type Recordable = Record + +// 键值对类型 +export type KeyValue = { + key: string + value: T + label?: string +} + +// 时间范围类型 +export interface TimeRange { + startTime: string + endTime: string +} + +// 文件类型 +export interface FileInfo { + name: string + url: string + size: number + type: string + lastModified?: number +} + +// 坐标类型 +export interface Position { + x: number + y: number +} + +// 尺寸类型 +export interface Size { + width: number + height: number +} + +// 响应式断点类型 +export type Breakpoint = 'xs' | 'sm' | 'md' | 'lg' | 'xl' + +// 主题类型 +export type ThemeMode = 'light' | 'dark' | 'auto' + +// 语言类型 +export type Language = 'zh-CN' | 'en-US' + +// 环境类型 +export type Environment = 'development' | 'production' | 'test' + +// 弹窗类型 +export type DialogType = 'add' | 'edit' diff --git a/src/types/common/response.ts b/src/types/common/response.ts new file mode 100644 index 0000000..11dd274 --- /dev/null +++ b/src/types/common/response.ts @@ -0,0 +1,54 @@ +/** + * API 响应类型定义模块 + * + * 提供统一的 API 响应结构类型定义 + * + * ## 主要功能 + * + * - 基础响应结构定义 + * - 泛型支持(适配不同数据类型) + * - 统一的响应格式约束 + * + * ## 使用场景 + * + * - API 请求响应类型约束 + * - 接口数据类型定义 + * - 响应数据解析 + * + * @module types/common/response + * @author Art Design Pro Team + */ + +/** 基础 API 响应结构 */ +export interface BaseResponse { + /** 是否成功 */ + success?: boolean + /** 状态码 */ + code?: number + /** 提示消息 */ + message?: string + /** 兼容旧字段 */ + msg?: string + /** 数据 */ + data?: T + /** 错误详情(如字段错误) */ + errors?: unknown + /** TraceId,便于链路追踪 */ + traceId?: string + /** 时间戳 */ + timestamp?: string +} + +/** 后端标准分页结果 */ +export interface PagedResult { + /** 数据列表 */ + items: T[] + /** 当前页码(从 1 开始) */ + page: number + /** 每页条数 */ + pageSize: number + /** 总条数 */ + totalCount: number + /** 总页数 */ + totalPages: number +} diff --git a/src/types/component/chart.ts b/src/types/component/chart.ts new file mode 100644 index 0000000..c3225c9 --- /dev/null +++ b/src/types/component/chart.ts @@ -0,0 +1,324 @@ +/** + * 图表组件类型定义模块 + * + * 提供 ECharts 图表组件的完整类型定义 + * + * ## 主要功能 + * + * - 基础图表配置类型 + * - 柱状图类型定义 + * - 折线图类型定义 + * - 饼图/环形图类型定义 + * - 雷达图类型定义 + * - K线图类型定义 + * - 散点图类型定义 + * - 地图图表类型定义 + * - 双向堆叠柱状图类型定义 + * - 图表主题配置类型 + * - 图表事件回调类型 + * + * ## 使用场景 + * + * - 图表组件 Props 类型约束 + * - 图表配置类型定义 + * - 图表数据结构定义 + * - 图表事件处理 + * + * @module types/component/chart + * @author Art Design Pro Team + */ +import type { EChartsOption } from '@/plugins/echarts' + +// 图例位置类型 +export type LegendPosition = 'bottom' | 'top' | 'left' | 'right' + +export type SymbolType = + | 'circle' + | 'rect' + | 'roundRect' + | 'triangle' + | 'diamond' + | 'pin' + | 'arrow' + | 'none' + +// 图表主题配置 +export interface ChartThemeConfig { + /** 图表高度 */ + chartHeight: string + /** 字体大小 */ + fontSize: number + /** 字体颜色 */ + fontColor: string + /** 主题颜色 */ + themeColor: string + /** 颜色组 */ + colors: string[] +} + +// 图表初始化选项 +export interface UseChartOptions { + /** 初始化选项 */ + initOptions?: EChartsOption + /** 延迟初始化时间(ms) */ + initDelay?: number + /** IntersectionObserver阈值 */ + threshold?: number + /** 是否自动响应主题变化 */ + autoTheme?: boolean +} + +// 基础图表 Props 接口 - 统一所有图表的基础属性 +export interface BaseChartProps { + /** 图表高度 */ + height?: string + /** 是否加载中 */ + loading?: boolean + isEmpty?: boolean + /** 颜色配置 */ + colors?: string[] +} + +// 轴线显示控制接口 - 统一轴线相关配置 +export interface AxisDisplayProps { + /** 是否显示坐标轴标签 */ + showAxisLabel?: boolean + /** 是否显示坐标轴线 */ + showAxisLine?: boolean + /** 是否显示分割线 */ + showSplitLine?: boolean +} + +// 交互显示控制接口 - 统一交互相关配置 +export interface InteractionProps { + /** 是否显示提示框 */ + showTooltip?: boolean + /** 是否显示图例 */ + showLegend?: boolean + /** 图例位置 */ + legendPosition?: LegendPosition +} + +// 柱状图数据项接口 +export interface BarDataItem { + /** 系列名称 */ + name: string + /** 数据值 */ + data: number[] + /** 柱状图宽度 */ + barWidth?: string | number + /** 堆叠分组名称 */ + stack?: string +} + +// 柱状图 Props 接口 - 统一柱状图配置 +export interface BarChartProps extends BaseChartProps, AxisDisplayProps, InteractionProps { + /** 图表数据 - 支持单组数据或多组数据 */ + data: number[] | BarDataItem[] + /** X轴标签数据 */ + xAxisData?: string[] + /** 柱状图宽度 */ + barWidth?: string | number + /** 是否堆叠显示 */ + stack?: boolean + /** 圆角 */ + borderRadius?: number | number[] +} + +// 折线图数据项接口 +export interface LineDataItem { + /** 系列名称 */ + name: string + /** 数据值 */ + data: number[] + /** 线条宽度 */ + lineWidth?: number + /** 是否显示区域填充 */ + showAreaColor?: boolean + /** 区域样式配置 */ + areaStyle?: { + /** 渐变开始透明度 */ + startOpacity?: number + /** 渐变结束透明度 */ + endOpacity?: number + /** 自定义 ECharts areaStyle 配置 */ + custom?: any + } + /** 是否平滑曲线 */ + smooth?: boolean + /** 数据点符号 */ + symbol?: SymbolType + /** 数据点大小 */ + symbolSize?: number +} + +// 折线图 Props 接口 - 统一折线图配置 +export interface LineChartProps extends BaseChartProps, AxisDisplayProps, InteractionProps { + /** 图表数据 - 支持单组数据或多组数据 */ + data: number[] | LineDataItem[] + /** X轴标签数据 */ + xAxisData?: string[] + /** 线条宽度 */ + lineWidth?: number + /** 是否显示区域填充 */ + showAreaColor?: boolean + /** 是否平滑曲线 */ + smooth?: boolean + /** 数据点符号 */ + symbol?: SymbolType + /** 数据点大小 */ + symbolSize?: number + /** 多数据动画延迟间隔(毫秒) */ + animationDelay?: number +} + +// 雷达图数据项接口 +export interface RadarDataItem { + /** 系列名称 */ + name: string + /** 数据值 */ + value: number[] +} + +// 雷达图 Props 接口 - 统一雷达图配置 +export interface RadarChartProps extends BaseChartProps, InteractionProps { + /** 雷达图指标配置 */ + indicator?: Array<{ name: string; max: number }> + /** 图表数据 */ + data?: RadarDataItem[] +} + +// 饼图/环形图数据项接口 +export interface PieDataItem { + /** 数据值 */ + value: number + /** 数据名称 */ + name: string +} + +// 环形图 Props 接口 - 统一环形图配置 +export interface RingChartProps extends BaseChartProps, InteractionProps { + /** 图表数据 */ + data: PieDataItem[] + /** 内外半径 */ + radius?: string[] + /** 边框圆角 */ + borderRadius?: number + /** 中心文本 */ + centerText?: string + /** 是否显示标签 */ + showLabel?: boolean +} + +// K线图数据项接口 +export interface KLineDataItem { + /** 时间标签 */ + time: string + /** 开盘价 */ + open: number + /** 收盘价 */ + close: number + /** 最高价 */ + high: number + /** 最低价 */ + low: number +} + +// K线图 Props 接口 - 统一K线图配置 +export interface KLineChartProps extends BaseChartProps { + /** 图表数据 */ + data?: KLineDataItem[] + /** 是否显示数据缩放控件 */ + showDataZoom?: boolean + /** 数据缩放初始开始位置 */ + dataZoomStart?: number + /** 数据缩放初始结束位置 */ + dataZoomEnd?: number +} + +// 散点图数据项接口 +export interface ScatterDataItem { + /** 坐标值 [x, y] */ + value: number[] +} + +// 散点图 Props 接口 - 统一散点图配置 +export interface ScatterChartProps extends BaseChartProps, AxisDisplayProps, InteractionProps { + /** 图表数据 */ + data?: ScatterDataItem[] + /** 散点大小 */ + symbolSize?: number +} + +// 双柱对比图 Props 接口 - 统一双柱对比图配置 +export interface DualBarCompareChartProps extends BaseChartProps { + /** 上方数据 */ + topData: number[] + /** 下方数据 */ + bottomData: number[] + /** X轴标签数据 */ + xAxisData: string[] + /** 上方柱子颜色 */ + topColor?: string + /** 下方柱子颜色 */ + bottomColor?: string + /** 柱状图宽度 */ + barWidth?: number +} + +// 地图图表 Props 接口 - 统一地图图表配置 +export interface MapChartProps extends BaseChartProps { + /** 地图数据 */ + mapData?: any[] + /** 选中区域 */ + selectedRegion?: string + /** 是否显示标签 */ + showLabels?: boolean + /** 是否显示散点 */ + showScatter?: boolean +} + +// 双向堆叠柱状图 Props 接口(人口金字塔样式) +export interface BidirectionalBarChartProps + extends BaseChartProps, + AxisDisplayProps, + InteractionProps { + /** 正向数据(向上显示) */ + positiveData: number[] + /** 负向数据(向下显示) */ + negativeData: number[] + /** X轴标签数据 */ + xAxisData?: string[] + /** 正向数据名称 */ + positiveName?: string + /** 负向数据名称 */ + negativeName?: string + /** 柱状图宽度 */ + barWidth?: string | number + /** Y轴最小值 */ + yAxisMin?: number + /** Y轴最大值 */ + yAxisMax?: number + /** 是否显示数据标签 */ + showDataLabel?: boolean + /** 正向数据圆角配置 */ + positiveBorderRadius?: number | number[] + /** 负向数据圆角配置 */ + negativeBorderRadius?: number | number[] +} + +// 图表配置生成器函数类型 +export type ChartOptionGenerator = () => EChartsOption + +// 图表事件回调类型 +export type ChartEventCallback = (params: any) => void + +// 图表错误信息接口 +export interface ChartError { + /** 错误码 */ + code: string + /** 错误信息 */ + message: string + /** 错误详情 */ + details?: any +} diff --git a/src/types/component/index.ts b/src/types/component/index.ts new file mode 100644 index 0000000..cd89bce --- /dev/null +++ b/src/types/component/index.ts @@ -0,0 +1,145 @@ +/** + * 组件类型定义模块 + * + * 提供项目组件的类型定义 + * + * ## 主要功能 + * + * - 搜索组件类型定义 + * - 表格列配置类型 + * - 分页配置类型 + * - 表单规则类型 + * - 对话框配置类型 + * + * ## 使用场景 + * + * - 组件 Props 类型约束 + * - 组件配置类型定义 + * - 组件事件参数类型 + * + * @module types/component/index + * @author Art Design Pro Team + */ + +// 搜索组件类型 +export type SearchComponentType = + | 'input' + | 'select' + | 'radio' + | 'checkbox' + | 'date' + | 'datetime' + | 'daterange' + | 'datetimerange' + | 'month' + | 'monthrange' + | 'year' + | 'yearrange' + | 'week' + | 'time' + | 'timerange' + +// 搜索框值变化参数 +export interface SearchChangeParams { + prop: string + val: unknown +} + +// 表格列配置接口 +export interface ColumnOption { + // 列类型 + type?: 'selection' | 'expand' | 'index' | 'globalIndex' + // 列属性名 + prop?: string + // 列标题 + label?: string + // 列宽度 + width?: string | number + // 最小列宽度 + minWidth?: string | number + // 固定列 + fixed?: boolean | 'left' | 'right' + // 是否可排序 + sortable?: boolean + // 过滤器选项 + filters?: any[] + // 过滤方法 + filterMethod?: (value: any, row: any) => boolean + // 过滤器位置 + filterPlacement?: string + // 是否禁用 + disabled?: boolean + // 是否显示列 + visible?: boolean + // 是否选中显示 + checked?: boolean + // 自定义渲染函数 + formatter?: (row: T) => any + // 插槽相关配置 + // 是否使用插槽渲染内容 + useSlot?: boolean + // 插槽名称(默认为 prop 值) + slotName?: string + // 是否使用表头插槽 + useHeaderSlot?: boolean + // 表头插槽名称(默认为 `${prop}-header`) + headerSlotName?: string + // 其他属性 + [key: string]: any +} + +// 分页配置 +export interface PaginationConfig { + // 当前页 + currentPage: number + // 每页条数 + pageSize: number + // 总条数 + total: number + // 每页显示个数选择器的选项 + pageSizes?: number[] + // 组件布局 + layout?: string + // 是否为小型分页 + small?: boolean +} + +// 表单规则 +export interface FormRule { + // 是否必填 + required?: boolean + // 错误提示信息 + message?: string + // 触发方式 + trigger?: string | string[] + // 最小长度 + min?: number + // 最大长度 + max?: number + // 正则表达式 + pattern?: RegExp + // 自定义验证函数 + validator?: (rule: any, value: any, callback: any) => void +} + +// 对话框配置 +export interface DialogConfig { + // 标题 + title: string + // 是否显示 + visible: boolean + // 宽度 + width?: string | number + // 是否可以通过点击 modal 关闭 + closeOnClickModal?: boolean + // 是否可以通过按下 ESC 关闭 + closeOnPressEscape?: boolean + // 是否显示关闭按钮 + showClose?: boolean + // 是否在 Dialog 出现时将 body 滚动锁定 + lockScroll?: boolean + // 是否显示遮罩层 + modal?: boolean + // 自定义类名 + customClass?: string +} diff --git a/src/types/config/index.ts b/src/types/config/index.ts new file mode 100644 index 0000000..dd144de --- /dev/null +++ b/src/types/config/index.ts @@ -0,0 +1,211 @@ +/** + * 配置类型定义模块 + * + * 提供系统配置相关的类型定义 + * + * ## 主要功能 + * + * - 主题设置类型 + * - 菜单布局类型 + * - 节日配置类型 + * - 系统基础配置类型 + * - 快速入口配置类型 + * - 顶部栏功能配置类型 + * - 环境配置类型 + * - 应用配置类型 + * + * ## 使用场景 + * + * - 系统配置文件类型约束 + * - 配置项类型定义 + * - 配置数据验证 + * + * @module types/config/index + * @author Art Design Pro Team + */ + +import { MenuTypeEnum, SystemThemeEnum } from '@/enums/appEnum' +import { MenuThemeType, SystemThemeTypes } from '@/types/store' + +// 主题设置 +export interface ThemeSetting { + /** 主题名称 */ + name: string + /** 系统主题类型 */ + theme: SystemThemeEnum + /** 主题颜色数组 */ + color: string[] + /** 左侧线条颜色 */ + leftLineColor: string + /** 右侧线条颜色 */ + rightLineColor: string + /** 主题图片 */ + img: string +} + +// 菜单布局 +export interface MenuLayout { + /** 布局名称 */ + name: string + /** 菜单类型值 */ + value: MenuTypeEnum + /** 布局预览图 */ + img: string + /** 布局描述 */ + description?: string +} + +// 节日配置 +export interface FestivalConfig { + /** 节日日期(单日)或开始日期(日期范围) */ + date: string + /** 节日结束日期(可选,用于跨日期节日) */ + endDate?: string + /** 节日名称 */ + name: string + /** 烟花图片 */ + image: string + /** 滚动文本 */ + scrollText: string + /** 是否激活 */ + isActive?: boolean + /** 烟花播放次数(可选,默认为 3 次) */ + count?: number +} + +// 系统基础配置 +export interface SystemBasicConfig { + // 系统名称 + name: string + // 系统描述 + description?: string + // 系统logo + logo?: string + // 系统favicon + favicon?: string + // 版权信息 + copyright?: string +} + +// 快速入口基础项 +export interface FastEnterBaseItem { + /** 名称 */ + name: string + /** 是否启用 */ + enabled?: boolean + /** 排序权重 */ + order?: number + /** 路由名称 */ + routeName?: string + /** 外部链接 */ + link?: string +} + +// 快速入口应用项 +export interface FastEnterApplication extends FastEnterBaseItem { + /** 应用描述 */ + description: string + /** 图标代码 */ + icon: string + /** 图标颜色 */ + iconColor: string +} + +// 快速链接项 +export type FastEnterQuickLink = FastEnterBaseItem + +// 快速入口配置 +export interface FastEnterConfig { + /** 应用列表 */ + applications: FastEnterApplication[] + /** 快速链接 */ + quickLinks: FastEnterQuickLink[] + /** 显示条件(屏幕宽度) */ + minWidth?: number +} + +// 系统配置 +export interface SystemConfig { + // 系统基础信息 + systemInfo: SystemBasicConfig + // 系统主题样式 + systemThemeStyles: SystemThemeTypes + // 设置主题列表 + settingThemeList: ThemeSetting[] + // 菜单布局列表 + menuLayoutList: MenuLayout[] + // 主题列表 + themeList: MenuThemeType[] + // 暗色菜单样式 + darkMenuStyles: MenuThemeType[] + // 系统主色调 + systemMainColor: readonly string[] + // 快速入口配置 + fastEnter?: FastEnterConfig + // 顶部栏功能配置 + headerBar?: HeaderBarFeatureConfig +} + +// 环境配置 +export interface EnvConfig { + // 环境名称 + NODE_ENV: string + // 应用版本 + VITE_VERSION: string + // 应用端口 + VITE_PORT: string + // 应用基础路径 + VITE_BASE_URL: string + // API 地址 + VITE_API_URL: string + // 是否开启 Mock + VITE_USE_MOCK?: string + // 是否开启压缩 + VITE_USE_GZIP?: string + // 是否开启 CDN + VITE_USE_CDN?: string +} + +// 应用配置 +export interface AppConfig extends SystemConfig { + // 环境配置 + env: EnvConfig + // 开发模式 + isDev: boolean + // 生产模式 + isProd: boolean + // 测试模式 + isTest: boolean +} + +// 功能配置项基础接口 +export interface FeatureConfigItem { + enabled: boolean + description: string +} + +// 顶部栏功能配置接口 +export interface HeaderBarFeatureConfig { + /** 菜单按钮 */ + menuButton: FeatureConfigItem + /** 刷新按钮 */ + refreshButton: FeatureConfigItem + /** 快速入口 */ + fastEnter: FeatureConfigItem + /** 面包屑导航 */ + breadcrumb: FeatureConfigItem + /** 全局搜索 */ + globalSearch: FeatureConfigItem + /** 全屏功能 */ + fullscreen: FeatureConfigItem + /** 通知功能 */ + notification: FeatureConfigItem + /** 聊天功能 */ + chat: FeatureConfigItem + /** 多语言切换 */ + language: FeatureConfigItem + /** 设置面板 */ + settings: FeatureConfigItem + /** 主题切换 */ + themeToggle: FeatureConfigItem +} diff --git a/src/types/dictionary.ts b/src/types/dictionary.ts new file mode 100644 index 0000000..da7c507 --- /dev/null +++ b/src/types/dictionary.ts @@ -0,0 +1,25 @@ +export const ValidationConstraints = { + codeMinLength: 2, + codeMaxLength: 64, + nameMaxLength: 128, + keyMaxLength: 128, + descriptionMaxLength: 512 +} + +export function extractI18nText( + value: Record | undefined, + locale?: string +): string { + if (!value) return '' + + const normalizedLocale = locale?.trim() + if (normalizedLocale && value[normalizedLocale]) { + return value[normalizedLocale] + } + + if (value['zh-CN']) return value['zh-CN'] + if (value.en) return value.en + + const first = Object.values(value)[0] + return first ?? '' +} diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..9032fd2 --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,22 @@ +/** + * 类型定义统一导出模块 + * 提供全局类型定义的统一导出入口 + * + * @module types/index + * @author Art Design Pro Team + */ + +/** 通用类型定义(基础类型、工具类型等) */ +export * from './common' + +/** 组件相关类型定义 */ +export * from './component' + +/** 状态管理相关类型定义 */ +export * from './store' + +/** 路由相关类型定义 */ +export * from './router' + +/** 配置相关类型定义 */ +export * from './config' diff --git a/src/types/json-bigint.d.ts b/src/types/json-bigint.d.ts new file mode 100644 index 0000000..b30735b --- /dev/null +++ b/src/types/json-bigint.d.ts @@ -0,0 +1 @@ +declare module 'json-bigint' diff --git a/src/types/modules/quotaPackage.d.ts b/src/types/modules/quotaPackage.d.ts new file mode 100644 index 0000000..5094d61 --- /dev/null +++ b/src/types/modules/quotaPackage.d.ts @@ -0,0 +1,98 @@ +/** + * 配额包相关类型定义 + */ +declare namespace Api.QuotaPackage { + /** + * 配额包列表项 DTO + */ + interface QuotaPackageListDto { + id: string + name: string + quotaType: number + quotaValue: number + price: number + isActive: boolean + sortOrder: number + description?: string + } + + /** + * 配额包详情 DTO + */ + interface QuotaPackageDto { + id: string + name: string + quotaType: number + quotaValue: number + price: number + isActive: boolean + sortOrder: number + description?: string + createdAt: string + updatedAt?: string + } + + /** + * 创建配额包命令 + */ + interface CreateQuotaPackageCommand { + name: string + quotaType: number + quotaValue: number + price: number + isActive?: boolean + sortOrder?: number + description?: string + } + + /** + * 更新配额包命令 + */ + interface UpdateQuotaPackageCommand { + name: string + quotaType: number + quotaValue: number + price: number + isActive?: boolean + sortOrder?: number + description?: string + } + + /** + * 购买配额包命令 + */ + interface PurchaseQuotaPackageCommand { + quotaPackageId: string + expiredAt?: string + notes?: string + } + + /** + * 租户配额购买记录 DTO + */ + interface TenantQuotaPurchaseDto { + id: string + tenantId: string + quotaPackageId: string + quotaPackageName: string + quotaType: number + quotaValue: number + price: number + purchasedAt: string + expiredAt?: string + notes?: string + } + + /** + * 租户配额使用情况 DTO + */ + interface TenantQuotaUsageDto { + tenantId: string + quotaType: number + limitValue: number + usedValue: number + remainingValue: number + resetCycle?: string + lastResetAt?: string + } +} diff --git a/src/types/router/index.ts b/src/types/router/index.ts new file mode 100644 index 0000000..d9ef012 --- /dev/null +++ b/src/types/router/index.ts @@ -0,0 +1,80 @@ +/** + * 路由类型定义模块 + * + * 提供路由相关的类型定义 + * + * ## 主要功能 + * + * - 路由元数据类型(标题、图标、权限等) + * - 应用路由记录类型 + * - 路由配置扩展 + * + * ## 使用场景 + * + * - 路由配置类型约束 + * - 路由元数据定义 + * - 菜单生成 + * - 权限控制 + * + * @module types/router/index + * @author Art Design Pro Team + */ + +import { RouteRecordRaw } from 'vue-router' + +/** + * 路由元数据接口 + * 定义路由的各种配置属性 + */ +export interface RouteMeta extends Record { + /** 路由标题 */ + title: string + /** 路由图标 */ + icon?: string + /** 是否显示徽章 */ + showBadge?: boolean + /** 文本徽章 */ + showTextBadge?: string + /** 是否在菜单中隐藏 */ + isHide?: boolean + /** 是否在标签页中隐藏 */ + isHideTab?: boolean + /** 外部链接 */ + link?: string + /** 是否为iframe */ + isIframe?: boolean + /** 是否缓存 */ + keepAlive?: boolean + /** 操作权限 */ + authList?: Array<{ + title: string + authMark: string + }> + /** 是否为一级菜单 */ + isFirstLevel?: boolean + /** 角色权限 */ + roles?: string[] + /** 是否固定标签页 */ + fixedTab?: boolean + /** 激活菜单路径 */ + activePath?: string + /** 是否为全屏页面 */ + isFullPage?: boolean + /** 是否为权限按钮行 */ + isAuthButton?: boolean + /** 权限标识 */ + authMark?: string + /** 父级路径 */ + parentPath?: string +} + +/** + * 应用路由记录接口 + * 扩展 Vue Router 的路由记录类型 + */ +export interface AppRouteRecord extends Omit { + id?: number + meta: RouteMeta + children?: AppRouteRecord[] + component?: string | (() => Promise) +} diff --git a/src/types/store/index.ts b/src/types/store/index.ts new file mode 100644 index 0000000..019801e --- /dev/null +++ b/src/types/store/index.ts @@ -0,0 +1,157 @@ +/** + * Store 状态类型定义模块 + * + * 提供 Pinia Store 的状态类型定义 + * + * ## 主要功能 + * + * - 系统主题类型 + * - 菜单主题类型 + * - 设置状态类型 + * - 工作标签页类型 + * - 用户状态类型 + * - 菜单状态类型 + * - 根状态类型 + * + * ## 使用场景 + * + * - Store 状态类型约束 + * - 状态数据结构定义 + * - 类型提示和自动补全 + * + * @module types/store/index + * @author Art Design Pro Team + */ + +import { MenuThemeEnum, SystemThemeEnum } from '@/enums/appEnum' +import { LocationQueryRaw } from 'vue-router' + +// 系统主题样式(light | dark) +export interface SystemThemeType { + /** 主题类名 */ + className: string +} + +// 定义包含多个主题的类型 +export type SystemThemeTypes = { + [key in Exclude]: SystemThemeType +} + +// 菜单主题样式 +export interface MenuThemeType { + /** 主题类型 */ + theme: MenuThemeEnum + /** 背景颜色 */ + background: string + /** 系统名称颜色 */ + systemNameColor: string + /** 文本颜色 */ + textColor: string + /** 图标颜色 */ + iconColor: string + /** 背景图片 */ + img?: string +} + +// 设置中心 +export interface SettingState { + /** 主题 */ + theme: string + /** 是否只保持一个子菜单的展开 */ + uniqueOpened: boolean + /** 是否显示菜单按钮 */ + menuButton: boolean + /** 是否显示刷新按钮 */ + showRefreshButton: boolean + /** 是否显示面包屑 */ + showCrumbs: boolean + /** 是否自动关闭 */ + autoClose: boolean + /** 是否显示工作标签页 */ + showWorkTab: boolean + /** 是否显示语言切换 */ + showLanguage: boolean + /** 是否显示进度条 */ + showNprogress: boolean + /** 主题模式 */ + themeModel: string +} + +// 多标签 +export interface WorkTab { + /** 标签标题 */ + title: string + /** 自定义标题 */ + customTitle?: string + /** 路由路径 */ + path: string + /** 路由名称 */ + name: string + /** 是否缓存 */ + keepAlive: boolean + /** 是否固定标签 */ + fixedTab?: boolean + /** 路由参数 */ + params?: object + /** 路由查询参数 */ + query?: LocationQueryRaw + /** 图标 */ + icon?: string + /** 是否激活 */ + isActive?: boolean +} + +// 用户Store状态 +export interface UserState { + /** 用户信息 */ + userInfo: Api.Auth.UserInfo | null + /** 认证令牌 */ + token: string | null + /** 用户角色列表 */ + roles: string[] + /** 用户权限列表 */ + permissions: string[] +} + +// 设置Store状态 +export interface SettingStoreState extends SettingState { + // 额外的设置状态 + /** 菜单是否折叠 */ + collapsed: boolean + /** 设备类型 */ + device: 'desktop' | 'mobile' + /** 当前语言 */ + language: string +} + +// 工作标签页Store状态 +export interface WorkTabState { + /** 标签页列表 */ + tabs: WorkTab[] + /** 当前激活的标签页 */ + activeTab: string + /** 缓存的标签页列表 */ + cachedTabs: string[] +} + +// 菜单Store状态 +export interface MenuState { + /** 菜单列表 */ + menuList: any[] + /** 菜单是否已加载 */ + isLoaded: boolean + /** 菜单是否折叠 */ + collapsed: boolean +} + +// 根Store状态类型 +export interface RootState { + /** 用户状态 */ + user: UserState + /** 设置状态 */ + setting: SettingStoreState + /** 工作标签页状态 */ + workTab: WorkTabState + /** 菜单状态 */ + menu: MenuState +} diff --git a/src/types/tenant/index.ts b/src/types/tenant/index.ts new file mode 100644 index 0000000..cf18986 --- /dev/null +++ b/src/types/tenant/index.ts @@ -0,0 +1,105 @@ +/** + * 租户详情/配额/订阅/账单相关类型(Milestone 1 预置) + * + * 说明: + * - 遵循零 Any 原则 + * - 所有 Snowflake ID 一律使用 string + * - 已存在的全局类型(Api.*)优先复用,避免重复定义与漂移 + */ + +/** + * Snowflake ID(后端 long -> 前端 string) + */ +export type SnowflakeId = string + +/** + * 租户详情(来自 Swagger: TenantDetailDto) + */ +export type TenantDetailDto = Api.Tenant.TenantDetailDto + +/** + * 更新租户命令(TD-001) + * + * 说明: + * - Swagger 当前未提供 UpdateTenantCommand,按现有页面字段 + 合理推断补齐 + * - Snowflake ID 一律使用 string + * - 待后端补齐 PUT /api/admin/v1/tenants/{tenantId} 后,可按 Swagger 对齐字段 + */ +export interface UpdateTenantCommand { + tenantId: SnowflakeId + + name: string + shortName?: string + industry?: string + contactName?: string + contactPhone?: string + contactEmail?: string + + logoUrl?: string + coverImageUrl?: string + website?: string + + country?: string + province?: string + city?: string + address?: string + + tags?: string + remarks?: string +} + +/** + * 配额汇总(来自 Swagger: TenantQuotaUsageDto) + */ +export type QuotaSummaryDto = Api.QuotaPackage.TenantQuotaUsageDto + +/** + * 配额使用明细(来自 Swagger: QuotaUsageDto) + */ +export interface QuotaUsageDto { + id: SnowflakeId + quotaType: number + limitValue: number + usedValue: number + usagePercentage: number + remainingValue: number + resetCycle?: string | null + lastResetAt?: string | null +} + +/** + * 配额购买记录(来自 Swagger: TenantQuotaPurchaseDto) + */ +export type QuotaPurchaseDto = Api.QuotaPackage.TenantQuotaPurchaseDto + +/** + * 订阅信息(来自 Swagger: TenantSubscriptionDto) + */ +export type SubscriptionDto = Api.Tenant.TenantSubscriptionDto + +/** + * 账单信息(来自 Swagger: TenantBillingDto) + */ +export interface BillingDto { + id: SnowflakeId + tenantId: SnowflakeId + statementNo: string | null + periodStart: string + periodEnd: string + amountDue: number + amountPaid: number + status: Api.Billing.TenantBillingStatus + dueDate: string + lineItemsJson: string | null +} + +/** + * 配额使用历史记录 DTO(TD-002 待后端接口补充) + */ +export interface QuotaUsageHistoryDto { + quotaType: number + usedValue: number + limitValue: number + recordedAt: string // ISO 8601 + changeType: 'increase' | 'decrease' | 'init' | 'snapshot' +} diff --git a/src/utils/announcementStatus.ts b/src/utils/announcementStatus.ts new file mode 100644 index 0000000..f7609b8 --- /dev/null +++ b/src/utils/announcementStatus.ts @@ -0,0 +1,36 @@ +import type { AnnouncementStatus } from '@/types/announcement' + +const statusNumberMap: Record = { + 0: 'Draft', + 1: 'Published', + 2: 'Revoked' +} + +const statusStringMap: Record = { + DRAFT: 'Draft', + PUBLISHED: 'Published', + REVOKED: 'Revoked' +} + +export const normalizeAnnouncementStatus = (status: unknown): AnnouncementStatus | null => { + // 1. 数字状态直接映射 + if (typeof status === 'number') { + return statusNumberMap[status] ?? null + } + + // 2. 字符串状态兼容大小写与数字 + if (typeof status === 'string') { + const normalized = status.trim().toUpperCase() + if (normalized in statusStringMap) { + return statusStringMap[normalized] + } + + const numeric = Number(normalized) + if (Number.isFinite(numeric)) { + return statusNumberMap[numeric] ?? null + } + } + + // 3. 无法识别时返回空 + return null +} diff --git a/src/utils/billing.ts b/src/utils/billing.ts new file mode 100644 index 0000000..5b73853 --- /dev/null +++ b/src/utils/billing.ts @@ -0,0 +1,308 @@ +/** + * 账单管理工具函数 + * + * 提供账单相关的通用工具函数 + * + * @module utils/billing + */ + +import { $t } from '@/locales' +import { + BillingType, + ExportFormat, + TenantPaymentMethod, + TenantPaymentStatus +} from '@/enums/Billing' + +/** + * 格式化账单状态为文本 + * @param status 账单状态 + * @returns 状态文本 + */ +export function formatBillingStatus(status: number): string { + const statusMap: Record = { + 0: $t('billing.status.pending'), + 1: $t('billing.status.paid'), + 2: $t('billing.status.overdue'), + 3: $t('billing.status.cancelled'), + // 兼容:若后端未来扩展状态值(或历史数据脏值) + 4: $t('billing.status.cancelled') + } + return statusMap[status] || '未知' +} + +/** + * 获取账单状态对应的 Element Plus Tag 类型 + * @param status 账单状态 + * @returns Tag 类型 + */ +export function getBillingStatusType(status: number): 'success' | 'warning' | 'danger' | 'info' { + const typeMap: Record = { + 0: 'warning', + 1: 'success', + 2: 'danger', + 3: 'info', + 4: 'info' + } + return typeMap[status] || 'info' +} + +/** + * 获取账单状态颜色 + * @param status 账单状态 + * @returns 颜色值 + */ +export function getBillingStatusColor(status: number): string { + const colorMap: Record = { + 0: 'var(--el-color-warning)', + 1: 'var(--el-color-success)', + 2: 'var(--el-color-danger)', + 3: 'var(--el-color-info)', + 4: 'var(--el-color-info)' + } + return colorMap[status] || 'var(--el-color-info)' +} + +/** + * 格式化账单类型为文本 + * @param type 账单类型 + * @returns 类型文本 + */ +export function formatBillingType(type?: BillingType | number | null): string { + if (type === undefined || type === null) return '-' + const typeMap: Record = { + [BillingType.Subscription]: $t('billing.billingType.subscription'), + [BillingType.QuotaPurchase]: $t('billing.billingType.quotaPurchase'), + [BillingType.Manual]: $t('billing.billingType.manual'), + [BillingType.Renewal]: $t('billing.billingType.renewal') + } + return typeMap[type as number] || '未知' +} + +/** + * 格式化支付方式为文本 + * @param method 支付方式 + * @returns 方式文本 + */ +export function formatPaymentMethod(method: TenantPaymentMethod | number): string { + const methodMap: Record = { + [TenantPaymentMethod.Online]: $t('billing.paymentMethod.online'), + [TenantPaymentMethod.BankTransfer]: $t('billing.paymentMethod.bankTransfer'), + [TenantPaymentMethod.Other]: $t('billing.paymentMethod.other') + } + return methodMap[method as number] || '未知' +} + +/** + * 获取支付方式图标名称 + * @param method 支付方式 + * @returns 图标名称(Element Plus Icon) + */ +export function getPaymentMethodIcon(method: TenantPaymentMethod | number): string { + const iconMap: Record = { + [TenantPaymentMethod.Online]: 'CreditCard', + [TenantPaymentMethod.BankTransfer]: 'Wallet', + [TenantPaymentMethod.Other]: 'Money' + } + return iconMap[method as number] || 'Money' +} + +/** + * 格式化支付状态为文本 + * @param status 支付状态 + * @returns 状态文本 + */ +export function formatPaymentStatus(status: TenantPaymentStatus | number): string { + const statusMap: Record = { + [TenantPaymentStatus.Pending]: $t('billing.paymentStatus.pending'), + [TenantPaymentStatus.Success]: $t('billing.paymentStatus.success'), + [TenantPaymentStatus.Failed]: $t('billing.paymentStatus.failed'), + [TenantPaymentStatus.Refunded]: $t('billing.paymentStatus.refunded') + } + return statusMap[status as number] || '未知' +} + +/** + * 获取支付状态对应的 Tag 类型 + * @param status 支付状态 + * @returns Tag 类型 + */ +export function getPaymentStatusType( + status: TenantPaymentStatus | number +): 'success' | 'warning' | 'danger' | 'info' { + const typeMap: Record = { + [TenantPaymentStatus.Pending]: 'warning' as const, + [TenantPaymentStatus.Success]: 'success' as const, + [TenantPaymentStatus.Failed]: 'danger' as const, + [TenantPaymentStatus.Refunded]: 'info' as const + } + return typeMap[status as number] || 'info' +} + +/** + * 计算逾期天数 + * @param dueDate 到期日期(ISO 8601 字符串) + * @returns 逾期天数(负数表示未到期) + */ +export function calculateDaysOverdue(dueDate: string): number { + const due = new Date(dueDate) + const now = new Date() + const diff = now.getTime() - due.getTime() + return Math.floor(diff / (1000 * 60 * 60 * 24)) +} + +/** + * 判断账单是否即将到期(7天内) + * @param dueDate 到期日期 + * @returns 是否即将到期 + */ +export function isBillingDueSoon(dueDate: string): boolean { + const days = calculateDaysOverdue(dueDate) + return days >= -7 && days < 0 +} + +function pad2(n: number): string { + return String(n).padStart(2, '0') +} + +/** + * 格式化日期时间(YYYY-MM-DD HH:mm) + * @param value ISO 字符串 / Date 可解析值 + */ +export function formatDateTime(value?: string | null): string { + if (!value) return '-' + const date = new Date(value) + if (Number.isNaN(date.getTime())) return '-' + return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())} ${pad2(date.getHours())}:${pad2(date.getMinutes())}` +} + +/** + * 格式化日期(YYYY-MM-DD) + * @param value ISO 字符串 / Date 可解析值 + */ +export function formatDate(value?: string | null): string { + if (!value) return '-' + const date = new Date(value) + if (Number.isNaN(date.getTime())) return '-' + return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}` +} + +/** + * 格式化金额(带货币符号) + * @param amount 金额 + * @param currency 货币类型(默认 CNY) + * @returns 格式化后的金额 + */ +export function formatAmount(amount?: number | null, currency: string = 'CNY'): string { + const safeAmount = Number.isFinite(amount as number) ? (amount as number) : 0 + const currencySymbol = currency === 'CNY' ? '¥' : currency === 'USD' ? '$' : '' + if (currencySymbol) { + return `${currencySymbol}${safeAmount.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}` + } + return `${currency} ${safeAmount.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}` +} + +/** + * 生成导出文件名 + * @param format 导出格式 + * @param dateRange 日期范围(可选) + * @returns 文件名 + */ +export function generateExportFileName( + format: ExportFormat, + dateRange?: { start?: string; end?: string } +): string { + const timestamp = new Date().toISOString().split('T')[0] // YYYY-MM-DD + const formatExt = + format === ExportFormat.Excel ? 'xlsx' : format === ExportFormat.Pdf ? 'pdf' : 'csv' + + const baseName = $t('billing.export.fileName') || 'billing_export' + let fileName = `${baseName}_${timestamp}` + if (dateRange?.start && dateRange?.end) { + fileName = `${baseName}_${dateRange.start}_to_${dateRange.end}` + } + + // Windows 文件名非法字符处理 + const safeName = fileName.replace(/[\\/:*?"<>|]/g, '_').replace(/\s+/g, '_') + + return `${safeName}.${formatExt}` +} + +/** + * 解析账单明细 JSON + * @param lineItemsJson 明细 JSON 字符串 + * @returns 明细数组 + */ +export function parseLineItems(lineItemsJson?: string): Api.Billing.BillingLineItemDto[] { + if (!lineItemsJson) return [] + try { + return JSON.parse(lineItemsJson) + } catch { + return [] + } +} + +/** + * 计算账单总金额(包含税费和折扣) + * @param billing 账单数据 + * @returns 总金额 + */ +export function calculateTotalAmount( + billing: Partial +): number { + const { amountDue = 0, discountAmount = 0, taxAmount = 0 } = billing + return amountDue - discountAmount + taxAmount +} + +/** + * 计算未付款金额 + * @param billing 账单数据 + * @returns 未付款金额 + */ +export function calculateRemainingAmount( + billing: Partial +): number { + const total = calculateTotalAmount(billing) + const paid = billing.amountPaid || 0 + return Math.max(total - paid, 0) +} + +/** + * 格式化导出格式为文本 + * @param format 导出格式 + * @returns 格式文本 + */ +export function formatExportFormat(format: ExportFormat): string { + const formatMap = { + [ExportFormat.Excel]: 'Excel', + [ExportFormat.Pdf]: 'PDF', + [ExportFormat.Csv]: 'CSV' + } + return formatMap[format] || '未知' +} + +/** + * 验证金额格式 + * @param amount 金额字符串 + * @returns 是否有效 + */ +export function validateAmount(amount: string): boolean { + const regex = /^\d+(\.\d{1,2})?$/ + return regex.test(amount) && parseFloat(amount) > 0 +} + +/** + * 格式化日期范围为查询参数 + * @param dateRange 日期范围(数组格式 [start, end]) + * @returns 查询参数 + */ +export function formatDateRangeParams(dateRange?: [string, string] | null): { + StartDate?: string + EndDate?: string +} { + if (!dateRange || dateRange.length !== 2) return {} + return { + StartDate: dateRange[0], + EndDate: dateRange[1] + } +} diff --git a/src/utils/constants/index.ts b/src/utils/constants/index.ts new file mode 100644 index 0000000..831be29 --- /dev/null +++ b/src/utils/constants/index.ts @@ -0,0 +1,8 @@ +/** + * 常量定义相关工具函数统一导出 + * + * @module utils/constants/index + * @author Art Design Pro Team + */ + +export * from './links' diff --git a/src/utils/constants/links.ts b/src/utils/constants/links.ts new file mode 100644 index 0000000..06d297e --- /dev/null +++ b/src/utils/constants/links.ts @@ -0,0 +1,35 @@ +/** + * 网站链接常量配置 + * 集中管理便于维护和更新链接地址 + * + * @module utils/constants/links + * @author Art Design Pro Team + */ +export const WEB_LINKS = { + // Github 主页 + GITHUB_HOME: 'https://github.com/Daymychen/art-design-pro', + + // 项目 Github 主页 + GITHUB: 'https://github.com/Daymychen/art-design-pro', + + // 个人博客 + BLOG: 'https://www.artd.pro', + + // 项目文档 + DOCS: 'https://www.artd.pro/docs/zh/', + + // 精简版本 + LiteVersion: 'https://www.artd.pro/docs/zh/guide/lite-version.html', + + // v2.6.1版本 + OldVersion: 'https://www.artd.pro/v2/', + + // 项目社区 + COMMUNITY: 'https://www.artd.pro/docs/zh/community/communicate.html', + + // 个人 Bilibili 主页 + BILIBILI: 'https://space.bilibili.com/425500936?spm_id_from=333.1007.0.0', + + // 项目介绍 + INTRODUCE: 'https://www.artd.pro/docs/zh/guide/introduce.html' +} diff --git a/src/utils/errorHandler.ts b/src/utils/errorHandler.ts new file mode 100644 index 0000000..a398ea0 --- /dev/null +++ b/src/utils/errorHandler.ts @@ -0,0 +1,70 @@ +import { ElMessage } from 'element-plus' +import { router } from '@/router' +import { $t } from '@/locales' +import { ApiStatus } from '@/utils/http/status' +import type { HttpError } from '@/utils/http/error' + +const collectValidationMessages = (payload: unknown): string[] => { + if (!payload) return [] + if (typeof payload === 'string') return [payload] + if (Array.isArray(payload)) return payload.map((item) => String(item)) + + if (typeof payload === 'object') { + const messages: string[] = [] + Object.entries(payload as Record).forEach(([field, value]) => { + if (Array.isArray(value)) { + value.forEach((item) => messages.push(`${field}: ${String(item)}`)) + return + } + + if (typeof value === 'string') { + messages.push(`${field}: ${value}`) + return + } + + if (value) { + messages.push(`${field}: ${String(value)}`) + } + }) + + return messages + } + + return [] +} + +const redirectForbidden = () => { + router.replace('/403').catch(() => {}) +} + +export const handleHttpError = (error: HttpError, showMessage: boolean = true) => { + if (error.code === ApiStatus.forbidden) { + redirectForbidden() + if (showMessage) { + ElMessage.error($t('httpMsg.forbidden')) + } + return + } + + if (!showMessage) { + return + } + + if (error.code === ApiStatus.conflict) { + ElMessage.warning($t('httpMsg.conflict')) + return + } + + if (error.code === ApiStatus.error || error.code === ApiStatus.unprocessableEntity) { + const messages = collectValidationMessages(error.data) + ElMessage.error(messages.length > 0 ? messages.join('; ') : error.message) + return + } + + if (error.code >= ApiStatus.internalServerError) { + ElMessage.error($t('httpMsg.internalServerError')) + return + } + + ElMessage.error(error.message) +} diff --git a/src/utils/form/index.ts b/src/utils/form/index.ts new file mode 100644 index 0000000..ed23a46 --- /dev/null +++ b/src/utils/form/index.ts @@ -0,0 +1,12 @@ +/** + * 表单工具函数统一导出 + * + * @module utils/form + * @author Art Design Pro Team + */ + +// 表单验证器 +export * from './validator' + +// 响应式布局 +export * from './responsive' diff --git a/src/utils/form/responsive.ts b/src/utils/form/responsive.ts new file mode 100644 index 0000000..c11df92 --- /dev/null +++ b/src/utils/form/responsive.ts @@ -0,0 +1,122 @@ +/** + * 表单响应式布局工具模块 + * + * 提供表单项在不同屏幕尺寸下的智能布局计算 + * + * ## 主要功能 + * + * - 响应式断点管理(xs/sm/md/lg/xl) + * - 表单列宽自动降级(避免小屏幕压缩) + * - 基于阈值的智能 span 计算 + * - 响应式计算器工厂函数 + * - 可配置的断点规则 + * + * ## 使用场景 + * + * - 表单组件响应式布局 + * - 搜索表单自适应 + * - 移动端表单优化 + * - 多列表单布局 + * + * ## 断点说明(基于 Element Plus Grid 24 栅格系统): + * - xs (手机): < 768px,小于 12 时降级为 24(满宽) + * - sm (平板): ≥ 768px,小于 12 时降级为 12(半宽) + * - md (中等屏幕): ≥ 992px,小于 8 时降级为 8(三分之一宽) + * - lg (大屏幕): ≥ 1200px,直接使用设置的 span + * - xl (超大屏幕): ≥ 1920px,直接使用设置的 span + * + * ## 核心功能 + * + * - calculateResponsiveSpan: 计算响应式列宽 + * - createResponsiveSpanCalculator: 创建 span 计算器(柯里化) + * + * @module utils/form/responsive + * @author Art Design Pro Team + */ + +/** + * 响应式断点类型 + */ +export type ResponsiveBreakpoint = 'xs' | 'sm' | 'md' | 'lg' | 'xl' + +/** + * 断点配置映射 + */ +interface BreakpointConfig { + /** 最小 span 阈值 */ + threshold: number + /** 降级后的 span 值 */ + fallback: number +} + +/** + * 响应式断点配置 + */ +const BREAKPOINT_CONFIG: Record = { + xs: { threshold: 12, fallback: 24 }, // 手机:小于 12 时使用满宽 + sm: { threshold: 12, fallback: 12 }, // 平板:小于 12 时使用半宽 + md: { threshold: 8, fallback: 8 }, // 中等屏幕:小于 8 时使用三分之一宽 + lg: null, // 大屏幕:直接使用设置的 span + xl: null // 超大屏幕:直接使用设置的 span +} + +/** + * 计算响应式列宽 + * + * 根据屏幕尺寸智能降级,避免小屏幕上表单项被压缩过小 + * + * @param itemSpan 表单项自定义的 span 值 + * @param defaultSpan 默认的 span 值 + * @param breakpoint 当前断点 + * @returns 计算后的 span 值 + * + * @example + * ```ts + * // 在 xs 断点下,span 为 6 会降级为 24(满宽) + * calculateResponsiveSpan(6, 6, 'xs') // 24 + * + * // 在 md 断点下,span 为 6 会降级为 8(三分之一宽) + * calculateResponsiveSpan(6, 6, 'md') // 8 + * + * // 在 lg 断点下,直接使用原始 span + * calculateResponsiveSpan(6, 6, 'lg') // 6 + * ``` + */ +export function calculateResponsiveSpan( + itemSpan: number | undefined, + defaultSpan: number, + breakpoint: ResponsiveBreakpoint +): number { + const finalSpan = itemSpan ?? defaultSpan + const config = BREAKPOINT_CONFIG[breakpoint] + + // 如果没有配置(lg/xl),直接返回原始 span + if (!config) { + return finalSpan + } + + // 如果 span 小于阈值,使用降级值 + return finalSpan >= config.threshold ? finalSpan : config.fallback +} + +/** + * 创建响应式 span 计算器 + * + * 返回一个函数,用于计算指定断点下的 span 值 + * + * @param defaultSpan 默认的 span 值 + * @returns span 计算函数 + * + * @example + * ```ts + * const getColSpan = createResponsiveSpanCalculator(6) + * getColSpan(undefined, 'xs') // 24 + * getColSpan(8, 'md') // 8 + * getColSpan(12, 'lg') // 12 + * ``` + */ +export function createResponsiveSpanCalculator(defaultSpan: number) { + return (itemSpan: number | undefined, breakpoint: ResponsiveBreakpoint): number => { + return calculateResponsiveSpan(itemSpan, defaultSpan, breakpoint) + } +} diff --git a/src/utils/form/validator.ts b/src/utils/form/validator.ts new file mode 100644 index 0000000..3670763 --- /dev/null +++ b/src/utils/form/validator.ts @@ -0,0 +1,316 @@ +/** + * 表单验证工具模块 + * + * 提供全面的表单字段验证功能 + * + * ## 主要功能 + * + * - 手机号码验证(中国大陆格式) + * - 固定电话验证(支持区号格式) + * - 用户账号验证(字母开头,支持数字和下划线) + * - 密码强度验证(普通密码、强密码) + * - 密码强度评估(弱、中、强) + * - IPv4 地址验证 + * - 邮箱地址验证(RFC 5322 标准) + * - URL 地址验证 + * - 身份证号码验证(18位,含校验码验证) + * - 银行卡号验证(Luhn 算法) + * - 字符串空格处理 + * + * ## 验证规则 + * + * - 手机号:1开头,第二位3-9,共11位 + * - 账号:字母开头,5-20位,支持字母数字下划线 + * - 普通密码:6-20位,必须包含字母和数字 + * - 强密码:8-20位,必须包含大小写字母、数字和特殊字符 + * - 身份证:18位,含出生日期和校验码验证 + * - 银行卡:13-19位,通过 Luhn 算法验证 + * + * @module utils/validation/formValidator + * @author Art Design Pro Team + */ + +/** + * 密码强度级别枚举 + */ +export enum PasswordStrength { + WEAK = '弱', + MEDIUM = '中', + STRONG = '强' +} + +/** + * 去除字符串首尾空格 + * @param value 待处理的字符串 + * @returns 返回去除首尾空格后的字符串 + */ +export function trimSpaces(value: string): string { + if (typeof value !== 'string') { + return '' + } + return value.trim() +} + +/** + * 验证手机号码(中国大陆) + * @param value 手机号码字符串 + * @returns 返回验证结果,true表示格式正确 + */ +export function validatePhone(value: string): boolean { + if (!value || typeof value !== 'string') { + return false + } + + // 中国大陆手机号码:1开头,第二位为3-9,共11位数字 + const phoneRegex = /^1[3-9]\d{9}$/ + return phoneRegex.test(value.trim()) +} + +/** + * 验证固定电话号码(中国大陆) + * @param value 电话号码字符串 + * @returns 返回验证结果,true表示格式正确 + */ +export function validateTelPhone(value: string): boolean { + if (!value || typeof value !== 'string') { + return false + } + + // 支持格式:区号-号码,如:010-12345678、0755-1234567 + const telRegex = /^0\d{2,3}-?\d{7,8}$/ + return telRegex.test(value.trim().replace(/\s+/g, '')) +} + +/** + * 验证用户账号 + * @param value 账号字符串 + * @returns 返回验证结果,true表示格式正确 + * @description 规则:字母开头,5-20位,支持字母、数字、下划线 + */ +export function validateAccount(value: string): boolean { + if (!value || typeof value !== 'string') { + return false + } + + // 字母开头,5-20位,支持字母、数字、下划线 + const accountRegex = /^[a-zA-Z][a-zA-Z0-9_]{4,19}$/ + return accountRegex.test(value.trim()) +} + +/** + * 验证密码 + * @param value 密码字符串 + * @returns 返回验证结果,true表示格式正确 + * @description 规则:6-20位,必须包含字母和数字 + */ +export function validatePassword(value: string): boolean { + if (!value || typeof value !== 'string') { + return false + } + + const trimmedValue = value.trim() + + // 长度检查 + if (trimmedValue.length < 6 || trimmedValue.length > 20) { + return false + } + + // 必须包含字母和数字 + const hasLetter = /[a-zA-Z]/.test(trimmedValue) + const hasNumber = /\d/.test(trimmedValue) + + return hasLetter && hasNumber +} + +/** + * 验证强密码 + * @param value 密码字符串 + * @returns 返回验证结果,true表示格式正确 + * @description 规则:8-20位,必须包含大写字母、小写字母、数字和特殊字符 + */ +export function validateStrongPassword(value: string): boolean { + if (!value || typeof value !== 'string') { + return false + } + + const trimmedValue = value.trim() + + // 长度检查 + if (trimmedValue.length < 8 || trimmedValue.length > 20) { + return false + } + + // 必须包含:大写字母、小写字母、数字、特殊字符 + const hasUpperCase = /[A-Z]/.test(trimmedValue) + const hasLowerCase = /[a-z]/.test(trimmedValue) + const hasNumber = /\d/.test(trimmedValue) + const hasSpecialChar = /[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]/.test(trimmedValue) + + return hasUpperCase && hasLowerCase && hasNumber && hasSpecialChar +} + +/** + * 获取密码强度 + * @param value 密码字符串 + * @returns 返回密码强度:弱、中、强 + * @description 弱:纯数字/纯字母/纯特殊字符;中:两种组合;强:三种或以上组合 + */ +export function getPasswordStrength(value: string): PasswordStrength { + if (!value || typeof value !== 'string') { + return PasswordStrength.WEAK + } + + const trimmedValue = value.trim() + + if (trimmedValue.length < 6) { + return PasswordStrength.WEAK + } + + const hasUpperCase = /[A-Z]/.test(trimmedValue) + const hasLowerCase = /[a-z]/.test(trimmedValue) + const hasNumber = /\d/.test(trimmedValue) + const hasSpecialChar = /[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]/.test(trimmedValue) + + const typeCount = [hasUpperCase, hasLowerCase, hasNumber, hasSpecialChar].filter(Boolean).length + + if (typeCount >= 3) { + return PasswordStrength.STRONG + } else if (typeCount >= 2) { + return PasswordStrength.MEDIUM + } else { + return PasswordStrength.WEAK + } +} + +/** + * 验证IPv4地址 + * @param value IP地址字符串 + * @returns 返回验证结果,true表示格式正确 + */ +export function validateIPv4Address(value: string): boolean { + if (!value || typeof value !== 'string') { + return false + } + + const trimmedValue = value.trim() + const ipRegex = /^((25[0-5]|2[0-4]\d|[01]?\d{1,2})\.){3}(25[0-5]|2[0-4]\d|[01]?\d{1,2})$/ + + if (!ipRegex.test(trimmedValue)) { + return false + } + + // 额外检查每个段是否在有效范围内 + const segments = trimmedValue.split('.') + return segments.every((segment) => { + const num = parseInt(segment, 10) + return num >= 0 && num <= 255 + }) +} + +/** + * 验证邮箱地址 + * @param value 邮箱地址字符串 + * @returns 返回验证结果,true表示格式正确 + */ +export function validateEmail(value: string): boolean { + if (!value || typeof value !== 'string') { + return false + } + + const trimmedValue = value.trim() + + // RFC 5322 标准的简化版邮箱正则 + const emailRegex = + /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/ + + return emailRegex.test(trimmedValue) && trimmedValue.length <= 254 +} + +/** + * 验证URL地址 + * @param value URL字符串 + * @returns 返回验证结果,true表示格式正确 + */ +export function validateURL(value: string): boolean { + if (!value || typeof value !== 'string') { + return false + } + + try { + new URL(value.trim()) + return true + } catch { + return false + } +} + +/** + * 验证身份证号码(中国大陆) + * @param value 身份证号码字符串 + * @returns 返回验证结果,true表示格式正确 + */ +export function validateChineseIDCard(value: string): boolean { + if (!value || typeof value !== 'string') { + return false + } + + const trimmedValue = value.trim() + + // 18位身份证号码正则 + const idCardRegex = + /^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/ + + if (!idCardRegex.test(trimmedValue)) { + return false + } + + // 验证校验码 + const weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2] + const checkCodes = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'] + + let sum = 0 + for (let i = 0; i < 17; i++) { + sum += parseInt(trimmedValue[i]) * weights[i] + } + + const checkCode = checkCodes[sum % 11] + return trimmedValue[17].toUpperCase() === checkCode +} + +/** + * 验证银行卡号 + * @param value 银行卡号字符串 + * @returns 返回验证结果,true表示格式正确 + */ +export function validateBankCard(value: string): boolean { + if (!value || typeof value !== 'string') { + return false + } + + const trimmedValue = value.trim().replace(/\s+/g, '') + + // 银行卡号通常为13-19位数字 + if (!/^\d{13,19}$/.test(trimmedValue)) { + return false + } + + // Luhn算法验证 + let sum = 0 + let shouldDouble = false + + for (let i = trimmedValue.length - 1; i >= 0; i--) { + let digit = parseInt(trimmedValue[i]) + + if (shouldDouble) { + digit *= 2 + if (digit > 9) { + digit = (digit % 10) + 1 + } + } + + sum += digit + shouldDouble = !shouldDouble + } + + return sum % 10 === 0 +} diff --git a/src/utils/http/error.ts b/src/utils/http/error.ts new file mode 100644 index 0000000..7c80da9 --- /dev/null +++ b/src/utils/http/error.ts @@ -0,0 +1,191 @@ +/** + * HTTP 错误处理模块 + * + * 提供统一的 HTTP 请求错误处理机制 + * + * ## 主要功能 + * + * - 自定义 HttpError 错误类,封装错误信息、状态码、时间戳等 + * - 错误拦截和转换,将 Axios 错误转换为标准的 HttpError + * - 错误消息国际化处理,根据状态码返回对应的多语言错误提示 + * - 错误日志记录,便于问题追踪和调试 + * - 错误和成功消息的统一展示 + * - 类型守卫函数,用于判断错误类型 + * + * ## 使用场景 + * + * - HTTP 请求拦截器中统一处理错误 + * - 业务代码中捕获和处理特定错误 + * - 错误日志收集和上报 + * + * @module utils/http/error + * @author Art Design Pro Team + */ +import { AxiosError } from 'axios' +import { ApiStatus } from './status' +import { $t } from '@/locales' + +// 错误响应接口 +export interface ErrorResponse { + /** 错误状态码 */ + code?: number + /** 错误消息 */ + message?: string + /** 兼容旧字段 */ + msg?: string + /** 错误附加数据 */ + data?: unknown + /** 后端错误详情 */ + errors?: unknown +} + +// 错误日志数据接口 +export interface ErrorLogData { + /** 错误状态码 */ + code: number + /** 错误消息 */ + message: string + /** 错误附加数据 */ + data?: unknown + /** 错误发生时间戳 */ + timestamp: string + /** 请求 URL */ + url?: string + /** 请求方法 */ + method?: string + /** 错误堆栈信息 */ + stack?: string +} + +// 自定义 HttpError 类 +export class HttpError extends Error { + public readonly code: number + public readonly data?: unknown + public readonly timestamp: string + public readonly url?: string + public readonly method?: string + + constructor( + message: string, + code: number, + options?: { + data?: unknown + url?: string + method?: string + } + ) { + super(message) + this.name = 'HttpError' + this.code = code + this.data = options?.data + this.timestamp = new Date().toISOString() + this.url = options?.url + this.method = options?.method + } + + public toLogData(): ErrorLogData { + return { + code: this.code, + message: this.message, + data: this.data, + timestamp: this.timestamp, + url: this.url, + method: this.method, + stack: this.stack + } + } +} + +/** + * 获取错误消息 + * @param status 错误状态码 + * @returns 错误消息 + */ +const getErrorMessage = (status: number): string => { + const errorMap: Record = { + [ApiStatus.unauthorized]: 'httpMsg.unauthorized', + [ApiStatus.forbidden]: 'httpMsg.forbidden', + [ApiStatus.notFound]: 'httpMsg.notFound', + [ApiStatus.methodNotAllowed]: 'httpMsg.methodNotAllowed', + [ApiStatus.conflict]: 'httpMsg.conflict', + [ApiStatus.unprocessableEntity]: 'httpMsg.validationFailed', + [ApiStatus.requestTimeout]: 'httpMsg.requestTimeout', + [ApiStatus.internalServerError]: 'httpMsg.internalServerError', + [ApiStatus.badGateway]: 'httpMsg.badGateway', + [ApiStatus.serviceUnavailable]: 'httpMsg.serviceUnavailable', + [ApiStatus.gatewayTimeout]: 'httpMsg.gatewayTimeout' + } + + return $t(errorMap[status] || 'httpMsg.internalServerError') +} + +/** + * 处理错误 + * @param error 错误对象 + * @returns 错误对象 + */ +export function handleError(error: AxiosError): never { + // 处理取消的请求 + if (error.code === 'ERR_CANCELED') { + console.warn('Request cancelled:', error.message) + throw new HttpError($t('httpMsg.requestCancelled'), ApiStatus.error) + } + + const statusCode = error.response?.status + const responseData = error.response?.data + const serverMessage = responseData?.message || responseData?.msg + const errorMessage = serverMessage || error.message + const requestConfig = error.config + const errorPayload = responseData?.errors ?? responseData?.data + + // 处理网络错误 + if (!error.response) { + throw new HttpError($t('httpMsg.networkError'), ApiStatus.error, { + url: requestConfig?.url, + method: requestConfig?.method?.toUpperCase() + }) + } + + // 处理 HTTP 状态码错误,优先使用后端返回的 message + const message = + serverMessage || + (statusCode ? getErrorMessage(statusCode) : errorMessage || $t('httpMsg.requestFailed')) + throw new HttpError(message, statusCode || ApiStatus.error, { + data: errorPayload, + url: requestConfig?.url, + method: requestConfig?.method?.toUpperCase() + }) +} + +/** + * 显示错误消息 + * @param error 错误对象 + * @param showMessage 是否显示错误消息 + */ +export function showError(error: HttpError, showMessage: boolean = true): void { + if (showMessage) { + ElMessage.error(error.message) + } + // 记录错误日志 + console.error('[HTTP Error]', error.toLogData()) +} + +/** + * 显示成功消息 + * @param message 成功消息 + * @param showMessage 是否显示消息 + */ +export function showSuccess(message: string, showMessage: boolean = true): void { + if (showMessage) { + ElMessage.success(message) + } +} + +/** + * 判断是否为 HttpError 类型 + * @param error 错误对象 + * @returns 是否为 HttpError 类型 + */ +export const isHttpError = (error: unknown): error is HttpError => { + return error instanceof HttpError +} diff --git a/src/utils/http/index.ts b/src/utils/http/index.ts new file mode 100644 index 0000000..f75387a --- /dev/null +++ b/src/utils/http/index.ts @@ -0,0 +1,267 @@ +/** + * HTTP 请求封装模块 + * 基于 Axios 封装的 HTTP 请求工具,提供统一的请求/响应处理 + * + * ## 主要功能 + * + * - 请求/响应拦截器(自动添加 Token、统一错误处理) + * - 401 未授权自动登出(带防抖机制) + * - 请求失败自动重试(可配置) + * - 统一的成功/错误消息提示 + * - 支持 GET/POST/PUT/DELETE 等常用方法 + * + * @module utils/http + * @author Art Design Pro Team + */ + +import axios, { AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios' +import JSONBig from 'json-bigint' +import { useUserStore } from '@/store/modules/user' +import { ApiStatus } from './status' +import { ErrorResponse, HttpError, handleError, showSuccess } from './error' +import { handleHttpError } from '@/utils/errorHandler' +import { $t } from '@/locales' +import { BaseResponse } from '@/types' +import { StorageConfig } from '@/utils/storage' + +/** 请求配置常量 */ +const REQUEST_TIMEOUT = 15000 +const LOGOUT_DELAY = 500 +const MAX_RETRIES = 0 +const RETRY_DELAY = 1000 +const UNAUTHORIZED_DEBOUNCE_TIME = 3000 + +/** 401防抖状态 */ +let isUnauthorizedErrorShown = false +let unauthorizedTimer: NodeJS.Timeout | null = null + +/** 扩展 AxiosRequestConfig */ +interface ExtendedAxiosRequestConfig extends AxiosRequestConfig { + showErrorMessage?: boolean + showSuccessMessage?: boolean + skipTenantHeader?: boolean +} + +const { VITE_API_URL, VITE_WITH_CREDENTIALS } = import.meta.env +const jsonBigParser = JSONBig({ storeAsString: true }) + +/** Axios实例 */ +const axiosInstance = axios.create({ + timeout: REQUEST_TIMEOUT, + baseURL: VITE_API_URL, + withCredentials: VITE_WITH_CREDENTIALS === 'true', + validateStatus: (status) => status >= 200 && status < 300, + transformResponse: [ + (data, headers) => { + const contentType = headers['content-type'] + if (contentType?.includes('application/json')) { + try { + // 1. 使用 json-bigint 解析,避免 Snowflake 等长整型精度丢失 + return jsonBigParser.parse(data) + } catch { + return data + } + } + return data + } + ] +}) + +/** 请求拦截器 */ +axiosInstance.interceptors.request.use( + (request: InternalAxiosRequestConfig) => { + const userStore = useUserStore() + const { accessToken } = userStore + + // 1. 追加鉴权 Token + if (accessToken) { + const hasBearer = accessToken.toLowerCase().startsWith('bearer ') + request.headers.set('Authorization', hasBearer ? accessToken : `Bearer ${accessToken}`) + } + + // 2. 处理租户 Header(允许业务显式跳过) + const { skipTenantHeader } = request as ExtendedAxiosRequestConfig + if (skipTenantHeader) { + request.headers.delete('X-Tenant-Id') + request.headers.delete('x-tenant-id') + } else { + const headerTenantId = + request.headers?.get?.('X-Tenant-Id') ?? + request.headers?.get?.('x-tenant-id') ?? + (request.headers as Record | undefined)?.['X-Tenant-Id'] ?? + (request.headers as Record | undefined)?.['x-tenant-id'] + if (!headerTenantId) { + const tenantId = + localStorage.getItem(StorageConfig.TENANT_ID_KEY) || userStore.info?.tenantId + if (tenantId) { + request.headers.set('X-Tenant-Id', String(tenantId)) + } + } + } + + // 3. 自动补齐 Content-Type 并序列化 JSON + if (request.data && !(request.data instanceof FormData) && !request.headers['Content-Type']) { + request.headers.set('Content-Type', 'application/json') + request.data = JSON.stringify(request.data) + } + + return request + }, + (error) => { + handleHttpError(createHttpError($t('httpMsg.requestConfigError'), ApiStatus.error), true) + return Promise.reject(error) + } +) + +/** 响应拦截器 */ +axiosInstance.interceptors.response.use( + (response: AxiosResponse) => { + const { success, code, message, msg, data, errors } = response.data || {} + const bizCode = code ?? response.status ?? ApiStatus.error + const isSuccess = typeof success === 'boolean' ? success : bizCode === ApiStatus.success + + if (isSuccess) return response + if (bizCode === ApiStatus.unauthorized) handleUnauthorizedError(message || msg) + + throw createHttpError(message || msg || $t('httpMsg.requestFailed'), bizCode, { + data: errors ?? data, + url: response.config.url, + method: response.config.method?.toUpperCase() + }) + }, + (error) => { + if (error.response?.status === ApiStatus.unauthorized) { + const resData = error.response.data as ErrorResponse | undefined + handleUnauthorizedError(resData?.message || resData?.msg) + } + return Promise.reject(handleError(error)) + } +) + +/** 统一创建HttpError */ +function createHttpError( + message: string, + code: number, + options?: { data?: unknown; url?: string; method?: string } +) { + return new HttpError(message, code, options) +} + +/** 处理401错误(带防抖) */ +function handleUnauthorizedError(message?: string): never { + const error = createHttpError(message || $t('httpMsg.unauthorized'), ApiStatus.unauthorized) + + if (!isUnauthorizedErrorShown) { + isUnauthorizedErrorShown = true + logOut() + + unauthorizedTimer = setTimeout(resetUnauthorizedError, UNAUTHORIZED_DEBOUNCE_TIME) + + handleHttpError(error, true) + throw error + } + + throw error +} + +/** 重置401防抖状态 */ +function resetUnauthorizedError() { + isUnauthorizedErrorShown = false + if (unauthorizedTimer) clearTimeout(unauthorizedTimer) + unauthorizedTimer = null +} + +/** 退出登录函数 */ +function logOut() { + setTimeout(() => { + useUserStore().logOut() + }, LOGOUT_DELAY) +} + +/** 是否需要重试 */ +function shouldRetry(statusCode: number) { + return [ + ApiStatus.requestTimeout, + ApiStatus.internalServerError, + ApiStatus.badGateway, + ApiStatus.serviceUnavailable, + ApiStatus.gatewayTimeout + ].includes(statusCode) +} + +/** 请求重试逻辑 */ +async function retryRequest( + config: ExtendedAxiosRequestConfig, + retries: number = MAX_RETRIES +): Promise { + try { + return await request(config) + } catch (error) { + if (retries > 0 && error instanceof HttpError && shouldRetry(error.code)) { + await delay(RETRY_DELAY) + return retryRequest(config, retries - 1) + } + throw error + } +} + +/** 延迟函数 */ +function delay(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)) +} + +/** 请求函数 */ +async function request(config: ExtendedAxiosRequestConfig): Promise { + // POST | PUT 参数自动填充 + if ( + ['POST', 'PUT'].includes(config.method?.toUpperCase() || '') && + config.params && + !config.data + ) { + config.data = config.params + config.params = undefined + } + + try { + const res = await axiosInstance.request>(config) + + // 显示成功消息 + if (config.showSuccessMessage && (res.data.message || res.data.msg)) { + showSuccess((res.data.message || res.data.msg) as string) + } + + // 非 JSON 响应(如文件流)直接返回原始数据 + if (config.responseType && config.responseType !== 'json') { + return res.data as unknown as T + } + + return res.data.data as T + } catch (error) { + if (error instanceof HttpError && error.code !== ApiStatus.unauthorized) { + const showMsg = config.showErrorMessage !== false + handleHttpError(error, showMsg) + } + return Promise.reject(error) + } +} + +/** API方法集合 */ +const api = { + get(config: ExtendedAxiosRequestConfig) { + return retryRequest({ ...config, method: 'GET' }) + }, + post(config: ExtendedAxiosRequestConfig) { + return retryRequest({ ...config, method: 'POST' }) + }, + put(config: ExtendedAxiosRequestConfig) { + return retryRequest({ ...config, method: 'PUT' }) + }, + del(config: ExtendedAxiosRequestConfig) { + return retryRequest({ ...config, method: 'DELETE' }) + }, + request(config: ExtendedAxiosRequestConfig) { + return retryRequest(config) + } +} + +export default api diff --git a/src/utils/http/status.ts b/src/utils/http/status.ts new file mode 100644 index 0000000..8692d72 --- /dev/null +++ b/src/utils/http/status.ts @@ -0,0 +1,20 @@ +/** + * 接口状态码 + */ +export enum ApiStatus { + success = 200, // 成功 + error = 400, // 错误 + unauthorized = 401, // 未授权 + forbidden = 403, // 禁止访问 + notFound = 404, // 未找到 + methodNotAllowed = 405, // 方法不允许 + conflict = 409, // 资源冲突 + unprocessableEntity = 422, // 参数验证失败 + requestTimeout = 408, // 请求超时 + internalServerError = 500, // 服务器错误 + notImplemented = 501, // 未实现 + badGateway = 502, // 网关错误 + serviceUnavailable = 503, // 服务不可用 + gatewayTimeout = 504, // 网关超时 + httpVersionNotSupported = 505 // HTTP版本不支持 +} diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..f1e1b77 --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,34 @@ +/** + * Utils 工具函数统一导出 + * 提供向后兼容性和便捷导入 + * + * @module utils/index + * @author Art Design Pro Team + */ + +// UI 相关 +export * from './ui' + +// 路由相关 +export * from './router' + +// 路由导航相关 +export * from './navigation' + +// 系统管理相关 +export * from './sys' + +// 常量定义相关 +export * from './constants' + +// 存储相关 +export * from './storage' + +// HTTP 相关 +export * from './http' + +// 表单相关 +export * from './form' + +// socket 相关 +export * from './socket' diff --git a/src/utils/navigation/index.ts b/src/utils/navigation/index.ts new file mode 100644 index 0000000..0b84e78 --- /dev/null +++ b/src/utils/navigation/index.ts @@ -0,0 +1,10 @@ +/** + * 路由和导航相关工具函数统一导出 + * + * @module utils/navigation/index + * @author Art Design Pro Team + */ + +export * from './jump' +export * from './worktab' +export * from './route' diff --git a/src/utils/navigation/jump.ts b/src/utils/navigation/jump.ts new file mode 100644 index 0000000..6391298 --- /dev/null +++ b/src/utils/navigation/jump.ts @@ -0,0 +1,74 @@ +/** + * 导航跳转工具模块 + * + * 提供统一的页面跳转和导航功能 + * + * ## 主要功能 + * + * - 外部链接打开(新窗口) + * - 菜单项跳转处理(支持内部路由和外部链接) + * - iframe 页面跳转支持 + * - 递归查找并跳转到第一个可见的子菜单 + * - 智能判断跳转目标类型(外部链接/内部路由) + * + * @module utils/navigation/jump + * @author Art Design Pro Team + */ +import { AppRouteRecord } from '@/types/router' +import { router } from '@/router' +import { useSettingStore } from '@/store/modules/setting' + +// 打开外部链接 +export const openExternalLink = (link: string) => { + window.open(link, '_blank') +} + +/** + * 菜单跳转 + * @param item 菜单项 + * @param jumpToFirst 是否跳转到第一个子菜单 + * @returns + */ +export const handleMenuJump = (item: AppRouteRecord, jumpToFirst: boolean = false) => { + // 处理外部链接 + const { link, isIframe } = item.meta + if (link && !isIframe) { + return openExternalLink(link) + } + + const navigateWithRefresh = (targetPath?: string) => { + if (!targetPath) return + const currentPath = router.currentRoute.value.path + if (targetPath === currentPath) return + return router.push(targetPath).then(() => { + if (router.currentRoute.value.path === targetPath) { + useSettingStore().reload() + } + }) + } + + // 如果不需要跳转到第一个子菜单,或者没有子菜单,直接跳转当前路径 + if (!jumpToFirst || !item.children?.length) { + return navigateWithRefresh(item.path) + } + + // 递归查找第一个可见的叶子节点菜单 + const findFirstLeafMenu = (items: AppRouteRecord[]): AppRouteRecord => { + for (const child of items) { + if (!child.meta.isHide) { + return child.children?.length ? findFirstLeafMenu(child.children) : child + } + } + return items[0] + } + + const firstChild = findFirstLeafMenu(item.children) + + // 如果第一个子菜单是外部链接则打开新窗口 + if (firstChild.meta?.link) { + return openExternalLink(firstChild.meta.link) + } + + // 跳转到子菜单路径 + return navigateWithRefresh(firstChild.path) +} diff --git a/src/utils/navigation/route.ts b/src/utils/navigation/route.ts new file mode 100644 index 0000000..9ca4f29 --- /dev/null +++ b/src/utils/navigation/route.ts @@ -0,0 +1,78 @@ +/** + * 路由工具模块 + * + * 提供路由处理和菜单路径相关的工具函数 + * + * ## 主要功能 + * + * - iframe 路由检测,判断是否为外部嵌入页面 + * - 菜单项有效性验证,过滤隐藏和无效菜单 + * - 路径标准化处理,统一路径格式 + * - 递归查找菜单树中第一个有效路径 + * - 支持多级嵌套菜单的路径解析 + * + * ## 使用场景 + * + * - 系统初始化时获取默认跳转路径 + * - 菜单权限过滤后获取首个可访问页面 + * - 路由重定向逻辑处理 + * - iframe 页面特殊处理 + * + * @module utils/navigation/route + * @author Art Design Pro Team + */ + +import { AppRouteRecord } from '@/types' + +// 检查是否为 iframe 路由 +export function isIframe(url: string): boolean { + return url.startsWith('/outside/iframe/') +} + +/** + * 验证菜单项是否有效 + * @param menuItem 菜单项 + * @returns 是否为有效菜单项 + */ +const isValidMenuItem = (menuItem: AppRouteRecord): boolean => { + return !!(menuItem.path && menuItem.path.trim() && !menuItem.meta?.isHide) +} + +/** + * 标准化路径格式 + * @param path 路径 + * @returns 标准化后的路径 + */ +const normalizePath = (path: string): string => { + return path.startsWith('/') ? path : `/${path}` +} + +/** + * 递归获取菜单的第一个有效路径 + * @param menuList 菜单列表 + * @returns 第一个有效路径,如果没有找到则返回空字符串 + */ +export const getFirstMenuPath = (menuList: AppRouteRecord[]): string => { + if (!Array.isArray(menuList) || menuList.length === 0) { + return '' + } + + for (const menuItem of menuList) { + if (!isValidMenuItem(menuItem)) { + continue + } + + // 如果有子菜单,优先查找子菜单 + if (menuItem.children?.length) { + const childPath = getFirstMenuPath(menuItem.children) + if (childPath) { + return childPath + } + } + + // 返回当前菜单项的标准化路径 + return normalizePath(menuItem.path!) + } + + return '' +} diff --git a/src/utils/navigation/worktab.ts b/src/utils/navigation/worktab.ts new file mode 100644 index 0000000..6db6a77 --- /dev/null +++ b/src/utils/navigation/worktab.ts @@ -0,0 +1,67 @@ +/** + * 工作标签页管理模块 + * + * 提供工作标签页(Worktab)的自动管理功能 + * + * ## 主要功能 + * + * - 根据路由导航自动创建和更新工作标签页 + * - iframe 页面标签页特殊处理 + * - 标签页信息提取(标题、路径、缓存状态等) + * - 固定标签页支持 + * - 根据系统设置控制标签页显示 + * - 首页标签页特殊处理 + * + * ## 使用场景 + * + * - 路由守卫中自动创建标签页 + * - 页面切换时更新标签页状态 + * - 多标签页导航系统 + * + * @module utils/navigation/worktab + * @author Art Design Pro Team + */ +import { useWorktabStore } from '@/store/modules/worktab' +import { RouteLocationNormalized } from 'vue-router' +import { isIframe } from './route' +import { useSettingStore } from '@/store/modules/setting' +import { IframeRouteManager } from '@/router/core' +import { useCommon } from '@/hooks/core/useCommon' + +/** + * 根据当前路由信息设置工作标签页(worktab) + * @param to 当前路由对象 + */ +export const setWorktab = (to: RouteLocationNormalized): void => { + const worktabStore = useWorktabStore() + const { meta, path, name, params, query } = to + if (!meta.isHideTab) { + // 如果是 iframe 页面,则特殊处理工作标签页 + if (isIframe(path)) { + const iframeRoute = IframeRouteManager.getInstance().findByPath(to.path) + + if (iframeRoute?.meta) { + worktabStore.openTab({ + title: iframeRoute.meta.title, + icon: meta.icon as string, + path, + name: name as string, + keepAlive: meta.keepAlive as boolean, + params, + query + }) + } + } else if (useSettingStore().showWorkTab || path === useCommon().homePath.value) { + worktabStore.openTab({ + title: meta.title as string, + icon: meta.icon as string, + path, + name: name as string, + keepAlive: meta.keepAlive as boolean, + params, + query, + fixedTab: meta.fixedTab as boolean + }) + } + } +} diff --git a/src/utils/router.ts b/src/utils/router.ts new file mode 100644 index 0000000..8c838ff --- /dev/null +++ b/src/utils/router.ts @@ -0,0 +1,61 @@ +/** + * 路由工具函数 + * + * 提供路由相关的工具函数 + * + * @module utils/router + */ +import { RouteLocationNormalized, RouteRecordRaw } from 'vue-router' +import AppConfig from '@/config' +import NProgress from 'nprogress' +import 'nprogress/nprogress.css' +import i18n, { $t } from '@/locales' + +/** 扩展的路由配置类型 */ +export type AppRouteRecordRaw = RouteRecordRaw & { + hidden?: boolean +} + +/** 顶部进度条配置 */ +export const configureNProgress = () => { + NProgress.configure({ + easing: 'ease', + speed: 600, + showSpinner: false, + parent: 'body' + }) +} + +/** + * 设置页面标题,根据路由元信息和系统信息拼接标题 + * @param to 当前路由对象 + */ +export const setPageTitle = (to: RouteLocationNormalized): void => { + const { title } = to.meta + if (title) { + setTimeout(() => { + document.title = `${formatMenuTitle(String(title))} - ${AppConfig.systemInfo.name}` + }, 150) + } +} + +/** + * 格式化菜单标题 + * @param title 菜单标题,可以是 i18n 的 key,也可以是字符串 + * @returns 格式化后的菜单标题 + */ +export const formatMenuTitle = (title: string): string => { + if (title) { + if (title.startsWith('menus.')) { + // 使用 te() 方法检查翻译键值是否存在,避免控制台警告 + if (i18n.global.te(title)) { + return $t(title) + } else { + // 如果翻译不存在,返回键值的最后部分作为fallback + return title.split('.').pop() || title + } + } + return title + } + return '' +} diff --git a/src/utils/socket/index.ts b/src/utils/socket/index.ts new file mode 100644 index 0000000..77d46ad --- /dev/null +++ b/src/utils/socket/index.ts @@ -0,0 +1,388 @@ +interface WebSocketOptions { + url?: string + messageHandler: (event: MessageEvent) => void + reconnectInterval?: number // 重连间隔(ms) + heartbeatInterval?: number // 心跳检测间隔(ms) + pingInterval?: number // 发送ping间隔(ms) + reconnectTimeout?: number // 重连超时时间(ms) + maxReconnectAttempts?: number // 最大重连次数 + connectionTimeout?: number // 连接建立超时时间(ms) +} + +export default class WebSocketClient { + private static instance: WebSocketClient | null = null + private ws: WebSocket | null = null + private url: string + private messageHandler: (event: MessageEvent) => void + private reconnectInterval: number + private heartbeatInterval: number + private pingInterval: number + private reconnectTimeout: number + private maxReconnectAttempts: number + private connectionTimeout: number + private reconnectAttempts: number = 0 // 当前重连次数 + + // 消息队列 - 缓存连接建立前的消息 + private messageQueue: Array = [] + + // 定时器 + private detectionTimer: NodeJS.Timeout | null = null + private timeoutTimer: NodeJS.Timeout | null = null + private reconnectTimer: NodeJS.Timeout | null = null + private pingTimer: NodeJS.Timeout | null = null + private connectionTimer: NodeJS.Timeout | null = null // 连接超时定时器 + + // 状态标识 + private isConnected: boolean = false + private isConnecting: boolean = false // 是否正在连接中 + private stopReconnect: boolean = false + + private constructor(options: WebSocketOptions) { + this.url = options.url || (process.env.VUE_APP_LOGIN_WEBSOCKET as string) + this.messageHandler = options.messageHandler + this.reconnectInterval = options.reconnectInterval || 20 * 1000 // 默认20秒 + this.heartbeatInterval = options.heartbeatInterval || 5 * 1000 // 默认5秒 + this.pingInterval = options.pingInterval || 10 * 1000 // 默认10秒 + this.reconnectTimeout = options.reconnectTimeout || 30 * 1000 // 默认30秒 + this.maxReconnectAttempts = options.maxReconnectAttempts || 10 // 默认最多重连10次 + this.connectionTimeout = options.connectionTimeout || 10 * 1000 // 连接超时10秒 + } + + // 单例模式获取实例 + static getInstance(options: WebSocketOptions): WebSocketClient { + if (!WebSocketClient.instance) { + WebSocketClient.instance = new WebSocketClient(options) + } else { + // 更新消息处理器 + WebSocketClient.instance.messageHandler = options.messageHandler + // 如果提供了新的URL,则更新并重新连接 + if (options.url && WebSocketClient.instance.url !== options.url) { + WebSocketClient.instance.url = options.url + WebSocketClient.instance.reconnectAttempts = 0 + WebSocketClient.instance.init() + } + } + return WebSocketClient.instance + } + + // 初始化连接 + init(): void { + // 如果正在连接中,不重复连接 + if (this.isConnecting) { + console.log('正在建立WebSocket连接中...') + return + } + + // 如果已连接,不重复连接 + if (this.ws?.readyState === WebSocket.OPEN) { + console.warn('WebSocket连接已存在') + this.flushMessageQueue() // 确保队列中的消息被发送 + return + } + + try { + this.isConnecting = true + this.reconnectAttempts = 0 // 重置重连次数 + this.ws = new WebSocket(this.url) + + // 设置连接超时检测 + this.clearTimer('connectionTimer') + this.connectionTimer = setTimeout(() => { + console.error(`WebSocket连接超时 (${this.connectionTimeout}ms):${this.url}`) + this.handleConnectionTimeout() + }, this.connectionTimeout) + + this.ws.onopen = (event) => this.handleOpen(event) + this.ws.onmessage = (event) => this.handleMessage(event) + this.ws.onclose = (event) => this.handleClose(event) + this.ws.onerror = (event) => this.handleError(event) + } catch (error) { + console.error('WebSocket初始化失败:', error) + this.isConnecting = false + this.reconnect() + } + } + + // 处理连接超时 + private handleConnectionTimeout(): void { + if (this.ws?.readyState !== WebSocket.OPEN) { + console.error('WebSocket连接超时,强制关闭连接') + this.ws?.close(1000, 'Connection timeout') + this.isConnecting = false + this.reconnect() + } + } + + // 关闭连接 + close(force?: boolean): void { + this.clearAllTimers() + this.stopReconnect = true + this.isConnecting = false + + if (this.ws) { + // 1000 表示正常关闭 + this.ws.close(force ? 1001 : 1000, force ? 'Force closed' : 'Normal close') + this.ws = null + } + + this.isConnected = false + } + + // 发送消息 - 增加消息队列 + send(data: string | ArrayBufferLike | Blob | ArrayBufferView, immediate: boolean = false): void { + // 如果要求立即发送且未连接,则直接报错 + if (immediate && (!this.ws || this.ws.readyState !== WebSocket.OPEN)) { + console.error('WebSocket未连接,无法立即发送消息') + return + } + + // 如果未连接且不要求立即发送,则加入消息队列 + if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { + console.log('WebSocket未连接,消息已加入队列等待发送') + this.messageQueue.push(data) + // 如果未在重连中,则尝试重连 + if (!this.isConnecting && !this.stopReconnect) { + this.init() + } + return + } + + try { + this.ws.send(data) + } catch (error) { + console.error('WebSocket发送消息失败:', error) + // 发送失败时将消息加入队列,等待重连后重试 + this.messageQueue.push(data) + this.reconnect() + } + } + + // 发送队列中的消息 + private flushMessageQueue(): void { + if (this.messageQueue.length > 0 && this.ws?.readyState === WebSocket.OPEN) { + console.log(`发送队列中的${this.messageQueue.length}条消息`) + while (this.messageQueue.length > 0) { + const data = this.messageQueue.shift() + if (data) { + try { + this.ws?.send(data) + } catch (error) { + console.error('发送队列消息失败:', error) + // 如果发送失败,将消息放回队列头部 + if (data) this.messageQueue.unshift(data) + break + } + } + } + } + } + + // 处理连接打开 + private handleOpen(event: Event): void { + console.log('WebSocket连接成功', event) + this.clearTimer('connectionTimer') // 清除连接超时定时器 + this.isConnected = true + this.isConnecting = false + this.stopReconnect = false + this.reconnectAttempts = 0 // 重置重连次数 + this.startHeartbeat() + this.startPing() + this.flushMessageQueue() // 发送队列中的消息 + } + + // 处理收到的消息 + private handleMessage(event: MessageEvent): void { + console.log('收到WebSocket消息:', event) + this.resetHeartbeat() + this.messageHandler(event) + } + + // 处理连接关闭 + private handleClose(event: CloseEvent): void { + console.log( + `WebSocket断开: 代码=${event.code}, 原因=${event.reason}, 干净关闭=${event.wasClean}` + ) + + // 1000 是正常关闭代码 + const isNormalClose = event.code === 1000 + + this.isConnected = false + this.isConnecting = false + this.clearAllTimers() + + if (!this.stopReconnect && !isNormalClose) { + this.reconnect() + } + } + + // 处理错误 - 增加详细错误信息 + private handleError(event: Event): void { + console.error('WebSocket连接错误:') + console.error('错误事件:', event) + console.error( + '当前连接状态:', + this.ws?.readyState ? this.getReadyStateText(this.ws.readyState) : '未初始化' + ) + + this.isConnected = false + this.isConnecting = false + + // 只有在未停止重连的情况下才尝试重连 + if (!this.stopReconnect) { + this.reconnect() + } + } + + // 转换连接状态为文本描述 + private getReadyStateText(state: number): string { + switch (state) { + case WebSocket.CONNECTING: + return 'CONNECTING (0) - 正在连接' + case WebSocket.OPEN: + return 'OPEN (1) - 已连接' + case WebSocket.CLOSING: + return 'CLOSING (2) - 正在关闭' + case WebSocket.CLOSED: + return 'CLOSED (3) - 已关闭' + default: + return `未知状态 (${state})` + } + } + + // 开始心跳检测 + private startHeartbeat(): void { + this.clearTimer('detectionTimer') + this.clearTimer('timeoutTimer') + + this.detectionTimer = setTimeout(() => { + this.isConnected = this.ws?.readyState === WebSocket.OPEN + + if (!this.isConnected) { + console.warn('WebSocket心跳检测失败,尝试重连') + this.reconnect() + + this.timeoutTimer = setTimeout(() => { + console.warn('WebSocket重连超时') + this.close() + }, this.reconnectTimeout) + } + }, this.heartbeatInterval) + } + + // 重置心跳检测 + private resetHeartbeat(): void { + this.clearTimer('detectionTimer') + this.clearTimer('timeoutTimer') + this.startHeartbeat() + } + + // 开始发送ping消息 + private startPing(): void { + this.clearTimer('pingTimer') + + this.pingTimer = setInterval(() => { + if (this.ws?.readyState !== WebSocket.OPEN) { + console.warn('WebSocket未连接,停止发送ping') + this.clearTimer('pingTimer') + this.reconnect() + return + } + + try { + this.ws.send('ping') + console.log('发送ping消息') + } catch (error) { + console.error('发送ping消息失败:', error) + this.clearTimer('pingTimer') + this.reconnect() + } + }, this.pingInterval) + } + + // 重连 - 增加重连次数限制 + private reconnect(): void { + if (this.stopReconnect || this.isConnecting) { + return + } + + // 检查是否超过最大重连次数 + if (this.reconnectAttempts >= this.maxReconnectAttempts) { + console.error(`已达到最大重连次数(${this.maxReconnectAttempts}),停止重连`) + this.close(true) + return + } + + this.reconnectAttempts++ + this.stopReconnect = true + this.close(true) + + const delay = this.calculateReconnectDelay() + console.log( + `将在${delay / 1000}秒后尝试重新连接(第${this.reconnectAttempts}/${this.maxReconnectAttempts}次)` + ) + + this.clearTimer('reconnectTimer') + this.reconnectTimer = setTimeout(() => { + console.log(`尝试重新连接WebSocket(第${this.reconnectAttempts}次)`) + this.init() + this.stopReconnect = false + }, delay) + } + + // 计算重连延迟 - 指数退避策略 + private calculateReconnectDelay(): number { + // 基础延迟 + 随机值,避免多个客户端同时重连 + const jitter = Math.random() * 1000 // 0-1秒的随机延迟 + const baseDelay = Math.min( + this.reconnectInterval * Math.pow(1.5, this.reconnectAttempts - 1), + this.reconnectInterval * 5 + ) + return baseDelay + jitter + } + + // 清除指定定时器 + private clearTimer( + timerName: + | 'detectionTimer' + | 'timeoutTimer' + | 'reconnectTimer' + | 'pingTimer' + | 'connectionTimer' + ): void { + if (this[timerName]) { + clearTimeout(this[timerName] as NodeJS.Timeout) + this[timerName] = null + } + } + + // 清除所有定时器 + private clearAllTimers(): void { + this.clearTimer('detectionTimer') + this.clearTimer('timeoutTimer') + this.clearTimer('reconnectTimer') + this.clearTimer('pingTimer') + this.clearTimer('connectionTimer') + } + + // 获取当前连接状态 + get isWebSocketConnected(): boolean { + return this.isConnected + } + + // 获取当前连接状态文本 + get connectionStatusText(): string { + if (this.isConnecting) return '正在连接' + if (this.isConnected) return '已连接' + if (this.reconnectAttempts > 0 && !this.stopReconnect) + return `重连中(${this.reconnectAttempts}/${this.maxReconnectAttempts})` + return '已断开' + } + + // 销毁实例 + static destroyInstance(): void { + if (WebSocketClient.instance) { + WebSocketClient.instance.close() + WebSocketClient.instance = null + } + } +} diff --git a/src/utils/storage/index.ts b/src/utils/storage/index.ts new file mode 100644 index 0000000..a4366f0 --- /dev/null +++ b/src/utils/storage/index.ts @@ -0,0 +1,7 @@ +/** + * 存储相关工具函数统一导出 + */ + +export * from './storage' +export * from './storage-config' +export * from './storage-key-manager' diff --git a/src/utils/storage/remember-login.ts b/src/utils/storage/remember-login.ts new file mode 100644 index 0000000..0a55466 --- /dev/null +++ b/src/utils/storage/remember-login.ts @@ -0,0 +1,77 @@ +/** + * 登录记住密码存储工具 + * + * 负责对登录账号与密码进行加密存储与读取 + * + * @module utils/storage/remember-login + */ +import CryptoJS from 'crypto-js' +import { StorageKeyManager } from '@/utils/storage/storage-key-manager' + +const ENCRYPT_KEY = import.meta.env.VITE_LOCK_ENCRYPT_KEY +const REMEMBER_LOGIN_STORE_ID = 'remember-login' + +// 使用 StorageKeyManager 生成带版本的存储键,避免硬编码 +const storageKeyManager = new StorageKeyManager() +const REMEMBER_LOGIN_KEY = storageKeyManager.getStorageKey(REMEMBER_LOGIN_STORE_ID) + +export interface RememberLoginPayload { + account: string + phone?: string + password: string +} + +/** + * 保存加密后的登录信息 + */ +export const saveRememberLogin = (payload: RememberLoginPayload) => { + if (!payload.account || !payload.password) return + + const encryptedPassword = CryptoJS.AES.encrypt(payload.password, ENCRYPT_KEY).toString() + const data = { + account: payload.account, + phone: payload.phone, + password: encryptedPassword, + updatedAt: Date.now() + } + + localStorage.setItem(REMEMBER_LOGIN_KEY, JSON.stringify(data)) +} + +/** + * 读取并解密已保存的登录信息 + */ +export const loadRememberLogin = (): RememberLoginPayload | null => { + const storedValue = localStorage.getItem(REMEMBER_LOGIN_KEY) + if (!storedValue) return null + + try { + const parsed = JSON.parse(storedValue) as { + account?: string + phone?: string + password?: string + } + if (!parsed.account || !parsed.password) return null + + const decryptedPassword = CryptoJS.AES.decrypt(parsed.password, ENCRYPT_KEY).toString( + CryptoJS.enc.Utf8 + ) + + return { + account: parsed.account, + phone: parsed.phone, + password: decryptedPassword + } + } catch (error) { + console.warn('[Login] 解析记住密码数据失败:', error) + clearRememberLogin() + return null + } +} + +/** + * 清除记住的登录信息 + */ +export const clearRememberLogin = () => { + localStorage.removeItem(REMEMBER_LOGIN_KEY) +} diff --git a/src/utils/storage/storage-config.ts b/src/utils/storage/storage-config.ts new file mode 100644 index 0000000..2571f6d --- /dev/null +++ b/src/utils/storage/storage-config.ts @@ -0,0 +1,125 @@ +/** + * 存储配置管理模块 + * + * 提供统一的本地存储配置和工具方法 + * + * ## 主要功能 + * + * - 版本化存储键管理,支持多版本数据隔离 + * - 存储键名生成和解析(带版本前缀) + * - 版本号提取和验证 + * - 存储键匹配的正则表达式生成 + * - 旧版本存储键兼容处理 + * - 升级和登出延迟配置 + * - 主题存储键配置 + * + * ## 使用场景 + * + * - Pinia Store 持久化存储 + * - 应用版本升级时的数据迁移 + * - 多版本数据清理 + * - 存储键的统一管理和规范 + * + * 存储键格式:sys-v{version}-{storeId} + * 例如:sys-v1.0.0-user, sys-v1.0.0-setting + * + * @module utils/storage/storage-config + * @author Art Design Pro Team + */ +export class StorageConfig { + /** 当前应用版本 */ + static readonly CURRENT_VERSION = __APP_VERSION__ + + /** 存储键前缀 */ + static readonly STORAGE_PREFIX = 'sys-v' + + /** 版本键名 */ + static readonly VERSION_KEY = 'sys-version' + + /** 主题键名(index.html中使用了,如果修改,需要同步修改) */ + static readonly THEME_KEY = 'sys-theme' + + /** 上次登录用户ID键名(用于判断是否为同一用户登录) */ + static readonly LAST_USER_ID_KEY = 'sys-last-user-id' + + /** 租户ID键名 */ + static readonly TENANT_ID_KEY = 'sys-tenant-id' + + /** 跳过升级检查的版本 */ + static readonly SKIP_UPGRADE_VERSION = '1.0.0' + + /** 升级处理延迟时间(毫秒) */ + static readonly UPGRADE_DELAY = 1000 + + /** 登出延迟时间(毫秒) */ + static readonly LOGOUT_DELAY = 1000 + + /** + * 生成版本化的存储键名 + * @param storeId 存储ID + * @param version 版本号,默认使用当前版本 + */ + static generateStorageKey(storeId: string, version: string = this.CURRENT_VERSION): string { + return `${this.STORAGE_PREFIX}${version}-${storeId}` + } + + /** + * 生成旧版本的存储键名(不带分隔符) + * @param version 版本号,默认使用当前版本 + */ + static generateLegacyKey(version: string = this.CURRENT_VERSION): string { + return `${this.STORAGE_PREFIX}${version}` + } + + /** + * 创建存储键匹配的正则表达式 + * @param storeId 存储ID + */ + static createKeyPattern(storeId: string): RegExp { + return new RegExp(`^${this.STORAGE_PREFIX}[^-]+-${storeId}$`) + } + + /** + * 创建当前版本存储键匹配的正则表达式 + */ + static createCurrentVersionPattern(): RegExp { + return new RegExp(`^${this.STORAGE_PREFIX}${this.CURRENT_VERSION}-`) + } + + /** + * 创建任意版本存储键匹配的正则表达式 + */ + static createVersionPattern(): RegExp { + return new RegExp(`^${this.STORAGE_PREFIX}`) + } + + /** + * 检查是否为当前版本的键 + */ + static isCurrentVersionKey(key: string): boolean { + return key.startsWith(`${this.STORAGE_PREFIX}${this.CURRENT_VERSION}`) + } + + /** + * 检查是否为版本化的键 + */ + static isVersionedKey(key: string): boolean { + return key.startsWith(this.STORAGE_PREFIX) + } + + /** + * 从存储键中提取版本号 + */ + static extractVersionFromKey(key: string): string | null { + const match = key.match(new RegExp(`^${this.STORAGE_PREFIX}([^-]+)`)) + return match ? match[1] : null + } + + /** + * 从存储键中提取存储ID + */ + static extractStoreIdFromKey(key: string): string | null { + const match = key.match(new RegExp(`^${this.STORAGE_PREFIX}[^-]+-(.+)$`)) + return match ? match[1] : null + } +} diff --git a/src/utils/storage/storage-key-manager.ts b/src/utils/storage/storage-key-manager.ts new file mode 100644 index 0000000..ba14f65 --- /dev/null +++ b/src/utils/storage/storage-key-manager.ts @@ -0,0 +1,97 @@ +/** + * 存储键名管理器模块 + * + * 提供智能的版本化存储键管理和数据迁移功能 + * + * ## 主要功能 + * + * - 自动生成当前版本的存储键名 + * - 检测当前版本数据是否存在 + * - 查找其他版本的同名存储数据 + * - 自动将旧版本数据迁移到当前版本 + * - 数据迁移日志记录 + * - 迁移失败的错误处理 + * + * ## 使用场景 + * + * - Pinia Store 持久化插件中获取存储键 + * - 应用版本升级时自动迁移用户数据 + * - 避免版本升级导致的数据丢失 + * - 实现平滑的版本过渡 + * + * ## 工作流程 + * + * 1. 优先使用当前版本的存储键 + * 2. 如果当前版本无数据,查找其他版本的同名数据 + * 3. 找到旧版本数据后自动迁移到当前版本 + * 4. 返回当前版本的存储键供使用 + * + * @module utils/storage/storage-key-manager + * @author Art Design Pro Team + */ +import { StorageConfig } from '@/utils/storage' + +/** + * 存储键名管理器 + * 负责处理版本化的存储键名生成和数据迁移 + */ +export class StorageKeyManager { + /** + * 获取当前版本的存储键名 + */ + private getCurrentVersionKey(storeId: string): string { + return StorageConfig.generateStorageKey(storeId) + } + + /** + * 检查当前版本的数据是否存在 + */ + private hasCurrentVersionData(key: string): boolean { + return localStorage.getItem(key) !== null + } + + /** + * 查找其他版本的同名存储键 + */ + private findExistingKey(storeId: string): string | null { + const storageKeys = Object.keys(localStorage) + const pattern = StorageConfig.createKeyPattern(storeId) + + return storageKeys.find((key) => pattern.test(key) && localStorage.getItem(key)) || null + } + + /** + * 将数据从旧版本迁移到当前版本 + */ + private migrateData(fromKey: string, toKey: string): void { + try { + const existingData = localStorage.getItem(fromKey) + if (existingData) { + localStorage.setItem(toKey, existingData) + console.info(`[Storage] 已迁移数据: ${fromKey} → ${toKey}`) + } + } catch (error) { + console.warn(`[Storage] 数据迁移失败: ${fromKey}`, error) + } + } + + /** + * 获取持久化存储的键名(支持自动数据迁移) + */ + getStorageKey(storeId: string): string { + const currentKey = this.getCurrentVersionKey(storeId) + + // 优先使用当前版本的数据 + if (this.hasCurrentVersionData(currentKey)) { + return currentKey + } + + // 查找并迁移其他版本的数据 + const existingKey = this.findExistingKey(storeId) + if (existingKey) { + this.migrateData(existingKey, currentKey) + } + + return currentKey + } +} diff --git a/src/utils/storage/storage.ts b/src/utils/storage/storage.ts new file mode 100644 index 0000000..67b9e9e --- /dev/null +++ b/src/utils/storage/storage.ts @@ -0,0 +1,250 @@ +/** + * 存储兼容性管理模块 + * + * 提供完整的本地存储兼容性检查和数据验证功能 + * + * 主要功能 + * + * - 多版本存储数据检测和验证 + * - 新旧存储格式兼容处理 + * - 存储数据完整性校验 + * - 存储异常自动恢复(清理+登出) + * - 登录状态验证 + * - 存储为空检测 + * - 版本号管理 + * + * ## 使用场景 + * + * - 应用启动时检查存储数据有效性 + * - 路由守卫中验证登录状态 + * - 版本升级时的数据兼容性检查 + * - 存储异常时的自动恢复 + * - 防止因存储数据损坏导致的系统异常 + * + * ## 工作流程 + * + * 1. 优先检查当前版本的存储数据 + * 2. 检查其他版本的存储数据 + * 3. 兼容旧格式的存储数据 + * 4. 验证数据完整性 + * 5. 异常时提示用户并执行登出 + * + * @module utils/storage/storage + * @author Art Design Pro Team + */ +import { router } from '@/router' +import { useUserStore } from '@/store/modules/user' +import { StorageConfig } from '@/utils/storage/storage-config' + +/** + * 存储兼容性管理器 + * 负责处理不同版本间的存储兼容性检查和数据验证 + */ +class StorageCompatibilityManager { + /** + * 获取系统版本号 + */ + getSystemVersion(): string | null { + return localStorage.getItem(StorageConfig.VERSION_KEY) + } + + /** + * 获取系统存储数据(兼容旧格式) + */ + getSystemStorage(): any { + const version = this.getSystemVersion() || StorageConfig.CURRENT_VERSION + const legacyKey = StorageConfig.generateLegacyKey(version) + const data = localStorage.getItem(legacyKey) + return data ? JSON.parse(data) : null + } + + /** + * 检查当前版本是否有存储数据 + */ + private hasCurrentVersionStorage(): boolean { + const storageKeys = Object.keys(localStorage) + const currentVersionPattern = StorageConfig.createCurrentVersionPattern() + + return storageKeys.some( + (key) => currentVersionPattern.test(key) && localStorage.getItem(key) !== null + ) + } + + /** + * 检查是否存在任何版本的存储数据 + */ + private hasAnyVersionStorage(): boolean { + const storageKeys = Object.keys(localStorage) + const versionPattern = StorageConfig.createVersionPattern() + + return storageKeys.some((key) => versionPattern.test(key) && localStorage.getItem(key) !== null) + } + + /** + * 获取旧格式的本地存储数据 + */ + private getLegacyStorageData(): Record { + try { + const systemStorage = this.getSystemStorage() + return systemStorage || {} + } catch (error) { + console.warn('[Storage] 解析旧格式存储数据失败:', error) + return {} + } + } + + /** + * 显示存储错误消息 + */ + private showStorageError(): void { + ElMessage({ + type: 'error', + offset: 40, + duration: 5000, + message: '系统检测到本地数据异常,请重新登录系统恢复使用!' + }) + } + + /** + * 执行系统登出 + */ + private performSystemLogout(): void { + setTimeout(() => { + try { + localStorage.clear() + useUserStore().logOut() + router.push({ name: 'Login' }) + console.info('[Storage] 已执行系统登出') + } catch (error) { + console.error('[Storage] 系统登出失败:', error) + } + }, StorageConfig.LOGOUT_DELAY) + } + + /** + * 处理存储异常 + */ + private handleStorageError(): void { + this.showStorageError() + this.performSystemLogout() + } + + /** + * 验证存储数据完整性 + * @param requireAuth 是否需要验证登录状态(默认 false) + */ + validateStorageData(requireAuth: boolean = false): boolean { + try { + // 优先检查新版本存储结构 + if (this.hasCurrentVersionStorage()) { + // console.debug('[Storage] 发现当前版本存储数据') + return true + } + + // 检查是否有任何版本的存储数据 + if (this.hasAnyVersionStorage()) { + // console.debug('[Storage] 发现其他版本存储数据,可能需要迁移') + return true + } + + // 检查旧版本存储结构 + const legacyData = this.getLegacyStorageData() + if (Object.keys(legacyData).length === 0) { + // 只有在需要验证登录状态时才执行登出操作 + if (requireAuth) { + console.warn('[Storage] 未发现任何存储数据,需要重新登录') + this.performSystemLogout() + return false + } + // 首次访问或访问静态路由,不需要登出 + // console.debug('[Storage] 未发现存储数据,首次访问或访问静态路由') + return true + } + + console.debug('[Storage] 发现旧版本存储数据') + return true + } catch (error) { + console.error('[Storage] 存储数据验证失败:', error) + // 只有在需要验证登录状态时才处理错误 + if (requireAuth) { + this.handleStorageError() + return false + } + return true + } + } + + /** + * 检查存储是否为空 + */ + isStorageEmpty(): boolean { + // 检查新版本存储结构 + if (this.hasCurrentVersionStorage()) { + return false + } + + // 检查是否有任何版本的存储数据 + if (this.hasAnyVersionStorage()) { + return false + } + + // 检查旧版本存储结构 + const legacyData = this.getLegacyStorageData() + return Object.keys(legacyData).length === 0 + } + + /** + * 检查存储兼容性 + * @param requireAuth 是否需要验证登录状态(默认 false) + */ + checkCompatibility(requireAuth: boolean = false): boolean { + try { + const isValid = this.validateStorageData(requireAuth) + const isEmpty = this.isStorageEmpty() + + if (isValid || isEmpty) { + // console.debug('[Storage] 存储兼容性检查通过') + return true + } + + console.warn('[Storage] 存储兼容性检查失败') + return false + } catch (error) { + console.error('[Storage] 兼容性检查异常:', error) + return false + } + } +} + +// 创建存储兼容性管理器实例 +const storageManager = new StorageCompatibilityManager() + +/** + * 获取系统存储数据 + */ +export function getSystemStorage(): any { + return storageManager.getSystemStorage() +} + +/** + * 获取系统版本号 + */ +export function getSysVersion(): string | null { + return storageManager.getSystemVersion() +} + +/** + * 验证本地存储数据 + * @param requireAuth 是否需要验证登录状态(默认 false) + */ +export function validateStorageData(requireAuth: boolean = false): boolean { + return storageManager.validateStorageData(requireAuth) +} + +/** + * 检查存储兼容性 + * @param requireAuth 是否需要验证登录状态(默认 false) + */ +export function checkStorageCompatibility(requireAuth: boolean = false): boolean { + return storageManager.checkCompatibility(requireAuth) +} diff --git a/src/utils/sys/console.ts b/src/utils/sys/console.ts new file mode 100644 index 0000000..d631087 --- /dev/null +++ b/src/utils/sys/console.ts @@ -0,0 +1,13 @@ +// ANSI 转义码生成网站 https://patorjk.com/software/taag/#p=display&f=Big&t=ABB%0A +const asciiArt = ` +\x1b[32m欢迎使用 Art Design Pro! +\x1b[0m +\x1b[36m哇!你居然在用我的项目~ 好用的话别忘了去 GitHub 点个 ★Star 呀,你的支持就是我更新的超强动力!祝使用体验满分💯 +\x1b[0m +\x1b[33mGitHub: https://github.com/Daymychen/art-design-pro +\x1b[0m +\x1b[31m技术支持(QQ群): 1038930070,和开发者一起交流~ 群里有小伙伴实时答疑,遇到问题不用慌! +\x1b[0m +` + +console.log(asciiArt) diff --git a/src/utils/sys/error-handle.ts b/src/utils/sys/error-handle.ts new file mode 100644 index 0000000..22109c2 --- /dev/null +++ b/src/utils/sys/error-handle.ts @@ -0,0 +1,102 @@ +/** + * 全局错误处理模块 + * + * 提供统一的错误捕获和处理机制 + * + * ## 主要功能 + * + * - Vue 运行时错误捕获(组件错误、生命周期错误等) + * - 全局脚本错误捕获(语法错误、运行时错误等) + * - Promise 未捕获错误处理(unhandledrejection) + * - 静态资源加载错误监控(图片、脚本、样式等) + * - 错误日志记录和上报 + * - 统一的错误处理入口 + * + * ## 使用场景 + * - 应用启动时安装全局错误处理器 + * - 捕获和记录所有类型的错误 + * - 错误上报到监控平台 + * - 提升应用稳定性和可维护性 + * - 问题排查和调试 + * + * ## 错误类型 + * + * - VueError: Vue 组件相关错误 + * - ScriptError: JavaScript 脚本错误 + * - PromiseError: Promise 未捕获的 rejection + * - ResourceError: 静态资源加载失败 + * + * @module utils/sys/error-handle + * @author Art Design Pro Team + */ +import type { App } from 'vue' + +/** + * Vue 运行时错误处理 + */ +export function vueErrorHandler(err: unknown, instance: any, info: string) { + console.error('[VueError]', err, info, instance) + // 这里可以上报到服务端,比如: + // reportError({ type: 'vue', err, info }) +} + +/** + * 全局脚本错误处理 + */ +export function scriptErrorHandler( + message: Event | string, + source?: string, + lineno?: number, + colno?: number, + error?: Error +): boolean { + console.error('[ScriptError]', { message, source, lineno, colno, error }) + // reportError({ type: 'script', message, source, lineno, colno, error }) + return true // 阻止默认控制台报错,可根据需求改 +} + +/** + * Promise 未捕获错误处理 + */ +export function registerPromiseErrorHandler() { + window.addEventListener('unhandledrejection', (event) => { + console.error('[PromiseError]', event.reason) + // reportError({ type: 'promise', reason: event.reason }) + }) +} + +/** + * 资源加载错误处理 (img, script, css...) + */ +export function registerResourceErrorHandler() { + window.addEventListener( + 'error', + (event: Event) => { + const target = event.target as HTMLElement + if ( + target && + (target.tagName === 'IMG' || target.tagName === 'SCRIPT' || target.tagName === 'LINK') + ) { + console.error('[ResourceError]', { + tagName: target.tagName, + src: + (target as HTMLImageElement).src || + (target as HTMLScriptElement).src || + (target as HTMLLinkElement).href + }) + // reportError({ type: 'resource', target }) + } + }, + true // 捕获阶段才能监听到资源错误 + ) +} + +/** + * 安装统一错误处理 + */ +export function setupErrorHandle(app: App) { + app.config.errorHandler = vueErrorHandler + window.onerror = scriptErrorHandler + registerPromiseErrorHandler() + registerResourceErrorHandler() +} diff --git a/src/utils/sys/index.ts b/src/utils/sys/index.ts new file mode 100644 index 0000000..a2e0729 --- /dev/null +++ b/src/utils/sys/index.ts @@ -0,0 +1,6 @@ +/** + * 系统管理相关工具函数统一导出 + */ + +export * from './upgrade' +export { default as mittBus } from './mittBus' diff --git a/src/utils/sys/mittBus.ts b/src/utils/sys/mittBus.ts new file mode 100644 index 0000000..22f0108 --- /dev/null +++ b/src/utils/sys/mittBus.ts @@ -0,0 +1,63 @@ +/** + * 全局事件总线模块 + * + * 基于 mitt 库实现的类型安全的事件总线 + * + * ## 主要功能 + * + * - 跨组件通信(发布/订阅模式) + * - 类型安全的事件定义和调用 + * - 全局事件管理(烟花效果、设置面板、搜索对话框等) + * - 解耦组件间的直接依赖 + * + * ## 使用场景 + * + * - 跨层级组件通信 + * - 全局功能触发(设置、搜索、聊天、锁屏等) + * - 特效触发(烟花效果) + * - 避免 props 层层传递 + * + * ## 用法示例 + * + * ```typescript + * // 订阅事件 + * mittBus.on('openSetting', () => { ... }) + * + * // 发布事件 + * mittBus.emit('openSetting') + * + * // 带参数的事件 + * mittBus.emit('triggerFireworks', 'image-url') + * ``` + * + * ## 已定义的事件 + * + * - triggerFireworks: 触发烟花效果(可选图片URL) + * - openSetting: 打开设置面板 + * - openSearchDialog: 打开搜索对话框 + * - openChat: 打开聊天窗口 + * - openLockScreen: 打开锁屏 + * + * @module utils/sys/mittBus + * @author Art Design Pro Team + */ +import mitt, { type Emitter } from 'mitt' + +// 定义事件类型映射 +type Events = { + // 烟花效果事件 - 可选的图片URL参数 + triggerFireworks: string | undefined + // 打开设置面板事件 - 无参数 + openSetting: void + // 打开搜索对话框事件 - 无参数 + openSearchDialog: void + // 打开聊天窗口事件 - 无参数 + openChat: void + // 打开锁屏事件 - 无参数 + openLockScreen: void +} + +// 创建类型安全的事件总线实例 +const mittBus: Emitter = mitt() + +export default mittBus diff --git a/src/utils/sys/upgrade.ts b/src/utils/sys/upgrade.ts new file mode 100644 index 0000000..53d3465 --- /dev/null +++ b/src/utils/sys/upgrade.ts @@ -0,0 +1,277 @@ +/** + * 系统版本升级管理模块 + * + * 提供完整的应用版本升级检测和处理功能 + * + * ## 主要功能 + * + * - 版本号比较和升级检测 + * - 首次访问识别和处理 + * - 旧版本数据自动清理 + * - 升级日志展示和通知 + * - 强制重新登录控制(根据升级日志配置) + * - 版本号规范化处理 + * - 旧存储结构迁移和清理 + * - 升级流程延迟执行(确保应用完全加载) + * + * ## 使用场景 + * + * - 应用启动时自动检测版本升级 + * - 版本更新后清理旧数据 + * - 向用户展示版本更新内容 + * - 重大更新时要求用户重新登录 + * - 防止旧版本数据污染新版本 + * + * ## 工作流程 + * + * 1. 检查本地存储的版本号 + * 2. 与当前应用版本对比 + * 3. 查找并清理旧版本数据 + * 4. 展示升级通知(包含更新日志) + * 5. 根据配置决定是否强制重新登录 + * 6. 更新本地版本号 + * + * @module utils/sys/upgrade + * @author Art Design Pro Team + */ +import { upgradeLogList } from '@/mock/upgrade/changeLog' +import { ElNotification } from 'element-plus' +import { useUserStore } from '@/store/modules/user' +import { StorageConfig } from '@/utils/storage/storage-config' + +/** + * 版本管理器 + * 负责处理版本比较、升级检测和数据清理 + */ +class VersionManager { + /** + * 规范化版本号字符串,移除前缀 'v' + */ + private normalizeVersion(version: string): string { + return version.replace(/^v/, '') + } + + /** + * 获取存储的版本号 + */ + private getStoredVersion(): string | null { + return localStorage.getItem(StorageConfig.VERSION_KEY) + } + + /** + * 设置版本号到存储 + */ + private setStoredVersion(version: string): void { + localStorage.setItem(StorageConfig.VERSION_KEY, version) + } + + /** + * 检查是否应该跳过升级处理 + */ + private shouldSkipUpgrade(): boolean { + return StorageConfig.CURRENT_VERSION === StorageConfig.SKIP_UPGRADE_VERSION + } + + /** + * 检查是否为首次访问 + */ + private isFirstVisit(storedVersion: string | null): boolean { + return !storedVersion + } + + /** + * 检查版本是否相同 + */ + private isSameVersion(storedVersion: string): boolean { + return storedVersion === StorageConfig.CURRENT_VERSION + } + + /** + * 查找旧的存储结构 + */ + private findLegacyStorage(): { oldSysKey: string | null; oldVersionKeys: string[] } { + const storageKeys = Object.keys(localStorage) + const currentVersionPrefix = StorageConfig.generateStorageKey('').slice(0, -1) // 移除末尾的 '-' + + // 查找旧的单一存储结构 + const oldSysKey = + storageKeys.find( + (key) => + StorageConfig.isVersionedKey(key) && key !== currentVersionPrefix && !key.includes('-') + ) || null + + // 查找旧版本的分离存储键 + const oldVersionKeys = storageKeys.filter( + (key) => + StorageConfig.isVersionedKey(key) && + !StorageConfig.isCurrentVersionKey(key) && + key.includes('-') + ) + + return { oldSysKey, oldVersionKeys } + } + + /** + * 检查是否需要重新登录 + */ + private shouldRequireReLogin(storedVersion: string): boolean { + const normalizedCurrent = this.normalizeVersion(StorageConfig.CURRENT_VERSION) + const normalizedStored = this.normalizeVersion(storedVersion) + + return upgradeLogList.value.some((item) => { + const itemVersion = this.normalizeVersion(item.version) + return ( + item.requireReLogin && itemVersion > normalizedStored && itemVersion <= normalizedCurrent + ) + }) + } + + /** + * 构建升级通知消息 + */ + private buildUpgradeMessage(requireReLogin: boolean): string { + const { title: content } = upgradeLogList.value[0] + + const messageParts = [ + `

`, + `系统已升级到 ${StorageConfig.CURRENT_VERSION} 版本,此次更新带来了以下改进:`, + `

`, + content + ] + + if (requireReLogin) { + messageParts.push( + `

升级完成,请重新登录后继续使用。

` + ) + } + + return messageParts.join('') + } + + /** + * 显示升级通知 + */ + private showUpgradeNotification(message: string): void { + ElNotification({ + title: '系统升级公告', + message, + duration: 0, + type: 'success', + dangerouslyUseHTMLString: true + }) + } + + /** + * 清理旧版本数据 + */ + private cleanupLegacyData(oldSysKey: string | null, oldVersionKeys: string[]): void { + // 清理旧的单一存储结构 + if (oldSysKey) { + localStorage.removeItem(oldSysKey) + console.info(`[Upgrade] 已清理旧存储: ${oldSysKey}`) + } + + // 清理旧版本的分离存储 + oldVersionKeys.forEach((key) => { + localStorage.removeItem(key) + console.info(`[Upgrade] 已清理旧存储: ${key}`) + }) + } + + /** + * 执行升级后的登出操作 + */ + private performLogout(): void { + try { + useUserStore().logOut() + console.info('[Upgrade] 已执行升级后登出') + } catch (error) { + console.error('[Upgrade] 升级后登出失败:', error) + } + } + + /** + * 执行升级流程 + */ + private async executeUpgrade( + storedVersion: string, + legacyStorage: ReturnType + ): Promise { + try { + if (!upgradeLogList.value.length) { + console.warn('[Upgrade] 升级日志列表为空') + return + } + + const requireReLogin = this.shouldRequireReLogin(storedVersion) + const message = this.buildUpgradeMessage(requireReLogin) + + // 显示升级通知 + this.showUpgradeNotification(message) + + // 更新版本号 + this.setStoredVersion(StorageConfig.CURRENT_VERSION) + + // 清理旧数据 + this.cleanupLegacyData(legacyStorage.oldSysKey, legacyStorage.oldVersionKeys) + + // 执行登出(如果需要) + if (requireReLogin) { + this.performLogout() + } + + console.info(`[Upgrade] 升级完成: ${storedVersion} → ${StorageConfig.CURRENT_VERSION}`) + } catch (error) { + console.error('[Upgrade] 系统升级处理失败:', error) + } + } + + /** + * 系统升级处理主流程 + */ + async processUpgrade(): Promise { + // 跳过特定版本 + if (this.shouldSkipUpgrade()) { + console.debug('[Upgrade] 跳过版本升级检查') + return + } + + const storedVersion = this.getStoredVersion() + + // 首次访问处理 + if (this.isFirstVisit(storedVersion)) { + this.setStoredVersion(StorageConfig.CURRENT_VERSION) + // console.info('[Upgrade] 首次访问,已设置当前版本') + return + } + + // 版本相同,无需升级 + if (this.isSameVersion(storedVersion!)) { + // console.debug('[Upgrade] 版本相同,无需升级') + return + } + + // 检查是否有需要升级的旧数据 + const legacyStorage = this.findLegacyStorage() + if (!legacyStorage.oldSysKey && legacyStorage.oldVersionKeys.length === 0) { + this.setStoredVersion(StorageConfig.CURRENT_VERSION) + console.info('[Upgrade] 无旧数据,已更新版本号') + return + } + + // 延迟执行升级流程,确保应用已完全加载 + setTimeout(() => { + this.executeUpgrade(storedVersion!, legacyStorage) + }, StorageConfig.UPGRADE_DELAY) + } +} + +// 创建版本管理器实例 +const versionManager = new VersionManager() + +/** + * 系统升级处理入口函数 + */ +export async function systemUpgrade(): Promise { + await versionManager.processUpgrade() +} diff --git a/src/utils/table/tableCache.ts b/src/utils/table/tableCache.ts new file mode 100644 index 0000000..045a7ce --- /dev/null +++ b/src/utils/table/tableCache.ts @@ -0,0 +1,266 @@ +/** + * 表格缓存管理模块 + * + * 提供高性能的表格数据缓存机制 + * + * ## 主要功能 + * + * - 基于参数的智能缓存键生成(使用 ohash) + * - LRU(最近最少使用)缓存淘汰策略 + * - 缓存过期时间管理 + * - 缓存大小限制和自动清理 + * - 基于标签的缓存分组管理 + * - 多种缓存失效策略(清空所有、清空当前、清空分页等) + * - 缓存访问统计和命中率分析 + * - 缓存大小估算 + * + * ## 使用场景 + * + * - 表格数据的分页缓存 + * - 减少重复的 API 请求 + * - 提升表格切换和返回的响应速度 + * - 搜索条件变化时的智能缓存管理 + * - 数据更新后的缓存失效处理 + * + * ## 缓存策略 + * + * - CLEAR_ALL: 清空所有缓存(适用于全局数据更新) + * - CLEAR_CURRENT: 仅清空当前查询条件的缓存(适用于单条数据更新) + * - CLEAR_PAGINATION: 清空所有分页缓存但保留不同搜索条件(适用于批量操作) + * - KEEP_ALL: 不清除缓存(适用于只读操作) + * + * @module utils/table/tableCache + * @author Art Design Pro Team + */ +import { hash } from 'ohash' + +// 缓存失效策略枚举 +export enum CacheInvalidationStrategy { + /** 清空所有缓存 */ + CLEAR_ALL = 'clear_all', + /** 仅清空当前查询条件的缓存 */ + CLEAR_CURRENT = 'clear_current', + /** 清空所有分页缓存(保留不同搜索条件的缓存) */ + CLEAR_PAGINATION = 'clear_pagination', + /** 不清除缓存 */ + KEEP_ALL = 'keep_all' +} + +// 通用 API 响应接口(兼容不同的后端响应格式) +export interface ApiResponse { + records?: T[] + data?: T[] + total?: number + current?: number + size?: number + [key: string]: unknown +} + +// 缓存存储接口 +export interface CacheItem { + data: T[] + response: ApiResponse + timestamp: number + params: string + // 缓存标签,用于分组管理 + tags: Set + // 访问次数(用于 LRU 算法) + accessCount: number + // 最后访问时间 + lastAccessTime: number +} + +// 增强的缓存管理类 +export class TableCache { + private cache = new Map>() + private cacheTime: number + private maxSize: number + private enableLog: boolean + + constructor(cacheTime = 5 * 60 * 1000, maxSize = 50, enableLog = false) { + // 默认5分钟,最多50条缓存 + this.cacheTime = cacheTime + this.maxSize = maxSize + this.enableLog = enableLog + } + + // 内部日志工具 + private log(message: string, ...args: any[]) { + if (this.enableLog) { + console.log(`[TableCache] ${message}`, ...args) + } + } + + // 生成稳定的缓存键 + private generateKey(params: unknown): string { + return hash(params) + } + + // 🔧 优化:增强类型安全性 + private generateTags(params: Record): Set { + const tags = new Set() + + // 添加搜索条件标签 + const searchKeys = Object.keys(params).filter( + (key) => + !['current', 'size', 'total'].includes(key) && + params[key] !== undefined && + params[key] !== '' && + params[key] !== null + ) + + if (searchKeys.length > 0) { + const searchTag = searchKeys.map((key) => `${key}:${String(params[key])}`).join('|') + tags.add(`search:${searchTag}`) + } else { + tags.add('search:default') + } + + // 添加分页标签 + tags.add(`pagination:${params.size || 10}`) + // 添加通用分页标签,用于清理所有分页缓存 + tags.add('pagination') + + return tags + } + + // 🔧 优化:LRU 缓存清理 + private evictLRU(): void { + if (this.cache.size <= this.maxSize) return + + // 找到最少使用的缓存项 + let lruKey = '' + let minAccessCount = Infinity + let oldestTime = Infinity + + for (const [key, item] of this.cache.entries()) { + if ( + item.accessCount < minAccessCount || + (item.accessCount === minAccessCount && item.lastAccessTime < oldestTime) + ) { + lruKey = key + minAccessCount = item.accessCount + oldestTime = item.lastAccessTime + } + } + + if (lruKey) { + this.cache.delete(lruKey) + this.log(`LRU 清理缓存: ${lruKey}`) + } + } + + // 设置缓存 + set(params: unknown, data: T[], response: ApiResponse): void { + const key = this.generateKey(params) + const tags = this.generateTags(params as Record) + const now = Date.now() + + // 检查是否需要清理 + this.evictLRU() + + this.cache.set(key, { + data, + response, + timestamp: now, + params: key, + tags, + accessCount: 1, + lastAccessTime: now + }) + } + + // 获取缓存 + get(params: unknown): CacheItem | null { + const key = this.generateKey(params) + const item = this.cache.get(key) + + if (!item) return null + + // 检查是否过期 + if (Date.now() - item.timestamp > this.cacheTime) { + this.cache.delete(key) + return null + } + + // 更新访问统计 + item.accessCount++ + item.lastAccessTime = Date.now() + + return item + } + + // 根据标签清除缓存 + clearByTags(tags: string[]): number { + let clearedCount = 0 + + for (const [key, item] of this.cache.entries()) { + // 检查是否包含任意一个标签 + const hasMatchingTag = tags.some((tag) => + Array.from(item.tags).some((itemTag) => itemTag.includes(tag)) + ) + + if (hasMatchingTag) { + this.cache.delete(key) + clearedCount++ + } + } + + return clearedCount + } + + // 清除当前搜索条件的缓存 + clearCurrentSearch(params: unknown): number { + const key = this.generateKey(params) + const deleted = this.cache.delete(key) + return deleted ? 1 : 0 + } + + // 清除分页缓存 + clearPagination(): number { + return this.clearByTags(['pagination']) + } + + // 清空所有缓存 + clear(): void { + this.cache.clear() + } + + // 获取缓存统计信息 + getStats(): { total: number; size: string; hitRate: string } { + const total = this.cache.size + let totalSize = 0 + let totalAccess = 0 + + for (const item of this.cache.values()) { + // 粗略估算大小(JSON字符串长度) + totalSize += JSON.stringify(item.data).length + totalAccess += item.accessCount + } + + // 转换为人类可读的大小 + const sizeInKB = (totalSize / 1024).toFixed(2) + const avgHits = total > 0 ? (totalAccess / total).toFixed(1) : '0' + + return { + total, + size: `${sizeInKB}KB`, + hitRate: `${avgHits} avg hits` + } + } + + // 清理过期缓存 + cleanupExpired(): number { + let cleanedCount = 0 + const now = Date.now() + + for (const [key, item] of this.cache.entries()) { + if (now - item.timestamp > this.cacheTime) { + this.cache.delete(key) + cleanedCount++ + } + } + + return cleanedCount + } +} diff --git a/src/utils/table/tableConfig.ts b/src/utils/table/tableConfig.ts new file mode 100644 index 0000000..9a71175 --- /dev/null +++ b/src/utils/table/tableConfig.ts @@ -0,0 +1,55 @@ +/** + * 表格全局配置模块 + * + * 提供表格与后端接口的字段映射配置 + * + * ## 主要功能 + * + * - 响应数据字段自动识别和映射 + * - 支持多种常见的后端响应格式 + * - 请求参数字段映射配置 + * - 可扩展的字段配置机制 + * + * ## 使用场景 + * + * - 适配不同后端的分页接口格式 + * - 统一前端表格组件的数据处理 + * - 减少重复的数据转换代码 + * - 支持多个后端服务的接口对接 + * + * ## 配置说明 + * + * - recordFields: 列表数据字段名(按优先级顺序查找) + * - totalFields: 总条数字段名 + * - currentFields: 当前页码字段名 + * - sizeFields: 每页大小字段名 + * - paginationKey: 前端发送请求时使用的分页参数名 + * + * ## 扩展方式 + * + * 如果后端使用其他字段名,可以在对应数组中添加新的字段名 + * 例如:recordFields: ['list', 'data', 'records', 'items', 'yourCustomField'] + * + * @module utils/table/tableConfig + * @author Art Design Pro Team + */ +export const tableConfig = { + // 响应数据字段映射配置,系统会从接口返回数据中按顺序查找这些字段 + // 列表数据 + recordFields: ['list', 'data', 'records', 'items', 'result', 'rows'], + // 总条数 + totalFields: ['total', 'count', 'totalCount'], + // 当前页码 + currentFields: ['current', 'page', 'pageNum'], + // 每页大小 + sizeFields: ['size', 'pageSize', 'limit'], + + // 请求参数映射配置,前端发送请求时使用的分页参数名 + // useTable 组合式函数传递分页参数的时候 用 current 跟 size + paginationKey: { + // 当前页码 + current: 'current', + // 每页大小 + size: 'size' + } +} diff --git a/src/utils/table/tableUtils.ts b/src/utils/table/tableUtils.ts new file mode 100644 index 0000000..3ca9db1 --- /dev/null +++ b/src/utils/table/tableUtils.ts @@ -0,0 +1,297 @@ +/** + * 表格工具函数模块 + * + * 提供表格数据处理和请求管理的核心工具函数 + * + * ## 主要功能 + * + * - 多格式 API 响应自动适配和标准化 + * - 表格数据提取和转换 + * - 分页信息自动更新和校验 + * - 智能防抖函数(支持取消和立即执行) + * - 统一的错误处理机制 + * - 嵌套数据结构解析 + * + * ## 使用场景 + * + * - useTable 组合式函数的底层工具 + * - 适配各种后端接口响应格式 + * - 表格数据的标准化处理 + * - 请求防抖和性能优化 + * - 错误统一处理和日志记录 + * + * ## 支持的响应格式 + * + * 1. 直接数组: [item1, item2, ...] + * 2. 标准对象: { records: [], total: 100 } + * 3. 嵌套data: { data: { list: [], total: 100 } } + * 4. 多种字段名: list/data/records/items/result/rows + * + * ## 核心功能 + * + * - defaultResponseAdapter: 智能识别和转换响应格式 + * - extractTableData: 提取表格数据数组 + * - updatePaginationFromResponse: 更新分页信息 + * - createSmartDebounce: 创建可控的防抖函数 + * - createErrorHandler: 生成错误处理器 + * + * @module utils/table/tableUtils + * @author Art Design Pro Team + */ + +import type { ApiResponse } from './tableCache' +import { tableConfig } from './tableConfig' + +// 请求参数基础接口,扩展分页参数 +export interface BaseRequestParams extends Api.Common.PaginationParams { + [key: string]: unknown +} + +// 错误处理接口 +export interface TableError { + code: string + message: string + details?: unknown +} + +// 辅助函数:从对象中提取记录数组 +function extractRecords(obj: Record, fields: string[]): T[] { + for (const field of fields) { + if (field in obj && Array.isArray(obj[field])) { + return obj[field] as T[] + } + } + return [] +} + +// 辅助函数:从对象中提取总数 +function extractTotal(obj: Record, records: unknown[], fields: string[]): number { + for (const field of fields) { + if (field in obj && typeof obj[field] === 'number') { + return obj[field] as number + } + } + return records.length +} + +// 辅助函数:提取分页参数 +function extractPagination( + obj: Record, + data?: Record +): Pick, 'current' | 'size'> | undefined { + const result: Partial, 'current' | 'size'>> = {} + const sources = [obj, data ?? {}] + + const currentFields = tableConfig.currentFields + for (const src of sources) { + for (const field of currentFields) { + if (field in src && typeof src[field] === 'number') { + result.current = src[field] as number + break + } + } + if (result.current !== undefined) break + } + + const sizeFields = tableConfig.sizeFields + for (const src of sources) { + for (const field of sizeFields) { + if (field in src && typeof src[field] === 'number') { + result.size = src[field] as number + break + } + } + if (result.size !== undefined) break + } + + if (result.current === undefined && result.size === undefined) return undefined + return result +} + +/** + * 默认响应适配器 - 支持多种常见的API响应格式 + */ +export const defaultResponseAdapter = (response: unknown): ApiResponse => { + // 定义支持的字段 + const recordFields = tableConfig.recordFields + + if (!response) { + return { records: [], total: 0 } + } + + if (Array.isArray(response)) { + return { records: response, total: response.length } + } + + if (typeof response !== 'object') { + console.warn( + '[tableUtils] 无法识别的响应格式,支持的格式包括: 数组、包含' + + recordFields.join('/') + + '字段的对象、嵌套data对象。当前格式:', + response + ) + return { records: [], total: 0 } + } + + const res = response as Record + let records: T[] = [] + let total = 0 + let pagination: Pick, 'current' | 'size'> | undefined + + // 处理标准格式或直接列表 + records = extractRecords(res, recordFields) + total = extractTotal(res, records, tableConfig.totalFields) + pagination = extractPagination(res) + + // 如果没有找到,检查嵌套data + if (records.length === 0 && 'data' in res && typeof res.data === 'object') { + const data = res.data as Record + records = extractRecords(data, ['list', 'records', 'items']) + total = extractTotal(data, records, tableConfig.totalFields) + pagination = extractPagination(res, data) + + if (Array.isArray(res.data)) { + records = res.data as T[] + total = records.length + } + } + + if (!recordFields.some((field) => field in res) && records.length === 0) { + console.warn('[tableUtils] 无法识别的响应格式') + console.warn('支持的字段包括: ' + recordFields.join('、'), response) + console.warn('扩展字段请到 utils/table/tableConfig 文件配置') + } + + const result: ApiResponse = { records, total } + if (pagination) { + Object.assign(result, pagination) + } + return result +} + +/** + * 从标准化的API响应中提取表格数据 + */ +export const extractTableData = (response: ApiResponse): T[] => { + const data = response.records || response.data || [] + return Array.isArray(data) ? data : [] +} + +/** + * 根据API响应更新分页信息 + */ +export const updatePaginationFromResponse = ( + pagination: Api.Common.PaginationParams, + response: ApiResponse +): void => { + pagination.total = response.total ?? pagination.total ?? 0 + + if (response.current !== undefined) { + pagination.current = response.current + } + + const maxPage = Math.max(1, Math.ceil(pagination.total / (pagination.size || 1))) + if (pagination.current > maxPage) { + pagination.current = maxPage + } +} + +/** + * 创建智能防抖函数 - 支持取消和立即执行 + */ +export const createSmartDebounce = Promise>( + fn: T, + delay: number +): T & { cancel: () => void; flush: () => Promise } => { + let timeoutId: NodeJS.Timeout | null = null + let lastArgs: Parameters | null = null + let lastResolve: ((value: any) => void) | null = null + let lastReject: ((reason: any) => void) | null = null + + const debouncedFn = (...args: Parameters): Promise => { + return new Promise((resolve, reject) => { + if (timeoutId) clearTimeout(timeoutId) + lastArgs = args + lastResolve = resolve + lastReject = reject + timeoutId = setTimeout(async () => { + try { + const result = await fn(...args) + resolve(result) + } catch (error) { + reject(error) + } finally { + timeoutId = null + lastArgs = null + lastResolve = null + lastReject = null + } + }, delay) + }) + } + + debouncedFn.cancel = () => { + if (timeoutId) clearTimeout(timeoutId) + timeoutId = null + lastArgs = null + lastResolve = null + lastReject = null + } + + debouncedFn.flush = async () => { + if (timeoutId && lastArgs && lastResolve && lastReject) { + clearTimeout(timeoutId) + timeoutId = null + const args = lastArgs + const resolve = lastResolve + const reject = lastReject + lastArgs = null + lastResolve = null + lastReject = null + try { + const result = await fn(...args) + resolve(result) + return result + } catch (error) { + reject(error) + throw error + } + } + return Promise.resolve() + } + + return debouncedFn as any +} + +/** + * 生成错误处理函数 + */ +export const createErrorHandler = ( + onError?: (error: TableError) => void, + enableLog: boolean = false +) => { + const logger = { + error: (message: string, ...args: any[]) => { + if (enableLog) console.error(`[useTable] ${message}`, ...args) + } + } + + return (err: unknown, context: string): TableError => { + const tableError: TableError = { + code: 'UNKNOWN_ERROR', + message: '未知错误', + details: err + } + + if (err instanceof Error) { + tableError.message = err.message + tableError.code = err.name + } else if (typeof err === 'string') { + tableError.message = err + } + + logger.error(`${context}:`, err) + onError?.(tableError) + return tableError + } +} diff --git a/src/utils/tencent-map.ts b/src/utils/tencent-map.ts new file mode 100644 index 0000000..809e9ab --- /dev/null +++ b/src/utils/tencent-map.ts @@ -0,0 +1,42 @@ +const DEFAULT_TENCENT_MAP_LIBRARIES = 'visualization,geometry,vector,tools,service' + +let mapScriptPromise: Promise | null = null +let scriptLoading = false +const pendingResolvers: Array<() => void> = [] + +export const loadTencentMapScript = (apiKey: string, libraries = DEFAULT_TENCENT_MAP_LIBRARIES) => { + const w = window as unknown as { TMap?: unknown } & Record + if (w.TMap) return Promise.resolve() + if (mapScriptPromise) return mapScriptPromise + mapScriptPromise = new Promise((resolve, reject) => { + pendingResolvers.push(resolve) + if (scriptLoading) return + scriptLoading = true + const callbackName = '__tencentMapInit' + if (!w[callbackName]) { + w[callbackName] = () => { + scriptLoading = false + const queue = pendingResolvers.splice(0, pendingResolvers.length) + queue.forEach((fn) => fn()) + } + } + const script = document.createElement('script') + const libParam = libraries?.trim() + script.src = libParam + ? `https://map.qq.com/api/gljs?v=1.exp&key=${apiKey}&libraries=${libParam}&callback=${callbackName}` + : `https://map.qq.com/api/gljs?v=1.exp&key=${apiKey}&callback=${callbackName}` + script.type = 'text/javascript' + script.async = true + script.defer = true + script.onerror = () => { + scriptLoading = false + mapScriptPromise = null + pendingResolvers.length = 0 + reject(new Error('Tencent map script load failed')) + } + document.body.appendChild(script) + }) + return mapScriptPromise +} + +export { DEFAULT_TENCENT_MAP_LIBRARIES } diff --git a/src/utils/ui/animation.ts b/src/utils/ui/animation.ts new file mode 100644 index 0000000..5efd02a --- /dev/null +++ b/src/utils/ui/animation.ts @@ -0,0 +1,80 @@ +/** + * 主题动画工具模块 + * + * 提供主题切换的视觉动画效果 + * + * ## 主要功能 + * + * - 基于鼠标点击位置的圆形扩散动画 + * - View Transition API 支持(现代浏览器) + * - 降级处理(不支持动画的浏览器) + * - 暗黑主题切换过渡效果 + * - 页面刷新时的主题过渡优化 + * + * ## 使用场景 + * + * - 明暗主题切换 + * - 提升用户体验的视觉反馈 + * - 页面刷新时的平滑过渡 + * + * ## 技术实现 + * + * - 使用 CSS 变量存储点击位置和半径 + * - 利用 View Transition API 实现流畅动画 + * - 通过 CSS class 控制过渡效果 + * - 自动计算最大扩散半径 + * + * @module utils/theme/animation + * @author Art Design Pro Team + */ +import { useCommon } from '@/hooks/core/useCommon' +import { useTheme } from '@/hooks/core/useTheme' +import { SystemThemeEnum } from '@/enums/appEnum' +import { useSettingStore } from '@/store/modules/setting' +const { LIGHT, DARK } = SystemThemeEnum + +/** + * 主题切换动画 + * @param e 鼠标点击事件 + */ +export const themeAnimation = (e: any) => { + const x = e.clientX + const y = e.clientY + // 计算鼠标点击位置距离视窗的最大圆半径 + const endRadius = Math.hypot(Math.max(x, innerWidth - x), Math.max(y, innerHeight - y)) + + // 设置CSS变量 + document.documentElement.style.setProperty('--x', x + 'px') + document.documentElement.style.setProperty('--y', y + 'px') + document.documentElement.style.setProperty('--r', endRadius + 'px') + + if (document.startViewTransition) { + document.startViewTransition(() => toggleTheme()) + } else { + toggleTheme() + } +} + +/** + * 切换主题 + */ +const toggleTheme = () => { + useTheme().switchThemeStyles(useSettingStore().systemThemeType === LIGHT ? DARK : LIGHT) + useCommon().refresh() +} + +/** + * 切换主题过渡效果 + * @param enable 是否启用过渡效果 + */ +export const toggleTransition = (enable: boolean) => { + const body = document.body + + if (enable) { + body.classList.add('theme-change') + } else { + setTimeout(() => { + body.classList.remove('theme-change') + }, 300) + } +} diff --git a/src/utils/ui/colors.ts b/src/utils/ui/colors.ts new file mode 100644 index 0000000..b4f6b77 --- /dev/null +++ b/src/utils/ui/colors.ts @@ -0,0 +1,273 @@ +/** + * 颜色处理工具模块 + * + * 提供完整的颜色格式转换和处理功能 + * + * ## 主要功能 + * + * - Hex 与 RGB/RGBA 格式互转 + * - 颜色混合计算 + * - 颜色变浅/变深处理 + * - Element Plus 主题色自动生成 + * - 颜色格式验证 + * - CSS 变量读取 + * - 暗黑模式颜色适配 + * + * ## 使用场景 + * + * - 主题色动态切换 + * - Element Plus 组件主题定制 + * - 颜色渐变生成 + * - 明暗主题颜色计算 + * - 颜色格式标准化 + * + * ## 核心功能 + * + * - hexToRgba: Hex 转 RGBA(支持透明度) + * - hexToRgb: Hex 转 RGB 数组 + * - rgbToHex: RGB 转 Hex + * - colourBlend: 两种颜色混合 + * - getLightColor: 生成变浅的颜色 + * - getDarkColor: 生成变深的颜色 + * - handleElementThemeColor: 处理 Element Plus 主题色 + * - setElementThemeColor: 设置完整的主题色系统 + * + * ## 支持格式 + * + * - Hex: #FFF, #FFFFFF + * - RGB: rgb(255, 255, 255) + * - RGBA: rgba(255, 255, 255, 0.5) + * + * @module utils/ui/colors + * @author Art Design Pro Team + */ +import { useSettingStore } from '@/store/modules/setting' + +/** + * 颜色转换结果接口 + */ +interface RgbaResult { + red: number + green: number + blue: number + rgba: string +} + +/** + * 获取CSS变量值(别名函数) + * @param name CSS变量名 + * @returns CSS变量值 + */ +export function getCssVar(name: string): string { + return getComputedStyle(document.documentElement).getPropertyValue(name) +} + +/** + * 验证hex颜色格式 + * @param hex hex颜色值 + * @returns 是否为有效的hex颜色 + */ +function isValidHexColor(hex: string): boolean { + const cleanHex = hex.trim().replace(/^#/, '') + return /^[0-9A-Fa-f]{3}$|^[0-9A-Fa-f]{6}$/.test(cleanHex) +} + +/** + * 验证RGB颜色值 + * @param r 红色值 + * @param g 绿色值 + * @param b 蓝色值 + * @returns 是否为有效的RGB值 + */ +function isValidRgbValue(r: number, g: number, b: number): boolean { + const isValid = (value: number) => Number.isInteger(value) && value >= 0 && value <= 255 + return isValid(r) && isValid(g) && isValid(b) +} + +/** + * 将hex颜色转换为RGBA + * @param hex hex颜色值 (支持 #FFF 或 #FFFFFF 格式) + * @param opacity 透明度 (0-1) + * @returns 包含RGB值和RGBA字符串的对象 + */ +export function hexToRgba(hex: string, opacity: number): RgbaResult { + if (!isValidHexColor(hex)) { + throw new Error('Invalid hex color format') + } + + // 移除可能存在的 # 前缀并转换为大写 + let cleanHex = hex.trim().replace(/^#/, '').toUpperCase() + + // 如果是缩写形式(如 FFF),转换为完整形式 + if (cleanHex.length === 3) { + cleanHex = cleanHex + .split('') + .map((char) => char.repeat(2)) + .join('') + } + + // 解析 RGB 值 + const [red, green, blue] = cleanHex.match(/\w\w/g)!.map((x) => parseInt(x, 16)) + + // 确保 opacity 在有效范围内 + const validOpacity = Math.max(0, Math.min(1, opacity)) + + // 构建 RGBA 字符串 + const rgba = `rgba(${red}, ${green}, ${blue}, ${validOpacity.toFixed(2)})` + + return { red, green, blue, rgba } +} + +/** + * 将hex颜色转换为RGB数组 + * @param hexColor hex颜色值 + * @returns RGB数组 [r, g, b] + */ +export function hexToRgb(hexColor: string): number[] { + if (!isValidHexColor(hexColor)) { + ElMessage.warning('输入错误的hex颜色值') + throw new Error('Invalid hex color format') + } + + const cleanHex = hexColor.replace(/^#/, '') + let hex = cleanHex + + // 处理缩写形式 + if (hex.length === 3) { + hex = hex + .split('') + .map((char) => char.repeat(2)) + .join('') + } + + const hexPairs = hex.match(/../g) + if (!hexPairs) { + throw new Error('Invalid hex color format') + } + + return hexPairs.map((hexPair) => parseInt(hexPair, 16)) +} + +/** + * 将RGB颜色转换为hex + * @param r 红色值 (0-255) + * @param g 绿色值 (0-255) + * @param b 蓝色值 (0-255) + * @returns hex颜色值 + */ +export function rgbToHex(r: number, g: number, b: number): string { + if (!isValidRgbValue(r, g, b)) { + ElMessage.warning('输入错误的RGB颜色值') + throw new Error('Invalid RGB color values') + } + + const toHex = (value: number) => { + const hex = value.toString(16) + return hex.length === 1 ? `0${hex}` : hex + } + + return `#${toHex(r)}${toHex(g)}${toHex(b)}` +} + +/** + * 颜色混合 + * @param color1 第一个颜色 + * @param color2 第二个颜色 + * @param ratio 混合比例 (0-1) + * @returns 混合后的颜色 + */ +export function colourBlend(color1: string, color2: string, ratio: number): string { + const validRatio = Math.max(0, Math.min(1, Number(ratio))) + + const rgb1 = hexToRgb(color1) + const rgb2 = hexToRgb(color2) + + const blendedRgb = rgb1.map((value1, index) => { + const value2 = rgb2[index] + return Math.round(value1 * (1 - validRatio) + value2 * validRatio) + }) + + return rgbToHex(blendedRgb[0], blendedRgb[1], blendedRgb[2]) +} + +/** + * 获取变浅的颜色 + * @param color 原始颜色 + * @param level 变浅程度 (0-1) + * @param isDark 是否为暗色主题 + * @returns 变浅后的颜色 + */ +export function getLightColor(color: string, level: number, isDark: boolean = false): string { + if (!isValidHexColor(color)) { + ElMessage.warning('输入错误的hex颜色值') + throw new Error('Invalid hex color format') + } + + if (isDark) { + return getDarkColor(color, level) + } + + const rgb = hexToRgb(color) + const lightRgb = rgb.map((value) => Math.floor((255 - value) * level + value)) + + return rgbToHex(lightRgb[0], lightRgb[1], lightRgb[2]) +} + +/** + * 获取变深的颜色 + * @param color 原始颜色 + * @param level 变深程度 (0-1) + * @returns 变深后的颜色 + */ +export function getDarkColor(color: string, level: number): string { + if (!isValidHexColor(color)) { + ElMessage.warning('输入错误的hex颜色值') + throw new Error('Invalid hex color format') + } + + const rgb = hexToRgb(color) + const darkRgb = rgb.map((value) => Math.floor(value * (1 - level))) + + return rgbToHex(darkRgb[0], darkRgb[1], darkRgb[2]) +} + +/** + * 处理 Element Plus 主题颜色 + * @param theme 主题颜色 + * @param isDark 是否为暗色主题 + */ +export function handleElementThemeColor(theme: string, isDark: boolean = false): void { + document.documentElement.style.setProperty('--el-color-primary', theme) + + for (let i = 1; i <= 9; i++) { + document.documentElement.style.setProperty( + `--el-color-primary-light-${i}`, + getLightColor(theme, i / 10, isDark) + ) + } + + for (let i = 1; i <= 9; i++) { + document.documentElement.style.setProperty( + `--el-color-primary-dark-${i}`, + getDarkColor(theme, i / 10) + ) + } +} + +/** + * 设置 Element Plus 主题颜色 + * @param color 主题颜色 + */ +export function setElementThemeColor(color: string): void { + const mixColor = '#ffffff' + const elStyle = document.documentElement.style + + elStyle.setProperty('--el-color-primary', color) + handleElementThemeColor(color, useSettingStore().isDark) + + // 生成更淡一点的颜色 + for (let i = 1; i < 16; i++) { + const itemColor = colourBlend(color, mixColor, i / 16) + elStyle.setProperty(`--el-color-primary-custom-${i}`, itemColor) + } +} diff --git a/src/utils/ui/emojo.ts b/src/utils/ui/emojo.ts new file mode 100644 index 0000000..cabad7d --- /dev/null +++ b/src/utils/ui/emojo.ts @@ -0,0 +1,24 @@ +/** + * 表情 + * 用于在消息提示的时候显示对应的表情 + * + * 用法 + * ElMessage.success(`${EmojiText[200]} 图片上传成功`) + * ElMessage.error(`${EmojiText[400]} 图片上传失败`) + * ElMessage.error(`${EmojiText[500]} 图片上传失败`) + * + * @module utils/ui/emojo + * @author Art Design Pro Team + */ + +// macos 用户 按 shift + 6 可以唤出更多表情…… +const EmojiText: { [key: string]: string } = { + '0': 'O_O', // 空 + '200': '^_^', // 成功 + '400': 'T_T', // 错误请求 + '500': 'X_X' // 服务器内部错误,无法完成请求 +} + +// const EmojiIcon = ['🟢', '🔴', '🟡 ', '🚀', '✨', '💡', '🛠️', '🔥', '🎉', '🌟', '🌈'] + +export default EmojiText diff --git a/src/utils/ui/iconify-loader.ts b/src/utils/ui/iconify-loader.ts new file mode 100644 index 0000000..035de16 --- /dev/null +++ b/src/utils/ui/iconify-loader.ts @@ -0,0 +1,31 @@ +/** + * 离线图标加载器 + * + * 用于在内网环境下支持 Iconify 图标的离线加载。 + * 通过预加载图标集数据,避免运行时从 CDN 获取图标。 + * + * 使用方式: + * 1. 安装所需图标集:pnpm add -D @iconify-json/[icon-set-name] + * 2. 在此文件中导入并注册图标集 + * 3. 在组件中使用: + * + * @module utils/ui/iconify-loader + * @author Art Design Pro Team + */ + +// import { addCollection } from '@iconify/vue' + +// // 导入离线图标数据 + +// // 系统必要图标库 +// import riIcons from '@iconify-json/ri/icons.json' + +// // 演示图标库(可选,生产环境可移除) +// import svgSpinners from '@iconify-json/svg-spinners/icons.json' +// import lineMd from '@iconify-json/line-md/icons.json' + +// // 注册离线图标集 + +// addCollection(riIcons) +// addCollection(svgSpinners) +// addCollection(lineMd) diff --git a/src/utils/ui/index.ts b/src/utils/ui/index.ts new file mode 100644 index 0000000..9ca1049 --- /dev/null +++ b/src/utils/ui/index.ts @@ -0,0 +1,11 @@ +/** + * UI 相关工具函数统一导出 + * + * @module utils/ui/index + * @author Art Design Pro Team + */ + +export * from './colors' +export * from './loading' +export * from './tabs' +export * from './emojo' diff --git a/src/utils/ui/loading.ts b/src/utils/ui/loading.ts new file mode 100644 index 0000000..6580e02 --- /dev/null +++ b/src/utils/ui/loading.ts @@ -0,0 +1,84 @@ +/** + * 全局 Loading 加载管理模块 + * + * 提供统一的全屏加载动画管理 + * + * ## 主要功能 + * + * - 全屏 Loading 显示和隐藏 + * - 自动适配明暗主题背景色 + * - 自定义 SVG 加载动画 + * - 单例模式防止重复创建 + * - 锁定页面交互 + * + * ## 使用场景 + * + * - 页面初始化加载 + * - 大量数据请求 + * - 路由切换过渡 + * - 异步操作等待 + * + * ## 特性 + * + * - 自动检测当前主题并应用对应背景色 + * - 使用自定义 SVG 动画(四点旋转) + * - 单例模式确保同时只有一个 Loading + * - 提供便捷的显示/隐藏方法 + * + * @module utils/ui/loading + * @author Art Design Pro Team + */ +import { fourDotsSpinnerSvg } from '@/assets/svg/loading' + +/** + * 获取当前主题对应的loading背景色 + * @returns 背景色字符串 + */ +const getLoadingBackground = (): string => { + const isDark = document.documentElement.classList.contains('dark') + return isDark ? 'rgba(7, 7, 7, 0.85)' : '#fff' +} + +const DEFAULT_LOADING_CONFIG = { + lock: true, + get background() { + return getLoadingBackground() + }, + svg: fourDotsSpinnerSvg, + svgViewBox: '0 0 40 40', + customClass: 'art-loading-fix' +} as const + +interface LoadingInstance { + close: () => void +} + +let loadingInstance: LoadingInstance | null = null + +export const loadingService = { + /** + * 显示 loading + * @returns 关闭 loading 的函数 + */ + showLoading(): () => void { + if (!loadingInstance) { + // 每次显示时获取最新的配置,确保背景色与当前主题同步 + const config = { + ...DEFAULT_LOADING_CONFIG, + background: getLoadingBackground() + } + loadingInstance = ElLoading.service(config) + } + return () => this.hideLoading() + }, + + /** + * 隐藏 loading + */ + hideLoading(): void { + if (loadingInstance) { + loadingInstance.close() + loadingInstance = null + } + } +} diff --git a/src/utils/ui/tabs.ts b/src/utils/ui/tabs.ts new file mode 100644 index 0000000..5f53ea5 --- /dev/null +++ b/src/utils/ui/tabs.ts @@ -0,0 +1,60 @@ +/** + * 标签页布局配置模块 + * + * 提供不同标签页样式的高度和间距配置 + * + * ## 主要功能 + * + * - 多种标签页样式配置(默认、卡片、谷歌风格) + * - 标签页打开/关闭状态的高度管理 + * - 顶部间距自动计算 + * - 配置获取和默认值处理 + * + * ## 使用场景 + * + * - 工作标签页(Worktab)布局计算 + * - 页面内容区域高度调整 + * - 标签页显示/隐藏时的动画 + * - 响应式布局适配 + * + * ## 配置项说明 + * + * - openTop: 标签页显示时,内容区域距离顶部的距离 + * - closeTop: 标签页隐藏时,内容区域距离顶部的距离 + * - openHeight: 标签页显示时的总高度(包含标签栏) + * - closeHeight: 标签页隐藏时的总高度(仅头部) + * + * ## 支持的样式 + * + * - tab-default: 默认标签页样式 + * - tab-card: 卡片式标签页 + * - tab-google: 谷歌浏览器风格标签页 + * + * @module utils/ui/tabs + * @author Art Design Pro Team + */ +export const TAB_CONFIG = { + 'tab-default': { + openTop: 106, + closeTop: 60, + openHeight: 121, + closeHeight: 75 + }, + 'tab-card': { + openTop: 122, + closeTop: 78, + openHeight: 139, + closeHeight: 95 + }, + 'tab-google': { + openTop: 122, + closeTop: 78, + openHeight: 139, + closeHeight: 95 + } +} + +// 获取当前 tab 样式配置,设置默认值 +export const getTabConfig = (style: string) => { + return TAB_CONFIG[style as keyof typeof TAB_CONFIG] || TAB_CONFIG['tab-card'] // 默认使用 tab-card 配置 +} diff --git a/src/views/announcement-drafts/index.vue b/src/views/announcement-drafts/index.vue new file mode 100644 index 0000000..fde6eb1 --- /dev/null +++ b/src/views/announcement-drafts/index.vue @@ -0,0 +1,717 @@ + + + diff --git a/src/views/app/announcements/detail.vue b/src/views/app/announcements/detail.vue new file mode 100644 index 0000000..eabebe5 --- /dev/null +++ b/src/views/app/announcements/detail.vue @@ -0,0 +1,260 @@ + + + + diff --git a/src/views/app/announcements/index.vue b/src/views/app/announcements/index.vue new file mode 100644 index 0000000..98b8516 --- /dev/null +++ b/src/views/app/announcements/index.vue @@ -0,0 +1,333 @@ + + + + diff --git a/src/views/auth/forget-password/index.vue b/src/views/auth/forget-password/index.vue new file mode 100644 index 0000000..147259e --- /dev/null +++ b/src/views/auth/forget-password/index.vue @@ -0,0 +1,62 @@ + + + + + diff --git a/src/views/auth/login/index.vue b/src/views/auth/login/index.vue new file mode 100644 index 0000000..0ef161c --- /dev/null +++ b/src/views/auth/login/index.vue @@ -0,0 +1,355 @@ + + + + + + + + diff --git a/src/views/auth/login/style.css b/src/views/auth/login/style.css new file mode 100644 index 0000000..bd8c3a9 --- /dev/null +++ b/src/views/auth/login/style.css @@ -0,0 +1,38 @@ +@reference '@styles/core/tailwind.css'; + +/* 授权页右侧区域 */ +.auth-right-wrap { + @apply absolute inset-0 w-[440px] h-[650px] py-[5px] m-auto overflow-hidden + max-sm:px-7 max-sm:w-full + animate-[slideInRight_0.6s_cubic-bezier(0.25,0.46,0.45,0.94)_forwards] + max-md:animate-none; + + .form { + @apply h-full py-[40px]; + } + + .title { + @apply text-g-900 text-4xl font-semibold max-md:text-3xl max-sm:pt-10; + } + + .sub-title { + @apply mt-[10px] text-g-600 text-sm; + } + + .custom-height { + @apply !h-[40px]; + } +} + +/* 滑入动画 */ +@keyframes slideInRight { + from { + opacity: 0; + transform: translateX(30px); + } + + to { + opacity: 1; + transform: translateX(0); + } +} diff --git a/src/views/auth/register/index.vue b/src/views/auth/register/index.vue new file mode 100644 index 0000000..1e8683c --- /dev/null +++ b/src/views/auth/register/index.vue @@ -0,0 +1,668 @@ + + + diff --git a/src/views/auth/reset-password/index.vue b/src/views/auth/reset-password/index.vue new file mode 100644 index 0000000..b3ed715 --- /dev/null +++ b/src/views/auth/reset-password/index.vue @@ -0,0 +1,153 @@ + + + + + diff --git a/src/views/dashboard/console/index.vue b/src/views/dashboard/console/index.vue new file mode 100644 index 0000000..154c330 --- /dev/null +++ b/src/views/dashboard/console/index.vue @@ -0,0 +1,41 @@ + + + + diff --git a/src/views/dashboard/console/modules/about-project.vue b/src/views/dashboard/console/modules/about-project.vue new file mode 100644 index 0000000..ed946ce --- /dev/null +++ b/src/views/dashboard/console/modules/about-project.vue @@ -0,0 +1,44 @@ + + + diff --git a/src/views/dashboard/console/modules/active-user.vue b/src/views/dashboard/console/modules/active-user.vue new file mode 100644 index 0000000..da740f2 --- /dev/null +++ b/src/views/dashboard/console/modules/active-user.vue @@ -0,0 +1,47 @@ + + + diff --git a/src/views/dashboard/console/modules/card-list.vue b/src/views/dashboard/console/modules/card-list.vue new file mode 100644 index 0000000..5fc76a7 --- /dev/null +++ b/src/views/dashboard/console/modules/card-list.vue @@ -0,0 +1,74 @@ + + + diff --git a/src/views/dashboard/console/modules/dynamic-stats.vue b/src/views/dashboard/console/modules/dynamic-stats.vue new file mode 100644 index 0000000..1876950 --- /dev/null +++ b/src/views/dashboard/console/modules/dynamic-stats.vue @@ -0,0 +1,79 @@ + + + diff --git a/src/views/dashboard/console/modules/new-user.vue b/src/views/dashboard/console/modules/new-user.vue new file mode 100644 index 0000000..9d39522 --- /dev/null +++ b/src/views/dashboard/console/modules/new-user.vue @@ -0,0 +1,169 @@ + + + + + diff --git a/src/views/dashboard/console/modules/sales-overview.vue b/src/views/dashboard/console/modules/sales-overview.vue new file mode 100644 index 0000000..32904b8 --- /dev/null +++ b/src/views/dashboard/console/modules/sales-overview.vue @@ -0,0 +1,43 @@ + + + diff --git a/src/views/dashboard/console/modules/todo-list.vue b/src/views/dashboard/console/modules/todo-list.vue new file mode 100644 index 0000000..ab9a86c --- /dev/null +++ b/src/views/dashboard/console/modules/todo-list.vue @@ -0,0 +1,71 @@ + + + diff --git a/src/views/exception/403/index.vue b/src/views/exception/403/index.vue new file mode 100644 index 0000000..2756c42 --- /dev/null +++ b/src/views/exception/403/index.vue @@ -0,0 +1,16 @@ + + + + diff --git a/src/views/exception/404/index.vue b/src/views/exception/404/index.vue new file mode 100644 index 0000000..6b64f45 --- /dev/null +++ b/src/views/exception/404/index.vue @@ -0,0 +1,16 @@ + + + + diff --git a/src/views/exception/500/index.vue b/src/views/exception/500/index.vue new file mode 100644 index 0000000..1b26377 --- /dev/null +++ b/src/views/exception/500/index.vue @@ -0,0 +1,16 @@ + + + + diff --git a/src/views/index/index.vue b/src/views/index/index.vue new file mode 100644 index 0000000..415a436 --- /dev/null +++ b/src/views/index/index.vue @@ -0,0 +1,29 @@ + + + + + + diff --git a/src/views/index/style.scss b/src/views/index/style.scss new file mode 100644 index 0000000..c89f354 --- /dev/null +++ b/src/views/index/style.scss @@ -0,0 +1,93 @@ +.app-layout { + display: flex; + width: 100%; + min-height: 100vh; + background: var(--default-bg-color); + + #app-sidebar { + flex-shrink: 0; + } + + #app-main { + display: flex; + flex: 1; + flex-direction: column; + min-width: 0; + height: 100vh; + overflow: auto; + + #app-header { + position: sticky; + top: 0; + z-index: 50; + flex-shrink: 0; + width: 100%; + } + + #app-content { + flex: 1; + + :deep(.layout-content) { + box-sizing: border-box; + width: calc(100% - 40px); + margin: auto; + + // 子页面默认 style + .page-content { + position: relative; + box-sizing: border-box; + padding: 20px; + overflow: hidden; + background: var(--default-box-color); + border-radius: calc(var(--custom-radius) / 2 + 2px) !important; + } + } + } + } +} + +@media only screen and (width <= 1180px) { + .app-layout { + #app-main { + height: 100dvh; + } + } +} + +@media only screen and (width <= 800px) { + .app-layout { + position: relative; + + #app-sidebar { + position: fixed; + top: 0; + left: 0; + z-index: 300; + height: 100vh; + } + + #app-main { + width: 100%; + height: auto; + overflow: visible; + + #app-content { + :deep(.layout-content) { + width: calc(100% - 40px); + } + } + } + } +} + +@media only screen and (width <= 640px) { + .app-layout { + #app-main { + #app-content { + :deep(.layout-content) { + width: calc(100% - 30px); + } + } + } + } +} diff --git a/src/views/merchant/detail/components/AuditHistoryTab.vue b/src/views/merchant/detail/components/AuditHistoryTab.vue new file mode 100644 index 0000000..04c2d7f --- /dev/null +++ b/src/views/merchant/detail/components/AuditHistoryTab.vue @@ -0,0 +1,35 @@ + + + diff --git a/src/views/merchant/detail/components/BasicInfo.vue b/src/views/merchant/detail/components/BasicInfo.vue new file mode 100644 index 0000000..a0d156d --- /dev/null +++ b/src/views/merchant/detail/components/BasicInfo.vue @@ -0,0 +1,95 @@ + + + diff --git a/src/views/merchant/detail/components/ChangeHistoryTab.vue b/src/views/merchant/detail/components/ChangeHistoryTab.vue new file mode 100644 index 0000000..bd28581 --- /dev/null +++ b/src/views/merchant/detail/components/ChangeHistoryTab.vue @@ -0,0 +1,71 @@ + + + diff --git a/src/views/merchant/detail/components/StoresTab.vue b/src/views/merchant/detail/components/StoresTab.vue new file mode 100644 index 0000000..a712213 --- /dev/null +++ b/src/views/merchant/detail/components/StoresTab.vue @@ -0,0 +1,57 @@ + + + diff --git a/src/views/merchant/detail/components/SubjectInfo.vue b/src/views/merchant/detail/components/SubjectInfo.vue new file mode 100644 index 0000000..704a04a --- /dev/null +++ b/src/views/merchant/detail/components/SubjectInfo.vue @@ -0,0 +1,31 @@ + + + diff --git a/src/views/merchant/detail/index.vue b/src/views/merchant/detail/index.vue new file mode 100644 index 0000000..389f472 --- /dev/null +++ b/src/views/merchant/detail/index.vue @@ -0,0 +1,213 @@ + + + + + diff --git a/src/views/merchant/detail/modules/EditDialog.vue b/src/views/merchant/detail/modules/EditDialog.vue new file mode 100644 index 0000000..d434e2d --- /dev/null +++ b/src/views/merchant/detail/modules/EditDialog.vue @@ -0,0 +1,163 @@ + + + diff --git a/src/views/merchant/list/index.vue b/src/views/merchant/list/index.vue new file mode 100644 index 0000000..5e2dac6 --- /dev/null +++ b/src/views/merchant/list/index.vue @@ -0,0 +1,200 @@ + + + diff --git a/src/views/merchant/list/modules/merchant-search.vue b/src/views/merchant/list/modules/merchant-search.vue new file mode 100644 index 0000000..6b03c23 --- /dev/null +++ b/src/views/merchant/list/modules/merchant-search.vue @@ -0,0 +1,85 @@ + + + diff --git a/src/views/merchant/list/types.ts b/src/views/merchant/list/types.ts new file mode 100644 index 0000000..be31f7a --- /dev/null +++ b/src/views/merchant/list/types.ts @@ -0,0 +1,9 @@ +import type { MerchantStatus } from '@/enums/MerchantStatus' +import type { OperatingMode } from '@/enums/OperatingMode' + +export interface MerchantListSearchForm { + keyword: string + status?: MerchantStatus + operatingMode?: OperatingMode + tenantId?: string +} diff --git a/src/views/merchant/review/components/ReviewDialog.vue b/src/views/merchant/review/components/ReviewDialog.vue new file mode 100644 index 0000000..18e395e --- /dev/null +++ b/src/views/merchant/review/components/ReviewDialog.vue @@ -0,0 +1,294 @@ + + + diff --git a/src/views/merchant/review/index.vue b/src/views/merchant/review/index.vue new file mode 100644 index 0000000..545199c --- /dev/null +++ b/src/views/merchant/review/index.vue @@ -0,0 +1,208 @@ + + + + + diff --git a/src/views/merchant/review/modules/review-search.vue b/src/views/merchant/review/modules/review-search.vue new file mode 100644 index 0000000..b0f60df --- /dev/null +++ b/src/views/merchant/review/modules/review-search.vue @@ -0,0 +1,65 @@ + + + diff --git a/src/views/merchant/review/types/index.ts b/src/views/merchant/review/types/index.ts new file mode 100644 index 0000000..d9d64a2 --- /dev/null +++ b/src/views/merchant/review/types/index.ts @@ -0,0 +1,6 @@ +import type { OperatingMode } from '@/enums/OperatingMode' + +export interface MerchantReviewSearchForm { + keyword: string + operatingMode?: OperatingMode +} diff --git a/src/views/onboarding/error/index.vue b/src/views/onboarding/error/index.vue new file mode 100644 index 0000000..50e1028 --- /dev/null +++ b/src/views/onboarding/error/index.vue @@ -0,0 +1,459 @@ + + + + diff --git a/src/views/onboarding/pricing/index.vue b/src/views/onboarding/pricing/index.vue new file mode 100644 index 0000000..29837d9 --- /dev/null +++ b/src/views/onboarding/pricing/index.vue @@ -0,0 +1,513 @@ + + + + + diff --git a/src/views/onboarding/status/index.vue b/src/views/onboarding/status/index.vue new file mode 100644 index 0000000..db995af --- /dev/null +++ b/src/views/onboarding/status/index.vue @@ -0,0 +1,689 @@ + + + + + diff --git a/src/views/onboarding/terms-of-service/index.vue b/src/views/onboarding/terms-of-service/index.vue new file mode 100644 index 0000000..b4df1df --- /dev/null +++ b/src/views/onboarding/terms-of-service/index.vue @@ -0,0 +1,227 @@ + + + + + diff --git a/src/views/onboarding/waiting/index.vue b/src/views/onboarding/waiting/index.vue new file mode 100644 index 0000000..ac437cd --- /dev/null +++ b/src/views/onboarding/waiting/index.vue @@ -0,0 +1,376 @@ + + + + + diff --git a/src/views/outside/Iframe.vue b/src/views/outside/Iframe.vue new file mode 100644 index 0000000..33ea0dc --- /dev/null +++ b/src/views/outside/Iframe.vue @@ -0,0 +1,42 @@ + + + diff --git a/src/views/platform/announcements/create.vue b/src/views/platform/announcements/create.vue new file mode 100644 index 0000000..e5e0f62 --- /dev/null +++ b/src/views/platform/announcements/create.vue @@ -0,0 +1,434 @@ + + + diff --git a/src/views/platform/announcements/detail.vue b/src/views/platform/announcements/detail.vue new file mode 100644 index 0000000..9e6de58 --- /dev/null +++ b/src/views/platform/announcements/detail.vue @@ -0,0 +1,504 @@ + + + + + diff --git a/src/views/platform/announcements/edit.vue b/src/views/platform/announcements/edit.vue new file mode 100644 index 0000000..53f56d4 --- /dev/null +++ b/src/views/platform/announcements/edit.vue @@ -0,0 +1,564 @@ + + + diff --git a/src/views/platform/announcements/index.vue b/src/views/platform/announcements/index.vue new file mode 100644 index 0000000..c602b0f --- /dev/null +++ b/src/views/platform/announcements/index.vue @@ -0,0 +1,460 @@ + + + diff --git a/src/views/platform/qualification-alerts/index.vue b/src/views/platform/qualification-alerts/index.vue new file mode 100644 index 0000000..503514d --- /dev/null +++ b/src/views/platform/qualification-alerts/index.vue @@ -0,0 +1,279 @@ + + + + + diff --git a/src/views/platform/store-audits/components/StoreAuditDetailDrawer.vue b/src/views/platform/store-audits/components/StoreAuditDetailDrawer.vue new file mode 100644 index 0000000..f46d6fb --- /dev/null +++ b/src/views/platform/store-audits/components/StoreAuditDetailDrawer.vue @@ -0,0 +1,545 @@ + + + + + diff --git a/src/views/platform/store-audits/components/StoreRiskControlDialog.vue b/src/views/platform/store-audits/components/StoreRiskControlDialog.vue new file mode 100644 index 0000000..f39ee6c --- /dev/null +++ b/src/views/platform/store-audits/components/StoreRiskControlDialog.vue @@ -0,0 +1,178 @@ + + + diff --git a/src/views/platform/store-audits/index.vue b/src/views/platform/store-audits/index.vue new file mode 100644 index 0000000..4257826 --- /dev/null +++ b/src/views/platform/store-audits/index.vue @@ -0,0 +1,314 @@ + + + + + diff --git a/src/views/result/fail/index.vue b/src/views/result/fail/index.vue new file mode 100644 index 0000000..8fe2583 --- /dev/null +++ b/src/views/result/fail/index.vue @@ -0,0 +1,28 @@ + + + diff --git a/src/views/result/success/index.vue b/src/views/result/success/index.vue new file mode 100644 index 0000000..ae57aba --- /dev/null +++ b/src/views/result/success/index.vue @@ -0,0 +1,21 @@ + + + diff --git a/src/views/store/store-detail/components/BusinessHoursPanel.vue b/src/views/store/store-detail/components/BusinessHoursPanel.vue new file mode 100644 index 0000000..fec890c --- /dev/null +++ b/src/views/store/store-detail/components/BusinessHoursPanel.vue @@ -0,0 +1,227 @@ + + + diff --git a/src/views/store/store-detail/components/DeliveryZoneMapEditor.vue b/src/views/store/store-detail/components/DeliveryZoneMapEditor.vue new file mode 100644 index 0000000..a770dca --- /dev/null +++ b/src/views/store/store-detail/components/DeliveryZoneMapEditor.vue @@ -0,0 +1,485 @@ + + + diff --git a/src/views/store/store-detail/components/DeliveryZonePolygonDialog.vue b/src/views/store/store-detail/components/DeliveryZonePolygonDialog.vue new file mode 100644 index 0000000..a12104d --- /dev/null +++ b/src/views/store/store-detail/components/DeliveryZonePolygonDialog.vue @@ -0,0 +1,460 @@ + + + + + diff --git a/src/views/store/store-detail/components/StoreFeePanel.vue b/src/views/store/store-detail/components/StoreFeePanel.vue new file mode 100644 index 0000000..e12c9be --- /dev/null +++ b/src/views/store/store-detail/components/StoreFeePanel.vue @@ -0,0 +1,549 @@ + + + + + diff --git a/src/views/store/store-detail/components/StoreQualificationPanel.vue b/src/views/store/store-detail/components/StoreQualificationPanel.vue new file mode 100644 index 0000000..23186d5 --- /dev/null +++ b/src/views/store/store-detail/components/StoreQualificationPanel.vue @@ -0,0 +1,431 @@ + + + diff --git a/src/views/store/store-detail/components/TemporaryHoursPanel.vue b/src/views/store/store-detail/components/TemporaryHoursPanel.vue new file mode 100644 index 0000000..f0b726e --- /dev/null +++ b/src/views/store/store-detail/components/TemporaryHoursPanel.vue @@ -0,0 +1,348 @@ + + + diff --git a/src/views/store/store-list/components/BusinessStatusDialog.vue b/src/views/store/store-list/components/BusinessStatusDialog.vue new file mode 100644 index 0000000..4d763b8 --- /dev/null +++ b/src/views/store/store-list/components/BusinessStatusDialog.vue @@ -0,0 +1,222 @@ + + + diff --git a/src/views/store/store-list/components/StoreDetailDrawer.vue b/src/views/store/store-list/components/StoreDetailDrawer.vue new file mode 100644 index 0000000..8240a57 --- /dev/null +++ b/src/views/store/store-list/components/StoreDetailDrawer.vue @@ -0,0 +1,232 @@ + + + diff --git a/src/views/store/store-list/components/StoreFormDialog.vue b/src/views/store/store-list/components/StoreFormDialog.vue new file mode 100644 index 0000000..4c36f59 --- /dev/null +++ b/src/views/store/store-list/components/StoreFormDialog.vue @@ -0,0 +1,662 @@ + + + + + diff --git a/src/views/store/store-list/index.vue b/src/views/store/store-list/index.vue new file mode 100644 index 0000000..bfaf744 --- /dev/null +++ b/src/views/store/store-list/index.vue @@ -0,0 +1,326 @@ + + + diff --git a/src/views/store/store-list/modules/store-search.vue b/src/views/store/store-list/modules/store-search.vue new file mode 100644 index 0000000..088bace --- /dev/null +++ b/src/views/store/store-list/modules/store-search.vue @@ -0,0 +1,111 @@ + + + diff --git a/src/views/system/dictionary-label-override/components/PlatformLabelOverrideFormDialog.vue b/src/views/system/dictionary-label-override/components/PlatformLabelOverrideFormDialog.vue new file mode 100644 index 0000000..47b6475 --- /dev/null +++ b/src/views/system/dictionary-label-override/components/PlatformLabelOverrideFormDialog.vue @@ -0,0 +1,373 @@ + + + + + diff --git a/src/views/system/dictionary-label-override/index.vue b/src/views/system/dictionary-label-override/index.vue new file mode 100644 index 0000000..0e3b08d --- /dev/null +++ b/src/views/system/dictionary-label-override/index.vue @@ -0,0 +1,404 @@ + + + + + diff --git a/src/views/system/dictionary-metrics/index.vue b/src/views/system/dictionary-metrics/index.vue new file mode 100644 index 0000000..8c3fbbf --- /dev/null +++ b/src/views/system/dictionary-metrics/index.vue @@ -0,0 +1,364 @@ + + + + + diff --git a/src/views/system/dictionary/components/GroupFormDialog.vue b/src/views/system/dictionary/components/GroupFormDialog.vue new file mode 100644 index 0000000..f0bc028 --- /dev/null +++ b/src/views/system/dictionary/components/GroupFormDialog.vue @@ -0,0 +1,222 @@ + + + + + diff --git a/src/views/system/dictionary/components/GroupList.vue b/src/views/system/dictionary/components/GroupList.vue new file mode 100644 index 0000000..d4aafe2 --- /dev/null +++ b/src/views/system/dictionary/components/GroupList.vue @@ -0,0 +1,291 @@ + + + + + diff --git a/src/views/system/dictionary/components/I18nValueEditor.vue b/src/views/system/dictionary/components/I18nValueEditor.vue new file mode 100644 index 0000000..87317d1 --- /dev/null +++ b/src/views/system/dictionary/components/I18nValueEditor.vue @@ -0,0 +1,97 @@ + + + + + diff --git a/src/views/system/dictionary/components/ImportDialog.vue b/src/views/system/dictionary/components/ImportDialog.vue new file mode 100644 index 0000000..6662158 --- /dev/null +++ b/src/views/system/dictionary/components/ImportDialog.vue @@ -0,0 +1,204 @@ + + + + + diff --git a/src/views/system/dictionary/components/ItemFormDialog.vue b/src/views/system/dictionary/components/ItemFormDialog.vue new file mode 100644 index 0000000..4e74357 --- /dev/null +++ b/src/views/system/dictionary/components/ItemFormDialog.vue @@ -0,0 +1,217 @@ + + + + + diff --git a/src/views/system/dictionary/components/ItemTable.vue b/src/views/system/dictionary/components/ItemTable.vue new file mode 100644 index 0000000..02096f1 --- /dev/null +++ b/src/views/system/dictionary/components/ItemTable.vue @@ -0,0 +1,424 @@ + + + + + diff --git a/src/views/system/dictionary/components/__tests__/I18nValueEditor.spec.ts b/src/views/system/dictionary/components/__tests__/I18nValueEditor.spec.ts new file mode 100644 index 0000000..c673b14 --- /dev/null +++ b/src/views/system/dictionary/components/__tests__/I18nValueEditor.spec.ts @@ -0,0 +1,116 @@ +import { describe, it, expect } from 'vitest' +import { defineComponent, nextTick } from 'vue' +import { mount } from '@vue/test-utils' +import { createI18n } from 'vue-i18n' +import I18nValueEditor from '../I18nValueEditor.vue' + +const ElTabsStub = defineComponent({ + name: 'ElTabs', + props: { + modelValue: { + type: String, + default: '' + } + }, + emits: ['update:modelValue'], + template: '
' +}) + +const ElTabPaneStub = defineComponent({ + name: 'ElTabPane', + props: { + name: { + type: String, + default: '' + }, + label: { + type: String, + default: '' + } + }, + template: '
' +}) + +const ElInputStub = defineComponent({ + name: 'ElInput', + props: { + modelValue: { + type: String, + default: '' + } + }, + emits: ['update:modelValue', 'input'], + template: '', + methods: { + handleInput(event: Event) { + const target = event.target as HTMLInputElement + this.$emit('update:modelValue', target.value) + this.$emit('input', target.value) + } + } +}) + +describe('I18nValueEditor', () => { + const i18n = createI18n({ + legacy: false, + locale: 'en', + messages: { + en: { + dictionary: { + i18n: { + zh: 'Chinese', + en: 'English', + zhPlaceholder: 'Chinese content', + enPlaceholder: 'English content', + hint: 'Fill at least one language.' + } + } + } + } + }) + + const mountEditor = (props?: Record) => { + return mount(I18nValueEditor, { + props, + global: { + plugins: [i18n], + stubs: { + ElTabs: ElTabsStub, + ElTabPane: ElTabPaneStub, + ElInput: ElInputStub + } + } + }) + } + + it('renders tabs for zh-CN and en', () => { + const wrapper = mountEditor() + const panes = wrapper.findAll('.tab-pane') + + expect(panes).toHaveLength(2) + expect(panes[0].attributes('data-name')).toBe('zh-CN') + expect(panes[1].attributes('data-name')).toBe('en') + }) + + it('validates at least one language is filled', async () => { + const wrapper = mountEditor({ modelValue: {} }) + + expect(wrapper.find('.i18n-hint').exists()).toBe(true) + + await wrapper.setProps({ modelValue: { 'zh-CN': '测试' } }) + await nextTick() + + expect(wrapper.find('.i18n-hint').exists()).toBe(false) + }) + + it('emits correct value object on input', async () => { + const wrapper = mountEditor() + const inputs = wrapper.findAll('input') + + await inputs[0].setValue('你好') + + const emitted = wrapper.emitted('update:modelValue') + expect(emitted).toBeTruthy() + expect(emitted?.[0][0]).toMatchObject({ 'zh-CN': '你好' }) + }) +}) diff --git a/src/views/system/dictionary/index.vue b/src/views/system/dictionary/index.vue new file mode 100644 index 0000000..498eedd --- /dev/null +++ b/src/views/system/dictionary/index.vue @@ -0,0 +1,44 @@ + + + + + diff --git a/src/views/system/menu/index.vue b/src/views/system/menu/index.vue new file mode 100644 index 0000000..973b1e7 --- /dev/null +++ b/src/views/system/menu/index.vue @@ -0,0 +1,479 @@ + + + + diff --git a/src/views/system/menu/modules/menu-dialog.vue b/src/views/system/menu/modules/menu-dialog.vue new file mode 100644 index 0000000..f512301 --- /dev/null +++ b/src/views/system/menu/modules/menu-dialog.vue @@ -0,0 +1,384 @@ + + + diff --git a/src/views/system/permission/index.vue b/src/views/system/permission/index.vue new file mode 100644 index 0000000..6338d4a --- /dev/null +++ b/src/views/system/permission/index.vue @@ -0,0 +1,274 @@ + + + diff --git a/src/views/system/role-template/index.vue b/src/views/system/role-template/index.vue new file mode 100644 index 0000000..321a2cd --- /dev/null +++ b/src/views/system/role-template/index.vue @@ -0,0 +1,276 @@ + + + + + + diff --git a/src/views/system/role-template/modules/role-template-dialog.vue b/src/views/system/role-template/modules/role-template-dialog.vue new file mode 100644 index 0000000..b757b72 --- /dev/null +++ b/src/views/system/role-template/modules/role-template-dialog.vue @@ -0,0 +1,182 @@ + + + diff --git a/src/views/system/role-template/modules/role-template-permission-dialog.vue b/src/views/system/role-template/modules/role-template-permission-dialog.vue new file mode 100644 index 0000000..2308269 --- /dev/null +++ b/src/views/system/role-template/modules/role-template-permission-dialog.vue @@ -0,0 +1,452 @@ + + + + + diff --git a/src/views/system/role-template/modules/role-template-search.vue b/src/views/system/role-template/modules/role-template-search.vue new file mode 100644 index 0000000..89280c4 --- /dev/null +++ b/src/views/system/role-template/modules/role-template-search.vue @@ -0,0 +1,54 @@ + + + diff --git a/src/views/system/tenant-role/index.vue b/src/views/system/tenant-role/index.vue new file mode 100644 index 0000000..6a87b81 --- /dev/null +++ b/src/views/system/tenant-role/index.vue @@ -0,0 +1,305 @@ + + + + + + diff --git a/src/views/system/tenant-role/modules/role-edit-dialog.vue b/src/views/system/tenant-role/modules/role-edit-dialog.vue new file mode 100644 index 0000000..dd161cc --- /dev/null +++ b/src/views/system/tenant-role/modules/role-edit-dialog.vue @@ -0,0 +1,246 @@ + + + + + diff --git a/src/views/system/tenant-role/modules/role-permission-dialog.vue b/src/views/system/tenant-role/modules/role-permission-dialog.vue new file mode 100644 index 0000000..8b47698 --- /dev/null +++ b/src/views/system/tenant-role/modules/role-permission-dialog.vue @@ -0,0 +1,433 @@ + + + + + diff --git a/src/views/system/tenant-role/modules/role-search.vue b/src/views/system/tenant-role/modules/role-search.vue new file mode 100644 index 0000000..3ce769e --- /dev/null +++ b/src/views/system/tenant-role/modules/role-search.vue @@ -0,0 +1,66 @@ + + + diff --git a/src/views/system/user-center/index.vue b/src/views/system/user-center/index.vue new file mode 100644 index 0000000..1093050 --- /dev/null +++ b/src/views/system/user-center/index.vue @@ -0,0 +1,247 @@ + + + + diff --git a/src/views/system/user/index.vue b/src/views/system/user/index.vue new file mode 100644 index 0000000..6d2d3b0 --- /dev/null +++ b/src/views/system/user/index.vue @@ -0,0 +1,1211 @@ + + + + + diff --git a/src/views/system/user/modules/user-dialog.vue b/src/views/system/user/modules/user-dialog.vue new file mode 100644 index 0000000..8c7ba32 --- /dev/null +++ b/src/views/system/user/modules/user-dialog.vue @@ -0,0 +1,496 @@ + + + diff --git a/src/views/system/user/modules/user-search.vue b/src/views/system/user/modules/user-search.vue new file mode 100644 index 0000000..4f65d7b --- /dev/null +++ b/src/views/system/user/modules/user-search.vue @@ -0,0 +1,174 @@ + + + diff --git a/src/views/tenant/announcements/components/AnnouncementDetailDrawer.vue b/src/views/tenant/announcements/components/AnnouncementDetailDrawer.vue new file mode 100644 index 0000000..ea74cbc --- /dev/null +++ b/src/views/tenant/announcements/components/AnnouncementDetailDrawer.vue @@ -0,0 +1,404 @@ + + + diff --git a/src/views/tenant/announcements/components/AnnouncementFormDialog.vue b/src/views/tenant/announcements/components/AnnouncementFormDialog.vue new file mode 100644 index 0000000..0c71481 --- /dev/null +++ b/src/views/tenant/announcements/components/AnnouncementFormDialog.vue @@ -0,0 +1,463 @@ + + + diff --git a/src/views/tenant/announcements/create.vue b/src/views/tenant/announcements/create.vue new file mode 100644 index 0000000..937859b --- /dev/null +++ b/src/views/tenant/announcements/create.vue @@ -0,0 +1,746 @@ + + + diff --git a/src/views/tenant/announcements/edit.vue b/src/views/tenant/announcements/edit.vue new file mode 100644 index 0000000..9c2e29c --- /dev/null +++ b/src/views/tenant/announcements/edit.vue @@ -0,0 +1,621 @@ + + + diff --git a/src/views/tenant/announcements/index.vue b/src/views/tenant/announcements/index.vue new file mode 100644 index 0000000..6b12240 --- /dev/null +++ b/src/views/tenant/announcements/index.vue @@ -0,0 +1,698 @@ + + + + + diff --git a/src/views/tenant/billing/components/BatchActionToolbar.vue b/src/views/tenant/billing/components/BatchActionToolbar.vue new file mode 100644 index 0000000..5599881 --- /dev/null +++ b/src/views/tenant/billing/components/BatchActionToolbar.vue @@ -0,0 +1,134 @@ + + + + + diff --git a/src/views/tenant/billing/components/BillingDetailDrawer.vue b/src/views/tenant/billing/components/BillingDetailDrawer.vue new file mode 100644 index 0000000..fb25f54 --- /dev/null +++ b/src/views/tenant/billing/components/BillingDetailDrawer.vue @@ -0,0 +1,545 @@ + + + diff --git a/src/views/tenant/billing/components/CreateBillingDialog.vue b/src/views/tenant/billing/components/CreateBillingDialog.vue new file mode 100644 index 0000000..c1425ff --- /dev/null +++ b/src/views/tenant/billing/components/CreateBillingDialog.vue @@ -0,0 +1,373 @@ + + + diff --git a/src/views/tenant/billing/components/ExportDialog.vue b/src/views/tenant/billing/components/ExportDialog.vue new file mode 100644 index 0000000..8961635 --- /dev/null +++ b/src/views/tenant/billing/components/ExportDialog.vue @@ -0,0 +1,256 @@ + + + + + diff --git a/src/views/tenant/billing/components/RecordPaymentDialog.vue b/src/views/tenant/billing/components/RecordPaymentDialog.vue new file mode 100644 index 0000000..1764ef9 --- /dev/null +++ b/src/views/tenant/billing/components/RecordPaymentDialog.vue @@ -0,0 +1,289 @@ + + + + + diff --git a/src/views/tenant/billing/index.vue b/src/views/tenant/billing/index.vue new file mode 100644 index 0000000..8c9ff7d --- /dev/null +++ b/src/views/tenant/billing/index.vue @@ -0,0 +1,1233 @@ + + + + + diff --git a/src/views/tenant/billing/statistics.vue b/src/views/tenant/billing/statistics.vue new file mode 100644 index 0000000..fe4f40b --- /dev/null +++ b/src/views/tenant/billing/statistics.vue @@ -0,0 +1,443 @@ + + + + + diff --git a/src/views/tenant/dashboard/index.vue b/src/views/tenant/dashboard/index.vue new file mode 100644 index 0000000..ced627e --- /dev/null +++ b/src/views/tenant/dashboard/index.vue @@ -0,0 +1,538 @@ + + + + + diff --git a/src/views/tenant/dictionary-override/components/CustomItemsPanel.vue b/src/views/tenant/dictionary-override/components/CustomItemsPanel.vue new file mode 100644 index 0000000..a280abf --- /dev/null +++ b/src/views/tenant/dictionary-override/components/CustomItemsPanel.vue @@ -0,0 +1,201 @@ + + + + + diff --git a/src/views/tenant/dictionary-override/components/DualPaneView.vue b/src/views/tenant/dictionary-override/components/DualPaneView.vue new file mode 100644 index 0000000..b675863 --- /dev/null +++ b/src/views/tenant/dictionary-override/components/DualPaneView.vue @@ -0,0 +1,63 @@ + + + + + diff --git a/src/views/tenant/dictionary-override/components/LabelOverrideFormDialog.vue b/src/views/tenant/dictionary-override/components/LabelOverrideFormDialog.vue new file mode 100644 index 0000000..3b1aae2 --- /dev/null +++ b/src/views/tenant/dictionary-override/components/LabelOverrideFormDialog.vue @@ -0,0 +1,274 @@ + + + + + diff --git a/src/views/tenant/dictionary-override/components/LabelOverridePanel.vue b/src/views/tenant/dictionary-override/components/LabelOverridePanel.vue new file mode 100644 index 0000000..2a597a8 --- /dev/null +++ b/src/views/tenant/dictionary-override/components/LabelOverridePanel.vue @@ -0,0 +1,269 @@ + + + + + diff --git a/src/views/tenant/dictionary-override/components/OverrideToggle.vue b/src/views/tenant/dictionary-override/components/OverrideToggle.vue new file mode 100644 index 0000000..a6136c9 --- /dev/null +++ b/src/views/tenant/dictionary-override/components/OverrideToggle.vue @@ -0,0 +1,68 @@ + + + + + diff --git a/src/views/tenant/dictionary-override/components/SortableDragDrop.vue b/src/views/tenant/dictionary-override/components/SortableDragDrop.vue new file mode 100644 index 0000000..8336493 --- /dev/null +++ b/src/views/tenant/dictionary-override/components/SortableDragDrop.vue @@ -0,0 +1,175 @@ + + + + + diff --git a/src/views/tenant/dictionary-override/components/SystemItemsPanel.vue b/src/views/tenant/dictionary-override/components/SystemItemsPanel.vue new file mode 100644 index 0000000..8776873 --- /dev/null +++ b/src/views/tenant/dictionary-override/components/SystemItemsPanel.vue @@ -0,0 +1,107 @@ + + + + + diff --git a/src/views/tenant/dictionary-override/index.vue b/src/views/tenant/dictionary-override/index.vue new file mode 100644 index 0000000..5a8d81a --- /dev/null +++ b/src/views/tenant/dictionary-override/index.vue @@ -0,0 +1,261 @@ + + + + + diff --git a/src/views/tenant/dictionary/index.vue b/src/views/tenant/dictionary/index.vue new file mode 100644 index 0000000..30da699 --- /dev/null +++ b/src/views/tenant/dictionary/index.vue @@ -0,0 +1,44 @@ + + + + + diff --git a/src/views/tenant/package/composables/useFeatureStrategyTable.ts b/src/views/tenant/package/composables/useFeatureStrategyTable.ts new file mode 100644 index 0000000..9db869f --- /dev/null +++ b/src/views/tenant/package/composables/useFeatureStrategyTable.ts @@ -0,0 +1,115 @@ +import { computed, type ComputedRef } from 'vue' +import type { Composer } from 'vue-i18n' +import type { FeaturePolicyFieldDef, FeaturePolicyModel } from '../types' +import { FeaturePolicySections, parseFeaturePolicyJson } from '../types' + +export interface FeatureStrategyRow { + name: string + valueText: string + isEnabled: boolean + conditionsText: string +} + +/** + * 功能策略表格数据生成 composable + * @param t i18n 实例 + * @param i18nPrefix 国际化 key 前缀(如 'tenantPackage.detail.featureStrategy') + * @param jsonValue 功能策略 JSON 字符串(响应式) + */ +export function useFeatureStrategyTable( + t: Composer['t'], + i18nPrefix: string, + jsonValue: ComputedRef +) { + // 1. 构建 label key 映射表 + const featurePolicyLabelKeyMap = new Map( + FeaturePolicySections.flatMap((section) => + section.fields.map((field) => [field.key, field.labelKey] as const) + ) + ) + + // 2. 解析结果 + const parseResult = computed(() => parseFeaturePolicyJson(jsonValue.value || '')) + const isValidJson = computed(() => parseResult.value.isValidJson) + + // 3. 辅助函数 + const resolvePolicyLabel = (key: string) => { + const labelKey = featurePolicyLabelKeyMap.get(key) + return labelKey ? t(labelKey) : t(`${i18nPrefix}.conditions.unknownDependency`, { key }) + } + + const getPolicyValue = (model: FeaturePolicyModel, keyPath: string): unknown => { + const [group, ...rest] = keyPath.split('.') + const fieldKey = rest.join('.') + + if (group === 'features') return model.features?.[fieldKey] + if (group === 'quotas') return model.quotas?.[fieldKey] + return undefined + } + + const formatPolicyValue = (field: FeaturePolicyFieldDef, model: FeaturePolicyModel): string => { + const raw = getPolicyValue(model, field.key) + + if (field.type === 'boolean') { + return raw ? t('common.yes') : t('common.no') + } + + if (field.type === 'number') { + return typeof raw === 'number' ? String(raw) : t(`${i18nPrefix}.value.unlimited`) + } + + return raw === null || raw === undefined ? '' : String(raw) + } + + const buildPolicyConditions = (field: FeaturePolicyFieldDef): string => { + const parts: string[] = [] + + if (field.dependsOnKey) { + parts.push( + t(`${i18nPrefix}.conditions.dependsOnEnabled`, { + name: resolvePolicyLabel(field.dependsOnKey) + }) + ) + } + + if (typeof field.min === 'number') { + parts.push(t(`${i18nPrefix}.conditions.min`, { min: field.min })) + } + + if (typeof field.max === 'number') { + parts.push(t(`${i18nPrefix}.conditions.max`, { max: field.max })) + } + + return parts.length ? parts.join(';') : t(`${i18nPrefix}.noConditions`) + } + + const buildFeatureStrategyRow = ( + field: FeaturePolicyFieldDef, + model: FeaturePolicyModel + ): FeatureStrategyRow => { + const dependsOk = field.dependsOnKey ? !!getPolicyValue(model, field.dependsOnKey) : true + const raw = getPolicyValue(model, field.key) + const isEnabled = field.type === 'boolean' ? !!raw : dependsOk + + return { + name: t(field.labelKey), + valueText: formatPolicyValue(field, model), + isEnabled, + conditionsText: buildPolicyConditions(field) + } + } + + // 4. 生成表格行数据 + const rows = computed(() => { + const model = parseResult.value.model + return FeaturePolicySections.flatMap((section) => + section.fields.map((field) => buildFeatureStrategyRow(field, model)) + ) + }) + + return { + rows, + isValidJson, + parseResult + } +} diff --git a/src/views/tenant/package/index.vue b/src/views/tenant/package/index.vue new file mode 100644 index 0000000..ce1bcce --- /dev/null +++ b/src/views/tenant/package/index.vue @@ -0,0 +1,1301 @@ + + + + + diff --git a/src/views/tenant/package/modules/package-detail-drawer.vue b/src/views/tenant/package/modules/package-detail-drawer.vue new file mode 100644 index 0000000..adc90b4 --- /dev/null +++ b/src/views/tenant/package/modules/package-detail-drawer.vue @@ -0,0 +1,319 @@ + + + + + diff --git a/src/views/tenant/package/modules/package-feature-policy-dialog.vue b/src/views/tenant/package/modules/package-feature-policy-dialog.vue new file mode 100644 index 0000000..7c86648 --- /dev/null +++ b/src/views/tenant/package/modules/package-feature-policy-dialog.vue @@ -0,0 +1,399 @@ + + + + + diff --git a/src/views/tenant/package/modules/package-form-dialog.vue b/src/views/tenant/package/modules/package-form-dialog.vue new file mode 100644 index 0000000..f0c7820 --- /dev/null +++ b/src/views/tenant/package/modules/package-form-dialog.vue @@ -0,0 +1,662 @@ + + + diff --git a/src/views/tenant/package/modules/package-quota-dialog.vue b/src/views/tenant/package/modules/package-quota-dialog.vue new file mode 100644 index 0000000..e6d10ae --- /dev/null +++ b/src/views/tenant/package/modules/package-quota-dialog.vue @@ -0,0 +1,362 @@ + + + + + diff --git a/src/views/tenant/package/modules/package-search.vue b/src/views/tenant/package/modules/package-search.vue new file mode 100644 index 0000000..43cd8c5 --- /dev/null +++ b/src/views/tenant/package/modules/package-search.vue @@ -0,0 +1,72 @@ + + + diff --git a/src/views/tenant/package/types/feature-policy-schema.ts b/src/views/tenant/package/types/feature-policy-schema.ts new file mode 100644 index 0000000..15edaf9 --- /dev/null +++ b/src/views/tenant/package/types/feature-policy-schema.ts @@ -0,0 +1,355 @@ +export type FeaturePolicyValueType = 'boolean' | 'number' | 'string' | 'multi_boolean' + +export interface FeaturePolicyFieldDef { + key: string + labelKey: string + type: FeaturePolicyValueType + placeholderKey?: string + min?: number + max?: number + unitKey?: string + dependsOnKey?: string +} + +export interface FeaturePolicySectionDef { + key: string + titleKey: string + fields: FeaturePolicyFieldDef[] +} + +export interface FeaturePolicyCustomItem { + key: string + label: string + type: 'boolean' | 'number' | 'string' + value: boolean | number | string +} + +export interface FeaturePolicyModel { + features: Record + quotas: Record + extra: { + customItems: FeaturePolicyCustomItem[] + unknown?: Record + } +} + +export interface FeaturePolicyPreset { + key: string + labelKey: string + value: Partial +} + +export const FeaturePolicySections: FeaturePolicySectionDef[] = [ + { + key: 'features', + titleKey: 'tenantPackage.featurePolicy.sections.features', + fields: [ + { + key: 'features.reportsExport', + labelKey: 'tenantPackage.featurePolicy.features.reportsExport', + type: 'boolean' + }, + { + key: 'features.printing', + labelKey: 'tenantPackage.featurePolicy.features.printing', + type: 'boolean' + }, + { + key: 'features.apiAccess', + labelKey: 'tenantPackage.featurePolicy.features.apiAccess', + type: 'boolean' + }, + { + key: 'features.marketingCoupon', + labelKey: 'tenantPackage.featurePolicy.features.coupon', + type: 'boolean' + }, + { + key: 'features.marketingFullReduction', + labelKey: 'tenantPackage.featurePolicy.features.fullReduction', + type: 'boolean' + }, + { + key: 'features.marketingMember', + labelKey: 'tenantPackage.featurePolicy.features.member', + type: 'boolean' + }, + { + key: 'features.marketingPoints', + labelKey: 'tenantPackage.featurePolicy.features.points', + type: 'boolean' + } + ] + }, + { + key: 'quotas', + titleKey: 'tenantPackage.featurePolicy.sections.quotas', + fields: [ + { + key: 'quotas.maxProducts', + labelKey: 'tenantPackage.featurePolicy.quotas.maxProducts', + type: 'number', + min: 0, + placeholderKey: 'tenantPackage.featurePolicy.quotas.unlimitedPlaceholder' + }, + { + key: 'quotas.maxMenus', + labelKey: 'tenantPackage.featurePolicy.quotas.maxMenus', + type: 'number', + min: 0, + placeholderKey: 'tenantPackage.featurePolicy.quotas.unlimitedPlaceholder' + }, + { + key: 'quotas.maxApiCallsPerDay', + labelKey: 'tenantPackage.featurePolicy.quotas.maxApiCallsPerDay', + type: 'number', + min: 0, + placeholderKey: 'tenantPackage.featurePolicy.quotas.unlimitedPlaceholder', + dependsOnKey: 'features.apiAccess' + } + ] + } +] + +export const FeaturePolicyPresets: FeaturePolicyPreset[] = [ + { + key: 'blank', + labelKey: 'tenantPackage.featurePolicy.presets.blank', + value: { + features: {}, + quotas: {} + } + }, + { + key: 'standard', + labelKey: 'tenantPackage.featurePolicy.presets.standard', + value: { + features: { + reportsExport: true, + printing: true, + apiAccess: false, + marketingCoupon: false, + marketingFullReduction: true, + marketingMember: false, + marketingPoints: false + }, + quotas: { + maxProducts: 500, + maxMenus: 30 + } + } + }, + { + key: 'pro', + labelKey: 'tenantPackage.featurePolicy.presets.pro', + value: { + features: { + reportsExport: true, + printing: true, + apiAccess: true, + marketingCoupon: true, + marketingFullReduction: true, + marketingMember: true, + marketingPoints: true + }, + quotas: { + maxProducts: 3000, + maxMenus: 200, + maxApiCallsPerDay: 20000 + } + } + } +] + +export function createDefaultFeaturePolicy(): FeaturePolicyModel { + return { + features: {}, + quotas: {}, + extra: { + customItems: [], + unknown: {} + } + } +} + +export function parseFeaturePolicyJson(value: string): { + model: FeaturePolicyModel + isValidJson: boolean +} { + // 1. 空值视为默认 + if (!value?.trim()) return { model: createDefaultFeaturePolicy(), isValidJson: true } + + // 2. JSON 解析失败则回退默认,但标记无效 + let raw: unknown + try { + raw = JSON.parse(value) + } catch { + return { model: createDefaultFeaturePolicy(), isValidJson: false } + } + + if (!raw || typeof raw !== 'object') { + return { model: createDefaultFeaturePolicy(), isValidJson: false } + } + + // 3. 兼容旧结构:limits/api/reports/printing/marketing + const rawObj = raw as Record + const model = createDefaultFeaturePolicy() + + // 3.1 映射 features + const features: Record = {} + const rawReports = toRecord(rawObj.reports) + const rawPrinting = toRecord(rawObj.printing) + const rawApi = toRecord(rawObj.api) + const rawMarketing = toRecord(rawObj.marketing) + + if (typeof rawReports.exportEnabled === 'boolean') + features.reportsExport = rawReports.exportEnabled + if (typeof rawPrinting.enabled === 'boolean') features.printing = rawPrinting.enabled + if (typeof rawApi.enabled === 'boolean') features.apiAccess = rawApi.enabled + + if (typeof rawMarketing.coupon === 'boolean') features.marketingCoupon = rawMarketing.coupon + if (typeof rawMarketing.fullReduction === 'boolean') + features.marketingFullReduction = rawMarketing.fullReduction + if (typeof rawMarketing.member === 'boolean') features.marketingMember = rawMarketing.member + if (typeof rawMarketing.points === 'boolean') features.marketingPoints = rawMarketing.points + + model.features = features + + // 3.2 映射 quotas + const quotas: Record = {} + const rawLimits = toRecord(rawObj.limits) + quotas.maxProducts = normalizeNumber(rawLimits.maxProducts) + quotas.maxMenus = normalizeNumber(rawLimits.maxMenus) + quotas.maxApiCallsPerDay = normalizeNumber(rawApi.maxCallsPerDay) + model.quotas = quotas + + // 3.3 合并 extra:优先保留 raw.extra,再叠加未知顶层字段 + const rawExtra = toRecord(rawObj.extra) + const unknown: Record = {} + const knownKeys = new Set(['limits', 'api', 'reports', 'printing', 'marketing', 'extra']) + Object.keys(rawObj).forEach((k) => { + if (!knownKeys.has(k)) unknown[k] = rawObj[k] + }) + + model.extra = { + customItems: normalizeCustomItems(rawExtra.customItems), + unknown: { ...unknown, ...rawExtra } + } + + return { model, isValidJson: true } +} + +export function serializeFeaturePolicy(model: FeaturePolicyModel): string { + // 1. 兼容旧结构输出:limits/api/reports/printing/marketing + extra + const features = model.features || {} + const quotas = model.quotas || {} + + const payload: Record = { + limits: { + maxProducts: normalizeNumber(quotas.maxProducts), + maxMenus: normalizeNumber(quotas.maxMenus) + }, + api: { + enabled: !!features.apiAccess, + maxCallsPerDay: features.apiAccess ? normalizeNumber(quotas.maxApiCallsPerDay) : undefined + }, + reports: { + exportEnabled: !!features.reportsExport + }, + printing: { + enabled: !!features.printing + }, + marketing: { + coupon: !!features.marketingCoupon, + fullReduction: !!features.marketingFullReduction, + member: !!features.marketingMember, + points: !!features.marketingPoints + } + } + + // 2. 合并 extra(保留未知字段 + 自定义扩展项) + const extra: Record = { ...(model.extra?.unknown || {}) } + extra.customItems = normalizeCustomItems(model.extra?.customItems) + + if (Object.keys(extra).length) { + payload.extra = extra + } + + return JSON.stringify(payload, null, 2) +} + +export function validateFeaturePolicy(model: FeaturePolicyModel): string[] { + const errors: string[] = [] + + // 1. 数值校验:非负 + const quotas = model.quotas || {} + const numberKeys: Array<[string, unknown]> = [ + ['quotas.maxProducts', quotas.maxProducts], + ['quotas.maxMenus', quotas.maxMenus], + ['quotas.maxApiCallsPerDay', quotas.maxApiCallsPerDay] + ] + numberKeys.forEach(([key, value]) => { + if (value === null || value === undefined || value === '') return + const num = Number(value) + if (!Number.isFinite(num) || num < 0) errors.push(key) + }) + + // 2. 依赖校验:API 未开启则不允许填写 maxApiCallsPerDay + if ( + !model.features?.apiAccess && + quotas.maxApiCallsPerDay !== undefined && + quotas.maxApiCallsPerDay !== null + ) { + errors.push('quotas.maxApiCallsPerDay') + } + + // 3. 自定义扩展项 key 校验与去重 + const seen = new Set() + ;(model.extra?.customItems || []).forEach((it, idx) => { + const key = (it?.key || '').trim() + const label = (it?.label || '').trim() + + if (!key) errors.push(`extra.customItems[${idx}].key`) + if (!label) errors.push(`extra.customItems[${idx}].label`) + if (key && !/^[a-zA-Z][a-zA-Z0-9_]{0,63}$/.test(key)) + errors.push(`extra.customItems[${idx}].key`) + if (key && seen.has(key)) errors.push(`extra.customItems[${idx}].key`) + if (key) seen.add(key) + }) + + return errors +} + +function toRecord(value: unknown): Record { + return value && typeof value === 'object' ? (value as Record) : {} +} + +function normalizeNumber(value: unknown): number | undefined { + if (value === null || value === undefined || value === '') return undefined + const num = Number(value) + if (!Number.isFinite(num)) return undefined + if (num < 0) return 0 + return Math.floor(num) +} + +function normalizeCustomItems(value: unknown): FeaturePolicyCustomItem[] { + if (!Array.isArray(value)) return [] + return value + .filter((x) => x && typeof x === 'object') + .map((x) => x as FeaturePolicyCustomItem) + .map((x) => ({ + key: String(x.key ?? '').trim(), + label: String(x.label ?? '').trim(), + type: (x.type === 'boolean' || x.type === 'number' || x.type === 'string' + ? x.type + : 'string') as 'boolean' | 'number' | 'string', + value: + x.type === 'boolean' + ? !!x.value + : x.type === 'number' + ? Number.isFinite(Number(x.value)) + ? Number(x.value) + : 0 + : String(x.value ?? '') + })) +} diff --git a/src/views/tenant/package/types/index.ts b/src/views/tenant/package/types/index.ts new file mode 100644 index 0000000..8eca96f --- /dev/null +++ b/src/views/tenant/package/types/index.ts @@ -0,0 +1 @@ +export * from './feature-policy-schema' diff --git a/src/views/tenant/quota-package/components/PurchaseQuotaDialog.vue b/src/views/tenant/quota-package/components/PurchaseQuotaDialog.vue new file mode 100644 index 0000000..929805d --- /dev/null +++ b/src/views/tenant/quota-package/components/PurchaseQuotaDialog.vue @@ -0,0 +1,215 @@ + + + + + diff --git a/src/views/tenant/quota-package/components/QuotaAlertConfigPanel.vue b/src/views/tenant/quota-package/components/QuotaAlertConfigPanel.vue new file mode 100644 index 0000000..34be64c --- /dev/null +++ b/src/views/tenant/quota-package/components/QuotaAlertConfigPanel.vue @@ -0,0 +1,137 @@ + + + + + diff --git a/src/views/tenant/quota-package/components/QuotaPackageFormDialog.vue b/src/views/tenant/quota-package/components/QuotaPackageFormDialog.vue new file mode 100644 index 0000000..ac49fc5 --- /dev/null +++ b/src/views/tenant/quota-package/components/QuotaPackageFormDialog.vue @@ -0,0 +1,239 @@ + + + diff --git a/src/views/tenant/quota-package/components/QuotaPackageListPanel.vue b/src/views/tenant/quota-package/components/QuotaPackageListPanel.vue new file mode 100644 index 0000000..84fe996 --- /dev/null +++ b/src/views/tenant/quota-package/components/QuotaPackageListPanel.vue @@ -0,0 +1,327 @@ + + + + + diff --git a/src/views/tenant/quota-package/components/TenantQuotaPurchaseList.vue b/src/views/tenant/quota-package/components/TenantQuotaPurchaseList.vue new file mode 100644 index 0000000..9458723 --- /dev/null +++ b/src/views/tenant/quota-package/components/TenantQuotaPurchaseList.vue @@ -0,0 +1,270 @@ + + + + + diff --git a/src/views/tenant/quota-package/components/TenantQuotaUsageDashboard.vue b/src/views/tenant/quota-package/components/TenantQuotaUsageDashboard.vue new file mode 100644 index 0000000..1ff5f37 --- /dev/null +++ b/src/views/tenant/quota-package/components/TenantQuotaUsageDashboard.vue @@ -0,0 +1,276 @@ + + + + + diff --git a/src/views/tenant/quota-package/index.vue b/src/views/tenant/quota-package/index.vue new file mode 100644 index 0000000..0ed457d --- /dev/null +++ b/src/views/tenant/quota-package/index.vue @@ -0,0 +1,38 @@ + + + + + diff --git a/src/views/tenant/review/components/ReviewDialog.vue b/src/views/tenant/review/components/ReviewDialog.vue new file mode 100644 index 0000000..3c63e80 --- /dev/null +++ b/src/views/tenant/review/components/ReviewDialog.vue @@ -0,0 +1,890 @@ + + + + + diff --git a/src/views/tenant/review/index.vue b/src/views/tenant/review/index.vue new file mode 100644 index 0000000..c6137e7 --- /dev/null +++ b/src/views/tenant/review/index.vue @@ -0,0 +1,245 @@ + + + + + diff --git a/src/views/tenant/review/modules/tenant-review-search.vue b/src/views/tenant/review/modules/tenant-review-search.vue new file mode 100644 index 0000000..e68c572 --- /dev/null +++ b/src/views/tenant/review/modules/tenant-review-search.vue @@ -0,0 +1,103 @@ + + + diff --git a/src/views/tenant/review/types/index.ts b/src/views/tenant/review/types/index.ts new file mode 100644 index 0000000..a7d524b --- /dev/null +++ b/src/views/tenant/review/types/index.ts @@ -0,0 +1 @@ +export * from './search-form' diff --git a/src/views/tenant/review/types/search-form.ts b/src/views/tenant/review/types/search-form.ts new file mode 100644 index 0000000..ff12852 --- /dev/null +++ b/src/views/tenant/review/types/search-form.ts @@ -0,0 +1,10 @@ +import type { TenantStatus } from '@/enums/TenantStatus' +import type { TenantVerificationStatus } from '@/enums/TenantVerificationStatus' + +export interface TenantReviewSearchForm { + tenantName: string + contactName: string + contactPhone: string + status?: TenantStatus + verificationStatus?: TenantVerificationStatus +} diff --git a/src/views/tenant/subscription/components/BatchExtendDialog.vue b/src/views/tenant/subscription/components/BatchExtendDialog.vue new file mode 100644 index 0000000..c3dfaa0 --- /dev/null +++ b/src/views/tenant/subscription/components/BatchExtendDialog.vue @@ -0,0 +1,326 @@ + + + + + diff --git a/src/views/tenant/subscription/components/BatchRemindDialog.vue b/src/views/tenant/subscription/components/BatchRemindDialog.vue new file mode 100644 index 0000000..5713a44 --- /dev/null +++ b/src/views/tenant/subscription/components/BatchRemindDialog.vue @@ -0,0 +1,286 @@ + + + + + diff --git a/src/views/tenant/subscription/components/ChangePlanDialog.vue b/src/views/tenant/subscription/components/ChangePlanDialog.vue new file mode 100644 index 0000000..7451e44 --- /dev/null +++ b/src/views/tenant/subscription/components/ChangePlanDialog.vue @@ -0,0 +1,200 @@ + + + + + diff --git a/src/views/tenant/subscription/components/ExtendSubscriptionDialog.vue b/src/views/tenant/subscription/components/ExtendSubscriptionDialog.vue new file mode 100644 index 0000000..bc05229 --- /dev/null +++ b/src/views/tenant/subscription/components/ExtendSubscriptionDialog.vue @@ -0,0 +1,180 @@ + + + + + diff --git a/src/views/tenant/subscription/components/StatusChangeDialog.vue b/src/views/tenant/subscription/components/StatusChangeDialog.vue new file mode 100644 index 0000000..f44e80e --- /dev/null +++ b/src/views/tenant/subscription/components/StatusChangeDialog.vue @@ -0,0 +1,174 @@ + + + diff --git a/src/views/tenant/subscription/components/SubscriptionDetailDrawer.vue b/src/views/tenant/subscription/components/SubscriptionDetailDrawer.vue new file mode 100644 index 0000000..264d026 --- /dev/null +++ b/src/views/tenant/subscription/components/SubscriptionDetailDrawer.vue @@ -0,0 +1,525 @@ + + + + + diff --git a/src/views/tenant/subscription/index.vue b/src/views/tenant/subscription/index.vue new file mode 100644 index 0000000..a65638d --- /dev/null +++ b/src/views/tenant/subscription/index.vue @@ -0,0 +1,503 @@ + + + + + diff --git a/src/views/tenant/tenant-list/components/ImageUploadField.vue b/src/views/tenant/tenant-list/components/ImageUploadField.vue new file mode 100644 index 0000000..bd177cb --- /dev/null +++ b/src/views/tenant/tenant-list/components/ImageUploadField.vue @@ -0,0 +1,139 @@ + + + diff --git a/src/views/tenant/tenant-list/components/TenantDetailDrawer.vue b/src/views/tenant/tenant-list/components/TenantDetailDrawer.vue new file mode 100644 index 0000000..09fb012 --- /dev/null +++ b/src/views/tenant/tenant-list/components/TenantDetailDrawer.vue @@ -0,0 +1,529 @@ + + + diff --git a/src/views/tenant/tenant-list/components/TenantEdit.vue b/src/views/tenant/tenant-list/components/TenantEdit.vue new file mode 100644 index 0000000..86092fd --- /dev/null +++ b/src/views/tenant/tenant-list/components/TenantEdit.vue @@ -0,0 +1,300 @@ + + + diff --git a/src/views/tenant/tenant-list/components/TenantExtendSubscriptionDialog.vue b/src/views/tenant/tenant-list/components/TenantExtendSubscriptionDialog.vue new file mode 100644 index 0000000..ec0a428 --- /dev/null +++ b/src/views/tenant/tenant-list/components/TenantExtendSubscriptionDialog.vue @@ -0,0 +1,128 @@ + + + diff --git a/src/views/tenant/tenant-list/components/TenantFreezeDialog.vue b/src/views/tenant/tenant-list/components/TenantFreezeDialog.vue new file mode 100644 index 0000000..9ae48c5 --- /dev/null +++ b/src/views/tenant/tenant-list/components/TenantFreezeDialog.vue @@ -0,0 +1,133 @@ + + + diff --git a/src/views/tenant/tenant-list/components/TenantImpersonateDialog.vue b/src/views/tenant/tenant-list/components/TenantImpersonateDialog.vue new file mode 100644 index 0000000..478fc9a --- /dev/null +++ b/src/views/tenant/tenant-list/components/TenantImpersonateDialog.vue @@ -0,0 +1,143 @@ + + + diff --git a/src/views/tenant/tenant-list/components/TenantManualCreateDialog.vue b/src/views/tenant/tenant-list/components/TenantManualCreateDialog.vue new file mode 100644 index 0000000..2c9da2e --- /dev/null +++ b/src/views/tenant/tenant-list/components/TenantManualCreateDialog.vue @@ -0,0 +1,987 @@ + + + diff --git a/src/views/tenant/tenant-list/components/TenantQuotaDrawer.vue b/src/views/tenant/tenant-list/components/TenantQuotaDrawer.vue new file mode 100644 index 0000000..c941a12 --- /dev/null +++ b/src/views/tenant/tenant-list/components/TenantQuotaDrawer.vue @@ -0,0 +1,477 @@ + + + + + + + diff --git a/src/views/tenant/tenant-list/components/TenantResetAdminDialog.vue b/src/views/tenant/tenant-list/components/TenantResetAdminDialog.vue new file mode 100644 index 0000000..6d86f5d --- /dev/null +++ b/src/views/tenant/tenant-list/components/TenantResetAdminDialog.vue @@ -0,0 +1,145 @@ + + + diff --git a/src/views/tenant/tenant-list/index.vue b/src/views/tenant/tenant-list/index.vue new file mode 100644 index 0000000..3dabd1a --- /dev/null +++ b/src/views/tenant/tenant-list/index.vue @@ -0,0 +1,352 @@ + + + + + diff --git a/src/views/tenant/tenant-list/modules/tenant-search.vue b/src/views/tenant/tenant-list/modules/tenant-search.vue new file mode 100644 index 0000000..6bc9a46 --- /dev/null +++ b/src/views/tenant/tenant-list/modules/tenant-search.vue @@ -0,0 +1,111 @@ + + + diff --git a/src/views/test/RichTextEditorTest.vue b/src/views/test/RichTextEditorTest.vue new file mode 100644 index 0000000..7cac835 --- /dev/null +++ b/src/views/test/RichTextEditorTest.vue @@ -0,0 +1,158 @@ + + + + + + diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..4331962 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "esnext", + "module": "esnext", + "moduleResolution": "node", + "strict": true, + "jsx": "preserve", + "sourceMap": true, + "resolveJsonModule": true, + "esModuleInterop": true, + "lib": ["esnext", "dom"], + "types": ["vite/client", "node", "element-plus/global"], + "skipLibCheck": true, + "baseUrl": ".", + "paths": { + "@/*": ["src/*"], + "@views/*": ["src/views/*"], + "@imgs/*": ["src/assets/images/*"], + "@icons/*": ["src/assets/icons/*"], + "@utils/*": ["src/utils/*"], + "@stores/*": ["src/store/*"], + "@plugins/*": ["src/plugins/*"], + "@styles/*": ["src/assets/styles/*"] + } + }, + "include": ["src/**/*", "src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], + "exclude": ["node_modules", "dist", "**/*.js"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..b704f07 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,156 @@ +import { defineConfig, loadEnv } from 'vite' +import vue from '@vitejs/plugin-vue' +import path from 'path' +import { fileURLToPath } from 'url' +import vueDevTools from 'vite-plugin-vue-devtools' +import viteCompression from 'vite-plugin-compression' +import Components from 'unplugin-vue-components/vite' +import AutoImport from 'unplugin-auto-import/vite' +import ElementPlus from 'unplugin-element-plus/vite' +import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' +import tailwindcss from '@tailwindcss/vite' +// import { visualizer } from 'rollup-plugin-visualizer' + +export default ({ mode }: { mode: string }) => { + const root = process.cwd() + const env = loadEnv(mode, root) + const { VITE_VERSION, VITE_PORT, VITE_BASE_URL, VITE_API_URL, VITE_API_PROXY_URL } = env + + console.log(`🚀 API_URL = ${VITE_API_URL}`) + console.log(`🚀 VERSION = ${VITE_VERSION}`) + + return defineConfig({ + define: { + __APP_VERSION__: JSON.stringify(VITE_VERSION) + }, + base: VITE_BASE_URL, + server: { + port: Number(VITE_PORT), + proxy: { + '/api': { + target: VITE_API_PROXY_URL, + changeOrigin: true + } + }, + host: true + }, + // 路径别名 + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)), + '@views': resolvePath('src/views'), + '@imgs': resolvePath('src/assets/images'), + '@icons': resolvePath('src/assets/icons'), + '@utils': resolvePath('src/utils'), + '@stores': resolvePath('src/store'), + '@styles': resolvePath('src/assets/styles') + } + }, + build: { + target: 'es2015', + outDir: 'dist', + chunkSizeWarningLimit: 2000, + minify: 'terser', + terserOptions: { + compress: { + // 生产环境去除部分 console(保留 warn/error,便于线上排障) + pure_funcs: ['console.log', 'console.debug', 'console.info'], + // 生产环境去除 debugger + drop_debugger: true + } + }, + dynamicImportVarsOptions: { + warnOnError: true, + exclude: [], + include: ['src/views/**/*.vue'] + } + }, + plugins: [ + vue(), + tailwindcss(), + // 自动按需导入 API + AutoImport({ + imports: ['vue', 'vue-router', 'pinia', '@vueuse/core'], + dts: 'src/types/import/auto-imports.d.ts', + resolvers: [ElementPlusResolver()], + eslintrc: { + enabled: true, + filepath: './.auto-import.json', + globalsPropValue: true + } + }), + // 自动按需导入组件 + Components({ + dts: 'src/types/import/components.d.ts', + resolvers: [ElementPlusResolver()] + }), + // 按需定制主题配置 + ElementPlus({ + useSource: true + }), + // 压缩 + viteCompression({ + verbose: false, // 是否在控制台输出压缩结果 + disable: false, // 是否禁用 + algorithm: 'gzip', // 压缩算法 + ext: '.gz', // 压缩后的文件名后缀 + threshold: 10240, // 只有大小大于该值的资源会被处理 10240B = 10KB + deleteOriginFile: false // 压缩后是否删除原文件 + }), + vueDevTools() + // 打包分析 + // visualizer({ + // open: true, + // gzipSize: true, + // brotliSize: true, + // filename: 'dist/stats.html' // 分析图生成的文件名及路径 + // }), + ], + // 依赖预构建:避免运行时重复请求与转换,提升首次加载速度 + optimizeDeps: { + include: [ + 'echarts/core', + 'echarts/charts', + 'echarts/components', + 'echarts/renderers', + 'xlsx', + 'xgplayer', + 'crypto-js', + 'file-saver', + 'vue-img-cutter', + 'element-plus/es', + 'element-plus/es/components/*/style/css', + 'element-plus/es/components/*/style/index' + ] + }, + css: { + preprocessorOptions: { + // sass variable and mixin + scss: { + additionalData: ` + @use "@styles/core/el-light.scss" as *; + @use "@styles/core/mixin.scss" as *; + ` + } + }, + postcss: { + plugins: [ + { + postcssPlugin: 'internal:charset-removal', + AtRule: { + charset: (atRule) => { + if (atRule.name === 'charset') { + atRule.remove() + } + } + } + } + ] + } + } + }) +} + +function resolvePath(paths: string) { + return path.resolve(__dirname, paths) +} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..8b8f76f --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,30 @@ +import { defineConfig } from 'vitest/config' +import vue from '@vitejs/plugin-vue' +import path from 'path' +import { fileURLToPath } from 'url' + +export default defineConfig({ + plugins: [vue()], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)), + '@views': resolvePath('src/views'), + '@imgs': resolvePath('src/assets/images'), + '@icons': resolvePath('src/assets/icons'), + '@utils': resolvePath('src/utils'), + '@stores': resolvePath('src/store'), + '@styles': resolvePath('src/assets/styles') + } + }, + test: { + environment: 'jsdom', + globals: false, + clearMocks: true, + // Windows + vite-node sourcemap edge case can surface as unhandled errors. + dangerouslyIgnoreUnhandledErrors: true + } +}) + +function resolvePath(paths: string) { + return path.resolve(__dirname, paths) +}