diff --git a/CLAUDE.md b/CLAUDE.md
index 8058a00..1a89804 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -87,10 +87,10 @@
8. **400(预检撞车)vs 409(竞态 IntegrityError)并存**——API 冻结前统一。
9. **limiter 全量清扫**(token/登录限流的内存键永不回访,长跑进程缓慢累积;收口 = 定期清扫)。
10. **lead_push_service 的 settings re-export 锚**(app.engine.config 的 settings 在 lead_push_service 顶部 re-export,22 处测试 monkeypatch 依赖此锚——改动前先迁测试)。
-11. **密钥掩码未实现**——GET config/settings 返回明文 api_key/smtp 密码;Phase 3 前端对接前必须定掩码哨兵方案(GET 吐 `__SET__` + PUT 收哨兵保留旧值)(出处:终审审查 I-2)。
-12. **站点管理权限现为仅超管**(spec §7.3 写租户管理员同左)——Phase 3 对接时决策放开或改 spec(出处:终审审查 M-2)。
+11. ~~密钥掩码未实现~~(Phase 3 收口,commit 0c25287:GET config/settings 四处响应密钥值掩码为哨兵 `__SET__`,PUT 收哨兵保留库值;前端 `admin/src/api/mask.ts` secretToSubmit/secretPlaceholder 同款契约)。
+12. ~~站点管理权限现为仅超管~~(Phase 3 收口,commit 0c25287:放开租户管理员同权,与 spec §7.3 原文一致)。
-**Phase 3 清理单**(Phase 1/2 审查挂账滚动合并;~~删除线~~ 项已在 Phase 2 完成):
+**Phase 3 清理单**(Phase 1/2 审查挂账滚动合并;~~删除线~~ 项已结,结项时点见各条注记;未结项滚入 Phase 4 清理单增补):
1. ~~FakeAI 三份拷贝收敛进 conftest~~(Task 10 完成:conftest.py 单一定义,tests/ 零拷贝)。
2. ~~rate_limit 与 ws_conn_limit 的 0 语义统一 + 配置类型校验~~(Task 10 完成)。
@@ -98,19 +98,41 @@
4. ~~avatar 从 seed_profiles 迁 uploads 卷~~(Task 7 完成:上传 + 三级解析链)。
5. **AIService.from_runtime 类方法**——组装正典化(出处:引擎改造审查)。
6. **boundary_filter 视图常量退役**——admin 重写后旧测试迁移时(出处:引擎改造审查)。
-7. **push enabled 布尔空槽语义**——admin 配置显式化时解决(出处:push 审查)。
+7. ~~push enabled 布尔空槽语义~~(Phase 3 收口:凭据页显式布尔开关 ElSwitch,normalize `enabled === true` 空槽归 false,提交永为显式布尔)。
8. **contact.template_languages 运行期接线**——后台批次(出处:配置 schema 审查)。
9. **话术含引号/换行的转义**——编辑器批次(`_format_deflection_pool` docstring 有注)。
10. **产品决策项**:国名纠正 e164 变化时是否补发更正邮件(上游行为更优);REST/WS 服务端 IP 口径是否统一(出处:上游吸收审查)。
11. **/health 分级健康**——Phase 4 部署批次(进程存活之外按依赖分级上报;出处:终审审查 M-3)。
12. **PromptProvider.from_dir 的 FileNotFoundError 带路径上下文**——提示词目录缺失时异常信息含实际路径(出处:终审审查 M-4)。
-## Phase 3 计划编写注意
+## Phase 3 完成状态(2026-10-13 收尾核对)
-- conftest 三坑纪律延续:`async_db` 夹具用 NullPool;TestClient 必须走 portal 桥接事件循环;禁止 `asyncio.run` 直开租户 session_factory(三个坑都在 tests/conftest.py 有既成解法,照抄勿发明)。
-- `build_app` / `app_client` / `TenantTestClient` 夹具已就绪,新测试优先复用而非自建 app 实例;**平台层测试用 `db_source_app` / `super_client` 夹具**(conftest.py:DB 源装配 + 超管登录态客户端,Phase 2 全部平台路由测试的既成姿势)。
-- 引擎侧读配置一律经 **config_views 单入口**(app/platform/config_views.py:resolve_email/push/ai_settings、resolve_contact_flags、context_seed_map、name_to_region_extras)——新消费点不再自写 resolver。
-- 统计前端照 **stats.py docstring 的指标口径表**展示(会话数不要求有消息、线索=三选一非空、推送成功率分母口径、北京日界趋势等,歧义处已定死)。
+**测试基线:后端 pytest 595 passed / 前端 vitest 156 passed**(Phase 3 起步 139,Task 9 平台设置页 +14、终审修复通用 JSON 组 +3;两端数字均已实跑核对,后端唯一 warning 仍为 starlette TestClient 弃用提示,无害)。`pnpm build`(vue-tsc --noEmit && vite build)零错误。后续回归以此为准。
+
+**交付形态**:`admin/` 子目录的 Vue3 + TS + Vite + Element Plus SPA(art-design-pro 模板,pnpm 管理),与后端同仓。本地起法:
+
+- 后端:`PYTHONUTF8=1 .venv/Scripts/python -m uvicorn app.main:app --port 8018 --workers 1`(不变)。
+- 前端:`cd admin && pnpm install && pnpm dev`(默认 3006,被占自动顺延);Vite 代理 `/api` → 8018,`/api/public` 与 `/static` 走 Host 改写代理(`VITE_TENANT_PREVIEW_HOST`,见 admin/.env.development 注释)。`pnpm test` 跑 vitest;`pnpm build` 出 dist。
+- 登录:超管 / 租户管理员同一登录页(滑块验证为纯前端交互校验);must_change_password 由后端 409 门强制,前端跳 `#/auth/force-change-password`。
+
+**admin 能力一览**(Phase 2 平台 API 的全量对接;菜单按角色过滤,后端 require_super / _tenant_guard 兜底):
+
+- 认证流:登录 / 强制改密 / 退出;pinia + 内存 token,401 统一跳登录,改密 409 契约锁定。
+- 仪表盘:平台总览 + 租户卡片墙(超管)/ 我的指标(租户管理员),时间窗切换。
+- 租户管理(仅超管):列表 / 创建向导(消费出厂档案)/ 详情编辑 / 站点与域名子资源 / 停用复启。
+- 会话与线索:列表(筛选 / 分页 / 租户切换)/ 详情消息流 / 推送日志 / 删除。
+- 配置中心:品牌(表单 / 本地简化预览 / 嵌入代码 / 头像上传)、提示词四件套与结构化词表、试金石面板、凭据(邮件 / 推送 / AI 表单 + SMTP 测试 + 推送探测)、contact/engine/misc 三组通用 JSON 编辑(终审 I-1 补齐 spec §7.3 全组覆盖)、版本历史(两版对比 / 回滚 / 组级重置出厂)。
+- 统计分析:卡片 / 转化漏斗 / 按日趋势 / 意向·来源·站点·国家分布(ECharts)/ 推送健康表;时间与租户筛选(口径同 stats.py docstring 口径表)。
+- 平台设置(仅超管):自由 KV 表的推荐键表单(ai.* / smtp.*,键名是前端约定)+ 存储总览表格;保存 = 逐键 PUT(只提交脏键)。**当前仅存储与展示——resolver 的平台默认回退链未接线(页头注记),引擎不读取这些值**;密钥键掩码哨兵交互与凭据页同款(清除 / 恢复保持)。
+
+**前端实现约定**(后续维护须知):
+
+- 视图逻辑沉淀在各 `views/**/model.ts` 纯函数(掩码判定 / 归一 / 脏检测 / 提交值构造 / 校验),vitest 直接覆盖;.vue 只做装配。auto-import 生效(ref/reactive/ElMessage 等无需手写 import)。
+- 密钥键判定前端镜像后端 `_SETTING_SECRET_MARKERS` 子串匹配('monkey' 含 'key' 也命中——与后端一致是契约,不"修正")。
+- PUT config 整组替换语义:前端封装读-改-写(GET 全组 → 改 → PUT);平台设置不同,为逐键 upsert。
+- `components.d.ts` 构建漂移:`pnpm build`/dev 会重写入库的 dts(仅行尾差异)——`git diff --ignore-cr-at-eol` 为空即 `git checkout --` 还原入库版(超集原则,勿提交行尾噪音)。
+
+**滚动纪律**(自 Phase 3 计划注意延续):conftest 三坑(NullPool / portal 桥接 / 禁 asyncio.run)、平台层测试用 `db_source_app` / `super_client` 夹具、引擎读配置走 config_views 单入口、统计展示照 stats.py 口径表——均照抄勿发明。
## Phase 4 部署记档(提前锚定,部署批次展开成 docs/deploy-production.md)
@@ -119,10 +141,21 @@
- **TENANT_SOURCE=db 默认**(生产装配形态);`file` 为冷备/本地调试回退。
- **admin 域名反代区分**:`/api/platform/*` 前缀豁免租户 Host 解析——平台后台 API 可与租户聊天域名同源反代(按路径分流)或独立域名,两形态皆可;租户聊天面(`/`、`/static`、WS、`/api/public`、`/api/chat*`)仍严格按 Host 解析租户,未知 Host 404。
- **ADMIN_PASSWORD 默认值警示**(终审 I-4):lifespan 启动检测到 ADMIN_PASSWORD 仍是出厂默认(admin123)时打 WARNING 日志——生产部署必须设 ADMIN_PASSWORD 环境变量换掉,且超管首登立即改密(must_change_password 门强制)。
+- **Phase 4 清理单增补**(Phase 3 冒烟沉淀 + 终审记档;Phase 3 清理单未结项 5/6/8/9/10 一并滚入):
+ 1. `components.d.ts` 构建行尾漂移(长期收口 = 生成物出库或 EOL 规范钩子,现阶段靠手工还原)。
+ 2. 推送探测 env 回退真址:租户 push URL 空槽时 `resolve_push_settings` 回退 env `PUSH_API_URL`(默认即真 UniService 生产址)——后台点「探测」会对真实服务发空批量 POST(`{"messages": []}`,按契约文档应为无操作,但属真实出站)。收口 = 部署设 stub 域或探测端点拒绝 env 回退 URL。
+ 3. 生产反代需规划 admin 域的头像/品牌公开端点:`/api/public/*` 按租户 Host 解析,admin 域直挂必 404——开发期靠 Vite Host 改写代理,生产要么同源反代改 Host(单租户形态)要么前端改用租户域名绝对 URL。
+ 4. `VITE_TENANT_PREVIEW_HOST` 单一预览域名:开发代理只指向一个租户 Host,切换租户时头像等公开资源预览不准(.env.development 已注记)。
+ 5. **resolver 平台默认回退链接入 platform_settings**(终审 I-3 记档):平台设置当前仅存储与展示(admin 页头有注记),引擎不读取——回退链接线是 Phase 4 的功能项。
+ 6. **email/test 与 push/test 速率限制**(终审 I-3 记档):两测试端点无限流,复用登录限流器模式收口。
+ 7. **锁屏功能已摘除入口**(终审 M-1):用户菜单项删除 + 全局组件 enabled:false 不再挂载(AES 存 localStorage + 公开模板默认密钥,防御价值为零);代码待 Phase 4 决定删否。
+ 8. **租户切换器 composable 收拢**(终审 M-3 不做记档):仪表盘/会话/配置三页各自内联实现同一模式,收拢归后续。
+ 9. **ECharts 按需收窄**(终审 M-4 不做记档):stats 的 plugins/echarts.ts 全量注册,bundle 体积优化归后续。
## 文档索引
- 设计文档(spec):`docs/superpowers/specs/2026-10-12-unified-kefu-platform-design.md`
- Phase 1 实施计划:`docs/superpowers/plans/2026-10-12-phase1-engine-merge-tenantization.md`
- Phase 2 实施计划:`docs/superpowers/plans/2026-10-13-phase2-platform-layer.md`
+- Phase 3 实施计划:`docs/superpowers/plans/2026-10-13-phase3-admin-spa.md`
- 部署坑手册:Phase 4 补 `docs/deploy-production.md`(尚未存在;Phase 4 部署记档段已锚定要点)
diff --git a/README.md b/README.md
index 15183dd..e29375e 100644
--- a/README.md
+++ b/README.md
@@ -1,15 +1,16 @@
# all-kefu
-统一多租户 AI 客服平台:一套引擎 + 租户配置包,消除此前"每客户复制一套代码"的分叉模式。项目由 ai-kefu / ai-kefu-jc 两个复制分叉项目合并而来(jc 为主线),Phase 1 引擎合并与租户化改造、Phase 2 平台层(platform.db + admin API)已完成。
+统一多租户 AI 客服平台:一套引擎 + 租户配置包,消除此前"每客户复制一套代码"的分叉模式。项目由 ai-kefu / ai-kefu-jc 两个复制分叉项目合并而来(jc 为主线),Phase 1 引擎合并与租户化改造、Phase 2 平台层(platform.db + admin API)、Phase 3 管理后台 SPA 均已完成。
-**现状能力**(Phase 2 收尾时点):
+**现状能力**(Phase 3 收尾时点):
- 多租户引擎:TenantRuntime 隔离边界,engine 包不感知平台层,可整体拎走独立部署。
- 站点模型:一租户多站点(品牌/主题/站点级覆盖),词表、提示词、品牌全部配置化;配置存 platform.db 版本快照(分组校验 / 回滚 / 热加载),seed_profiles 为出厂档案与测试形态。
- Host 路由:按域名解析租户,未知 Host 404;WS 通道租户化客户端 key 防串库,另有限流与每 IP 连接上限。
- 引擎行为:cc 区号补全判定树、多语言墙(中/西/葡借道/俄/阿)、dual-contact 分步双要、CC 强制墙、lead 邮件推送。
-- 平台层(Phase 2):scrypt 认证 + 内存 token + 登录限流;租户管理(创建向导 / 站点 / 域名 / 停用);配置版本管线(整组替换 / 回滚 / 重置出厂 / 试金石);头像上传(魔数校验 + uuid 重命名);数据视图(会话 / 推送日志 / 删除,租户 scope 强制);统计(卡片 / 漏斗 / 趋势 / 分布 / 推送健康,逐库聚合禁 ATTACH);DB 源生产装配(TENANT_SOURCE=db 默认 + bootstrap_platform.py 引导)。admin API 全在 `/api/platform` 前缀(Host 豁免、路由依赖守护),端点清单见 [CLAUDE.md](CLAUDE.md)。
+- 平台层:scrypt 认证 + 内存 token + 登录限流;租户管理(创建向导 / 站点 / 域名 / 停用);配置版本管线(整组替换 / 回滚 / 重置出厂 / 试金石);头像上传(魔数校验 + uuid 重命名);数据视图(会话 / 推送日志 / 删除,租户 scope 强制);统计(卡片 / 漏斗 / 趋势 / 分布 / 推送健康,逐库聚合禁 ATTACH);密钥掩码哨兵(`__SET__` 往返);DB 源生产装配(TENANT_SOURCE=db 默认 + bootstrap_platform.py 引导)。admin API 全在 `/api/platform` 前缀(Host 豁免、路由依赖守护),端点清单见 [CLAUDE.md](CLAUDE.md)。
+- 管理后台 SPA(Phase 3,`admin/` 目录,Vue3 + Element Plus + pnpm):登录 / 强制改密 / 角色化菜单(超管 vs 租户管理员);仪表盘(平台总览 + 租户卡片墙 / 我的指标);租户管理与站点域名;会话与线索(筛选 / 详情消息流 / 推送日志 / 删除);配置中心(品牌 + 本地预览 + 嵌入代码 + 头像上传 / 提示词词表 / 试金石 / 凭据掩码编辑 + SMTP 测试 + 推送探测 / contact·engine·misc 通用 JSON 编辑 / 版本对比回滚重置);统计分析(ECharts 漏斗 / 趋势 / 分布 / 推送健康);平台设置(ai.* / smtp.* 推荐键 KV 表单 + 存储总览,现阶段仅存储不接引擎回退链)。
-**本地跑法与工作约定**:见 [CLAUDE.md](CLAUDE.md)(含 uvicorn 启动命令、Host 访问方式、Windows PYTHONUTF8 要求、全量回归基线 566)。
+**本地跑法**:后端 `PYTHONUTF8=1 .venv/Scripts/python -m uvicorn app.main:app --port 8018 --workers 1`;管理后台 `cd admin && pnpm install && pnpm dev`(Vite 代理 /api → 8018)。工作约定与测试基线(后端 pytest 595 / 前端 vitest 156)见 [CLAUDE.md](CLAUDE.md)。
设计文档(spec):[docs/superpowers/specs/2026-10-12-unified-kefu-platform-design.md](docs/superpowers/specs/2026-10-12-unified-kefu-platform-design.md)
diff --git a/admin/.env.development b/admin/.env.development
new file mode 100644
index 0000000..49c0be7
--- /dev/null
+++ b/admin/.env.development
@@ -0,0 +1,42 @@
+# 【开发】环境变量
+#
+# 说明:模板原把通用变量放 .env(全模式加载),但根 .gitignore 的
+# `.env` 规则会误伤 admin/.env——入库文件只有 .env.development /
+# .env.production 两份(Phase 3 终审 I-2 收口)。通用变量因此按
+# "两份都写"纪律维护:改通用项时两文件同步改。
+
+# 版本号(vite.config.ts 的 __APP_VERSION__ define)
+VITE_VERSION = 0.1.0
+
+# dev server 端口号(被占自动顺延)
+VITE_PORT = 3006
+
+# 应用部署基础路径(如部署在子目录 /admin,则设置为 /admin/)
+VITE_BASE_URL = /
+
+# 权限模式【 frontend 前端模式 / backend 后端模式 】
+VITE_ACCESS_MODE = frontend
+
+# 跨域请求时是否携带 Cookie(开启前需确保后端支持)
+VITE_WITH_CREDENTIALS = false
+
+# 是否打开路由信息
+VITE_OPEN_ROUTE_INFO = false
+
+# 锁屏加密密钥(锁屏入口已摘除——Phase 3 终审 M-1;密钥保留防遗留代码异常)
+VITE_LOCK_ENCRYPT_KEY = s3cur3k3y4adpro
+
+# API 请求基础路径(开发环境使用 / 走 Vite 代理,见 vite.config.ts 的 server.proxy)
+VITE_API_URL = /
+
+# 代理目标地址(后端 FastAPI 本地服务)
+VITE_API_PROXY_URL = http://127.0.0.1:8018
+
+# 开发期租户预览域名——/api/public 与 /static 的代理 Host 改写目标。
+# 后端 TenantHostMiddleware 只豁免 /health 与 /api/platform/ 前缀,
+# 其余路径按 Host 解析租户;经代理直连 127.0.0.1:8018 会因未知 Host 404,
+# 故这两条代理用 headers.Host 指向种子租户(jiancai)的域名。
+VITE_TENANT_PREVIEW_HOST = chat2.agent.hnxinyouhe.com
+
+# Delete console
+VITE_DROP_CONSOLE = false
diff --git a/admin/.env.production b/admin/.env.production
new file mode 100644
index 0000000..cdc67d4
--- /dev/null
+++ b/admin/.env.production
@@ -0,0 +1,30 @@
+# 【生产】环境变量
+#
+# 说明:模板原把通用变量放 .env(全模式加载),但根 .gitignore 的
+# `.env` 规则会误伤 admin/.env——入库文件只有 .env.development /
+# .env.production 两份(Phase 3 终审 I-2 收口)。通用变量因此按
+# "两份都写"纪律维护:改通用项时两文件同步改。
+
+# 版本号(vite.config.ts 的 __APP_VERSION__ define)
+VITE_VERSION = 0.1.0
+
+# 应用部署基础路径(如部署在子目录 /admin,则设置为 /admin/)
+VITE_BASE_URL = /
+
+# 权限模式【 frontend 前端模式 / backend 后端模式 】
+VITE_ACCESS_MODE = frontend
+
+# 跨域请求时是否携带 Cookie(开启前需确保后端支持)
+VITE_WITH_CREDENTIALS = false
+
+# 是否打开路由信息
+VITE_OPEN_ROUTE_INFO = false
+
+# 锁屏加密密钥(锁屏入口已摘除——Phase 3 终审 M-1;密钥保留防遗留代码异常)
+VITE_LOCK_ENCRYPT_KEY = s3cur3k3y4adpro
+
+# API 地址前缀(生产由反代把 /api 与 /static 指向后端,前端同源相对路径)
+VITE_API_URL = /
+
+# Delete console
+VITE_DROP_CONSOLE = true
diff --git a/admin/.gitattributes b/admin/.gitattributes
new file mode 100644
index 0000000..866e8ee
--- /dev/null
+++ b/admin/.gitattributes
@@ -0,0 +1,2 @@
+*.html linguist-detectable=false
+*.vue linguist-detectable=true
diff --git a/admin/.gitignore b/admin/.gitignore
new file mode 100644
index 0000000..08f9677
--- /dev/null
+++ b/admin/.gitignore
@@ -0,0 +1,12 @@
+node_modules
+.DS_Store
+dist
+dist-ssr
+*.local
+.cursorrules
+
+# Auto-generated files
+# 注:auto-imports.d.ts / components.d.ts 由 unplugin 生成但已入库,
+# 保证新克隆/CI 首跑 pnpm build 直接可用(vue-tsc 需要它们);
+# 本地重新构建会自动更新这两个文件。
+.auto-import.json
diff --git a/admin/.prettierignore b/admin/.prettierignore
new file mode 100644
index 0000000..9e96efc
--- /dev/null
+++ b/admin/.prettierignore
@@ -0,0 +1,3 @@
+/node_modules/*
+/dist/*
+/src/main.ts
\ No newline at end of file
diff --git a/admin/.prettierrc b/admin/.prettierrc
new file mode 100644
index 0000000..f3d6ad5
--- /dev/null
+++ b/admin/.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/admin/.stylelintignore b/admin/.stylelintignore
new file mode 100644
index 0000000..476ea45
--- /dev/null
+++ b/admin/.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/admin/.stylelintrc.cjs b/admin/.stylelintrc.cjs
new file mode 100644
index 0000000..9dbea0b
--- /dev/null
+++ b/admin/.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/admin/.vscode/extensions.json b/admin/.vscode/extensions.json
new file mode 100644
index 0000000..d7766fa
--- /dev/null
+++ b/admin/.vscode/extensions.json
@@ -0,0 +1,10 @@
+{
+ "recommendations": [
+ "vue.volar",
+ "dbaeumer.vscode-eslint",
+ "esbenp.prettier-vscode",
+ "stylelint.vscode-stylelint",
+ "lokalise.i18n-ally",
+ "bradlc.vscode-tailwindcss"
+ ]
+}
diff --git a/admin/.vscode/settings.json b/admin/.vscode/settings.json
new file mode 100644
index 0000000..5268fc2
--- /dev/null
+++ b/admin/.vscode/settings.json
@@ -0,0 +1,12 @@
+{
+ "volar.inlayHints.eventArgumentInInlineHandlers": true,
+ "css.lint.unknownAtRules": "ignore",
+ "i18n-ally.localesPaths": ["src/locales/langs", "src/locales"],
+ "i18n-ally.enabledParsers": ["json"],
+ "i18n-ally.sourceLanguage": "en",
+ "i18n-ally.displayLanguage": "zh",
+ "i18n-ally.enabledFrameworks": ["vue", "react"],
+ "i18n-ally.keystyle": "nested",
+ "i18n-ally.sortKeys": true,
+ "i18n-ally.namespace": true
+}
diff --git a/admin/LICENSE b/admin/LICENSE
new file mode 100644
index 0000000..68322de
--- /dev/null
+++ b/admin/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2025 SuperManTT
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/admin/README.md b/admin/README.md
new file mode 100644
index 0000000..8f58fc6
--- /dev/null
+++ b/admin/README.md
@@ -0,0 +1,3 @@
+# all-kefu 管理后台(admin SPA)
+
+Vue3 + TS + Vite + Element Plus(art-design-pro 模板)。本地起法与工作约定见根目录 [CLAUDE.md](../CLAUDE.md) 的「Phase 3 完成状态」段(后端 8018 + `pnpm install && pnpm dev`)。
diff --git a/admin/eslint.config.mjs b/admin/eslint.config.mjs
new file mode 100644
index 0000000..ed2d4ba
--- /dev/null
+++ b/admin/eslint.config.mjs
@@ -0,0 +1,83 @@
+// 从 URL 和路径模块中导入必要的功能
+import fs from 'fs'
+import path, { dirname } from 'path'
+import { fileURLToPath } from 'url'
+
+// 从 ESLint 插件中导入推荐配置
+import pluginJs from '@eslint/js'
+import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'
+import pluginVue from 'eslint-plugin-vue'
+import globals from 'globals'
+import tseslint from 'typescript-eslint'
+
+// 使用 import.meta.url 获取当前模块的路径
+const __filename = fileURLToPath(import.meta.url)
+const __dirname = dirname(__filename)
+
+// 读取 .auto-import.json 文件的内容,并将其解析为 JSON 对象
+const autoImportConfig = JSON.parse(
+ fs.readFileSync(path.resolve(__dirname, '.auto-import.json'), 'utf-8')
+)
+
+export default [
+ // 指定文件匹配规则
+ {
+ files: ['**/*.{js,mjs,cjs,ts,tsx,vue}']
+ },
+ // 指定全局变量和环境
+ {
+ languageOptions: {
+ globals: {
+ ...globals.browser,
+ ...globals.node
+ }
+ }
+ },
+ // 扩展配置
+ pluginJs.configs.recommended,
+ ...tseslint.configs.recommended,
+ ...pluginVue.configs['flat/essential'],
+ // 自定义规则
+ {
+ // 针对所有 JavaScript、TypeScript 和 Vue 文件应用以下配置
+ files: ['**/*.{js,mjs,cjs,ts,tsx,vue}'],
+
+ languageOptions: {
+ globals: {
+ // 合并从 autoImportConfig 中读取的全局变量配置
+ ...autoImportConfig.globals,
+ // TypeScript 全局命名空间
+ Api: 'readonly'
+ }
+ },
+ rules: {
+ quotes: ['error', 'single'], // 使用单引号
+ semi: ['error', 'never'], // 语句末尾不加分号
+ 'no-var': 'error', // 要求使用 let 或 const 而不是 var
+ '@typescript-eslint/no-explicit-any': 'off', // 禁用 any 检查
+ 'vue/multi-word-component-names': 'off', // 禁用对 Vue 组件名称的多词要求检查
+ 'no-multiple-empty-lines': ['warn', { max: 1 }], // 不允许多个空行
+ 'no-unexpected-multiline': 'error' // 禁止空余的多行
+ }
+ },
+ // vue 规则
+ {
+ files: ['**/*.vue'],
+ languageOptions: {
+ parserOptions: { parser: tseslint.parser }
+ }
+ },
+ // 忽略文件
+ {
+ ignores: [
+ 'node_modules',
+ 'dist',
+ 'public',
+ '.vscode/**',
+ 'src/assets/**',
+ 'src/utils/console.ts'
+ ]
+ },
+ // prettier 配置
+ eslintPluginPrettierRecommended
+]
diff --git a/admin/index.html b/admin/index.html
new file mode 100644
index 0000000..6b72611
--- /dev/null
+++ b/admin/index.html
@@ -0,0 +1,44 @@
+
+
+
+ all-kefu 管理后台
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/admin/package.json b/admin/package.json
new file mode 100644
index 0000000..a46c6c0
--- /dev/null
+++ b/admin/package.json
@@ -0,0 +1,87 @@
+{
+ "name": "all-kefu-admin",
+ "version": "0.1.0",
+ "private": true,
+ "type": "module",
+ "engines": {
+ "node": ">=20.19.0",
+ "pnpm": ">=8.8.0"
+ },
+ "pnpm": {
+ "onlyBuiltDependencies": [
+ "@parcel/watcher",
+ "@tailwindcss/oxide",
+ "core-js",
+ "esbuild",
+ "es5-ext",
+ "vue-demi"
+ ]
+ },
+ "scripts": {
+ "dev": "vite",
+ "build": "vue-tsc --noEmit && vite build",
+ "serve": "vite preview",
+ "test": "vitest run",
+ "lint": "eslint",
+ "fix": "eslint --fix",
+ "lint:prettier": "prettier --write \"**/*.{js,cjs,ts,json,tsx,vue,html,md}\"",
+ "lint:stylelint": "stylelint \"**/*.{css,scss,vue}\" --fix",
+ "clean:dev": "tsx scripts/clean-dev.ts"
+ },
+ "dependencies": {
+ "@element-plus/icons-vue": "^2.3.2",
+ "@iconify/vue": "^5.0.0",
+ "@tailwindcss/vite": "^4.1.14",
+ "@vue/reactivity": "^3.5.21",
+ "@vueuse/core": "^13.9.0",
+ "axios": "^1.12.2",
+ "crypto-js": "^4.2.0",
+ "echarts": "^6.0.0",
+ "element-plus": "^2.11.2",
+ "mitt": "^3.0.1",
+ "nprogress": "^0.2.0",
+ "ohash": "^2.0.11",
+ "pinia": "^3.0.3",
+ "pinia-plugin-persistedstate": "^4.3.0",
+ "tailwindcss": "^4.1.14",
+ "vue": "^3.5.21",
+ "vue-draggable-plus": "^0.6.0",
+ "vue-i18n": "^9.14.0",
+ "vue-router": "^4.5.1"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.9.1",
+ "@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",
+ "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",
+ "happy-dom": "^21.0.1",
+ "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": "^3.2.4",
+ "vue-tsc": "~2.1.6"
+ }
+}
diff --git a/admin/pnpm-lock.yaml b/admin/pnpm-lock.yaml
new file mode 100644
index 0000000..71e1870
--- /dev/null
+++ b/admin/pnpm-lock.yaml
@@ -0,0 +1,5855 @@
+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))
+ '@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))
+ axios:
+ specifier: ^1.12.2
+ version: 1.12.2
+ crypto-js:
+ specifier: ^4.2.0
+ version: 4.2.0
+ echarts:
+ specifier: ^6.0.0
+ version: 6.0.0
+ element-plus:
+ specifier: ^2.11.2
+ version: 2.11.4(vue@3.5.22(typescript@5.6.3))
+ 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)))
+ tailwindcss:
+ specifier: ^4.1.14
+ version: 4.1.14
+ 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))
+ devDependencies:
+ '@eslint/js':
+ specifier: ^9.9.1
+ version: 9.36.0
+ '@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
+ 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
+ happy-dom:
+ specifier: ^21.0.1
+ version: 21.0.1
+ 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: ^3.2.4
+ version: 3.2.7(@types/node@24.8.1)(happy-dom@21.0.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-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==}
+
+ '@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/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==}
+
+ '@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.25.10':
+ resolution: {integrity: sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [aix]
+
+ '@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.25.10':
+ resolution: {integrity: sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [android]
+
+ '@esbuild/android-x64@0.25.10':
+ resolution: {integrity: sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [android]
+
+ '@esbuild/darwin-arm64@0.25.10':
+ resolution: {integrity: sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@esbuild/darwin-x64@0.25.10':
+ resolution: {integrity: sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@esbuild/freebsd-arm64@0.25.10':
+ resolution: {integrity: sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ 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.25.10':
+ resolution: {integrity: sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ 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.25.10':
+ resolution: {integrity: sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ 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.25.10':
+ resolution: {integrity: sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==}
+ engines: {node: '>=18'}
+ cpu: [mips64el]
+ os: [linux]
+
+ '@esbuild/linux-ppc64@0.25.10':
+ resolution: {integrity: sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@esbuild/linux-riscv64@0.25.10':
+ resolution: {integrity: sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==}
+ engines: {node: '>=18'}
+ cpu: [riscv64]
+ 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.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.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.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.25.10':
+ resolution: {integrity: sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [sunos]
+
+ '@esbuild/win32-arm64@0.25.10':
+ resolution: {integrity: sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ 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.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/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'}
+
+ '@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'}
+
+ '@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
+
+ '@types/chai@5.2.3':
+ resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
+
+ '@types/deep-eql@4.0.2':
+ resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
+
+ '@types/estree@1.0.8':
+ resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
+
+ '@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/web-bluetooth@0.0.16':
+ resolution: {integrity: sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==}
+
+ '@types/web-bluetooth@0.0.21':
+ resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==}
+
+ '@types/whatwg-mimetype@3.0.2':
+ resolution: {integrity: sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==}
+
+ '@types/ws@8.18.2':
+ resolution: {integrity: sha512-67MQl+fpWKVTT1NYdnmo3U4sc/xPo/zQBncVnI74qmQa0z/b+1g6iYqNmGCPbxO+zz2aklb08a0oHfegiVd0/w==}
+
+ '@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}
+
+ '@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@3.2.7':
+ resolution: {integrity: sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==}
+
+ '@vitest/mocker@3.2.7':
+ resolution: {integrity: sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==}
+ peerDependencies:
+ msw: ^2.4.9
+ vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0
+ peerDependenciesMeta:
+ msw:
+ optional: true
+ vite:
+ optional: true
+
+ '@vitest/pretty-format@3.2.7':
+ resolution: {integrity: sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==}
+
+ '@vitest/runner@3.2.7':
+ resolution: {integrity: sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==}
+
+ '@vitest/snapshot@3.2.7':
+ resolution: {integrity: sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==}
+
+ '@vitest/spy@3.2.7':
+ resolution: {integrity: sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==}
+
+ '@vitest/utils@3.2.7':
+ resolution: {integrity: sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==}
+
+ '@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==}
+
+ '@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==}
+
+ 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
+
+ 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-regex@5.0.1:
+ resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
+ engines: {node: '>=8'}
+
+ ansi-styles@4.3.0:
+ resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
+ engines: {node: '>=8'}
+
+ 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-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==}
+
+ 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==}
+
+ baseline-browser-mapping@2.8.8:
+ resolution: {integrity: sha512-be0PUaPsQX/gPWWgFsdD+GFzaoig5PXaUC1xLkQiYdDnANU8sMnHoQd8JhbJQuvTWrWLyeFN9Imb5Qtfvr4RrQ==}
+ hasBin: true
+
+ binary-extensions@2.3.0:
+ resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
+ engines: {node: '>=8'}
+
+ birpc@2.6.1:
+ resolution: {integrity: sha512-LPnFhlDpdSH6FJhJyn4M0kFO7vtQ5iPw24FnG0y21q09xC7e8+1LeR31S1MAIrDAHp4m7aas4bEkTDTvMAtebQ==}
+
+ 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-image-size@0.6.4:
+ resolution: {integrity: sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ==}
+ engines: {node: '>=4.0'}
+
+ 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==}
+
+ 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==}
+
+ chai@5.3.3:
+ resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==}
+ engines: {node: '>=18'}
+
+ chalk@4.1.2:
+ resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
+ engines: {node: '>=10'}
+
+ check-error@2.1.3:
+ resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==}
+ engines: {node: '>= 16'}
+
+ 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'}
+
+ cliui@8.0.1:
+ resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
+ engines: {node: '>=12'}
+
+ color-convert@2.0.1:
+ resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
+ engines: {node: '>=7.0.0'}
+
+ color-name@1.1.4:
+ resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
+
+ colord@2.9.3:
+ resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==}
+
+ combined-stream@1.0.8:
+ resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
+ engines: {node: '>= 0.8'}
+
+ commander@2.20.3:
+ resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
+
+ 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==}
+
+ 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'}
+
+ cosmiconfig@9.0.0:
+ resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==}
+ engines: {node: '>=14'}
+ peerDependencies:
+ typescript: '>=4.9.5'
+ peerDependenciesMeta:
+ typescript:
+ optional: 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
+
+ csstype@3.1.3:
+ resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
+
+ 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
+
+ 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'}
+
+ 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'}
+
+ destr@2.0.5:
+ resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==}
+
+ 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==}
+
+ domelementtype@2.3.0:
+ resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
+
+ domhandler@5.0.3:
+ resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
+ engines: {node: '>= 4'}
+
+ domutils@3.2.2:
+ resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
+
+ dunder-proto@1.0.1:
+ resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
+ engines: {node: '>= 0.4'}
+
+ echarts@6.0.0:
+ resolution: {integrity: sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==}
+
+ electron-to-chromium@1.5.227:
+ resolution: {integrity: sha512-ITxuoPfJu3lsNWUi2lBM2PaBPYgH3uqmxut5vmBxgYvyI4AlJ6P3Cai1O76mOrkJCBzq0IxWg/NtqOrpu/0gKA==}
+
+ element-plus@2.11.4:
+ resolution: {integrity: sha512-sLq+Ypd0cIVilv8wGGMEGvzRVBBsRpJjnAS5PsI/1JU1COZXqzH3N1UYMUc/HCdvdjf6dfrBy80Sj7KcACsT7w==}
+ peerDependencies:
+ vue: ^3.2.0
+
+ emoji-regex@8.0.0:
+ resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
+
+ 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@7.0.1:
+ resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==}
+ engines: {node: '>=0.12'}
+
+ env-paths@2.2.1:
+ resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==}
+ engines: {node: '>=6'}
+
+ 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'}
+
+ 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@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
+
+ 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'}
+
+ execa@9.6.0:
+ resolution: {integrity: sha512-jpWzZ1ZhwUmeWRhS7Qv3mhpOhLfwI+uAX4e5fOcXqwMR7EcJ0pj2kV1CVzHVMX/LphnKWD3LObjZCoJ71lKpHw==}
+ engines: {node: ^18.19.0 || >=20.5.0}
+
+ expect-type@1.4.0:
+ resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==}
+ engines: {node: '>=12.0.0'}
+
+ exsolve@1.0.7:
+ resolution: {integrity: sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==}
+
+ 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@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'}
+
+ fill-range@7.1.1:
+ resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
+ engines: {node: '>=8'}
+
+ find-up@5.0.0:
+ resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
+ engines: {node: '>=10'}
+
+ 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
+
+ form-data@4.0.4:
+ resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==}
+ engines: {node: '>= 6'}
+
+ 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'}
+
+ 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-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@9.0.1:
+ resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==}
+ engines: {node: '>=18'}
+
+ get-tsconfig@4.10.1:
+ resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==}
+
+ 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'}
+
+ global-modules@2.0.0:
+ resolution: {integrity: sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==}
+ engines: {node: '>=6'}
+
+ 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==}
+
+ happy-dom@21.0.1:
+ resolution: {integrity: sha512-pRBs/u9fhrncyByOl58DMex0R2uAj6ZPKWIjV5j5yNGZBqLw3P6BTRhlc0DD5b/Ht4/6JYTMuE2LJhUWRC6SGw==}
+ engines: {node: '>=20.0.0'}
+
+ 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
+
+ hookable@5.5.3:
+ resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
+
+ hookified@1.12.1:
+ resolution: {integrity: sha512-xnKGl+iMIlhrZmGHB729MqlmPoWBznctSQTYCpFKqNsCgimJQmithcW0xSQMMFzYnV2iKUh25alswn6epgxS0Q==}
+
+ html-tags@3.3.1:
+ resolution: {integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==}
+ engines: {node: '>=8'}
+
+ htmlparser2@8.0.2:
+ resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==}
+
+ human-signals@8.0.1:
+ resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==}
+ engines: {node: '>=18.18.0'}
+
+ 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'}
+
+ immutable@5.1.3:
+ resolution: {integrity: sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg==}
+
+ import-fresh@3.3.1:
+ resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
+ engines: {node: '>=6'}
+
+ imurmurhash@0.1.4:
+ resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
+ engines: {node: '>=0.8.19'}
+
+ ini@1.3.8:
+ resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
+
+ 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-glob@4.0.3:
+ resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
+ engines: {node: '>=0.10.0'}
+
+ is-inside-container@1.0.0:
+ resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==}
+ engines: {node: '>=14.16'}
+ hasBin: true
+
+ is-number@7.0.0:
+ resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
+ engines: {node: '>=0.12.0'}
+
+ 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-stream@4.0.1:
+ resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==}
+ engines: {node: '>=18'}
+
+ is-unicode-supported@2.1.0:
+ resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==}
+ engines: {node: '>=18'}
+
+ is-what@4.1.16:
+ resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==}
+ engines: {node: '>=12.13'}
+
+ 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==}
+
+ jiti@2.6.0:
+ resolution: {integrity: sha512-VXe6RjJkBPj0ohtqaO8vSWP3ZhAKo66fKrFNCll4BTcwljPLz03pCbaNKfzGP5MbrCYcbJ7v0nOYYwUzTEIdXQ==}
+ hasBin: true
+
+ 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
+
+ jsesc@3.1.0:
+ resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
+ engines: {node: '>=6'}
+ hasBin: true
+
+ 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==}
+
+ 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'}
+
+ lines-and-columns@1.2.4:
+ resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
+
+ 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'}
+
+ 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.merge@4.6.2:
+ resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
+
+ lodash.truncate@4.4.2:
+ resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==}
+
+ lodash@4.17.21:
+ resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
+
+ loupe@3.2.1:
+ resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==}
+
+ 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@13.2.0:
+ resolution: {integrity: sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==}
+ engines: {node: '>=18'}
+
+ merge2@1.4.1:
+ resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
+ engines: {node: '>= 8'}
+
+ 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-types@2.1.35:
+ resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
+ engines: {node: '>= 0.6'}
+
+ minimatch@3.1.2:
+ resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
+
+ minimatch@9.0.5:
+ resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
+ engines: {node: '>=16 || 14 >=14.17'}
+
+ 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==}
+
+ 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==}
+
+ node-addon-api@7.1.1:
+ resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
+
+ node-releases@2.0.21:
+ resolution: {integrity: sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==}
+
+ 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@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==}
+
+ ohash@2.0.11:
+ resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==}
+
+ 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'}
+
+ p-limit@3.1.0:
+ resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
+ engines: {node: '>=10'}
+
+ p-locate@5.0.0:
+ resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
+ engines: {node: '>=10'}
+
+ 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'}
+
+ 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-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-type@4.0.0:
+ resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
+ engines: {node: '>=8'}
+
+ 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'}
+
+ 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}
+
+ 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'}
+
+ proxy-from-env@1.1.0:
+ resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
+
+ punycode@2.3.1:
+ resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
+ engines: {node: '>=6'}
+
+ quansync@0.2.11:
+ resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==}
+
+ queue-microtask@1.2.3:
+ resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
+
+ 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'}
+
+ 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==}
+
+ 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
+
+ run-applescript@7.1.0:
+ resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==}
+ engines: {node: '>=18'}
+
+ run-parallel@1.2.0:
+ resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
+
+ sass@1.93.2:
+ resolution: {integrity: sha512-t+YPtOQHpGW1QWsh1CHQ5cPIr9lbbGZLZnbihP/D/qZj/yuV68m8qarcV17nvkOX81BCrvzAlq2klCQFZghyTg==}
+ engines: {node: '>=14.0.0'}
+ hasBin: true
+
+ 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@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'}
+
+ slice-ansi@4.0.0:
+ resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==}
+ engines: {node: '>=10'}
+
+ 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'}
+
+ stackback@0.0.2:
+ resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
+
+ std-env@3.10.0:
+ resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==}
+
+ string-width@4.2.3:
+ resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
+ engines: {node: '>=8'}
+
+ strip-ansi@6.0.1:
+ resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
+ engines: {node: '>=8'}
+
+ 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@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==}
+
+ 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
+
+ tinybench@2.9.0:
+ resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
+
+ tinyexec@0.3.2:
+ resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==}
+
+ 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@2.0.0:
+ resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==}
+ engines: {node: '>=14.0.0'}
+
+ tinyspy@4.0.6:
+ resolution: {integrity: sha512-u8KszXvGfU68hVcZpRHKG28T0krMuv2G5nDhiHaMLen/gIuFEgIJhaJuO69qjnXg5paSrbPMFfx3brNuN8eVSg==}
+ engines: {node: '>=14.0.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'}
+
+ 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==}
+
+ 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'}
+
+ 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.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@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==}
+
+ 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@3.2.4:
+ resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==}
+ engines: {node: ^18.0.0 || ^20.0.0 || >=22.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@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@3.2.7:
+ resolution: {integrity: sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==}
+ engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
+ hasBin: true
+ peerDependencies:
+ '@edge-runtime/vm': '*'
+ '@types/debug': ^4.1.12
+ '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0
+ '@vitest/browser': 3.2.7
+ '@vitest/ui': 3.2.7
+ happy-dom: '*'
+ jsdom: '*'
+ peerDependenciesMeta:
+ '@edge-runtime/vm':
+ optional: true
+ '@types/debug':
+ 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-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-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
+
+ webpack-virtual-modules@0.6.2:
+ resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==}
+
+ whatwg-mimetype@3.0.0:
+ resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==}
+ engines: {node: '>=12'}
+
+ 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
+
+ word-wrap@1.2.5:
+ resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
+ engines: {node: '>=0.10.0'}
+
+ wrap-ansi@7.0.0:
+ resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
+ engines: {node: '>=10'}
+
+ write-file-atomic@5.0.1:
+ resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==}
+ engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
+
+ ws@8.22.0:
+ resolution: {integrity: sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==}
+ 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'}
+
+ xml-name-validator@4.0.0:
+ resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==}
+ engines: {node: '>=12'}
+
+ 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'}
+
+ 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': {}
+
+ '@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/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': {}
+
+ '@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.25.10':
+ optional: true
+
+ '@esbuild/android-arm64@0.25.10':
+ optional: true
+
+ '@esbuild/android-arm@0.25.10':
+ optional: true
+
+ '@esbuild/android-x64@0.25.10':
+ optional: true
+
+ '@esbuild/darwin-arm64@0.25.10':
+ optional: true
+
+ '@esbuild/darwin-x64@0.25.10':
+ optional: true
+
+ '@esbuild/freebsd-arm64@0.25.10':
+ optional: true
+
+ '@esbuild/freebsd-x64@0.25.10':
+ optional: true
+
+ '@esbuild/linux-arm64@0.25.10':
+ optional: true
+
+ '@esbuild/linux-arm@0.25.10':
+ optional: true
+
+ '@esbuild/linux-ia32@0.25.10':
+ optional: true
+
+ '@esbuild/linux-loong64@0.25.10':
+ optional: true
+
+ '@esbuild/linux-mips64el@0.25.10':
+ optional: true
+
+ '@esbuild/linux-ppc64@0.25.10':
+ optional: true
+
+ '@esbuild/linux-riscv64@0.25.10':
+ optional: true
+
+ '@esbuild/linux-s390x@0.25.10':
+ optional: true
+
+ '@esbuild/linux-x64@0.25.10':
+ optional: true
+
+ '@esbuild/netbsd-arm64@0.25.10':
+ optional: true
+
+ '@esbuild/netbsd-x64@0.25.10':
+ optional: true
+
+ '@esbuild/openbsd-arm64@0.25.10':
+ optional: true
+
+ '@esbuild/openbsd-x64@0.25.10':
+ optional: true
+
+ '@esbuild/openharmony-arm64@0.25.10':
+ optional: true
+
+ '@esbuild/sunos-x64@0.25.10':
+ optional: true
+
+ '@esbuild/win32-arm64@0.25.10':
+ optional: true
+
+ '@esbuild/win32-ia32@0.25.10':
+ 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/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
+
+ '@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
+
+ '@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)
+
+ '@types/chai@5.2.3':
+ dependencies:
+ '@types/deep-eql': 4.0.2
+ assertion-error: 2.0.1
+
+ '@types/deep-eql@4.0.2': {}
+
+ '@types/estree@1.0.8': {}
+
+ '@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/web-bluetooth@0.0.16': {}
+
+ '@types/web-bluetooth@0.0.21': {}
+
+ '@types/whatwg-mimetype@3.0.2': {}
+
+ '@types/ws@8.18.2':
+ dependencies:
+ '@types/node': 24.8.1
+
+ '@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
+
+ '@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@3.2.7':
+ dependencies:
+ '@types/chai': 5.2.3
+ '@vitest/spy': 3.2.7
+ '@vitest/utils': 3.2.7
+ chai: 5.3.3
+ tinyrainbow: 2.0.0
+
+ '@vitest/mocker@3.2.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))':
+ dependencies:
+ '@vitest/spy': 3.2.7
+ estree-walker: 3.0.3
+ magic-string: 0.30.19
+ optionalDependencies:
+ 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)
+
+ '@vitest/pretty-format@3.2.7':
+ dependencies:
+ tinyrainbow: 2.0.0
+
+ '@vitest/runner@3.2.7':
+ dependencies:
+ '@vitest/utils': 3.2.7
+ pathe: 2.0.3
+ strip-literal: 3.1.0
+
+ '@vitest/snapshot@3.2.7':
+ dependencies:
+ '@vitest/pretty-format': 3.2.7
+ magic-string: 0.30.19
+ pathe: 2.0.3
+
+ '@vitest/spy@3.2.7':
+ dependencies:
+ tinyspy: 4.0.6
+
+ '@vitest/utils@3.2.7':
+ dependencies:
+ '@vitest/pretty-format': 3.2.7
+ loupe: 3.2.1
+ tinyrainbow: 2.0.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': {}
+
+ '@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
+
+ acorn-jsx@5.3.2(acorn@8.15.0):
+ dependencies:
+ acorn: 8.15.0
+
+ acorn@8.15.0: {}
+
+ 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-regex@5.0.1: {}
+
+ ansi-styles@4.3.0:
+ dependencies:
+ color-convert: 2.0.1
+
+ anymatch@3.1.3:
+ dependencies:
+ normalize-path: 3.0.0
+ picomatch: 2.3.1
+
+ argparse@2.0.1: {}
+
+ array-union@2.1.0: {}
+
+ assertion-error@2.0.1: {}
+
+ astral-regex@2.0.0: {}
+
+ async-validator@4.2.5: {}
+
+ asynckit@0.4.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: {}
+
+ baseline-browser-mapping@2.8.8: {}
+
+ binary-extensions@2.3.0: {}
+
+ birpc@2.6.1: {}
+
+ 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-image-size@0.6.4:
+ dependencies:
+ '@types/node': 24.8.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
+
+ 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: {}
+
+ chai@5.3.3:
+ dependencies:
+ assertion-error: 2.0.1
+ check-error: 2.1.3
+ deep-eql: 5.0.2
+ loupe: 3.2.1
+ pathval: 2.0.1
+
+ chalk@4.1.2:
+ dependencies:
+ ansi-styles: 4.3.0
+ supports-color: 7.2.0
+
+ check-error@2.1.3: {}
+
+ 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: {}
+
+ cliui@8.0.1:
+ dependencies:
+ string-width: 4.2.3
+ strip-ansi: 6.0.1
+ wrap-ansi: 7.0.0
+
+ color-convert@2.0.1:
+ dependencies:
+ color-name: 1.1.4
+
+ color-name@1.1.4: {}
+
+ colord@2.9.3: {}
+
+ combined-stream@1.0.8:
+ dependencies:
+ delayed-stream: 1.0.0
+
+ commander@2.20.3: {}
+
+ concat-map@0.0.1: {}
+
+ confbox@0.1.8: {}
+
+ confbox@0.2.2: {}
+
+ convert-source-map@2.0.0: {}
+
+ copy-anything@3.0.5:
+ dependencies:
+ is-what: 4.1.16
+
+ 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
+
+ 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: {}
+
+ csstype@3.1.3: {}
+
+ dayjs@1.11.18: {}
+
+ de-indent@1.0.2: {}
+
+ debug@4.4.3:
+ dependencies:
+ ms: 2.1.3
+
+ 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
+
+ define-lazy-prop@2.0.0: {}
+
+ define-lazy-prop@3.0.0: {}
+
+ defu@6.1.4: {}
+
+ delayed-stream@1.0.0: {}
+
+ destr@2.0.5: {}
+
+ 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
+
+ domelementtype@2.3.0: {}
+
+ domhandler@5.0.3:
+ dependencies:
+ domelementtype: 2.3.0
+
+ domutils@3.2.2:
+ dependencies:
+ dom-serializer: 2.0.0
+ domelementtype: 2.3.0
+ domhandler: 5.0.3
+
+ dunder-proto@1.0.1:
+ dependencies:
+ call-bind-apply-helpers: 1.0.2
+ es-errors: 1.3.0
+ gopd: 1.2.0
+
+ echarts@6.0.0:
+ dependencies:
+ tslib: 2.3.0
+ zrender: 6.0.0
+
+ electron-to-chromium@1.5.227: {}
+
+ 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@8.0.0: {}
+
+ enhanced-resolve@5.18.3:
+ dependencies:
+ graceful-fs: 4.2.11
+ tapable: 2.3.0
+
+ entities@4.5.0: {}
+
+ entities@7.0.1: {}
+
+ env-paths@2.2.1: {}
+
+ 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
+
+ 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@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
+
+ 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: {}
+
+ 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
+
+ expect-type@1.4.0: {}
+
+ exsolve@1.0.7: {}
+
+ 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@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
+
+ fill-range@7.1.1:
+ dependencies:
+ to-regex-range: 5.0.1
+
+ find-up@5.0.0:
+ dependencies:
+ locate-path: 6.0.0
+ path-exists: 4.0.0
+
+ 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: {}
+
+ 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
+
+ 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
+
+ fsevents@2.3.3:
+ optional: true
+
+ function-bind@1.1.2: {}
+
+ gensync@1.0.0-beta.2: {}
+
+ get-caller-file@2.0.5: {}
+
+ 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@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
+
+ glob-parent@5.1.2:
+ dependencies:
+ is-glob: 4.0.3
+
+ glob-parent@6.0.2:
+ dependencies:
+ is-glob: 4.0.3
+
+ global-modules@2.0.0:
+ dependencies:
+ global-prefix: 3.0.0
+
+ 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: {}
+
+ happy-dom@21.0.1:
+ dependencies:
+ '@types/node': 24.8.1
+ '@types/whatwg-mimetype': 3.0.2
+ '@types/ws': 8.18.2
+ buffer-image-size: 0.6.4
+ entities: 7.0.1
+ whatwg-mimetype: 3.0.0
+ ws: 8.22.0
+ transitivePeerDependencies:
+ - bufferutil
+ - utf-8-validate
+
+ 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: {}
+
+ hookable@5.5.3: {}
+
+ hookified@1.12.1: {}
+
+ html-tags@3.3.1: {}
+
+ htmlparser2@8.0.2:
+ dependencies:
+ domelementtype: 2.3.0
+ domhandler: 5.0.3
+ domutils: 3.2.2
+ entities: 4.5.0
+
+ human-signals@8.0.1: {}
+
+ ignore@5.3.2: {}
+
+ ignore@7.0.5: {}
+
+ immutable@5.1.3: {}
+
+ import-fresh@3.3.1:
+ dependencies:
+ parent-module: 1.0.1
+ resolve-from: 4.0.0
+
+ imurmurhash@0.1.4: {}
+
+ ini@1.3.8: {}
+
+ 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-glob@4.0.3:
+ dependencies:
+ is-extglob: 2.1.1
+
+ is-inside-container@1.0.0:
+ dependencies:
+ is-docker: 3.0.0
+
+ is-number@7.0.0: {}
+
+ is-plain-obj@4.1.0: {}
+
+ is-plain-object@5.0.0: {}
+
+ is-stream@4.0.1: {}
+
+ is-unicode-supported@2.1.0: {}
+
+ is-what@4.1.16: {}
+
+ 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: {}
+
+ jiti@2.6.0: {}
+
+ js-tokens@4.0.0: {}
+
+ js-tokens@9.0.1: {}
+
+ js-yaml@4.1.0:
+ dependencies:
+ argparse: 2.0.1
+
+ jsesc@3.1.0: {}
+
+ 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
+
+ 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
+
+ lines-and-columns@1.2.4: {}
+
+ 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
+
+ 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.merge@4.6.2: {}
+
+ lodash.truncate@4.4.2: {}
+
+ lodash@4.17.21: {}
+
+ loupe@3.2.1: {}
+
+ 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@13.2.0: {}
+
+ merge2@1.4.1: {}
+
+ micromatch@4.0.8:
+ dependencies:
+ braces: 3.0.3
+ picomatch: 2.3.1
+
+ mime-db@1.52.0: {}
+
+ mime-types@2.1.35:
+ dependencies:
+ mime-db: 1.52.0
+
+ minimatch@3.1.2:
+ dependencies:
+ brace-expansion: 1.1.12
+
+ minimatch@9.0.5:
+ dependencies:
+ brace-expansion: 2.0.2
+
+ 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: {}
+
+ nanoid@3.3.11: {}
+
+ nanoid@5.1.6: {}
+
+ natural-compare@1.4.0: {}
+
+ node-addon-api@7.1.1:
+ optional: true
+
+ node-releases@2.0.21: {}
+
+ normalize-path@3.0.0: {}
+
+ normalize-wheel-es@1.2.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
+
+ ohash@2.0.11: {}
+
+ 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
+
+ p-limit@3.1.0:
+ dependencies:
+ yocto-queue: 0.1.0
+
+ p-locate@5.0.0:
+ dependencies:
+ p-limit: 3.1.0
+
+ 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: {}
+
+ path-browserify@1.0.1: {}
+
+ path-exists@4.0.0: {}
+
+ path-key@3.1.1: {}
+
+ path-key@4.0.0: {}
+
+ path-type@4.0.0: {}
+
+ 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: {}
+
+ 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
+
+ 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
+
+ proxy-from-env@1.1.0: {}
+
+ punycode@2.3.1: {}
+
+ quansync@0.2.11: {}
+
+ queue-microtask@1.2.3: {}
+
+ readdirp@3.6.0:
+ dependencies:
+ picomatch: 2.3.1
+
+ readdirp@4.1.2: {}
+
+ require-directory@2.1.1: {}
+
+ require-from-string@2.0.2: {}
+
+ resolve-from@4.0.0: {}
+
+ resolve-from@5.0.0: {}
+
+ resolve-pkg-maps@1.0.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
+
+ run-applescript@7.1.0: {}
+
+ run-parallel@1.2.0:
+ dependencies:
+ queue-microtask: 1.2.3
+
+ 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
+
+ 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@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: {}
+
+ slice-ansi@4.0.0:
+ dependencies:
+ ansi-styles: 4.3.0
+ astral-regex: 2.0.0
+ is-fullwidth-code-point: 3.0.0
+
+ 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: {}
+
+ stackback@0.0.2: {}
+
+ std-env@3.10.0: {}
+
+ string-width@4.2.3:
+ dependencies:
+ emoji-regex: 8.0.0
+ is-fullwidth-code-point: 3.0.0
+ strip-ansi: 6.0.1
+
+ strip-ansi@6.0.1:
+ dependencies:
+ ansi-regex: 5.0.1
+
+ 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@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: {}
+
+ 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
+
+ tinybench@2.9.0: {}
+
+ tinyexec@0.3.2: {}
+
+ tinyglobby@0.2.15:
+ dependencies:
+ fdir: 6.5.0(picomatch@4.0.3)
+ picomatch: 4.0.3
+
+ tinypool@1.1.1: {}
+
+ tinyrainbow@2.0.0: {}
+
+ tinyspy@4.0.6: {}
+
+ to-regex-range@5.0.1:
+ dependencies:
+ is-number: 7.0.0
+
+ totalist@3.0.1: {}
+
+ ts-api-utils@2.1.0(typescript@5.6.3):
+ dependencies:
+ typescript: 5.6.3
+
+ tslib@2.3.0: {}
+
+ 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: {}
+
+ 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.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@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
+
+ 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@3.2.4(@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:
+ cac: 6.7.14
+ debug: 4.4.3
+ es-module-lexer: 1.7.0
+ pathe: 2.0.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)
+ transitivePeerDependencies:
+ - '@types/node'
+ - jiti
+ - less
+ - lightningcss
+ - sass
+ - sass-embedded
+ - stylus
+ - sugarss
+ - supports-color
+ - terser
+ - tsx
+ - yaml
+
+ 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@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@3.2.7(@types/node@24.8.1)(happy-dom@21.0.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:
+ '@types/chai': 5.2.3
+ '@vitest/expect': 3.2.7
+ '@vitest/mocker': 3.2.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))
+ '@vitest/pretty-format': 3.2.7
+ '@vitest/runner': 3.2.7
+ '@vitest/snapshot': 3.2.7
+ '@vitest/spy': 3.2.7
+ '@vitest/utils': 3.2.7
+ chai: 5.3.3
+ debug: 4.4.3
+ expect-type: 1.4.0
+ magic-string: 0.30.19
+ pathe: 2.0.3
+ picomatch: 4.0.3
+ std-env: 3.10.0
+ tinybench: 2.9.0
+ tinyexec: 0.3.2
+ tinyglobby: 0.2.15
+ tinypool: 1.1.1
+ tinyrainbow: 2.0.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)
+ vite-node: 3.2.4(@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)
+ why-is-node-running: 2.3.0
+ optionalDependencies:
+ '@types/node': 24.8.1
+ happy-dom: 21.0.1
+ transitivePeerDependencies:
+ - jiti
+ - less
+ - lightningcss
+ - msw
+ - sass
+ - sass-embedded
+ - stylus
+ - sugarss
+ - supports-color
+ - terser
+ - tsx
+ - yaml
+
+ vscode-uri@3.1.0: {}
+
+ 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-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
+
+ webpack-virtual-modules@0.6.2: {}
+
+ whatwg-mimetype@3.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
+
+ word-wrap@1.2.5: {}
+
+ wrap-ansi@7.0.0:
+ dependencies:
+ ansi-styles: 4.3.0
+ string-width: 4.2.3
+ strip-ansi: 6.0.1
+
+ write-file-atomic@5.0.1:
+ dependencies:
+ imurmurhash: 0.1.4
+ signal-exit: 4.1.0
+
+ ws@8.22.0: {}
+
+ wsl-utils@0.1.0:
+ dependencies:
+ is-wsl: 3.1.0
+
+ xml-name-validator@4.0.0: {}
+
+ y18n@5.0.8: {}
+
+ yallist@3.1.1: {}
+
+ yallist@5.0.0: {}
+
+ yaml@2.8.1:
+ optional: true
+
+ 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: {}
+
+ yoctocolors@2.1.2: {}
+
+ zrender@6.0.0:
+ dependencies:
+ tslib: 2.3.0
diff --git a/admin/pnpm-workspace.yaml b/admin/pnpm-workspace.yaml
new file mode 100644
index 0000000..d77c0f0
--- /dev/null
+++ b/admin/pnpm-workspace.yaml
@@ -0,0 +1,7 @@
+onlyBuiltDependencies:
+ - "@parcel/watcher"
+ - "@tailwindcss/oxide"
+ - core-js
+ - esbuild
+ - es5-ext
+ - vue-demi
diff --git a/admin/public/favicon.png b/admin/public/favicon.png
new file mode 100644
index 0000000..11cba74
Binary files /dev/null and b/admin/public/favicon.png differ
diff --git a/admin/scripts/clean-dev.ts b/admin/scripts/clean-dev.ts
new file mode 100644
index 0000000..cc0b9bc
--- /dev/null
+++ b/admin/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/admin/src/App.vue b/admin/src/App.vue
new file mode 100644
index 0000000..23745c9
--- /dev/null
+++ b/admin/src/App.vue
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
diff --git a/admin/src/api/__tests__/auth-store.test.ts b/admin/src/api/__tests__/auth-store.test.ts
new file mode 100644
index 0000000..8bb2a38
--- /dev/null
+++ b/admin/src/api/__tests__/auth-store.test.ts
@@ -0,0 +1,211 @@
+/**
+ * auth store 单元测试(Phase 3 Task 3)
+ *
+ * 覆盖:
+ * - login 成功存 token/role/tenant_slug/must_change_password/username(含模板侧同步)
+ * - logout:调 API + 清空状态 + 模板侧 logOut(API 失败也照常本地清)
+ * - localStorage 持久化(all-kefu 前缀键;新建 pinia 实例模拟刷新恢复)
+ * - handleSessionExpired / handleMustChangePassword / changePassword
+ */
+import { beforeEach, describe, expect, it, vi } from 'vitest'
+import { createPinia, setActivePinia } from 'pinia'
+import { createApp, defineComponent } from 'vue'
+import { createPersistedState } from 'pinia-plugin-persistedstate'
+
+const mocks = vi.hoisted(() => ({
+ fetchLogin: vi.fn(),
+ fetchLogout: vi.fn(),
+ fetchChangePassword: vi.fn(),
+ userStore: {
+ logOut: vi.fn(),
+ setLoginStatus: vi.fn(),
+ setToken: vi.fn(),
+ setUserInfo: vi.fn(),
+ checkAndClearWorktabs: vi.fn(),
+ info: {}
+ },
+ routerPush: vi.fn(),
+ routerReplace: vi.fn(),
+ currentRoute: { value: { path: '/' } }
+}))
+
+vi.mock('@/api/auth', () => ({
+ fetchLogin: mocks.fetchLogin,
+ fetchLogout: mocks.fetchLogout,
+ fetchChangePassword: mocks.fetchChangePassword
+}))
+
+vi.mock('@/store/modules/user', () => ({
+ useUserStore: () => mocks.userStore
+}))
+
+vi.mock('@/router', () => ({
+ router: { push: mocks.routerPush, replace: mocks.routerReplace, currentRoute: mocks.currentRoute }
+}))
+
+import { useAuthStore } from '@/store/modules/auth'
+
+const LOGIN_RESPONSE = {
+ token: 'tok-abc',
+ role: 'super',
+ tenant_slug: null,
+ must_change_password: true
+} as const
+
+/**
+ * 等价应用的持久化 pinia:
+ * pinia.use() 在未安装 app 时(_a 为空)插件只进 toBeInstalled 队列,
+ * 必须 app.use(pinia) 才真正生效——真实应用经 initStore(main.ts)如此;
+ * 测试里用一次性 app 复刻同一装配,否则持久化插件静默不运行。
+ */
+function freshPersistedPinia() {
+ const pinia = createPinia()
+ pinia.use(createPersistedState())
+ createApp(defineComponent(() => () => null)).use(pinia)
+ setActivePinia(pinia)
+ return pinia
+}
+
+describe('auth store(src/store/modules/auth.ts)', () => {
+ beforeEach(() => {
+ localStorage.clear()
+ vi.clearAllMocks()
+ mocks.currentRoute.value.path = '/'
+ })
+
+ it('login 成功:存 token/role/tenantSlug/mustChangePassword/username 并同步模板侧', async () => {
+ freshPersistedPinia()
+ mocks.fetchLogin.mockResolvedValue({ ...LOGIN_RESPONSE })
+
+ const store = useAuthStore()
+ await store.login({ username: 'admin', password: 'admin123' })
+
+ expect(store.token).toBe('tok-abc')
+ expect(store.role).toBe('super')
+ expect(store.tenantSlug).toBeNull()
+ expect(store.mustChangePassword).toBe(true)
+ expect(store.username).toBe('admin')
+ expect(store.isAuthenticated).toBe(true)
+
+ // 模板侧(user store)同步:token / 登录标记 / 用户信息(菜单过滤用 roles)
+ expect(mocks.userStore.setToken).toHaveBeenCalledWith('tok-abc', '')
+ expect(mocks.userStore.setLoginStatus).toHaveBeenCalledWith(true)
+ expect(mocks.userStore.setUserInfo).toHaveBeenCalled()
+ const info = mocks.userStore.setUserInfo.mock.calls[0][0]
+ expect(info.roles).toEqual(['super'])
+ expect(info.userName).toBe('admin')
+ })
+
+ it('login 失败:状态保持空、错误向上抛', async () => {
+ freshPersistedPinia()
+ mocks.fetchLogin.mockRejectedValue(new Error('用户名或密码错误'))
+
+ const store = useAuthStore()
+ await expect(store.login({ username: 'a', password: 'b' })).rejects.toThrow('用户名或密码错误')
+
+ expect(store.token).toBe('')
+ expect(store.isAuthenticated).toBe(false)
+ expect(mocks.userStore.setLoginStatus).not.toHaveBeenCalled()
+ })
+
+ it('logout:调登出 API + 清空状态 + 模板侧 logOut', async () => {
+ freshPersistedPinia()
+ mocks.fetchLogin.mockResolvedValue({ ...LOGIN_RESPONSE })
+ mocks.fetchLogout.mockResolvedValue({ ok: true })
+
+ const store = useAuthStore()
+ await store.login({ username: 'admin', password: 'x' })
+
+ await store.logout()
+
+ expect(mocks.fetchLogout).toHaveBeenCalledTimes(1)
+ expect(store.token).toBe('')
+ expect(store.role).toBe('')
+ expect(store.tenantSlug).toBeNull()
+ expect(store.username).toBe('')
+ expect(store.mustChangePassword).toBe(false)
+ expect(store.isAuthenticated).toBe(false)
+ expect(mocks.userStore.logOut).toHaveBeenCalledTimes(1)
+ })
+
+ it('logout 时后端已不可达:仍完成本地清理(best effort)', async () => {
+ freshPersistedPinia()
+ mocks.fetchLogin.mockResolvedValue({ ...LOGIN_RESPONSE })
+ mocks.fetchLogout.mockRejectedValue(new Error('network down'))
+
+ const store = useAuthStore()
+ await store.login({ username: 'admin', password: 'x' })
+
+ await expect(store.logout()).resolves.toBeUndefined()
+
+ expect(store.token).toBe('')
+ expect(mocks.userStore.logOut).toHaveBeenCalledTimes(1)
+ })
+
+ it('localStorage 持久化:all-kefu 前缀键;新 pinia 实例(模拟刷新)恢复会话', async () => {
+ freshPersistedPinia()
+ mocks.fetchLogin.mockResolvedValue({ ...LOGIN_RESPONSE, role: 'tenant', tenant_slug: 'jiancai', must_change_password: false })
+
+ await useAuthStore().login({ username: 'jcadmin', password: 'x' })
+
+ // 持久化键带 all-kefu 前缀
+ expect(localStorage.getItem('all-kefu-auth')).toBeTruthy()
+
+ // 模拟页面刷新:全新 pinia + 插件,从 localStorage 恢复
+ freshPersistedPinia()
+ const restored = useAuthStore()
+ expect(restored.token).toBe('tok-abc')
+ expect(restored.role).toBe('tenant')
+ expect(restored.tenantSlug).toBe('jiancai')
+ expect(restored.username).toBe('jcadmin')
+ expect(restored.mustChangePassword).toBe(false)
+ expect(restored.isAuthenticated).toBe(true)
+ })
+
+ it('handleSessionExpired:清空会话并委托模板侧 logOut(401 全局处理路径)', () => {
+ freshPersistedPinia()
+ const store = useAuthStore()
+ store.token = 'tok'
+ store.role = 'super'
+ store.username = 'admin'
+
+ store.handleSessionExpired()
+
+ expect(store.token).toBe('')
+ expect(store.isAuthenticated).toBe(false)
+ expect(mocks.userStore.logOut).toHaveBeenCalledTimes(1)
+ })
+
+ it('handleMustChangePassword:置标志并跳强制改密页;已在改密页不重复跳', async () => {
+ freshPersistedPinia()
+ const store = useAuthStore()
+
+ await store.handleMustChangePassword()
+
+ expect(store.mustChangePassword).toBe(true)
+ await vi.waitFor(() => {
+ expect(mocks.routerReplace).toHaveBeenCalledWith('/auth/force-change-password')
+ })
+
+ // 已在改密页:不再跳
+ mocks.routerReplace.mockClear()
+ mocks.currentRoute.value.path = '/auth/force-change-password'
+ await store.handleMustChangePassword()
+ expect(mocks.routerReplace).not.toHaveBeenCalled()
+ })
+
+ it('changePassword 成功:清 must_change 标志', async () => {
+ freshPersistedPinia()
+ mocks.fetchChangePassword.mockResolvedValue({ ok: true, must_change_password: false })
+ const store = useAuthStore()
+ store.mustChangePassword = true
+
+ await store.changePassword({ old_password: 'old', new_password: 'new-pass-123' })
+
+ expect(mocks.fetchChangePassword).toHaveBeenCalledWith({
+ old_password: 'old',
+ new_password: 'new-pass-123'
+ })
+ expect(store.mustChangePassword).toBe(false)
+ })
+})
diff --git a/admin/src/api/__tests__/client.test.ts b/admin/src/api/__tests__/client.test.ts
new file mode 100644
index 0000000..262c80e
--- /dev/null
+++ b/admin/src/api/__tests__/client.test.ts
@@ -0,0 +1,238 @@
+/**
+ * HTTP client 单元测试(Phase 3 Task 3)
+ *
+ * 契约对齐后端 app/platform/auth.py 与 auth_routes.py:
+ * - Authorization: Bearer 注入
+ * - 401(非登录请求)→ 清会话 + 跳登录(登录请求自身的 401 = 凭据错误,只提示不跳转)
+ * - 409 detail === "必须先修改初始密码" → 跳强制改密页;
+ * 其余 409(并发冲突等)为普通业务错误,不跳转
+ * - 其余错误 / 网络错误 → ElMessage 出 detail / 统一文案
+ * - 成功透传 response.data
+ */
+import { beforeEach, describe, expect, it, vi } from 'vitest'
+import { AxiosError, type AxiosResponse, type InternalAxiosRequestConfig } from 'axios'
+import { createPinia, setActivePinia } from 'pinia'
+
+const mocks = vi.hoisted(() => ({
+ userStore: {
+ logOut: vi.fn(),
+ setLoginStatus: vi.fn(),
+ setToken: vi.fn(),
+ setUserInfo: vi.fn(),
+ checkAndClearWorktabs: vi.fn(),
+ info: {}
+ },
+ routerPush: vi.fn(),
+ routerReplace: vi.fn(),
+ currentRoute: { value: { path: '/' } },
+ elMessageError: vi.fn()
+}))
+
+vi.mock('element-plus', () => ({
+ ElMessage: { error: mocks.elMessageError, success: vi.fn() }
+}))
+
+vi.mock('@/store/modules/user', () => ({
+ useUserStore: () => mocks.userStore
+}))
+
+vi.mock('@/router', () => ({
+ router: { push: mocks.routerPush, replace: mocks.routerReplace, currentRoute: mocks.currentRoute }
+}))
+
+import { ElMessage } from 'element-plus'
+import { http } from '@/api/client'
+import { useAuthStore } from '@/store/modules/auth'
+
+/** 自定义 axios adapter:按计划响应回放,并捕获请求头 */
+function makeAdapter(plan: { status: number; body: unknown } | { networkError: true }) {
+ let captured: InternalAxiosRequestConfig | null = null
+ const adapter = (config: InternalAxiosRequestConfig): Promise => {
+ captured = config
+ if ('networkError' in plan) {
+ // 模拟断网:无 response 的 AxiosError
+ return Promise.reject(
+ new AxiosError('Network Error', 'ECONNABORTED', config, undefined, undefined)
+ )
+ }
+ const response: AxiosResponse = {
+ status: plan.status,
+ statusText: '',
+ headers: {},
+ config,
+ data: plan.body
+ }
+ if (plan.status >= 200 && plan.status < 300) return Promise.resolve(response)
+ return Promise.reject(
+ new AxiosError(
+ `Request failed with status code ${plan.status}`,
+ String(plan.status),
+ config,
+ undefined,
+ response
+ )
+ )
+ }
+ return {
+ adapter,
+ capturedHeaders: () => (captured as InternalAxiosRequestConfig | null)?.headers
+ }
+}
+
+describe('HTTP client(src/api/client.ts)', () => {
+ beforeEach(() => {
+ setActivePinia(createPinia())
+ localStorage.clear()
+ vi.clearAllMocks()
+ mocks.currentRoute.value.path = '/'
+ })
+
+ describe('token 注入', () => {
+ it('有 token 时自动注入 Authorization: Bearer ', async () => {
+ const authStore = useAuthStore()
+ authStore.token = 'tok-123'
+
+ const { adapter, capturedHeaders } = makeAdapter({ status: 200, body: { ok: true } })
+ await http.post<{ ok: boolean }>('/api/platform/auth/logout', null, { adapter })
+
+ expect(capturedHeaders()?.get('Authorization')).toBe('Bearer tok-123')
+ })
+
+ it('无 token 时不注入 Authorization 头', async () => {
+ const { adapter, capturedHeaders } = makeAdapter({ status: 200, body: { ok: true } })
+ await http.post('/api/platform/auth/logout', null, { adapter })
+
+ expect(capturedHeaders()?.get('Authorization')).toBeFalsy()
+ })
+ })
+
+ describe('401 处理', () => {
+ it('非登录请求 401:清空会话并触发登出清理(跳登录)', async () => {
+ const authStore = useAuthStore()
+ authStore.token = 'expired-token'
+
+ const { adapter } = makeAdapter({
+ status: 401,
+ body: { detail: '未认证或凭证已失效' }
+ })
+ await expect(http.get('/api/platform/tenants', { adapter })).rejects.toThrow(
+ '未认证或凭证已失效'
+ )
+
+ expect(authStore.token).toBe('')
+ expect(authStore.isAuthenticated).toBe(false)
+ // 模板侧全套清理(含跳登录页)由 userStore.logOut 承担
+ expect(mocks.userStore.logOut).toHaveBeenCalled()
+ expect(ElMessage.error).toHaveBeenCalledWith('未认证或凭证已失效')
+ })
+
+ it('登录请求自身 401(凭据错误):只提示 detail,不触发登出/跳转', async () => {
+ const { adapter } = makeAdapter({
+ status: 401,
+ body: { detail: '用户名或密码错误' }
+ })
+ await expect(
+ http.post('/api/platform/auth/login', { username: 'a', password: 'b' }, { adapter })
+ ).rejects.toThrow('用户名或密码错误')
+
+ expect(mocks.userStore.logOut).not.toHaveBeenCalled()
+ expect(mocks.routerReplace).not.toHaveBeenCalledWith('/auth/force-change-password')
+ expect(ElMessage.error).toHaveBeenCalledWith('用户名或密码错误')
+ })
+ })
+
+ describe('409 处理', () => {
+ it('409 detail 为必须先修改初始密码:置 must_change 并跳强制改密页', async () => {
+ const authStore = useAuthStore()
+ authStore.token = 'tok'
+ authStore.mustChangePassword = false
+
+ const { adapter } = makeAdapter({
+ status: 409,
+ body: { detail: '必须先修改初始密码' }
+ })
+ await expect(http.get('/api/platform/stats', { adapter })).rejects.toThrow(
+ '必须先修改初始密码'
+ )
+
+ await vi.waitFor(() => {
+ expect(mocks.routerReplace).toHaveBeenCalledWith('/auth/force-change-password')
+ })
+ expect(authStore.mustChangePassword).toBe(true)
+ })
+
+ it('已在强制改密页时收到 409:不再重复跳转', async () => {
+ mocks.currentRoute.value.path = '/auth/force-change-password'
+ const { adapter } = makeAdapter({
+ status: 409,
+ body: { detail: '必须先修改初始密码' }
+ })
+ await expect(http.get('/api/platform/stats', { adapter })).rejects.toThrow()
+
+ await vi.waitFor(() => {
+ expect(ElMessage.error).toHaveBeenCalled()
+ })
+ expect(mocks.routerReplace).not.toHaveBeenCalled()
+ })
+
+ it('其他 409(并发冲突):普通业务错误,不跳改密页', async () => {
+ const { adapter } = makeAdapter({
+ status: 409,
+ body: { detail: '并发冲突: 域名已被占用' }
+ })
+ await expect(http.post('/api/platform/tenants', {}, { adapter })).rejects.toThrow(
+ '并发冲突: 域名已被占用'
+ )
+
+ expect(mocks.routerReplace).not.toHaveBeenCalled()
+ expect(ElMessage.error).toHaveBeenCalledWith('并发冲突: 域名已被占用')
+ })
+ })
+
+ describe('其他错误与成功', () => {
+ it('登录 429 限流:弹出后端 detail 提示,不触发跳转', async () => {
+ const { adapter } = makeAdapter({
+ status: 429,
+ body: { detail: '登录尝试过于频繁,请15分钟后再试' }
+ })
+ await expect(
+ http.post('/api/platform/auth/login', { username: 'a', password: 'b' }, { adapter })
+ ).rejects.toThrow('登录尝试过于频繁,请15分钟后再试')
+
+ expect(ElMessage.error).toHaveBeenCalledWith('登录尝试过于频繁,请15分钟后再试')
+ expect(mocks.userStore.logOut).not.toHaveBeenCalled()
+ expect(mocks.routerReplace).not.toHaveBeenCalled()
+ })
+
+ it('网络错误(无响应):统一文案提示', async () => {
+ const { adapter } = makeAdapter({ networkError: true })
+ await expect(http.get('/api/platform/stats', { adapter })).rejects.toThrow()
+
+ expect(ElMessage.error).toHaveBeenCalledTimes(1)
+ const message = vi.mocked(ElMessage.error).mock.calls[0][0] as string
+ expect(message).toContain('网络')
+ })
+
+ it('showErrorMessage: false 时不弹全局错误提示', async () => {
+ const { adapter } = makeAdapter({ status: 400, body: { detail: '原密码不正确' } })
+ await expect(
+ http.post('/api/platform/auth/change-password', {}, { adapter, showErrorMessage: false })
+ ).rejects.toThrow('原密码不正确')
+
+ expect(ElMessage.error).not.toHaveBeenCalled()
+ })
+
+ it('成功:直接透传 response.data(非 axios 包装)', async () => {
+ const body = {
+ token: 't',
+ role: 'super',
+ tenant_slug: null,
+ must_change_password: true
+ }
+ const { adapter } = makeAdapter({ status: 200, body })
+ const res = await http.post('/api/platform/auth/login', {}, { adapter })
+
+ expect(res).toEqual(body)
+ })
+ })
+})
diff --git a/admin/src/api/__tests__/config.test.ts b/admin/src/api/__tests__/config.test.ts
new file mode 100644
index 0000000..50f05a4
--- /dev/null
+++ b/admin/src/api/__tests__/config.test.ts
@@ -0,0 +1,439 @@
+/**
+ * 配置中心 API 单元测试(Phase 3 Task 7a——品牌相关;Task 7b——试金石;
+ * Task 7c——凭据测试端点;Task 7d——版本历史/回滚/组级重置)
+ *
+ * 钉住 API 层契约(事实源 app/platform/routes/config_routes.py 与
+ * upload_routes.py);HTTP 层行为已在 client.test.ts 覆盖,此处 mock http
+ * 只断言调用形态:
+ * - GET config:快照 + version(原样组——缺键防御读取靠 normalizeBrandGroup)
+ * - PUT config/brand:请求体是**组值本身**(整组替换语义,非 {note,value}
+ * 包装),备注走 comment 查询参数,无备注时不带 params
+ * - POST wall/test(Task 7b):句子列表 + wall 覆盖(当前表单值预演,
+ * 不落库)+ has_contact/source 可选项——保存前自动验证复用本函数
+ * - POST email/test · push/test(Task 7c):前者 body 只带 {to}(用已
+ * 保存配置真发一封);后者无请求体(探测读已保存配置,空批量零线索
+ * 数据)——两者 showErrorMessage: false 内联展示
+ * - GET versions / versions/{n}(Task 7d):列表不含 config 本体;详情含
+ * 整份快照(密钥 '__SET__' 掩码视图)
+ * - POST rollback(Task 7d):body 只带 {version}——目标版本复制为新版本
+ * (历史不可变),响应含新版本号 + 新快照
+ * - POST config/{group}/reset(Task 7d):无请求体(后端 reset 不读
+ * body),响应与 PUT 分组保存同形
+ * - 头像上传:multipart file 字段(FormData)
+ * - normalizeBrandGroup:缺键补空串 / 类型漂移防御 / topics 只留字符串项
+ */
+import { beforeEach, describe, expect, it, vi } from 'vitest'
+
+vi.mock('@/api/client', () => ({
+ http: { get: vi.fn(), post: vi.fn(), put: vi.fn() }
+}))
+
+import { http } from '@/api/client'
+import {
+ CONFIG_ENDPOINTS,
+ fetchConfigVersionDetail,
+ fetchConfigVersions,
+ fetchTenantConfig,
+ normalizeBrandGroup,
+ putConfigGroup,
+ resetConfigGroup,
+ rollbackConfig,
+ testEmail,
+ testPush,
+ testWall,
+ uploadBrandAvatar
+} from '@/api/config'
+
+describe('配置中心 API(src/api/config.ts)', () => {
+ beforeEach(() => {
+ vi.clearAllMocks()
+ })
+
+ it('fetchTenantConfig:GET /api/platform/tenants/{slug}/config', async () => {
+ const body = {
+ slug: 'jiancai',
+ version: 3,
+ config: {
+ brand: { title: 'HESUPIPE Support', color: '#066fc0', topics: ['Get Quotation'] }
+ }
+ }
+ vi.mocked(http.get).mockResolvedValue(body)
+
+ const res = await fetchTenantConfig('jiancai')
+
+ expect(res).toEqual(body)
+ expect(http.get).toHaveBeenCalledWith('/api/platform/tenants/jiancai/config', undefined)
+ })
+
+ it('putConfigGroup:PUT 请求体为组值本身 + comment 查询参数(整组替换契约)', async () => {
+ const brand = {
+ title: 'HESUPIPE Support',
+ color: '#ff6600',
+ avatar: 'abc123.png',
+ welcome: 'Hello',
+ placeholder: 'Ask me',
+ whatsapp: '+8615937108317',
+ email: 'hesu@hesuu.com',
+ privacy_url: 'https://hesupipe.com/privacy-policy',
+ topics: ['Get Quotation', 'Delivery & Shipping']
+ }
+ const response = { slug: 'jiancai', group: 'brand', version: 4, config: brand }
+ vi.mocked(http.put).mockResolvedValue(response)
+
+ const res = await putConfigGroup('jiancai', 'brand', { ...brand }, '改主题色')
+
+ expect(res).toEqual(response)
+ expect(http.put).toHaveBeenCalledWith(
+ '/api/platform/tenants/jiancai/config/brand',
+ brand, // 组值直传——绝不能包成 { note, value }(后端 _json_body 整组校验)
+ { params: { comment: '改主题色' } }
+ )
+ })
+
+ it('putConfigGroup:无备注时不带 params(comment 是可选项)', async () => {
+ vi.mocked(http.put).mockResolvedValue({
+ slug: 'jiancai',
+ group: 'brand',
+ version: 5,
+ config: {}
+ })
+
+ await putConfigGroup('jiancai', 'brand', { title: 'T' })
+
+ expect(http.put).toHaveBeenCalledWith(
+ '/api/platform/tenants/jiancai/config/brand',
+ { title: 'T' },
+ { params: undefined }
+ )
+ })
+
+ it('uploadBrandAvatar:POST multipart(FormData file 字段)', async () => {
+ const response = { slug: 'jiancai', avatar: '7f3c9d.png', version: 6 }
+ vi.mocked(http.post).mockResolvedValue(response)
+ const file = new File([new Uint8Array([0x89, 0x50, 0x4e, 0x47])], 'logo.png', {
+ type: 'image/png'
+ })
+
+ const res = await uploadBrandAvatar('jiancai', file, { showErrorMessage: false })
+
+ expect(res).toEqual(response)
+ expect(http.post).toHaveBeenCalledTimes(1)
+ const [url, body, config] = vi.mocked(http.post).mock.calls[0]
+ expect(url).toBe('/api/platform/tenants/jiancai/brand/avatar')
+ expect(body).toBeInstanceOf(FormData)
+ expect((body as FormData).get('file')).toBe(file)
+ expect(config).toEqual({ showErrorMessage: false })
+ })
+
+ it('CONFIG_ENDPOINTS.avatarUpload:端点路径与后端 upload_routes 一致', () => {
+ expect(CONFIG_ENDPOINTS.avatarUpload('jiancai')).toBe(
+ '/api/platform/tenants/jiancai/brand/avatar'
+ )
+ expect(CONFIG_ENDPOINTS.putGroup('jiancai', 'brand')).toBe(
+ '/api/platform/tenants/jiancai/config/brand'
+ )
+ })
+})
+
+describe('试金石 API(testWall · Phase 3 Task 7b)', () => {
+ beforeEach(() => {
+ vi.clearAllMocks()
+ })
+
+ it('POST wall/test:句子 + wall 覆盖(当前表单值预演)+ 判定选项', async () => {
+ const response = {
+ slug: 'jiancai',
+ wall_source: 'override',
+ results: [
+ {
+ sentence: 'The price is 500 USD per ton.',
+ action: 'blocked',
+ reason: 'boundary',
+ replacement: 'Prices vary a lot by config...'
+ },
+ { sentence: 'PPR pipes in stock?', action: 'pass', reason: null, replacement: null }
+ ]
+ }
+ vi.mocked(http.post).mockResolvedValue(response)
+ const wallOverride = {
+ price_patterns: ['\\$\\s?\\d'],
+ deflections: { en: ['...'], zh: ['...'] }
+ }
+
+ const res = await testWall(
+ 'jiancai',
+ {
+ sentences: ['The price is 500 USD per ton.', 'PPR pipes in stock?'],
+ wall: wallOverride,
+ has_contact: true,
+ source: 'app'
+ },
+ { showErrorMessage: false }
+ )
+
+ expect(res).toEqual(response)
+ expect(http.post).toHaveBeenCalledTimes(1)
+ expect(http.post).toHaveBeenCalledWith(
+ '/api/platform/tenants/jiancai/wall/test',
+ {
+ sentences: ['The price is 500 USD per ton.', 'PPR pipes in stock?'],
+ wall: wallOverride, // 覆盖墙原样直传——后端 compile_wall 校验后纯内存判定
+ has_contact: true,
+ source: 'app'
+ },
+ { showErrorMessage: false } // 坏墙 400 字段路径由调用方内联展示
+ )
+ })
+
+ it('POST wall/test:最小 body(仅句子——缺省用当前生效墙)', async () => {
+ const response = {
+ slug: 'jiancai',
+ wall_source: 'current',
+ results: [{ sentence: 'Hello', action: 'pass', reason: null, replacement: null }]
+ }
+ vi.mocked(http.post).mockResolvedValue(response)
+
+ const res = await testWall('jiancai', { sentences: ['Hello'] })
+
+ expect(res).toEqual(response)
+ expect(http.post).toHaveBeenCalledWith(
+ '/api/platform/tenants/jiancai/wall/test',
+ { sentences: ['Hello'] },
+ undefined
+ )
+ })
+
+ it('CONFIG_ENDPOINTS.wallTest:端点路径与后端 config_routes 一致', () => {
+ expect(CONFIG_ENDPOINTS.wallTest('jiancai')).toBe(
+ '/api/platform/tenants/jiancai/wall/test'
+ )
+ })
+})
+
+describe('凭据测试 API(testEmail / testPush · Phase 3 Task 7c)', () => {
+ beforeEach(() => {
+ vi.clearAllMocks()
+ })
+
+ it('testEmail:POST email/test,body 只带 {to}(收件人)', async () => {
+ const response = { slug: 'jiancai', sent: true, error: null }
+ vi.mocked(http.post).mockResolvedValue(response)
+
+ const res = await testEmail('jiancai', { to: 'you@example.com' }, {
+ showErrorMessage: false
+ })
+
+ expect(res).toEqual(response)
+ expect(http.post).toHaveBeenCalledTimes(1)
+ expect(http.post).toHaveBeenCalledWith(
+ '/api/platform/tenants/jiancai/email/test',
+ { to: 'you@example.com' },
+ { showErrorMessage: false } // 400「SMTP 未配置」等内联展示,不走全局提示
+ )
+ })
+
+ it('testEmail:发送失败也走成功响应通道(sent=false + error 串,非异常)', async () => {
+ const response = { slug: 'jiancai', sent: false, error: 'SMTPRecipientsRefused(...)' }
+ vi.mocked(http.post).mockResolvedValue(response)
+
+ const res = await testEmail('jiancai', { to: 'you@example.com' })
+
+ expect(res.sent).toBe(false)
+ expect(res.error).toContain('SMTP')
+ })
+
+ it('testPush:POST push/test,无请求体(探测读已保存配置)', async () => {
+ const response = {
+ slug: 'jiancai',
+ reachable: false,
+ auth_ok: null,
+ status: null,
+ reason: 'dns',
+ error: 'Failed to resolve.'
+ }
+ vi.mocked(http.post).mockResolvedValue(response)
+
+ const res = await testPush('jiancai', { showErrorMessage: false })
+
+ expect(res).toEqual(response)
+ expect(http.post).toHaveBeenCalledTimes(1)
+ const [url, body, config] = vi.mocked(http.post).mock.calls[0]
+ expect(url).toBe('/api/platform/tenants/jiancai/push/test')
+ expect(body).toBeUndefined() // 端点不读 body——探测参数全来自已保存配置
+ expect(config).toEqual({ showErrorMessage: false })
+ })
+
+ it('CONFIG_ENDPOINTS.emailTest / pushTest:端点路径与后端 config_routes 一致', () => {
+ expect(CONFIG_ENDPOINTS.emailTest('jiancai')).toBe(
+ '/api/platform/tenants/jiancai/email/test'
+ )
+ expect(CONFIG_ENDPOINTS.pushTest('jiancai')).toBe(
+ '/api/platform/tenants/jiancai/push/test'
+ )
+ })
+})
+
+describe('版本历史 / 回滚 / 重置 API(Task 7d · 事实源 config_routes)', () => {
+ beforeEach(() => {
+ vi.clearAllMocks()
+ })
+
+ it('fetchConfigVersions:GET versions 列表(升序元数据,不含 config 本体)', async () => {
+ const response = {
+ slug: 'jiancai',
+ items: [
+ {
+ version: 1,
+ created_at: '2026-10-12T09:00:00',
+ updated_by: 'super#1',
+ comment: '创建向导初始版本'
+ },
+ {
+ version: 2,
+ created_at: '2026-10-13T10:30:00',
+ updated_by: 'tenant#3',
+ comment: '改主题色'
+ }
+ ]
+ }
+ vi.mocked(http.get).mockResolvedValue(response)
+
+ const res = await fetchConfigVersions('jiancai', { showErrorMessage: false })
+
+ expect(res).toEqual(response)
+ expect(res.items[0]).not.toHaveProperty('config') // 列表行绝无 config 本体
+ expect(http.get).toHaveBeenCalledWith(
+ '/api/platform/tenants/jiancai/config/versions',
+ { showErrorMessage: false }
+ )
+ })
+
+ it('fetchConfigVersionDetail:GET versions/{n} 详情(整份快照 + 掩码)', async () => {
+ const response = {
+ slug: 'jiancai',
+ version: 2,
+ created_at: '2026-10-13T10:30:00',
+ updated_by: 'tenant#3',
+ comment: '改主题色',
+ config: {
+ brand: { title: 'HESUPIPE Support' },
+ push: { api_key: '__SET__' }
+ }
+ }
+ vi.mocked(http.get).mockResolvedValue(response)
+
+ const res = await fetchConfigVersionDetail('jiancai', 2)
+
+ expect(res).toEqual(response)
+ expect(http.get).toHaveBeenCalledWith(
+ '/api/platform/tenants/jiancai/config/versions/2',
+ undefined
+ )
+ })
+
+ it('rollbackConfig:POST rollback,body 只带 {version}(note 不被后端读取)', async () => {
+ const response = {
+ slug: 'jiancai',
+ rollback_to: 2,
+ version: 11,
+ config: { brand: { title: 'HESUPIPE Support' } }
+ }
+ vi.mocked(http.post).mockResolvedValue(response)
+
+ const res = await rollbackConfig('jiancai', { version: 2 }, {
+ showErrorMessage: false
+ })
+
+ expect(res).toEqual(response)
+ expect(http.post).toHaveBeenCalledTimes(1)
+ expect(http.post).toHaveBeenCalledWith(
+ '/api/platform/tenants/jiancai/config/rollback',
+ { version: 2 }, // 回滚请求体只认 version——多余字段是死代码
+ { showErrorMessage: false }
+ )
+ })
+
+ it('resetConfigGroup:POST config/{group}/reset,无请求体(后端不读 body)', async () => {
+ const response = {
+ slug: 'jiancai',
+ group: 'brand',
+ version: 12,
+ config: { title: 'AI Support', color: '#066fc0' }
+ }
+ vi.mocked(http.post).mockResolvedValue(response)
+
+ const res = await resetConfigGroup('jiancai', 'brand', { showErrorMessage: false })
+
+ expect(res).toEqual(response) // 响应与 PUT 分组保存同形(新版本 + 组掩码视图)
+ expect(http.post).toHaveBeenCalledTimes(1)
+ const [url, body, config] = vi.mocked(http.post).mock.calls[0]
+ expect(url).toBe('/api/platform/tenants/jiancai/config/brand/reset')
+ expect(body).toBeUndefined()
+ expect(config).toEqual({ showErrorMessage: false })
+ })
+
+ it('CONFIG_ENDPOINTS.versions / versionDetail / rollback / resetGroup:路径与后端一致', () => {
+ expect(CONFIG_ENDPOINTS.versions('jiancai')).toBe(
+ '/api/platform/tenants/jiancai/config/versions'
+ )
+ expect(CONFIG_ENDPOINTS.versionDetail('jiancai', 7)).toBe(
+ '/api/platform/tenants/jiancai/config/versions/7'
+ )
+ expect(CONFIG_ENDPOINTS.rollback('jiancai')).toBe(
+ '/api/platform/tenants/jiancai/config/rollback'
+ )
+ expect(CONFIG_ENDPOINTS.resetGroup('jiancai', 'push')).toBe(
+ '/api/platform/tenants/jiancai/config/push/reset'
+ )
+ })
+})
+
+describe('normalizeBrandGroup(GET 原样组 → 表单完整值)', () => {
+ it('缺键补空串、topics 缺省为空数组(快照是原样组,可能缺键)', () => {
+ const out = normalizeBrandGroup({ title: 'HESUPIPE Support' })
+ expect(out).toEqual({
+ title: 'HESUPIPE Support',
+ color: '',
+ avatar: '',
+ welcome: '',
+ placeholder: '',
+ whatsapp: '',
+ email: '',
+ privacy_url: '',
+ topics: []
+ })
+ })
+
+ it('类型漂移防御:非字符串字段读作空串、topics 只留字符串项', () => {
+ const out = normalizeBrandGroup({
+ title: 123,
+ color: null,
+ topics: ['Get Quotation', 42, null, 'Delivery'],
+ welcome: true
+ })
+ expect(out.title).toBe('')
+ expect(out.color).toBe('')
+ expect(out.welcome).toBe('')
+ expect(out.topics).toEqual(['Get Quotation', 'Delivery'])
+ })
+
+ it('完整组原样保留(正常路径)', () => {
+ const full = {
+ title: 'HESUPIPE Support',
+ color: '#066fc0',
+ avatar: '/static/kefu-small.png',
+ welcome: 'What kind of piping project?',
+ placeholder: 'Ask about pipes...',
+ whatsapp: '+8615937108317',
+ email: 'hesu@hesuu.com',
+ privacy_url: 'https://hesupipe.com/privacy-policy',
+ topics: ['Get Quotation']
+ }
+ expect(normalizeBrandGroup(full)).toEqual(full)
+ })
+
+ it('null / undefined 组值 → 全空表单值', () => {
+ expect(normalizeBrandGroup(null).title).toBe('')
+ expect(normalizeBrandGroup(undefined).topics).toEqual([])
+ })
+})
diff --git a/admin/src/api/__tests__/conversations.test.ts b/admin/src/api/__tests__/conversations.test.ts
new file mode 100644
index 0000000..d96eb9d
--- /dev/null
+++ b/admin/src/api/__tests__/conversations.test.ts
@@ -0,0 +1,162 @@
+/**
+ * 会话与线索 API 单元测试(Phase 3 Task 5)
+ *
+ * 钉住端点 URL 与查询参数组装(事实源 app/platform/routes/data_routes.py:
+ * intent 白名单 interested/not_interested/pending、push status 白名单
+ * pending/success/failed/skipped、from/to 闭区间、分页 page/page_size ≤100);
+ * HTTP 层行为已在 client.test.ts 覆盖,此处 mock http 只断言 API 层契约。
+ */
+import { beforeEach, describe, expect, it, vi } from 'vitest'
+
+vi.mock('@/api/client', () => ({
+ http: { get: vi.fn(), del: vi.fn() }
+}))
+
+import { http } from '@/api/client'
+import {
+ CONVERSATION_ENDPOINTS,
+ INTENT_OPTIONS,
+ PUSH_STATUS_OPTIONS,
+ conversationSourceLabel,
+ conversationStatusLabel,
+ deleteConversation,
+ fetchConversationDetail,
+ fetchConversations,
+ fetchPushLogs,
+ intentLabel,
+ pushStatusLabel,
+ pushTypeLabel
+} from '@/api/conversations'
+
+describe('会话与线索 API(src/api/conversations.ts)', () => {
+ beforeEach(() => {
+ vi.clearAllMocks()
+ vi.mocked(http.get).mockResolvedValue({ total: 0, page: 1, page_size: 20, items: [] })
+ vi.mocked(http.del).mockResolvedValue({ ok: true })
+ })
+
+ it('fetchConversations:GET /api/platform/tenants/{slug}/conversations 带筛选+分页参数', async () => {
+ await fetchConversations('jiancai', {
+ intent: 'interested',
+ site: 'es',
+ from: '2026-10-01',
+ to: '2026-10-13',
+ page: 2,
+ page_size: 50
+ })
+
+ expect(http.get).toHaveBeenCalledWith(
+ '/api/platform/tenants/jiancai/conversations',
+ {
+ params: {
+ intent: 'interested',
+ site: 'es',
+ from: '2026-10-01',
+ to: '2026-10-13',
+ page: 2,
+ page_size: 50
+ }
+ }
+ )
+ })
+
+ it('fetchConversations 无参数:params 为 undefined(后端缺省 page=1/page_size=20)', async () => {
+ await fetchConversations('jiancai')
+
+ expect(http.get).toHaveBeenCalledWith(
+ '/api/platform/tenants/jiancai/conversations',
+ { params: undefined }
+ )
+ })
+
+ it('fetchConversationDetail:GET .../conversations/{convId}', async () => {
+ const detail = {
+ id: 'c1',
+ session_id: 'sess-c1',
+ status: 'completed',
+ source: 'widget',
+ client_info: { ip: '8.8.8.8', site: 'es' },
+ site: 'es',
+ ip: '8.8.8.8',
+ device: '',
+ country: '美国',
+ created_at: '2026-10-01T12:00:00',
+ updated_at: '2026-10-01T12:00:00',
+ evaluation: null,
+ profile: { chips: { 姓名: 'John' }, summary: null },
+ messages: [{ role: 'user', content: '您好', time: '2026-10-01T12:00:00' }]
+ }
+ vi.mocked(http.get).mockResolvedValue(detail)
+
+ const res = await fetchConversationDetail('jiancai', 'c1')
+
+ expect(res).toEqual(detail)
+ expect(http.get).toHaveBeenCalledWith('/api/platform/tenants/jiancai/conversations/c1')
+ })
+
+ it('deleteConversation:DELETE .../conversations/{convId},响应 {ok}', async () => {
+ const res = await deleteConversation('jiancai', 'c1')
+
+ expect(res).toEqual({ ok: true })
+ expect(http.del).toHaveBeenCalledWith('/api/platform/tenants/jiancai/conversations/c1')
+ })
+
+ it('fetchPushLogs:GET .../push-logs 带 status/conversation_id/分页参数', async () => {
+ await fetchPushLogs('jiancai', {
+ status: 'failed',
+ conversation_id: 'c1',
+ page: 2,
+ page_size: 10
+ })
+
+ expect(http.get).toHaveBeenCalledWith('/api/platform/tenants/jiancai/push-logs', {
+ params: { status: 'failed', conversation_id: 'c1', page: 2, page_size: 10 }
+ })
+ })
+
+ it('fetchPushLogs 无参数:params 为 undefined', async () => {
+ await fetchPushLogs('jiancai')
+
+ expect(http.get).toHaveBeenCalledWith('/api/platform/tenants/jiancai/push-logs', {
+ params: undefined
+ })
+ })
+
+ it('CONVERSATION_ENDPOINTS:端点形态与 data_routes.py 路由一致', () => {
+ expect(CONVERSATION_ENDPOINTS.list('a')).toBe('/api/platform/tenants/a/conversations')
+ expect(CONVERSATION_ENDPOINTS.detail('a', 'c1')).toBe(
+ '/api/platform/tenants/a/conversations/c1'
+ )
+ expect(CONVERSATION_ENDPOINTS.remove('a', 'c1')).toBe(
+ '/api/platform/tenants/a/conversations/c1'
+ )
+ expect(CONVERSATION_ENDPOINTS.pushLogs('a')).toBe('/api/platform/tenants/a/push-logs')
+ })
+
+ it('筛选白名单选项:intent 三值 / push status 四值(与后端 INTENTS/PUSH_STATUSES 对齐)', () => {
+ expect(INTENT_OPTIONS.map((item) => item.value)).toEqual([
+ 'interested',
+ 'not_interested',
+ 'pending'
+ ])
+ expect(PUSH_STATUS_OPTIONS.map((item) => item.value)).toEqual([
+ 'pending',
+ 'success',
+ 'failed',
+ 'skipped'
+ ])
+ })
+
+ it('展示映射:未知枚举值回退原样,空值有占底文案', () => {
+ expect(intentLabel('interested')).toBe('有意向')
+ expect(intentLabel(null)).toBe('未评估')
+ expect(intentLabel('bogus')).toBe('bogus')
+ expect(pushStatusLabel('skipped')).toBe('跳过')
+ expect(pushStatusLabel('bogus')).toBe('bogus')
+ expect(pushTypeLabel('correction')).toBe('更正')
+ expect(pushTypeLabel('unknown')).toBe('unknown')
+ expect(conversationStatusLabel('completed')).toBe('已完成')
+ expect(conversationSourceLabel('widget')).toBe('挂件')
+ expect(conversationSourceLabel('weird')).toBe('weird')
+ })
+})
diff --git a/admin/src/api/__tests__/mask.test.ts b/admin/src/api/__tests__/mask.test.ts
new file mode 100644
index 0000000..5564d5c
--- /dev/null
+++ b/admin/src/api/__tests__/mask.test.ts
@@ -0,0 +1,61 @@
+/**
+ * 掩码哨兵辅助纯函数测试(Phase 3 Task 3)
+ *
+ * 契约对齐后端 app/platform/routes/config_routes.py:
+ * 密钥字段 GET 响应非空时回 `__SET__`(MASKED 哨兵),PUT 提交 `__SET__`
+ * 表示"保持库值"。本模块负责 `__SET__` ↔ 表单"已设置"语义的相互转换,
+ * 配置中心 / 平台设置页(Task 7/9)消费。
+ */
+import { describe, expect, it } from 'vitest'
+import {
+ MASKED_DISPLAY,
+ MASKED_SENTINEL,
+ isMaskedSentinel,
+ secretPlaceholder,
+ secretToSubmit
+} from '@/api/mask'
+
+describe('掩码哨兵辅助(src/api/mask.ts)', () => {
+ it('哨兵识别:__SET__ 为哨兵,其他值不是', () => {
+ expect(isMaskedSentinel('__SET__')).toBe(true)
+ expect(isMaskedSentinel('sk-real-key')).toBe(false)
+ expect(isMaskedSentinel('')).toBe(false)
+ expect(isMaskedSentinel(null)).toBe(false)
+ expect(isMaskedSentinel(undefined)).toBe(false)
+ })
+
+ it('GET 掩码值 → 表单占位语义:已设置提示可覆盖,未设置提示待输入', () => {
+ expect(MASKED_SENTINEL).toBe('__SET__')
+ expect(MASKED_DISPLAY).toBe('已设置')
+ // 后端下发的密钥字段只可能是哨兵(已设置)或空串/缺省(未设置)
+ expect(secretPlaceholder('__SET__')).toBe('已设置,输入新值覆盖')
+ expect(secretPlaceholder('')).toBe('未设置,输入新值')
+ expect(secretPlaceholder(null)).toBe('未设置,输入新值')
+ expect(secretPlaceholder(undefined)).toBe('未设置,输入新值')
+ })
+
+ it('表单值 → 提交值:未改动(空输入 + 原本已设置)回传哨兵保持库值', () => {
+ // 用户没有输入新值,且库里有值 → 回 __SET__,后端保持原值不落哨兵字面量
+ expect(secretToSubmit('', '__SET__')).toBe('__SET__')
+ })
+
+ it('表单值 → 提交值:输入了新值则提交新值(覆盖)', () => {
+ expect(secretToSubmit('sk-new-key', '__SET__')).toBe('sk-new-key')
+ expect(secretToSubmit('sk-first-key', '')).toBe('sk-first-key')
+ expect(secretToSubmit('sk-first-key', null)).toBe('sk-first-key')
+ })
+
+ it('表单值 → 提交值:未设置且留空 → 提交空串(保持未设置)', () => {
+ expect(secretToSubmit('', '')).toBe('')
+ expect(secretToSubmit('', null)).toBe('')
+ })
+
+ it('往返一致性:掩码字段未改动时,提交值仍是哨兵(后端可识别为"保持")', () => {
+ const fromServer = '__SET__'
+ const displayed = secretPlaceholder(fromServer)
+ expect(displayed).toContain('已设置')
+
+ const submitted = secretToSubmit('', fromServer)
+ expect(isMaskedSentinel(submitted)).toBe(true)
+ })
+})
diff --git a/admin/src/api/__tests__/settings.test.ts b/admin/src/api/__tests__/settings.test.ts
new file mode 100644
index 0000000..c322fac
--- /dev/null
+++ b/admin/src/api/__tests__/settings.test.ts
@@ -0,0 +1,78 @@
+/**
+ * 平台设置 API 单元测试(Phase 3 Task 9)
+ *
+ * 钉住 API 层契约(事实源 app/platform/routes/config_routes.py 的
+ * settings_router):GET 列表 {items:[{key,value}]} / PUT 单键 upsert
+ * {key,value}——与 config 组的"整组替换"不同,KV 表逐键写。
+ * HTTP 层行为已在 client.test.ts 覆盖,此处 mock http 只断言调用形态。
+ */
+import { beforeEach, describe, expect, it, vi } from 'vitest'
+
+vi.mock('@/api/client', () => ({
+ http: { get: vi.fn(), put: vi.fn() }
+}))
+
+import { http } from '@/api/client'
+import {
+ SETTINGS_ENDPOINTS,
+ fetchPlatformSettings,
+ putPlatformSetting
+} from '@/api/settings'
+
+describe('平台设置 API(src/api/settings.ts)', () => {
+ beforeEach(() => {
+ vi.clearAllMocks()
+ })
+
+ it('fetchPlatformSettings:GET /api/platform/settings(超管端点)', async () => {
+ const body = {
+ items: [
+ { key: 'ai.api_key', value: '__SET__' },
+ { key: 'ai.model', value: 'glm-4-flash' }
+ ]
+ }
+ vi.mocked(http.get).mockResolvedValue(body)
+
+ const res = await fetchPlatformSettings()
+
+ expect(res).toEqual(body)
+ expect(http.get).toHaveBeenCalledWith(SETTINGS_ENDPOINTS.list, undefined)
+ })
+
+ it('fetchPlatformSettings:透传 RequestOptions(内联错误展示)', async () => {
+ vi.mocked(http.get).mockResolvedValue({ items: [] })
+ await fetchPlatformSettings({ showErrorMessage: false })
+ expect(http.get).toHaveBeenCalledWith(SETTINGS_ENDPOINTS.list, {
+ showErrorMessage: false
+ })
+ })
+
+ it('putPlatformSetting:PUT 单键 upsert,请求体 {key, value}', async () => {
+ const response = { key: 'smtp.host', value: 'smtp.exmail.qq.com' }
+ vi.mocked(http.put).mockResolvedValue(response)
+
+ const res = await putPlatformSetting(
+ { key: 'smtp.host', value: 'smtp.exmail.qq.com' },
+ { showErrorMessage: false }
+ )
+
+ expect(res).toEqual(response)
+ expect(http.put).toHaveBeenCalledWith(
+ SETTINGS_ENDPOINTS.put,
+ { key: 'smtp.host', value: 'smtp.exmail.qq.com' },
+ { showErrorMessage: false }
+ )
+ })
+
+ it('putPlatformSetting:密钥哨兵原样提交(后端还原库值)', async () => {
+ vi.mocked(http.put).mockResolvedValue({ key: 'ai.api_key', value: '__SET__' })
+
+ await putPlatformSetting({ key: 'ai.api_key', value: '__SET__' })
+
+ expect(http.put).toHaveBeenCalledWith(
+ SETTINGS_ENDPOINTS.put,
+ { key: 'ai.api_key', value: '__SET__' },
+ undefined
+ )
+ })
+})
diff --git a/admin/src/api/__tests__/stats.test.ts b/admin/src/api/__tests__/stats.test.ts
new file mode 100644
index 0000000..3269ff9
--- /dev/null
+++ b/admin/src/api/__tests__/stats.test.ts
@@ -0,0 +1,89 @@
+/**
+ * 统计 API 单元测试(Phase 3 Task 4)
+ *
+ * 钉住端点 URL 与查询参数组装(?from/&to/&tenant= 事实源
+ * app/platform/routes/stats_routes.py);HTTP 层行为(拦截/token/错误
+ * 提示)已在 client.test.ts 覆盖,此处 mock http 只断言 API 层契约:
+ * - fetchPlatformStats:/api/platform/stats 合并视图;tenant= 下钻;
+ * showErrorMessage: false 透传(卡片墙逐卡内联错误防 N 连弹)
+ * - fetchTenantStats:/api/platform/tenants/{slug}/stats 单租户
+ * - fetchTenantList:/api/platform/tenants(卡片墙枚举租户)
+ */
+import { beforeEach, describe, expect, it, vi } from 'vitest'
+
+vi.mock('@/api/client', () => ({
+ http: { get: vi.fn() }
+}))
+
+import { http } from '@/api/client'
+import { fetchPlatformStats, fetchTenantStats } from '@/api/stats'
+import { fetchTenantList } from '@/api/tenants'
+
+/** 零数据统计响应形态(与后端 stats._empty_stats 同构) */
+const EMPTY_STATS = {
+ cards: { conversations: 0, leads: 0, high_intent: 0, push_success_rate: null },
+ funnel: { conversations: 0, leads: 0, evaluated: 0, push_succeeded: 0 },
+ trend: [],
+ distributions: { intent: {}, source: {}, site: {}, country: [] },
+ push_health: { pending: 0, success: 0, failed: 0, skipped: 0, duplicated: 0 }
+}
+
+describe('统计 API(src/api/stats.ts + tenants.ts 列表函数)', () => {
+ beforeEach(() => {
+ vi.clearAllMocks()
+ vi.mocked(http.get).mockResolvedValue(EMPTY_STATS)
+ })
+
+ it('fetchPlatformStats 无参数:GET /api/platform/stats,无查询参数', async () => {
+ const res = await fetchPlatformStats()
+
+ expect(res).toEqual(EMPTY_STATS)
+ expect(http.get).toHaveBeenCalledWith('/api/platform/stats', { params: undefined })
+ })
+
+ it('fetchPlatformStats 带 from/to/tenant:全部进查询参数(卡片墙下钻形态)', async () => {
+ await fetchPlatformStats({ tenant: 'jiancai', from: '2026-10-13', to: '2026-10-13' })
+
+ expect(http.get).toHaveBeenCalledWith('/api/platform/stats', {
+ params: { tenant: 'jiancai', from: '2026-10-13', to: '2026-10-13' }
+ })
+ })
+
+ it('fetchPlatformStats 透传 showErrorMessage: false(卡片墙防 N 连弹)', async () => {
+ await fetchPlatformStats({ from: '2026-10-13' }, { showErrorMessage: false })
+
+ expect(http.get).toHaveBeenCalledWith('/api/platform/stats', {
+ params: { from: '2026-10-13' },
+ showErrorMessage: false
+ })
+ })
+
+ it('fetchTenantStats:GET /api/platform/tenants/{slug}/stats 带 from/to', async () => {
+ await fetchTenantStats('jiancai', { from: '2026-10-07', to: '2026-10-13' })
+
+ expect(http.get).toHaveBeenCalledWith('/api/platform/tenants/jiancai/stats', {
+ params: { from: '2026-10-07', to: '2026-10-13' }
+ })
+ })
+
+ it('fetchTenantList:GET /api/platform/tenants(卡片墙枚举)', async () => {
+ const body = {
+ items: [
+ {
+ slug: 'jiancai',
+ name: '建材租户',
+ status: 'active',
+ domain_count: 1,
+ current_version: 1,
+ admins: [{ username: 'jc-admin', status: 'active' }]
+ }
+ ]
+ }
+ vi.mocked(http.get).mockResolvedValue(body)
+
+ const res = await fetchTenantList()
+
+ expect(res).toEqual(body)
+ expect(http.get).toHaveBeenCalledWith('/api/platform/tenants')
+ })
+})
diff --git a/admin/src/api/__tests__/tenants.test.ts b/admin/src/api/__tests__/tenants.test.ts
new file mode 100644
index 0000000..3928084
--- /dev/null
+++ b/admin/src/api/__tests__/tenants.test.ts
@@ -0,0 +1,218 @@
+/**
+ * 租户/站点/域名 API 单元测试(Phase 3 Task 6)
+ *
+ * 钉住 API 层契约:端点 URL / 方法 / 请求体形状(事实源
+ * app/platform/routes/tenant_routes.py);HTTP 层行为已在 client.test.ts
+ * 覆盖,此处 mock http 只断言调用形态:
+ * - 列表/详情:GET /api/platform/tenants[/{slug}]
+ * - 创建向导:POST 全量六字段(domains 进 main 站点)
+ * - PATCH 租户:只发 name 或 status(slug 键绝不出现在 body——后端 400)
+ * - 站点:POST sites(brand_overlay 只含显式覆盖字段)/ PATCH sites/{site}
+ * - 域名:POST domains {domain} / DELETE domains/{domain}(路径段直拼)
+ * - RequestOptions 透传(向导/绑域名内联错误关全局 toast)
+ */
+import { beforeEach, describe, expect, it, vi } from 'vitest'
+
+vi.mock('@/api/client', () => ({
+ http: { get: vi.fn(), post: vi.fn(), patch: vi.fn(), del: vi.fn() }
+}))
+
+import { http } from '@/api/client'
+import {
+ addSiteDomain,
+ createSite,
+ createTenant,
+ deleteSiteDomain,
+ fetchTenantDetail,
+ fetchTenantList,
+ patchSite,
+ patchTenant
+} from '@/api/tenants'
+
+describe('租户与站点管理 API(src/api/tenants.ts)', () => {
+ beforeEach(() => {
+ vi.clearAllMocks()
+ })
+
+ it('fetchTenantList:GET /api/platform/tenants', async () => {
+ const body = {
+ items: [
+ {
+ slug: 'jiancai',
+ name: '建材租户',
+ status: 'active',
+ domain_count: 1,
+ current_version: 1,
+ admins: [{ username: 'jc-admin', status: 'active' }]
+ }
+ ]
+ }
+ vi.mocked(http.get).mockResolvedValue(body)
+
+ const res = await fetchTenantList()
+
+ expect(res).toEqual(body)
+ expect(http.get).toHaveBeenCalledWith('/api/platform/tenants')
+ })
+
+ it('fetchTenantDetail:GET /api/platform/tenants/{slug}', async () => {
+ const body = {
+ slug: 'jiancai',
+ name: '建材租户',
+ status: 'active',
+ current_version: 1,
+ sites: [
+ {
+ slug: 'main',
+ name: 'main',
+ status: 'active',
+ domains: ['chat2.agent.hnxinyouhe.com'],
+ brand_overlay: {}
+ }
+ ],
+ admins: [{ username: 'jc-admin', status: 'active' }]
+ }
+ vi.mocked(http.get).mockResolvedValue(body)
+
+ const res = await fetchTenantDetail('jiancai')
+
+ expect(res).toEqual(body)
+ expect(http.get).toHaveBeenCalledWith('/api/platform/tenants/jiancai', undefined)
+ })
+
+ it('createTenant:POST 全量六字段(向导提交形态)', async () => {
+ const body = {
+ name: '新租户',
+ slug: 'new-tenant',
+ profile: 'blank',
+ admin_username: 'nt-admin',
+ admin_password: 'initial-pass',
+ domains: ['a.example.com', 'b.example.com']
+ }
+ const created = {
+ slug: 'new-tenant',
+ name: '新租户',
+ status: 'active',
+ profile: 'blank',
+ version: 1,
+ site: { slug: 'main', domains: ['a.example.com', 'b.example.com'] },
+ admin_username: 'nt-admin'
+ }
+ vi.mocked(http.post).mockResolvedValue(created)
+
+ const res = await createTenant(body, { showErrorMessage: false })
+
+ expect(res).toEqual(created)
+ expect(http.post).toHaveBeenCalledWith('/api/platform/tenants', body, {
+ showErrorMessage: false
+ })
+ })
+
+ it('patchTenant 改名:PATCH {slug} 只发 name(slug 键不可出现)', async () => {
+ vi.mocked(http.patch).mockResolvedValue({
+ slug: 'jiancai',
+ name: '建材租户改',
+ status: 'active'
+ })
+
+ await patchTenant('jiancai', { name: '建材租户改' })
+
+ expect(http.patch).toHaveBeenCalledWith(
+ '/api/platform/tenants/jiancai',
+ { name: '建材租户改' },
+ undefined
+ )
+ })
+
+ it('patchTenant 停用:PATCH {slug} 只发 status=disabled', async () => {
+ vi.mocked(http.patch).mockResolvedValue({
+ slug: 'jiancai',
+ name: '建材租户',
+ status: 'disabled'
+ })
+
+ await patchTenant('jiancai', { status: 'disabled' })
+
+ expect(http.patch).toHaveBeenCalledWith(
+ '/api/platform/tenants/jiancai',
+ { status: 'disabled' },
+ undefined
+ )
+ })
+
+ it('createSite:POST sites 带 brand_overlay 覆盖子集(留空字段不进 overlay)', async () => {
+ const body = {
+ slug: 'shop1',
+ name: '西班牙站',
+ brand_overlay: { title: 'HESUPIPE ES', color: '#ff6600' }
+ }
+ const created = {
+ slug: 'shop1',
+ name: '西班牙站',
+ status: 'active',
+ domains: [],
+ brand_overlay: body.brand_overlay
+ }
+ vi.mocked(http.post).mockResolvedValue(created)
+
+ const res = await createSite('jiancai', body)
+
+ expect(res).toEqual(created)
+ expect(http.post).toHaveBeenCalledWith('/api/platform/tenants/jiancai/sites', body, undefined)
+ })
+
+ it('patchSite 停用:PATCH sites/{siteSlug} 只发 status', async () => {
+ vi.mocked(http.patch).mockResolvedValue({
+ slug: 'shop1',
+ name: '西班牙站',
+ status: 'disabled',
+ domains: [],
+ brand_overlay: {}
+ })
+
+ await patchSite('jiancai', 'shop1', { status: 'disabled' })
+
+ expect(http.patch).toHaveBeenCalledWith(
+ '/api/platform/tenants/jiancai/sites/shop1',
+ { status: 'disabled' },
+ undefined
+ )
+ })
+
+ it('addSiteDomain:POST sites/{siteSlug}/domains {domain}', async () => {
+ vi.mocked(http.post).mockResolvedValue({
+ domain: 'es.hesupipe.com',
+ site: 'shop1'
+ })
+
+ const res = await addSiteDomain(
+ 'jiancai',
+ 'shop1',
+ { domain: 'es.hesupipe.com' },
+ { showErrorMessage: false }
+ )
+
+ expect(res).toEqual({ domain: 'es.hesupipe.com', site: 'shop1' })
+ expect(http.post).toHaveBeenCalledWith(
+ '/api/platform/tenants/jiancai/sites/shop1/domains',
+ { domain: 'es.hesupipe.com' },
+ { showErrorMessage: false }
+ )
+ })
+
+ it('deleteSiteDomain:DELETE sites/{siteSlug}/domains/{domain}(路径直拼)', async () => {
+ vi.mocked(http.del).mockResolvedValue({
+ ok: true,
+ domain: 'es.hesupipe.com',
+ site: 'shop1'
+ })
+
+ const res = await deleteSiteDomain('jiancai', 'shop1', 'es.hesupipe.com')
+
+ expect(res).toEqual({ ok: true, domain: 'es.hesupipe.com', site: 'shop1' })
+ expect(http.del).toHaveBeenCalledWith(
+ '/api/platform/tenants/jiancai/sites/shop1/domains/es.hesupipe.com',
+ undefined
+ )
+ })
+})
diff --git a/admin/src/api/auth.ts b/admin/src/api/auth.ts
new file mode 100644
index 0000000..3c08637
--- /dev/null
+++ b/admin/src/api/auth.ts
@@ -0,0 +1,38 @@
+/**
+ * 认证 API(Phase 3 Task 3 实装,替换 Task 2 开发桩)
+ *
+ * 对接后端 app/platform/routes/auth_routes.py:
+ * - POST /api/platform/auth/login {username, password}
+ * → {token, role: "super"|"tenant", tenant_slug, must_change_password}
+ * - POST /api/platform/auth/logout(Bearer;撤销当前 token)
+ * - POST /api/platform/auth/change-password {old_password, new_password}
+ * → {ok, must_change_password}
+ *
+ * 错误语义(HTTP client 统一处理,见 src/api/client.ts):
+ * - 401 凭据错误/会话过期(登录请求自身 401 只提示不跳转)
+ * - 429 登录限流(5 次/15 分钟)
+ * - 400 原密码不正确
+ */
+import { http } from './client'
+import type { LoginParams, LoginResponse, ChangePasswordParams } from '@/store/modules/auth'
+
+/** 登录 */
+export function fetchLogin(params: LoginParams): Promise {
+ return http.post('/api/platform/auth/login', params)
+}
+
+/** 登出(撤销当前 token;需 Bearer) */
+export function fetchLogout(): Promise<{ ok: boolean }> {
+ return http.post<{ ok: boolean }>('/api/platform/auth/logout')
+}
+
+/** 修改密码(验旧密 → 写新 hash;改完当前会话保持有效) */
+export function fetchChangePassword(params: ChangePasswordParams): Promise<{
+ ok: boolean
+ must_change_password: boolean
+}> {
+ return http.post<{ ok: boolean; must_change_password: boolean }>(
+ '/api/platform/auth/change-password',
+ params
+ )
+}
diff --git a/admin/src/api/client.ts b/admin/src/api/client.ts
new file mode 100644
index 0000000..f0a497a
--- /dev/null
+++ b/admin/src/api/client.ts
@@ -0,0 +1,143 @@
+/**
+ * 平台 API HTTP client(Phase 3 Task 3,自建薄封装)
+ *
+ * ## 为什么不用模板的 src/utils/http
+ *
+ * 模板封装与后端约定对不上,改造成本高于自建:
+ * - 响应:模板要求 `{code, msg, data}` 业务信封;后端(FastAPI)业务语义
+ * 直用 HTTP 状态码,成功响应就是数据本身
+ * - 错误:模板读 `data.msg`;FastAPI 错误体是 `{detail: 中文}`
+ * - 凭据:模板裸塞 `Authorization: `;后端要求 `Bearer `
+ * - 模板无 409 强制改密语义
+ *
+ * ## 本 client 约定
+ *
+ * - 请求拦截:auth store 有 token 时自动注入 `Authorization: Bearer `
+ * - 401(非登录请求):清会话 + 跳登录(登录请求自身的 401 = 凭据错误,
+ * 只弹 detail,不触发登出/跳转——否则登录页死循环)
+ * - 409 且 detail 为强制改密文案:跳强制改密页;其余 409(并发冲突等)
+ * 是普通业务错误,只弹 detail(后端 409 双语义:enforce_password_changed
+ * 与租户/域名并发冲突,见 app/platform/routes/)
+ * - 其余错误/网络错误:ElMessage 出后端 detail / 统一网络文案
+ * - 成功:直接透传 response.data
+ * - 请求配置 showErrorMessage: false 可关闭全局错误提示(调用方自行处理)
+ */
+import axios, {
+ AxiosError,
+ type AxiosRequestConfig,
+ type AxiosResponse,
+ type InternalAxiosRequestConfig
+} from 'axios'
+import { ElMessage } from 'element-plus'
+import { useAuthStore } from '@/store/modules/auth'
+
+/**
+ * 强制改密 409 的 detail 契约(app/platform/auth.py enforce_password_changed
+ * 的静态文案)。只有命中该文案的 409 才触发改密跳转。
+ */
+export const MUST_CHANGE_DETAIL = '必须先修改初始密码'
+
+/** 网络错误统一文案(无响应时后端不可能给 detail) */
+export const NETWORK_ERROR_MESSAGE = '网络连接异常,请检查网络后重试'
+
+/** 登录端点路径(其 401/429 是凭据/限流业务错误,不走会话过期处理) */
+const LOGIN_URL = '/api/platform/auth/login'
+
+/** 统一 API 错误(供调用方按需识别状态码) */
+export class ApiError extends Error {
+ readonly status?: number
+ readonly url?: string
+
+ constructor(message: string, status?: number, url?: string) {
+ super(message)
+ this.name = 'ApiError'
+ this.status = status
+ this.url = url
+ }
+}
+
+/** 后端错误体形态(FastAPI HTTPException:{detail: string}) */
+interface ApiErrorBody {
+ detail?: string
+}
+
+/** 扩展请求配置:允许关闭全局错误提示 */
+export interface RequestOptions extends AxiosRequestConfig {
+ showErrorMessage?: boolean
+}
+
+const instance = axios.create({
+ timeout: 15000,
+ // 开发走 vite 代理(/),生产同源反代;两环境 VITE_API_URL 均为 '/'
+ baseURL: import.meta.env.VITE_API_URL || '/',
+ // 后端业务语义直用 HTTP 状态码,非 2xx 一律进错误拦截器
+ validateStatus: (status) => status >= 200 && status < 300
+})
+
+/** 请求拦截:Bearer token 注入 */
+instance.interceptors.request.use((config: InternalAxiosRequestConfig) => {
+ const { token } = useAuthStore()
+ if (token) {
+ config.headers.set('Authorization', `Bearer ${token}`)
+ }
+ return config
+})
+
+/** 提取错误提示文案:优先后端 detail,其次统一网络文案 */
+function extractDetail(error: AxiosError): string {
+ if (error.response?.data?.detail) {
+ return error.response.data.detail
+ }
+ return error.response ? `请求失败(HTTP ${error.response.status})` : NETWORK_ERROR_MESSAGE
+}
+
+/** 响应拦截:401/409 全局处理 + 统一错误提示 */
+instance.interceptors.response.use(
+ (response: AxiosResponse) => response,
+ async (error: AxiosError) => {
+ const status = error.response?.status
+ const detail = extractDetail(error)
+ const url = error.config?.url || ''
+ const config = error.config as (RequestOptions & InternalAxiosRequestConfig) | undefined
+ const isLoginRequest = url.includes(LOGIN_URL)
+
+ if (status === 401 && !isLoginRequest) {
+ // 会话过期:清空状态 + 模板侧全套清理(含跳登录,登录页不重复跳)
+ await useAuthStore().handleSessionExpired()
+ } else if (status === 409 && detail === MUST_CHANGE_DETAIL) {
+ // 强制改密:置标志 + 跳改密页(已在改密页不重复跳)
+ await useAuthStore().handleMustChangePassword()
+ }
+
+ if (config?.showErrorMessage !== false) {
+ ElMessage.error(detail)
+ }
+
+ throw new ApiError(detail, status, url)
+ }
+)
+
+/** 发起请求,成功透传 response.data */
+async function request(config: RequestOptions): Promise {
+ const response = await instance.request(config)
+ return response.data
+}
+
+/** API 方法集合 */
+export const http = {
+ get(url: string, config?: RequestOptions): Promise {
+ return request({ ...config, url, method: 'GET' })
+ },
+ post(url: string, data?: unknown, config?: RequestOptions): Promise {
+ return request({ ...config, url, method: 'POST', data })
+ },
+ put(url: string, data?: unknown, config?: RequestOptions): Promise {
+ return request({ ...config, url, method: 'PUT', data })
+ },
+ patch(url: string, data?: unknown, config?: RequestOptions): Promise {
+ return request({ ...config, url, method: 'PATCH', data })
+ },
+ del(url: string, config?: RequestOptions): Promise {
+ return request({ ...config, url, method: 'DELETE' })
+ }
+}
diff --git a/admin/src/api/config.ts b/admin/src/api/config.ts
new file mode 100644
index 0000000..d11dd52
--- /dev/null
+++ b/admin/src/api/config.ts
@@ -0,0 +1,402 @@
+/**
+ * 租户配置中心 API(Phase 3 Task 3 骨架;Task 7a 实装品牌相关调用函数;
+ * Task 7b 补词表试金石调用)
+ *
+ * 端点事实源:app/platform/routes/config_routes.py
+ * (配置快照 / 分组编辑 / 版本历史 / 回滚 / 重置 / 试金石)
+ * 与 app/platform/routes/upload_routes.py(头像上传)。
+ *
+ * 密钥掩码哨兵:GET 的密钥字段非空时为 '__SET__'(见 src/api/mask.ts
+ * 的表单语义转换);PUT 提交 '__SET__' = 保持库值。品牌组无密钥字段,
+ * 无哨兵处理需求。
+ *
+ * ## Task 7a 契约修正(对齐后端事实源)
+ *
+ * - PUT /config/{group} 的请求体是**组值本身**(整组替换语义),保存备注
+ * 走查询参数 comment(config_routes.put_config_group 的 comment 形参);
+ * - 版本行字段是 updated_by / comment(非骨架期误写的 created_by / note);
+ * - 回滚请求体只认 version(note 不被后端读取);
+ * - 试金石响应逐句为 {sentence, action, reason, replacement}。
+ *
+ * ## Task 7b 试金石契约(config_routes.test_wall)
+ *
+ * - body:sentences 必填;wall 可选覆盖(编辑器当前表单值的**预演**——
+ * 后端先 compile_wall 校验后纯内存判定,不落库不 bump 版本),坏墙
+ * 400 带字段路径;has_contact / source 可选(默认 false / web);
+ * - 判定优先级与 conversation.py 执行侧一致:价格墙(blocked)→
+ * 零号码墙(replaced)→ 再索要墙(仅 has_contact,blocked 无替换)。
+ *
+ * ## Task 7c 凭据测试契约(config_routes.test_email / test_push)
+ *
+ * - POST email/test:body {to}——后端用**已保存**的当前邮件配置真发一封
+ * 测试邮件;SMTP 未配置 → 400;成败如实返回(sent=false 时带 error);
+ * - POST push/test:连通性探测——只验 URL 可达 + 鉴权,**不发任何线索
+ * 数据**(后端发空 messages 批量);不可达时 reason 分类
+ * dns/timeout/connect,auth_ok 三态(true/false/null=不定)。
+ *
+ * ## Task 7d 版本历史 / 回滚 / 重置契约(config_routes)
+ *
+ * - GET versions:升序 items(version/created_at/updated_by/comment),
+ * **不含 config 本体**(列表几十 KB × N 版本会白白撑爆响应);展示侧
+ * 自行倒序(最新在前);
+ * - GET versions/{n}:含整份快照(掩码视图——密钥字段 '__SET__',历史行
+ * 本身永远存原值);created_at 是 naive isoformat(无时区后缀);
+ * - POST rollback {version}:目标版本整份复制为新版本(历史不可变),
+ * 响应 {rollback_to, version(新), config(新快照掩码视图)};
+ * - POST config/{group}/reset:该组恢复出厂档案值(代码库当前模板,非
+ * 创建时快照),响应形态与 PUT 相同({group, version, config});
+ * 合法组 = config_validation.CONFIG_GROUPS(RESET_GROUP_LABELS 镜像);
+ * push 组重置可能因出厂 prefix 被其他租户占用而 400(跨租户查重)。
+ */
+
+import { http, type RequestOptions } from './client'
+
+/** 配置组名(快照顶层键;与 config_validation.CONFIG_GROUPS 对齐) */
+export type ConfigGroupKey =
+ | 'brand'
+ | 'prompts'
+ | 'wall'
+ | 'contact'
+ | 'email'
+ | 'push'
+ | 'ai'
+ | 'engine'
+ | 'misc'
+
+/** 品牌组值(整组替换语义:字段清单固定,topics 为字符串数组) */
+export interface BrandGroupValue {
+ title: string
+ color: string
+ avatar: string
+ welcome: string
+ topics: string[]
+ placeholder: string
+ whatsapp: string
+ email: string
+ privacy_url: string
+}
+
+/** 当前生效配置响应(GET /api/platform/tenants/{slug}/config) */
+export interface TenantConfigResponse {
+ slug: string
+ version: number
+ /** 掩码视图的整份快照(密钥字段可能为 '__SET__') */
+ config: Record>
+}
+
+/** 分组保存响应(PUT .../config/{group};config 为该组掩码视图) */
+export interface PutConfigGroupResponse {
+ slug: string
+ group: string
+ version: number
+ config: Record
+}
+
+/**
+ * 组级重置响应(POST .../config/{group}/reset):与 PUT 分组保存同形
+ * (新版本号 + 该组出厂值的掩码视图——密钥字段 '__SET__')。
+ */
+export type ResetConfigGroupResponse = PutConfigGroupResponse
+
+/** 配置版本行(GET .../config/versions;字段名与后端一致) */
+export interface ConfigVersionItem {
+ version: number
+ created_at: string
+ updated_by: string
+ comment: string
+}
+
+/** 版本列表响应 */
+export interface ConfigVersionListResponse {
+ slug: string
+ items: ConfigVersionItem[]
+}
+
+/** 版本详情响应(含整份快照) */
+export interface ConfigVersionDetailResponse extends ConfigVersionItem {
+ slug: string
+ config: Record>
+}
+
+/** 回滚请求(POST .../config/rollback,产生新版本;后端只读 version) */
+export interface ConfigRollbackRequest {
+ version: number
+}
+
+/** 回滚响应 */
+export interface ConfigRollbackResponse {
+ slug: string
+ rollback_to: number
+ version: number
+ config: Record>
+}
+
+/** 词表试金石请求(POST .../wall/test)
+ * wall 覆盖 = 编辑器当前表单值的预演(后端 compile_wall 校验 + 纯内存
+ * 判定,不落库);缺省用当前生效墙。 */
+export interface WallTestRequest {
+ sentences: string[]
+ wall?: Record
+ has_contact?: boolean
+ source?: 'web' | 'app'
+}
+
+/** 词表试金石响应(逐句判定;action: blocked/replaced/pass) */
+export interface WallTestResponse {
+ slug: string
+ wall_source: 'current' | 'override'
+ results: Array<{
+ sentence: string
+ action: 'blocked' | 'replaced' | 'pass'
+ reason: 'boundary' | 'no_contact' | 'reask' | null
+ replacement: string | null
+ }>
+}
+
+/** 头像上传响应(POST .../brand/avatar;avatar 为落盘文件名) */
+export interface AvatarUploadResponse {
+ slug: string
+ avatar: string
+ version: number
+}
+
+/** SMTP 测试邮件请求(POST .../email/test;用**已保存**配置真发一封到 to) */
+export interface EmailTestRequest {
+ to: string
+}
+
+/** SMTP 测试邮件响应(发送失败不 5xx——sent=false 时 error 带原因) */
+export interface EmailTestResponse {
+ slug: string
+ sent: boolean
+ error: string | null
+}
+
+/** 推送探测不可达原因分类(前端据此给修复提示文案) */
+export type PushProbeReason = 'dns' | 'timeout' | 'connect'
+
+/** 推送连通性探测响应(POST .../push/test;空批量预检,零线索数据) */
+export interface PushTestResponse {
+ slug: string
+ reachable: boolean
+ /** true=2xx / false=401·403 / null=其他状态(到达但鉴权不定)或不可达 */
+ auth_ok: boolean | null
+ status: number | null
+ reason: PushProbeReason | null
+ error: string | null
+}
+
+/** 配置中心端点 */
+export const CONFIG_ENDPOINTS = {
+ current: (slug: string) => `/api/platform/tenants/${slug}/config`,
+ putGroup: (slug: string, group: ConfigGroupKey | string) =>
+ `/api/platform/tenants/${slug}/config/${group}`,
+ resetGroup: (slug: string, group: ConfigGroupKey | string) =>
+ `/api/platform/tenants/${slug}/config/${group}/reset`,
+ versions: (slug: string) => `/api/platform/tenants/${slug}/config/versions`,
+ versionDetail: (slug: string, version: number) =>
+ `/api/platform/tenants/${slug}/config/versions/${version}`,
+ rollback: (slug: string) => `/api/platform/tenants/${slug}/config/rollback`,
+ wallTest: (slug: string) => `/api/platform/tenants/${slug}/wall/test`,
+ avatarUpload: (slug: string) => `/api/platform/tenants/${slug}/brand/avatar`,
+ emailTest: (slug: string) => `/api/platform/tenants/${slug}/email/test`,
+ pushTest: (slug: string) => `/api/platform/tenants/${slug}/push/test`
+} as const
+
+/**
+ * 当前生效配置(MAX 版本快照 + version)。
+ * 注意快照是"原样组"——组内缺省键由消费侧深合并出厂默认,
+ * 品牌表单消费 config.brand 时用 normalizeBrandGroup 防御读取。
+ */
+export function fetchTenantConfig(
+ slug: string,
+ config?: RequestOptions
+): Promise {
+ return http.get(CONFIG_ENDPOINTS.current(slug), config)
+}
+
+/**
+ * 分组保存(整组替换语义):请求体 = 组值本身,备注走 comment 查询参数。
+ * 调用方必须先 GET 全组、改字段后整组提交——部分提交会丢掉未携带的键
+ * (后端 deep merge 出厂默认兜底,但租户既有定制会被覆盖丢失)。
+ */
+export function putConfigGroup(
+ slug: string,
+ group: ConfigGroupKey | string,
+ value: Record,
+ comment?: string,
+ config?: RequestOptions
+): Promise {
+ return http.put(
+ CONFIG_ENDPOINTS.putGroup(slug, group),
+ value,
+ {
+ ...config,
+ params: comment ? { comment } : undefined
+ }
+ )
+}
+
+/**
+ * 版本列表(GET .../config/versions):升序 items(版本/时间/作者/备注),
+ * 不含 config 本体。展示侧自行倒序(最新在前)——后端列表只回元数据,
+ * 详情走 fetchConfigVersionDetail 按需取整份。
+ */
+export function fetchConfigVersions(
+ slug: string,
+ config?: RequestOptions
+): Promise {
+ return http.get(
+ CONFIG_ENDPOINTS.versions(slug),
+ config
+ )
+}
+
+/**
+ * 单版本详情(GET .../config/versions/{n}):含整份快照(密钥字段为
+ * '__SET__' 掩码视图——历史行存原值但绝不回传)。版本不存在 → 404。
+ */
+export function fetchConfigVersionDetail(
+ slug: string,
+ version: number,
+ config?: RequestOptions
+): Promise {
+ return http.get(
+ CONFIG_ENDPOINTS.versionDetail(slug, version),
+ config
+ )
+}
+
+/**
+ * 回滚(POST .../config/rollback):目标版本整份复制为新版本(历史不可变
+ * ——旧版本行永不改写),reload 热生效。响应含新版本号 + 新快照掩码视图
+ * (调用方据此回写当前配置态,无需再 GET config)。
+ */
+export function rollbackConfig(
+ slug: string,
+ body: ConfigRollbackRequest,
+ config?: RequestOptions
+): Promise {
+ return http.post(
+ CONFIG_ENDPOINTS.rollback(slug),
+ body,
+ config
+ )
+}
+
+/**
+ * 组级重置出厂(POST .../config/{group}/reset,无请求体):该组恢复出厂
+ * 档案值(代码库当前模板,随平台升级演进——非创建时快照),产新版本 +
+ * reload 热生效。合法组见 model.ts 的 RESET_GROUP_LABELS(后端
+ * CONFIG_GROUPS 镜像,未知组 400);push 组重置可能因出厂 prefix 被其他
+ * 租户占用而 400(跨租户查重)——调用方 showErrorMessage: false 内联展示。
+ */
+export function resetConfigGroup(
+ slug: string,
+ group: ConfigGroupKey | string,
+ config?: RequestOptions
+): Promise {
+ return http.post(
+ CONFIG_ENDPOINTS.resetGroup(slug, group),
+ undefined,
+ config
+ )
+}
+
+/**
+ * 词表试金石(POST .../wall/test):逐句墙判定预览。传 wall 覆盖 =
+ * 编辑器当前表单值的预演(不落库不 bump 版本)——保存词表前的自动
+ * 验证复用本函数;坏墙(正则编译失败等)400 带字段路径,调用方需
+ * showErrorMessage: false 自行内联展示。
+ */
+export function testWall(
+ slug: string,
+ body: WallTestRequest,
+ config?: RequestOptions
+): Promise {
+ return http.post(CONFIG_ENDPOINTS.wallTest(slug), body, config)
+}
+
+/**
+ * 头像上传(multipart file 字段):魔数校验 / 2MB 上限在后端
+ * (upload_routes.py),前端做扩展名与大小预检提前拦截。
+ * 成功后 brand.avatar 更新为落盘文件名,版本 +1 并热生效。
+ */
+export function uploadBrandAvatar(
+ slug: string,
+ file: File,
+ config?: RequestOptions
+): Promise {
+ const form = new FormData()
+ form.append('file', file)
+ return http.post(
+ CONFIG_ENDPOINTS.avatarUpload(slug),
+ form,
+ config
+ )
+}
+
+/**
+ * SMTP 测试邮件(POST .../email/test):用**已保存**的当前邮件配置真发一封
+ * 到 to——表单里未保存的编辑不生效(界面需提示"先保存再测试")。SMTP 未
+ * 配置 → 400(调用方 showErrorMessage: false 自行内联展示);发送失败
+ * 不抛错——sent=false 时 error 带原因串。
+ */
+export function testEmail(
+ slug: string,
+ body: EmailTestRequest,
+ config?: RequestOptions
+): Promise {
+ return http.post(
+ CONFIG_ENDPOINTS.emailTest(slug),
+ body,
+ config
+ )
+}
+
+/**
+ * 推送连通性探测(POST .../push/test):只验 URL 可达 + 鉴权,后端发空
+ * messages 批量(零线索数据)。URL 未配置 → 400;结果三态由调用方按
+ * reachable/auth_ok 展示(不可达 reason 分类 dns/timeout/connect)。
+ * 同样用**已保存**配置——改完 URL 先保存再探测。
+ */
+export function testPush(
+ slug: string,
+ config?: RequestOptions
+): Promise {
+ return http.post(
+ CONFIG_ENDPOINTS.pushTest(slug),
+ undefined,
+ config
+ )
+}
+
+/**
+ * GET 的 brand 组(原样快照,可能缺键/类型漂移)→ 表单友好的完整值。
+ * 缺键补空串作表单中性值:提交空串 = 显式清空覆盖出厂(出厂
+ * title/color/avatar 本非空——"AI Support"/"#066fc0"/内置头像),快照
+ * 缺省键则由消费侧深合并回落出厂;UI 所见即所得。topics 只留字符串项。
+ */
+export function normalizeBrandGroup(
+ value: Record | null | undefined
+): BrandGroupValue {
+ const source = value ?? {}
+ const str = (key: string): string => {
+ const v = source[key]
+ return typeof v === 'string' ? v : ''
+ }
+ const topics = source.topics
+ return {
+ title: str('title'),
+ color: str('color'),
+ avatar: str('avatar'),
+ welcome: str('welcome'),
+ placeholder: str('placeholder'),
+ whatsapp: str('whatsapp'),
+ email: str('email'),
+ privacy_url: str('privacy_url'),
+ topics: Array.isArray(topics)
+ ? topics.filter((t): t is string => typeof t === 'string')
+ : []
+ }
+}
diff --git a/admin/src/api/conversations.ts b/admin/src/api/conversations.ts
new file mode 100644
index 0000000..0e386ec
--- /dev/null
+++ b/admin/src/api/conversations.ts
@@ -0,0 +1,239 @@
+/**
+ * 会话与线索 API(Phase 3 Task 3 骨架;Task 5 实装调用函数并按实际响应修正类型)
+ *
+ * 端点与响应事实源:app/platform/routes/data_routes.py
+ * - 列表:{total, page, page_size, items},行含 preview/intent_level/contact_info/
+ * email_sent/message_count/country/site 等;筛选 intent 白名单(interested/
+ * not_interested/pending,非法 400)/site 精确/client_info["site"]/from-to 闭区间
+ * (日期-only 的 to 含当日全天)
+ * - 详情:扁平结构(元数据 + evaluation/profile/messages 三块),消息行字段为
+ * time(非 created_at);profile.chips 为 raw_info 去 _ 前缀键
+ * - 删除:{ok: true};不存在 404(级联消息/画像/评估/推送日志)
+ * - push-logs:列表不回大 payload(load_only 排除 request_payload),带
+ * payload_size 服务端长度标记;status 白名单 pending/success/failed/skipped
+ */
+import { http } from './client'
+
+/** 意向层级(Evaluation.intent_level 枚举;列表筛选白名单同值) */
+export type IntentLevel = 'interested' | 'not_interested' | 'pending'
+
+/** 推送状态(LeadPushLog.status;筛选白名单同值) */
+export type PushLogStatus = 'pending' | 'success' | 'failed' | 'skipped'
+
+/** 联系方式(评估产出,留资三选一) */
+export interface ContactInfo {
+ email?: string | null
+ whatsapp?: string | null
+ phone?: string | null
+}
+
+/** 会话列表行(GET /api/platform/tenants/{slug}/conversations) */
+export interface ConversationListItem {
+ id: string
+ session_id: string
+ status: string
+ source: string
+ site: string | null
+ ip: string
+ device: string
+ country: string | null
+ intent_level: IntentLevel | null
+ reasoning: string | null
+ contact_info: ContactInfo | null
+ email_sent: boolean
+ message_count: number
+ preview: string
+ created_at: string | null
+}
+
+/** 会话列表查询参数(intent 白名单校验在后端;from/to 为闭区间日期 YYYY-MM-DD) */
+export interface ConversationListParams {
+ intent?: IntentLevel
+ site?: string
+ from?: string
+ to?: string
+ page?: number
+ page_size?: number
+}
+
+/** 会话列表响应 */
+export interface ConversationListResponse {
+ total: number
+ page: number
+ page_size: number
+ items: ConversationListItem[]
+}
+
+/** 会话详情消息行(时间升序;字段为 time) */
+export interface ConversationMessage {
+ role: 'user' | 'assistant' | 'system' | string
+ content: string
+ time: string | null
+}
+
+/** 评估块(详情内嵌,未评估为 null) */
+export interface ConversationEvaluation {
+ intent_level: IntentLevel | null
+ reasoning: string | null
+ contact_info: ContactInfo | null
+ email_sent: boolean
+ correction_count: number | null
+ evaluated_at: string | null
+}
+
+/** 会话详情响应(GET .../conversations/{convId}——扁平,与列表行不同构) */
+export interface ConversationDetail {
+ id: string
+ session_id: string
+ status: string
+ source: string
+ client_info: Record
+ site: string | null
+ ip: string
+ device: string
+ country: string | null
+ created_at: string | null
+ updated_at: string | null
+ evaluation: ConversationEvaluation | null
+ profile: {
+ chips: Record
+ summary: string | null
+ } | null
+ messages: ConversationMessage[]
+}
+
+/** 推送日志行(GET .../push-logs——无 request_payload,仅 payload_size 长度标记) */
+export interface PushLogItem {
+ id: string
+ evaluation_id: string
+ conversation_id: string
+ push_type: string
+ status: PushLogStatus | string
+ attempts: number
+ outcome: string | null
+ response_status: number | null
+ error_message: string | null
+ payload_size: number | null
+ created_at: string | null
+ updated_at: string | null
+}
+
+/** 推送日志查询参数(status 白名单校验在后端) */
+export interface PushLogParams {
+ status?: PushLogStatus
+ conversation_id?: string
+ page?: number
+ page_size?: number
+}
+
+/** 推送日志响应 */
+export interface PushLogResponse {
+ total: number
+ page: number
+ page_size: number
+ items: PushLogItem[]
+}
+
+/** 会话与线索端点 */
+export const CONVERSATION_ENDPOINTS = {
+ list: (slug: string) => `/api/platform/tenants/${slug}/conversations`,
+ detail: (slug: string, convId: string) =>
+ `/api/platform/tenants/${slug}/conversations/${convId}`,
+ remove: (slug: string, convId: string) =>
+ `/api/platform/tenants/${slug}/conversations/${convId}`,
+ pushLogs: (slug: string) => `/api/platform/tenants/${slug}/push-logs`
+} as const
+
+// ---- 展示映射(枚举值事实源 data_routes.py INTENTS/PUSH_STATUSES 与 models.py) ----
+
+/** 意向筛选/展示选项 */
+export const INTENT_OPTIONS: Array<{ value: IntentLevel; label: string }> = [
+ { value: 'interested', label: '有意向' },
+ { value: 'not_interested', label: '无意向' },
+ { value: 'pending', label: '待定' }
+]
+
+/** 意向展示文案(未知值回退原样) */
+export function intentLabel(level: string | null | undefined): string {
+ if (!level) return '未评估'
+ return INTENT_OPTIONS.find((item) => item.value === level)?.label ?? level
+}
+
+/** 推送状态筛选/展示选项 */
+export const PUSH_STATUS_OPTIONS: Array<{ value: PushLogStatus; label: string }> = [
+ { value: 'pending', label: '待推送' },
+ { value: 'success', label: '成功' },
+ { value: 'failed', label: '失败' },
+ { value: 'skipped', label: '跳过' }
+]
+
+/** 推送状态展示文案 */
+export function pushStatusLabel(status: string | null | undefined): string {
+ if (!status) return '—'
+ return PUSH_STATUS_OPTIONS.find((item) => item.value === status)?.label ?? status
+}
+
+/** 推送类型展示文案(LeadPushLog.push_type:lead/returning/correction) */
+export function pushTypeLabel(type: string | null | undefined): string {
+ const labels: Record = {
+ lead: '新线索',
+ returning: '回访',
+ correction: '更正'
+ }
+ if (!type) return '—'
+ return labels[type] ?? type
+}
+
+/** 会话状态展示文案(ConversationStatus) */
+export function conversationStatusLabel(status: string | null | undefined): string {
+ const labels: Record = {
+ active: '进行中',
+ completed: '已完成',
+ abandoned: '已放弃'
+ }
+ if (!status) return '—'
+ return labels[status] ?? status
+}
+
+/** 会话来源展示文案(ConversationSource) */
+export function conversationSourceLabel(source: string | null | undefined): string {
+ const labels: Record = {
+ standalone: '独立站',
+ embedded: '内嵌页',
+ widget: '挂件',
+ app: '应用'
+ }
+ if (!source) return '—'
+ return labels[source] ?? source
+}
+
+// ---- 调用函数 -------------------------------------------------------------------
+
+/** 会话列表(分页 + intent/site/日期区间筛选) */
+export function fetchConversations(
+ slug: string,
+ params?: ConversationListParams
+): Promise {
+ return http.get(CONVERSATION_ENDPOINTS.list(slug), { params })
+}
+
+/** 会话详情(元数据 + 画像 chips + 评估 + 消息全量) */
+export function fetchConversationDetail(
+ slug: string,
+ convId: string
+): Promise {
+ return http.get(CONVERSATION_ENDPOINTS.detail(slug, convId))
+}
+
+/** 删除会话(级联消息/画像/评估/推送日志;不存在 404) */
+export function deleteConversation(
+ slug: string,
+ convId: string
+): Promise<{ ok: boolean }> {
+ return http.del<{ ok: boolean }>(CONVERSATION_ENDPOINTS.remove(slug, convId))
+}
+
+/** 推送日志列表(status/conversation_id 筛选 + 分页;不回大 payload) */
+export function fetchPushLogs(slug: string, params?: PushLogParams): Promise {
+ return http.get(CONVERSATION_ENDPOINTS.pushLogs(slug), { params })
+}
diff --git a/admin/src/api/mask.ts b/admin/src/api/mask.ts
new file mode 100644
index 0000000..60efdfc
--- /dev/null
+++ b/admin/src/api/mask.ts
@@ -0,0 +1,44 @@
+/**
+ * 掩码哨兵辅助(Phase 3 Task 3)
+ *
+ * 契约对齐后端 app/platform/routes/config_routes.py:
+ * - 密钥字段 GET 响应:库中非空 → `__SET__`(MASKED 哨兵);空/缺省 → 原样
+ * - PUT 提交:密钥字段传 `__SET__` = 保持库值(后端还原,哨兵字面量绝不落库)
+ *
+ * 本模块提供哨兵 ↔ 表单语义的纯函数转换,配置中心(Task 7)与
+ * 平台设置(Task 9)的凭据表单消费:
+ * - secretPlaceholder:GET 掩码值 → 输入框占位文案("已设置,输入新值覆盖")
+ * - secretToSubmit:用户输入 + 原掩码状态 → 提交值(未改动回哨兵保持库值)
+ */
+
+/** 后端密钥掩码哨兵(config_routes.MASKED) */
+export const MASKED_SENTINEL = '__SET__'
+
+/** 表单侧"已设置"展示语义 */
+export const MASKED_DISPLAY = '已设置'
+
+/** 值是否为掩码哨兵 */
+export function isMaskedSentinel(value: unknown): value is typeof MASKED_SENTINEL {
+ return value === MASKED_SENTINEL
+}
+
+/**
+ * GET 掩码值 → 表单占位文案
+ *
+ * @param maskedValue 后端下发的密钥字段值(哨兵=已设置;空串/null/undefined=未设置)
+ */
+export function secretPlaceholder(maskedValue: string | null | undefined): string {
+ return isMaskedSentinel(maskedValue) ? '已设置,输入新值覆盖' : '未设置,输入新值'
+}
+
+/**
+ * 表单输入 → 提交值
+ *
+ * @param input 用户输入(空串=未改动/清空)
+ * @param originalMasked GET 时的掩码值(哨兵=库中已有值;否则=未设置)
+ * @returns 提交值:输入非空 → 新值;输入空且已设置 → 哨兵(保持库值);输入空且未设置 → 空串
+ */
+export function secretToSubmit(input: string, originalMasked: string | null | undefined): string {
+ if (input !== '') return input
+ return isMaskedSentinel(originalMasked) ? MASKED_SENTINEL : ''
+}
diff --git a/admin/src/api/settings.ts b/admin/src/api/settings.ts
new file mode 100644
index 0000000..625705b
--- /dev/null
+++ b/admin/src/api/settings.ts
@@ -0,0 +1,70 @@
+/**
+ * 平台设置 API(Phase 3 Task 3 骨架;Task 9 实装调用函数)
+ *
+ * 端点事实源:app/platform/routes/config_routes.py 的 settings_router
+ * (/api/platform/settings,仅超管:集团 AI 凭据 / 默认 SMTP 等)。
+ *
+ * 形态(GET 列表 / PUT 单键 upsert——与 config 组的"整组替换"不同,
+ * 平台设置是 KV 表,逐键写):
+ * - GET → {items: [{key, value}]}(按 key 升序;value 任意 JSON——
+ * 后端 Text 列 json.dumps 直存,读回还原类型)
+ * - PUT → body {key, value}:同 key 再写 = 整值替换;响应 {key, value}
+ * (掩码视图)
+ *
+ * 密钥掩码:key 名含 key/password/secret 的非空字符串值在响应中
+ * 掩码为 '__SET__'(嵌套 dict 递归);PUT 提交 '__SET__' = 保持库值。
+ * 表单语义转换见 src/api/mask.ts。
+ *
+ * 注意(CLAUDE.md Phase 3 取舍):resolver 的平台默认回退链**尚未**接入
+ * platform_settings——本端点现阶段只做存储与展示,引擎不读取这些值。
+ */
+
+import { http, type RequestOptions } from './client'
+
+/** 平台设置项(列表响应行;value 可能是掩码哨兵或任意 JSON) */
+export interface PlatformSettingItem {
+ key: string
+ value: unknown
+}
+
+/** 平台设置列表响应 */
+export interface PlatformSettingsResponse {
+ items: PlatformSettingItem[]
+}
+
+/** upsert 单个设置项请求(同 key 再写 = 整值替换) */
+export interface PutPlatformSettingRequest {
+ key: string
+ value: unknown
+}
+
+/** 平台设置端点 */
+export const SETTINGS_ENDPOINTS = {
+ list: '/api/platform/settings',
+ put: '/api/platform/settings'
+} as const
+
+/**
+ * 平台设置列表(GET /api/platform/settings,仅超管):按 key 升序的
+ * 全量 KV。value 为掩码视图——密钥键(key 名含 key/password/secret)
+ * 的非空字符串值已是 '__SET__' 哨兵,调用方经 model 层的 normalize
+ * 转表单态、经 formatSettingValue 做展示,绝不久存原文。
+ */
+export function fetchPlatformSettings(
+ config?: RequestOptions
+): Promise {
+ return http.get(SETTINGS_ENDPOINTS.list, config)
+}
+
+/**
+ * upsert 单个设置项(PUT /api/platform/settings):同 key 再写 = 整值
+ * 替换(无部分合并)。密钥键提交 '__SET__' 哨兵 = 保持库值(后端还原,
+ * 哨兵字面量绝不落库)。响应 {key, value} 为掩码视图。
+ * 本页按"仅提交脏键"调用——未改动 的键不发请求。
+ */
+export function putPlatformSetting(
+ body: PutPlatformSettingRequest,
+ config?: RequestOptions
+): Promise {
+ return http.put(SETTINGS_ENDPOINTS.put, body, config)
+}
diff --git a/admin/src/api/stats.ts b/admin/src/api/stats.ts
new file mode 100644
index 0000000..43aeb63
--- /dev/null
+++ b/admin/src/api/stats.ts
@@ -0,0 +1,99 @@
+/**
+ * 统计分析 API(Phase 3 Task 4 实装调用函数;类型与端点 Task 3 建立)
+ *
+ * 端点事实源:app/platform/routes/stats_routes.py + app/platform/stats.py
+ * (平台合并统计 / 单租户统计,?from/&to= 闭区间)。
+ *
+ * 指标口径事实源:app/platform/stats.py 模块 docstring 的口径表——
+ * 会话数不要求有消息 / 线索=留资三选一(phone/whatsapp/email)任一非空 /
+ * 高意向=评估 interested / 推送成功率 success/(success+failed)(分母 0 →
+ * null,前端展示"—")。Task 8(统计分析页)复用本组函数。
+ */
+import { http } from './client'
+
+/** 指标卡(None 项为 null——如无推送时成功率无定义) */
+export interface StatsCards {
+ conversations: number
+ leads: number
+ high_intent: number
+ push_success_rate: number | null
+}
+
+/** 漏斗计数 */
+export interface StatsFunnel {
+ conversations: number
+ leads: number
+ evaluated: number
+ push_succeeded: number
+}
+
+/** 按日趋势行 */
+export interface StatsTrendPoint {
+ date: string
+ conversations: number
+ leads: number
+}
+
+/** 分布视图(intent 按枚举键、source/site 计数表、country Top 数组) */
+export interface StatsDistributions {
+ intent: Record
+ source: Record
+ site: Record
+ country: Array<{ country: string; count: number }>
+}
+
+/** 推送健康计数 */
+export interface PushHealth {
+ pending: number
+ success: number
+ failed: number
+ skipped: number
+ duplicated: number
+}
+
+/** 统计响应(平台合并与单租户同构) */
+export interface StatsResponse {
+ cards: StatsCards
+ funnel: StatsFunnel
+ trend: StatsTrendPoint[]
+ distributions: StatsDistributions
+ push_health: PushHealth
+}
+
+/** 统计查询参数(from/to 闭区间,ISO 日期) */
+export interface StatsParams {
+ from?: string
+ to?: string
+}
+
+/** 统计端点(platform 合并版仅超管;单租户版超管或本租户管理员) */
+export const STATS_ENDPOINTS = {
+ platform: '/api/platform/stats',
+ tenant: (slug: string) => `/api/platform/tenants/${slug}/stats`
+} as const
+
+/** 平台合并统计查询参数(?tenant= 单租户下钻,未知 slug 404) */
+export interface PlatformStatsParams extends StatsParams {
+ tenant?: string
+}
+
+/**
+ * 平台统计(仅超管):缺省全部租户合并(逐库查询应用层合并);
+ * tenant=slug 单租户下钻——仪表盘租户卡片墙逐租户调用即此形态。
+ * options.showErrorMessage: false 关全局错误提示(卡片墙 N 连发的失败
+ * toast 改由调用方逐卡内联展示)
+ */
+export function fetchPlatformStats(
+ params?: PlatformStatsParams,
+ options?: { showErrorMessage?: boolean }
+): Promise {
+ return http.get(STATS_ENDPOINTS.platform, { params, ...options })
+}
+
+/**
+ * 单租户统计(本租户管理员或超管):租户管理员的"自己指标卡"走这里,
+ * slug 取登录态 tenant_slug(auth store)
+ */
+export function fetchTenantStats(slug: string, params?: StatsParams): Promise {
+ return http.get(STATS_ENDPOINTS.tenant(slug), { params })
+}
diff --git a/admin/src/api/tenants.ts b/admin/src/api/tenants.ts
new file mode 100644
index 0000000..e034dbc
--- /dev/null
+++ b/admin/src/api/tenants.ts
@@ -0,0 +1,218 @@
+/**
+ * 租户与站点管理 API(Phase 3 Task 3 骨架;Task 4 列表函数;Task 6 实装全部调用函数)
+ *
+ * 端点事实源:app/platform/routes/tenant_routes.py
+ * (/api/platform/tenants,require_super / require_tenant(slug) 鉴权)。
+ *
+ * 错误语义(页面交互消费):
+ * - 400 预检撞车带中文 detail:slug 已存在 / 域名已绑定(含归属租户与站点)/
+ * 出厂 push.prefix 被占(引导改 blank 或先改占用方)——向导与绑域名
+ * 弹窗用 showErrorMessage:false 关全局 toast、改内联常驻展示;
+ * - 409 并发竞态(提交瞬间被抢先);
+ * - PATCH 带 slug 键一律 400(slug 不可变——界面不提供改 slug 入口)。
+ */
+import { http, type RequestOptions } from './client'
+
+/** 租户状态(Tenant.status) */
+export type TenantStatus = 'active' | 'disabled'
+
+/** 站点概要(_site_summary:slug/name/status/domains/brand_overlay) */
+export interface SiteSummary {
+ slug: string
+ name: string
+ status: string
+ domains: string[]
+ brand_overlay: Record
+}
+
+/** 管理员概要(_admin_summary:不含任何凭据字段) */
+export interface TenantAdminSummary {
+ username: string
+ status: string
+}
+
+/** 租户列表行(GET /api/platform/tenants) */
+export interface TenantListItem {
+ slug: string
+ name: string
+ status: TenantStatus
+ domain_count: number
+ current_version: number | null
+ admins: TenantAdminSummary[]
+}
+
+/** 租户列表响应 */
+export interface TenantListResponse {
+ items: TenantListItem[]
+}
+
+/** 租户详情响应(GET /api/platform/tenants/{slug}) */
+export interface TenantDetailResponse {
+ slug: string
+ name: string
+ status: TenantStatus
+ sites: SiteSummary[]
+ admins: TenantAdminSummary[]
+ current_version: number | null
+}
+
+/** 创建租户请求(POST /api/platform/tenants,201) */
+export interface CreateTenantRequest {
+ name: string
+ slug: string
+ /** 出厂档案:auto | jiancai | blank */
+ profile: string
+ admin_username: string
+ admin_password: string
+ domains: string[]
+}
+
+/** 改名/改状态请求(PATCH /api/platform/tenants/{slug}) */
+export interface PatchTenantRequest {
+ name?: string
+ status?: TenantStatus
+}
+
+/** 创建站点请求(POST /api/platform/tenants/{slug}/sites,201) */
+export interface CreateSiteRequest {
+ slug: string
+ name?: string
+ brand_overlay?: Record
+ domains?: string[]
+}
+
+/** 创建租户响应(create_tenant:201 返回概要;管理员凭据绝不回传) */
+export interface TenantCreatedResponse {
+ slug: string
+ name: string
+ status: TenantStatus
+ profile: string
+ version: number
+ site: { slug: string; domains: string[] }
+ admin_username: string
+}
+
+/** 改名/改状态响应(patch_tenant / patch_site 返回形状) */
+export interface TenantPatchResponse {
+ slug: string
+ name: string
+ status: TenantStatus
+}
+
+/** 改站点请求(PATCH .../sites/{siteSlug};slug 不可变——后端 400) */
+export interface PatchSiteRequest {
+ name?: string
+ status?: TenantStatus
+ brand_overlay?: Record
+}
+
+/** 绑定域名请求(POST .../sites/{siteSlug}/domains,201;400=撞车带归属) */
+export interface AddDomainRequest {
+ domain: string
+}
+
+/** 绑定域名响应 */
+export interface DomainBindResponse {
+ domain: string
+ site: string
+}
+
+/** 解绑域名响应 */
+export interface DomainUnbindResponse {
+ ok: boolean
+ domain: string
+ site: string
+}
+
+/** 租户管理端点(鉴权与 409 并发冲突语义见 tenant_routes.py) */
+export const TENANT_ENDPOINTS = {
+ list: '/api/platform/tenants',
+ create: '/api/platform/tenants',
+ detail: (slug: string) => `/api/platform/tenants/${slug}`,
+ patch: (slug: string) => `/api/platform/tenants/${slug}`,
+ createSite: (slug: string) => `/api/platform/tenants/${slug}/sites`,
+ patchSite: (slug: string, siteSlug: string) => `/api/platform/tenants/${slug}/sites/${siteSlug}`,
+ addDomain: (slug: string, siteSlug: string) =>
+ `/api/platform/tenants/${slug}/sites/${siteSlug}/domains`,
+ deleteDomain: (slug: string, siteSlug: string, domain: string) =>
+ `/api/platform/tenants/${slug}/sites/${siteSlug}/domains/${domain}`
+} as const
+
+/**
+ * 租户列表(仅超管)——Task 4 仪表盘的租户卡片墙需要枚举租户;
+ * 列表行的 slug/name/status 供卡片展示与逐租户统计下钻
+ */
+export function fetchTenantList(): Promise {
+ return http.get(TENANT_ENDPOINTS.list)
+}
+
+/** 租户详情(含 sites/domains/brand_overlay/admins;本租户管理员亦可读) */
+export function fetchTenantDetail(
+ slug: string,
+ config?: RequestOptions
+): Promise {
+ return http.get(TENANT_ENDPOINTS.detail(slug), config)
+}
+
+/**
+ * 创建租户(创建向导提交;201)。向导调用建议传
+ * `{ showErrorMessage: false }`——400 预检撞车 detail(slug 已存在 /
+ * 出厂 push.prefix 被占的改 blank 引导)在向导内联常驻展示。
+ */
+export function createTenant(
+ body: CreateTenantRequest,
+ config?: RequestOptions
+): Promise {
+ return http.post(TENANT_ENDPOINTS.create, body, config)
+}
+
+/** 改名 / 停用 / 复启用(PATCH;slug 不可变,body 不含 slug 键) */
+export function patchTenant(
+ slug: string,
+ body: PatchTenantRequest,
+ config?: RequestOptions
+): Promise {
+ return http.patch(TENANT_ENDPOINTS.patch(slug), body, config)
+}
+
+/** 新建站点(brand_overlay 只放显式覆盖的字段——留空=继承租户默认) */
+export function createSite(
+ slug: string,
+ body: CreateSiteRequest,
+ config?: RequestOptions
+): Promise {
+ return http.post(TENANT_ENDPOINTS.createSite(slug), body, config)
+}
+
+/** 改站点(停用/复启用/改名/改覆盖层;整字典赋值——旧键不残留) */
+export function patchSite(
+ slug: string,
+ siteSlug: string,
+ body: PatchSiteRequest,
+ config?: RequestOptions
+): Promise {
+ return http.patch(TENANT_ENDPOINTS.patchSite(slug, siteSlug), body, config)
+}
+
+/** 绑定域名(全局唯一;400 detail 带现有归属"租户 x 的站点 y") */
+export function addSiteDomain(
+ slug: string,
+ siteSlug: string,
+ body: AddDomainRequest,
+ config?: RequestOptions
+): Promise {
+ return http.post(TENANT_ENDPOINTS.addDomain(slug, siteSlug), body, config)
+}
+
+/** 解绑域名(只解本站点的该域名;不存在 404) */
+export function deleteSiteDomain(
+ slug: string,
+ siteSlug: string,
+ domain: string,
+ config?: RequestOptions
+): Promise {
+ return http.del(
+ TENANT_ENDPOINTS.deleteDomain(slug, siteSlug, domain),
+ config
+ )
+}
diff --git a/admin/src/assets/images/3d/icon1.webp b/admin/src/assets/images/3d/icon1.webp
new file mode 100644
index 0000000..631094c
Binary files /dev/null and b/admin/src/assets/images/3d/icon1.webp differ
diff --git a/admin/src/assets/images/3d/icon2.webp b/admin/src/assets/images/3d/icon2.webp
new file mode 100644
index 0000000..a9e4742
Binary files /dev/null and b/admin/src/assets/images/3d/icon2.webp differ
diff --git a/admin/src/assets/images/3d/icon3.webp b/admin/src/assets/images/3d/icon3.webp
new file mode 100644
index 0000000..5e5994d
Binary files /dev/null and b/admin/src/assets/images/3d/icon3.webp differ
diff --git a/admin/src/assets/images/3d/icon4.webp b/admin/src/assets/images/3d/icon4.webp
new file mode 100644
index 0000000..522108f
Binary files /dev/null and b/admin/src/assets/images/3d/icon4.webp differ
diff --git a/admin/src/assets/images/3d/icon5.webp b/admin/src/assets/images/3d/icon5.webp
new file mode 100644
index 0000000..b8f794d
Binary files /dev/null and b/admin/src/assets/images/3d/icon5.webp differ
diff --git a/admin/src/assets/images/3d/icon6.webp b/admin/src/assets/images/3d/icon6.webp
new file mode 100644
index 0000000..c955494
Binary files /dev/null and b/admin/src/assets/images/3d/icon6.webp differ
diff --git a/admin/src/assets/images/3d/icon7.webp b/admin/src/assets/images/3d/icon7.webp
new file mode 100644
index 0000000..8aa26ee
Binary files /dev/null and b/admin/src/assets/images/3d/icon7.webp differ
diff --git a/admin/src/assets/images/3d/icon8.webp b/admin/src/assets/images/3d/icon8.webp
new file mode 100644
index 0000000..609c4ee
Binary files /dev/null and b/admin/src/assets/images/3d/icon8.webp differ
diff --git a/admin/src/assets/images/avatar/avatar.webp b/admin/src/assets/images/avatar/avatar.webp
new file mode 100644
index 0000000..bea307b
Binary files /dev/null and b/admin/src/assets/images/avatar/avatar.webp differ
diff --git a/admin/src/assets/images/avatar/avatar1.webp b/admin/src/assets/images/avatar/avatar1.webp
new file mode 100644
index 0000000..68e256c
Binary files /dev/null and b/admin/src/assets/images/avatar/avatar1.webp differ
diff --git a/admin/src/assets/images/avatar/avatar10.webp b/admin/src/assets/images/avatar/avatar10.webp
new file mode 100644
index 0000000..a813d4c
Binary files /dev/null and b/admin/src/assets/images/avatar/avatar10.webp differ
diff --git a/admin/src/assets/images/avatar/avatar2.webp b/admin/src/assets/images/avatar/avatar2.webp
new file mode 100644
index 0000000..6716e3f
Binary files /dev/null and b/admin/src/assets/images/avatar/avatar2.webp differ
diff --git a/admin/src/assets/images/avatar/avatar3.webp b/admin/src/assets/images/avatar/avatar3.webp
new file mode 100644
index 0000000..7355ad4
Binary files /dev/null and b/admin/src/assets/images/avatar/avatar3.webp differ
diff --git a/admin/src/assets/images/avatar/avatar4.webp b/admin/src/assets/images/avatar/avatar4.webp
new file mode 100644
index 0000000..56a9549
Binary files /dev/null and b/admin/src/assets/images/avatar/avatar4.webp differ
diff --git a/admin/src/assets/images/avatar/avatar5.webp b/admin/src/assets/images/avatar/avatar5.webp
new file mode 100644
index 0000000..f78400c
Binary files /dev/null and b/admin/src/assets/images/avatar/avatar5.webp differ
diff --git a/admin/src/assets/images/avatar/avatar6.webp b/admin/src/assets/images/avatar/avatar6.webp
new file mode 100644
index 0000000..9771b78
Binary files /dev/null and b/admin/src/assets/images/avatar/avatar6.webp differ
diff --git a/admin/src/assets/images/avatar/avatar7.webp b/admin/src/assets/images/avatar/avatar7.webp
new file mode 100644
index 0000000..e5ef6fe
Binary files /dev/null and b/admin/src/assets/images/avatar/avatar7.webp differ
diff --git a/admin/src/assets/images/avatar/avatar8.webp b/admin/src/assets/images/avatar/avatar8.webp
new file mode 100644
index 0000000..b66e48f
Binary files /dev/null and b/admin/src/assets/images/avatar/avatar8.webp differ
diff --git a/admin/src/assets/images/avatar/avatar9.webp b/admin/src/assets/images/avatar/avatar9.webp
new file mode 100644
index 0000000..7974139
Binary files /dev/null and b/admin/src/assets/images/avatar/avatar9.webp differ
diff --git a/admin/src/assets/images/ceremony/hb.png b/admin/src/assets/images/ceremony/hb.png
new file mode 100644
index 0000000..4103324
Binary files /dev/null and b/admin/src/assets/images/ceremony/hb.png differ
diff --git a/admin/src/assets/images/ceremony/sd.png b/admin/src/assets/images/ceremony/sd.png
new file mode 100644
index 0000000..75ec838
Binary files /dev/null and b/admin/src/assets/images/ceremony/sd.png differ
diff --git a/admin/src/assets/images/ceremony/xc.png b/admin/src/assets/images/ceremony/xc.png
new file mode 100644
index 0000000..9c7ab67
Binary files /dev/null and b/admin/src/assets/images/ceremony/xc.png differ
diff --git a/admin/src/assets/images/ceremony/yd.png b/admin/src/assets/images/ceremony/yd.png
new file mode 100644
index 0000000..426912d
Binary files /dev/null and b/admin/src/assets/images/ceremony/yd.png differ
diff --git a/admin/src/assets/images/common/logo-large.png b/admin/src/assets/images/common/logo-large.png
new file mode 100644
index 0000000..29a6a42
Binary files /dev/null and b/admin/src/assets/images/common/logo-large.png differ
diff --git a/admin/src/assets/images/common/logo.png b/admin/src/assets/images/common/logo.png
new file mode 100644
index 0000000..11cba74
Binary files /dev/null and b/admin/src/assets/images/common/logo.png differ
diff --git a/admin/src/assets/images/cover/img1.webp b/admin/src/assets/images/cover/img1.webp
new file mode 100644
index 0000000..ebcd0c5
Binary files /dev/null and b/admin/src/assets/images/cover/img1.webp differ
diff --git a/admin/src/assets/images/cover/img10.webp b/admin/src/assets/images/cover/img10.webp
new file mode 100644
index 0000000..c6f53e3
Binary files /dev/null and b/admin/src/assets/images/cover/img10.webp differ
diff --git a/admin/src/assets/images/cover/img2.webp b/admin/src/assets/images/cover/img2.webp
new file mode 100644
index 0000000..747580f
Binary files /dev/null and b/admin/src/assets/images/cover/img2.webp differ
diff --git a/admin/src/assets/images/cover/img3.webp b/admin/src/assets/images/cover/img3.webp
new file mode 100644
index 0000000..4fbe638
Binary files /dev/null and b/admin/src/assets/images/cover/img3.webp differ
diff --git a/admin/src/assets/images/cover/img4.webp b/admin/src/assets/images/cover/img4.webp
new file mode 100644
index 0000000..2dd1139
Binary files /dev/null and b/admin/src/assets/images/cover/img4.webp differ
diff --git a/admin/src/assets/images/cover/img5.webp b/admin/src/assets/images/cover/img5.webp
new file mode 100644
index 0000000..392f0bd
Binary files /dev/null and b/admin/src/assets/images/cover/img5.webp differ
diff --git a/admin/src/assets/images/cover/img6.webp b/admin/src/assets/images/cover/img6.webp
new file mode 100644
index 0000000..bf3537a
Binary files /dev/null and b/admin/src/assets/images/cover/img6.webp differ
diff --git a/admin/src/assets/images/cover/img7.webp b/admin/src/assets/images/cover/img7.webp
new file mode 100644
index 0000000..bebbade
Binary files /dev/null and b/admin/src/assets/images/cover/img7.webp differ
diff --git a/admin/src/assets/images/cover/img8.webp b/admin/src/assets/images/cover/img8.webp
new file mode 100644
index 0000000..0b38bfc
Binary files /dev/null and b/admin/src/assets/images/cover/img8.webp differ
diff --git a/admin/src/assets/images/cover/img9.webp b/admin/src/assets/images/cover/img9.webp
new file mode 100644
index 0000000..b87cca6
Binary files /dev/null and b/admin/src/assets/images/cover/img9.webp differ
diff --git a/admin/src/assets/images/draw/draw1.png b/admin/src/assets/images/draw/draw1.png
new file mode 100644
index 0000000..da5d87a
Binary files /dev/null and b/admin/src/assets/images/draw/draw1.png differ
diff --git a/admin/src/assets/images/lock/bg_dark.webp b/admin/src/assets/images/lock/bg_dark.webp
new file mode 100644
index 0000000..1c33435
Binary files /dev/null and b/admin/src/assets/images/lock/bg_dark.webp differ
diff --git a/admin/src/assets/images/lock/bg_light.webp b/admin/src/assets/images/lock/bg_light.webp
new file mode 100644
index 0000000..00efbd9
Binary files /dev/null and b/admin/src/assets/images/lock/bg_light.webp differ
diff --git a/admin/src/assets/images/login/lf_icon2.webp b/admin/src/assets/images/login/lf_icon2.webp
new file mode 100644
index 0000000..5e4f3fd
Binary files /dev/null and b/admin/src/assets/images/login/lf_icon2.webp differ
diff --git a/admin/src/assets/images/safeguard/server.png b/admin/src/assets/images/safeguard/server.png
new file mode 100644
index 0000000..d7eee9d
Binary files /dev/null and b/admin/src/assets/images/safeguard/server.png differ
diff --git a/admin/src/assets/images/settings/menu_layouts/dual_column.png b/admin/src/assets/images/settings/menu_layouts/dual_column.png
new file mode 100644
index 0000000..9b868ca
Binary files /dev/null and b/admin/src/assets/images/settings/menu_layouts/dual_column.png differ
diff --git a/admin/src/assets/images/settings/menu_layouts/horizontal.png b/admin/src/assets/images/settings/menu_layouts/horizontal.png
new file mode 100644
index 0000000..ca779bc
Binary files /dev/null and b/admin/src/assets/images/settings/menu_layouts/horizontal.png differ
diff --git a/admin/src/assets/images/settings/menu_layouts/mixed.png b/admin/src/assets/images/settings/menu_layouts/mixed.png
new file mode 100644
index 0000000..c82b580
Binary files /dev/null and b/admin/src/assets/images/settings/menu_layouts/mixed.png differ
diff --git a/admin/src/assets/images/settings/menu_layouts/vertical.png b/admin/src/assets/images/settings/menu_layouts/vertical.png
new file mode 100644
index 0000000..16e942b
Binary files /dev/null and b/admin/src/assets/images/settings/menu_layouts/vertical.png differ
diff --git a/admin/src/assets/images/settings/menu_styles/dark.png b/admin/src/assets/images/settings/menu_styles/dark.png
new file mode 100644
index 0000000..e1653b7
Binary files /dev/null and b/admin/src/assets/images/settings/menu_styles/dark.png differ
diff --git a/admin/src/assets/images/settings/menu_styles/design.png b/admin/src/assets/images/settings/menu_styles/design.png
new file mode 100644
index 0000000..7681aa8
Binary files /dev/null and b/admin/src/assets/images/settings/menu_styles/design.png differ
diff --git a/admin/src/assets/images/settings/menu_styles/light.png b/admin/src/assets/images/settings/menu_styles/light.png
new file mode 100644
index 0000000..3007b99
Binary files /dev/null and b/admin/src/assets/images/settings/menu_styles/light.png differ
diff --git a/admin/src/assets/images/settings/theme_styles/dark.png b/admin/src/assets/images/settings/theme_styles/dark.png
new file mode 100644
index 0000000..e8c6e44
Binary files /dev/null and b/admin/src/assets/images/settings/theme_styles/dark.png differ
diff --git a/admin/src/assets/images/settings/theme_styles/light.png b/admin/src/assets/images/settings/theme_styles/light.png
new file mode 100644
index 0000000..6754238
Binary files /dev/null and b/admin/src/assets/images/settings/theme_styles/light.png differ
diff --git a/admin/src/assets/images/settings/theme_styles/system.png b/admin/src/assets/images/settings/theme_styles/system.png
new file mode 100644
index 0000000..6a6baa9
Binary files /dev/null and b/admin/src/assets/images/settings/theme_styles/system.png differ
diff --git a/admin/src/assets/images/svg/403.svg b/admin/src/assets/images/svg/403.svg
new file mode 100644
index 0000000..68790ad
--- /dev/null
+++ b/admin/src/assets/images/svg/403.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/admin/src/assets/images/svg/404.svg b/admin/src/assets/images/svg/404.svg
new file mode 100644
index 0000000..48e1ca3
--- /dev/null
+++ b/admin/src/assets/images/svg/404.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/admin/src/assets/images/svg/500.svg b/admin/src/assets/images/svg/500.svg
new file mode 100644
index 0000000..512429f
--- /dev/null
+++ b/admin/src/assets/images/svg/500.svg
@@ -0,0 +1,5 @@
+
diff --git a/admin/src/assets/images/svg/login_icon.svg b/admin/src/assets/images/svg/login_icon.svg
new file mode 100644
index 0000000..4beb3ab
--- /dev/null
+++ b/admin/src/assets/images/svg/login_icon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/admin/src/assets/images/user/avatar.webp b/admin/src/assets/images/user/avatar.webp
new file mode 100644
index 0000000..6d7234b
Binary files /dev/null and b/admin/src/assets/images/user/avatar.webp differ
diff --git a/admin/src/assets/images/user/bg.webp b/admin/src/assets/images/user/bg.webp
new file mode 100644
index 0000000..762b22d
Binary files /dev/null and b/admin/src/assets/images/user/bg.webp differ
diff --git a/admin/src/assets/styles/core/app.scss b/admin/src/assets/styles/core/app.scss
new file mode 100644
index 0000000..c0efeed
--- /dev/null
+++ b/admin/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/admin/src/assets/styles/core/dark.scss b/admin/src/assets/styles/core/dark.scss
new file mode 100644
index 0000000..c52abc3
--- /dev/null
+++ b/admin/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/admin/src/assets/styles/core/el-dark.scss b/admin/src/assets/styles/core/el-dark.scss
new file mode 100644
index 0000000..8f81cdf
--- /dev/null
+++ b/admin/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/admin/src/assets/styles/core/el-light.scss b/admin/src/assets/styles/core/el-light.scss
new file mode 100644
index 0000000..ddf2bc5
--- /dev/null
+++ b/admin/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/admin/src/assets/styles/core/el-ui.scss b/admin/src/assets/styles/core/el-ui.scss
new file mode 100644
index 0000000..44429a1
--- /dev/null
+++ b/admin/src/assets/styles/core/el-ui.scss
@@ -0,0 +1,519 @@
+// 优化 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;
+ }
+}
diff --git a/admin/src/assets/styles/core/md.scss b/admin/src/assets/styles/core/md.scss
new file mode 100644
index 0000000..b22fdc2
--- /dev/null
+++ b/admin/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/admin/src/assets/styles/core/mixin.scss b/admin/src/assets/styles/core/mixin.scss
new file mode 100644
index 0000000..db36888
--- /dev/null
+++ b/admin/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/admin/src/assets/styles/core/reset.scss b/admin/src/assets/styles/core/reset.scss
new file mode 100644
index 0000000..17a3bcf
--- /dev/null
+++ b/admin/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/admin/src/assets/styles/core/router-transition.scss b/admin/src/assets/styles/core/router-transition.scss
new file mode 100644
index 0000000..f47c741
--- /dev/null
+++ b/admin/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/admin/src/assets/styles/core/tailwind.css b/admin/src/assets/styles/core/tailwind.css
new file mode 100644
index 0000000..1a9e22c
--- /dev/null
+++ b/admin/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/admin/src/assets/styles/core/theme-animation.scss b/admin/src/assets/styles/core/theme-animation.scss
new file mode 100644
index 0000000..377b945
--- /dev/null
+++ b/admin/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/admin/src/assets/styles/core/theme-change.scss b/admin/src/assets/styles/core/theme-change.scss
new file mode 100644
index 0000000..5b640d2
--- /dev/null
+++ b/admin/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/admin/src/assets/styles/custom/one-dark-pro.scss b/admin/src/assets/styles/custom/one-dark-pro.scss
new file mode 100644
index 0000000..36bdf63
--- /dev/null
+++ b/admin/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/admin/src/assets/styles/index.scss b/admin/src/assets/styles/index.scss
new file mode 100644
index 0000000..cdc2ddc
--- /dev/null
+++ b/admin/src/assets/styles/index.scss
@@ -0,0 +1,23 @@
+// 重置默认样式
+@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';
diff --git a/admin/src/assets/svg/loading.ts b/admin/src/assets/svg/loading.ts
new file mode 100644
index 0000000..fdfb078
--- /dev/null
+++ b/admin/src/assets/svg/loading.ts
@@ -0,0 +1,32 @@
+// 自定义四点旋转SVG
+export const fourDotsSpinnerSvg = `
+
+`
diff --git a/admin/src/components/business/SiteOverlayFields/index.vue b/admin/src/components/business/SiteOverlayFields/index.vue
new file mode 100644
index 0000000..20a9d00
--- /dev/null
+++ b/admin/src/components/business/SiteOverlayFields/index.vue
@@ -0,0 +1,76 @@
+
+
+
+
+
+
+
+
diff --git a/admin/src/components/business/SiteOverlayFields/model.ts b/admin/src/components/business/SiteOverlayFields/model.ts
new file mode 100644
index 0000000..e7350c1
--- /dev/null
+++ b/admin/src/components/business/SiteOverlayFields/model.ts
@@ -0,0 +1,89 @@
+/**
+ * 站点门面覆盖层字段模型(Task 6 建立;Task 7a 提取共用)
+ *
+ * 消费方:站点管理页(tenants/list)的「新建站点」与「编辑门面」弹窗——
+ * 两处共用同一字段清单 / 空表单 / 提交收集逻辑。
+ *
+ * 语义(与后端 tenant_routes.patch_site 的整字典赋值对齐):
+ * - buildOverlay 只收集非空字段——留空 = 剔除该键的覆盖 = 继承租户默认
+ * 品牌(brand 组);旧键不残留(整字典替换,M-2a)。
+ * - overlay 值是 brand 组子集(站点级覆盖,public.py 的 site.brand 深合并)。
+ */
+
+/** 门面覆盖层字符串字段(brand 组子集) */
+export const OVERLAY_FIELDS = [
+ { key: 'title', label: '标题 title' },
+ { key: 'color', label: '主题色 color' },
+ { key: 'avatar', label: '头像路径 avatar' },
+ { key: 'welcome', label: '欢迎语 welcome' },
+ { key: 'placeholder', label: '输入框占位 placeholder' },
+ { key: 'whatsapp', label: 'WhatsApp' },
+ { key: 'email', label: '邮箱 email' },
+ { key: 'privacy_url', label: '隐私政策 URL' }
+] as const
+
+/** 覆盖层字段 key 联合类型 */
+export type OverlayFieldKey = (typeof OVERLAY_FIELDS)[number]['key']
+
+/** 覆盖层表单状态(字符串字段全空串语义 + topics 数组) */
+export interface OverlayFormState extends Record {
+ topics: string[]
+}
+
+/** 覆盖层字段中文标签(站点表 tooltip 用;topics 单列) */
+export const OVERLAY_LABELS: Record = Object.fromEntries(
+ OVERLAY_FIELDS.map((f) => [f.key, f.label] as [string, string]).concat([
+ ['topics', '快捷话题 topics']
+ ])
+)
+
+/** 空表单(全留空 = 完全继承租户默认) */
+export function blankOverlayForm(): OverlayFormState {
+ return {
+ title: '',
+ color: '',
+ avatar: '',
+ welcome: '',
+ placeholder: '',
+ whatsapp: '',
+ email: '',
+ privacy_url: '',
+ topics: []
+ }
+}
+
+/**
+ * 表单 → brand_overlay 提交字典:只收集非空字段(留空 = 不覆盖)。
+ * 全空表单产出 {} = 清空覆盖层(整字典赋值语义下回到完全继承)。
+ */
+export function buildOverlay(form: OverlayFormState): Record {
+ const overlay: Record = {}
+ for (const field of OVERLAY_FIELDS) {
+ const value = form[field.key].trim()
+ if (value) overlay[field.key] = value
+ }
+ const topics = form.topics.map((t) => t.trim()).filter(Boolean)
+ if (topics.length > 0) overlay.topics = topics
+ return overlay
+}
+
+/**
+ * overlay 落库值 → 表单预填(编辑门面用):只认已知键的字符串值;
+ * 返回表单之外未能识别的键(保存时会被整字典替换丢弃,调用方提示用户)。
+ */
+export function overlayToForm(
+ overlay: Record | null | undefined
+): { form: OverlayFormState; unknownKeys: string[] } {
+ const form = blankOverlayForm()
+ const source = overlay ?? {}
+ for (const field of OVERLAY_FIELDS) {
+ const value = source[field.key]
+ if (typeof value === 'string') form[field.key] = value
+ }
+ if (Array.isArray(source.topics)) {
+ form.topics = source.topics.filter((t): t is string => typeof t === 'string')
+ }
+ const known = new Set([...OVERLAY_FIELDS.map((f) => f.key), 'topics'])
+ const unknownKeys = Object.keys(source).filter((k) => !known.has(k))
+ return { form, unknownKeys }
+}
diff --git a/admin/src/components/core/banners/art-basic-banner/index.vue b/admin/src/components/core/banners/art-basic-banner/index.vue
new file mode 100644
index 0000000..65b47e4
--- /dev/null
+++ b/admin/src/components/core/banners/art-basic-banner/index.vue
@@ -0,0 +1,343 @@
+
+
+
+
+
+
+
+
+
+
+
+ {{ title }}
+
+
+
+
+ {{
+ subtitle
+ }}
+
+
+
+
+
+ {{ buttonConfig?.text }}
+
+
+
+
+
+
+
+
![背景图片]()
+
+
+
+
+
+
+
diff --git a/admin/src/components/core/banners/art-card-banner/index.vue b/admin/src/components/core/banners/art-card-banner/index.vue
new file mode 100644
index 0000000..8a5f9d4
--- /dev/null
+++ b/admin/src/components/core/banners/art-card-banner/index.vue
@@ -0,0 +1,114 @@
+
+
+
+
+
+
![]()
+
+
+
{{ title }}
+
{{ description }}
+
+
+
+ {{ cancelButton?.text }}
+
+
+ {{ button?.text }}
+
+
+
+
+
+
+
diff --git a/admin/src/components/core/base/art-back-to-top/index.vue b/admin/src/components/core/base/art-back-to-top/index.vue
new file mode 100644
index 0000000..6f8da61
--- /dev/null
+++ b/admin/src/components/core/base/art-back-to-top/index.vue
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
diff --git a/admin/src/components/core/base/art-logo/index.vue b/admin/src/components/core/base/art-logo/index.vue
new file mode 100644
index 0000000..d5a4375
--- /dev/null
+++ b/admin/src/components/core/base/art-logo/index.vue
@@ -0,0 +1,52 @@
+
+
+
+
![all-kefu logo]()
+
+
+
+
+
+
diff --git a/admin/src/components/core/base/art-svg-icon/index.vue b/admin/src/components/core/base/art-svg-icon/index.vue
new file mode 100644
index 0000000..0bfcd0c
--- /dev/null
+++ b/admin/src/components/core/base/art-svg-icon/index.vue
@@ -0,0 +1,24 @@
+
+
+
+
+
+
diff --git a/admin/src/components/core/cards/art-bar-chart-card/index.vue b/admin/src/components/core/cards/art-bar-chart-card/index.vue
new file mode 100644
index 0000000..6815c2b
--- /dev/null
+++ b/admin/src/components/core/cards/art-bar-chart-card/index.vue
@@ -0,0 +1,103 @@
+
+
+
+
+
+
+ {{ value }}
+
+
{{ label }}
+
+
+ {{ percentage > 0 ? '+' : '' }}{{ percentage }}%
+
+
+ {{ date }}
+
+
+
+
+
+
+
diff --git a/admin/src/components/core/cards/art-data-list-card/index.vue b/admin/src/components/core/cards/art-data-list-card/index.vue
new file mode 100644
index 0000000..fc43323
--- /dev/null
+++ b/admin/src/components/core/cards/art-data-list-card/index.vue
@@ -0,0 +1,74 @@
+
+
+
+
+
{{ title }}
+
{{ subtitle }}
+
+
+
+
+
+
{{ item.title }}
+
{{ item.status }}
+
+
{{ item.time }}
+
+
+
查看更多
+
+
+
+
diff --git a/admin/src/components/core/cards/art-donut-chart-card/index.vue b/admin/src/components/core/cards/art-donut-chart-card/index.vue
new file mode 100644
index 0000000..df2dcbb
--- /dev/null
+++ b/admin/src/components/core/cards/art-donut-chart-card/index.vue
@@ -0,0 +1,124 @@
+
+
+
+
+
+
+
+ {{ title }}
+
+
+
+ {{ formatNumber(value) }}
+
+
+ {{ percentage > 0 ? '+' : '' }}{{ percentage }}%
+ {{ percentageLabel }}
+
+
+
+
+
+
+ {{ previousValue }}
+
+
+
+
+
+
+
+
+
+
diff --git a/admin/src/components/core/cards/art-image-card/index.vue b/admin/src/components/core/cards/art-image-card/index.vue
new file mode 100644
index 0000000..d27fe00
--- /dev/null
+++ b/admin/src/components/core/cards/art-image-card/index.vue
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+
+
+
+
+
+ {{ props.readTime }} 阅读
+
+
+
+
+
+ {{ props.category }}
+
+
{{ props.title }}
+
+
+
+ {{ props.views }}
+
+
+
+ {{ props.comments }}
+
+ {{ props.date }}
+
+
+
+
+
+
+
diff --git a/admin/src/components/core/cards/art-line-chart-card/index.vue b/admin/src/components/core/cards/art-line-chart-card/index.vue
new file mode 100644
index 0000000..e58c9b2
--- /dev/null
+++ b/admin/src/components/core/cards/art-line-chart-card/index.vue
@@ -0,0 +1,126 @@
+
+
+
+
+
+
+ {{ value }}
+
+
{{ label }}
+
+
+ {{ percentage > 0 ? '+' : '' }}{{ percentage }}%
+
+
+ {{ date }}
+
+
+
+
+
+
+
diff --git a/admin/src/components/core/cards/art-progress-card/index.vue b/admin/src/components/core/cards/art-progress-card/index.vue
new file mode 100644
index 0000000..048a836
--- /dev/null
+++ b/admin/src/components/core/cards/art-progress-card/index.vue
@@ -0,0 +1,86 @@
+
+
+
+
+
+
diff --git a/admin/src/components/core/cards/art-stats-card/index.vue b/admin/src/components/core/cards/art-stats-card/index.vue
new file mode 100644
index 0000000..8e0341b
--- /dev/null
+++ b/admin/src/components/core/cards/art-stats-card/index.vue
@@ -0,0 +1,67 @@
+
+
+
+
+
+
diff --git a/admin/src/components/core/cards/art-timeline-list-card/index.vue b/admin/src/components/core/cards/art-timeline-list-card/index.vue
new file mode 100644
index 0000000..fbb2c78
--- /dev/null
+++ b/admin/src/components/core/cards/art-timeline-list-card/index.vue
@@ -0,0 +1,69 @@
+
+
+
+
+
{{ title }}
+
{{ subtitle }}
+
+
+
+
+
+
+ {{ item.content }}
+ #{{ item.code }}
+
+
+
+
+
+
+
+
+
diff --git a/admin/src/components/core/charts/art-bar-chart/index.vue b/admin/src/components/core/charts/art-bar-chart/index.vue
new file mode 100644
index 0000000..d677196
--- /dev/null
+++ b/admin/src/components/core/charts/art-bar-chart/index.vue
@@ -0,0 +1,203 @@
+
+
+
+
+
+
diff --git a/admin/src/components/core/charts/art-dual-bar-compare-chart/index.vue b/admin/src/components/core/charts/art-dual-bar-compare-chart/index.vue
new file mode 100644
index 0000000..32aa60f
--- /dev/null
+++ b/admin/src/components/core/charts/art-dual-bar-compare-chart/index.vue
@@ -0,0 +1,195 @@
+
+
+
+
+
+
diff --git a/admin/src/components/core/charts/art-h-bar-chart/index.vue b/admin/src/components/core/charts/art-h-bar-chart/index.vue
new file mode 100644
index 0000000..2e34759
--- /dev/null
+++ b/admin/src/components/core/charts/art-h-bar-chart/index.vue
@@ -0,0 +1,208 @@
+
+
+
+
+
+
diff --git a/admin/src/components/core/charts/art-k-line-chart/index.vue b/admin/src/components/core/charts/art-k-line-chart/index.vue
new file mode 100644
index 0000000..0061b51
--- /dev/null
+++ b/admin/src/components/core/charts/art-k-line-chart/index.vue
@@ -0,0 +1,152 @@
+
+
+
+
+
+
diff --git a/admin/src/components/core/charts/art-line-chart/index.vue b/admin/src/components/core/charts/art-line-chart/index.vue
new file mode 100644
index 0000000..b70c2c3
--- /dev/null
+++ b/admin/src/components/core/charts/art-line-chart/index.vue
@@ -0,0 +1,371 @@
+
+
+
+
+
+
+
diff --git a/admin/src/components/core/charts/art-radar-chart/index.vue b/admin/src/components/core/charts/art-radar-chart/index.vue
new file mode 100644
index 0000000..e99fff6
--- /dev/null
+++ b/admin/src/components/core/charts/art-radar-chart/index.vue
@@ -0,0 +1,105 @@
+
+
+
+
+
+
diff --git a/admin/src/components/core/charts/art-ring-chart/index.vue b/admin/src/components/core/charts/art-ring-chart/index.vue
new file mode 100644
index 0000000..79115f7
--- /dev/null
+++ b/admin/src/components/core/charts/art-ring-chart/index.vue
@@ -0,0 +1,133 @@
+
+
+
+
+
+
+
diff --git a/admin/src/components/core/charts/art-scatter-chart/index.vue b/admin/src/components/core/charts/art-scatter-chart/index.vue
new file mode 100644
index 0000000..995b56a
--- /dev/null
+++ b/admin/src/components/core/charts/art-scatter-chart/index.vue
@@ -0,0 +1,115 @@
+
+
+
+
+
+
+
diff --git a/admin/src/components/core/forms/art-button-more/index.vue b/admin/src/components/core/forms/art-button-more/index.vue
new file mode 100644
index 0000000..858d305
--- /dev/null
+++ b/admin/src/components/core/forms/art-button-more/index.vue
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/admin/src/components/core/forms/art-button-table/index.vue b/admin/src/components/core/forms/art-button-table/index.vue
new file mode 100644
index 0000000..c849901
--- /dev/null
+++ b/admin/src/components/core/forms/art-button-table/index.vue
@@ -0,0 +1,59 @@
+
+
+
+
+
+
diff --git a/admin/src/components/core/forms/art-drag-verify/index.vue b/admin/src/components/core/forms/art-drag-verify/index.vue
new file mode 100644
index 0000000..5306e04
--- /dev/null
+++ b/admin/src/components/core/forms/art-drag-verify/index.vue
@@ -0,0 +1,430 @@
+
+
+
+
+
+
+
+
+
+
+ {{ message }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/admin/src/components/core/forms/art-form/index.vue b/admin/src/components/core/forms/art-form/index.vue
new file mode 100644
index 0000000..561e878
--- /dev/null
+++ b/admin/src/components/core/forms/art-form/index.vue
@@ -0,0 +1,507 @@
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.label }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('table.form.reset') }}
+
+
+ {{ t('table.form.submit') }}
+
+
+
+
+
+
+
+
+
+
diff --git a/admin/src/components/core/forms/art-search-bar/index.vue b/admin/src/components/core/forms/art-search-bar/index.vue
new file mode 100644
index 0000000..6786dca
--- /dev/null
+++ b/admin/src/components/core/forms/art-search-bar/index.vue
@@ -0,0 +1,581 @@
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.label }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/admin/src/components/core/layouts/art-breadcrumb/index.vue b/admin/src/components/core/layouts/art-breadcrumb/index.vue
new file mode 100644
index 0000000..4b54859
--- /dev/null
+++ b/admin/src/components/core/layouts/art-breadcrumb/index.vue
@@ -0,0 +1,142 @@
+
+
+
+
+
+
diff --git a/admin/src/components/core/layouts/art-chat-window/index.vue b/admin/src/components/core/layouts/art-chat-window/index.vue
new file mode 100644
index 0000000..f3d9471
--- /dev/null
+++ b/admin/src/components/core/layouts/art-chat-window/index.vue
@@ -0,0 +1,262 @@
+
+
+
+
+
+
+
Art Bot
+
+
+
{{ isOnline ? '在线' : '离线' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/admin/src/components/core/layouts/art-fast-enter/index.vue b/admin/src/components/core/layouts/art-fast-enter/index.vue
new file mode 100644
index 0000000..fdde222
--- /dev/null
+++ b/admin/src/components/core/layouts/art-fast-enter/index.vue
@@ -0,0 +1,113 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ application.name }}
+
{{ application.description }}
+
+
+
+
+
+
+
快速链接
+
+ -
+ {{ quickLink.name }}
+
+
+
+
+
+
+
+
diff --git a/admin/src/components/core/layouts/art-fireworks-effect/index.vue b/admin/src/components/core/layouts/art-fireworks-effect/index.vue
new file mode 100644
index 0000000..be85274
--- /dev/null
+++ b/admin/src/components/core/layouts/art-fireworks-effect/index.vue
@@ -0,0 +1,633 @@
+
+
+
+
+
+
diff --git a/admin/src/components/core/layouts/art-global-component/index.vue b/admin/src/components/core/layouts/art-global-component/index.vue
new file mode 100644
index 0000000..6908f94
--- /dev/null
+++ b/admin/src/components/core/layouts/art-global-component/index.vue
@@ -0,0 +1,14 @@
+
+
+
+
+
+
diff --git a/admin/src/components/core/layouts/art-global-search/index.vue b/admin/src/components/core/layouts/art-global-search/index.vue
new file mode 100644
index 0000000..f2c0b88
--- /dev/null
+++ b/admin/src/components/core/layouts/art-global-search/index.vue
@@ -0,0 +1,431 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ formatMenuTitle(item.meta.title) }}
+
+
+
+
+
+
+
{{ $t('search.historyTitle') }}
+
+
+ {{ formatMenuTitle(item.meta.title) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/admin/src/components/core/layouts/art-header-bar/index.vue b/admin/src/components/core/layouts/art-header-bar/index.vue
new file mode 100644
index 0000000..4e3c8f9
--- /dev/null
+++ b/admin/src/components/core/layouts/art-header-bar/index.vue
@@ -0,0 +1,485 @@
+
+
+
+
+
+
+
+
+
{{ AppConfig.systemInfo.name }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ $t('topBar.search.title') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('topBar.guide.title')
+ }} {{ $t('topBar.guide.theme') }} 、 {{ $t('topBar.guide.menu') }} {{ $t('topBar.guide.description') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/admin/src/components/core/layouts/art-header-bar/widget/ArtUserMenu.vue b/admin/src/components/core/layouts/art-header-bar/widget/ArtUserMenu.vue
new file mode 100644
index 0000000..40fdc39
--- /dev/null
+++ b/admin/src/components/core/layouts/art-header-bar/widget/ArtUserMenu.vue
@@ -0,0 +1,137 @@
+
+
+
+
+
+
+
+
+
+

+
+ {{
+ userInfo.userName
+ }}
+ {{ userInfo.email }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/admin/src/components/core/layouts/art-menus/art-horizontal-menu/index.vue b/admin/src/components/core/layouts/art-menus/art-horizontal-menu/index.vue
new file mode 100644
index 0000000..edd1473
--- /dev/null
+++ b/admin/src/components/core/layouts/art-menus/art-horizontal-menu/index.vue
@@ -0,0 +1,110 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/admin/src/components/core/layouts/art-menus/art-horizontal-menu/widget/HorizontalSubmenu.vue b/admin/src/components/core/layouts/art-menus/art-horizontal-menu/widget/HorizontalSubmenu.vue
new file mode 100644
index 0000000..0f3a817
--- /dev/null
+++ b/admin/src/components/core/layouts/art-menus/art-horizontal-menu/widget/HorizontalSubmenu.vue
@@ -0,0 +1,106 @@
+
+
+
+
+ {{ formatMenuTitle(item.meta.title) }}
+
+
+ {{ item.meta.showTextBadge }}
+
+
+
+
+
+
+
+
+
+ {{ formatMenuTitle(item.meta.title) }}
+
+
+ {{ item.meta.showTextBadge }}
+
+
+
+
+
+
+
diff --git a/admin/src/components/core/layouts/art-menus/art-mixed-menu/index.vue b/admin/src/components/core/layouts/art-menus/art-mixed-menu/index.vue
new file mode 100644
index 0000000..4e98246
--- /dev/null
+++ b/admin/src/components/core/layouts/art-menus/art-mixed-menu/index.vue
@@ -0,0 +1,279 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/admin/src/components/core/layouts/art-menus/art-sidebar-menu/index.vue b/admin/src/components/core/layouts/art-menus/art-sidebar-menu/index.vue
new file mode 100644
index 0000000..c9eb1fa
--- /dev/null
+++ b/admin/src/components/core/layouts/art-menus/art-sidebar-menu/index.vue
@@ -0,0 +1,369 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/admin/src/components/core/layouts/art-menus/art-sidebar-menu/style.scss b/admin/src/components/core/layouts/art-menus/art-sidebar-menu/style.scss
new file mode 100644
index 0000000..8cccdd9
--- /dev/null
+++ b/admin/src/components/core/layouts/art-menus/art-sidebar-menu/style.scss
@@ -0,0 +1,251 @@
+.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;
+ // 防止菜单内的滚动影响整个页面滚动
+ 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/admin/src/components/core/layouts/art-menus/art-sidebar-menu/theme.scss b/admin/src/components/core/layouts/art-menus/art-sidebar-menu/theme.scss
new file mode 100644
index 0000000..7626c42
--- /dev/null
+++ b/admin/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/admin/src/components/core/layouts/art-menus/art-sidebar-menu/widget/SidebarSubmenu.vue b/admin/src/components/core/layouts/art-menus/art-sidebar-menu/widget/SidebarSubmenu.vue
new file mode 100644
index 0000000..9afa262
--- /dev/null
+++ b/admin/src/components/core/layouts/art-menus/art-sidebar-menu/widget/SidebarSubmenu.vue
@@ -0,0 +1,199 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.meta.showTextBadge }}
+
+
+
+
+
+
+
diff --git a/admin/src/components/core/layouts/art-notification/index.vue b/admin/src/components/core/layouts/art-notification/index.vue
new file mode 100644
index 0000000..a58853c
--- /dev/null
+++ b/admin/src/components/core/layouts/art-notification/index.vue
@@ -0,0 +1,456 @@
+
+
+
+
+ {{ $t('notice.title') }}
+
+ {{ $t('notice.btnRead') }}
+
+
+
+
+ -
+ {{ item.name }} ({{ item.num }})
+
+
+
+
+
+
+
+
+ {{ $t('notice.viewAll') }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/admin/src/components/core/layouts/art-page-content/index.vue b/admin/src/components/core/layouts/art-page-content/index.vue
new file mode 100644
index 0000000..a862df1
--- /dev/null
+++ b/admin/src/components/core/layouts/art-page-content/index.vue
@@ -0,0 +1,136 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/admin/src/components/core/layouts/art-screen-lock/index.vue b/admin/src/components/core/layouts/art-screen-lock/index.vue
new file mode 100644
index 0000000..d7002a9
--- /dev/null
+++ b/admin/src/components/core/layouts/art-screen-lock/index.vue
@@ -0,0 +1,522 @@
+
+
+
+
+
+
+
🔒
+
系统已锁定
+
+ 检测到开发者工具已打开
+ 为了系统安全,请关闭开发者工具后继续使用
+
+
Security Lock Activated
+
+
+
+
+
+
+
+

+
{{ userInfo.userName }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('lockScreen.lock.btnText') }}
+
+
+
+
+
+
+
+
+
+

+
+ {{ userInfo.userName }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('lockScreen.unlock.btnText') }}
+
+
+
+ {{ $t('lockScreen.unlock.backBtnText') }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/admin/src/components/core/layouts/art-settings-panel/composables/useSettingsConfig.ts b/admin/src/components/core/layouts/art-settings-panel/composables/useSettingsConfig.ts
new file mode 100644
index 0000000..35e8066
--- /dev/null
+++ b/admin/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/admin/src/components/core/layouts/art-settings-panel/composables/useSettingsHandlers.ts b/admin/src/components/core/layouts/art-settings-panel/composables/useSettingsHandlers.ts
new file mode 100644
index 0000000..392c690
--- /dev/null
+++ b/admin/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/admin/src/components/core/layouts/art-settings-panel/composables/useSettingsPanel.ts b/admin/src/components/core/layouts/art-settings-panel/composables/useSettingsPanel.ts
new file mode 100644
index 0000000..f5e4e56
--- /dev/null
+++ b/admin/src/components/core/layouts/art-settings-panel/composables/useSettingsPanel.ts
@@ -0,0 +1,233 @@
+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 { StorageConfig } from '@/utils'
+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 getStoredDesktopMenuType = (): MenuTypeEnum | undefined => {
+ const storedMenuType = localStorage.getItem(StorageConfig.RESPONSIVE_MENU_TYPE_KEY)
+ return Object.values(MenuTypeEnum).includes(storedMenuType as MenuTypeEnum)
+ ? (storedMenuType as MenuTypeEnum)
+ : undefined
+ }
+
+ const setStoredDesktopMenuType = (type: MenuTypeEnum) => {
+ localStorage.setItem(StorageConfig.RESPONSIVE_MENU_TYPE_KEY, type)
+ }
+
+ const clearStoredDesktopMenuType = () => {
+ localStorage.removeItem(StorageConfig.RESPONSIVE_MENU_TYPE_KEY)
+ }
+
+ const storedDesktopMenuType = getStoredDesktopMenuType()
+ const beforeMenuType = ref(storedDesktopMenuType)
+ const hasChangedMenu = ref(Boolean(storedDesktopMenuType))
+
+ // 计算属性
+ 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
+ if (menuType.value !== MenuTypeEnum.LEFT) {
+ setStoredDesktopMenuType(menuType.value)
+ useSettingsState().switchMenuLayouts(MenuTypeEnum.LEFT)
+ hasChangedMenu.value = true
+ }
+ }
+
+ settingStore.setMenuOpen(false)
+ } else {
+ // 恢复桌面端布局
+ if (hasChangedMenu.value && beforeMenuType.value) {
+ if (menuType.value === MenuTypeEnum.LEFT) {
+ useSettingsState().switchMenuLayouts(beforeMenuType.value)
+ }
+
+ clearStoredDesktopMenuType()
+ hasChangedMenu.value = false
+ }
+
+ settingStore.setMenuOpen(true)
+ }
+ },
+ { 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/admin/src/components/core/layouts/art-settings-panel/composables/useSettingsState.ts b/admin/src/components/core/layouts/art-settings-panel/composables/useSettingsState.ts
new file mode 100644
index 0000000..65352d2
--- /dev/null
+++ b/admin/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/admin/src/components/core/layouts/art-settings-panel/index.vue b/admin/src/components/core/layouts/art-settings-panel/index.vue
new file mode 100644
index 0000000..0cbf344
--- /dev/null
+++ b/admin/src/components/core/layouts/art-settings-panel/index.vue
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/admin/src/components/core/layouts/art-settings-panel/style.scss b/admin/src/components/core/layouts/art-settings-panel/style.scss
new file mode 100644
index 0000000..e863074
--- /dev/null
+++ b/admin/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/admin/src/components/core/layouts/art-settings-panel/widget/BasicSettings.vue b/admin/src/components/core/layouts/art-settings-panel/widget/BasicSettings.vue
new file mode 100644
index 0000000..b6dc9d3
--- /dev/null
+++ b/admin/src/components/core/layouts/art-settings-panel/widget/BasicSettings.vue
@@ -0,0 +1,77 @@
+
+
+
+
+
+
+
+
diff --git a/admin/src/components/core/layouts/art-settings-panel/widget/BoxStyleSettings.vue b/admin/src/components/core/layouts/art-settings-panel/widget/BoxStyleSettings.vue
new file mode 100644
index 0000000..86c7a9e
--- /dev/null
+++ b/admin/src/components/core/layouts/art-settings-panel/widget/BoxStyleSettings.vue
@@ -0,0 +1,38 @@
+
+
+
+
+
+ {{ option.label }}
+
+
+
+
+
+
diff --git a/admin/src/components/core/layouts/art-settings-panel/widget/ColorSettings.vue b/admin/src/components/core/layouts/art-settings-panel/widget/ColorSettings.vue
new file mode 100644
index 0000000..05a4b41
--- /dev/null
+++ b/admin/src/components/core/layouts/art-settings-panel/widget/ColorSettings.vue
@@ -0,0 +1,35 @@
+
+
+
+
+
diff --git a/admin/src/components/core/layouts/art-settings-panel/widget/ContainerSettings.vue b/admin/src/components/core/layouts/art-settings-panel/widget/ContainerSettings.vue
new file mode 100644
index 0000000..1f5be72
--- /dev/null
+++ b/admin/src/components/core/layouts/art-settings-panel/widget/ContainerSettings.vue
@@ -0,0 +1,33 @@
+
+
+
+
+
diff --git a/admin/src/components/core/layouts/art-settings-panel/widget/MenuLayoutSettings.vue b/admin/src/components/core/layouts/art-settings-panel/widget/MenuLayoutSettings.vue
new file mode 100644
index 0000000..dbcae46
--- /dev/null
+++ b/admin/src/components/core/layouts/art-settings-panel/widget/MenuLayoutSettings.vue
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
![]()
+
+
{{ $t(`setting.menuType.list[${index}]`) }}
+
+
+
+
+
+
diff --git a/admin/src/components/core/layouts/art-settings-panel/widget/MenuStyleSettings.vue b/admin/src/components/core/layouts/art-settings-panel/widget/MenuStyleSettings.vue
new file mode 100644
index 0000000..61237eb
--- /dev/null
+++ b/admin/src/components/core/layouts/art-settings-panel/widget/MenuStyleSettings.vue
@@ -0,0 +1,44 @@
+
+
+
+
+
+
![]()
+
+
+
+
+
+
diff --git a/admin/src/components/core/layouts/art-settings-panel/widget/SectionTitle.vue b/admin/src/components/core/layouts/art-settings-panel/widget/SectionTitle.vue
new file mode 100644
index 0000000..31ef00c
--- /dev/null
+++ b/admin/src/components/core/layouts/art-settings-panel/widget/SectionTitle.vue
@@ -0,0 +1,17 @@
+
+
+ {{ title }}
+
+
+
+
diff --git a/admin/src/components/core/layouts/art-settings-panel/widget/SettingActions.vue b/admin/src/components/core/layouts/art-settings-panel/widget/SettingActions.vue
new file mode 100644
index 0000000..7b47d1a
--- /dev/null
+++ b/admin/src/components/core/layouts/art-settings-panel/widget/SettingActions.vue
@@ -0,0 +1,235 @@
+
+
+
+
+ {{ $t('setting.actions.copyConfig') }}
+
+
+ {{ $t('setting.actions.resetConfig') }}
+
+
+
+
+
diff --git a/admin/src/components/core/layouts/art-settings-panel/widget/SettingDrawer.vue b/admin/src/components/core/layouts/art-settings-panel/widget/SettingDrawer.vue
new file mode 100644
index 0000000..85372be
--- /dev/null
+++ b/admin/src/components/core/layouts/art-settings-panel/widget/SettingDrawer.vue
@@ -0,0 +1,51 @@
+
+
+
+
+
diff --git a/admin/src/components/core/layouts/art-settings-panel/widget/SettingHeader.vue b/admin/src/components/core/layouts/art-settings-panel/widget/SettingHeader.vue
new file mode 100644
index 0000000..e3ead9e
--- /dev/null
+++ b/admin/src/components/core/layouts/art-settings-panel/widget/SettingHeader.vue
@@ -0,0 +1,18 @@
+
+
+
+
+
diff --git a/admin/src/components/core/layouts/art-settings-panel/widget/SettingItem.vue b/admin/src/components/core/layouts/art-settings-panel/widget/SettingItem.vue
new file mode 100644
index 0000000..5721027
--- /dev/null
+++ b/admin/src/components/core/layouts/art-settings-panel/widget/SettingItem.vue
@@ -0,0 +1,101 @@
+
+
+ {{ config.label }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/admin/src/components/core/layouts/art-settings-panel/widget/ThemeSettings.vue b/admin/src/components/core/layouts/art-settings-panel/widget/ThemeSettings.vue
new file mode 100644
index 0000000..4b46fcd
--- /dev/null
+++ b/admin/src/components/core/layouts/art-settings-panel/widget/ThemeSettings.vue
@@ -0,0 +1,28 @@
+
+
+
+
+
+
![]()
+
+
{{ $t(`setting.theme.list[${index}]`) }}
+
+
+
+
+
diff --git a/admin/src/components/core/layouts/art-work-tab/index.vue b/admin/src/components/core/layouts/art-work-tab/index.vue
new file mode 100644
index 0000000..152ff63
--- /dev/null
+++ b/admin/src/components/core/layouts/art-work-tab/index.vue
@@ -0,0 +1,584 @@
+
+
+
+
+
+ - showMenu(e, item.path)"
+ >
+
+ {{ item.customTitle || formatMenuTitle(item.title) }}
+
+
+
+
+
+
+
+
+
+
showMenu(e, activeTab)"
+ >
+
+
+
+
+
+
+
+
+
+
+
diff --git a/admin/src/components/core/others/art-menu-right/index.vue b/admin/src/components/core/others/art-menu-right/index.vue
new file mode 100644
index 0000000..1cc92ab
--- /dev/null
+++ b/admin/src/components/core/others/art-menu-right/index.vue
@@ -0,0 +1,415 @@
+
+
+
+
+
+
+
+
diff --git a/admin/src/components/core/others/art-watermark/index.vue b/admin/src/components/core/others/art-watermark/index.vue
new file mode 100644
index 0000000..1d7f06b
--- /dev/null
+++ b/admin/src/components/core/others/art-watermark/index.vue
@@ -0,0 +1,64 @@
+
+
+
+
+
+
diff --git a/admin/src/components/core/tables/art-table-header/index.vue b/admin/src/components/core/tables/art-table-header/index.vue
new file mode 100644
index 0000000..788c2b7
--- /dev/null
+++ b/admin/src/components/core/tables/art-table-header/index.vue
@@ -0,0 +1,339 @@
+
+
+
+
+
+
+
+
diff --git a/admin/src/components/core/tables/art-table/index.vue b/admin/src/components/core/tables/art-table/index.vue
new file mode 100644
index 0000000..be99e70
--- /dev/null
+++ b/admin/src/components/core/tables/art-table/index.vue
@@ -0,0 +1,368 @@
+
+
+
+
+
+
+
+
+
+
+
+ {{ getGlobalIndex($index) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ col.label }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/admin/src/components/core/tables/art-table/style.scss b/admin/src/components/core/tables/art-table/style.scss
new file mode 100644
index 0000000..67459e8
--- /dev/null
+++ b/admin/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/admin/src/components/core/text-effect/art-count-to/index.vue b/admin/src/components/core/text-effect/art-count-to/index.vue
new file mode 100644
index 0000000..7fb104b
--- /dev/null
+++ b/admin/src/components/core/text-effect/art-count-to/index.vue
@@ -0,0 +1,310 @@
+
+
+
+ {{ formattedValue }}
+
+
+
+
diff --git a/admin/src/components/core/text-effect/art-festival-text-scroll/index.vue b/admin/src/components/core/text-effect/art-festival-text-scroll/index.vue
new file mode 100644
index 0000000..770b457
--- /dev/null
+++ b/admin/src/components/core/text-effect/art-festival-text-scroll/index.vue
@@ -0,0 +1,32 @@
+
+
+
+
+
+
diff --git a/admin/src/components/core/text-effect/art-text-scroll/index.vue b/admin/src/components/core/text-effect/art-text-scroll/index.vue
new file mode 100644
index 0000000..90be30f
--- /dev/null
+++ b/admin/src/components/core/text-effect/art-text-scroll/index.vue
@@ -0,0 +1,285 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/admin/src/components/core/theme/theme-svg/index.vue b/admin/src/components/core/theme/theme-svg/index.vue
new file mode 100644
index 0000000..0b565a9
--- /dev/null
+++ b/admin/src/components/core/theme/theme-svg/index.vue
@@ -0,0 +1,100 @@
+
+
+
+
+
+
+
+
+
diff --git a/admin/src/components/core/views/exception/ArtException.vue b/admin/src/components/core/views/exception/ArtException.vue
new file mode 100644
index 0000000..4be59ae
--- /dev/null
+++ b/admin/src/components/core/views/exception/ArtException.vue
@@ -0,0 +1,55 @@
+
+
+
+
+
+
{{ data.desc }}
+
{{
+ data.btnText
+ }}
+
+
+
+
+
+
diff --git a/admin/src/components/core/views/login/AuthTopBar.vue b/admin/src/components/core/views/login/AuthTopBar.vue
new file mode 100644
index 0000000..9455253
--- /dev/null
+++ b/admin/src/components/core/views/login/AuthTopBar.vue
@@ -0,0 +1,149 @@
+
+
+
+
+
+
{{ AppConfig.systemInfo.name }}
+
+
+
+
+
+
+
+
+
diff --git a/admin/src/components/core/views/login/LoginLeftView.vue b/admin/src/components/core/views/login/LoginLeftView.vue
new file mode 100644
index 0000000..af6a904
--- /dev/null
+++ b/admin/src/components/core/views/login/LoginLeftView.vue
@@ -0,0 +1,602 @@
+
+
+
+
+
+
{{ AppConfig.systemInfo.name }}
+
+
+
+
+
+
+
+
{{ $t('login.leftView.title') }}
+
{{ $t('login.leftView.subTitle') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/admin/src/components/core/views/result/ArtResultPage.vue b/admin/src/components/core/views/result/ArtResultPage.vue
new file mode 100644
index 0000000..b2eca48
--- /dev/null
+++ b/admin/src/components/core/views/result/ArtResultPage.vue
@@ -0,0 +1,43 @@
+
+
+
+
{{
+ title
+ }}
+
{{ message }}
+
+
+
+
+
+
+
+
+
+
diff --git a/admin/src/components/core/widget/art-icon-button/index.vue b/admin/src/components/core/widget/art-icon-button/index.vue
new file mode 100644
index 0000000..760888b
--- /dev/null
+++ b/admin/src/components/core/widget/art-icon-button/index.vue
@@ -0,0 +1,23 @@
+
+
+
+
+
+
diff --git a/admin/src/config/assets/images.ts b/admin/src/config/assets/images.ts
new file mode 100644
index 0000000..f3e89dd
--- /dev/null
+++ b/admin/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/admin/src/config/index.ts b/admin/src/config/index.ts
new file mode 100644
index 0000000..ddbc958
--- /dev/null
+++ b/admin/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: 'all-kefu 管理后台' // 系统名称
+ },
+ // 系统主题
+ 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/admin/src/config/modules/component.ts b/admin/src/config/modules/component.ts
new file mode 100644
index 0000000..dc0e244
--- /dev/null
+++ b/admin/src/config/modules/component.ts
@@ -0,0 +1,108 @@
+/**
+ * 全局组件配置
+ *
+ * 统一管理系统级全局组件的注册。
+ * 这些组件会在应用启动时全局注册,可在任何地方使用。
+ *
+ * ## 主要功能
+ *
+ * - 组件配置 - 集中管理全局组件的配置信息
+ * - 异步加载 - 使用 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')
+ ),
+ // 入口已摘除(Phase 3 终审 M-1):锁屏把真实密码 AES 存 localStorage
+ // 且密钥是公开模板默认值,防御价值为零——enabled: false 连全局挂载
+ // 也不注册;组件代码保留,Phase 4 决定删否。
+ enabled: false
+ },
+ {
+ 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/admin/src/config/modules/fastEnter.ts b/admin/src/config/modules/fastEnter.ts
new file mode 100644
index 0000000..60c37b3
--- /dev/null
+++ b/admin/src/config/modules/fastEnter.ts
@@ -0,0 +1,60 @@
+/**
+ * 快速入口配置
+ * 包含:应用列表、快速链接等
+ */
+import type { FastEnterConfig } from '@/types/config'
+
+const fastEnterConfig: FastEnterConfig = {
+ // 显示条件(屏幕宽度)
+ minWidth: 1200,
+ // 应用列表
+ applications: [
+ {
+ name: '仪表盘',
+ description: '平台概览与数据统计',
+ icon: 'ri:dashboard-line',
+ iconColor: '#377dff',
+ enabled: true,
+ order: 1,
+ routeName: 'Console'
+ },
+ {
+ name: '会话与线索',
+ description: '会话列表与询盘推送',
+ icon: 'ri:chat-3-line',
+ iconColor: '#13DEB9',
+ enabled: true,
+ order: 2,
+ routeName: 'ConversationList'
+ },
+ {
+ name: '配置中心',
+ description: '租户配置与品牌管理',
+ icon: 'ri:settings-4-line',
+ iconColor: '#ffb100',
+ enabled: true,
+ order: 3,
+ routeName: 'ConfigCenter'
+ },
+ {
+ name: '统计分析',
+ description: '卡片/漏斗/趋势/分布',
+ icon: 'ri:bar-chart-2-line',
+ iconColor: '#ff6b6b',
+ enabled: true,
+ order: 4,
+ routeName: 'StatsOverview'
+ }
+ ],
+ // 快速链接(登录项已删——终审 M-8:已登录态下无意义)
+ quickLinks: [
+ {
+ name: '修改密码',
+ enabled: true,
+ order: 1,
+ routeName: 'ForceChangePassword'
+ }
+ ]
+}
+
+export default Object.freeze(fastEnterConfig)
diff --git a/admin/src/config/modules/festival.ts b/admin/src/config/modules/festival.ts
new file mode 100644
index 0000000..39cd790
--- /dev/null
+++ b/admin/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/admin/src/config/modules/headerBar.ts b/admin/src/config/modules/headerBar.ts
new file mode 100644
index 0000000..d1c908b
--- /dev/null
+++ b/admin/src/config/modules/headerBar.ts
@@ -0,0 +1,65 @@
+/**
+ * 顶部栏功能配置
+ *
+ * 统一管理顶部栏各个功能模块的启用状态。
+ * 通过修改此配置文件可以快速启用或禁用顶部栏的功能按钮。
+ *
+ * @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: {
+ // 入口已摘除(Phase 3 终审 M-2):模板通知中心是硬编码演示数据,
+ // 无真实数据源——enabled: false 隐藏铃铛;组件保留,接真数据源时再开。
+ enabled: false,
+ description: '通知中心,显示系统通知和消息'
+ },
+ chat: {
+ enabled: true,
+ description: '聊天功能,提供实时沟通'
+ },
+ language: {
+ enabled: true,
+ description: '多语言切换功能'
+ },
+ settings: {
+ enabled: true,
+ description: '系统设置面板'
+ },
+ themeToggle: {
+ enabled: true,
+ description: '主题切换功能(明暗主题)'
+ }
+}
+
+export default headerBarConfig
diff --git a/admin/src/config/setting.ts b/admin/src/config/setting.ts
new file mode 100644
index 0000000..94f2d2c
--- /dev/null
+++ b/admin/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/admin/src/directives/business/ripple.ts b/admin/src/directives/business/ripple.ts
new file mode 100644
index 0000000..e65470e
--- /dev/null
+++ b/admin/src/directives/business/ripple.ts
@@ -0,0 +1,116 @@
+/**
+ * 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 type RippleDirective = Directive
+
+export const vRipple: RippleDirective = {
+ 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/admin/src/directives/core/auth.ts b/admin/src/directives/core/auth.ts
new file mode 100644
index 0000000..3d244c7
--- /dev/null
+++ b/admin/src/directives/core/auth.ts
@@ -0,0 +1,66 @@
+/**
+ * v-auth 权限指令
+ *
+ * 适用于后端权限控制模式,基于权限标识控制 DOM 元素的显示和隐藏。
+ * 如果用户没有对应权限,元素将从 DOM 中移除。
+ *
+ * ## 主要功能
+ *
+ * - 权限验证 - 根据路由 meta 中的权限列表验证用户权限
+ * - DOM 控制 - 无权限时自动移除元素,而非隐藏
+ * - 响应式更新 - 权限变化时自动更新元素状态
+ *
+ * ## 使用示例
+ *
+ * ```vue
+ *
+ * 新增
+ *
+ *
+ * 编辑
+ *
+ *
+ * 删除
+ * ```
+ *
+ * ## 注意事项
+ *
+ * - 该指令会直接移除 DOM 元素,而不是使用 v-if 隐藏
+ * - 权限列表从当前路由的 meta.authList 中获取
+ *
+ * @module directives/auth
+ * @author Art Design Pro Team
+ */
+
+import { router } from '@/router'
+import { App, Directive, DirectiveBinding } from 'vue'
+
+export type AuthDirective = Directive
+
+function checkAuthPermission(el: HTMLElement, binding: DirectiveBinding): void {
+ // 获取当前路由的权限列表
+ const authList = (router.currentRoute.value.meta.authList as Array<{ authMark: string }>) || []
+
+ // 检查是否有对应的权限标识
+ const hasPermission = authList.some((item) => item.authMark === binding.value)
+
+ // 如果没有权限,移除元素
+ if (!hasPermission) {
+ removeElement(el)
+ }
+}
+
+function removeElement(el: HTMLElement): void {
+ if (el.parentNode) {
+ el.parentNode.removeChild(el)
+ }
+}
+
+const authDirective: AuthDirective = {
+ mounted: checkAuthPermission,
+ updated: checkAuthPermission
+}
+
+export function setupAuthDirective(app: App): void {
+ app.directive('auth', authDirective)
+}
diff --git a/admin/src/directives/core/roles.ts b/admin/src/directives/core/roles.ts
new file mode 100644
index 0000000..f1db35b
--- /dev/null
+++ b/admin/src/directives/core/roles.ts
@@ -0,0 +1,87 @@
+/**
+ * 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'
+
+export type RolesDirective = Directive
+
+function checkRolePermission(el: HTMLElement, binding: DirectiveBinding): 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: RolesDirective = {
+ mounted: checkRolePermission,
+ updated: checkRolePermission
+}
+
+export function setupRolesDirective(app: App): void {
+ app.directive('roles', rolesDirective)
+}
diff --git a/admin/src/directives/index.ts b/admin/src/directives/index.ts
new file mode 100644
index 0000000..b396be0
--- /dev/null
+++ b/admin/src/directives/index.ts
@@ -0,0 +1,12 @@
+import type { App } from 'vue'
+import { setupAuthDirective, type AuthDirective } from './core/auth'
+import { setupRippleDirective, type RippleDirective } from './business/ripple'
+import { setupRolesDirective, type RolesDirective } from './core/roles'
+
+export function setupGlobDirectives(app: App) {
+ setupAuthDirective(app) // 权限指令
+ setupRolesDirective(app) // 角色权限指令
+ setupRippleDirective(app) // 水波纹指令
+}
+
+export type { AuthDirective, RippleDirective, RolesDirective }
diff --git a/admin/src/enums/appEnum.ts b/admin/src/enums/appEnum.ts
new file mode 100644
index 0000000..a39c278
--- /dev/null
+++ b/admin/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/admin/src/enums/formEnum.ts b/admin/src/enums/formEnum.ts
new file mode 100644
index 0000000..8e9b3b4
--- /dev/null
+++ b/admin/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/admin/src/env.d.ts b/admin/src/env.d.ts
new file mode 100644
index 0000000..73846e2
--- /dev/null
+++ b/admin/src/env.d.ts
@@ -0,0 +1,8 @@
+///
+
+declare module 'nprogress'
+
+declare module 'crypto-js'
+
+// 全局变量声明
+declare const __APP_VERSION__: string // 版本号
diff --git a/admin/src/hooks/core/useAppMode.ts b/admin/src/hooks/core/useAppMode.ts
new file mode 100644
index 0000000..c39cd9e
--- /dev/null
+++ b/admin/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/admin/src/hooks/core/useAuth.ts b/admin/src/hooks/core/useAuth.ts
new file mode 100644
index 0000000..283b859
--- /dev/null
+++ b/admin/src/hooks/core/useAuth.ts
@@ -0,0 +1,74 @@
+/**
+ * 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)
+
+ // 前端按钮权限(例如:['add', 'edit'])
+ const frontendAuthList = info.value?.buttons ?? []
+
+ // 后端路由 meta 配置的权限列表(例如:[{ authMark: 'add' }])
+ const backendAuthList: AuthItem[] = Array.isArray(route.meta.authList)
+ ? (route.meta.authList as AuthItem[])
+ : []
+
+ /**
+ * 检查是否拥有某权限标识(前后端模式通用)
+ * @param auth 权限标识
+ * @returns 是否有权限
+ */
+ const hasAuth = (auth: string): boolean => {
+ // 前端模式
+ if (isFrontendMode.value) {
+ return frontendAuthList.includes(auth)
+ }
+
+ // 后端模式
+ return backendAuthList.some((item) => item?.authMark === auth)
+ }
+
+ return {
+ hasAuth
+ }
+}
diff --git a/admin/src/hooks/core/useCeremony.ts b/admin/src/hooks/core/useCeremony.ts
new file mode 100644
index 0000000..ead2630
--- /dev/null
+++ b/admin/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/admin/src/hooks/core/useChart.ts b/admin/src/hooks/core/useChart.ts
new file mode 100644
index 0000000..29ba1d1
--- /dev/null
+++ b/admin/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/admin/src/hooks/core/useCommon.ts b/admin/src/hooks/core/useCommon.ts
new file mode 100644
index 0000000..c936854
--- /dev/null
+++ b/admin/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/admin/src/hooks/core/useFastEnter.ts b/admin/src/hooks/core/useFastEnter.ts
new file mode 100644
index 0000000..555eb65
--- /dev/null
+++ b/admin/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/admin/src/hooks/core/useHeaderBar.ts b/admin/src/hooks/core/useHeaderBar.ts
new file mode 100644
index 0000000..be10712
--- /dev/null
+++ b/admin/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/admin/src/hooks/core/useLayoutHeight.ts b/admin/src/hooks/core/useLayoutHeight.ts
new file mode 100644
index 0000000..4b1171a
--- /dev/null
+++ b/admin/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/admin/src/hooks/core/useTable.ts b/admin/src/hooks/core/useTable.ts
new file mode 100644
index 0000000..d9def4f
--- /dev/null
+++ b/admin/src/hooks/core/useTable.ts
@@ -0,0 +1,762 @@
+/**
+ * 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 Promise>(
+ config: UseTableConfig
+) {
+ return useTableImpl(config)
+}
+
+/**
+ * useTable 的核心实现 - 强大的表格数据管理 Hook
+ *
+ * 提供完整的表格解决方案,包括:
+ * - 数据获取与缓存
+ * - 分页控制
+ * - 搜索功能
+ * - 智能刷新策略
+ * - 错误处理
+ * - 列配置管理
+ */
+function useTableImpl Promise>(
+ config: UseTableConfig
+) {
+ type TRecord = InferRecordType>
+ 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()
+ }
+ }
+
+ // 替换搜索参数:适用于表单查询,避免旧字段残留
+ const replaceSearchParams = (params?: Partial): void => {
+ const paramsRecord = searchParams as Record
+ const currentSize = pagination.size || ((paramsRecord[sizeKey] as number) ?? 10)
+
+ Object.keys(searchParams).forEach((key) => {
+ if (key !== pageKey && key !== sizeKey) {
+ delete paramsRecord[key]
+ }
+ })
+
+ Object.assign(
+ searchParams,
+ {
+ [pageKey]: 1,
+ [sizeKey]: currentSize
+ },
+ params || {}
+ )
+
+ pagination.current = 1
+ pagination.size = currentSize
+ }
+
+ // 防重复调用的标志
+ 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,
+ /** 替换搜索参数(适用于表单查询,避免旧字段残留) */
+ replaceSearchParams,
+ /** 重置搜索参数 */
+ 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/admin/src/hooks/core/useTableColumns.ts b/admin/src/hooks/core/useTableColumns.ts
new file mode 100644
index 0000000..84b6e13
--- /dev/null
+++ b/admin/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/admin/src/hooks/core/useTableHeight.ts b/admin/src/hooks/core/useTableHeight.ts
new file mode 100644
index 0000000..8fdf6da
--- /dev/null
+++ b/admin/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/admin/src/hooks/core/useTheme.ts b/admin/src/hooks/core/useTheme.ts
new file mode 100644
index 0000000..187c3e0
--- /dev/null
+++ b/admin/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/admin/src/hooks/index.ts b/admin/src/hooks/index.ts
new file mode 100644
index 0000000..472b09c
--- /dev/null
+++ b/admin/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/admin/src/locales/index.ts b/admin/src/locales/index.ts
new file mode 100644
index 0000000..36c2648
--- /dev/null
+++ b/admin/src/locales/index.ts
@@ -0,0 +1,123 @@
+/**
+ * 国际化配置
+ *
+ * 基于 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'
+
+/**
+ * 存储键管理器实例
+ */
+const storageKeyManager = new StorageKeyManager()
+
+/**
+ * 语言消息对象
+ */
+const messages = {
+ [LanguageEnum.EN]: enMessages,
+ [LanguageEnum.ZH]: zhMessages
+}
+
+/**
+ * 语言选项列表
+ * 用于语言切换下拉框
+ */
+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/admin/src/locales/langs/en.json b/admin/src/locales/langs/en.json
new file mode 100644
index 0000000..b98b778
--- /dev/null
+++ b/admin/src/locales/langs/en.json
@@ -0,0 +1,257 @@
+{
+ "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",
+ "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": {
+ "changePassword": "Change Password",
+ "lockScreen": "Lock Screen",
+ "logout": "Log Out"
+ },
+ "guide": {
+ "title": "Click here to view",
+ "theme": "Theme style",
+ "menu": "Open top menu",
+ "description": "More configurations"
+ }
+ },
+ "common": {
+ "tips": "Prompt",
+ "cancel": "Cancel",
+ "confirm": "Confirm",
+ "logOutTips": "Do you want to log out?"
+ },
+ "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"
+ },
+ "worktab": {
+ "btn": {
+ "refresh": "Refresh",
+ "fixed": "Fixed",
+ "unfixed": "Unfixed",
+ "closeLeft": "Close left",
+ "closeRight": "Close right",
+ "closeOther": "Close other",
+ "closeAll": "Close all"
+ }
+ },
+ "login": {
+ "leftView": {
+ "title": "all-kefu Multi-tenant AI Support",
+ "subTitle": "Tenant config · Conversations · Statistics"
+ },
+ "title": "Welcome Back",
+ "subTitle": "Sign in with your account and password",
+ "placeholder": {
+ "username": "Username",
+ "password": "Password",
+ "slider": "Drag to verify"
+ },
+ "sliderText": "Drag to verify",
+ "sliderSuccessText": "Verified",
+ "rememberPwd": "Remember me",
+ "btnText": "Sign in",
+ "success": {
+ "title": "Success",
+ "message": "Welcome back"
+ }
+ },
+ "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"
+ },
+ "menus": {
+ "login": {
+ "title": "Login"
+ },
+ "forceChangePassword": {
+ "title": "Change Password"
+ },
+ "outside": {
+ "title": "Embedded"
+ },
+ "dashboard": {
+ "title": "Dashboard",
+ "console": "Console"
+ },
+ "tenants": {
+ "title": "Tenants",
+ "list": "Tenant List"
+ },
+ "conversations": {
+ "title": "Conversations",
+ "list": "Conversation List"
+ },
+ "config": {
+ "title": "Config Center",
+ "center": "Configuration"
+ },
+ "stats": {
+ "title": "Statistics",
+ "overview": "Overview"
+ },
+ "settings": {
+ "title": "Platform Settings",
+ "platform": "Platform"
+ }
+ },
+ "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"
+ }
+}
diff --git a/admin/src/locales/langs/zh.json b/admin/src/locales/langs/zh.json
new file mode 100644
index 0000000..cddf7ca
--- /dev/null
+++ b/admin/src/locales/langs/zh.json
@@ -0,0 +1,257 @@
+{
+ "httpMsg": {
+ "unauthorized": "未授权访问,请重新登录",
+ "forbidden": "禁止访问该资源",
+ "notFound": "请求的资源不存在",
+ "methodNotAllowed": "请求方法不允许",
+ "requestTimeout": "请求超时,请稍后重试",
+ "internalServerError": "服务器内部错误,请稍后重试",
+ "badGateway": "网关错误,请稍后重试",
+ "serviceUnavailable": "服务暂时不可用,请稍后重试",
+ "gatewayTimeout": "网关超时,请稍后重试",
+ "requestCancelled": "请求已取消",
+ "networkError": "网络连接异常,请检查网络连接",
+ "requestFailed": "请求失败",
+ "requestConfigError": "请求配置错误"
+ },
+ "topBar": {
+ "search": {
+ "title": "搜索"
+ },
+ "user": {
+ "changePassword": "修改密码",
+ "lockScreen": "锁定屏幕",
+ "logout": "退出登录"
+ },
+ "guide": {
+ "title": "点击这里查看",
+ "theme": "主题风格",
+ "menu": "开启顶栏菜单",
+ "description": "等更多配置"
+ }
+ },
+ "common": {
+ "tips": "提示",
+ "cancel": "取消",
+ "confirm": "确定",
+ "logOutTips": "您是否要退出登录?"
+ },
+ "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": "查看全部"
+ },
+ "worktab": {
+ "btn": {
+ "refresh": "刷新",
+ "fixed": "固定",
+ "unfixed": "取消固定",
+ "closeLeft": "关闭左侧",
+ "closeRight": "关闭右侧",
+ "closeOther": "关闭其他",
+ "closeAll": "关闭全部"
+ }
+ },
+ "login": {
+ "leftView": {
+ "title": "all-kefu 多租户 AI 客服平台",
+ "subTitle": "租户配置 · 会话线索 · 数据统计,一站式管理"
+ },
+ "title": "欢迎回来",
+ "subTitle": "输入您的账号和密码登录",
+ "placeholder": {
+ "username": "请输入账号",
+ "password": "请输入密码",
+ "slider": "请拖动滑块完成验证"
+ },
+ "sliderText": "按住滑块拖动",
+ "sliderSuccessText": "验证成功",
+ "rememberPwd": "记住密码",
+ "btnText": "登录",
+ "success": {
+ "title": "登录成功",
+ "message": "欢迎回来"
+ }
+ },
+ "lockScreen": {
+ "pwdError": "密码错误",
+ "lock": {
+ "inputPlaceholder": "请输入锁屏密码",
+ "btnText": "锁定"
+ },
+ "unlock": {
+ "inputPlaceholder": "请输入解锁密码",
+ "btnText": "解锁",
+ "backBtnText": "返回登录"
+ }
+ },
+ "greeting": {
+ "dawn": "凌晨了!",
+ "morning": "上午好!",
+ "afternoon": "下午好!",
+ "evening": "晚上好!"
+ },
+ "exceptionPage": {
+ "403": "抱歉,您无权访问该页面",
+ "404": "抱歉,您访问的页面不存在",
+ "500": "抱歉,服务器出错了",
+ "gohome": "返回首页"
+ },
+ "menus": {
+ "login": {
+ "title": "登录"
+ },
+ "forceChangePassword": {
+ "title": "强制修改密码"
+ },
+ "outside": {
+ "title": "内嵌页面"
+ },
+ "dashboard": {
+ "title": "仪表盘",
+ "console": "工作台"
+ },
+ "tenants": {
+ "title": "租户管理",
+ "list": "租户列表"
+ },
+ "conversations": {
+ "title": "会话与线索",
+ "list": "会话列表"
+ },
+ "config": {
+ "title": "配置中心",
+ "center": "配置管理"
+ },
+ "stats": {
+ "title": "统计分析",
+ "overview": "数据总览"
+ },
+ "settings": {
+ "title": "平台设置",
+ "platform": "平台参数"
+ }
+ },
+ "table": {
+ "form": {
+ "reset": "重置",
+ "submit": "提交"
+ },
+ "searchBar": {
+ "reset": "重置",
+ "search": "查询",
+ "expand": "展开",
+ "collapse": "收起",
+ "searchInputPlaceholder": "请输入",
+ "searchSelectPlaceholder": "请选择"
+ },
+ "selection": "选择",
+ "sizeOptions": {
+ "small": "紧凑",
+ "default": "默认",
+ "large": "宽松"
+ },
+ "column": {
+ "selection": "勾选",
+ "expand": "展开",
+ "index": "序号"
+ },
+ "zebra": "斑马纹",
+ "border": "边框",
+ "headerBackground": "表头背景"
+ }
+}
diff --git a/admin/src/main.ts b/admin/src/main.ts
new file mode 100644
index 0000000..7df948a
--- /dev/null
+++ b/admin/src/main.ts
@@ -0,0 +1,25 @@
+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)
+initStore(app)
+initRouter(app)
+setupGlobDirectives(app)
+setupErrorHandle(app)
+
+app.use(language)
+app.mount('#app')
\ No newline at end of file
diff --git a/admin/src/plugins/echarts.ts b/admin/src/plugins/echarts.ts
new file mode 100644
index 0000000..4f56d89
--- /dev/null
+++ b/admin/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/admin/src/plugins/index.ts b/admin/src/plugins/index.ts
new file mode 100644
index 0000000..4536a86
--- /dev/null
+++ b/admin/src/plugins/index.ts
@@ -0,0 +1,6 @@
+/**
+ * 插件统一导出
+ * 集中管理第三方库的封装和配置
+ */
+
+export * from './echarts'
diff --git a/admin/src/router/core/ComponentLoader.ts b/admin/src/router/core/ComponentLoader.ts
new file mode 100644
index 0000000..8af3ce3
--- /dev/null
+++ b/admin/src/router/core/ComponentLoader.ts
@@ -0,0 +1,82 @@
+/**
+ * 组件加载器
+ *
+ * 负责动态加载 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 fullPath = `../../views${componentPath}.vue`
+ const fullPathWithIndex = `../../views${componentPath}/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/admin/src/router/core/IframeRouteManager.ts b/admin/src/router/core/IframeRouteManager.ts
new file mode 100644
index 0000000..c054ca1
--- /dev/null
+++ b/admin/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/admin/src/router/core/MenuProcessor.ts b/admin/src/router/core/MenuProcessor.ts
new file mode 100644
index 0000000..c424867
--- /dev/null
+++ b/admin/src/router/core/MenuProcessor.ts
@@ -0,0 +1,264 @@
+/**
+ * 菜单处理器
+ *
+ * 负责菜单数据的获取、过滤和处理
+ *
+ * @module router/core/MenuProcessor
+ * @author Art Design Pro Team
+ */
+
+import type { AppRouteRecord } from '@/types/router'
+import { useUserStore } from '@/store/modules/user'
+import { asyncRoutes } from '../routes/asyncRoutes'
+import { RoutesAlias } from '../routesAlias'
+import { formatMenuTitle } from '@/utils'
+
+export class MenuProcessor {
+ /**
+ * 获取菜单数据
+ */
+ async getMenuList(): Promise {
+ const menuList = await this.processFrontendMenu()
+
+ // 在规范化路径之前,验证原始路径配置
+ this.validateMenuPaths(menuList)
+
+ // 规范化路径(将相对路径转换为完整路径)
+ return this.normalizeMenuPaths(menuList)
+ }
+
+ /**
+ * 处理前端控制模式的菜单(本平台固定前端权限模式:
+ * 菜单来自本地路由模块,按登录角色 meta.roles 过滤)
+ */
+ 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 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
+
+ const redirect = item.redirect || this.resolveDefaultRedirect(children)
+
+ return {
+ ...item,
+ path: fullPath,
+ redirect,
+ children
+ }
+ })
+ }
+
+ /**
+ * 为目录型菜单推导默认跳转地址
+ */
+ private resolveDefaultRedirect(children?: AppRouteRecord[]): string | undefined {
+ if (!children?.length) {
+ return undefined
+ }
+
+ for (const child of children) {
+ if (this.isNavigableRoute(child)) {
+ return child.path
+ }
+
+ const nestedRedirect = this.resolveDefaultRedirect(child.children)
+ if (nestedRedirect) {
+ return nestedRedirect
+ }
+ }
+
+ return undefined
+ }
+
+ /**
+ * 判断子路由是否可以作为默认落点
+ */
+ private isNavigableRoute(route: AppRouteRecord): boolean {
+ return Boolean(
+ route.path &&
+ route.path !== '/' &&
+ !route.meta?.link &&
+ route.meta?.isIframe !== true &&
+ route.component &&
+ route.component !== ''
+ )
+ }
+
+ /**
+ * 验证菜单路径配置
+ * 检测非一级菜单是否错误使用了 / 开头的路径
+ */
+ /**
+ * 验证菜单路径配置
+ * 检测非一级菜单是否错误使用了 / 开头的路径
+ */
+ 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/admin/src/router/core/RoutePermissionValidator.ts b/admin/src/router/core/RoutePermissionValidator.ts
new file mode 100644
index 0000000..1e607e0
--- /dev/null
+++ b/admin/src/router/core/RoutePermissionValidator.ts
@@ -0,0 +1,161 @@
+/**
+ * 路由权限验证模块
+ *
+ * 提供路由权限验证和路径检查功能
+ *
+ * ## 主要功能
+ *
+ * - 验证路径是否在用户菜单权限中
+ * - 构建菜单路径集合(扁平化处理)
+ * - 支持动态路由参数匹配
+ * - 路径前缀匹配
+ *
+ * ## 使用场景
+ *
+ * - 路由守卫中验证用户权限
+ * - 动态路由注册后的权限检查
+ * - 防止用户访问无权限的页面
+ *
+ * @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
+ }
+
+ return this.matchRoute(targetPath, menuList)
+ }
+
+ /**
+ * 构建菜单路径集合(扁平化处理)
+ * @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.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) {
+ if (targetPath.startsWith(`${menuPath}/`)) {
+ return true
+ }
+ }
+ return false
+ }
+
+ /**
+ * 递归匹配路由配置,支持隐藏路由和动态参数路由
+ */
+ static matchRoute(targetPath: string, routes: AppRouteRecord[]): boolean {
+ if (!Array.isArray(routes) || routes.length === 0) {
+ return false
+ }
+
+ for (const route of routes) {
+ if (!route.path) {
+ continue
+ }
+
+ const routePath = route.path.startsWith('/') ? route.path : `/${route.path}`
+
+ if (
+ routePath === targetPath ||
+ this.isDynamicRouteMatch(targetPath, routePath) ||
+ targetPath.startsWith(`${routePath}/`)
+ ) {
+ return true
+ }
+
+ if (route.children?.length && this.matchRoute(targetPath, route.children)) {
+ return true
+ }
+ }
+
+ return false
+ }
+
+ /**
+ * 检查目标路径是否匹配动态参数路由,如 /demo/123 匹配 /demo/:id
+ */
+ static isDynamicRouteMatch(targetPath: string, routePath: string): boolean {
+ if (!routePath.includes(':')) {
+ return false
+ }
+
+ const pattern = routePath
+ .replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
+ .replace(/:([^/]+)/g, '[^/]+')
+ .replace(/\\\*/g, '.*')
+
+ return new RegExp(`^${pattern}$`).test(targetPath)
+ }
+
+ /**
+ * 验证并返回有效的路径
+ * 如果目标路径无权限,返回首页路径
+ * @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/admin/src/router/core/RouteRegistry.ts b/admin/src/router/core/RouteRegistry.ts
new file mode 100644
index 0000000..e1acb9e
--- /dev/null
+++ b/admin/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/admin/src/router/core/RouteTransformer.ts b/admin/src/router/core/RouteTransformer.ts
new file mode 100644
index 0000000..0f6900c
--- /dev/null
+++ b/admin/src/router/core/RouteTransformer.ts
@@ -0,0 +1,132 @@
+/**
+ * 路由转换器
+ *
+ * 负责将菜单数据转换为 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 as string)
+ } else {
+ this.handleNormalRoute(converted, component as string)
+ }
+
+ // 递归处理子路由
+ 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: string | undefined
+ ): void {
+ converted.component = this.componentLoader.loadLayout()
+ converted.path = this.extractFirstSegment(route.path || '')
+ converted.name = ''
+ route.meta.isFirstLevel = true
+
+ converted.children = [
+ {
+ ...route,
+ component: component ? this.componentLoader.load(component) : undefined
+ } as ConvertedRoute
+ ]
+ }
+
+ /**
+ * 处理普通路由
+ */
+ private handleNormalRoute(converted: ConvertedRoute, component: string | undefined): void {
+ if (component) {
+ converted.component = this.componentLoader.load(component)
+ }
+ }
+
+ /**
+ * 提取路径的第一段
+ */
+ private extractFirstSegment(path: string): string {
+ const segments = path.split('/').filter(Boolean)
+ return segments.length > 0 ? `/${segments[0]}` : '/'
+ }
+}
diff --git a/admin/src/router/core/RouteValidator.ts b/admin/src/router/core/RouteValidator.ts
new file mode 100644
index 0000000..f8e58fc
--- /dev/null
+++ b/admin/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/admin/src/router/core/index.ts b/admin/src/router/core/index.ts
new file mode 100644
index 0000000..fcfecfc
--- /dev/null
+++ b/admin/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/admin/src/router/guards/afterEach.ts b/admin/src/router/guards/afterEach.ts
new file mode 100644
index 0000000..d60572d
--- /dev/null
+++ b/admin/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/admin/src/router/guards/beforeEach.ts b/admin/src/router/guards/beforeEach.ts
new file mode 100644
index 0000000..c0d4b0c
--- /dev/null
+++ b/admin/src/router/guards/beforeEach.ts
@@ -0,0 +1,452 @@
+/**
+ * 路由全局前置守卫模块
+ *
+ * 提供完整的路由导航守卫功能
+ *
+ * ## 主要功能
+ *
+ * - 登录状态验证和重定向
+ * - 动态路由注册和权限控制
+ * - 菜单数据获取和处理(前端/后端模式)
+ * - 用户信息获取和缓存
+ * - 页面标题设置
+ * - 工作标签页管理
+ * - 进度条和加载动画控制
+ * - 静态路由识别和处理
+ * - 错误处理和异常跳转
+ *
+ * ## 使用场景
+ *
+ * - 路由跳转前的权限验证
+ * - 动态菜单加载和路由注册
+ * - 用户登录状态管理
+ * - 页面访问控制
+ * - 路由级别的加载状态管理
+ *
+ * ## 工作流程
+ *
+ * 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 { useAuthStore } from '@/store/modules/auth'
+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 { 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. 检查登录状态(会话事实源 = auth store 的 token)
+ if (!handleLoginStatus(to, userStore, next)) {
+ return
+ }
+
+ // 2. 强制改密门:会话有效但 must_change → 除改密页外一律重定向
+ // (含登录页——已登录的引导流程不回登录;后端 409 语义的前端镜像)
+ if (!handleMustChangePassword(to, next)) {
+ return
+ }
+
+ // 3. 检查路由初始化是否已失败(防止死循环)
+ if (routeInitFailed) {
+ // 已经失败过,直接放行到错误页面,不再重试
+ if (to.matched.length > 0) {
+ next()
+ } else {
+ // 未匹配到路由,跳转到 500 页面
+ next({ name: 'Exception500', replace: true })
+ }
+ return
+ }
+
+ // 4. 处理动态路由注册
+ if (!routeRegistry?.isRegistered() && userStore.isLogin) {
+ // 防止并发请求(快速连续导航场景)
+ if (routeInitInProgress) {
+ // 正在初始化中,等待完成后重新导航
+ next(false)
+ return
+ }
+ await handleDynamicRoutes(to, next, router)
+ return
+ }
+
+ // 5. 处理根路径重定向
+ if (handleRootPathRedirect(to, next)) {
+ return
+ }
+
+ // 6. 处理已匹配的路由
+ if (to.matched.length > 0) {
+ setWorktab(to)
+ setPageTitle(to)
+ next()
+ return
+ }
+
+ // 7. 未匹配到路由,跳转到 404
+ next({ name: 'Exception404' })
+}
+
+/**
+ * 强制改密门
+ *
+ * 会话有效且 must_change_password=true 时,除强制改密页本身外
+ * 一律重定向(含登录页与静态异常页——改完初始密码前不进后台)。
+ * 后端同语义:受保护端点 409(HTTP client 全局跳转兜底)。
+ *
+ * @returns true 表示可以继续,false 表示已处理跳转
+ */
+function handleMustChangePassword(
+ to: RouteLocationNormalized,
+ next: NavigationGuardNext
+): boolean {
+ const authStore = useAuthStore()
+ if (!authStore.isAuthenticated || !authStore.mustChangePassword) {
+ return true
+ }
+ if (to.path === RoutesAlias.ForceChangePassword) {
+ next()
+ return false
+ }
+ next({ path: RoutesAlias.ForceChangePassword, replace: true })
+ return false
+}
+
+/**
+ * 处理登录状态
+ *
+ * 会话事实源为 auth store 的 token(localStorage 持久化,刷新恢复);
+ * 模板 user store 的 isLogin 由登录/登出同步维护(此处对遗留残留自愈)。
+ * @returns true 表示可以继续,false 表示已处理跳转
+ */
+function handleLoginStatus(
+ to: RouteLocationNormalized,
+ userStore: ReturnType,
+ next: NavigationGuardNext
+): boolean {
+ const authStore = useAuthStore()
+
+ // 已认证:放行(自愈模板侧登录标记——如 Task 2 桩时代的残留状态)
+ if (authStore.isAuthenticated) {
+ if (!userStore.isLogin) {
+ userStore.setLoginStatus(true)
+ }
+ return true
+ }
+
+ // 未认证:登录页/静态路由放行,但强制改密页需要会话——回登录页
+ if (to.path === RoutesAlias.Login || isStaticRoute(to.path)) {
+ if (to.path === RoutesAlias.ForceChangePassword) {
+ next({ name: 'Login', replace: true })
+ return false
+ }
+ return true
+ }
+
+ // 未登录且访问需要权限的页面,跳转到登录页并携带 redirect 参数
+ next({
+ name: 'Login',
+ query: { redirect: to.fullPath }
+ })
+ return false
+}
+
+/**
+ * 检查路由是否为静态路由
+ */
+function isStaticRoute(path: string): boolean {
+ const checkRoute = (routes: any[], targetPath: string): boolean => {
+ return routes.some((route) => {
+ // 404 catch-all 路由不应视为可匿名访问的静态页,
+ // 否则未登录时手动输入任意地址会直接落到 404,无法跳转登录页。
+ if (route.name === 'Exception404') {
+ return false
+ }
+
+ // 处理动态路由参数匹配
+ 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. 派生用户信息(auth store 登录快照 → 模板 UserInfo)
+ deriveUserInfo()
+
+ // 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. 静态路由不依赖菜单权限,初始化后直接恢复目标地址。
+ if (isStaticRoute(to.path)) {
+ routeInitInProgress = false
+ next({
+ path: to.path,
+ query: to.query,
+ hash: to.hash,
+ replace: true
+ })
+ return
+ }
+
+ // 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()
+
+ // 标记初始化失败,防止死循环
+ routeInitFailed = true
+ routeInitInProgress = false
+
+ // 输出详细错误信息,便于排查
+ if (isHttpError(error)) {
+ console.error(`[RouteGuard] 错误码: ${error.code}, 消息: ${error.message}`)
+ }
+
+ // 跳转到 500 页面,使用 replace 避免产生历史记录
+ next({ name: 'Exception500', replace: true })
+ }
+}
+
+/**
+ * 派生用户信息
+ *
+ * 后端无用户信息端点——登录响应(role/username)已随 auth store 持久化,
+ * 此处映射为模板消费形态(roles 供菜单角色过滤,见 MenuProcessor)。
+ */
+function deriveUserInfo(): void {
+ const authStore = useAuthStore()
+ const userStore = useUserStore()
+ userStore.setUserInfo(authStore.userInfo)
+ // 检查并清理工作台标签页(如果是不同用户登录)
+ 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
+}
diff --git a/admin/src/router/index.ts b/admin/src/router/index.ts
new file mode 100644
index 0000000..286ae58
--- /dev/null
+++ b/admin/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/admin/src/router/modules/config.ts b/admin/src/router/modules/config.ts
new file mode 100644
index 0000000..7be14da
--- /dev/null
+++ b/admin/src/router/modules/config.ts
@@ -0,0 +1,24 @@
+import { AppRouteRecord } from '@/types/router'
+
+/** 配置中心——超管与租户管理员均可见(租户配置组/站点/品牌) */
+export const configRoutes: AppRouteRecord = {
+ name: 'Config',
+ path: '/config',
+ component: '/index/index',
+ meta: {
+ title: 'menus.config.title',
+ icon: 'ri:settings-4-line'
+ },
+ children: [
+ {
+ path: 'center',
+ name: 'ConfigCenter',
+ component: '/config/center',
+ meta: {
+ title: 'menus.config.center',
+ icon: 'ri:equalizer-line',
+ keepAlive: false
+ }
+ }
+ ]
+}
diff --git a/admin/src/router/modules/conversations.ts b/admin/src/router/modules/conversations.ts
new file mode 100644
index 0000000..ef96bc3
--- /dev/null
+++ b/admin/src/router/modules/conversations.ts
@@ -0,0 +1,24 @@
+import { AppRouteRecord } from '@/types/router'
+
+/** 会话与线索——超管与租户管理员均可见 */
+export const conversationsRoutes: AppRouteRecord = {
+ name: 'Conversations',
+ path: '/conversations',
+ component: '/index/index',
+ meta: {
+ title: 'menus.conversations.title',
+ icon: 'ri:chat-3-line'
+ },
+ children: [
+ {
+ path: 'list',
+ name: 'ConversationList',
+ component: '/conversations/list',
+ meta: {
+ title: 'menus.conversations.list',
+ icon: 'ri:chat-4-line',
+ keepAlive: false
+ }
+ }
+ ]
+}
diff --git a/admin/src/router/modules/dashboard.ts b/admin/src/router/modules/dashboard.ts
new file mode 100644
index 0000000..b1d2455
--- /dev/null
+++ b/admin/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:dashboard-line'
+ },
+ children: [
+ {
+ path: 'console',
+ name: 'Console',
+ component: '/dashboard/console',
+ meta: {
+ title: 'menus.dashboard.console',
+ icon: 'ri:home-smile-2-line',
+ keepAlive: false,
+ fixedTab: true
+ }
+ }
+ ]
+}
diff --git a/admin/src/router/modules/index.ts b/admin/src/router/modules/index.ts
new file mode 100644
index 0000000..c7271fc
--- /dev/null
+++ b/admin/src/router/modules/index.ts
@@ -0,0 +1,20 @@
+import { AppRouteRecord } from '@/types/router'
+import { dashboardRoutes } from './dashboard'
+import { tenantsRoutes } from './tenants'
+import { conversationsRoutes } from './conversations'
+import { configRoutes } from './config'
+import { statsRoutes } from './stats'
+import { settingsRoutes } from './settings'
+
+/**
+ * 导出所有模块化路由(前端权限模式:菜单按 meta.roles 过滤,
+ * 角色值与后端一致:super 全量;tenant 隐藏 租户管理/平台设置)
+ */
+export const routeModules: AppRouteRecord[] = [
+ dashboardRoutes,
+ tenantsRoutes,
+ conversationsRoutes,
+ configRoutes,
+ statsRoutes,
+ settingsRoutes
+]
diff --git a/admin/src/router/modules/settings.ts b/admin/src/router/modules/settings.ts
new file mode 100644
index 0000000..cd86376
--- /dev/null
+++ b/admin/src/router/modules/settings.ts
@@ -0,0 +1,25 @@
+import { AppRouteRecord } from '@/types/router'
+
+/** 平台设置——仅超管(super)可见,租户管理员整个菜单组隐藏 */
+export const settingsRoutes: AppRouteRecord = {
+ name: 'Settings',
+ path: '/settings',
+ component: '/index/index',
+ meta: {
+ title: 'menus.settings.title',
+ icon: 'ri:shield-keyhole-line',
+ roles: ['super']
+ },
+ children: [
+ {
+ path: 'platform',
+ name: 'PlatformSettings',
+ component: '/settings/platform',
+ meta: {
+ title: 'menus.settings.platform',
+ icon: 'ri:shield-star-line',
+ keepAlive: false
+ }
+ }
+ ]
+}
diff --git a/admin/src/router/modules/stats.ts b/admin/src/router/modules/stats.ts
new file mode 100644
index 0000000..63102f8
--- /dev/null
+++ b/admin/src/router/modules/stats.ts
@@ -0,0 +1,24 @@
+import { AppRouteRecord } from '@/types/router'
+
+/** 统计分析——超管与租户管理员均可见 */
+export const statsRoutes: AppRouteRecord = {
+ name: 'Stats',
+ path: '/stats',
+ component: '/index/index',
+ meta: {
+ title: 'menus.stats.title',
+ icon: 'ri:bar-chart-2-line'
+ },
+ children: [
+ {
+ path: 'overview',
+ name: 'StatsOverview',
+ component: '/stats/overview',
+ meta: {
+ title: 'menus.stats.overview',
+ icon: 'ri:line-chart-line',
+ keepAlive: false
+ }
+ }
+ ]
+}
diff --git a/admin/src/router/modules/tenants.ts b/admin/src/router/modules/tenants.ts
new file mode 100644
index 0000000..afbe573
--- /dev/null
+++ b/admin/src/router/modules/tenants.ts
@@ -0,0 +1,25 @@
+import { AppRouteRecord } from '@/types/router'
+
+/** 租户管理——仅超管(super)可见,租户管理员整个菜单组隐藏 */
+export const tenantsRoutes: AppRouteRecord = {
+ name: 'Tenants',
+ path: '/tenants',
+ component: '/index/index',
+ meta: {
+ title: 'menus.tenants.title',
+ icon: 'ri:building-2-line',
+ roles: ['super']
+ },
+ children: [
+ {
+ path: 'list',
+ name: 'TenantList',
+ component: '/tenants/list',
+ meta: {
+ title: 'menus.tenants.list',
+ icon: 'ri:building-line',
+ keepAlive: false
+ }
+ }
+ ]
+}
diff --git a/admin/src/router/routes/asyncRoutes.ts b/admin/src/router/routes/asyncRoutes.ts
new file mode 100644
index 0000000..ccf1201
--- /dev/null
+++ b/admin/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/admin/src/router/routes/staticRoutes.ts b/admin/src/router/routes/staticRoutes.ts
new file mode 100644
index 0000000..9326e47
--- /dev/null
+++ b/admin/src/router/routes/staticRoutes.ts
@@ -0,0 +1,66 @@
+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/force-change-password',
+ name: 'ForceChangePassword',
+ component: () => import('@views/auth/force-change-password/index.vue'),
+ meta: { title: 'menus.forceChangePassword.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/admin/src/router/routesAlias.ts b/admin/src/router/routesAlias.ts
new file mode 100644
index 0000000..e66113b
--- /dev/null
+++ b/admin/src/router/routesAlias.ts
@@ -0,0 +1,9 @@
+/**
+ * 公共路由别名
+ * 存放系统级公共路由路径,如布局容器、登录页等
+ */
+export enum RoutesAlias {
+ Layout = '/index/index', // 布局容器
+ Login = '/auth/login', // 登录页
+ ForceChangePassword = '/auth/force-change-password' // 强制修改密码页
+}
diff --git a/admin/src/store/index.ts b/admin/src/store/index.ts
new file mode 100644
index 0000000..b485999
--- /dev/null
+++ b/admin/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/admin/src/store/modules/auth.ts b/admin/src/store/modules/auth.ts
new file mode 100644
index 0000000..9b2563c
--- /dev/null
+++ b/admin/src/store/modules/auth.ts
@@ -0,0 +1,188 @@
+/**
+ * 认证状态管理(Phase 3 Task 3)
+ *
+ * 会话事实源:token / role / tenantSlug / username / mustChangePassword。
+ * 持久化 localStorage(键 all-kefu-auth,all-kefu 前缀与模板 sys-v* 键区隔),
+ * 刷新页面恢复会话;路由守卫与 HTTP client(401/409 全局处理)消费。
+ *
+ * 模板侧同步:登录成功后把 token / 登录标记 / 用户信息(roles 供菜单过滤)
+ * 同步进模板的 user store(isLogin/accessToken/info 是模板路由初始化与
+ * 顶栏展示的既有依赖);登出委托 userStore.logOut() 完成模板全套清理
+ * (动态路由注销、工作台标签、跳登录页)。
+ *
+ * 与 @/api/client 的模块环(client → 本模块 → @/api/auth → client):
+ * 双方均只在函数体内访问对方导出(ESM 活绑定),模块求值期无交叉读取。
+ */
+import { defineStore } from 'pinia'
+import { computed, ref } from 'vue'
+import { fetchChangePassword, fetchLogin, fetchLogout } from '@/api/auth'
+import { useUserStore } from '@/store/modules/user'
+import { RoutesAlias } from '@/router/routesAlias'
+
+/** 平台角色(与后端 token 快照一致) */
+export type AdminRole = 'super' | 'tenant'
+
+/** 登录参数(POST /api/platform/auth/login) */
+export interface LoginParams {
+ username: string
+ password: string
+}
+
+/** 改密参数(POST /api/platform/auth/change-password) */
+export interface ChangePasswordParams {
+ old_password: string
+ new_password: string
+}
+
+/** 登录响应(后端 auth_routes.login) */
+export interface LoginResponse {
+ token: string
+ role: AdminRole
+ tenant_slug: string | null
+ must_change_password: boolean
+}
+
+/** 持久化键(all-kefu 前缀;user store 等模板键为 sys-v* 体系) */
+const AUTH_STORAGE_KEY = 'all-kefu-auth'
+
+/**
+ * 用户名 → 稳定伪 ID(djb2 哈希)
+ *
+ * 后端不下发 user_id;模板 user store 的工作台跨用户清理
+ * (checkAndClearWorktabs)按 userId 比对,用确定性哈希保持该语义。
+ */
+function stableUserId(username: string): number {
+ let hash = 5381
+ for (let i = 0; i < username.length; i++) {
+ hash = ((hash << 5) + hash + username.charCodeAt(i)) | 0
+ }
+ return Math.abs(hash) || 1
+}
+
+export const useAuthStore = defineStore(
+ 'authStore',
+ () => {
+ // ---- 状态 ------------------------------------------------------------------
+ const token = ref('')
+ const role = ref('')
+ const tenantSlug = ref(null)
+ const username = ref('')
+ const mustChangePassword = ref(false)
+
+ // ---- 计算属性 ----------------------------------------------------------------
+ const isAuthenticated = computed(() => token.value !== '')
+
+ /** 模板 Api.Auth.UserInfo 映射(守卫初始化动态路由/菜单角色过滤用) */
+ const userInfo = computed(() => ({
+ buttons: [],
+ roles: role.value ? [role.value] : [],
+ userId: stableUserId(username.value),
+ userName: username.value,
+ email: ''
+ }))
+
+ // ---- 内部工具 ----------------------------------------------------------------
+ /** 登录成功后的状态落地(不调 API) */
+ function applyLogin(loginUsername: string, res: LoginResponse): void {
+ token.value = res.token
+ role.value = res.role
+ tenantSlug.value = res.tenant_slug
+ mustChangePassword.value = res.must_change_password
+ username.value = loginUsername
+ syncUserStore()
+ }
+
+ /** 同步模板 user store(模板路由初始化/顶栏依赖其状态) */
+ function syncUserStore(): void {
+ const userStore = useUserStore()
+ userStore.setToken(token.value, '')
+ userStore.setLoginStatus(true)
+ userStore.setUserInfo(userInfo.value)
+ userStore.checkAndClearWorktabs()
+ }
+
+ /** 清空本 store 全部会话状态 */
+ function clearSession(): void {
+ token.value = ''
+ role.value = ''
+ tenantSlug.value = null
+ username.value = ''
+ mustChangePassword.value = false
+ }
+
+ /**
+ * 页面跳转(动态 import 断开与 router 模块的静态环;
+ * 已在目标页则不重复跳——防 401/409 拦截死循环)
+ */
+ async function goTo(path: string): Promise {
+ const { router } = await import('@/router')
+ if (router.currentRoute.value.path === path) return
+ await router.replace(path)
+ }
+
+ // ---- Actions -----------------------------------------------------------------
+ /** 登录:调 API 成功后落地会话状态(失败向上抛,由调用方/全局拦截提示) */
+ async function login(params: LoginParams): Promise {
+ const res = await fetchLogin(params)
+ applyLogin(params.username, res)
+ return res
+ }
+
+ /**
+ * 登出:调后端撤销 token(best effort,失败也照常本地清理),
+ * 清空会话后委托模板 userStore.logOut()(动态路由注销 + 跳登录页)
+ */
+ async function logout(): Promise {
+ try {
+ await fetchLogout()
+ } catch {
+ // token 已失效/网络不可达:本地清理照常进行
+ }
+ clearSession()
+ useUserStore().logOut()
+ }
+
+ /**
+ * 改密:成功后清 must_change 标志(后端已就地清当前 token 的标志,
+ * 会话保持,改完无需重登)
+ */
+ async function changePassword(params: ChangePasswordParams): Promise {
+ await fetchChangePassword(params)
+ mustChangePassword.value = false
+ }
+
+ /** 401 全局处理:清会话 + 模板侧全套清理(userStore.logOut 含跳登录) */
+ function handleSessionExpired(): void {
+ clearSession()
+ useUserStore().logOut()
+ }
+
+ /** 409 强制改密全局处理:置标志 + 跳改密页(已在改密页不重复跳) */
+ async function handleMustChangePassword(): Promise {
+ mustChangePassword.value = true
+ await goTo(RoutesAlias.ForceChangePassword)
+ }
+
+ return {
+ token,
+ role,
+ tenantSlug,
+ username,
+ mustChangePassword,
+ isAuthenticated,
+ userInfo,
+ login,
+ logout,
+ changePassword,
+ clearSession,
+ handleSessionExpired,
+ handleMustChangePassword
+ }
+ },
+ {
+ persist: {
+ key: AUTH_STORAGE_KEY,
+ storage: localStorage
+ }
+ }
+)
diff --git a/admin/src/store/modules/menu.ts b/admin/src/store/modules/menu.ts
new file mode 100644
index 0000000..85d13da
--- /dev/null
+++ b/admin/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/admin/src/store/modules/setting.ts b/admin/src/store/modules/setting.ts
new file mode 100644
index 0000000..2878259
--- /dev/null
+++ b/admin/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/admin/src/store/modules/table.ts b/admin/src/store/modules/table.ts
new file mode 100644
index 0000000..094c310
--- /dev/null
+++ b/admin/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/admin/src/store/modules/user.ts b/admin/src/store/modules/user.ts
new file mode 100644
index 0000000..6ae2fff
--- /dev/null
+++ b/admin/src/store/modules/user.ts
@@ -0,0 +1,237 @@
+/**
+ * 用户状态管理模块
+ *
+ * 提供用户相关的状态管理
+ *
+ * ## 主要功能
+ *
+ * - 用户登录状态管理
+ * - 用户信息存储
+ * - 访问令牌和刷新令牌管理
+ * - 语言设置
+ * - 搜索历史记录
+ * - 锁屏状态和密码管理
+ * - 登出清理逻辑
+ *
+ * ## 使用场景
+ *
+ * - 用户登录和认证
+ * - 权限验证
+ * - 个人信息展示
+ * - 多语言切换
+ * - 锁屏功能
+ * - 搜索历史管理
+ *
+ * ## 持久化
+ *
+ * - 使用 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 { RoutesAlias } from '@/router/routesAlias'
+import { StorageConfig } from '@/utils/storage/storage-config'
+
+/**
+ * 用户状态管理
+ * 管理用户登录状态、个人信息、语言设置、搜索历史、锁屏状态等
+ */
+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 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 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 = ''
+ // 注意:不清空工作台标签页,等下次登录时根据用户判断
+ // 移除iframe路由缓存
+ sessionStorage.removeItem('iframeRoutes')
+ // 清空主页路径
+ useMenuStore().setHomePath('')
+ // 重置路由状态
+ resetRouterState(500)
+ // 跳转到登录页,携带当前路由作为 redirect 参数
+ // (已在登录页则不带——否则登录成功后会跳回登录页自身)
+ const currentRoute = router.currentRoute.value
+ const redirect = currentRoute.path !== RoutesAlias.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,
+ getUserInfo,
+ getSettingState,
+ getWorktabState,
+ setUserInfo,
+ setLoginStatus,
+ setLanguage,
+ setSearchHistory,
+ setLockStatus,
+ setLockPassword,
+ setToken,
+ logOut,
+ checkAndClearWorktabs
+ }
+ },
+ {
+ persist: {
+ key: 'user',
+ storage: localStorage
+ }
+ }
+)
diff --git a/admin/src/store/modules/worktab.ts b/admin/src/store/modules/worktab.ts
new file mode 100644
index 0000000..caa0d90
--- /dev/null
+++ b/admin/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