yinxia.yang 1 өдөр өмнө
commit
9b095f9988
74 өөрчлөгдсөн 11691 нэмэгдсэн , 0 устгасан
  1. 13 0
      .env.development
  2. 12 0
      .env.production
  3. 24 0
      .gitignore
  4. 132 0
      README.md
  5. 13 0
      index.html
  6. 5163 0
      package-lock.json
  7. 33 0
      package.json
  8. 0 0
      public/favicon.svg
  9. 24 0
      public/icons.svg
  10. 57 0
      src/App.vue
  11. 114 0
      src/api/index.ts
  12. 9 0
      src/api/indexType.ts
  13. 18 0
      src/api/types/TableDataInfo.ts
  14. 19 0
      src/api/types/WMS_ledger_flow_detail_request.ts
  15. 16 0
      src/api/types/WmsBasicDataQueryRequest.ts
  16. 55 0
      src/api/types/WmsBasicDataResponse.ts
  17. 27 0
      src/api/types/WmsBasicDataSaveRequest.ts
  18. 29 0
      src/api/types/WmsDashboardResponse.ts
  19. 5 0
      src/api/types/WmsDashboardTodayFlowVO.ts
  20. 35 0
      src/api/types/WmsFileAttachmentResponse.ts
  21. 11 0
      src/api/types/WmsIdRequest.ts
  22. 36 0
      src/api/types/WmsInboundPageRequest.ts
  23. 104 0
      src/api/types/WmsInboundRecordResponse.ts
  24. 47 0
      src/api/types/WmsInboundSaveRequest.ts
  25. 3 0
      src/api/types/WmsInboundSubmitRequest.ts
  26. 20 0
      src/api/types/WmsLedgerAdminSummaryResponse.ts
  27. 52 0
      src/api/types/WmsLedgerFlowQueryRequest.ts
  28. 113 0
      src/api/types/WmsLedgerFlowVO.ts
  29. 40 0
      src/api/types/WmsOutboundPageRequest.ts
  30. 112 0
      src/api/types/WmsOutboundRecordResponse.ts
  31. 55 0
      src/api/types/WmsOutboundSaveRequest.ts
  32. 3 0
      src/api/types/WmsOutboundSubmitRequest.ts
  33. 15 0
      src/api/types/WmsReversalPreCheckRequest.ts
  34. 47 0
      src/api/types/WmsReversalPreCheckResponse.ts
  35. 85 0
      src/api/types/WmsReversalRecordResponse.ts
  36. 31 0
      src/api/types/WmsReversalSubmitRequest.ts
  37. 28 0
      src/api/types/WmsSkuQueryPageRequest.ts
  38. 83 0
      src/api/types/WmsSkuResponse.ts
  39. 47 0
      src/api/types/WmsSkuSaveRequest.ts
  40. 11 0
      src/api/types/WmsSkuSearchRequest.ts
  41. 27 0
      src/api/types/WmsSkuUpdateRequest.ts
  42. 15 0
      src/api/types/WmsStatusUpdateRequest.ts
  43. 8 0
      src/api/types/common.ts
  44. 31 0
      src/api/types/index.ts
  45. BIN
      src/assets/hero.png
  46. 0 0
      src/assets/vite.svg
  47. 1 0
      src/assets/vue.svg
  48. 35 0
      src/components/EntryAudit.vue
  49. 49 0
      src/components/EvidenceUpload.vue
  50. 95 0
      src/components/HelloWorld.vue
  51. 50 0
      src/http/index.ts
  52. 14 0
      src/main.ts
  53. 26 0
      src/router/index.ts
  54. 108 0
      src/store/index.ts
  55. 693 0
      src/style/operations.css
  56. 883 0
      src/style/prototype.css
  57. 296 0
      src/style/style.css
  58. 152 0
      src/utils/auth.ts
  59. 6 0
      src/utils/entry.ts
  60. 96 0
      src/utils/ui.ts
  61. 81 0
      src/utils/upload.ts
  62. 9 0
      src/utils/uploads.ts
  63. 390 0
      src/views/Dashboard.vue
  64. 366 0
      src/views/Inbound.vue
  65. 283 0
      src/views/Ledger.vue
  66. 79 0
      src/views/Login.vue
  67. 199 0
      src/views/Login/index.vue
  68. 224 0
      src/views/Master.vue
  69. 386 0
      src/views/Outbound.vue
  70. 290 0
      src/views/Sku.vue
  71. 15 0
      tsconfig.app.json
  72. 7 0
      tsconfig.json
  73. 23 0
      tsconfig.node.json
  74. 13 0
      vite.config.ts

+ 13 - 0
.env.development

@@ -0,0 +1,13 @@
+VITE_USE_MOCK=true
+
+# 主服务
+VITE_API_BASE_URL=/api
+VITE_API_WMS_URL=http://192.168.32.159:8096
+
+
+# 授权服务
+VITE_API_AUTH_URL=https://oidc-dev.mangoo.hobbystocks.cn
+VITE_OAUTH_RESPONSE_MODE=form_post
+# login
+VITE_OAUTH_REDIRECT_URI=http://192.168.32.135:5173/login
+

+ 12 - 0
.env.production

@@ -0,0 +1,12 @@
+VITE_USE_MOCK=false
+
+# 主服务
+VITE_API_BASE_URL=/api
+VITE_API_WMS_URL=/
+
+
+# 授权服务
+VITE_API_AUTH_URL=https://oidc-dev.mangoo.hobbystocks.cn
+VITE_OAUTH_RESPONSE_MODE=form_post
+# login
+VITE_OAUTH_REDIRECT_URI=http://192.168.32.135:5173/login

+ 24 - 0
.gitignore

@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?

+ 132 - 0
README.md

@@ -0,0 +1,132 @@
+# HS 仓储运营台
+
+上海闵行仓一期前端工程。Vue 3、TypeScript、Vite、Element Plus、Pinia、Axios、MSW。原始 PRD、交互说明及 HTML 原型保留在上级目录。
+
+## 运行
+
+建议 Node.js 22.13+。已在 Node 22.12 下通过构建及测试,但部分开发依赖提示更高版本要求。
+
+在本目录执行:
+
+```sh
+npm install
+npm run dev
+```
+
+打开终端打印的地址,通常是 http://localhost:5173/。演示账号 `demo`,密码 `123`,仅用于本地 Mock。
+
+```sh
+npm test
+npm run build
+npm run preview
+```
+
+开发模式默认开启 Mock。生产构建默认关闭 Mock,`preview` 需要配套后端,不会自动使用演示数据。构建有大 chunk 警告,不影响运行;正式发布前建议进行路由和组件按需拆包。
+
+## 功能与数据
+
+- 登录、工作台、基础资料、SKU、入库、出库、库存台账。
+- 草稿不影响库存,可编辑删除;完成流水不可编辑删除,使用反向流水冲销。
+- 正整数数量、禁止负库存、剩余可冲销数量限制、历史 SKU 快照。
+- 内外部订单、条件物流字段、图片上传及预览。
+- 查询筛选、分页、SKU 库存和关联流水查看。
+
+工作台当日笔数按上海时区的完成时间统计,包含冲销产生的对应方向流水,不跨 SKU 汇总数量。
+
+Mock 数据保存在当前浏览器、当前站点的 IndexedDB(idb-keyval 默认数据库内的 `warehouse-db-v1` 键),刷新后保留。图片以 data URL 存储,单张支持 5MB 内 PNG/JPEG/WebP,不适合真实大量附件。会话放在 sessionStorage。不要录入敏感或正式业务数据,也不要多标签页并发写入;Mock 不提供跨标签页事务一致性。
+
+首次使用自动载入原型 Mock 数据:9 项基础资料、5 个 SKU、8 条已完成流水及 2 条草稿,含内部订单、外部订单、快递、闪送和部分冲销场景。SKU000001 至 SKU000005 的库存依次为 1 个、1 盒、3 个、2 盒、10 包。示例凭证可预览,明确标记 Mock,不是真实订单或物流凭证。
+
+种子数据保留原型的 2026-09-05 至 2026-09-07 日期,其他日期查看工作台时,当日出入库笔数不会包含这些历史流水。仅新库或基础资料完全未修改的原始空库会自动初始化,已有业务记录或基础资料修改不会被覆盖。清理站点数据会删除全部本地业务记录,请先确认无需要保留的数据。
+
+## 接口接入
+
+### 统一认证登录
+
+真实认证模式下,业务接口返回 HTTP 401 时自动清理认证态,生成新的 PKCE 会话并跳转认证中心;同一页面跳转期间的并发 401 只处理一次。Mock 模式仍返回本地登录页,403 不触发认证跳转。授权配置错误或跳转失败时回退到登录页手动重试;主动退出仍暂停自动认证。此并发保护不限制跨页面的重新认证次数,若后端持续拒绝新令牌,可能反复跳转,需排查认证与业务后端配置。
+
+`src/store/index.ts` 统一导出仓储 `useWarehouse` 和认证 `useStore`(默认导出)。认证工具通过 Pinia 读写令牌、显示名称、角色与 PKCE 临时会话,并同步持久化到当前标签页的 sessionStorage,刷新或授权跳转后可恢复;退出或认证失效时清理相应缓存。PKCE 会话不设置本地过期时间,保留一次性消费校验;重新发起授权会覆盖旧会话,缓存仍受 sessionStorage 生命周期约束。仓储数据不重复缓存到认证 Store,仍由原 API 和 Mock IndexedDB 管理。
+
+`src/views/Login/index.vue` 为登录入口。`VITE_USE_MOCK=true` 时保留本地账号登录;关闭 Mock 后使用 OAuth 授权码 + PKCE S256 登录,认证失败不会自动循环跳转。
+
+在本地环境文件或构建环境中配置以下变量(示例域名需替换为实际认证服务):
+
+```dotenv
+VITE_USE_MOCK=false
+VITE_OAUTH_REDIRECT_URI=http://localhost:5175/
+VITE_OAUTH_RESPONSE_MODE=query
+```
+
+授权服务地址 `VITE_API_AUTH_URL`;系统自动拼接 `/oauth2/authorize` 和 `/oauth2/token`,单独配置的完整接口地址优先。客户端 ID 默认 `client`,实际值必须与认证中心注册信息一致。
+
+认证中心需注册支持 PKCE 的公共客户端,并将回调地址加入白名单。回调地址必须与配置完全一致且不含 `#`,未配置时默认使用当前站点 origin + `/login`,部署服务需将该路径回退到应用入口。默认采用 `response_mode=query`,应用读取地址栏中的 code/state 后清除临时参数,再进入 Hash 路由。只有接入服务端 POST 回调转发后,才可将 `VITE_OAUTH_RESPONSE_MODE` 改为 `form_post`。
+
+Token 接口使用表单编码,需允许前端跨域访问或配置同源代理,返回标准 `access_token` 和 `token_type=Bearer`。不在前端配置 client_secret;若认证中心强制要求客户端密钥或 form_post,应由后端代理完成回调与令牌交换。未接入权限接口,不将未经验证的 JWT 内容作为权限依据;可选的令牌响应字段 displayName 仅用于界面显示。
+
+PKCE 使用 CryptoJS 同步计算 SHA-256/Base64URL,随机值仍由 `crypto.getRandomValues` 生成。授权链接包含随机 state 和 nonce,临时会话使用 sessionStorage 持久化,不设置本地过期时间,保留 state、回调地址和一次性消费校验;认证中心仍决定授权码和令牌的有效期。目前不消费 ID Token,nonce 发送和缓存不代表已完成 ID Token 校验。生产环境应使用 HTTPS。现有本地退出只清除本应用会话,不会注销认证中心的 SSO 会话。环境变量修改后需重启开发服务或重新构建。
+
+页面通过 `src/api/index.ts` 中的统一接口方法请求;`src/http/index.ts` 配置 Axios 实例、请求 Token 注入和响应 401 拦截;`src/mock.ts` 在启用 Mock 时拦截接口并返回模拟数据;`src/domain.ts` 定义数据模型及库存规则。
+
+在部署环境配置 `VITE_USE_MOCK=false` 和 `VITE_API_BASE_URL`,并为跨域或同源 `/api` 配置真实服务。所有 VITE 变量都会进入前端,不能存储秘密。
+
+### 多服务配置
+
+SKU 管理统一使用 `/api/wms/sku/page` 服务端分页,请求类型为 `WmsSkuQueryPageRequest`,支持 `keyword`、`spec`、`status`、`categoryId`、`sportTypeId`、`brandId` 筛选。`keyword` 同时匹配 SKU 编码和商品名称,不再发送旧的 `skuId`、`productName` 查询字段,也不使用下拉搜索接口替代管理查询。列表按响应 `stock` 展示库存,0 正常显示,字段缺失显示占位符,不在前端计算或跨 SKU 汇总。详情展示 `recentFlows` 字符串列表;契约注明库存仅分页返回,详情不展示未经重新查询的列表库存。
+
+库存台账顶部统计独立请求 `/api/wms/ledger/adminSummary`:优先使用实际接口的 `warehouseName` 展示仓库名称,兼容生成契约中的 `queryScope`;兼容类型定义在应用层,不修改生成文件。`totalStockCount` 为启用 SKU 数,`todayFlowCount` 为按创建时间统计的当日入库、出库、冲销/更正流水数。统计不由当前页或筛选结果推算,冲销成功后重新获取;零值正常展示,加载或失败时显示占位符。
+
+台账普通流水详情同时传递 `flowNo`、`flowType`、`recordId`。冲销方向按 `reversalDirection` 数字值处理(1=入库、2=出库);台账详情缺少方向时回退到原流水类型,列表缺少方向时回退到带符号数量。首页轨迹同时兼容旧的 `IN` / `OUT` 方向值。
+
+当前 `WmsSkuUpdateRequest` 包含分类、运动类型、品牌、规格字段,但生成服务说明仍限定仅修改商品名称、配置明细、备注。前端保留原编辑限制,待后端明确规则后再同步,避免仅依据可选字段扩展业务权限。
+
+WMS 接口通过 `api.wmsApi` 调用。应用使用的接口模型保存在 `src/api/types`,由 `src/api/indexType.ts` 统一转出;这些类型文件需要提交到 Git。`automation/apiType` 仅是临时接口契约,不参与应用编译,也不需要提交。接口更新后需将相关模型同步到 `src/api/types`,再核对 API 封装与页面用法。基础资料 `dataType` 使用数字:1=产品种类、2=运动类型、3=品牌。
+
+出入库分页分别使用 `WmsInboundPageRequest` / `WmsOutboundPageRequest`,与草稿保存请求分离。列表与详情使用新版 `Response` 模型;响应附件从 `attachments` 读取,提交时仍使用 `fileUrls` / `fileNames` / `fileTypes`。操作人优先显示 `updateUser`,为空时使用 `createUser`。分页继续读取顶层 `rows` / `total`,业务成功码兼容 `0` 和 `200`。生成目录仅作为接口契约,运行时仍经 Axios 实例发送请求,不直接使用生成客户端的固定地址。
+
+`services.wms` 使用独立的 `VITE_API_WMS_URL`,只配置服务根地址,不要追加 `/api` 或 `/api/wms`,接口封装已包含 `/api/wms/...` 前缀。开发环境配置为 `http://192.168.32.159:8096`;生产环境配置为 `/`,由部署代理转发同源 `/api/wms` 请求;未配置时也使用同源请求。该实例始终请求真实 WMS,不受 Mock 开关控制,本地 Mock 不代表真实接口已完成联调。
+
+页面继续调用 `api`,由 API 层选择服务。`src/http/index.ts` 中的 `services.warehouse` 和 `services.files` 是独立 Axios 实例,共享 Token 注入和全局并发 401 跳转保护;`http` 保留为仓储实例的兼容别名。认证实例 `authHttp` 不注入业务 Token,也不触发业务 401 跳转。
+
+```dotenv
+VITE_USE_MOCK=false
+VITE_API_BASE_URL=/api
+VITE_API_WAREHOUSE_URL=https://warehouse.example.com/api
+VITE_API_FILES_URL=https://files.example.com/api
+```
+
+两个服务地址未配置时分别回退到 `VITE_API_BASE_URL`,该变量未配置时使用 `/api`。现有业务接口使用仓储服务,上传接口使用文件服务,路径仍为 `/files`。Mock 模式下两者固定使用本地 `/api`,数据仍由 MSW 和 IndexedDB 模拟,不代表已接入真实服务。
+
+后续新增服务,在 `services` 中注册实例,然后在 API 方法中选择该实例:
+
+```ts
+// services 中新增一项,地址应配置为可信后端。
+orders: createServiceHttp(import.meta.env.VITE_API_ORDERS_URL || defaultBaseURL)
+
+// API 方法中调用;响应类型定义在 src/api/indexType.ts。
+orders: async () => (await services.orders.get<Order[]>('/orders')).data
+```
+
+默认服务工厂会发送当前业务 Bearer Token,仅用于接受同一认证体系令牌的可信服务。无需业务认证的服务使用 `createServiceHttp(baseURL, false)`;不同认证体系需要单独设计令牌获取与失效处理,不能直接复用当前 Token。配置跨域地址时需由后端允许当前站点、Authorization 请求头及对应请求方法;相对地址需由开发或部署代理转发,本项目不会根据这些变量自动创建代理。修改环境变量后需要重启开发服务或重新构建。
+
+| 方法 | 路径 | 用途 |
+| --- | --- | --- |
+| POST | /login | `{username,password}`,返回 `{username,token}` |
+| GET | /state | 返回 `{masters,skus,entries,sequence}` |
+| POST | /masters | 新增或更新基础资料 |
+| DELETE | /masters/:id | 删除未使用资料 |
+| POST | /skus | 新增或更新 SKU |
+| POST | /entries | `{data,submit}`,保存草稿或提交 |
+| DELETE | /entries/:id | 删除草稿 |
+| POST | /entries/:id/reverse | `{quantity,evidence,remark}`,冲销 |
+| POST | /files | multipart `file`,返回 `{id,name,url}` |
+
+除登录外使用 `Authorization: Bearer <token>`。字段错误返回 HTTP 422 `{message,fields}`,未登录返回 401。完整实体字段见 domain 类型;当前聚合 state 接口服务于原型联调,真实数据量增长后应改为后端分页查询。
+
+真实后端必须独立实现鉴权、权限、库存事务、并发校验、幂等提交、附件存储及审计,不能将前端校验或 Mock 会话当作安全保证。
+
+## 验证范围
+
+- TypeScript 与 Vite 生产构建通过。
+- 9 项 Vitest 测试通过:6 项业务测试覆盖草稿与重复提交、数量和库存限制、部分及全额冲销、反向回补、凭证及字段清理、快照保留;3 项 Mock 测试覆盖原型库存与关联、流水提交规则、空库初始化及已有数据保护。
+- 浏览器已验证登录、页面创建 SKU、Mock 接口入出库及冲销、台账库存显示;390px 手机页面无页面级横向溢出。
+- 尚未覆盖全部表单分支的自动化 UI 回归,也未接入或验证真实后端。

+ 13 - 0
index.html

@@ -0,0 +1,13 @@
+<!doctype html>
+<html lang="zh-CN">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
+    <title>HS 仓储运营台</title>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script type="module" src="/src/main.ts"></script>
+  </body>
+</html>

+ 5163 - 0
package-lock.json

@@ -0,0 +1,5163 @@
+{
+  "name": "warehouse-web",
+  "version": "0.0.0",
+  "lockfileVersion": 2,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "warehouse-web",
+      "version": "0.0.0",
+      "dependencies": {
+        "@element-plus/icons-vue": "^2.3.2",
+        "axios": "^1.20.0",
+        "crypto-js": "^4.2.0",
+        "element-plus": "^2.14.5",
+        "jwt-decode": "^4.0.0",
+        "pinia": "^4.0.3",
+        "vue": "^3.5.42",
+        "vue-router": "^5.3.1"
+      },
+      "devDependencies": {
+        "@types/crypto-js": "^4.2.2",
+        "@types/node": "^24.13.3",
+        "@vitejs/plugin-vue": "^6.0.8",
+        "@vue/tsconfig": "^0.9.1",
+        "openapi-typescript-codegen": "^0.31.0",
+        "typescript": "~6.0.2",
+        "vite": "^8.3.0",
+        "vitest": "^5.0.0",
+        "vue-tsc": "^3.3.11"
+      }
+    },
+    "node_modules/@apidevtools/json-schema-ref-parser": {
+      "version": "14.2.1",
+      "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-14.2.1.tgz",
+      "integrity": "sha512-HmdFw9CDYqM6B25pqGBpNeLCKvGPlIx1EbLrVL0zPvj50CJQUHyBNBw45Muk0kEIkogo1VZvOKHajdMuAzSxRg==",
+      "dev": true,
+      "dependencies": {
+        "js-yaml": "^4.1.0"
+      },
+      "engines": {
+        "node": ">= 20"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/philsturgeon"
+      },
+      "peerDependencies": {
+        "@types/json-schema": "^7.0.15"
+      }
+    },
+    "node_modules/@babel/helper-string-parser": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
+      "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-validator-identifier": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
+      "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/parser": {
+      "version": "7.29.8",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz",
+      "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==",
+      "dependencies": {
+        "@babel/types": "^7.29.8"
+      },
+      "bin": {
+        "parser": "bin/babel-parser.js"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@babel/types": {
+      "version": "7.29.8",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz",
+      "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==",
+      "dependencies": {
+        "@babel/helper-string-parser": "^7.29.7",
+        "@babel/helper-validator-identifier": "^7.29.7"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@ctrl/tinycolor": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.2.1.tgz",
+      "integrity": "sha512-Vh5uy9Y1JBMId6J1f68TMXnZrTCvNdo9Fu2k0d1YnJngR+sM5kWTKl441c/3TFs+cG0NWOjRWgQ/iaWhbYx+7A==",
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/@element-plus/icons-vue": {
+      "version": "2.3.2",
+      "resolved": "https://registry.npmjs.org/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz",
+      "integrity": "sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==",
+      "peerDependencies": {
+        "vue": "^3.2.0"
+      }
+    },
+    "node_modules/@floating-ui/core": {
+      "version": "1.8.0",
+      "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz",
+      "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==",
+      "dependencies": {
+        "@floating-ui/utils": "^0.2.12"
+      }
+    },
+    "node_modules/@floating-ui/dom": {
+      "version": "1.8.0",
+      "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz",
+      "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==",
+      "dependencies": {
+        "@floating-ui/core": "^1.8.0",
+        "@floating-ui/utils": "^0.2.12"
+      }
+    },
+    "node_modules/@floating-ui/utils": {
+      "version": "0.2.12",
+      "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz",
+      "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww=="
+    },
+    "node_modules/@inquirer/ansi": {
+      "version": "2.0.8",
+      "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.8.tgz",
+      "integrity": "sha512-WpQM+Ti6Z40EFwwt+uL2p4UabT+W179zHp6HhLVOzfbwnVn05IPO/eXIZXGNqcT1jbQ15SujNLzQ39k4QPPxBQ==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "engines": {
+        "node": ">=23.5.0 || ^22.13.0 || ^20.17.0"
+      }
+    },
+    "node_modules/@inquirer/confirm": {
+      "version": "6.3.2",
+      "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.3.2.tgz",
+      "integrity": "sha512-Xvr/0HggjddPtGppuqVmxhTw+Hr8PvsZ/k0HmOEaAqQEt80OITNkFWnsdNmyT0/eM4Ab+iJLx2R8rctlEyfSVg==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "dependencies": {
+        "@inquirer/core": "^12.0.3",
+        "@inquirer/type": "4.1.1"
+      },
+      "engines": {
+        "node": ">=23.5.0 || ^22.13.0 || ^20.17.0"
+      },
+      "peerDependencies": {
+        "@types/node": ">=18"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@inquirer/core": {
+      "version": "12.0.3",
+      "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-12.0.3.tgz",
+      "integrity": "sha512-wsSy0sznmXwkty+2PzZwx00Cazc/E0r0B7mAzdGROz2Ct+DFZXaK7WDjGZvgjRldxH5ZhFVfF2lgkYrqgOw2KA==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "dependencies": {
+        "@inquirer/ansi": "^2.0.8",
+        "@inquirer/figures": "^2.0.9",
+        "@inquirer/type": "4.1.1",
+        "cli-width": "^4.1.0",
+        "fast-wrap-ansi": "^0.2.0",
+        "mute-stream": "^3.0.0",
+        "signal-exit": "^4.1.0"
+      },
+      "engines": {
+        "node": ">=23.5.0 || ^22.13.0 || ^20.17.0"
+      },
+      "peerDependencies": {
+        "@types/node": ">=18"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@inquirer/figures": {
+      "version": "2.0.9",
+      "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.9.tgz",
+      "integrity": "sha512-EAWgUTGQ/Umgga51dE3B2PUHbufuXarDfg86uVgoSgNHNNQnyFKcOrQLWVqYMghuSyHh8+2HUH0Js9cTC1WAdg==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "engines": {
+        "node": ">=23.5.0 || ^22.13.0 || ^20.17.0"
+      }
+    },
+    "node_modules/@inquirer/type": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.1.1.tgz",
+      "integrity": "sha512-yJoHYrMnxIsJZCY+0Vb66Dy3he3kL3e2wOBKhoSwWWAzZAY82emlxwgprCtp6yRixvNRNq9ztfRWQYPNr3Go7A==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "engines": {
+        "node": ">=23.5.0 || ^22.13.0 || ^20.17.0"
+      },
+      "peerDependencies": {
+        "@types/node": ">=18"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@jridgewell/gen-mapping": {
+      "version": "0.3.13",
+      "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+      "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+      "dependencies": {
+        "@jridgewell/sourcemap-codec": "^1.5.0",
+        "@jridgewell/trace-mapping": "^0.3.24"
+      }
+    },
+    "node_modules/@jridgewell/remapping": {
+      "version": "2.3.5",
+      "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+      "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+      "dependencies": {
+        "@jridgewell/gen-mapping": "^0.3.5",
+        "@jridgewell/trace-mapping": "^0.3.24"
+      }
+    },
+    "node_modules/@jridgewell/resolve-uri": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+      "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@jridgewell/sourcemap-codec": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz",
+      "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw=="
+    },
+    "node_modules/@jridgewell/trace-mapping": {
+      "version": "0.3.31",
+      "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+      "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+      "dependencies": {
+        "@jridgewell/resolve-uri": "^3.1.0",
+        "@jridgewell/sourcemap-codec": "^1.4.14"
+      }
+    },
+    "node_modules/@mswjs/interceptors": {
+      "version": "0.41.9",
+      "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.41.9.tgz",
+      "integrity": "sha512-VVPPgHyQ6ShqnrmDWuxjmUIsO9gWyOZFmuOfLd9LfBGQJwZfy0gvv9pbHSJuoFNIYC7ZDX9aoFwowjcdSC4E8w==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "dependencies": {
+        "@open-draft/deferred-promise": "^2.2.0",
+        "@open-draft/logger": "^0.3.0",
+        "@open-draft/until": "^2.0.0",
+        "is-node-process": "^1.2.0",
+        "outvariant": "^1.4.3",
+        "strict-event-emitter": "^0.5.1"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@mswjs/interceptors/node_modules/@open-draft/deferred-promise": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz",
+      "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==",
+      "dev": true,
+      "optional": true,
+      "peer": true
+    },
+    "node_modules/@open-draft/deferred-promise": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-3.0.0.tgz",
+      "integrity": "sha512-XW375UK8/9SqUVNVa6M0yEy8+iTi4QN5VZ7aZuRFQmy76LRwI9wy5F4YIBU6T+eTe2/DNDo8tqu8RHlwLHM6RA==",
+      "dev": true,
+      "optional": true,
+      "peer": true
+    },
+    "node_modules/@open-draft/logger": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz",
+      "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "dependencies": {
+        "is-node-process": "^1.2.0",
+        "outvariant": "^1.4.0"
+      }
+    },
+    "node_modules/@open-draft/until": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz",
+      "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==",
+      "dev": true,
+      "optional": true,
+      "peer": true
+    },
+    "node_modules/@oxc-project/types": {
+      "version": "0.149.0",
+      "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.149.0.tgz",
+      "integrity": "sha512-Efcc+iF0j3Bf67YjEqIqWXbX5XddXoK/Mw4K1/JuXwRCZ8N16VR7iT23nlCc9XrveFVh/E5Rqs2StT0V8v9LdA==",
+      "devOptional": true,
+      "funding": {
+        "url": "https://github.com/sponsors/oxc-project"
+      }
+    },
+    "node_modules/@popperjs/core": {
+      "name": "@sxzz/popperjs-es",
+      "version": "2.11.8",
+      "resolved": "https://registry.npmjs.org/@sxzz/popperjs-es/-/popperjs-es-2.11.8.tgz",
+      "integrity": "sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==",
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/popperjs"
+      }
+    },
+    "node_modules/@rolldown/binding-android-arm-eabi": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.8.tgz",
+      "integrity": "sha512-tN5aztYkKCte4i5SIrrz5yK/HMjEuCqCSCJa418jOV8tZ1cBY3YF2otxB1ktPxzsLA1BeTqwapK0bfjxNvHJVw==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-android-arm64": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.8.tgz",
+      "integrity": "sha512-dIYTWl9XprMUiQFoc55KUyk/oS8SKYH3zFl0LTR7RT0Xj4hgSVyuJcroH8JUu8RcpF8fTB6E0aOwCkZoYPcDSQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-darwin-arm64": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.8.tgz",
+      "integrity": "sha512-PCSDQGXD2IyTEFrcgPyBM8jJuGmrbCMuoIOXdbEGVemruKACXoLQJrb+A45Z0L5t1RQkdfJprAYPkikbh7dzdA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-darwin-x64": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.8.tgz",
+      "integrity": "sha512-Uk7lRsGhPFHVX/sAUC6D5H9Ol30dFHd6iquokll2th3LpdJ3F5CzQB+7DHn0Ri2mG+U7k2zXiPHDrwZenXhwSA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-freebsd-x64": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.8.tgz",
+      "integrity": "sha512-DjszaTEVogPqA5bYzsEeqDCQxbcp2fexQwKcRspYji2yzR68fCf+e4fx6kBSRDwX5/brZaHw/hWS9+A/+/w9sQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.8.tgz",
+      "integrity": "sha512-zmwa7FTmdzB6aaEEuuls18H6Ap5JmJPSoPTuXixeJZV6tG40SyLkApQtz1g8ptZtiEKqj9OM0oNLPh1AgvE31Q==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-linux-arm64-gnu": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.8.tgz",
+      "integrity": "sha512-KdYQDPHwJVnbFwdTGMgxsI9SqblBlz6STGM+w1We/d5B8OWWidYH0MwkU/uA1wM5fIpO2MkOVxXrNzzuZhw9ew==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-linux-arm64-musl": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.8.tgz",
+      "integrity": "sha512-jFJTifHnNPY+yzOoNZQfSIysrVyXzEQPhPnOUjmD1bcQGHH6s7c8cViKWar8YplQImE5N9JRqMCLrM2CdxOrZA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-linux-ppc64-gnu": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.8.tgz",
+      "integrity": "sha512-FhiOziBDWPBjbcmRzfLyIJnaP7AVMFXT7YCXPjXxj7wKU3vx24RjrCNN/zjvVa+N2vVoHJwCoUBvsrN/DG3zIA==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-linux-s390x-gnu": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.8.tgz",
+      "integrity": "sha512-WnHfADMzOV2Y55wlx1hzzQnar/wDt/VdvWSD99r18Mz9ylNieIGOkRx3UV21h7m/eJvjySYJkO26VvGNFkwsIQ==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-linux-x64-gnu": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.8.tgz",
+      "integrity": "sha512-H9tRr5ibfXFVLxbPOseVewewFpl28zcEdjRDt2FTUZU7odxP0gEv1ki4/kGmcGOh78oRwZuuQllGLZ9zTJp84g==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-linux-x64-musl": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.8.tgz",
+      "integrity": "sha512-UefiqfM3D6IVNlZ8tSGs9+Ejjud2T+oxO0IHADU45Y+lyEjD2dVFyZHbkfX0LUb5Zugo/oIv1eCO/KVYhgYJYA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-openharmony-arm64": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.8.tgz",
+      "integrity": "sha512-637Ke4kWSy6rp9cxQ9gMOXlxPgIw/c1beASV4M//3+9I4uwBVOOl74G+e3zyU3u19U7RkRl/HuewixZ/Z6+Rjg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "openharmony"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-win32-arm64-msvc": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.8.tgz",
+      "integrity": "sha512-xWBkPOF1Q9k/Gv1nQXnVdLxKu74jXppuOM4Z3mnypVUJJJwLsMl7hNJGRAUJoG8A5MgOI1ACKM+wBFxSJzKy4A==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-win32-x64-msvc": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.8.tgz",
+      "integrity": "sha512-uz2ZvfgXbxqNwijjjbxrnvALwpyODDcgc1T1N8N3rf/DXKQmaFwmB4LX4yyjggpwN2obdQLb2rgirX5ffCWYng==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/pluginutils": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
+      "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
+      "devOptional": true
+    },
+    "node_modules/@types/chai": {
+      "version": "5.2.3",
+      "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
+      "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
+      "dev": true,
+      "dependencies": {
+        "@types/deep-eql": "*",
+        "assertion-error": "^2.0.1"
+      }
+    },
+    "node_modules/@types/crypto-js": {
+      "version": "4.2.2",
+      "resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.2.2.tgz",
+      "integrity": "sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==",
+      "dev": true
+    },
+    "node_modules/@types/deep-eql": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
+      "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
+      "dev": true
+    },
+    "node_modules/@types/estree": {
+      "version": "1.0.9",
+      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
+      "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
+      "dev": true
+    },
+    "node_modules/@types/json-schema": {
+      "version": "7.0.15",
+      "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+      "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+      "dev": true,
+      "peer": true
+    },
+    "node_modules/@types/lodash": {
+      "version": "4.17.25",
+      "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.25.tgz",
+      "integrity": "sha512-+K1NIO8I+F9/wNulfVvu23QYd0Pe9/OCqRrim4NoYIf1VoEDL90Ve4ClzpyqBLc7NpGGWRvYNCKZ1BE/Jpf8dQ=="
+    },
+    "node_modules/@types/lodash-es": {
+      "version": "4.17.12",
+      "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz",
+      "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
+      "dependencies": {
+        "@types/lodash": "*"
+      }
+    },
+    "node_modules/@types/node": {
+      "version": "24.13.4",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.4.tgz",
+      "integrity": "sha512-YJ7EqCstVTzIr0fMr7qul/977en+pQHrfmuKIo6Zr9i75Be21dr3MovcfvGtyvi2HAUrRerWps5sMO9I7WaxDw==",
+      "dev": true,
+      "dependencies": {
+        "undici-types": "~7.18.0"
+      }
+    },
+    "node_modules/@types/set-cookie-parser": {
+      "version": "2.4.10",
+      "resolved": "https://registry.npmjs.org/@types/set-cookie-parser/-/set-cookie-parser-2.4.10.tgz",
+      "integrity": "sha512-GGmQVGpQWUe5qglJozEjZV/5dyxbOOZ0LHe/lqyWssB88Y4svNfst0uqBVscdDeIKl5Jy5+aPSvy7mI9tYRguw==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
+    "node_modules/@types/statuses": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.6.tgz",
+      "integrity": "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==",
+      "dev": true,
+      "optional": true,
+      "peer": true
+    },
+    "node_modules/@types/web-bluetooth": {
+      "version": "0.0.21",
+      "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
+      "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA=="
+    },
+    "node_modules/@vitejs/plugin-vue": {
+      "version": "6.0.8",
+      "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.8.tgz",
+      "integrity": "sha512-0ZjgOg7oO6farnNGup7yvoM/YXZV84OZxHAwtflItNa/6zzQyVb5LNxyea3FEKEX2XlagIKzrlH7wwxkKgtiew==",
+      "dev": true,
+      "dependencies": {
+        "@rolldown/pluginutils": "^1.0.1"
+      },
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      },
+      "peerDependencies": {
+        "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0",
+        "vue": "^3.2.25"
+      }
+    },
+    "node_modules/@vitest/mocker": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.0.tgz",
+      "integrity": "sha512-66PGTMIiVJP3t4a5yxU9qPtf7MdTBs8jmToMvy+HVflB3Yy13WJZTtPePdvU+wjRV02SKK5doLbSA6o9pwOmiA==",
+      "dev": true,
+      "dependencies": {
+        "@jridgewell/trace-mapping": "0.3.31",
+        "@vitest/spy": "5.0.0",
+        "estree-walker": "^3.0.3",
+        "magic-string": "^1.2.3"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      },
+      "peerDependencies": {
+        "msw": "^2.4.9",
+        "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
+      },
+      "peerDependenciesMeta": {
+        "msw": {
+          "optional": true
+        },
+        "vite": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@vitest/mocker/node_modules/estree-walker": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
+      "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+      "dev": true,
+      "dependencies": {
+        "@types/estree": "^1.0.0"
+      }
+    },
+    "node_modules/@vitest/mocker/node_modules/magic-string": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.3.1.tgz",
+      "integrity": "sha512-rm91zr2Ou+XueDTohjQQjdQEcYM6zVi8KVUCG8Ec3vHwUEKrhSdCNyfuIywkA6hcCAteIn0ZOtAHA6eGpiX+Pg==",
+      "dev": true,
+      "dependencies": {
+        "@jridgewell/sourcemap-codec": "^1.6.0"
+      }
+    },
+    "node_modules/@vitest/spy": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.0.tgz",
+      "integrity": "sha512-uy+luWBAPw9XfthoHi5AkfHUnuPYEESjl0p/r+meoBnU8bxg5GDQ3Ey8MjcJ6sqahkL4PFyrvfMJJBw7LbU06g==",
+      "dev": true,
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "node_modules/@volar/language-core": {
+      "version": "2.4.28",
+      "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.28.tgz",
+      "integrity": "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==",
+      "dev": true,
+      "dependencies": {
+        "@volar/source-map": "2.4.28"
+      }
+    },
+    "node_modules/@volar/source-map": {
+      "version": "2.4.28",
+      "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.28.tgz",
+      "integrity": "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==",
+      "dev": true
+    },
+    "node_modules/@volar/typescript": {
+      "version": "2.4.28",
+      "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.28.tgz",
+      "integrity": "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==",
+      "dev": true,
+      "dependencies": {
+        "@volar/language-core": "2.4.28",
+        "path-browserify": "^1.0.1",
+        "vscode-uri": "^3.0.8"
+      }
+    },
+    "node_modules/@vue-macros/common": {
+      "version": "3.1.4",
+      "resolved": "https://registry.npmjs.org/@vue-macros/common/-/common-3.1.4.tgz",
+      "integrity": "sha512-/5Fv+6DgIcM9ajY05ZmKBv+LMX1M9A0X+IUwDRVdt67ciw8OV9bvG2r34p3RiEadlsQybjhKPRKNXDC8Bp23cw==",
+      "dependencies": {
+        "@vue/compiler-sfc": "^3.5.22",
+        "ast-kit": "^2.1.2",
+        "local-pkg": "^1.1.2",
+        "magic-string-ast": "^1.0.2",
+        "unplugin-utils": "^0.3.0"
+      },
+      "engines": {
+        "node": ">=20.19.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/vue-macros"
+      },
+      "peerDependencies": {
+        "vue": "^2.7.0 || ^3.2.25"
+      },
+      "peerDependenciesMeta": {
+        "vue": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@vue/compiler-core": {
+      "version": "3.5.42",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.42.tgz",
+      "integrity": "sha512-2Ye1ilMtKXxl8qZUrQ5j0CdgenFp/HFQmta6rfRyfEsTG69L6Wk+tWuNoHYHMx9E8tF2Slvdg1FuwDvAXdy1LQ==",
+      "dependencies": {
+        "@babel/parser": "^7.29.8",
+        "@vue/shared": "3.5.42",
+        "entities": "^7.0.1",
+        "estree-walker": "^2.0.2",
+        "source-map-js": "^1.2.1"
+      }
+    },
+    "node_modules/@vue/compiler-dom": {
+      "version": "3.5.42",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.42.tgz",
+      "integrity": "sha512-qbhQZEFmycr+ni/qyuccS4sucNN7VAbDfbkvNxWOX2VfgFm90MNs3/UhRNKoPMEIVn0F8gdlYjLPvqxHwHeQOA==",
+      "dependencies": {
+        "@vue/compiler-core": "3.5.42",
+        "@vue/shared": "3.5.42"
+      }
+    },
+    "node_modules/@vue/compiler-sfc": {
+      "version": "3.5.42",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.42.tgz",
+      "integrity": "sha512-fkCAFB4okcAANGMThboWnScp/gzWjU0ZSkVnjTIiplmMDq2uq0tIB3j+xVu4rhv5rvOgBySCysudmbMd6xRRqw==",
+      "dependencies": {
+        "@babel/parser": "^7.29.8",
+        "@vue/compiler-core": "3.5.42",
+        "@vue/compiler-dom": "3.5.42",
+        "@vue/compiler-ssr": "3.5.42",
+        "@vue/shared": "3.5.42",
+        "estree-walker": "^2.0.2",
+        "magic-string": "^0.30.21",
+        "postcss": "^8.5.19",
+        "source-map-js": "^1.2.1"
+      }
+    },
+    "node_modules/@vue/compiler-ssr": {
+      "version": "3.5.42",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.42.tgz",
+      "integrity": "sha512-xmLk3wLkbizPAiLyomjgFFosf2ys9b5Ghb+oh/k2tnvipNz8OFrQOiTcWCzyK7MpBp9KkyGtfvgfLUivbmuGYA==",
+      "dependencies": {
+        "@vue/compiler-dom": "3.5.42",
+        "@vue/shared": "3.5.42"
+      }
+    },
+    "node_modules/@vue/devtools-api": {
+      "version": "8.2.1",
+      "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.2.1.tgz",
+      "integrity": "sha512-6u4vXBlIBAC1wMplIZgpyPn7uh/s4Bf6F5bMzvLv+EdJ0aHs/+4B7Ygv864EStQSjRbsRzTko/kUG1A1IejQ3A==",
+      "dependencies": {
+        "@vue/devtools-kit": "^8.2.1"
+      }
+    },
+    "node_modules/@vue/devtools-kit": {
+      "version": "8.2.1",
+      "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.2.1.tgz",
+      "integrity": "sha512-FIGIuq3AWReEpbAHY/cRGeHDfI0qOb8OCQ3YjbEAX04uaxIDbGc9rhkbVcG7rnfHPXE3RsU5KrWOu9V/okd8AQ==",
+      "dependencies": {
+        "@vue/devtools-shared": "^8.2.1",
+        "birpc": "^2.6.1",
+        "hookable": "^5.5.3",
+        "perfect-debounce": "^2.0.0"
+      }
+    },
+    "node_modules/@vue/devtools-shared": {
+      "version": "8.2.1",
+      "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.2.1.tgz",
+      "integrity": "sha512-Fkac7lUdGReh6pVOi3AYPRGe82LQqRmAfThW7RRligOAP0ZA/Z1z9XLHDM9dv34pV2HRc79DK8uKPeG2fLnA/g=="
+    },
+    "node_modules/@vue/language-core": {
+      "version": "3.3.11",
+      "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.3.11.tgz",
+      "integrity": "sha512-QJmpliwAVpC/OxubIByPAhNzsQPRc8/gxlN2qnVzVfIMjMDz/9RnXRFoetjz5yEgXVXyp4LqhXq3V53PjmNzFw==",
+      "dev": true,
+      "dependencies": {
+        "@volar/language-core": "2.4.28",
+        "@vue/compiler-dom": "^3.5.0",
+        "@vue/shared": "^3.5.0",
+        "alien-signals": "^3.2.1",
+        "muggle-string": "^0.4.1",
+        "path-browserify": "^1.0.1",
+        "picomatch": "^4.0.4"
+      }
+    },
+    "node_modules/@vue/reactivity": {
+      "version": "3.5.42",
+      "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.42.tgz",
+      "integrity": "sha512-TzNNfKpb7hDxbQltwAut8VDQA5YP+BuRlxntHUuRjyKwlMvmAPbs3unhCvieijifY6vFfVBwsS7wG/C7uq+bEQ==",
+      "dependencies": {
+        "@vue/shared": "3.5.42"
+      }
+    },
+    "node_modules/@vue/runtime-core": {
+      "version": "3.5.42",
+      "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.42.tgz",
+      "integrity": "sha512-9uACtuHs7vJGkm5Bp3xu4xRDLFTIYy5DgxpToVjqGIAhAEKwQfsaLvKINhM6nFVp6bZPRFGdDqd1g52MqKsotA==",
+      "dependencies": {
+        "@vue/reactivity": "3.5.42",
+        "@vue/shared": "3.5.42"
+      }
+    },
+    "node_modules/@vue/runtime-dom": {
+      "version": "3.5.42",
+      "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.42.tgz",
+      "integrity": "sha512-rsCmhiWLaRxGltLwhlCWyYkFn7WAbKRh0q17eZ1A6Dq6eqc2ACQ61IIryxz0LrsvCzHSilLA9JHovVwM8CNE2g==",
+      "dependencies": {
+        "@vue/reactivity": "3.5.42",
+        "@vue/runtime-core": "3.5.42",
+        "@vue/shared": "3.5.42",
+        "csstype": "^3.2.3"
+      }
+    },
+    "node_modules/@vue/server-renderer": {
+      "version": "3.5.42",
+      "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.42.tgz",
+      "integrity": "sha512-2++5dUyYS4gvo7xQXSECUDhB7TS0aOl5SeVfC5qSq1Jgfhjvegw1zqhwTIR3imZ+QYPJQw9gfcFvXGAjGZ7ajQ==",
+      "dependencies": {
+        "@vue/compiler-ssr": "3.5.42",
+        "@vue/runtime-dom": "3.5.42",
+        "@vue/shared": "3.5.42"
+      }
+    },
+    "node_modules/@vue/shared": {
+      "version": "3.5.42",
+      "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.42.tgz",
+      "integrity": "sha512-2rPxex1jQf4jvl9MOHl6YaXCPcrNqz/FstMOEh3QWY+/OME9nQTvl9WYeCwhW7AFjaR0SnngZGlp/wkR6rkI6g=="
+    },
+    "node_modules/@vue/tsconfig": {
+      "version": "0.9.1",
+      "resolved": "https://registry.npmjs.org/@vue/tsconfig/-/tsconfig-0.9.1.tgz",
+      "integrity": "sha512-buvjm+9NzLCJL29KY1j1991YYJ5e6275OiK+G4jtmfIb+z4POywbdm0wXusT9adVWqe0xqg70TbI7+mRx4uU9w==",
+      "dev": true,
+      "peerDependencies": {
+        "typescript": ">= 5.8",
+        "vue": "^3.4.0"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        },
+        "vue": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@vueuse/core": {
+      "version": "14.4.0",
+      "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.4.0.tgz",
+      "integrity": "sha512-X4WHz1HlCzCBoYXesUkifzzWBAcZgXG8Fi5iNPQg/epdzOB3gu8Fawj3hvuwYR1nGcXGnvxwYYcUC/71++svtQ==",
+      "dependencies": {
+        "@types/web-bluetooth": "^0.0.21",
+        "@vueuse/metadata": "14.4.0",
+        "@vueuse/shared": "14.4.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      },
+      "peerDependencies": {
+        "vue": "^3.5.0"
+      }
+    },
+    "node_modules/@vueuse/metadata": {
+      "version": "14.4.0",
+      "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.4.0.tgz",
+      "integrity": "sha512-swx/255R6JyHZFJhx845iz5CRWDZdCfvkZOpACWc5+c5WHcG24mv8gUT1WIdFQaHt6dq79rvILd9QnCWiyVm9g==",
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      }
+    },
+    "node_modules/@vueuse/shared": {
+      "version": "14.4.0",
+      "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.4.0.tgz",
+      "integrity": "sha512-JRgY90Sz8DDtPMsaDflvPMp9xYk69JZAmbuDvAquUVXKr2gEjqtzGNTTthLfckH0BzBqvnu31gb4a8TGLRe79g==",
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      },
+      "peerDependencies": {
+        "vue": "^3.5.0"
+      }
+    },
+    "node_modules/acorn": {
+      "version": "8.18.0",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz",
+      "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==",
+      "bin": {
+        "acorn": "bin/acorn"
+      },
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/agent-base": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
+      "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+      "dependencies": {
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 6.0.0"
+      }
+    },
+    "node_modules/alien-signals": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.2.1.tgz",
+      "integrity": "sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==",
+      "dev": true
+    },
+    "node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/ansi-styles": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "dependencies": {
+        "color-convert": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/argparse": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+      "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+      "dev": true
+    },
+    "node_modules/assertion-error": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
+      "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
+      "dev": true,
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/ast-kit": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/ast-kit/-/ast-kit-2.2.0.tgz",
+      "integrity": "sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw==",
+      "dependencies": {
+        "@babel/parser": "^7.28.5",
+        "pathe": "^2.0.3"
+      },
+      "engines": {
+        "node": ">=20.19.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sxzz"
+      }
+    },
+    "node_modules/ast-walker-scope": {
+      "version": "0.9.0",
+      "resolved": "https://registry.npmjs.org/ast-walker-scope/-/ast-walker-scope-0.9.0.tgz",
+      "integrity": "sha512-IJdzo2vLiElBxKzwS36VsCue/62d6IdWjnPB2v3nuPKeWGynp6FF/CYoLa5i/3jXH/z97ZDdsXz6abpgM6w07A==",
+      "dependencies": {
+        "@babel/parser": "^7.29.2",
+        "@babel/types": "^7.29.0",
+        "ast-kit": "^2.2.0"
+      },
+      "engines": {
+        "node": ">=20.19.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sxzz"
+      }
+    },
+    "node_modules/async-validator": {
+      "version": "4.2.5",
+      "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz",
+      "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg=="
+    },
+    "node_modules/asynckit": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
+    },
+    "node_modules/axios": {
+      "version": "1.20.0",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-1.20.0.tgz",
+      "integrity": "sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==",
+      "dependencies": {
+        "follow-redirects": "^1.16.0",
+        "form-data": "^4.0.6",
+        "https-proxy-agent": "^5.0.1",
+        "proxy-from-env": "^2.1.0"
+      }
+    },
+    "node_modules/birpc": {
+      "version": "2.9.0",
+      "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz",
+      "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==",
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      }
+    },
+    "node_modules/call-bind-apply-helpers": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+      "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/camelcase": {
+      "version": "6.3.0",
+      "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
+      "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
+      "dev": true,
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/chai": {
+      "version": "6.2.2",
+      "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
+      "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==",
+      "dev": true,
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/chokidar": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz",
+      "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==",
+      "dependencies": {
+        "readdirp": "^5.0.0"
+      },
+      "engines": {
+        "node": ">= 20.19.0"
+      },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
+    "node_modules/cli-width": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz",
+      "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "engines": {
+        "node": ">= 12"
+      }
+    },
+    "node_modules/cliui": {
+      "version": "8.0.1",
+      "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
+      "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "dependencies": {
+        "string-width": "^4.2.0",
+        "strip-ansi": "^6.0.1",
+        "wrap-ansi": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/color-convert": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "dependencies": {
+        "color-name": "~1.1.4"
+      },
+      "engines": {
+        "node": ">=7.0.0"
+      }
+    },
+    "node_modules/color-name": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+      "dev": true,
+      "optional": true,
+      "peer": true
+    },
+    "node_modules/combined-stream": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+      "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+      "dependencies": {
+        "delayed-stream": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/commander": {
+      "version": "14.0.3",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
+      "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
+      "dev": true,
+      "engines": {
+        "node": ">=20"
+      }
+    },
+    "node_modules/confbox": {
+      "version": "0.2.4",
+      "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz",
+      "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ=="
+    },
+    "node_modules/cookie": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
+      "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/crypto-js": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
+      "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
+      "deprecated": "Active development of CryptoJS has been discontinued. This library is no longer maintained."
+    },
+    "node_modules/csstype": {
+      "version": "3.2.3",
+      "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+      "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="
+    },
+    "node_modules/dayjs": {
+      "version": "1.11.23",
+      "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.23.tgz",
+      "integrity": "sha512-QDTCU0M0MxR3hQfnlDJfwekQiaanm1ubOD231u73WBckQ/fsamwRLiE2GBz6D3a/xF1NgfiDLJjXBa1hYOYTtQ=="
+    },
+    "node_modules/debug": {
+      "version": "4.4.3",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+      "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/delayed-stream": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+      "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/detect-libc": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+      "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+      "devOptional": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/dunder-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+      "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.2.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/element-plus": {
+      "version": "2.14.5",
+      "resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.14.5.tgz",
+      "integrity": "sha512-bghYy/S+qg87enHPXELirhEdDqsVAUGcGpbGIeG8dz0kwpIkGz7gYsifulBshXX74iRtHib85XWQj0uSH2A1Yg==",
+      "dependencies": {
+        "@ctrl/tinycolor": "^4.2.0",
+        "@element-plus/icons-vue": "^2.3.2",
+        "@floating-ui/dom": "^1.8.0",
+        "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.8",
+        "@types/lodash": "^4.17.24",
+        "@types/lodash-es": "^4.17.12",
+        "@vueuse/core": "14.4.0",
+        "async-validator": "^4.2.5",
+        "dayjs": "^1.11.20",
+        "lodash": "^4.18.1",
+        "lodash-es": "^4.18.1",
+        "lodash-unified": "^1.0.3",
+        "memoize-one": "^6.0.0",
+        "normalize-wheel-es": "^1.2.0",
+        "vue-component-type-helpers": "^3.3.9"
+      },
+      "peerDependencies": {
+        "vue": "^3.3.7"
+      }
+    },
+    "node_modules/emoji-regex": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+      "dev": true,
+      "optional": true,
+      "peer": true
+    },
+    "node_modules/entities": {
+      "version": "7.0.1",
+      "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
+      "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
+      "engines": {
+        "node": ">=0.12"
+      },
+      "funding": {
+        "url": "https://github.com/fb55/entities?sponsor=1"
+      }
+    },
+    "node_modules/es-define-property": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+      "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-errors": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+      "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-module-lexer": {
+      "version": "2.3.2",
+      "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz",
+      "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==",
+      "dev": true
+    },
+    "node_modules/es-object-atoms": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
+      "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
+      "dependencies": {
+        "es-errors": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-set-tostringtag": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+      "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.6",
+        "has-tostringtag": "^1.0.2",
+        "hasown": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/escalade": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+      "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/estree-walker": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
+      "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
+    },
+    "node_modules/expect-type": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz",
+      "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==",
+      "dev": true,
+      "engines": {
+        "node": ">=12.0.0"
+      }
+    },
+    "node_modules/exsolve": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.1.1.tgz",
+      "integrity": "sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g=="
+    },
+    "node_modules/fast-string-truncated-width": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz",
+      "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==",
+      "dev": true,
+      "optional": true,
+      "peer": true
+    },
+    "node_modules/fast-string-width": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz",
+      "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "dependencies": {
+        "fast-string-truncated-width": "^3.0.2"
+      }
+    },
+    "node_modules/fast-wrap-ansi": {
+      "version": "0.2.2",
+      "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz",
+      "integrity": "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "dependencies": {
+        "fast-string-width": "^3.0.2"
+      }
+    },
+    "node_modules/fdir": {
+      "version": "6.5.0",
+      "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+      "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "peerDependencies": {
+        "picomatch": "^3 || ^4"
+      },
+      "peerDependenciesMeta": {
+        "picomatch": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/follow-redirects": {
+      "version": "1.16.0",
+      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
+      "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/RubenVerborgh"
+        }
+      ],
+      "engines": {
+        "node": ">=4.0"
+      },
+      "peerDependenciesMeta": {
+        "debug": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/form-data": {
+      "version": "4.0.6",
+      "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz",
+      "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==",
+      "dependencies": {
+        "asynckit": "^0.4.0",
+        "combined-stream": "^1.0.8",
+        "es-set-tostringtag": "^2.1.0",
+        "hasown": "^2.0.4",
+        "mime-types": "^2.1.35"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/fs-extra": {
+      "version": "11.4.0",
+      "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.4.0.tgz",
+      "integrity": "sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA==",
+      "dev": true,
+      "dependencies": {
+        "graceful-fs": "^4.2.0",
+        "jsonfile": "^6.0.1",
+        "universalify": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=14.14"
+      }
+    },
+    "node_modules/fsevents": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+      "dev": true,
+      "hasInstallScript": true,
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+      }
+    },
+    "node_modules/function-bind": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-caller-file": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+      "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "engines": {
+        "node": "6.* || 8.* || >= 10.*"
+      }
+    },
+    "node_modules/get-intrinsic": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+      "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+      "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"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+      "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+      "dependencies": {
+        "dunder-proto": "^1.0.1",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/gopd": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+      "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/graceful-fs": {
+      "version": "4.2.11",
+      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+      "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+      "dev": true
+    },
+    "node_modules/graphql": {
+      "version": "16.14.2",
+      "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.14.2.tgz",
+      "integrity": "sha512-Chq1s4CY7jmh8gO2qvLIJyfCDIN+EHLFW/9iShnp1z8FjBQMoodWP1kDC36VAMXXIvAjj4ARa7ntfAV2BrjsbA==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "engines": {
+        "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0"
+      }
+    },
+    "node_modules/handlebars": {
+      "version": "4.7.9",
+      "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz",
+      "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==",
+      "dev": true,
+      "dependencies": {
+        "minimist": "^1.2.5",
+        "neo-async": "^2.6.2",
+        "source-map": "^0.6.1",
+        "wordwrap": "^1.0.0"
+      },
+      "bin": {
+        "handlebars": "bin/handlebars"
+      },
+      "engines": {
+        "node": ">=0.4.7"
+      },
+      "optionalDependencies": {
+        "uglify-js": "^3.1.4"
+      }
+    },
+    "node_modules/has-symbols": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+      "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-tostringtag": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+      "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+      "dependencies": {
+        "has-symbols": "^1.0.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/hasown": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
+      "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
+      "dependencies": {
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/headers-polyfill": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-5.0.1.tgz",
+      "integrity": "sha512-1TJ6Fih/b8h5TIcv+1+Hw0PDQWJTKDKzFZzcKOiW1wJza3XoAQlkCuXLbymPYB8+ZQyw8mHvdw560e8zVFIWyA==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "dependencies": {
+        "@types/set-cookie-parser": "^2.4.10",
+        "set-cookie-parser": "^3.0.1"
+      }
+    },
+    "node_modules/hookable": {
+      "version": "5.5.3",
+      "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
+      "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="
+    },
+    "node_modules/https-proxy-agent": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
+      "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
+      "dependencies": {
+        "agent-base": "6",
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/is-fullwidth-code-point": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+      "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/is-node-process": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz",
+      "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==",
+      "dev": true,
+      "optional": true,
+      "peer": true
+    },
+    "node_modules/js-yaml": {
+      "version": "4.3.2",
+      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz",
+      "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/puzrin"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/nodeca"
+        }
+      ],
+      "dependencies": {
+        "argparse": "^2.0.1"
+      },
+      "bin": {
+        "js-yaml": "bin/js-yaml.js"
+      }
+    },
+    "node_modules/jsonfile": {
+      "version": "6.2.1",
+      "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz",
+      "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==",
+      "dev": true,
+      "dependencies": {
+        "universalify": "^2.0.0"
+      },
+      "optionalDependencies": {
+        "graceful-fs": "^4.1.6"
+      }
+    },
+    "node_modules/jwt-decode": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz",
+      "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==",
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/lightningcss": {
+      "version": "1.33.0",
+      "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz",
+      "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==",
+      "devOptional": true,
+      "dependencies": {
+        "detect-libc": "^2.0.3"
+      },
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      },
+      "optionalDependencies": {
+        "lightningcss-android-arm64": "1.33.0",
+        "lightningcss-darwin-arm64": "1.33.0",
+        "lightningcss-darwin-x64": "1.33.0",
+        "lightningcss-freebsd-x64": "1.33.0",
+        "lightningcss-linux-arm-gnueabihf": "1.33.0",
+        "lightningcss-linux-arm64-gnu": "1.33.0",
+        "lightningcss-linux-arm64-musl": "1.33.0",
+        "lightningcss-linux-x64-gnu": "1.33.0",
+        "lightningcss-linux-x64-musl": "1.33.0",
+        "lightningcss-win32-arm64-msvc": "1.33.0",
+        "lightningcss-win32-x64-msvc": "1.33.0"
+      }
+    },
+    "node_modules/lightningcss-android-arm64": {
+      "version": "1.33.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz",
+      "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/lightningcss-darwin-arm64": {
+      "version": "1.33.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz",
+      "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/lightningcss-darwin-x64": {
+      "version": "1.33.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz",
+      "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/lightningcss-freebsd-x64": {
+      "version": "1.33.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz",
+      "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/lightningcss-linux-arm-gnueabihf": {
+      "version": "1.33.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz",
+      "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/lightningcss-linux-arm64-gnu": {
+      "version": "1.33.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz",
+      "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/lightningcss-linux-arm64-musl": {
+      "version": "1.33.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz",
+      "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/lightningcss-linux-x64-gnu": {
+      "version": "1.33.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz",
+      "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/lightningcss-linux-x64-musl": {
+      "version": "1.33.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz",
+      "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/lightningcss-win32-arm64-msvc": {
+      "version": "1.33.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz",
+      "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/lightningcss-win32-x64-msvc": {
+      "version": "1.33.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz",
+      "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/local-pkg": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.2.1.tgz",
+      "integrity": "sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q==",
+      "dependencies": {
+        "mlly": "^1.7.4",
+        "pkg-types": "^2.3.0",
+        "quansync": "^0.2.11"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      }
+    },
+    "node_modules/lodash": {
+      "version": "4.18.1",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
+      "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="
+    },
+    "node_modules/lodash-es": {
+      "version": "4.18.1",
+      "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz",
+      "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A=="
+    },
+    "node_modules/lodash-unified": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/lodash-unified/-/lodash-unified-1.0.3.tgz",
+      "integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==",
+      "peerDependencies": {
+        "@types/lodash-es": "*",
+        "lodash": "*",
+        "lodash-es": "*"
+      }
+    },
+    "node_modules/magic-string": {
+      "version": "0.30.21",
+      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+      "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+      "dependencies": {
+        "@jridgewell/sourcemap-codec": "^1.5.5"
+      }
+    },
+    "node_modules/magic-string-ast": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/magic-string-ast/-/magic-string-ast-1.0.3.tgz",
+      "integrity": "sha512-CvkkH1i81zl7mmb94DsRiFeG9V2fR2JeuK8yDgS8oiZSFa++wWLEgZ5ufEOyLHbvSbD1gTRKv9NdX69Rnvr9JA==",
+      "dependencies": {
+        "magic-string": "^0.30.19"
+      },
+      "engines": {
+        "node": ">=20.19.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sxzz"
+      }
+    },
+    "node_modules/math-intrinsics": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+      "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/memoize-one": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
+      "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw=="
+    },
+    "node_modules/mime-db": {
+      "version": "1.52.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime-types": {
+      "version": "2.1.35",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+      "dependencies": {
+        "mime-db": "1.52.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/minimist": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+      "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+      "dev": true,
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/mlly": {
+      "version": "1.8.2",
+      "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz",
+      "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==",
+      "dependencies": {
+        "acorn": "^8.16.0",
+        "pathe": "^2.0.3",
+        "pkg-types": "^1.3.1",
+        "ufo": "^1.6.3"
+      }
+    },
+    "node_modules/mlly/node_modules/confbox": {
+      "version": "0.1.8",
+      "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz",
+      "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="
+    },
+    "node_modules/mlly/node_modules/pkg-types": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz",
+      "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==",
+      "dependencies": {
+        "confbox": "^0.1.8",
+        "mlly": "^1.7.4",
+        "pathe": "^2.0.1"
+      }
+    },
+    "node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+    },
+    "node_modules/msw": {
+      "version": "2.15.0",
+      "resolved": "https://registry.npmjs.org/msw/-/msw-2.15.0.tgz",
+      "integrity": "sha512-2wQAmKkQKxRuXvYJxVhPGG0wZNBQyD06oJvxqw90XqLvptdqxdlHrFUfEteKkpaNORX3Xzc+HtEl/q0nfmN2wQ==",
+      "dev": true,
+      "hasInstallScript": true,
+      "optional": true,
+      "peer": true,
+      "dependencies": {
+        "@inquirer/confirm": "^6.0.11",
+        "@mswjs/interceptors": "^0.41.3",
+        "@open-draft/deferred-promise": "^3.0.0",
+        "@types/statuses": "^2.0.6",
+        "cookie": "^1.1.1",
+        "graphql": "^16.13.2",
+        "headers-polyfill": "^5.0.1",
+        "is-node-process": "^1.2.0",
+        "outvariant": "^1.4.3",
+        "path-to-regexp": "^6.3.0",
+        "picocolors": "^1.1.1",
+        "rettime": "^0.11.11",
+        "statuses": "^2.0.2",
+        "strict-event-emitter": "^0.5.1",
+        "tough-cookie": "^6.0.1",
+        "type-fest": "^5.5.0",
+        "until-async": "^3.0.2",
+        "yargs": "^17.7.2"
+      },
+      "bin": {
+        "msw": "cli/index.js"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/mswjs"
+      },
+      "peerDependencies": {
+        "typescript": ">= 4.8.x"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/muggle-string": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz",
+      "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ=="
+    },
+    "node_modules/mute-stream": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-3.0.0.tgz",
+      "integrity": "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/nanoid": {
+      "version": "3.3.19",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz",
+      "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "bin": {
+        "nanoid": "bin/nanoid.cjs"
+      },
+      "engines": {
+        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+      }
+    },
+    "node_modules/neo-async": {
+      "version": "2.6.2",
+      "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
+      "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
+      "dev": true
+    },
+    "node_modules/normalize-wheel-es": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz",
+      "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw=="
+    },
+    "node_modules/nostics": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/nostics/-/nostics-1.2.0.tgz",
+      "integrity": "sha512-FGqEfhQjrvo1lL8KFifdTQiNwwQHJxC1jtYE1Rc54qF/jxONUNL+kC9gS1krX8Q65PgrQ5fCqH/I4NhWBvdSqg=="
+    },
+    "node_modules/obug": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz",
+      "integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==",
+      "dev": true,
+      "funding": [
+        "https://github.com/sponsors/sxzz",
+        "https://opencollective.com/debug"
+      ],
+      "engines": {
+        "node": ">=12.20.0"
+      }
+    },
+    "node_modules/openapi-typescript-codegen": {
+      "version": "0.31.0",
+      "resolved": "https://registry.npmjs.org/openapi-typescript-codegen/-/openapi-typescript-codegen-0.31.0.tgz",
+      "integrity": "sha512-ph3njJ7veDXy6QKLKHqNiyIqWMMu+rcdtU87XuwBo+QfBOpE1PvqzIWY3KMZBnLaoACxhtrEPf5re7Y/Ty9Bnw==",
+      "dev": true,
+      "dependencies": {
+        "@apidevtools/json-schema-ref-parser": "^14.2.1",
+        "camelcase": "^6.3.0",
+        "commander": "^14.0.3",
+        "fs-extra": "^11.3.5",
+        "handlebars": "^4.7.9"
+      },
+      "bin": {
+        "openapi": "bin/index.js"
+      }
+    },
+    "node_modules/outvariant": {
+      "version": "1.4.3",
+      "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz",
+      "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==",
+      "dev": true,
+      "optional": true,
+      "peer": true
+    },
+    "node_modules/path-browserify": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
+      "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
+      "dev": true
+    },
+    "node_modules/path-to-regexp": {
+      "version": "6.3.0",
+      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz",
+      "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==",
+      "dev": true,
+      "optional": true,
+      "peer": true
+    },
+    "node_modules/pathe": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
+      "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="
+    },
+    "node_modules/perfect-debounce": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz",
+      "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g=="
+    },
+    "node_modules/picocolors": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+      "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
+    },
+    "node_modules/picomatch": {
+      "version": "4.0.7",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
+      "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/pinia": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/pinia/-/pinia-4.0.3.tgz",
+      "integrity": "sha512-XMQqpvjgG7LMqVhhFUzKLT4KEbsYbfZZ0CZU9PgdFm1O2VKBmIkykL8+SfNhOaT7sR0wT6BEgKT0YNDYWgmVRA==",
+      "dependencies": {
+        "nostics": "^1.1.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/posva"
+      },
+      "peerDependencies": {
+        "@vue/devtools-api": "^8.1.5",
+        "typescript": ">=5.6.0",
+        "vue": "^3.5.11"
+      },
+      "peerDependenciesMeta": {
+        "@vue/devtools-api": {
+          "optional": false
+        },
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/pkg-types": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.3.tgz",
+      "integrity": "sha512-j/lCFdcppV0JxWpCEITdbDltBxPP6cHT+yNJ6Go2OgoSA9518X847X9z0p6LtA4Nc16+eQzCZjRrWanTGvHJ5w==",
+      "dependencies": {
+        "confbox": "^0.3.1",
+        "exsolve": "^1.1.1",
+        "pathe": "^2.0.3"
+      }
+    },
+    "node_modules/pkg-types/node_modules/confbox": {
+      "version": "0.3.1",
+      "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.3.1.tgz",
+      "integrity": "sha512-cKUSoKa8YxFZZSmraVi7onONx3amu77ngK3kGpsYHDH7drPwCRkQE1RYMPlLRrMtnciRj274XNRxcHxnKmDSnA=="
+    },
+    "node_modules/postcss": {
+      "version": "8.5.28",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz",
+      "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==",
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/postcss"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "dependencies": {
+        "nanoid": "^3.3.18",
+        "picocolors": "^1.1.1",
+        "source-map-js": "^1.2.1"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      }
+    },
+    "node_modules/proxy-from-env": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
+      "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/quansync": {
+      "version": "0.2.11",
+      "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz",
+      "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/antfu"
+        },
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/sxzz"
+        }
+      ]
+    },
+    "node_modules/readdirp": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz",
+      "integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==",
+      "engines": {
+        "node": ">= 20.19.0"
+      },
+      "funding": {
+        "type": "individual",
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
+    "node_modules/require-directory": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+      "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/rettime": {
+      "version": "0.11.11",
+      "resolved": "https://registry.npmjs.org/rettime/-/rettime-0.11.11.tgz",
+      "integrity": "sha512-ILJRqVWBCTlg9r42fFgwVZx1gnFAcQF8mRoMkbgQfIrjEDf9nbBFDFx00oloOa+Q869FUtaYDXZvEfnecQSCoQ==",
+      "dev": true,
+      "optional": true,
+      "peer": true
+    },
+    "node_modules/rolldown": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.8.tgz",
+      "integrity": "sha512-Z67nTmhZe7anqnM/EjI392w5i/ANUinjip7QYsOyN37oayduxt3ksdX0hf5OOamkAd53BiIHfbfSzfUmzKFQqQ==",
+      "devOptional": true,
+      "dependencies": {
+        "@oxc-project/types": "=0.149.0",
+        "@rolldown/pluginutils": "^1.0.0"
+      },
+      "bin": {
+        "rolldown": "bin/cli.mjs"
+      },
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      },
+      "optionalDependencies": {
+        "@rolldown/binding-android-arm-eabi": "1.2.8",
+        "@rolldown/binding-android-arm64": "1.2.8",
+        "@rolldown/binding-darwin-arm64": "1.2.8",
+        "@rolldown/binding-darwin-x64": "1.2.8",
+        "@rolldown/binding-freebsd-x64": "1.2.8",
+        "@rolldown/binding-linux-arm-gnueabihf": "1.2.8",
+        "@rolldown/binding-linux-arm64-gnu": "1.2.8",
+        "@rolldown/binding-linux-arm64-musl": "1.2.8",
+        "@rolldown/binding-linux-ppc64-gnu": "1.2.8",
+        "@rolldown/binding-linux-s390x-gnu": "1.2.8",
+        "@rolldown/binding-linux-x64-gnu": "1.2.8",
+        "@rolldown/binding-linux-x64-musl": "1.2.8",
+        "@rolldown/binding-openharmony-arm64": "1.2.8",
+        "@rolldown/binding-win32-arm64-msvc": "1.2.8",
+        "@rolldown/binding-win32-x64-msvc": "1.2.8"
+      }
+    },
+    "node_modules/scule": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/scule/-/scule-1.3.0.tgz",
+      "integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g=="
+    },
+    "node_modules/set-cookie-parser": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.2.tgz",
+      "integrity": "sha512-5/r/lTwbJ3zQ+qwdUFZYeRNqda7P5HD8zQKqlSjdGt1/S0cjLAphHusj4Y58ahDtWn/g32xrIS58/ikOvwl0Lw==",
+      "dev": true,
+      "optional": true,
+      "peer": true
+    },
+    "node_modules/siginfo": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
+      "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
+      "dev": true
+    },
+    "node_modules/signal-exit": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+      "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/source-map": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+      "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/source-map-js": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+      "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/stackback": {
+      "version": "0.0.2",
+      "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
+      "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
+      "dev": true
+    },
+    "node_modules/statuses": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+      "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/std-env": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz",
+      "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==",
+      "dev": true
+    },
+    "node_modules/strict-event-emitter": {
+      "version": "0.5.1",
+      "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz",
+      "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==",
+      "dev": true,
+      "optional": true,
+      "peer": true
+    },
+    "node_modules/string-width": {
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/strip-ansi": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/tagged-tag": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz",
+      "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "engines": {
+        "node": ">=20"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/tinybench": {
+      "version": "6.1.4",
+      "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.1.4.tgz",
+      "integrity": "sha512-9APumHG7r4yOk4X4WlkmE71aZcv1gvin1czO3OQ1U9iJcFA5Ja/ygyb0vPOVHTthFozUYs8CLoLUlM8grb2lTQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=20.0.0"
+      }
+    },
+    "node_modules/tinyexec": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz",
+      "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/tinyglobby": {
+      "version": "0.2.17",
+      "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
+      "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
+      "dependencies": {
+        "fdir": "^6.5.0",
+        "picomatch": "^4.0.4"
+      },
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/SuperchupuDev"
+      }
+    },
+    "node_modules/tldts": {
+      "version": "7.4.12",
+      "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.12.tgz",
+      "integrity": "sha512-WylhSDKVeYnWXL3a+vKTaOxjnOeEGw938hImY8zoRWJjRRK/Jp1K+IihBzIONpUmW4e3WmXT6q5FW6vlESVZCA==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "dependencies": {
+        "tldts-core": "^7.4.12"
+      },
+      "bin": {
+        "tldts": "bin/cli.js"
+      }
+    },
+    "node_modules/tldts-core": {
+      "version": "7.4.12",
+      "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.12.tgz",
+      "integrity": "sha512-nYNzS2WRf4QJmjzFFgAxLOBjyBxAGRbCy9PVBPaglcYyYajh40VBn+v5Ngr96ZMc7oM0+aCJdtQnNejvdBnXMQ==",
+      "dev": true,
+      "optional": true,
+      "peer": true
+    },
+    "node_modules/tough-cookie": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz",
+      "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "dependencies": {
+        "tldts": "^7.0.5"
+      },
+      "engines": {
+        "node": ">=16"
+      }
+    },
+    "node_modules/type-fest": {
+      "version": "5.9.0",
+      "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.9.0.tgz",
+      "integrity": "sha512-yANm3Jr3GiJ1qgJlxGAVxTOIcEOk1rhQHamlXtnrCK7EHP4HeM9OGxtMg/W7HFdrVzw/ZWJKGVIJusVH85sLtw==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "dependencies": {
+        "tagged-tag": "^1.0.0"
+      },
+      "engines": {
+        "node": ">=20"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/typescript": {
+      "version": "6.0.3",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
+      "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
+      "devOptional": true,
+      "bin": {
+        "tsc": "bin/tsc",
+        "tsserver": "bin/tsserver"
+      },
+      "engines": {
+        "node": ">=14.17"
+      }
+    },
+    "node_modules/ufo": {
+      "version": "1.6.4",
+      "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz",
+      "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA=="
+    },
+    "node_modules/uglify-js": {
+      "version": "3.19.3",
+      "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz",
+      "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==",
+      "dev": true,
+      "optional": true,
+      "bin": {
+        "uglifyjs": "bin/uglifyjs"
+      },
+      "engines": {
+        "node": ">=0.8.0"
+      }
+    },
+    "node_modules/undici-types": {
+      "version": "7.18.2",
+      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
+      "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
+      "dev": true
+    },
+    "node_modules/universalify": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+      "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+      "dev": true,
+      "engines": {
+        "node": ">= 10.0.0"
+      }
+    },
+    "node_modules/unplugin": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-3.3.0.tgz",
+      "integrity": "sha512-qa66K+crbfyE6JK10GjvbJeRrOsuC/JpbnHctfyp/i4oBTxWOzJfRZyDiOk1PtErMFRu8JhsU/wPvOdBNWe5Rg==",
+      "dependencies": {
+        "@jridgewell/remapping": "^2.3.5",
+        "picomatch": "^4.0.4",
+        "webpack-virtual-modules": "^0.6.2"
+      },
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      },
+      "peerDependencies": {
+        "@farmfe/core": "*",
+        "@rspack/core": "*",
+        "bun-types-no-globals": "*",
+        "esbuild": "*",
+        "rolldown": "*",
+        "rollup": "*",
+        "unloader": "*",
+        "vite": "*",
+        "webpack": "*"
+      },
+      "peerDependenciesMeta": {
+        "@farmfe/core": {
+          "optional": true
+        },
+        "@rspack/core": {
+          "optional": true
+        },
+        "bun-types-no-globals": {
+          "optional": true
+        },
+        "esbuild": {
+          "optional": true
+        },
+        "rolldown": {
+          "optional": true
+        },
+        "rollup": {
+          "optional": true
+        },
+        "unloader": {
+          "optional": true
+        },
+        "vite": {
+          "optional": true
+        },
+        "webpack": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/unplugin-utils": {
+      "version": "0.3.2",
+      "resolved": "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.3.2.tgz",
+      "integrity": "sha512-xVToRh2CTmLk2HnEG7ac4rl1MJTT3RFkpS8B++/SnB0kXvuaavD+n3m/vrzyWQOdJNSZQACnbz01pnppbwV5BA==",
+      "dependencies": {
+        "pathe": "^2.0.3",
+        "picomatch": "^4.0.4"
+      },
+      "engines": {
+        "node": ">=20.19.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sxzz"
+      }
+    },
+    "node_modules/until-async": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/until-async/-/until-async-3.0.2.tgz",
+      "integrity": "sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "funding": {
+        "url": "https://github.com/sponsors/kettanaito"
+      }
+    },
+    "node_modules/vite": {
+      "version": "8.3.0",
+      "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.0.tgz",
+      "integrity": "sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ==",
+      "devOptional": true,
+      "dependencies": {
+        "lightningcss": "^1.33.0",
+        "picomatch": "^4.0.7",
+        "postcss": "^8.5.28",
+        "rolldown": "~1.2.6",
+        "tinyglobby": "^0.2.17"
+      },
+      "bin": {
+        "vite": "bin/vite.js"
+      },
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      },
+      "funding": {
+        "url": "https://github.com/vitejs/vite?sponsor=1"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.3"
+      },
+      "peerDependencies": {
+        "@types/node": "^20.19.0 || >=22.12.0",
+        "@vitejs/devtools": "^0.7.1",
+        "esbuild": "^0.27.0 || ^0.28.0",
+        "jiti": ">=1.21.0",
+        "less": "^4.0.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
+        },
+        "@vitejs/devtools": {
+          "optional": true
+        },
+        "esbuild": {
+          "optional": true
+        },
+        "jiti": {
+          "optional": true
+        },
+        "less": {
+          "optional": true
+        },
+        "sass": {
+          "optional": true
+        },
+        "sass-embedded": {
+          "optional": true
+        },
+        "stylus": {
+          "optional": true
+        },
+        "sugarss": {
+          "optional": true
+        },
+        "terser": {
+          "optional": true
+        },
+        "tsx": {
+          "optional": true
+        },
+        "yaml": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/vitest": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.0.tgz",
+      "integrity": "sha512-gpsMNoRhMjMktVxPtstOH4/PJuPyovVaMDr4oDilXaGH1EcqM2OE96SoHT2VIQ6fTGtTjqmHDrEu2X9RQiXf8Q==",
+      "dev": true,
+      "dependencies": {
+        "@types/chai": "^5.2.2",
+        "@vitest/mocker": "5.0.0",
+        "chai": "^6.2.2",
+        "es-module-lexer": "^2.3.2",
+        "expect-type": "^1.4.0",
+        "magic-string": "^1.2.3",
+        "obug": "^2.1.4",
+        "picomatch": "^4.0.7",
+        "std-env": "^4.2.0",
+        "tinybench": "6.1.4",
+        "tinyexec": "1.3.0",
+        "tinyglobby": "^0.2.17",
+        "why-is-node-running": "^2.3.0"
+      },
+      "bin": {
+        "vitest": "vitest.mjs"
+      },
+      "engines": {
+        "node": "^22.12.0 || ^24.0.0 || >=26.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      },
+      "peerDependencies": {
+        "@edge-runtime/vm": "*",
+        "@opentelemetry/api": "^1.9.0",
+        "@types/node": "^22.0.0 || >=24.0.0",
+        "@vitest/browser-playwright": "5.0.0",
+        "@vitest/browser-preview": "5.0.0",
+        "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0",
+        "@vitest/coverage-istanbul": "5.0.0",
+        "@vitest/coverage-v8": "5.0.0",
+        "@vitest/ui": "5.0.0",
+        "happy-dom": "*",
+        "jsdom": "*",
+        "vite": "^6.4.0 || ^7.0.0 || ^8.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@edge-runtime/vm": {
+          "optional": true
+        },
+        "@opentelemetry/api": {
+          "optional": true
+        },
+        "@types/node": {
+          "optional": true
+        },
+        "@vitest/browser-playwright": {
+          "optional": true
+        },
+        "@vitest/browser-preview": {
+          "optional": true
+        },
+        "@vitest/browser-webdriverio": {
+          "optional": true
+        },
+        "@vitest/coverage-istanbul": {
+          "optional": true
+        },
+        "@vitest/coverage-v8": {
+          "optional": true
+        },
+        "@vitest/ui": {
+          "optional": true
+        },
+        "happy-dom": {
+          "optional": true
+        },
+        "jsdom": {
+          "optional": true
+        },
+        "vite": {
+          "optional": false
+        }
+      }
+    },
+    "node_modules/vitest/node_modules/magic-string": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.3.1.tgz",
+      "integrity": "sha512-rm91zr2Ou+XueDTohjQQjdQEcYM6zVi8KVUCG8Ec3vHwUEKrhSdCNyfuIywkA6hcCAteIn0ZOtAHA6eGpiX+Pg==",
+      "dev": true,
+      "dependencies": {
+        "@jridgewell/sourcemap-codec": "^1.6.0"
+      }
+    },
+    "node_modules/vscode-uri": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.2.0.tgz",
+      "integrity": "sha512-m2gXo3bn0G1kT9InzMf07fTbqMbGtyckj3bH5ktLO+1Ssv+yiATZ4dhwaQv9UZWxJh6E9IFGnQyjgWVDWVBDrg==",
+      "dev": true
+    },
+    "node_modules/vue": {
+      "version": "3.5.42",
+      "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.42.tgz",
+      "integrity": "sha512-4RyHQTbQvOPs3MfvUO1Sg0YRrKNnA0mAVtvpd12Tg1fKDN7OHBUl1IqSn8zGJjK9nI3NkNp8cgTpVrSZC5TTcA==",
+      "dependencies": {
+        "@vue/compiler-dom": "3.5.42",
+        "@vue/compiler-sfc": "3.5.42",
+        "@vue/runtime-dom": "3.5.42",
+        "@vue/server-renderer": "3.5.42",
+        "@vue/shared": "3.5.42"
+      },
+      "peerDependencies": {
+        "typescript": "*"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/vue-component-type-helpers": {
+      "version": "3.3.11",
+      "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-3.3.11.tgz",
+      "integrity": "sha512-LwcxzeliO9fkQcpJG0PoX8X5kmAhKmH9wkpDLxNabwzkQ9Zeib2YVHwFV4pcWmMLfXVfjr/dSV+DaJ3cIPgSNA=="
+    },
+    "node_modules/vue-router": {
+      "version": "5.3.1",
+      "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.3.1.tgz",
+      "integrity": "sha512-GDBZzgmILxA/kFnkFbJjQZdZ2QQbngnIMMuoUcjhZIfH1RGMaPjPwX5ASnV38qamuA9uhO0RDjSBHTDNG2uXyQ==",
+      "dependencies": {
+        "@vue-macros/common": "^3.1.3",
+        "@vue/devtools-api": "^8.1.5",
+        "ast-walker-scope": "^0.9.0",
+        "chokidar": "^5.0.0",
+        "confbox": "^0.2.4",
+        "local-pkg": "^1.2.1",
+        "magic-string": "^0.30.21",
+        "mlly": "^1.8.2",
+        "muggle-string": "^0.4.1",
+        "nostics": "^1.1.4",
+        "pathe": "^2.0.3",
+        "picomatch": "^4.0.5",
+        "scule": "^1.3.0",
+        "tinyglobby": "^0.2.17",
+        "unplugin": "^3.3.0",
+        "unplugin-utils": "^0.3.2"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/posva"
+      },
+      "peerDependencies": {
+        "@pinia/colada": ">=0.21.2",
+        "@vue/compiler-sfc": "^3.5.34 || ^4.0.0",
+        "pinia": "^3.0.4 || ^4.0.2",
+        "vite": "^7.3.0 || ^8.0.0",
+        "vue": "^3.5.34 || ^4.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@pinia/colada": {
+          "optional": true
+        },
+        "@vue/compiler-sfc": {
+          "optional": true
+        },
+        "pinia": {
+          "optional": true
+        },
+        "vite": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/vue-tsc": {
+      "version": "3.3.11",
+      "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.3.11.tgz",
+      "integrity": "sha512-gOb0B9rtU2+f1dszwPqSH5kAieIF9ReeLhD3kSRNHv5WZZUQz/JdVXW0RTdqhNTMlQkqKzrTTviqKr/4FYZraQ==",
+      "dev": true,
+      "dependencies": {
+        "@volar/typescript": "2.4.28",
+        "@vue/language-core": "3.3.11"
+      },
+      "bin": {
+        "vue-tsc": "bin/vue-tsc.js"
+      },
+      "peerDependencies": {
+        "typescript": ">=5.0.0"
+      }
+    },
+    "node_modules/webpack-virtual-modules": {
+      "version": "0.6.2",
+      "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz",
+      "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="
+    },
+    "node_modules/why-is-node-running": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
+      "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
+      "dev": true,
+      "dependencies": {
+        "siginfo": "^2.0.0",
+        "stackback": "0.0.2"
+      },
+      "bin": {
+        "why-is-node-running": "cli.js"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/wordwrap": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
+      "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==",
+      "dev": true
+    },
+    "node_modules/wrap-ansi": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+      "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "dependencies": {
+        "ansi-styles": "^4.0.0",
+        "string-width": "^4.1.0",
+        "strip-ansi": "^6.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
+    "node_modules/y18n": {
+      "version": "5.0.8",
+      "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+      "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/yargs": {
+      "version": "17.7.3",
+      "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz",
+      "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "dependencies": {
+        "cliui": "^8.0.1",
+        "escalade": "^3.1.1",
+        "get-caller-file": "^2.0.5",
+        "require-directory": "^2.1.1",
+        "string-width": "^4.2.3",
+        "y18n": "^5.0.5",
+        "yargs-parser": "^21.1.1"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/yargs-parser": {
+      "version": "21.1.1",
+      "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
+      "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "engines": {
+        "node": ">=12"
+      }
+    }
+  },
+  "dependencies": {
+    "@apidevtools/json-schema-ref-parser": {
+      "version": "14.2.1",
+      "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-14.2.1.tgz",
+      "integrity": "sha512-HmdFw9CDYqM6B25pqGBpNeLCKvGPlIx1EbLrVL0zPvj50CJQUHyBNBw45Muk0kEIkogo1VZvOKHajdMuAzSxRg==",
+      "dev": true,
+      "requires": {
+        "js-yaml": "^4.1.0"
+      }
+    },
+    "@babel/helper-string-parser": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
+      "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="
+    },
+    "@babel/helper-validator-identifier": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
+      "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="
+    },
+    "@babel/parser": {
+      "version": "7.29.8",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz",
+      "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==",
+      "requires": {
+        "@babel/types": "^7.29.8"
+      }
+    },
+    "@babel/types": {
+      "version": "7.29.8",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz",
+      "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==",
+      "requires": {
+        "@babel/helper-string-parser": "^7.29.7",
+        "@babel/helper-validator-identifier": "^7.29.7"
+      }
+    },
+    "@ctrl/tinycolor": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.2.1.tgz",
+      "integrity": "sha512-Vh5uy9Y1JBMId6J1f68TMXnZrTCvNdo9Fu2k0d1YnJngR+sM5kWTKl441c/3TFs+cG0NWOjRWgQ/iaWhbYx+7A=="
+    },
+    "@element-plus/icons-vue": {
+      "version": "2.3.2",
+      "resolved": "https://registry.npmjs.org/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz",
+      "integrity": "sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==",
+      "requires": {}
+    },
+    "@floating-ui/core": {
+      "version": "1.8.0",
+      "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz",
+      "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==",
+      "requires": {
+        "@floating-ui/utils": "^0.2.12"
+      }
+    },
+    "@floating-ui/dom": {
+      "version": "1.8.0",
+      "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz",
+      "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==",
+      "requires": {
+        "@floating-ui/core": "^1.8.0",
+        "@floating-ui/utils": "^0.2.12"
+      }
+    },
+    "@floating-ui/utils": {
+      "version": "0.2.12",
+      "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz",
+      "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww=="
+    },
+    "@inquirer/ansi": {
+      "version": "2.0.8",
+      "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.8.tgz",
+      "integrity": "sha512-WpQM+Ti6Z40EFwwt+uL2p4UabT+W179zHp6HhLVOzfbwnVn05IPO/eXIZXGNqcT1jbQ15SujNLzQ39k4QPPxBQ==",
+      "dev": true,
+      "optional": true,
+      "peer": true
+    },
+    "@inquirer/confirm": {
+      "version": "6.3.2",
+      "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.3.2.tgz",
+      "integrity": "sha512-Xvr/0HggjddPtGppuqVmxhTw+Hr8PvsZ/k0HmOEaAqQEt80OITNkFWnsdNmyT0/eM4Ab+iJLx2R8rctlEyfSVg==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "requires": {
+        "@inquirer/core": "^12.0.3",
+        "@inquirer/type": "4.1.1"
+      }
+    },
+    "@inquirer/core": {
+      "version": "12.0.3",
+      "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-12.0.3.tgz",
+      "integrity": "sha512-wsSy0sznmXwkty+2PzZwx00Cazc/E0r0B7mAzdGROz2Ct+DFZXaK7WDjGZvgjRldxH5ZhFVfF2lgkYrqgOw2KA==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "requires": {
+        "@inquirer/ansi": "^2.0.8",
+        "@inquirer/figures": "^2.0.9",
+        "@inquirer/type": "4.1.1",
+        "cli-width": "^4.1.0",
+        "fast-wrap-ansi": "^0.2.0",
+        "mute-stream": "^3.0.0",
+        "signal-exit": "^4.1.0"
+      }
+    },
+    "@inquirer/figures": {
+      "version": "2.0.9",
+      "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.9.tgz",
+      "integrity": "sha512-EAWgUTGQ/Umgga51dE3B2PUHbufuXarDfg86uVgoSgNHNNQnyFKcOrQLWVqYMghuSyHh8+2HUH0Js9cTC1WAdg==",
+      "dev": true,
+      "optional": true,
+      "peer": true
+    },
+    "@inquirer/type": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.1.1.tgz",
+      "integrity": "sha512-yJoHYrMnxIsJZCY+0Vb66Dy3he3kL3e2wOBKhoSwWWAzZAY82emlxwgprCtp6yRixvNRNq9ztfRWQYPNr3Go7A==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "requires": {}
+    },
+    "@jridgewell/gen-mapping": {
+      "version": "0.3.13",
+      "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+      "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+      "requires": {
+        "@jridgewell/sourcemap-codec": "^1.5.0",
+        "@jridgewell/trace-mapping": "^0.3.24"
+      }
+    },
+    "@jridgewell/remapping": {
+      "version": "2.3.5",
+      "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+      "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+      "requires": {
+        "@jridgewell/gen-mapping": "^0.3.5",
+        "@jridgewell/trace-mapping": "^0.3.24"
+      }
+    },
+    "@jridgewell/resolve-uri": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+      "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="
+    },
+    "@jridgewell/sourcemap-codec": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz",
+      "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw=="
+    },
+    "@jridgewell/trace-mapping": {
+      "version": "0.3.31",
+      "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+      "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+      "requires": {
+        "@jridgewell/resolve-uri": "^3.1.0",
+        "@jridgewell/sourcemap-codec": "^1.4.14"
+      }
+    },
+    "@mswjs/interceptors": {
+      "version": "0.41.9",
+      "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.41.9.tgz",
+      "integrity": "sha512-VVPPgHyQ6ShqnrmDWuxjmUIsO9gWyOZFmuOfLd9LfBGQJwZfy0gvv9pbHSJuoFNIYC7ZDX9aoFwowjcdSC4E8w==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "requires": {
+        "@open-draft/deferred-promise": "^2.2.0",
+        "@open-draft/logger": "^0.3.0",
+        "@open-draft/until": "^2.0.0",
+        "is-node-process": "^1.2.0",
+        "outvariant": "^1.4.3",
+        "strict-event-emitter": "^0.5.1"
+      },
+      "dependencies": {
+        "@open-draft/deferred-promise": {
+          "version": "2.2.0",
+          "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz",
+          "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==",
+          "dev": true,
+          "optional": true,
+          "peer": true
+        }
+      }
+    },
+    "@open-draft/deferred-promise": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-3.0.0.tgz",
+      "integrity": "sha512-XW375UK8/9SqUVNVa6M0yEy8+iTi4QN5VZ7aZuRFQmy76LRwI9wy5F4YIBU6T+eTe2/DNDo8tqu8RHlwLHM6RA==",
+      "dev": true,
+      "optional": true,
+      "peer": true
+    },
+    "@open-draft/logger": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz",
+      "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "requires": {
+        "is-node-process": "^1.2.0",
+        "outvariant": "^1.4.0"
+      }
+    },
+    "@open-draft/until": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz",
+      "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==",
+      "dev": true,
+      "optional": true,
+      "peer": true
+    },
+    "@oxc-project/types": {
+      "version": "0.149.0",
+      "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.149.0.tgz",
+      "integrity": "sha512-Efcc+iF0j3Bf67YjEqIqWXbX5XddXoK/Mw4K1/JuXwRCZ8N16VR7iT23nlCc9XrveFVh/E5Rqs2StT0V8v9LdA==",
+      "devOptional": true
+    },
+    "@popperjs/core": {
+      "version": "npm:@sxzz/popperjs-es@2.11.8",
+      "resolved": "https://registry.npmjs.org/@sxzz/popperjs-es/-/popperjs-es-2.11.8.tgz",
+      "integrity": "sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ=="
+    },
+    "@rolldown/binding-android-arm-eabi": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.8.tgz",
+      "integrity": "sha512-tN5aztYkKCte4i5SIrrz5yK/HMjEuCqCSCJa418jOV8tZ1cBY3YF2otxB1ktPxzsLA1BeTqwapK0bfjxNvHJVw==",
+      "dev": true,
+      "optional": true
+    },
+    "@rolldown/binding-android-arm64": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.8.tgz",
+      "integrity": "sha512-dIYTWl9XprMUiQFoc55KUyk/oS8SKYH3zFl0LTR7RT0Xj4hgSVyuJcroH8JUu8RcpF8fTB6E0aOwCkZoYPcDSQ==",
+      "dev": true,
+      "optional": true
+    },
+    "@rolldown/binding-darwin-arm64": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.8.tgz",
+      "integrity": "sha512-PCSDQGXD2IyTEFrcgPyBM8jJuGmrbCMuoIOXdbEGVemruKACXoLQJrb+A45Z0L5t1RQkdfJprAYPkikbh7dzdA==",
+      "dev": true,
+      "optional": true
+    },
+    "@rolldown/binding-darwin-x64": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.8.tgz",
+      "integrity": "sha512-Uk7lRsGhPFHVX/sAUC6D5H9Ol30dFHd6iquokll2th3LpdJ3F5CzQB+7DHn0Ri2mG+U7k2zXiPHDrwZenXhwSA==",
+      "dev": true,
+      "optional": true
+    },
+    "@rolldown/binding-freebsd-x64": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.8.tgz",
+      "integrity": "sha512-DjszaTEVogPqA5bYzsEeqDCQxbcp2fexQwKcRspYji2yzR68fCf+e4fx6kBSRDwX5/brZaHw/hWS9+A/+/w9sQ==",
+      "dev": true,
+      "optional": true
+    },
+    "@rolldown/binding-linux-arm-gnueabihf": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.8.tgz",
+      "integrity": "sha512-zmwa7FTmdzB6aaEEuuls18H6Ap5JmJPSoPTuXixeJZV6tG40SyLkApQtz1g8ptZtiEKqj9OM0oNLPh1AgvE31Q==",
+      "dev": true,
+      "optional": true
+    },
+    "@rolldown/binding-linux-arm64-gnu": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.8.tgz",
+      "integrity": "sha512-KdYQDPHwJVnbFwdTGMgxsI9SqblBlz6STGM+w1We/d5B8OWWidYH0MwkU/uA1wM5fIpO2MkOVxXrNzzuZhw9ew==",
+      "dev": true,
+      "optional": true
+    },
+    "@rolldown/binding-linux-arm64-musl": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.8.tgz",
+      "integrity": "sha512-jFJTifHnNPY+yzOoNZQfSIysrVyXzEQPhPnOUjmD1bcQGHH6s7c8cViKWar8YplQImE5N9JRqMCLrM2CdxOrZA==",
+      "dev": true,
+      "optional": true
+    },
+    "@rolldown/binding-linux-ppc64-gnu": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.8.tgz",
+      "integrity": "sha512-FhiOziBDWPBjbcmRzfLyIJnaP7AVMFXT7YCXPjXxj7wKU3vx24RjrCNN/zjvVa+N2vVoHJwCoUBvsrN/DG3zIA==",
+      "dev": true,
+      "optional": true
+    },
+    "@rolldown/binding-linux-s390x-gnu": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.8.tgz",
+      "integrity": "sha512-WnHfADMzOV2Y55wlx1hzzQnar/wDt/VdvWSD99r18Mz9ylNieIGOkRx3UV21h7m/eJvjySYJkO26VvGNFkwsIQ==",
+      "dev": true,
+      "optional": true
+    },
+    "@rolldown/binding-linux-x64-gnu": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.8.tgz",
+      "integrity": "sha512-H9tRr5ibfXFVLxbPOseVewewFpl28zcEdjRDt2FTUZU7odxP0gEv1ki4/kGmcGOh78oRwZuuQllGLZ9zTJp84g==",
+      "dev": true,
+      "optional": true
+    },
+    "@rolldown/binding-linux-x64-musl": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.8.tgz",
+      "integrity": "sha512-UefiqfM3D6IVNlZ8tSGs9+Ejjud2T+oxO0IHADU45Y+lyEjD2dVFyZHbkfX0LUb5Zugo/oIv1eCO/KVYhgYJYA==",
+      "dev": true,
+      "optional": true
+    },
+    "@rolldown/binding-openharmony-arm64": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.8.tgz",
+      "integrity": "sha512-637Ke4kWSy6rp9cxQ9gMOXlxPgIw/c1beASV4M//3+9I4uwBVOOl74G+e3zyU3u19U7RkRl/HuewixZ/Z6+Rjg==",
+      "dev": true,
+      "optional": true
+    },
+    "@rolldown/binding-win32-arm64-msvc": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.8.tgz",
+      "integrity": "sha512-xWBkPOF1Q9k/Gv1nQXnVdLxKu74jXppuOM4Z3mnypVUJJJwLsMl7hNJGRAUJoG8A5MgOI1ACKM+wBFxSJzKy4A==",
+      "dev": true,
+      "optional": true
+    },
+    "@rolldown/binding-win32-x64-msvc": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.8.tgz",
+      "integrity": "sha512-uz2ZvfgXbxqNwijjjbxrnvALwpyODDcgc1T1N8N3rf/DXKQmaFwmB4LX4yyjggpwN2obdQLb2rgirX5ffCWYng==",
+      "dev": true,
+      "optional": true
+    },
+    "@rolldown/pluginutils": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
+      "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
+      "devOptional": true
+    },
+    "@types/chai": {
+      "version": "5.2.3",
+      "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
+      "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
+      "dev": true,
+      "requires": {
+        "@types/deep-eql": "*",
+        "assertion-error": "^2.0.1"
+      }
+    },
+    "@types/crypto-js": {
+      "version": "4.2.2",
+      "resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.2.2.tgz",
+      "integrity": "sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==",
+      "dev": true
+    },
+    "@types/deep-eql": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
+      "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
+      "dev": true
+    },
+    "@types/estree": {
+      "version": "1.0.9",
+      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
+      "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
+      "dev": true
+    },
+    "@types/json-schema": {
+      "version": "7.0.15",
+      "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+      "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+      "dev": true,
+      "peer": true
+    },
+    "@types/lodash": {
+      "version": "4.17.25",
+      "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.25.tgz",
+      "integrity": "sha512-+K1NIO8I+F9/wNulfVvu23QYd0Pe9/OCqRrim4NoYIf1VoEDL90Ve4ClzpyqBLc7NpGGWRvYNCKZ1BE/Jpf8dQ=="
+    },
+    "@types/lodash-es": {
+      "version": "4.17.12",
+      "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz",
+      "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
+      "requires": {
+        "@types/lodash": "*"
+      }
+    },
+    "@types/node": {
+      "version": "24.13.4",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.4.tgz",
+      "integrity": "sha512-YJ7EqCstVTzIr0fMr7qul/977en+pQHrfmuKIo6Zr9i75Be21dr3MovcfvGtyvi2HAUrRerWps5sMO9I7WaxDw==",
+      "dev": true,
+      "requires": {
+        "undici-types": "~7.18.0"
+      }
+    },
+    "@types/set-cookie-parser": {
+      "version": "2.4.10",
+      "resolved": "https://registry.npmjs.org/@types/set-cookie-parser/-/set-cookie-parser-2.4.10.tgz",
+      "integrity": "sha512-GGmQVGpQWUe5qglJozEjZV/5dyxbOOZ0LHe/lqyWssB88Y4svNfst0uqBVscdDeIKl5Jy5+aPSvy7mI9tYRguw==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "requires": {
+        "@types/node": "*"
+      }
+    },
+    "@types/statuses": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.6.tgz",
+      "integrity": "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==",
+      "dev": true,
+      "optional": true,
+      "peer": true
+    },
+    "@types/web-bluetooth": {
+      "version": "0.0.21",
+      "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
+      "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA=="
+    },
+    "@vitejs/plugin-vue": {
+      "version": "6.0.8",
+      "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.8.tgz",
+      "integrity": "sha512-0ZjgOg7oO6farnNGup7yvoM/YXZV84OZxHAwtflItNa/6zzQyVb5LNxyea3FEKEX2XlagIKzrlH7wwxkKgtiew==",
+      "dev": true,
+      "requires": {
+        "@rolldown/pluginutils": "^1.0.1"
+      }
+    },
+    "@vitest/mocker": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.0.tgz",
+      "integrity": "sha512-66PGTMIiVJP3t4a5yxU9qPtf7MdTBs8jmToMvy+HVflB3Yy13WJZTtPePdvU+wjRV02SKK5doLbSA6o9pwOmiA==",
+      "dev": true,
+      "requires": {
+        "@jridgewell/trace-mapping": "0.3.31",
+        "@vitest/spy": "5.0.0",
+        "estree-walker": "^3.0.3",
+        "magic-string": "^1.2.3"
+      },
+      "dependencies": {
+        "estree-walker": {
+          "version": "3.0.3",
+          "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
+          "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+          "dev": true,
+          "requires": {
+            "@types/estree": "^1.0.0"
+          }
+        },
+        "magic-string": {
+          "version": "1.3.1",
+          "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.3.1.tgz",
+          "integrity": "sha512-rm91zr2Ou+XueDTohjQQjdQEcYM6zVi8KVUCG8Ec3vHwUEKrhSdCNyfuIywkA6hcCAteIn0ZOtAHA6eGpiX+Pg==",
+          "dev": true,
+          "requires": {
+            "@jridgewell/sourcemap-codec": "^1.6.0"
+          }
+        }
+      }
+    },
+    "@vitest/spy": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.0.tgz",
+      "integrity": "sha512-uy+luWBAPw9XfthoHi5AkfHUnuPYEESjl0p/r+meoBnU8bxg5GDQ3Ey8MjcJ6sqahkL4PFyrvfMJJBw7LbU06g==",
+      "dev": true
+    },
+    "@volar/language-core": {
+      "version": "2.4.28",
+      "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.28.tgz",
+      "integrity": "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==",
+      "dev": true,
+      "requires": {
+        "@volar/source-map": "2.4.28"
+      }
+    },
+    "@volar/source-map": {
+      "version": "2.4.28",
+      "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.28.tgz",
+      "integrity": "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==",
+      "dev": true
+    },
+    "@volar/typescript": {
+      "version": "2.4.28",
+      "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.28.tgz",
+      "integrity": "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==",
+      "dev": true,
+      "requires": {
+        "@volar/language-core": "2.4.28",
+        "path-browserify": "^1.0.1",
+        "vscode-uri": "^3.0.8"
+      }
+    },
+    "@vue-macros/common": {
+      "version": "3.1.4",
+      "resolved": "https://registry.npmjs.org/@vue-macros/common/-/common-3.1.4.tgz",
+      "integrity": "sha512-/5Fv+6DgIcM9ajY05ZmKBv+LMX1M9A0X+IUwDRVdt67ciw8OV9bvG2r34p3RiEadlsQybjhKPRKNXDC8Bp23cw==",
+      "requires": {
+        "@vue/compiler-sfc": "^3.5.22",
+        "ast-kit": "^2.1.2",
+        "local-pkg": "^1.1.2",
+        "magic-string-ast": "^1.0.2",
+        "unplugin-utils": "^0.3.0"
+      }
+    },
+    "@vue/compiler-core": {
+      "version": "3.5.42",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.42.tgz",
+      "integrity": "sha512-2Ye1ilMtKXxl8qZUrQ5j0CdgenFp/HFQmta6rfRyfEsTG69L6Wk+tWuNoHYHMx9E8tF2Slvdg1FuwDvAXdy1LQ==",
+      "requires": {
+        "@babel/parser": "^7.29.8",
+        "@vue/shared": "3.5.42",
+        "entities": "^7.0.1",
+        "estree-walker": "^2.0.2",
+        "source-map-js": "^1.2.1"
+      }
+    },
+    "@vue/compiler-dom": {
+      "version": "3.5.42",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.42.tgz",
+      "integrity": "sha512-qbhQZEFmycr+ni/qyuccS4sucNN7VAbDfbkvNxWOX2VfgFm90MNs3/UhRNKoPMEIVn0F8gdlYjLPvqxHwHeQOA==",
+      "requires": {
+        "@vue/compiler-core": "3.5.42",
+        "@vue/shared": "3.5.42"
+      }
+    },
+    "@vue/compiler-sfc": {
+      "version": "3.5.42",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.42.tgz",
+      "integrity": "sha512-fkCAFB4okcAANGMThboWnScp/gzWjU0ZSkVnjTIiplmMDq2uq0tIB3j+xVu4rhv5rvOgBySCysudmbMd6xRRqw==",
+      "requires": {
+        "@babel/parser": "^7.29.8",
+        "@vue/compiler-core": "3.5.42",
+        "@vue/compiler-dom": "3.5.42",
+        "@vue/compiler-ssr": "3.5.42",
+        "@vue/shared": "3.5.42",
+        "estree-walker": "^2.0.2",
+        "magic-string": "^0.30.21",
+        "postcss": "^8.5.19",
+        "source-map-js": "^1.2.1"
+      }
+    },
+    "@vue/compiler-ssr": {
+      "version": "3.5.42",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.42.tgz",
+      "integrity": "sha512-xmLk3wLkbizPAiLyomjgFFosf2ys9b5Ghb+oh/k2tnvipNz8OFrQOiTcWCzyK7MpBp9KkyGtfvgfLUivbmuGYA==",
+      "requires": {
+        "@vue/compiler-dom": "3.5.42",
+        "@vue/shared": "3.5.42"
+      }
+    },
+    "@vue/devtools-api": {
+      "version": "8.2.1",
+      "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.2.1.tgz",
+      "integrity": "sha512-6u4vXBlIBAC1wMplIZgpyPn7uh/s4Bf6F5bMzvLv+EdJ0aHs/+4B7Ygv864EStQSjRbsRzTko/kUG1A1IejQ3A==",
+      "requires": {
+        "@vue/devtools-kit": "^8.2.1"
+      }
+    },
+    "@vue/devtools-kit": {
+      "version": "8.2.1",
+      "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.2.1.tgz",
+      "integrity": "sha512-FIGIuq3AWReEpbAHY/cRGeHDfI0qOb8OCQ3YjbEAX04uaxIDbGc9rhkbVcG7rnfHPXE3RsU5KrWOu9V/okd8AQ==",
+      "requires": {
+        "@vue/devtools-shared": "^8.2.1",
+        "birpc": "^2.6.1",
+        "hookable": "^5.5.3",
+        "perfect-debounce": "^2.0.0"
+      }
+    },
+    "@vue/devtools-shared": {
+      "version": "8.2.1",
+      "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.2.1.tgz",
+      "integrity": "sha512-Fkac7lUdGReh6pVOi3AYPRGe82LQqRmAfThW7RRligOAP0ZA/Z1z9XLHDM9dv34pV2HRc79DK8uKPeG2fLnA/g=="
+    },
+    "@vue/language-core": {
+      "version": "3.3.11",
+      "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.3.11.tgz",
+      "integrity": "sha512-QJmpliwAVpC/OxubIByPAhNzsQPRc8/gxlN2qnVzVfIMjMDz/9RnXRFoetjz5yEgXVXyp4LqhXq3V53PjmNzFw==",
+      "dev": true,
+      "requires": {
+        "@volar/language-core": "2.4.28",
+        "@vue/compiler-dom": "^3.5.0",
+        "@vue/shared": "^3.5.0",
+        "alien-signals": "^3.2.1",
+        "muggle-string": "^0.4.1",
+        "path-browserify": "^1.0.1",
+        "picomatch": "^4.0.4"
+      }
+    },
+    "@vue/reactivity": {
+      "version": "3.5.42",
+      "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.42.tgz",
+      "integrity": "sha512-TzNNfKpb7hDxbQltwAut8VDQA5YP+BuRlxntHUuRjyKwlMvmAPbs3unhCvieijifY6vFfVBwsS7wG/C7uq+bEQ==",
+      "requires": {
+        "@vue/shared": "3.5.42"
+      }
+    },
+    "@vue/runtime-core": {
+      "version": "3.5.42",
+      "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.42.tgz",
+      "integrity": "sha512-9uACtuHs7vJGkm5Bp3xu4xRDLFTIYy5DgxpToVjqGIAhAEKwQfsaLvKINhM6nFVp6bZPRFGdDqd1g52MqKsotA==",
+      "requires": {
+        "@vue/reactivity": "3.5.42",
+        "@vue/shared": "3.5.42"
+      }
+    },
+    "@vue/runtime-dom": {
+      "version": "3.5.42",
+      "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.42.tgz",
+      "integrity": "sha512-rsCmhiWLaRxGltLwhlCWyYkFn7WAbKRh0q17eZ1A6Dq6eqc2ACQ61IIryxz0LrsvCzHSilLA9JHovVwM8CNE2g==",
+      "requires": {
+        "@vue/reactivity": "3.5.42",
+        "@vue/runtime-core": "3.5.42",
+        "@vue/shared": "3.5.42",
+        "csstype": "^3.2.3"
+      }
+    },
+    "@vue/server-renderer": {
+      "version": "3.5.42",
+      "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.42.tgz",
+      "integrity": "sha512-2++5dUyYS4gvo7xQXSECUDhB7TS0aOl5SeVfC5qSq1Jgfhjvegw1zqhwTIR3imZ+QYPJQw9gfcFvXGAjGZ7ajQ==",
+      "requires": {
+        "@vue/compiler-ssr": "3.5.42",
+        "@vue/runtime-dom": "3.5.42",
+        "@vue/shared": "3.5.42"
+      }
+    },
+    "@vue/shared": {
+      "version": "3.5.42",
+      "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.42.tgz",
+      "integrity": "sha512-2rPxex1jQf4jvl9MOHl6YaXCPcrNqz/FstMOEh3QWY+/OME9nQTvl9WYeCwhW7AFjaR0SnngZGlp/wkR6rkI6g=="
+    },
+    "@vue/tsconfig": {
+      "version": "0.9.1",
+      "resolved": "https://registry.npmjs.org/@vue/tsconfig/-/tsconfig-0.9.1.tgz",
+      "integrity": "sha512-buvjm+9NzLCJL29KY1j1991YYJ5e6275OiK+G4jtmfIb+z4POywbdm0wXusT9adVWqe0xqg70TbI7+mRx4uU9w==",
+      "dev": true,
+      "requires": {}
+    },
+    "@vueuse/core": {
+      "version": "14.4.0",
+      "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.4.0.tgz",
+      "integrity": "sha512-X4WHz1HlCzCBoYXesUkifzzWBAcZgXG8Fi5iNPQg/epdzOB3gu8Fawj3hvuwYR1nGcXGnvxwYYcUC/71++svtQ==",
+      "requires": {
+        "@types/web-bluetooth": "^0.0.21",
+        "@vueuse/metadata": "14.4.0",
+        "@vueuse/shared": "14.4.0"
+      }
+    },
+    "@vueuse/metadata": {
+      "version": "14.4.0",
+      "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.4.0.tgz",
+      "integrity": "sha512-swx/255R6JyHZFJhx845iz5CRWDZdCfvkZOpACWc5+c5WHcG24mv8gUT1WIdFQaHt6dq79rvILd9QnCWiyVm9g=="
+    },
+    "@vueuse/shared": {
+      "version": "14.4.0",
+      "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.4.0.tgz",
+      "integrity": "sha512-JRgY90Sz8DDtPMsaDflvPMp9xYk69JZAmbuDvAquUVXKr2gEjqtzGNTTthLfckH0BzBqvnu31gb4a8TGLRe79g==",
+      "requires": {}
+    },
+    "acorn": {
+      "version": "8.18.0",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz",
+      "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="
+    },
+    "agent-base": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
+      "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+      "requires": {
+        "debug": "4"
+      }
+    },
+    "alien-signals": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.2.1.tgz",
+      "integrity": "sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==",
+      "dev": true
+    },
+    "ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "dev": true,
+      "optional": true,
+      "peer": true
+    },
+    "ansi-styles": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "requires": {
+        "color-convert": "^2.0.1"
+      }
+    },
+    "argparse": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+      "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+      "dev": true
+    },
+    "assertion-error": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
+      "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
+      "dev": true
+    },
+    "ast-kit": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/ast-kit/-/ast-kit-2.2.0.tgz",
+      "integrity": "sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw==",
+      "requires": {
+        "@babel/parser": "^7.28.5",
+        "pathe": "^2.0.3"
+      }
+    },
+    "ast-walker-scope": {
+      "version": "0.9.0",
+      "resolved": "https://registry.npmjs.org/ast-walker-scope/-/ast-walker-scope-0.9.0.tgz",
+      "integrity": "sha512-IJdzo2vLiElBxKzwS36VsCue/62d6IdWjnPB2v3nuPKeWGynp6FF/CYoLa5i/3jXH/z97ZDdsXz6abpgM6w07A==",
+      "requires": {
+        "@babel/parser": "^7.29.2",
+        "@babel/types": "^7.29.0",
+        "ast-kit": "^2.2.0"
+      }
+    },
+    "async-validator": {
+      "version": "4.2.5",
+      "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz",
+      "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg=="
+    },
+    "asynckit": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
+    },
+    "axios": {
+      "version": "1.20.0",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-1.20.0.tgz",
+      "integrity": "sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==",
+      "requires": {
+        "follow-redirects": "^1.16.0",
+        "form-data": "^4.0.6",
+        "https-proxy-agent": "^5.0.1",
+        "proxy-from-env": "^2.1.0"
+      }
+    },
+    "birpc": {
+      "version": "2.9.0",
+      "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz",
+      "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw=="
+    },
+    "call-bind-apply-helpers": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+      "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+      "requires": {
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2"
+      }
+    },
+    "camelcase": {
+      "version": "6.3.0",
+      "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
+      "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
+      "dev": true
+    },
+    "chai": {
+      "version": "6.2.2",
+      "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
+      "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==",
+      "dev": true
+    },
+    "chokidar": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz",
+      "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==",
+      "requires": {
+        "readdirp": "^5.0.0"
+      }
+    },
+    "cli-width": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz",
+      "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==",
+      "dev": true,
+      "optional": true,
+      "peer": true
+    },
+    "cliui": {
+      "version": "8.0.1",
+      "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
+      "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "requires": {
+        "string-width": "^4.2.0",
+        "strip-ansi": "^6.0.1",
+        "wrap-ansi": "^7.0.0"
+      }
+    },
+    "color-convert": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "requires": {
+        "color-name": "~1.1.4"
+      }
+    },
+    "color-name": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+      "dev": true,
+      "optional": true,
+      "peer": true
+    },
+    "combined-stream": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+      "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+      "requires": {
+        "delayed-stream": "~1.0.0"
+      }
+    },
+    "commander": {
+      "version": "14.0.3",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
+      "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
+      "dev": true
+    },
+    "confbox": {
+      "version": "0.2.4",
+      "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz",
+      "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ=="
+    },
+    "cookie": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
+      "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
+      "dev": true,
+      "optional": true,
+      "peer": true
+    },
+    "crypto-js": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
+      "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="
+    },
+    "csstype": {
+      "version": "3.2.3",
+      "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+      "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="
+    },
+    "dayjs": {
+      "version": "1.11.23",
+      "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.23.tgz",
+      "integrity": "sha512-QDTCU0M0MxR3hQfnlDJfwekQiaanm1ubOD231u73WBckQ/fsamwRLiE2GBz6D3a/xF1NgfiDLJjXBa1hYOYTtQ=="
+    },
+    "debug": {
+      "version": "4.4.3",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+      "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+      "requires": {
+        "ms": "^2.1.3"
+      }
+    },
+    "delayed-stream": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+      "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
+    },
+    "detect-libc": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+      "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+      "devOptional": true
+    },
+    "dunder-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+      "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+      "requires": {
+        "call-bind-apply-helpers": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.2.0"
+      }
+    },
+    "element-plus": {
+      "version": "2.14.5",
+      "resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.14.5.tgz",
+      "integrity": "sha512-bghYy/S+qg87enHPXELirhEdDqsVAUGcGpbGIeG8dz0kwpIkGz7gYsifulBshXX74iRtHib85XWQj0uSH2A1Yg==",
+      "requires": {
+        "@ctrl/tinycolor": "^4.2.0",
+        "@element-plus/icons-vue": "^2.3.2",
+        "@floating-ui/dom": "^1.8.0",
+        "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.8",
+        "@types/lodash": "^4.17.24",
+        "@types/lodash-es": "^4.17.12",
+        "@vueuse/core": "14.4.0",
+        "async-validator": "^4.2.5",
+        "dayjs": "^1.11.20",
+        "lodash": "^4.18.1",
+        "lodash-es": "^4.18.1",
+        "lodash-unified": "^1.0.3",
+        "memoize-one": "^6.0.0",
+        "normalize-wheel-es": "^1.2.0",
+        "vue-component-type-helpers": "^3.3.9"
+      }
+    },
+    "emoji-regex": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+      "dev": true,
+      "optional": true,
+      "peer": true
+    },
+    "entities": {
+      "version": "7.0.1",
+      "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
+      "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="
+    },
+    "es-define-property": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+      "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="
+    },
+    "es-errors": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+      "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="
+    },
+    "es-module-lexer": {
+      "version": "2.3.2",
+      "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz",
+      "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==",
+      "dev": true
+    },
+    "es-object-atoms": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
+      "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
+      "requires": {
+        "es-errors": "^1.3.0"
+      }
+    },
+    "es-set-tostringtag": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+      "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+      "requires": {
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.6",
+        "has-tostringtag": "^1.0.2",
+        "hasown": "^2.0.2"
+      }
+    },
+    "escalade": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+      "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+      "dev": true,
+      "optional": true,
+      "peer": true
+    },
+    "estree-walker": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
+      "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
+    },
+    "expect-type": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz",
+      "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==",
+      "dev": true
+    },
+    "exsolve": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.1.1.tgz",
+      "integrity": "sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g=="
+    },
+    "fast-string-truncated-width": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz",
+      "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==",
+      "dev": true,
+      "optional": true,
+      "peer": true
+    },
+    "fast-string-width": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz",
+      "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "requires": {
+        "fast-string-truncated-width": "^3.0.2"
+      }
+    },
+    "fast-wrap-ansi": {
+      "version": "0.2.2",
+      "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz",
+      "integrity": "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "requires": {
+        "fast-string-width": "^3.0.2"
+      }
+    },
+    "fdir": {
+      "version": "6.5.0",
+      "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+      "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+      "requires": {}
+    },
+    "follow-redirects": {
+      "version": "1.16.0",
+      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
+      "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw=="
+    },
+    "form-data": {
+      "version": "4.0.6",
+      "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz",
+      "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==",
+      "requires": {
+        "asynckit": "^0.4.0",
+        "combined-stream": "^1.0.8",
+        "es-set-tostringtag": "^2.1.0",
+        "hasown": "^2.0.4",
+        "mime-types": "^2.1.35"
+      }
+    },
+    "fs-extra": {
+      "version": "11.4.0",
+      "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.4.0.tgz",
+      "integrity": "sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA==",
+      "dev": true,
+      "requires": {
+        "graceful-fs": "^4.2.0",
+        "jsonfile": "^6.0.1",
+        "universalify": "^2.0.0"
+      }
+    },
+    "fsevents": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+      "dev": true,
+      "optional": true
+    },
+    "function-bind": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="
+    },
+    "get-caller-file": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+      "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+      "dev": true,
+      "optional": true,
+      "peer": true
+    },
+    "get-intrinsic": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+      "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+      "requires": {
+        "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": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+      "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+      "requires": {
+        "dunder-proto": "^1.0.1",
+        "es-object-atoms": "^1.0.0"
+      }
+    },
+    "gopd": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+      "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="
+    },
+    "graceful-fs": {
+      "version": "4.2.11",
+      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+      "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+      "dev": true
+    },
+    "graphql": {
+      "version": "16.14.2",
+      "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.14.2.tgz",
+      "integrity": "sha512-Chq1s4CY7jmh8gO2qvLIJyfCDIN+EHLFW/9iShnp1z8FjBQMoodWP1kDC36VAMXXIvAjj4ARa7ntfAV2BrjsbA==",
+      "dev": true,
+      "optional": true,
+      "peer": true
+    },
+    "handlebars": {
+      "version": "4.7.9",
+      "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz",
+      "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==",
+      "dev": true,
+      "requires": {
+        "minimist": "^1.2.5",
+        "neo-async": "^2.6.2",
+        "source-map": "^0.6.1",
+        "uglify-js": "^3.1.4",
+        "wordwrap": "^1.0.0"
+      }
+    },
+    "has-symbols": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+      "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="
+    },
+    "has-tostringtag": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+      "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+      "requires": {
+        "has-symbols": "^1.0.3"
+      }
+    },
+    "hasown": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
+      "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
+      "requires": {
+        "function-bind": "^1.1.2"
+      }
+    },
+    "headers-polyfill": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-5.0.1.tgz",
+      "integrity": "sha512-1TJ6Fih/b8h5TIcv+1+Hw0PDQWJTKDKzFZzcKOiW1wJza3XoAQlkCuXLbymPYB8+ZQyw8mHvdw560e8zVFIWyA==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "requires": {
+        "@types/set-cookie-parser": "^2.4.10",
+        "set-cookie-parser": "^3.0.1"
+      }
+    },
+    "hookable": {
+      "version": "5.5.3",
+      "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
+      "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="
+    },
+    "https-proxy-agent": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
+      "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
+      "requires": {
+        "agent-base": "6",
+        "debug": "4"
+      }
+    },
+    "is-fullwidth-code-point": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+      "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+      "dev": true,
+      "optional": true,
+      "peer": true
+    },
+    "is-node-process": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz",
+      "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==",
+      "dev": true,
+      "optional": true,
+      "peer": true
+    },
+    "js-yaml": {
+      "version": "4.3.2",
+      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz",
+      "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==",
+      "dev": true,
+      "requires": {
+        "argparse": "^2.0.1"
+      }
+    },
+    "jsonfile": {
+      "version": "6.2.1",
+      "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz",
+      "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==",
+      "dev": true,
+      "requires": {
+        "graceful-fs": "^4.1.6",
+        "universalify": "^2.0.0"
+      }
+    },
+    "jwt-decode": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz",
+      "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA=="
+    },
+    "lightningcss": {
+      "version": "1.33.0",
+      "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz",
+      "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==",
+      "devOptional": true,
+      "requires": {
+        "detect-libc": "^2.0.3",
+        "lightningcss-android-arm64": "1.33.0",
+        "lightningcss-darwin-arm64": "1.33.0",
+        "lightningcss-darwin-x64": "1.33.0",
+        "lightningcss-freebsd-x64": "1.33.0",
+        "lightningcss-linux-arm-gnueabihf": "1.33.0",
+        "lightningcss-linux-arm64-gnu": "1.33.0",
+        "lightningcss-linux-arm64-musl": "1.33.0",
+        "lightningcss-linux-x64-gnu": "1.33.0",
+        "lightningcss-linux-x64-musl": "1.33.0",
+        "lightningcss-win32-arm64-msvc": "1.33.0",
+        "lightningcss-win32-x64-msvc": "1.33.0"
+      }
+    },
+    "lightningcss-android-arm64": {
+      "version": "1.33.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz",
+      "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==",
+      "dev": true,
+      "optional": true
+    },
+    "lightningcss-darwin-arm64": {
+      "version": "1.33.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz",
+      "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==",
+      "dev": true,
+      "optional": true
+    },
+    "lightningcss-darwin-x64": {
+      "version": "1.33.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz",
+      "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==",
+      "dev": true,
+      "optional": true
+    },
+    "lightningcss-freebsd-x64": {
+      "version": "1.33.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz",
+      "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==",
+      "dev": true,
+      "optional": true
+    },
+    "lightningcss-linux-arm-gnueabihf": {
+      "version": "1.33.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz",
+      "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==",
+      "dev": true,
+      "optional": true
+    },
+    "lightningcss-linux-arm64-gnu": {
+      "version": "1.33.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz",
+      "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==",
+      "dev": true,
+      "optional": true
+    },
+    "lightningcss-linux-arm64-musl": {
+      "version": "1.33.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz",
+      "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==",
+      "dev": true,
+      "optional": true
+    },
+    "lightningcss-linux-x64-gnu": {
+      "version": "1.33.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz",
+      "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==",
+      "dev": true,
+      "optional": true
+    },
+    "lightningcss-linux-x64-musl": {
+      "version": "1.33.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz",
+      "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==",
+      "dev": true,
+      "optional": true
+    },
+    "lightningcss-win32-arm64-msvc": {
+      "version": "1.33.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz",
+      "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==",
+      "dev": true,
+      "optional": true
+    },
+    "lightningcss-win32-x64-msvc": {
+      "version": "1.33.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz",
+      "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==",
+      "dev": true,
+      "optional": true
+    },
+    "local-pkg": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.2.1.tgz",
+      "integrity": "sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q==",
+      "requires": {
+        "mlly": "^1.7.4",
+        "pkg-types": "^2.3.0",
+        "quansync": "^0.2.11"
+      }
+    },
+    "lodash": {
+      "version": "4.18.1",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
+      "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="
+    },
+    "lodash-es": {
+      "version": "4.18.1",
+      "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz",
+      "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A=="
+    },
+    "lodash-unified": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/lodash-unified/-/lodash-unified-1.0.3.tgz",
+      "integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==",
+      "requires": {}
+    },
+    "magic-string": {
+      "version": "0.30.21",
+      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+      "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+      "requires": {
+        "@jridgewell/sourcemap-codec": "^1.5.5"
+      }
+    },
+    "magic-string-ast": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/magic-string-ast/-/magic-string-ast-1.0.3.tgz",
+      "integrity": "sha512-CvkkH1i81zl7mmb94DsRiFeG9V2fR2JeuK8yDgS8oiZSFa++wWLEgZ5ufEOyLHbvSbD1gTRKv9NdX69Rnvr9JA==",
+      "requires": {
+        "magic-string": "^0.30.19"
+      }
+    },
+    "math-intrinsics": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+      "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="
+    },
+    "memoize-one": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
+      "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw=="
+    },
+    "mime-db": {
+      "version": "1.52.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="
+    },
+    "mime-types": {
+      "version": "2.1.35",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+      "requires": {
+        "mime-db": "1.52.0"
+      }
+    },
+    "minimist": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+      "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+      "dev": true
+    },
+    "mlly": {
+      "version": "1.8.2",
+      "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz",
+      "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==",
+      "requires": {
+        "acorn": "^8.16.0",
+        "pathe": "^2.0.3",
+        "pkg-types": "^1.3.1",
+        "ufo": "^1.6.3"
+      },
+      "dependencies": {
+        "confbox": {
+          "version": "0.1.8",
+          "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz",
+          "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="
+        },
+        "pkg-types": {
+          "version": "1.3.1",
+          "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz",
+          "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==",
+          "requires": {
+            "confbox": "^0.1.8",
+            "mlly": "^1.7.4",
+            "pathe": "^2.0.1"
+          }
+        }
+      }
+    },
+    "ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+    },
+    "msw": {
+      "version": "2.15.0",
+      "resolved": "https://registry.npmjs.org/msw/-/msw-2.15.0.tgz",
+      "integrity": "sha512-2wQAmKkQKxRuXvYJxVhPGG0wZNBQyD06oJvxqw90XqLvptdqxdlHrFUfEteKkpaNORX3Xzc+HtEl/q0nfmN2wQ==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "requires": {
+        "@inquirer/confirm": "^6.0.11",
+        "@mswjs/interceptors": "^0.41.3",
+        "@open-draft/deferred-promise": "^3.0.0",
+        "@types/statuses": "^2.0.6",
+        "cookie": "^1.1.1",
+        "graphql": "^16.13.2",
+        "headers-polyfill": "^5.0.1",
+        "is-node-process": "^1.2.0",
+        "outvariant": "^1.4.3",
+        "path-to-regexp": "^6.3.0",
+        "picocolors": "^1.1.1",
+        "rettime": "^0.11.11",
+        "statuses": "^2.0.2",
+        "strict-event-emitter": "^0.5.1",
+        "tough-cookie": "^6.0.1",
+        "type-fest": "^5.5.0",
+        "until-async": "^3.0.2",
+        "yargs": "^17.7.2"
+      }
+    },
+    "muggle-string": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz",
+      "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ=="
+    },
+    "mute-stream": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-3.0.0.tgz",
+      "integrity": "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==",
+      "dev": true,
+      "optional": true,
+      "peer": true
+    },
+    "nanoid": {
+      "version": "3.3.19",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz",
+      "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug=="
+    },
+    "neo-async": {
+      "version": "2.6.2",
+      "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
+      "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
+      "dev": true
+    },
+    "normalize-wheel-es": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz",
+      "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw=="
+    },
+    "nostics": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/nostics/-/nostics-1.2.0.tgz",
+      "integrity": "sha512-FGqEfhQjrvo1lL8KFifdTQiNwwQHJxC1jtYE1Rc54qF/jxONUNL+kC9gS1krX8Q65PgrQ5fCqH/I4NhWBvdSqg=="
+    },
+    "obug": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz",
+      "integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==",
+      "dev": true
+    },
+    "openapi-typescript-codegen": {
+      "version": "0.31.0",
+      "resolved": "https://registry.npmjs.org/openapi-typescript-codegen/-/openapi-typescript-codegen-0.31.0.tgz",
+      "integrity": "sha512-ph3njJ7veDXy6QKLKHqNiyIqWMMu+rcdtU87XuwBo+QfBOpE1PvqzIWY3KMZBnLaoACxhtrEPf5re7Y/Ty9Bnw==",
+      "dev": true,
+      "requires": {
+        "@apidevtools/json-schema-ref-parser": "^14.2.1",
+        "camelcase": "^6.3.0",
+        "commander": "^14.0.3",
+        "fs-extra": "^11.3.5",
+        "handlebars": "^4.7.9"
+      }
+    },
+    "outvariant": {
+      "version": "1.4.3",
+      "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz",
+      "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==",
+      "dev": true,
+      "optional": true,
+      "peer": true
+    },
+    "path-browserify": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
+      "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
+      "dev": true
+    },
+    "path-to-regexp": {
+      "version": "6.3.0",
+      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz",
+      "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==",
+      "dev": true,
+      "optional": true,
+      "peer": true
+    },
+    "pathe": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
+      "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="
+    },
+    "perfect-debounce": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz",
+      "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g=="
+    },
+    "picocolors": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+      "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
+    },
+    "picomatch": {
+      "version": "4.0.7",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
+      "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="
+    },
+    "pinia": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/pinia/-/pinia-4.0.3.tgz",
+      "integrity": "sha512-XMQqpvjgG7LMqVhhFUzKLT4KEbsYbfZZ0CZU9PgdFm1O2VKBmIkykL8+SfNhOaT7sR0wT6BEgKT0YNDYWgmVRA==",
+      "requires": {
+        "nostics": "^1.1.4"
+      }
+    },
+    "pkg-types": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.3.tgz",
+      "integrity": "sha512-j/lCFdcppV0JxWpCEITdbDltBxPP6cHT+yNJ6Go2OgoSA9518X847X9z0p6LtA4Nc16+eQzCZjRrWanTGvHJ5w==",
+      "requires": {
+        "confbox": "^0.3.1",
+        "exsolve": "^1.1.1",
+        "pathe": "^2.0.3"
+      },
+      "dependencies": {
+        "confbox": {
+          "version": "0.3.1",
+          "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.3.1.tgz",
+          "integrity": "sha512-cKUSoKa8YxFZZSmraVi7onONx3amu77ngK3kGpsYHDH7drPwCRkQE1RYMPlLRrMtnciRj274XNRxcHxnKmDSnA=="
+        }
+      }
+    },
+    "postcss": {
+      "version": "8.5.28",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz",
+      "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==",
+      "requires": {
+        "nanoid": "^3.3.18",
+        "picocolors": "^1.1.1",
+        "source-map-js": "^1.2.1"
+      }
+    },
+    "proxy-from-env": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
+      "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA=="
+    },
+    "quansync": {
+      "version": "0.2.11",
+      "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz",
+      "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="
+    },
+    "readdirp": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz",
+      "integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA=="
+    },
+    "require-directory": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+      "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+      "dev": true,
+      "optional": true,
+      "peer": true
+    },
+    "rettime": {
+      "version": "0.11.11",
+      "resolved": "https://registry.npmjs.org/rettime/-/rettime-0.11.11.tgz",
+      "integrity": "sha512-ILJRqVWBCTlg9r42fFgwVZx1gnFAcQF8mRoMkbgQfIrjEDf9nbBFDFx00oloOa+Q869FUtaYDXZvEfnecQSCoQ==",
+      "dev": true,
+      "optional": true,
+      "peer": true
+    },
+    "rolldown": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.8.tgz",
+      "integrity": "sha512-Z67nTmhZe7anqnM/EjI392w5i/ANUinjip7QYsOyN37oayduxt3ksdX0hf5OOamkAd53BiIHfbfSzfUmzKFQqQ==",
+      "devOptional": true,
+      "requires": {
+        "@oxc-project/types": "=0.149.0",
+        "@rolldown/binding-android-arm-eabi": "1.2.8",
+        "@rolldown/binding-android-arm64": "1.2.8",
+        "@rolldown/binding-darwin-arm64": "1.2.8",
+        "@rolldown/binding-darwin-x64": "1.2.8",
+        "@rolldown/binding-freebsd-x64": "1.2.8",
+        "@rolldown/binding-linux-arm-gnueabihf": "1.2.8",
+        "@rolldown/binding-linux-arm64-gnu": "1.2.8",
+        "@rolldown/binding-linux-arm64-musl": "1.2.8",
+        "@rolldown/binding-linux-ppc64-gnu": "1.2.8",
+        "@rolldown/binding-linux-s390x-gnu": "1.2.8",
+        "@rolldown/binding-linux-x64-gnu": "1.2.8",
+        "@rolldown/binding-linux-x64-musl": "1.2.8",
+        "@rolldown/binding-openharmony-arm64": "1.2.8",
+        "@rolldown/binding-win32-arm64-msvc": "1.2.8",
+        "@rolldown/binding-win32-x64-msvc": "1.2.8",
+        "@rolldown/pluginutils": "^1.0.0"
+      }
+    },
+    "scule": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/scule/-/scule-1.3.0.tgz",
+      "integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g=="
+    },
+    "set-cookie-parser": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.2.tgz",
+      "integrity": "sha512-5/r/lTwbJ3zQ+qwdUFZYeRNqda7P5HD8zQKqlSjdGt1/S0cjLAphHusj4Y58ahDtWn/g32xrIS58/ikOvwl0Lw==",
+      "dev": true,
+      "optional": true,
+      "peer": true
+    },
+    "siginfo": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
+      "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
+      "dev": true
+    },
+    "signal-exit": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+      "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+      "dev": true,
+      "optional": true,
+      "peer": true
+    },
+    "source-map": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+      "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+      "dev": true
+    },
+    "source-map-js": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+      "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="
+    },
+    "stackback": {
+      "version": "0.0.2",
+      "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
+      "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
+      "dev": true
+    },
+    "statuses": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+      "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+      "dev": true,
+      "optional": true,
+      "peer": true
+    },
+    "std-env": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz",
+      "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==",
+      "dev": true
+    },
+    "strict-event-emitter": {
+      "version": "0.5.1",
+      "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz",
+      "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==",
+      "dev": true,
+      "optional": true,
+      "peer": true
+    },
+    "string-width": {
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "requires": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      }
+    },
+    "strip-ansi": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "requires": {
+        "ansi-regex": "^5.0.1"
+      }
+    },
+    "tagged-tag": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz",
+      "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==",
+      "dev": true,
+      "optional": true,
+      "peer": true
+    },
+    "tinybench": {
+      "version": "6.1.4",
+      "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.1.4.tgz",
+      "integrity": "sha512-9APumHG7r4yOk4X4WlkmE71aZcv1gvin1czO3OQ1U9iJcFA5Ja/ygyb0vPOVHTthFozUYs8CLoLUlM8grb2lTQ==",
+      "dev": true
+    },
+    "tinyexec": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz",
+      "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==",
+      "dev": true
+    },
+    "tinyglobby": {
+      "version": "0.2.17",
+      "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
+      "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
+      "requires": {
+        "fdir": "^6.5.0",
+        "picomatch": "^4.0.4"
+      }
+    },
+    "tldts": {
+      "version": "7.4.12",
+      "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.12.tgz",
+      "integrity": "sha512-WylhSDKVeYnWXL3a+vKTaOxjnOeEGw938hImY8zoRWJjRRK/Jp1K+IihBzIONpUmW4e3WmXT6q5FW6vlESVZCA==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "requires": {
+        "tldts-core": "^7.4.12"
+      }
+    },
+    "tldts-core": {
+      "version": "7.4.12",
+      "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.12.tgz",
+      "integrity": "sha512-nYNzS2WRf4QJmjzFFgAxLOBjyBxAGRbCy9PVBPaglcYyYajh40VBn+v5Ngr96ZMc7oM0+aCJdtQnNejvdBnXMQ==",
+      "dev": true,
+      "optional": true,
+      "peer": true
+    },
+    "tough-cookie": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz",
+      "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "requires": {
+        "tldts": "^7.0.5"
+      }
+    },
+    "type-fest": {
+      "version": "5.9.0",
+      "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.9.0.tgz",
+      "integrity": "sha512-yANm3Jr3GiJ1qgJlxGAVxTOIcEOk1rhQHamlXtnrCK7EHP4HeM9OGxtMg/W7HFdrVzw/ZWJKGVIJusVH85sLtw==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "requires": {
+        "tagged-tag": "^1.0.0"
+      }
+    },
+    "typescript": {
+      "version": "6.0.3",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
+      "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
+      "devOptional": true
+    },
+    "ufo": {
+      "version": "1.6.4",
+      "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz",
+      "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA=="
+    },
+    "uglify-js": {
+      "version": "3.19.3",
+      "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz",
+      "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==",
+      "dev": true,
+      "optional": true
+    },
+    "undici-types": {
+      "version": "7.18.2",
+      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
+      "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
+      "dev": true
+    },
+    "universalify": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+      "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+      "dev": true
+    },
+    "unplugin": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-3.3.0.tgz",
+      "integrity": "sha512-qa66K+crbfyE6JK10GjvbJeRrOsuC/JpbnHctfyp/i4oBTxWOzJfRZyDiOk1PtErMFRu8JhsU/wPvOdBNWe5Rg==",
+      "requires": {
+        "@jridgewell/remapping": "^2.3.5",
+        "picomatch": "^4.0.4",
+        "webpack-virtual-modules": "^0.6.2"
+      }
+    },
+    "unplugin-utils": {
+      "version": "0.3.2",
+      "resolved": "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.3.2.tgz",
+      "integrity": "sha512-xVToRh2CTmLk2HnEG7ac4rl1MJTT3RFkpS8B++/SnB0kXvuaavD+n3m/vrzyWQOdJNSZQACnbz01pnppbwV5BA==",
+      "requires": {
+        "pathe": "^2.0.3",
+        "picomatch": "^4.0.4"
+      }
+    },
+    "until-async": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/until-async/-/until-async-3.0.2.tgz",
+      "integrity": "sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==",
+      "dev": true,
+      "optional": true,
+      "peer": true
+    },
+    "vite": {
+      "version": "8.3.0",
+      "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.0.tgz",
+      "integrity": "sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ==",
+      "devOptional": true,
+      "requires": {
+        "fsevents": "~2.3.3",
+        "lightningcss": "^1.33.0",
+        "picomatch": "^4.0.7",
+        "postcss": "^8.5.28",
+        "rolldown": "~1.2.6",
+        "tinyglobby": "^0.2.17"
+      }
+    },
+    "vitest": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.0.tgz",
+      "integrity": "sha512-gpsMNoRhMjMktVxPtstOH4/PJuPyovVaMDr4oDilXaGH1EcqM2OE96SoHT2VIQ6fTGtTjqmHDrEu2X9RQiXf8Q==",
+      "dev": true,
+      "requires": {
+        "@types/chai": "^5.2.2",
+        "@vitest/mocker": "5.0.0",
+        "chai": "^6.2.2",
+        "es-module-lexer": "^2.3.2",
+        "expect-type": "^1.4.0",
+        "magic-string": "^1.2.3",
+        "obug": "^2.1.4",
+        "picomatch": "^4.0.7",
+        "std-env": "^4.2.0",
+        "tinybench": "6.1.4",
+        "tinyexec": "1.3.0",
+        "tinyglobby": "^0.2.17",
+        "why-is-node-running": "^2.3.0"
+      },
+      "dependencies": {
+        "magic-string": {
+          "version": "1.3.1",
+          "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.3.1.tgz",
+          "integrity": "sha512-rm91zr2Ou+XueDTohjQQjdQEcYM6zVi8KVUCG8Ec3vHwUEKrhSdCNyfuIywkA6hcCAteIn0ZOtAHA6eGpiX+Pg==",
+          "dev": true,
+          "requires": {
+            "@jridgewell/sourcemap-codec": "^1.6.0"
+          }
+        }
+      }
+    },
+    "vscode-uri": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.2.0.tgz",
+      "integrity": "sha512-m2gXo3bn0G1kT9InzMf07fTbqMbGtyckj3bH5ktLO+1Ssv+yiATZ4dhwaQv9UZWxJh6E9IFGnQyjgWVDWVBDrg==",
+      "dev": true
+    },
+    "vue": {
+      "version": "3.5.42",
+      "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.42.tgz",
+      "integrity": "sha512-4RyHQTbQvOPs3MfvUO1Sg0YRrKNnA0mAVtvpd12Tg1fKDN7OHBUl1IqSn8zGJjK9nI3NkNp8cgTpVrSZC5TTcA==",
+      "requires": {
+        "@vue/compiler-dom": "3.5.42",
+        "@vue/compiler-sfc": "3.5.42",
+        "@vue/runtime-dom": "3.5.42",
+        "@vue/server-renderer": "3.5.42",
+        "@vue/shared": "3.5.42"
+      }
+    },
+    "vue-component-type-helpers": {
+      "version": "3.3.11",
+      "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-3.3.11.tgz",
+      "integrity": "sha512-LwcxzeliO9fkQcpJG0PoX8X5kmAhKmH9wkpDLxNabwzkQ9Zeib2YVHwFV4pcWmMLfXVfjr/dSV+DaJ3cIPgSNA=="
+    },
+    "vue-router": {
+      "version": "5.3.1",
+      "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.3.1.tgz",
+      "integrity": "sha512-GDBZzgmILxA/kFnkFbJjQZdZ2QQbngnIMMuoUcjhZIfH1RGMaPjPwX5ASnV38qamuA9uhO0RDjSBHTDNG2uXyQ==",
+      "requires": {
+        "@vue-macros/common": "^3.1.3",
+        "@vue/devtools-api": "^8.1.5",
+        "ast-walker-scope": "^0.9.0",
+        "chokidar": "^5.0.0",
+        "confbox": "^0.2.4",
+        "local-pkg": "^1.2.1",
+        "magic-string": "^0.30.21",
+        "mlly": "^1.8.2",
+        "muggle-string": "^0.4.1",
+        "nostics": "^1.1.4",
+        "pathe": "^2.0.3",
+        "picomatch": "^4.0.5",
+        "scule": "^1.3.0",
+        "tinyglobby": "^0.2.17",
+        "unplugin": "^3.3.0",
+        "unplugin-utils": "^0.3.2"
+      }
+    },
+    "vue-tsc": {
+      "version": "3.3.11",
+      "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.3.11.tgz",
+      "integrity": "sha512-gOb0B9rtU2+f1dszwPqSH5kAieIF9ReeLhD3kSRNHv5WZZUQz/JdVXW0RTdqhNTMlQkqKzrTTviqKr/4FYZraQ==",
+      "dev": true,
+      "requires": {
+        "@volar/typescript": "2.4.28",
+        "@vue/language-core": "3.3.11"
+      }
+    },
+    "webpack-virtual-modules": {
+      "version": "0.6.2",
+      "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz",
+      "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="
+    },
+    "why-is-node-running": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
+      "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
+      "dev": true,
+      "requires": {
+        "siginfo": "^2.0.0",
+        "stackback": "0.0.2"
+      }
+    },
+    "wordwrap": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
+      "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==",
+      "dev": true
+    },
+    "wrap-ansi": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+      "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "requires": {
+        "ansi-styles": "^4.0.0",
+        "string-width": "^4.1.0",
+        "strip-ansi": "^6.0.0"
+      }
+    },
+    "y18n": {
+      "version": "5.0.8",
+      "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+      "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+      "dev": true,
+      "optional": true,
+      "peer": true
+    },
+    "yargs": {
+      "version": "17.7.3",
+      "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz",
+      "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "requires": {
+        "cliui": "^8.0.1",
+        "escalade": "^3.1.1",
+        "get-caller-file": "^2.0.5",
+        "require-directory": "^2.1.1",
+        "string-width": "^4.2.3",
+        "y18n": "^5.0.5",
+        "yargs-parser": "^21.1.1"
+      }
+    },
+    "yargs-parser": {
+      "version": "21.1.1",
+      "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
+      "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+      "dev": true,
+      "optional": true,
+      "peer": true
+    }
+  }
+}

+ 33 - 0
package.json

@@ -0,0 +1,33 @@
+{
+  "name": "warehouse-web",
+  "private": true,
+  "version": "0.0.0",
+  "type": "module",
+  "scripts": {
+    "dev": "vite",
+    "build": "vue-tsc -b && vite build",
+    "preview": "vite preview",
+    "test": "vitest run"
+  },
+  "dependencies": {
+    "@element-plus/icons-vue": "^2.3.2",
+    "axios": "^1.20.0",
+    "crypto-js": "^4.2.0",
+    "element-plus": "^2.14.5",
+    "jwt-decode": "^4.0.0",
+    "pinia": "^4.0.3",
+    "vue": "^3.5.42",
+    "vue-router": "^5.3.1"
+  },
+  "devDependencies": {
+    "@types/crypto-js": "^4.2.2",
+    "@types/node": "^24.13.3",
+    "@vitejs/plugin-vue": "^6.0.8",
+    "@vue/tsconfig": "^0.9.1",
+    "openapi-typescript-codegen": "^0.31.0",
+    "typescript": "~6.0.2",
+    "vite": "^8.3.0",
+    "vitest": "^5.0.0",
+    "vue-tsc": "^3.3.11"
+  }
+}

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
public/favicon.svg


+ 24 - 0
public/icons.svg

@@ -0,0 +1,24 @@
+<svg xmlns="http://www.w3.org/2000/svg">
+  <symbol id="bluesky-icon" viewBox="0 0 16 17">
+    <g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
+    <defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
+  </symbol>
+  <symbol id="discord-icon" viewBox="0 0 20 19">
+    <path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
+  </symbol>
+  <symbol id="documentation-icon" viewBox="0 0 21 20">
+    <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
+    <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
+    <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
+  </symbol>
+  <symbol id="github-icon" viewBox="0 0 19 19">
+    <path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
+  </symbol>
+  <symbol id="social-icon" viewBox="0 0 20 20">
+    <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
+    <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
+  </symbol>
+  <symbol id="x-icon" viewBox="0 0 19 19">
+    <path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
+  </symbol>
+</svg>

+ 57 - 0
src/App.vue

@@ -0,0 +1,57 @@
+<script setup lang="ts">
+	import { useRoute, useRouter } from 'vue-router'
+	import { useWarehouse } from './store'
+	import { clearAuthInfo, clearOAuthTemp, getDisplayName } from './utils/auth'
+	import { House, Collection, Box, Download, Upload, Notebook, SwitchButton } from '@element-plus/icons-vue'
+	const route = useRoute(); const router = useRouter()
+	const warehouse = useWarehouse()
+	const links = [{ path: '/dashboard', name: '工作台', icon: House }, { path: '/master', name: '基础资料管理', icon: Collection }, { path: '/sku', name: '商品 / SKU 管理', icon: Box }, { path: '/inbound', name: '入库管理', icon: Download }, { path: '/outbound', name: '出库管理', icon: Upload }, { path: '/ledger', name: '库存台账', icon: Notebook }]
+	function logout() { clearAuthInfo(); clearOAuthTemp(); sessionStorage.setItem('warehouse-auth-paused', '1'); router.push('/login') }
+	const user = getDisplayName
+	const currentDate = new Intl.DateTimeFormat('zh-CN', { year: 'numeric', month: 'long', day: 'numeric', weekday: 'long' }).format(new Date())
+</script>
+
+<template>
+	<router-view v-if="route.path === '/login'" />
+	<div v-else class="shell">
+		<aside class="sidebar">
+			<div class="brand">
+				<span class="brand-mark">H</span>
+				<div>
+					<strong>HS 仓储运营台</strong><small>WAREHOUSE OPS · v1.0</small>
+				</div>
+			</div>
+			<div class="warehouse">
+				<i class="pin" /><span>{{ warehouse.warehouseName }} · 唯一作业仓</span>
+			</div>
+			<div class="nav-label">OPERATIONS</div>
+			<nav aria-label="主导航">
+				<router-link v-for="link in links" :key="link.path" :to="link.path" :title="link.name"
+					:aria-label="link.name"><el-icon>
+						<component :is="link.icon" />
+					</el-icon><span>{{ link.name }}</span></router-link>
+			</nav>
+			<div class="sidebar-bottom">
+				<strong>仅限内部操作</strong><br>所有已完成流水均可追溯
+			</div>
+		</aside>
+		<div class="main">
+			<header class="topbar">
+				<div class="crumb">
+					<span>HS 内部系统</span>
+					<span class="slash">/</span>
+					<strong>{{ route.meta.title }}</strong>
+				</div>
+				<div class="user-actions">
+					<span class="date">{{ currentDate }}</span>
+					<div class="user">
+						<div class="avatar">{{ user().slice(0, 1).toUpperCase() }}</div>
+						<span>{{ user() }}</span>
+					</div>
+					<el-button :icon="SwitchButton" text title="退出登录" aria-label="退出登录" @click="logout" />
+				</div>
+			</header>
+			<main class="page"><router-view :key="route.path" /></main>
+		</div>
+	</div>
+</template>

+ 114 - 0
src/api/index.ts

@@ -0,0 +1,114 @@
+import { authHttp, services } from '../http'
+import { getOAuthConfig } from '../utils/auth'
+import type { ExchangeTokenResponse } from './indexType'
+import type {
+	WMS_ledger_flow_detail_request, WmsBasicDataQueryRequest, WmsBasicDataSaveRequest,
+	WmsIdRequest, WmsInboundPageRequest, WmsInboundSaveRequest, WmsInboundSubmitRequest, WmsLedgerFlowQueryRequest,
+	WmsOutboundPageRequest, WmsOutboundSaveRequest, WmsOutboundSubmitRequest, WmsReversalPreCheckRequest,
+	WmsReversalSubmitRequest, WmsSkuQueryPageRequest, WmsSkuSaveRequest, WmsSkuSearchRequest,
+	WmsSkuUpdateRequest, WmsStatusUpdateRequest, WmsDashboardResponse, WmsDashboardTodayFlowVO,
+	WmsBasicDataResponse, WmsInboundRecordResponse, WmsOutboundRecordResponse,
+	WmsLedgerFlowVO, WmsLedgerAdminSummaryResponse, WmsReversalRecordResponse, WmsReversalPreCheckResponse, WmsSkuResponse,
+} from './indexType'
+
+type WmsResult<Response, Row> = { code: number; msg?: string; data: Response; rows?: Row[]; total?: number }
+type WmsList<Row> = Row[] | { records?: Row[]; list?: Row[] }
+
+const wmsRequest = async <Response = Record<string, unknown>, Row = unknown>(path: string, data?: unknown, params?: Record<string, string>) => {
+	const { data: result } = await services.wms.post<WmsResult<Response, Row>>(`/api/wms/${path}`, data, { params })
+	if (result.code !== 0 && result.code !== 200) throw new Error(result.msg || '请求失败,请重试')
+	return result
+}
+
+const wmsPost = async <Response = Record<string, unknown>>(path: string, data?: unknown, params?: Record<string, string>) =>
+	(await wmsRequest<Response>(path, data, params)).data
+
+export const wmsApi = {
+	basicData: {
+		delete: (data: WmsIdRequest) => wmsPost('basicData/delete', data),
+		list: async (data: WmsBasicDataQueryRequest & { pageNo: number; pageSize: number }) => {
+			const result = await wmsRequest<unknown, WmsBasicDataResponse>('basicData/list', data)
+			if (!Array.isArray(result.rows) || typeof result.total !== 'number') throw new Error('基础资料列表格式不符合预期')
+			return { rows: result.rows, total: result.total }
+		},
+		listEnabled: (data: WmsBasicDataQueryRequest) => wmsPost<WmsList<WmsBasicDataResponse>>('basicData/listEnabled', data),
+		save: (data: WmsBasicDataSaveRequest) => wmsPost('basicData/save', data),
+		updateStatus: (data: WmsStatusUpdateRequest) => wmsPost('basicData/updateStatus', data),
+	},
+	dashboard: {
+		summary: () => wmsPost<WmsDashboardResponse>('dashboard/summary'),
+		todayTrace: async () => {
+			const result = await wmsPost<WmsDashboardTodayFlowVO[]>('dashboard/todayTrace')
+			if (!Array.isArray(result)) throw new Error('今日操作轨迹格式不符合预期')
+			return result
+		},
+	},
+	file: {
+		oss: async (filename: string) => {
+			const result = await wmsRequest<unknown>('file/oss', undefined, { filename })
+			return result.data ?? result.msg
+		},
+	},
+	inbound: {
+		deleteDraft: (data: WmsIdRequest) => wmsPost('inbound/deleteDraft', data),
+		detail: (data: WmsIdRequest) => wmsPost<WmsInboundRecordResponse>('inbound/detail', data),
+		page: async (data: WmsInboundPageRequest) => {
+			const result = await wmsRequest<unknown, WmsInboundRecordResponse>('inbound/page', data)
+			if (!Array.isArray(result.rows) || typeof result.total !== 'number') throw new Error('入库列表格式不符合预期')
+			return { rows: result.rows, total: result.total }
+		},
+		saveDraft: (data: WmsInboundSaveRequest) => wmsPost('inbound/saveDraft', data),
+		submit: (data: WmsInboundSubmitRequest) => wmsPost('inbound/submit', data),
+		updateDraft: (data: WmsInboundSaveRequest) => wmsPost('inbound/updateDraft', data),
+	},
+	ledger: {
+		adminSummary: () => wmsPost<WmsLedgerAdminSummaryResponse>('ledger/adminSummary'),
+		flowDetail: (data: WMS_ledger_flow_detail_request) => wmsPost<WmsLedgerFlowVO>('ledger/flowDetail', data),
+		flowList: async (data: WmsLedgerFlowQueryRequest) => {
+			const result = await wmsRequest<unknown, WmsLedgerFlowVO>('ledger/flowList', data)
+			if (!Array.isArray(result.rows) || typeof result.total !== 'number') throw new Error('台账列表格式不符合预期')
+			return { rows: result.rows, total: result.total }
+		},
+	},
+	outbound: {
+		deleteDraft: (data: WmsIdRequest) => wmsPost('outbound/deleteDraft', data),
+		detail: (data: WmsIdRequest) => wmsPost<WmsOutboundRecordResponse>('outbound/detail', data),
+		page: async (data: WmsOutboundPageRequest) => {
+			const result = await wmsRequest<unknown, WmsOutboundRecordResponse>('outbound/page', data)
+			if (!Array.isArray(result.rows) || typeof result.total !== 'number') throw new Error('出库列表格式不符合预期')
+			return { rows: result.rows, total: result.total }
+		},
+		saveDraft: (data: WmsOutboundSaveRequest) => wmsPost('outbound/saveDraft', data),
+		submit: (data: WmsOutboundSubmitRequest) => wmsPost('outbound/submit', data),
+		updateDraft: (data: WmsOutboundSaveRequest) => wmsPost('outbound/updateDraft', data),
+	},
+	reversal: {
+		detail: (data: WmsIdRequest) => wmsPost<WmsReversalRecordResponse>('reversal/detail', data),
+		preCheck: (data: WmsReversalPreCheckRequest) => wmsPost<WmsReversalPreCheckResponse>('reversal/preCheck', data),
+		submit: (data: WmsReversalSubmitRequest) => wmsPost('reversal/submit', data),
+	},
+	sku: {
+		detail: (data: WmsIdRequest) => wmsPost<WmsSkuResponse>('sku/detail', data),
+		page: async (data: WmsSkuQueryPageRequest & { pageNo: number; pageSize: number }) => {
+			const result = await wmsRequest<unknown, WmsSkuResponse>('sku/page', data)
+			if (!Array.isArray(result.rows) || typeof result.total !== 'number') throw new Error('SKU 列表格式不符合预期')
+			return { rows: result.rows, total: result.total }
+		},
+		save: (data: WmsSkuSaveRequest) => wmsPost('sku/save', data),
+		search: (data: WmsSkuSearchRequest) => wmsPost<WmsList<WmsSkuResponse>>('sku/search', data),
+		update: (data: WmsSkuUpdateRequest) => wmsPost('sku/update', data),
+		updateStatus: (data: WmsStatusUpdateRequest) => wmsPost('sku/updateStatus', data),
+	},
+}
+
+export const api = {
+	wmsApi,
+	exchangeToken: async (code : string, verifier : string, redirectURI : string) => {
+		const config = getOAuthConfig()
+		const body = new URLSearchParams({
+			grant_type: 'authorization_code', client_id: config.clientId,
+			redirect_uri: redirectURI, code_verifier: verifier, code
+		})
+		return (await authHttp.post<ExchangeTokenResponse>(config.tokenURL, body)).data
+	},
+}

+ 9 - 0
src/api/indexType.ts

@@ -0,0 +1,9 @@
+export type * from './types'
+
+export type Direction = 'inbound' | 'outbound'
+export type MasterKind = 'category' | 'sport' | 'brand'
+export interface Master { id: string; kind: MasterKind; name: string; enabled: boolean; order: number; operator: string; card?: boolean; skuCount?: number; updatedAt?: string }
+export interface Sku { id: string; categoryId: string; sportId: string; brandId: string; name: string; spec: string; config: string; remark: string; enabled: boolean; createdAt: string; operator: string }
+export interface Attachment { id: string; name: string; url: string }
+export interface Entry { id: string; direction: Direction; status: 'draft' | 'completed'; source: 'normal' | 'reversal'; skuId: string; quantity: number | null; orderType: string; so: string; logistics: string; company: string; tracking: string; evidence: Attachment[]; orderEvidence: Attachment[]; screenshots: Attachment[]; remark: string; originalId: string; createdAt: string; completedAt: string; operator: string; snapshot?: Sku & { category: string; sport: string; brand: string } }
+export interface ExchangeTokenResponse { access_token: string; token_type: string; displayName?: string }

+ 18 - 0
src/api/types/TableDataInfo.ts

@@ -0,0 +1,18 @@
+/* generated using openapi-typescript-codegen -- do not edit */
+/* istanbul ignore file */
+/* tslint:disable */
+/* eslint-disable */
+export type TableDataInfo = {
+    code?: number;
+    msg?: string;
+    /**
+     * 列表数据
+     */
+    rows?: Array<any>;
+    /**
+     * 总记录数
+     */
+    total?: number;
+    totalrows?: number;
+};
+

+ 19 - 0
src/api/types/WMS_ledger_flow_detail_request.ts

@@ -0,0 +1,19 @@
+/* generated using openapi-typescript-codegen -- do not edit */
+/* istanbul ignore file */
+/* tslint:disable */
+/* eslint-disable */
+export type WMS_ledger_flow_detail_request = {
+    /**
+     * 流水编号
+     */
+    flowNo?: string;
+    /**
+     * Flow type: 1=inbound, 2=outbound, 3=reversal
+     */
+    flowType?: number;
+    /**
+     * Flow record ID
+     */
+    recordId?: number;
+};
+

+ 16 - 0
src/api/types/WmsBasicDataQueryRequest.ts

@@ -0,0 +1,16 @@
+import type { PageRequest } from './common';
+export type WmsBasicDataQueryRequest = PageRequest & {
+    /**
+     * 资料名称,支持模糊查询
+     */
+    dataName?: string;
+    /**
+     * 资料类型:1=产品种类,2=运动类型,3=品牌
+     */
+    dataType?: number;
+    /**
+     * 状态:1=启用,0=停用
+     */
+    status?: number;
+};
+

+ 55 - 0
src/api/types/WmsBasicDataResponse.ts

@@ -0,0 +1,55 @@
+/* generated using openapi-typescript-codegen -- do not edit */
+/* istanbul ignore file */
+/* tslint:disable */
+/* eslint-disable */
+export type WmsBasicDataResponse = {
+    /**
+     * 创建时间
+     */
+    createTime?: string;
+    /**
+     * 创建人
+     */
+    createUser?: string;
+    /**
+     * 资料名称
+     */
+    dataName?: string;
+    /**
+     * 资料类型:1=产品种类,2=运动类型,3=品牌
+     */
+    dataType?: number;
+    /**
+     * 删除标志:0=未删除,1=已删除
+     */
+    delFlag?: number;
+    /**
+     * 主键ID
+     */
+    id?: number;
+    /**
+     * 是否被SKU引用:0=未引用,1=已引用
+     */
+    isUsed?: number;
+    /**
+     * 已关联的SKU数量,仅分页列表返回
+     */
+    skuCount?: number;
+    /**
+     * 排序号
+     */
+    sortOrder?: number;
+    /**
+     * 状态:1=启用,0=停用
+     */
+    status?: number;
+    /**
+     * 更新时间
+     */
+    updateTime?: string;
+    /**
+     * 更新人
+     */
+    updateUser?: string;
+};
+

+ 27 - 0
src/api/types/WmsBasicDataSaveRequest.ts

@@ -0,0 +1,27 @@
+/* generated using openapi-typescript-codegen -- do not edit */
+/* istanbul ignore file */
+/* tslint:disable */
+/* eslint-disable */
+export type WmsBasicDataSaveRequest = {
+    /**
+     * 资料名称
+     */
+    dataName?: string;
+    /**
+     * 资料类型:1=产品种类,2=运动类型,3=品牌
+     */
+    dataType?: number;
+    /**
+     * 记录ID,编辑时必填
+     */
+    id?: number;
+    /**
+     * 显示排序号
+     */
+    sortOrder?: number;
+    /**
+     * 状态:1=启用,0=停用
+     */
+    status?: number;
+};
+

+ 29 - 0
src/api/types/WmsDashboardResponse.ts

@@ -0,0 +1,29 @@
+/* generated using openapi-typescript-codegen -- do not edit */
+/* istanbul ignore file */
+/* tslint:disable */
+/* eslint-disable */
+export type WmsDashboardResponse = {
+    internalOrderCount?: number;
+    pendingDraftCount?: number;
+    /**
+     * SKU 总数(已启用)
+     */
+    skuCount?: number;
+    /**
+     * 当日入库笔数(已完成)
+     */
+    todayInboundCount?: number;
+    /**
+     * 当日出库笔数(已完成)
+     */
+    todayOutboundCount?: number;
+    /**
+     * 固定仓库标识
+     */
+    warehouse?: string;
+    /**
+     * 仓库名称
+     */
+    warehouseName?: string;
+};
+

+ 5 - 0
src/api/types/WmsDashboardTodayFlowVO.ts

@@ -0,0 +1,5 @@
+export type WmsDashboardTodayFlowVO = {
+    detail?: string;
+    label?: string;
+    time?: string;
+};

+ 35 - 0
src/api/types/WmsFileAttachmentResponse.ts

@@ -0,0 +1,35 @@
+/* generated using openapi-typescript-codegen -- do not edit */
+/* istanbul ignore file */
+/* tslint:disable */
+/* eslint-disable */
+export type WmsFileAttachmentResponse = {
+    /**
+     * 业务记录ID
+     */
+    bizId?: number;
+    /**
+     * 业务类型:1=入库,2=出库,3=冲销
+     */
+    bizType?: number;
+    /**
+     * 创建时间
+     */
+    createTime?: string;
+    /**
+     * 文件原始名称
+     */
+    fileName?: string;
+    /**
+     * 文件类型:1=照片凭证,2=物流截图,3=外部订单凭证
+     */
+    fileType?: number;
+    /**
+     * 文件访问地址
+     */
+    fileUrl?: string;
+    /**
+     * 主键ID
+     */
+    id?: number;
+};
+

+ 11 - 0
src/api/types/WmsIdRequest.ts

@@ -0,0 +1,11 @@
+/* generated using openapi-typescript-codegen -- do not edit */
+/* istanbul ignore file */
+/* tslint:disable */
+/* eslint-disable */
+export type WmsIdRequest = {
+    /**
+     * 记录ID
+     */
+    id?: number;
+};
+

+ 36 - 0
src/api/types/WmsInboundPageRequest.ts

@@ -0,0 +1,36 @@
+import type { PageRequest } from './common';
+export type WmsInboundPageRequest = PageRequest & {
+    /**
+     * 创建时间结束(yyyy-MM-dd HH:mm:ss)
+     */
+    endTime?: string;
+    /**
+     * 快递单号(模糊)
+     */
+    expressNo?: string;
+    /**
+     * 关键词(同时匹配 skuId / 商品名称,任意输入可查)
+     */
+    keyword?: string;
+    /**
+     * 物流方式:1=快递,2=闪送,3=其他
+     */
+    logisticsType?: number;
+    /**
+     * 规格(模糊查询)
+     */
+    spec?: string;
+    /**
+     * 创建时间开始(yyyy-MM-dd HH:mm:ss)
+     */
+    startTime?: string;
+    /**
+     * 单据状态:0=草稿,1=已完成
+     */
+    status?: number;
+    /**
+     * 操作人(模糊查询)
+     */
+    updateUser?: string;
+};
+

+ 104 - 0
src/api/types/WmsInboundRecordResponse.ts

@@ -0,0 +1,104 @@
+/* generated using openapi-typescript-codegen -- do not edit */
+/* istanbul ignore file */
+/* tslint:disable */
+/* eslint-disable */
+import type { WmsFileAttachmentResponse } from './WmsFileAttachmentResponse';
+export type WmsInboundRecordResponse = {
+    /**
+     * 累计已冲销数量
+     */
+    alreadyReversedQty?: number;
+    /**
+     * 附件列表
+     */
+    attachments?: Array<WmsFileAttachmentResponse>;
+    /**
+     * 配置明细(入库时快照)
+     */
+    configDetailSnapshot?: string;
+    /**
+     * 创建时间
+     */
+    createTime?: string;
+    /**
+     * 创建人
+     */
+    createUser?: string;
+    /**
+     * 删除标志:0=未删除,1=已删除
+     */
+    delFlag?: number;
+    /**
+     * 快递公司
+     */
+    expressCompany?: string;
+    /**
+     * 快递单号
+     */
+    expressNo?: string;
+    /**
+     * 扩展字段1
+     */
+    extend1?: string;
+    /**
+     * 扩展字段2
+     */
+    extend2?: string;
+    /**
+     * 扩展字段3
+     */
+    extend3?: string;
+    /**
+     * 入库流水编号,格式 IN-MMDD-序号
+     */
+    flowNo?: string;
+    /**
+     * 主键ID
+     */
+    id?: number;
+    /**
+     * 物流方式:1=快递,2=闪送,3=其他
+     */
+    logisticsType?: number;
+    /**
+     * 入库数量
+     */
+    qty?: number;
+    /**
+     * 备注
+     */
+    remark?: string;
+    /**
+     * 冲销状态:0=未冲销,1=部分冲销,2=已全额冲销
+     */
+    reversalStatus?: number;
+    /**
+     * SKU编码
+     */
+    skuId?: string;
+    /**
+     * 商品全称(入库时快照)
+     */
+    skuNameSnapshot?: string;
+    /**
+     * 规格(入库时快照)
+     */
+    specSnapshot?: string;
+    /**
+     * 单据状态:0=草稿,1=已完成
+     */
+    status?: number;
+    /**
+     * 更新时间
+     */
+    updateTime?: string;
+    /**
+     * 更新人
+     */
+    updateUser?: string;
+    /**
+     * 仓库编码
+     */
+    warehouseId?: string;
+};
+

+ 47 - 0
src/api/types/WmsInboundSaveRequest.ts

@@ -0,0 +1,47 @@
+/* generated using openapi-typescript-codegen -- do not edit */
+/* istanbul ignore file */
+/* tslint:disable */
+/* eslint-disable */
+export type WmsInboundSaveRequest = {
+    /**
+     * 快递公司(物流方式为 1 时必填)
+     */
+    expressCompany?: string;
+    /**
+     * 快递单号(物流方式为 1 时必填)
+     */
+    expressNo?: string;
+    /**
+     * 文件原始名称列表(与 fileUrls 对应)
+     */
+    fileNames?: Array<string>;
+    /**
+     * 文件类型列表(与 fileUrls 对应:1=照片凭证,2=物流截图)
+     */
+    fileTypes?: Array<number>;
+    /**
+     * 文件URL列表(提交时快递需≥1张)
+     */
+    fileUrls?: Array<string>;
+    /**
+     * 入库记录ID(编辑草稿时必填)
+     */
+    id?: number;
+    /**
+     * 物流方式:1=快递,2=闪送,3=其他;默认 1
+     */
+    logisticsType?: number;
+    /**
+     * 入库数量(提交时必填,>0 整数)
+     */
+    qty?: number;
+    /**
+     * 备注(选填)
+     */
+    remark?: string;
+    /**
+     * skuID(必填)
+     */
+    skuId?: string;
+};
+

+ 3 - 0
src/api/types/WmsInboundSubmitRequest.ts

@@ -0,0 +1,3 @@
+import type { SubmitDraftRequest } from './common';
+import type { WmsInboundSaveRequest } from './WmsInboundSaveRequest';
+export type WmsInboundSubmitRequest = SubmitDraftRequest<WmsInboundSaveRequest>;

+ 20 - 0
src/api/types/WmsLedgerAdminSummaryResponse.ts

@@ -0,0 +1,20 @@
+/* generated using openapi-typescript-codegen -- do not edit */
+/* istanbul ignore file */
+/* tslint:disable */
+/* eslint-disable */
+export type WmsLedgerAdminSummaryResponse = {
+    warehouseName?: string;
+    /**
+     * 查询范围,默认上海闵行仓
+     */
+    queryScope?: string;
+    /**
+     * 今日变动流水数:当日 (create_time 当天) 入库 + 出库 + 冲销/更正 流水总数
+     */
+    todayFlowCount?: number;
+    /**
+     * 库存总记录数:状态为已启用 (status=1) 的 SKU 数量
+     */
+    totalStockCount?: number;
+};
+

+ 52 - 0
src/api/types/WmsLedgerFlowQueryRequest.ts

@@ -0,0 +1,52 @@
+import type { PageRequest } from './common';
+export type WmsLedgerFlowQueryRequest = PageRequest & {
+    /**
+     * 创建时间结束(yyyy-MM-dd HH:mm:ss)
+     */
+    endTime?: string;
+    /**
+     * 快递单号(模糊)
+     */
+    expressNo?: string;
+    /**
+     * 流水来源:NORMAL / REVERSAL
+     */
+    flowSource?: string;
+    /**
+     * 流水类型:1=入库,2=出库,3=冲销(不传则全部)
+     */
+    flowType?: number;
+    /**
+     * 关键词(同时匹配 skuId / 商品名称 / SO 编号,任意输入可查)
+     */
+    keyword?: string;
+    /**
+     * 物流方式:1=快递,2=闪送,3=其他
+     */
+    logisticsType?: number;
+    /**
+     * 订单类型:1=内部订单,2=外部订单(出库筛选)
+     */
+    orderType?: number;
+    /**
+     * 原流水冲销状态:0=未冲销,1=部分冲销,2=已全额冲销
+     */
+    reversalStatus?: number;
+    /**
+     * 规格(模糊)
+     */
+    spec?: string;
+    /**
+     * 创建时间开始(yyyy-MM-dd HH:mm:ss)
+     */
+    startTime?: string;
+    /**
+     * 单据状态:0=草稿,1=已完成(不传则全部)
+     */
+    status?: number;
+    /**
+     * 操作人(模糊)
+     */
+    updateUser?: string;
+};
+

+ 113 - 0
src/api/types/WmsLedgerFlowVO.ts

@@ -0,0 +1,113 @@
+/* generated using openapi-typescript-codegen -- do not edit */
+/* istanbul ignore file */
+/* tslint:disable */
+/* eslint-disable */
+import type { WmsFileAttachmentResponse } from './WmsFileAttachmentResponse';
+export type WmsLedgerFlowVO = {
+    /**
+     * 已冲销数量(仅入库/出库有效,冲销流水为 null)
+     */
+    alreadyReversedQty?: number;
+    /**
+     * 附件列表
+     */
+    attachments?: Array<WmsFileAttachmentResponse>;
+    /**
+     * 配置明细(快照)
+     */
+    configDetail?: string;
+    /**
+     * 创建时间
+     */
+    createTime?: string;
+    /**
+     * 操作人
+     */
+    createUser?: string;
+    /**
+     * 快递公司
+     */
+    expressCompany?: string;
+    /**
+     * 快递单号
+     */
+    expressNo?: string;
+    /**
+     * 流水编号(IN-/OUT-/REV-前缀)
+     */
+    flowNo?: string;
+    /**
+     * 流水来源:NORMAL=正常, REVERSAL=冲销
+     */
+    flowSource?: string;
+    /**
+     * 流水类型:1=入库,2=出库,3=冲销
+     */
+    flowType?: number;
+    /**
+     * 物流方式:1=快递,2=闪送,3=其他
+     */
+    logisticsType?: number;
+    /**
+     * 订单类型:1=内部订单,2=外部订单
+     */
+    orderType?: number;
+    /**
+     * 关联原流水编号(冲销流水有值,入库/出库为 null)
+     */
+    originFlowNo?: string;
+    originFlowDetail?: string;
+    /**
+     * 数量(入库为正,出库/冲销出方向为负,冲销入方向为正)
+     */
+    qty?: number;
+    /**
+     * 原始记录 ID(用于查详情/凭证)
+     */
+    recordId?: number;
+    /**
+     * 备注
+     */
+    remark?: string;
+    /**
+     * 冲销方向:IN=产生入库, OUT=产生出库(冲销流水有值)
+     */
+    reversalDirection?: number;
+    /**
+     * 冲销原因
+     */
+    reversalReason?: string;
+    /**
+     * 原流水冲销状态:0=未冲销,1=部分冲销,2=已全额冲销;冲销流水为 null
+     */
+    reversalStatus?: number;
+    /**
+     * skuID
+     */
+    skuId?: string;
+    /**
+     * 商品全称(快照)
+     */
+    skuName?: string;
+    /**
+     * SO 编号
+     */
+    soNo?: string;
+    /**
+     * 规格(快照)
+     */
+    spec?: string;
+    /**
+     * 单据状态:0=草稿,1=已完成
+     */
+    status?: number;
+    /**
+     * 更新人
+     */
+    updateUser?: string;
+    /**
+     * 仓库编码
+     */
+    warehouseId?: string;
+};
+

+ 40 - 0
src/api/types/WmsOutboundPageRequest.ts

@@ -0,0 +1,40 @@
+import type { PageRequest } from './common';
+export type WmsOutboundPageRequest = PageRequest & {
+    /**
+     * 创建时间结束(yyyy-MM-dd HH:mm:ss)
+     */
+    endTime?: string;
+    /**
+     * 快递单号(模糊)
+     */
+    expressNo?: string;
+    /**
+     * 关键词(同时匹配 skuId / 商品名称 / SO 编号,任意输入可查)
+     */
+    keyword?: string;
+    /**
+     * 物流方式:1=快递,2=闪送,3=其他
+     */
+    logisticsType?: number;
+    /**
+     * 订单类型:1=内部订单,2=外部订单
+     */
+    orderType?: number;
+    /**
+     * 规格(模糊)
+     */
+    spec?: string;
+    /**
+     * 创建时间开始(yyyy-MM-dd HH:mm:ss)
+     */
+    startTime?: string;
+    /**
+     * 单据状态:0=草稿,1=已完成
+     */
+    status?: number;
+    /**
+     * 操作人(模糊)
+     */
+    updateUser?: string;
+};
+

+ 112 - 0
src/api/types/WmsOutboundRecordResponse.ts

@@ -0,0 +1,112 @@
+/* generated using openapi-typescript-codegen -- do not edit */
+/* istanbul ignore file */
+/* tslint:disable */
+/* eslint-disable */
+import type { WmsFileAttachmentResponse } from './WmsFileAttachmentResponse';
+export type WmsOutboundRecordResponse = {
+    /**
+     * 累计已冲销数量
+     */
+    alreadyReversedQty?: number;
+    /**
+     * 附件列表
+     */
+    attachments?: Array<WmsFileAttachmentResponse>;
+    /**
+     * 配置明细(出库时快照)
+     */
+    configDetailSnapshot?: string;
+    /**
+     * 创建时间
+     */
+    createTime?: string;
+    /**
+     * 创建人
+     */
+    createUser?: string;
+    /**
+     * 删除标志:0=未删除,1=已删除
+     */
+    delFlag?: number;
+    /**
+     * 快递公司
+     */
+    expressCompany?: string;
+    /**
+     * 快递单号
+     */
+    expressNo?: string;
+    /**
+     * 扩展字段1
+     */
+    extend1?: string;
+    /**
+     * 扩展字段2
+     */
+    extend2?: string;
+    /**
+     * 扩展字段3
+     */
+    extend3?: string;
+    /**
+     * 出库流水编号,格式 OUT-MMDD-序号
+     */
+    flowNo?: string;
+    /**
+     * 主键ID
+     */
+    id?: number;
+    /**
+     * 物流方式:1=快递,2=闪送,3=其他
+     */
+    logisticsType?: number;
+    /**
+     * 订单类型:1=内部订单,2=外部订单
+     */
+    orderType?: number;
+    /**
+     * 出库数量
+     */
+    qty?: number;
+    /**
+     * 备注
+     */
+    remark?: string;
+    /**
+     * 冲销状态:0=未冲销,1=部分冲销,2=已全额冲销
+     */
+    reversalStatus?: number;
+    /**
+     * SKU编码
+     */
+    skuId?: string;
+    /**
+     * 商品全称(出库时快照)
+     */
+    skuNameSnapshot?: string;
+    /**
+     * SO编号
+     */
+    soNo?: string;
+    /**
+     * 规格(出库时快照)
+     */
+    specSnapshot?: string;
+    /**
+     * 单据状态:0=草稿,1=已完成
+     */
+    status?: number;
+    /**
+     * 更新时间
+     */
+    updateTime?: string;
+    /**
+     * 更新人
+     */
+    updateUser?: string;
+    /**
+     * 仓库编码
+     */
+    warehouseId?: string;
+};
+

+ 55 - 0
src/api/types/WmsOutboundSaveRequest.ts

@@ -0,0 +1,55 @@
+/* generated using openapi-typescript-codegen -- do not edit */
+/* istanbul ignore file */
+/* tslint:disable */
+/* eslint-disable */
+export type WmsOutboundSaveRequest = {
+    /**
+     * 快递公司(外部订单且物流方式为 1 时必填)
+     */
+    expressCompany?: string;
+    /**
+     * 快递单号(外部订单且物流方式为 1 时必填)
+     */
+    expressNo?: string;
+    /**
+     * 文件原始名称列表
+     */
+    fileNames?: Array<string>;
+    /**
+     * 文件类型列表(2=物流截图,3=外部订单凭证)
+     */
+    fileTypes?: Array<number>;
+    /**
+     * 文件URL列表(外部订单需订单凭证;闪送/其他需物流截图)
+     */
+    fileUrls?: Array<string>;
+    /**
+     * 出库记录ID(编辑草稿时必填)
+     */
+    id?: number;
+    /**
+     * 物流方式(外部订单必填):1=快递,2=闪送,3=其他
+     */
+    logisticsType?: number;
+    /**
+     * 订单类型:1=内部订单,2=外部订单(必填)
+     */
+    orderType?: number;
+    /**
+     * 出库数量(提交时必填,>0 且不超过库存)
+     */
+    qty?: number;
+    /**
+     * 备注(选填)
+     */
+    remark?: string;
+    /**
+     * skuID(必填)
+     */
+    skuId?: string;
+    /**
+     * SO编号(内部订单必填)
+     */
+    soNo?: string;
+};
+

+ 3 - 0
src/api/types/WmsOutboundSubmitRequest.ts

@@ -0,0 +1,3 @@
+import type { SubmitDraftRequest } from './common';
+import type { WmsOutboundSaveRequest } from './WmsOutboundSaveRequest';
+export type WmsOutboundSubmitRequest = SubmitDraftRequest<WmsOutboundSaveRequest>;

+ 15 - 0
src/api/types/WmsReversalPreCheckRequest.ts

@@ -0,0 +1,15 @@
+/* generated using openapi-typescript-codegen -- do not edit */
+/* istanbul ignore file */
+/* tslint:disable */
+/* eslint-disable */
+export type WmsReversalPreCheckRequest = {
+    /**
+     * 原流水编号
+     */
+    originFlowNo?: string;
+    /**
+     * 原流水类型:1=入库,2=出库
+     */
+    originFlowType?: number;
+};
+

+ 47 - 0
src/api/types/WmsReversalPreCheckResponse.ts

@@ -0,0 +1,47 @@
+/* generated using openapi-typescript-codegen -- do not edit */
+/* istanbul ignore file */
+/* tslint:disable */
+/* eslint-disable */
+export type WmsReversalPreCheckResponse = {
+    /**
+     * 已冲销数量
+     */
+    alreadyReversedQty?: number;
+    /**
+     * 当前可用库存(原入库时展示,原出库时为 null)
+     */
+    currentStock?: number;
+    /**
+     * 原流水编号
+     */
+    originFlowNo?: string;
+    /**
+     * 原流水类型:1=入库,2=出库
+     */
+    originFlowType?: number;
+    /**
+     * 原流水数量
+     */
+    originQty?: number;
+    /**
+     * 剩余可冲销数量
+     */
+    remainReversalQty?: number;
+    /**
+     * 冲销方向:1=产生入库,2=产生出库
+     */
+    reversalDirection?: number;
+    /**
+     * skuID
+     */
+    skuId?: string;
+    /**
+     * 商品名称(快照)
+     */
+    skuName?: string;
+    /**
+     * 规格(快照)
+     */
+    spec?: string;
+};
+

+ 85 - 0
src/api/types/WmsReversalRecordResponse.ts

@@ -0,0 +1,85 @@
+/* generated using openapi-typescript-codegen -- do not edit */
+/* istanbul ignore file */
+/* tslint:disable */
+/* eslint-disable */
+import type { WmsFileAttachmentResponse } from './WmsFileAttachmentResponse';
+export type WmsReversalRecordResponse = {
+    /**
+     * 附件列表
+     */
+    attachments?: Array<WmsFileAttachmentResponse>;
+    /**
+     * 配置明细(冲销时快照)
+     */
+    configDetailSnapshot?: string;
+    /**
+     * 创建时间
+     */
+    createTime?: string;
+    /**
+     * 创建人
+     */
+    createUser?: string;
+    /**
+     * 删除标志:0=未删除,1=已删除
+     */
+    delFlag?: number;
+    /**
+     * 冲销流水编号,格式 REV-MMDD-序号
+     */
+    flowNo?: string;
+    /**
+     * 主键ID
+     */
+    id?: number;
+    /**
+     * 原流水编号
+     */
+    originFlowNo?: string;
+    originFlowDetail?: string;
+    /**
+     * 原流水类型:1=入库,2=出库
+     */
+    originFlowType?: number;
+    /**
+     * 冲销数量
+     */
+    qty?: number;
+    /**
+     * 冲销方向:1=产生入库,2=产生出库
+     */
+    reversalDirection?: number;
+    /**
+     * 冲销原因
+     */
+    reversalReason?: string;
+    /**
+     * SKU编码
+     */
+    skuId?: string;
+    /**
+     * 商品全称(冲销时快照)
+     */
+    skuNameSnapshot?: string;
+    /**
+     * 规格(冲销时快照)
+     */
+    specSnapshot?: string;
+    /**
+     * 单据状态:0=草稿,1=已完成
+     */
+    status?: number;
+    /**
+     * 更新时间
+     */
+    updateTime?: string;
+    /**
+     * 更新人
+     */
+    updateUser?: string;
+    /**
+     * 仓库编码
+     */
+    warehouseId?: string;
+};
+

+ 31 - 0
src/api/types/WmsReversalSubmitRequest.ts

@@ -0,0 +1,31 @@
+/* generated using openapi-typescript-codegen -- do not edit */
+/* istanbul ignore file */
+/* tslint:disable */
+/* eslint-disable */
+export type WmsReversalSubmitRequest = {
+    /**
+     * 文件原始名称列表
+     */
+    fileNames?: Array<string>;
+    /**
+     * 凭证文件URL列表(至少1张)
+     */
+    fileUrls?: Array<string>;
+    /**
+     * 原流水编号(必填)
+     */
+    originFlowNo?: string;
+    /**
+     * 原流水类型:1=入库,2=出库(必填)
+     */
+    originFlowType?: number;
+    /**
+     * 本次冲销数量(必填,>0,不超过剩余可冲销数量)
+     */
+    qty?: number;
+    /**
+     * 冲销原因(选填)
+     */
+    reversalReason?: string;
+};
+

+ 28 - 0
src/api/types/WmsSkuQueryPageRequest.ts

@@ -0,0 +1,28 @@
+import type { PageRequest } from './common';
+export type WmsSkuQueryPageRequest = PageRequest & {
+    /**
+     * 品牌ID
+     */
+    brandId?: number;
+    /**
+     * 产品种类ID
+     */
+    categoryId?: number;
+    /**
+     * 关键字(同时匹配 skuId / 商品名称,任意输入可查)
+     */
+    keyword?: string;
+    /**
+     * 商品规格,支持模糊查询
+     */
+    spec?: string;
+    /**
+     * 运动类型ID
+     */
+    sportTypeId?: number;
+    /**
+     * 状态:1=启用,0=停用
+     */
+    status?: number;
+};
+

+ 83 - 0
src/api/types/WmsSkuResponse.ts

@@ -0,0 +1,83 @@
+/* generated using openapi-typescript-codegen -- do not edit */
+/* istanbul ignore file */
+/* tslint:disable */
+/* eslint-disable */
+export type WmsSkuResponse = {
+    /**
+     * 品牌ID
+     */
+    brandId?: number;
+    /**
+     * 产品种类ID
+     */
+    categoryId?: number;
+    /**
+     * 配置明细
+     */
+    configDetail?: string;
+    /**
+     * 创建时间
+     */
+    createTime?: string;
+    /**
+     * 创建人
+     */
+    createUser?: string;
+    /**
+     * 删除标志:0=未删除,1=已删除
+     */
+    delFlag?: number;
+    /**
+     * 扩展字段1
+     */
+    extend1?: string;
+    /**
+     * 主键ID
+     */
+    id?: number;
+    /**
+     * 最近出入库时间
+     */
+    lastFlowTime?: string;
+    /**
+     * 商品全称
+     */
+    productName?: string;
+    /**
+     * 最近流水记录(详情接口返回),最多 10 条
+     */
+    recentFlows?: Array<string>;
+    /**
+     * 备注
+     */
+    remark?: string;
+    /**
+     * SKU编码,系统自动生成,格式 SKU000001
+     */
+    skuId?: string;
+    /**
+     * 规格
+     */
+    spec?: string;
+    /**
+     * 运动类型ID
+     */
+    sportTypeId?: number;
+    /**
+     * 状态:1=启用,0=停用
+     */
+    status?: number;
+    /**
+     * 当前库存,仅分页列表返回,从库存表查询;无记录时为 0
+     */
+    stock?: number;
+    /**
+     * 更新时间
+     */
+    updateTime?: string;
+    /**
+     * 更新人
+     */
+    updateUser?: string;
+};
+

+ 47 - 0
src/api/types/WmsSkuSaveRequest.ts

@@ -0,0 +1,47 @@
+/* generated using openapi-typescript-codegen -- do not edit */
+/* istanbul ignore file */
+/* tslint:disable */
+/* eslint-disable */
+export type WmsSkuSaveRequest = {
+    /**
+     * 品牌ID,球星卡必填
+     */
+    brandId?: number;
+    /**
+     * 产品种类ID
+     */
+    categoryId?: number;
+    /**
+     * 商品配置明细
+     */
+    configDetail?: string;
+    /**
+     * 预留扩展字段1
+     */
+    extend1?: string;
+    /**
+     * 预留扩展字段2
+     */
+    extend2?: string;
+    /**
+     * 预留扩展字段3
+     */
+    extend3?: string;
+    /**
+     * 商品全称
+     */
+    productName?: string;
+    /**
+     * 备注
+     */
+    remark?: string;
+    /**
+     * 商品规格,如箱、盒、个
+     */
+    spec?: string;
+    /**
+     * 运动类型ID,球星卡必填
+     */
+    sportTypeId?: number;
+};
+

+ 11 - 0
src/api/types/WmsSkuSearchRequest.ts

@@ -0,0 +1,11 @@
+/* generated using openapi-typescript-codegen -- do not edit */
+/* istanbul ignore file */
+/* tslint:disable */
+/* eslint-disable */
+export type WmsSkuSearchRequest = {
+    /**
+     * SKU编码或商品名称关键词
+     */
+    keyword?: string;
+};
+

+ 27 - 0
src/api/types/WmsSkuUpdateRequest.ts

@@ -0,0 +1,27 @@
+/* generated using openapi-typescript-codegen -- do not edit */
+/* istanbul ignore file */
+/* tslint:disable */
+/* eslint-disable */
+export type WmsSkuUpdateRequest = {
+    brandId?: number;
+    categoryId?: number;
+    /**
+     * 商品配置明细
+     */
+    configDetail?: string;
+    /**
+     * SKU记录ID
+     */
+    id?: number;
+    /**
+     * 商品全称
+     */
+    productName?: string;
+    /**
+     * 备注
+     */
+    remark?: string;
+    spec?: string;
+    sportTypeId?: number;
+};
+

+ 15 - 0
src/api/types/WmsStatusUpdateRequest.ts

@@ -0,0 +1,15 @@
+/* generated using openapi-typescript-codegen -- do not edit */
+/* istanbul ignore file */
+/* tslint:disable */
+/* eslint-disable */
+export type WmsStatusUpdateRequest = {
+    /**
+     * 记录ID
+     */
+    id?: number;
+    /**
+     * 状态:1=启用,0=停用
+     */
+    status?: number;
+};
+

+ 8 - 0
src/api/types/common.ts

@@ -0,0 +1,8 @@
+export type PageRequest = {
+    pageNo?: number;
+    pageSize?: number;
+};
+
+export type SubmitDraftRequest<SaveRequest> = Omit<SaveRequest, 'id'> & {
+    draftId?: number;
+};

+ 31 - 0
src/api/types/index.ts

@@ -0,0 +1,31 @@
+export type { TableDataInfo } from './TableDataInfo';
+export type { PageRequest, SubmitDraftRequest } from './common';
+export type { WMS_ledger_flow_detail_request } from './WMS_ledger_flow_detail_request';
+export type { WmsBasicDataQueryRequest } from './WmsBasicDataQueryRequest';
+export type { WmsBasicDataResponse } from './WmsBasicDataResponse';
+export type { WmsBasicDataSaveRequest } from './WmsBasicDataSaveRequest';
+export type { WmsDashboardResponse } from './WmsDashboardResponse';
+export type { WmsDashboardTodayFlowVO } from './WmsDashboardTodayFlowVO';
+export type { WmsFileAttachmentResponse } from './WmsFileAttachmentResponse';
+export type { WmsIdRequest } from './WmsIdRequest';
+export type { WmsInboundPageRequest } from './WmsInboundPageRequest';
+export type { WmsInboundRecordResponse } from './WmsInboundRecordResponse';
+export type { WmsInboundSaveRequest } from './WmsInboundSaveRequest';
+export type { WmsInboundSubmitRequest } from './WmsInboundSubmitRequest';
+export type { WmsLedgerAdminSummaryResponse } from './WmsLedgerAdminSummaryResponse';
+export type { WmsLedgerFlowQueryRequest } from './WmsLedgerFlowQueryRequest';
+export type { WmsLedgerFlowVO } from './WmsLedgerFlowVO';
+export type { WmsOutboundPageRequest } from './WmsOutboundPageRequest';
+export type { WmsOutboundRecordResponse } from './WmsOutboundRecordResponse';
+export type { WmsOutboundSaveRequest } from './WmsOutboundSaveRequest';
+export type { WmsOutboundSubmitRequest } from './WmsOutboundSubmitRequest';
+export type { WmsReversalPreCheckRequest } from './WmsReversalPreCheckRequest';
+export type { WmsReversalPreCheckResponse } from './WmsReversalPreCheckResponse';
+export type { WmsReversalRecordResponse } from './WmsReversalRecordResponse';
+export type { WmsReversalSubmitRequest } from './WmsReversalSubmitRequest';
+export type { WmsSkuQueryPageRequest } from './WmsSkuQueryPageRequest';
+export type { WmsSkuResponse } from './WmsSkuResponse';
+export type { WmsSkuSaveRequest } from './WmsSkuSaveRequest';
+export type { WmsSkuSearchRequest } from './WmsSkuSearchRequest';
+export type { WmsSkuUpdateRequest } from './WmsSkuUpdateRequest';
+export type { WmsStatusUpdateRequest } from './WmsStatusUpdateRequest';

BIN
src/assets/hero.png


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
src/assets/vite.svg


+ 1 - 0
src/assets/vue.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

+ 35 - 0
src/components/EntryAudit.vue

@@ -0,0 +1,35 @@
+<script setup lang="ts">
+import { computed } from 'vue'
+import type { Entry } from '../api/indexType'
+const props = defineProps<{ entry: Entry; reversal?: boolean; reversedQty?: number; currentStock?: number | null }>()
+const sku = computed(() => props.entry.snapshot)
+const reversedQuantity = computed(() => props.reversedQty ?? 0)
+const availableStock = computed(() => props.currentStock ?? null)
+const logisticsName = computed(() => ({ express: '快递', instant: '闪送', other: '其他' }[props.entry.logistics] || '—'))
+</script>
+
+<template>
+  <el-descriptions :column="1" border class="entry-audit">
+    <el-descriptions-item v-if="reversal" label="原流水编号">{{ entry.id }}</el-descriptions-item>
+    <el-descriptions-item label="skuID">{{ entry.skuId }}</el-descriptions-item>
+    <el-descriptions-item label="商品名称">{{ sku?.name }}</el-descriptions-item>
+    <el-descriptions-item label="规格">{{ sku?.spec }}</el-descriptions-item>
+    <el-descriptions-item label="仓库">上海闵行仓</el-descriptions-item>
+    <el-descriptions-item v-if="!reversal" label="操作人">{{ entry.operator }}</el-descriptions-item>
+    <template v-if="entry.source === 'normal'">
+      <el-descriptions-item label="原流水数量">{{ entry.quantity }}</el-descriptions-item>
+      <el-descriptions-item label="已冲销数量">{{ reversedQuantity }}</el-descriptions-item>
+      <el-descriptions-item label="剩余可冲销数量">{{ Number(entry.quantity) - reversedQuantity }}</el-descriptions-item>
+    </template>
+    <el-descriptions-item v-if="reversal && entry.direction === 'inbound'" label="当前可用库存">{{ availableStock ?? '—' }} {{ sku?.spec }}</el-descriptions-item>
+    <template v-if="!reversal">
+      <el-descriptions-item label="流水来源">{{ entry.source === 'normal' ? '正常录入' : '冲销' }}</el-descriptions-item>
+      <el-descriptions-item v-if="entry.source === 'normal' && (entry.direction === 'inbound' || entry.orderType === 'external')" label="物流方式">{{ logisticsName }}</el-descriptions-item>
+      <el-descriptions-item label="备注 / 冲销原因">{{ entry.remark || '—' }}</el-descriptions-item>
+    </template>
+  </el-descriptions>
+</template>
+
+<style scoped>
+.entry-audit { margin-bottom: 16px; overflow-wrap: anywhere; }
+</style>

+ 49 - 0
src/components/EvidenceUpload.vue

@@ -0,0 +1,49 @@
+<script setup lang="ts">
+	import { inject, onBeforeUnmount, ref, watch } from 'vue'
+	import { Upload, Delete } from '@element-plus/icons-vue'
+	import type { Attachment } from '../api/indexType'
+	import { createUploadId, uploadImage } from '../utils/upload'
+	import { report } from '../utils/ui'
+	import { uploadsKey } from '../utils/uploads'
+	const props = defineProps<{ active: boolean; disabled?: boolean }>()
+	const pendingUploads = inject(uploadsKey)
+	let generation = 0
+	function invalidate() { generation += 1 }
+	watch(() => props.active, invalidate, { flush: 'sync' })
+	onBeforeUnmount(invalidate)
+	const model = defineModel<Attachment[]>({ required: true })
+	const busy = ref(false)
+	async function select(event : Event) {
+		if (busy.value || props.disabled || !props.active) return
+		const input = event.target as HTMLInputElement
+		const currentGeneration = generation
+		busy.value = true
+		if (pendingUploads) pendingUploads.value += 1
+		try {
+			for (const file of Array.from(input.files || [])) {
+				const url = await uploadImage(file)
+				if (currentGeneration !== generation || !props.active) return
+				model.value = [...model.value, { id: createUploadId(), name: file.name, url }]
+			}
+		}
+		catch (error) { if (currentGeneration === generation) report(error) }
+		finally { busy.value = false; input.value = ''; if (pendingUploads) pendingUploads.value -= 1 }
+	}
+</script>
+<template>
+	<div class="evidence">
+		<label class="upload-button">
+			<el-icon>
+				<Upload />
+			</el-icon>{{ busy ? '上传中…' : '上传图片' }}
+			<input type="file" accept="image/png,image/jpeg,image/webp" multiple :disabled="busy || disabled" @change="select">
+		</label>
+		<div v-for="(file, index) in model" :key="file.id" class="attachment">
+			<el-image :src="file.url" :preview-src-list="model.map(item => item.url)" :initial-index="index"
+				preview-teleported fit="cover" />
+			<span>{{ file.name }}</span>
+			<el-button :disabled="busy || disabled" :icon="Delete" text aria-label="移除图片" title="移除图片"
+				@click="model = model.filter(item => item.id !== file.id)" />
+		</div>
+	</div>
+</template>

+ 95 - 0
src/components/HelloWorld.vue

@@ -0,0 +1,95 @@
+<script setup lang="ts">
+	import { ref } from 'vue'
+	import heroImg from '../assets/hero.png'
+	import viteLogo from '../assets/vite.svg'
+	import vueLogo from '../assets/vue.svg'
+
+	const count = ref(0)
+</script>
+
+<template>
+	<section id="center">
+		<div class="hero">
+			<img :src="heroImg" class="base" width="170" height="179" alt="" />
+			<img :src="vueLogo" class="framework" alt="Vue logo" />
+			<img :src="viteLogo" class="vite" alt="Vite logo" />
+		</div>
+		<div>
+			<h1>Get started</h1>
+			<p>Edit <code>src/App.vue</code> and save to test <code>HMR</code></p>
+		</div>
+		<button type="button" class="counter" @click="count++">
+			Count is {{ count }}
+		</button>
+	</section>
+
+	<div class="ticks"></div>
+
+	<section id="next-steps">
+		<div id="docs">
+			<svg class="icon" role="presentation" aria-hidden="true">
+				<use href="/icons.svg#documentation-icon"></use>
+			</svg>
+			<h2>Documentation</h2>
+			<p>Your questions, answered</p>
+			<ul>
+				<li>
+					<a href="https://vite.dev/" target="_blank">
+						<img class="logo" :src="viteLogo" alt="" />
+						Explore Vite
+					</a>
+				</li>
+				<li>
+					<a href="https://vuejs.org/" target="_blank">
+						<img class="button-icon" :src="vueLogo" alt="" />
+						Learn more
+					</a>
+				</li>
+			</ul>
+		</div>
+		<div id="social">
+			<svg class="icon" role="presentation" aria-hidden="true">
+				<use href="/icons.svg#social-icon"></use>
+			</svg>
+			<h2>Connect with us</h2>
+			<p>Join the Vite community</p>
+			<ul>
+				<li>
+					<a href="https://github.com/vitejs/vite" target="_blank">
+						<svg class="button-icon" role="presentation" aria-hidden="true">
+							<use href="/icons.svg#github-icon"></use>
+						</svg>
+						GitHub
+					</a>
+				</li>
+				<li>
+					<a href="https://chat.vite.dev/" target="_blank">
+						<svg class="button-icon" role="presentation" aria-hidden="true">
+							<use href="/icons.svg#discord-icon"></use>
+						</svg>
+						Discord
+					</a>
+				</li>
+				<li>
+					<a href="https://x.com/vite_js" target="_blank">
+						<svg class="button-icon" role="presentation" aria-hidden="true">
+							<use href="/icons.svg#x-icon"></use>
+						</svg>
+						X.com
+					</a>
+				</li>
+				<li>
+					<a href="https://bsky.app/profile/vite.dev" target="_blank">
+						<svg class="button-icon" role="presentation" aria-hidden="true">
+							<use href="/icons.svg#bluesky-icon"></use>
+						</svg>
+						Bluesky
+					</a>
+				</li>
+			</ul>
+		</div>
+	</section>
+
+	<div class="ticks"></div>
+	<section id="spacer"></section>
+</template>

+ 50 - 0
src/http/index.ts

@@ -0,0 +1,50 @@
+import axios from 'axios'
+import { clearAuthInfo, clearOAuthTemp, getOAuthLoginUrl, getToken } from '../utils/auth'
+
+// 跳转完成前只处理一次 401,避免并发请求覆盖本次授权的 PKCE 缓存。
+let redirectingToAuth = false
+
+export function createServiceHttp(baseURL: string, authenticated = true) {
+	const client = axios.create({ baseURL, timeout: 15000 })
+	if (!authenticated) return client
+
+client.interceptors.request.use(config => {
+	const token = getToken()
+	if (token) config.headers.Authorization = `Bearer ${token}`
+	return config
+})
+
+client.interceptors.response.use(
+	response => response,
+	error => {
+		if (error.response?.status === 401 && !redirectingToAuth) {
+			redirectingToAuth = true
+			clearAuthInfo()
+			clearOAuthTemp()
+			sessionStorage.removeItem('warehouse-auth-paused')
+			try {
+				location.assign(getOAuthLoginUrl())
+			} catch {
+				// 配置错误或跳转失败时回到登录页,由用户手动重试并查看错误。
+				clearOAuthTemp()
+				sessionStorage.setItem('warehouse-auth-paused', '1')
+				location.hash = '#/login'
+				redirectingToAuth = false
+			}
+		}
+		return Promise.reject(error)
+	},
+)
+	return client
+}
+
+const defaultBaseURL = import.meta.env.VITE_API_BASE_URL || '/api'
+
+export const services = {
+	warehouse: createServiceHttp(defaultBaseURL),
+	wms: createServiceHttp(import.meta.env.VITE_API_WMS_URL || ''),
+	files: createServiceHttp(import.meta.env.VITE_API_AUTH_URL || defaultBaseURL),
+}
+
+export const http = services.warehouse
+export const authHttp = services.files

+ 14 - 0
src/main.ts

@@ -0,0 +1,14 @@
+import { createApp } from 'vue'
+import { createPinia } from 'pinia'
+import ElementPlus from 'element-plus'
+import zhCn from 'element-plus/es/locale/lang/zh-cn'
+import 'element-plus/dist/index.css'
+import { router } from './router'
+import './style/operations.css'
+import './style/prototype.css'
+import App from './App.vue'
+
+async function start() {
+	createApp(App).use(createPinia()).use(router).use(ElementPlus, { locale: zhCn }).mount('#app')
+}
+start().catch(error => { document.getElementById('app')!.textContent = `启动失败:${error.message}` })

+ 26 - 0
src/router/index.ts

@@ -0,0 +1,26 @@
+import { createRouter, createWebHistory } from 'vue-router'
+import { getToken } from '../utils/auth'
+import Login from '../views/Login/index.vue'
+import Dashboard from '../views/Dashboard.vue'
+import Master from '../views/Master.vue'
+import Sku from '../views/Sku.vue'
+import Inbound from '../views/Inbound.vue'
+import Outbound from '../views/Outbound.vue'
+import Ledger from '../views/Ledger.vue'
+
+export const router = createRouter({ history: createWebHistory(), routes: [
+  { path: '/', redirect: '/dashboard' }, { path: '/login', component: Login },
+  { path: '/dashboard', component: Dashboard, meta: { title: '工作台' } },
+  { path: '/master', component: Master, meta: { title: '基础资料管理' } },
+  { path: '/sku', component: Sku, meta: { title: '商品 / SKU 管理' } },
+  { path: '/inbound', component: Inbound, meta: { title: '入库管理' } },
+  { path: '/outbound', component: Outbound, meta: { title: '出库管理' } },
+  { path: '/ledger', component: Ledger, meta: { title: '库存台账' } },
+  { path: '/:pathMatch(.*)*', redirect: '/dashboard' },
+] })
+
+router.beforeEach(to => {
+  const query = new URLSearchParams(location.search)
+  if (to.path !== '/login' && (query.has('code') || query.has('error'))) return '/login'
+  if (to.path !== '/login' && !getToken()) return '/login'
+})

+ 108 - 0
src/store/index.ts

@@ -0,0 +1,108 @@
+import { defineStore } from 'pinia'
+import { computed, ref, watch } from 'vue'
+
+// 沿用现有会话缓存键;认证信息只在当前标签页保存,不写入 localStorage。
+export const AUTH_TOKEN_KEY = 'warehouse-token'
+export const AUTH_DISPLAY_NAME_KEY = 'warehouse-user'
+export const AUTH_ROLE_KEY = 'warehouse-role'
+const transactionKey = 'warehouse-oauth-transaction'
+
+/** 登录成功后写入的认证信息;名称和角色可省略,实际权限仍由后端校验。 */
+export interface AuthPayload {
+	accessToken: string
+	displayName?: string
+	role?: string[]
+}
+
+/** 一次 OAuth 授权的完整上下文,供跳转返回后校验并换取令牌。 */
+export interface OAuthTransaction {
+	/** PKCE 随机原始值,仅用于换取令牌,不放入授权链接。 */
+	verifier: string
+	/** 与回调参数比对,确认回调属于本次授权请求。 */
+	state: string
+	/** 授权请求的随机值;缓存本字段不等于已完成 ID Token 校验。 */
+	nonce: string
+	/** 发起授权时的回调地址,换取令牌时必须保持一致。 */
+	redirectURI: string
+}
+
+/** 读取 JSON 缓存;缺失或解析失败时返回 null,字段类型由调用方继续校验。 */
+function readCache(key: string): unknown {
+	try { return JSON.parse(sessionStorage.getItem(key) || 'null') } catch { return null }
+}
+
+/** 认证态按标签页缓存,刷新和授权跳转后可恢复;不缓存仓储业务数据。 */
+export const useStore = defineStore('app', () => {
+	// 初始化时恢复认证信息,令牌不保存 Bearer 前缀,由请求拦截器统一添加。
+	const accessToken = ref((sessionStorage.getItem(AUTH_TOKEN_KEY) || '').replace(/^Bearer\s+/i, '').trim())
+	// token 是 accessToken 的只读兼容别名,避免维护两份令牌状态。
+	const token = computed(() => accessToken.value)
+	const displayName = ref(sessionStorage.getItem(AUTH_DISPLAY_NAME_KEY) || '')
+	const cachedRoles = readCache(AUTH_ROLE_KEY)
+	const role = ref<string[]>(Array.isArray(cachedRoles) ? cachedRoles.filter((value): value is string => typeof value === 'string') : [])
+	// 恢复字段类型正确的授权会话,不设置本地过期时间。
+	// verifier 格式和回调 state 等校验由认证工具在使用会话时继续执行。
+	const cachedTransaction = readCache(transactionKey)
+	const oauthTransaction = ref<OAuthTransaction | null>(null)
+	if (cachedTransaction && typeof cachedTransaction === 'object') {
+		const value = cachedTransaction as Partial<OAuthTransaction>
+		if (typeof value.verifier === 'string' && typeof value.state === 'string' && typeof value.nonce === 'string'
+			&& typeof value.redirectURI === 'string') {
+			oauthTransaction.value = value as OAuthTransaction
+		}
+	}
+	if (!oauthTransaction.value) sessionStorage.removeItem(transactionKey)
+	// 从同一份会话派生只读字段,无会话时统一返回空字符串。
+	const codeVerifier = computed(() => oauthTransaction.value?.verifier || '')
+	const oauthState = computed(() => oauthTransaction.value?.state || '')
+	const oauthNonce = computed(() => oauthTransaction.value?.nonce || '')
+
+	// 同步写入,确保 location 跳转前缓存已落地;直接修改响应式字段也会同步。
+	watch([accessToken, displayName, role], () => {
+		for (const [key, value] of [[AUTH_TOKEN_KEY, accessToken.value], [AUTH_DISPLAY_NAME_KEY, displayName.value],
+			[AUTH_ROLE_KEY, role.value.length ? JSON.stringify(role.value) : '']] as const) {
+			if (value) sessionStorage.setItem(key, value)
+			else sessionStorage.removeItem(key)
+		}
+	}, { deep: true, flush: 'sync' })
+	// 临时会话整体保存或删除,避免刷新后只恢复部分授权字段。
+	watch(oauthTransaction, value => {
+		if (value) sessionStorage.setItem(transactionKey, JSON.stringify(value))
+		else sessionStorage.removeItem(transactionKey)
+	}, { deep: true, flush: 'sync' })
+
+	/** 保存认证信息并恢复自动认证;空令牌会抛错,缓存由同步监听器更新。 */
+	function SET_AUTH_INFO(payload: AuthPayload) {
+		const nextToken = payload.accessToken.replace(/^Bearer\s+/i, '').trim()
+		if (!nextToken) throw new Error('访问令牌不能为空')
+		accessToken.value = nextToken
+		displayName.value = payload.displayName || '已认证用户'
+		role.value = payload.role || []
+		sessionStorage.removeItem('warehouse-auth-paused')
+	}
+	/** 清空本应用认证态及对应缓存;不注销认证中心会话,也不清理 PKCE。 */
+	function CLEAR_AUTH_INFO() {
+		accessToken.value = ''
+		displayName.value = ''
+		role.value = []
+	}
+	/** 用新授权会话替换旧会话;复制字段以避免调用方通过原对象修改状态。 */
+	function SET_OAUTH_TRANSACTION(value: OAuthTransaction) { oauthTransaction.value = { ...value } }
+	/** 清空授权临时状态及缓存,供回调消费、认证失败或退出时调用。 */
+	function CLEAR_OAUTH_TEMP() { oauthTransaction.value = null }
+	return { accessToken, token, displayName, role, codeVerifier, oauthState, oauthNonce, oauthTransaction,
+		SET_AUTH_INFO, CLEAR_AUTH_INFO, SET_OAUTH_TRANSACTION, CLEAR_OAUTH_TEMP }
+})
+
+export default useStore
+
+/** 仓储业务状态独立于认证态,仅仓库名称缓存到当前会话。 */
+export const useWarehouse = defineStore('warehouse', () => {
+	const warehouseNameKey = 'warehouse-name'
+	const warehouseName = ref(sessionStorage.getItem(warehouseNameKey)?.trim() || '上海闵行仓')
+	watch(warehouseName, value => sessionStorage.setItem(warehouseNameKey, value), { immediate: true, flush: 'sync' })
+	function setWarehouseName(value?: string | null) {
+		warehouseName.value = value?.trim() || '上海闵行仓'
+	}
+	return { warehouseName, setWarehouseName }
+})

+ 693 - 0
src/style/operations.css

@@ -0,0 +1,693 @@
+:root {
+	font-family: 'Avenir Next', 'PingFang SC', 'Microsoft YaHei', sans-serif;
+	color: #273530;
+	background: #f4f6f7;
+	font-synthesis: none;
+	letter-spacing: 0;
+	--el-color-primary: #187456;
+	--el-color-primary-light-3: #549e85;
+	--el-color-primary-light-5: #8bbbaa;
+	--el-color-primary-light-7: #bdd8cd;
+	--el-color-primary-light-8: #d1e5dd;
+	--el-color-primary-light-9: #edf5f1;
+	--el-color-primary-dark-2: #11583f;
+	--el-border-radius-base: 4px;
+	color-scheme: light
+}
+
+* {
+	box-sizing: border-box
+}
+
+body {
+	margin: 0;
+	font-size: 14px
+}
+
+button,
+input,
+textarea {
+	font: inherit
+}
+
+a {
+	color: inherit;
+	text-decoration: none
+}
+
+h1 {
+	font-size: 26px;
+	line-height: 1.35;
+	margin: 6px 0;
+	font-weight: 650
+}
+
+h2 {
+	font-size: 17px;
+	margin: 0 0 20px
+}
+
+button {
+	cursor: pointer
+}
+
+#app {
+	min-height: 100vh
+}
+
+.shell {
+	display: flex;
+	min-height: 100vh
+}
+
+.sidebar {
+	width: 230px;
+	flex-shrink: 0;
+	background: #183d33;
+	color: #e6eee9;
+	position: fixed;
+	inset: 0 auto 0 0;
+	display: flex;
+	flex-direction: column;
+	padding: 28px 16px
+}
+
+.brand {
+	display: flex;
+	align-items: center;
+	gap: 12px
+}
+
+.brand-mark {
+	display: grid;
+	place-items: center;
+	width: 40px;
+	height: 40px;
+	background: #d8e9dd;
+	color: #214537;
+	font-weight: 800;
+	font-size: 19px;
+	border-radius: 6px;
+	flex-shrink: 0
+}
+
+.brand strong {
+	font-size: 17px
+}
+
+.brand small {
+	display: block;
+	font-size: 9px;
+	margin-top: 6px;
+	opacity: .7
+}
+
+.warehouse {
+	padding: 22px 12px;
+	color: #bed1c6;
+	border-bottom: 1px solid #36554a;
+	margin-bottom: 20px
+}
+
+.sidebar nav {
+	display: grid;
+	gap: 8px
+}
+
+.sidebar nav a {
+	display: flex;
+	gap: 14px;
+	align-items: center;
+	padding: 14px 12px;
+	border-radius: 4px
+}
+
+.sidebar nav a:hover {
+	background: #254b3f
+}
+
+.sidebar nav .router-link-active {
+	background: #366650;
+	color: #fff
+}
+
+.sidebar nav .el-icon {
+	font-size: 19px
+}
+
+.sidebar-bottom {
+	margin-top: auto;
+	font-size: 11px;
+	line-height: 1.9;
+	color: #a9c1b6;
+	padding: 24px 12px 0
+}
+
+.main {
+	margin-left: 230px;
+	min-width: 0;
+	flex: 1
+}
+
+.topbar {
+	height: 66px;
+	border-bottom: 1px solid #dfe5e3;
+	background: #fff;
+	padding: 0 32px;
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+	color: #77837d
+}
+
+.topbar strong {
+	color: #35443b;
+	font-weight: 500
+}
+
+.user-actions {
+	display: flex;
+	align-items: center;
+	gap: 16px
+}
+
+.page {
+	padding: 30px 32px;
+	max-width: 1920px;
+	margin: auto
+}
+
+.page-heading {
+	display: flex;
+	justify-content: space-between;
+	align-items: center;
+	gap: 16px;
+	margin-bottom: 26px
+}
+
+.eyebrow {
+	font-size: 12px;
+	color: #78867d
+}
+
+.muted,
+.field-note {
+	color: #78867d;
+	font-size: 12px
+}
+
+.green {
+	color: #197354
+}
+
+.amber {
+	color: #ad7323
+}
+
+.metrics {
+	display: grid;
+	grid-template-columns: repeat(3, minmax(0, 1fr));
+	border-block: 1px solid #dce4df;
+	background: white;
+	margin-bottom: 32px
+}
+
+.metric {
+	padding: 26px;
+	display: grid;
+	gap: 14px;
+	border-right: 1px solid #e4e9e6
+}
+
+.metric:last-child {
+	border: 0
+}
+
+.metric strong {
+	font-size: 38px;
+	line-height: 1;
+	font-variant-numeric: tabular-nums
+}
+
+.metric small {
+	color: #819087
+}
+
+.metric.featured {
+	border-top: 3px solid #387958;
+	padding-top: 23px
+}
+
+.workspace-section {
+	padding: 8px 0 28px
+}
+
+.shortcuts {
+	display: flex;
+	flex-wrap: wrap;
+	border-block: 1px solid #dce4df;
+	background: #fff
+}
+
+.shortcuts button {
+	background: none;
+	border: 0;
+	border-right: 1px solid #e4e9e6;
+	flex: 1;
+	min-width: 145px;
+	padding: 22px 16px;
+	display: flex;
+	gap: 12px;
+	align-items: center;
+	justify-content: center;
+	color: #344b3e
+}
+
+.shortcuts button:hover {
+	background: #edf5f1
+}
+
+.shortcuts .el-icon {
+	font-size: 22px;
+	color: #317d5a
+}
+
+.shortcuts strong {
+	font-weight: 500
+}
+
+.filters {
+	display: flex;
+	flex-wrap: wrap;
+	gap: 12px;
+	margin: 0 0 18px;
+	align-items: center
+}
+
+.filters>.el-input {
+	width: 220px
+}
+
+.filters>.el-select {
+	width: 155px
+}
+
+.filters .el-date-editor {
+	max-width: 360px;
+	flex-grow: 0
+}
+
+.advanced {
+	padding: 16px 0;
+	border-block: 1px solid #dde4df
+}
+
+.el-table {
+	--el-table-header-bg-color: #edf1ef;
+	--el-table-header-text-color: #56645c;
+	--el-table-row-hover-bg-color: #f2f7f4;
+	font-size: 13px
+}
+
+.el-table th.el-table__cell {
+	height: 46px;
+	font-weight: 500
+}
+
+.el-table td.el-table__cell {
+	height: 54px
+}
+
+.el-pagination {
+	justify-content: flex-end;
+	margin: 22px 0;
+	flex-wrap: wrap
+}
+
+.el-dialog {
+	max-width: calc(100% - 28px);
+	border-radius: 6px;
+	padding: 24px
+}
+
+.el-dialog__header {
+	padding-bottom: 24px
+}
+
+.el-dialog__title {
+	font-weight: 600
+}
+
+.el-dialog__body {
+	max-height: 68vh;
+	overflow: auto;
+	padding: 0 2px
+}
+
+.form-grid {
+	display: grid;
+	grid-template-columns: repeat(2, minmax(0, 1fr));
+	gap: 0 20px
+}
+
+.form-grid .full {
+	grid-column: 1/-1
+}
+
+.el-form-item .el-select,
+.el-form-item .el-input-number {
+	width: 100%
+}
+
+.field-note {
+	display: block;
+	width: 100%;
+	margin-top: 6px
+}
+
+.detail-heading {
+	margin-top: 26px
+}
+
+.el-descriptions__label {
+	width: 150px
+}
+
+.el-descriptions__content {
+	overflow-wrap: anywhere
+}
+
+.evidence {
+	display: flex;
+	gap: 12px;
+	flex-wrap: wrap;
+	max-width: 100%
+}
+
+.upload-button {
+	border: 1px dashed #a4b7ac;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	gap: 8px;
+	min-height: 72px;
+	padding: 16px;
+	color: #39765a;
+	cursor: pointer;
+	border-radius: 4px
+}
+
+.upload-button input {
+	display: none
+}
+
+.attachment {
+	display: flex;
+	gap: 6px;
+	align-items: center;
+	max-width: 100%;
+	overflow-wrap: anywhere
+}
+
+.attachment .el-image {
+	width: 72px;
+	height: 72px;
+	flex-shrink: 0
+}
+
+.attachment span {
+	max-width: 140px;
+	overflow: hidden;
+	text-overflow: ellipsis
+}
+
+.detail-images {
+	display: flex;
+	gap: 16px;
+	flex-wrap: wrap
+}
+
+.detail-images>div {
+	width: 140px
+}
+
+.detail-images .el-image {
+	width: 140px;
+	height: 110px
+}
+
+.detail-images span {
+	display: block;
+	font-size: 12px;
+	overflow-wrap: anywhere
+}
+
+.login-screen {
+	min-height: 100vh;
+	display: grid;
+	place-items: center;
+	padding: 24px;
+	background: linear-gradient(120deg, #e4eeea, #f6f7f8 60%, #e8edf1)
+}
+
+.login-panel {
+	width: 100%;
+	max-width: 420px;
+	background: #fff;
+	border: 1px solid #dce5df;
+	border-radius: 6px;
+	padding: 40px
+}
+
+.login-brand {
+	margin-bottom: 38px
+}
+
+.login-panel h1 {
+	font-size: 24px;
+	margin-bottom: 28px
+}
+
+.login-submit {
+	width: 100%;
+	margin-top: 8px
+}
+
+.login-panel footer {
+	font-size: 10px;
+	color: #88948c;
+	margin-top: 32px;
+	text-align: center
+}
+
+.el-form-item__error {
+	position: static;
+	line-height: 1.5
+}
+
+.el-form-item__content {
+	align-items: flex-start
+}
+
+.el-message-box {
+	width: 420px;
+	max-width: calc(100vw - 32px);
+	padding: 22px 24px 20px;
+	border-radius: 8px;
+	box-shadow: 0 16px 48px #10281b29
+}
+
+.el-message-box__header {
+	padding: 0 28px 16px 0
+}
+
+.el-message-box__title {
+	font-size: 18px;
+	font-weight: 600;
+	line-height: 26px
+}
+
+.el-message-box__headerbtn {
+	top: 20px;
+	right: 20px
+}
+
+.el-message-box__content {
+	padding: 0;
+	line-height: 1.7
+}
+
+.el-message-box__message {
+	min-width: 0;
+	overflow-wrap: anywhere
+}
+
+.el-message-box__btns {
+	padding-top: 24px;
+	gap: 10px
+}
+
+.el-message-box__btns .el-button {
+	min-width: 72px;
+	height: 36px;
+	margin-left: 0;
+	border-radius: 6px
+}
+
+@media(max-width:1000px) {
+	.sidebar {
+		width: 190px;
+		padding-inline: 12px
+	}
+
+	.main {
+		margin-left: 190px
+	}
+
+	.page {
+		padding: 24px 20px
+	}
+
+	.topbar {
+		padding: 0 20px
+	}
+
+	.brand strong {
+		font-size: 15px
+	}
+
+	.brand small {
+		font-size: 8px
+	}
+
+	.filters>.el-input {
+		width: 190px
+	}
+}
+
+@media(max-width:680px) {
+	.shell {
+		display: block
+	}
+
+	.sidebar {
+		position: static;
+		width: 100%;
+		padding: 16px
+	}
+
+	.sidebar .brand {
+		margin-bottom: 16px
+	}
+
+	.warehouse,
+	.sidebar-bottom {
+		display: none
+	}
+
+	.sidebar nav {
+		display: grid;
+		grid-template-columns: repeat(3, minmax(0, 1fr));
+		gap: 6px
+	}
+
+	.sidebar nav a {
+		padding: 10px 4px;
+		gap: 6px;
+		font-size: 12px;
+		justify-content: center
+	}
+
+	.sidebar nav .el-icon {
+		font-size: 16px
+	}
+
+	.main {
+		margin-left: 0
+	}
+
+	.topbar {
+		height: 52px;
+		padding: 0 16px;
+		font-size: 12px
+	}
+
+	.page {
+		padding: 22px 14px
+	}
+
+	.page-heading {
+		margin-bottom: 20px
+	}
+
+	h1 {
+		font-size: 23px
+	}
+
+	.metrics {
+		gap: 0
+	}
+
+	.metric {
+		padding: 18px 10px;
+		font-size: 12px
+	}
+
+	.metric.featured {
+		padding-top: 15px
+	}
+
+	.metric strong {
+		font-size: 28px
+	}
+
+	.metric small {
+		font-size: 10px
+	}
+
+	.shortcuts button {
+		min-width: 46%;
+		justify-content: flex-start;
+		font-size: 13px;
+		padding: 18px 12px
+	}
+
+	.filters {
+		gap: 8px
+	}
+
+	.filters>.el-input,
+	.filters>.el-select {
+		width: calc(50% - 4px)
+	}
+
+	.filters>.el-date-editor {
+		width: 100%;
+		max-width: 100%
+	}
+
+	.form-grid {
+		grid-template-columns: minmax(0, 1fr)
+	}
+
+	.el-dialog {
+		padding: 20px 16px;
+		margin-top: 5vh
+	}
+
+	.el-dialog__body {
+		max-height: 72vh
+	}
+
+	.el-drawer__body {
+		padding: 16px
+	}
+
+	.el-descriptions__label {
+		width: 115px
+	}
+
+	.login-panel {
+		padding: 30px 24px
+	}
+}

+ 883 - 0
src/style/prototype.css

@@ -0,0 +1,883 @@
+:root {
+	--ink: #17242d;
+	--muted: #6d7b83;
+	--line: #dfe5e2;
+	--canvas: #f4f6f2;
+	--panel: #fffefa;
+	--nav: #16262e;
+	--nav-soft: #23373e;
+	--mint: #47b798;
+	--mint-dark: #16735d;
+	--amber: #f6af35;
+	--red: #d95e54;
+	--shadow: 0 16px 42px rgba(28, 48, 54, .08);
+	--el-color-primary: #16735d;
+	--el-border-radius-base: 8px;
+	color: var(--ink);
+	background: var(--canvas);
+}
+
+.shell {
+	display: grid;
+	grid-template-columns: 248px minmax(0, 1fr);
+}
+
+.sidebar {
+	position: sticky;
+	top: 0;
+	width: auto;
+	height: 100vh;
+	padding: 24px 14px;
+	background: var(--nav);
+	color: #dce9e8;
+	overflow: auto;
+}
+
+.brand {
+	padding: 0 10px 28px;
+}
+
+.brand-mark {
+	width: 35px;
+	height: 35px;
+	border-radius: 0;
+	background: var(--amber);
+	color: var(--nav);
+	font: 800 20px/1 Georgia, serif;
+	transform: rotate(-5deg);
+}
+
+.brand strong {
+	color: #fff;
+	font-size: 14px;
+	font-weight: 750;
+}
+
+.brand small {
+	margin-top: 3px;
+	font-size: 11px;
+	color: #90a9ab;
+	opacity: 1;
+}
+
+.warehouse {
+	display: flex;
+	align-items: center;
+	gap: 9px;
+	margin: 0 8px 20px;
+	padding: 10px 11px;
+	border: 1px solid #ffffff1a;
+	background: #ffffff0b;
+	border-radius: 10px;
+	color: #e3efed;
+	font-size: 12px;
+}
+
+.pin {
+	width: 8px;
+	height: 8px;
+	flex-shrink: 0;
+	border-radius: 50%;
+	background: var(--mint);
+	box-shadow: 0 0 0 4px #47b79821;
+}
+
+.nav-label {
+	padding: 0 12px 8px;
+	color: #789195;
+	font-size: 10px;
+	font-weight: 700;
+}
+
+.sidebar nav {
+	gap: 5px;
+}
+
+.sidebar nav a {
+	padding: 11px 12px;
+	gap: 12px;
+	color: #b9ccce;
+	border-radius: 10px;
+}
+
+.sidebar nav a:hover {
+	color: #fff;
+	background: #ffffff0f;
+}
+
+.sidebar nav .router-link-active {
+	background: var(--nav-soft);
+	color: #fff;
+	box-shadow: inset 3px 0 0 var(--amber);
+}
+
+.sidebar nav .el-icon {
+	font-size: 17px;
+}
+
+.sidebar-bottom {
+	padding: 18px 10px 3px;
+	border-top: 1px solid #ffffff1a;
+	color: #89a0a2;
+	font-size: 12px;
+	line-height: 1.7;
+}
+
+.sidebar-bottom strong {
+	color: #dce9e8;
+	font-weight: 600;
+}
+
+.main {
+	margin-left: 0;
+}
+
+.topbar {
+	height: 76px;
+	padding: 0 34px;
+	background: #fffefaeb;
+	border-color: var(--line);
+	gap: 20px;
+}
+
+.crumb {
+	display: flex;
+	align-items: center;
+	gap: 9px;
+	font-size: 13px;
+}
+
+.crumb .slash {
+	color: #b9c4c1;
+}
+
+.topbar strong {
+	font-size: 16px;
+	font-weight: 700;
+	color: var(--ink);
+}
+
+.user-actions {
+	gap: 11px;
+}
+
+.date {
+	font-size: 12px;
+	color: var(--muted);
+}
+
+.user {
+	display: flex;
+	align-items: center;
+	gap: 9px;
+	padding-left: 13px;
+	border-left: 1px solid var(--line);
+}
+
+.avatar {
+	width: 30px;
+	height: 30px;
+	display: grid;
+	place-items: center;
+	border-radius: 50%;
+	color: #fff;
+	background: #405961;
+	font-size: 12px;
+	font-weight: 700;
+}
+
+.page {
+	padding: 30px 34px 50px;
+	max-width: none;
+}
+
+h1 {
+	margin: 0;
+	font-size: 25px;
+	line-height: 1.2;
+	font-weight: 700;
+	letter-spacing: 0;
+}
+
+.page-heading {
+	align-items: flex-end;
+	margin-bottom: 22px;
+}
+
+.page-heading p {
+	margin: 7px 0 0;
+	color: var(--muted);
+}
+
+.el-button {
+	min-height: 38px;
+	font-weight: 650;
+	border-radius: 9px;
+}
+
+.el-button.is-link,
+.el-button.is-text {
+	min-height: 29px;
+}
+
+.metrics {
+	grid-template-columns: 1.15fr 1fr 1fr 1fr;
+	gap: 15px;
+	margin-bottom: 18px;
+	border: 0;
+	background: transparent;
+}
+
+.metric {
+	position: relative;
+	min-width: 0;
+	min-height: 132px;
+	display: block;
+	padding: 20px;
+	border: 1px solid #e9ece7;
+	border-radius: 16px;
+	background: var(--panel);
+	box-shadow: var(--shadow);
+}
+
+.metric.featured {
+	padding: 20px;
+	color: #f5fbf9;
+	background: linear-gradient(132deg, #1b3c42, #1a6358);
+	border: 0;
+}
+
+.metric>span {
+	color: var(--muted);
+	font-size: 12px;
+}
+
+.metric strong {
+	display: block;
+	margin-top: 10px;
+	font: 700 32px/1 Georgia, 'Songti SC', serif;
+}
+
+.metric strong small {
+	font: 600 13px/1 'Avenir Next', 'PingFang SC', sans-serif;
+	color: inherit;
+}
+
+.metric>small {
+	display: block;
+	margin-top: 6px;
+	font-size: 12px;
+	color: var(--muted);
+}
+
+.metric.featured>span {
+	color: #b5d6cf;
+}
+
+.metric.featured>small {
+	color: #d6ece7;
+}
+
+.green {
+	color: var(--mint-dark);
+}
+
+.amber {
+	color: #b97309;
+}
+
+.home-lower {
+	display: grid;
+	grid-template-columns: 1.36fr .84fr;
+	gap: 18px;
+}
+
+.home-panel {
+	min-width: 0;
+	background: var(--panel);
+	border: 1px solid #e9ece7;
+	border-radius: 16px;
+	box-shadow: var(--shadow);
+}
+
+.panel-head {
+	display: flex;
+	justify-content: space-between;
+	align-items: center;
+	gap: 8px;
+	padding: 18px 20px 14px;
+}
+
+.panel-head h2 {
+	margin: 0;
+	font-size: 15px;
+	font-weight: 750;
+}
+
+.panel-head p {
+	margin: 4px 0 0;
+	font-size: 12px;
+	color: var(--muted);
+}
+
+.quick-grid {
+	display: grid;
+	grid-template-columns: repeat(2, minmax(0, 1fr));
+	gap: 10px;
+	padding: 0 20px 20px;
+}
+
+.quick {
+	min-height: 84px;
+	min-width: 0;
+	padding: 15px;
+	border: 1px solid var(--line);
+	border-radius: 12px;
+	background: #fff;
+	color: var(--ink);
+	text-align: left;
+	transition: border-color .18s, transform .18s;
+}
+
+.quick:hover {
+	border-color: #a4d4c8;
+	transform: translateY(-2px);
+}
+
+.quick .el-icon {
+	display: block;
+	margin-bottom: 9px;
+	font-size: 18px;
+	color: var(--mint-dark);
+}
+
+.quick strong {
+	display: block;
+	font-size: 13px;
+}
+
+.quick span {
+	display: block;
+	margin-top: 4px;
+	font-size: 11px;
+	color: var(--muted);
+}
+
+.timeline {
+	padding: 2px 20px 20px;
+}
+
+.timeline-item {
+	display: grid;
+	grid-template-columns: 58px 12px minmax(0, 1fr);
+	gap: 10px;
+	min-height: 58px;
+	padding-bottom: 12px;
+	font-size: 12px;
+	color: var(--muted);
+}
+
+.timeline-item time {
+	padding-top: 2px;
+	font-family: monospace;
+	color: #7f9090;
+}
+
+.timeline-item strong {
+	display: block;
+	margin-bottom: 4px;
+	font-size: 13px;
+	color: var(--ink);
+	overflow-wrap: anywhere;
+}
+
+.timeline-item span {
+	overflow-wrap: anywhere;
+}
+
+.timeline-dot {
+	position: relative;
+	width: 10px;
+	height: 10px;
+	margin-top: 4px;
+	border-radius: 50%;
+	background: var(--mint);
+}
+
+.timeline-dot:after {
+	content: '';
+	position: absolute;
+	top: 11px;
+	left: 4px;
+	width: 2px;
+	height: 45px;
+	background: #dfebe7;
+}
+
+.timeline-item:last-child .timeline-dot:after {
+	display: none;
+}
+
+.login-screen {
+	padding: 28px;
+	background: linear-gradient(145deg, #f7fbff, #eef5f8);
+}
+
+.login-panel {
+	width: min(520px, 100%);
+	padding: 40px 42px 36px;
+	border: 1px solid #e0e8ed;
+	border-radius: 20px;
+	background: #fff;
+	box-shadow: 0 24px 70px #2244531f;
+}
+
+.login-brand {
+	padding: 0;
+	margin-bottom: 32px;
+}
+
+.login-brand .brand-mark {
+	width: 38px;
+	height: 38px;
+	border-radius: 10px;
+	background: #2f91e9;
+	color: #fff;
+	font-size: 21px;
+	transform: none;
+}
+
+.login-brand strong {
+	color: var(--ink);
+	font-size: 18px;
+}
+
+.login-brand small {
+	color: var(--muted);
+}
+
+.login-panel h1 {
+	margin-bottom: 8px;
+}
+
+.login-sub {
+	margin: 0 0 25px;
+	color: var(--muted);
+	line-height: 1.6;
+}
+
+.login-panel .el-form-item {
+	position: relative;
+	margin-bottom: 14px;
+}
+
+.login-panel .el-form-item__label {
+	position: absolute;
+	left: 14px;
+	top: 0;
+	z-index: 1;
+	line-height: 52px;
+	font-size: 13px;
+	color: #819097;
+	margin: 0;
+	pointer-events: none;
+}
+
+.login-panel .el-input__wrapper {
+	min-height: 52px;
+	padding-left: 96px;
+	border-radius: 9px;
+}
+
+.login-panel .el-form-item.is-error {
+	margin-bottom: 30px;
+}
+
+.login-panel .login-submit {
+	height: 52px;
+	margin-top: 19px;
+	background: #3d99ed;
+	border-color: #3d99ed;
+	font-size: 17px;
+}
+
+.login-panel footer {
+	margin-top: 22px;
+	color: #9aa7ac;
+	font-size: 11px;
+	text-align: center;
+}
+
+.filters {
+	display: flex;
+	flex-wrap: wrap;
+	gap: 10px;
+	padding: 14px;
+	margin-bottom: 14px;
+	border: 1px solid #e7ebe6;
+	border-radius: 13px;
+	background: var(--panel);
+}
+
+.filters>.el-input,
+.filters>.el-select {
+	flex: 1 1 150px;
+	width: auto;
+	min-width: 0;
+}
+
+.filters>.el-input:first-child {
+	flex: 2 1 230px;
+}
+
+.filters .el-input__wrapper,
+.filters .el-select__wrapper {
+	min-height: 36px;
+}
+
+.el-table {
+	--el-table-header-bg-color: #f7f9f5;
+	--el-table-tr-bg-color: #fffefa;
+	--el-table-bg-color: #fffefa;
+	--el-table-row-hover-bg-color: #fbfcfa;
+	--el-table-border-color: #eef0ec;
+	border: 1px solid #e7ebe6;
+	border-radius: 16px;
+	box-shadow: var(--shadow);
+	font-size: 12px;
+	color: #46545a;
+}
+
+.el-table th.el-table__cell {
+	padding: 13px 0;
+	color: #78858a;
+	font-size: 11px;
+}
+
+.el-table td.el-table__cell {
+	padding: 14px 0;
+}
+
+.el-table .cell {
+	padding: 0 14px;
+}
+
+.el-table .el-button {
+	font-size: 12px;
+}
+
+.el-tag {
+	border: 0;
+	border-radius: 999px;
+	font-size: 11px;
+	font-weight: 700;
+}
+
+.el-tag:before {
+	content: '';
+	display: inline-block;
+	width: 5px;
+	height: 5px;
+	margin-right: 5px;
+	border-radius: 50%;
+	background: currentColor;
+}
+
+.el-tabs__header {
+	width: max-content;
+	max-width: 100%;
+	padding: 5px;
+	margin-bottom: 15px;
+	border: 1px solid var(--line);
+	border-radius: 10px;
+	background: #fff;
+}
+
+.el-tabs__nav-wrap:after,
+.el-tabs__active-bar {
+	display: none;
+}
+
+.el-tabs__item,
+.el-tabs__item:nth-child(2),
+.el-tabs__item:last-child {
+	height: 34px;
+	padding: 8px 12px;
+	font-size: 12px;
+	font-weight: 700;
+	color: var(--muted);
+}
+
+.el-tabs__item.is-active {
+	border-radius: 7px;
+	background: #dff3ec;
+	color: #164e43;
+}
+
+.el-overlay {
+	background: #0f1d226b;
+	backdrop-filter: blur(3px);
+}
+
+.el-dialog {
+	padding: 0;
+	border-radius: 18px;
+	background: var(--panel);
+	box-shadow: 0 28px 80px #0000003b;
+	overflow: hidden;
+}
+
+.el-dialog__header {
+	padding: 20px 23px 16px;
+	margin: 0;
+	border-bottom: 1px solid var(--line);
+}
+
+.el-dialog__title {
+	font-size: 18px;
+	font-weight: 700;
+}
+
+.el-dialog__body {
+	padding: 20px 23px;
+	max-height: 65vh;
+	overflow: auto;
+}
+
+.el-dialog__footer {
+	padding: 14px 23px;
+	border-top: 1px solid var(--line);
+}
+
+.el-form-item__label {
+	color: #536367;
+	font-size: 12px;
+	font-weight: 700;
+}
+
+.el-dialog .el-input__wrapper,
+.el-dialog .el-select__wrapper {
+	min-height: 38px;
+}
+
+.el-input.is-disabled .el-input__wrapper,
+.el-input:has(input[readonly]) .el-input__wrapper {
+	background: #f3f6f3;
+}
+
+.el-dialog .form-grid {
+	gap: 0 14px;
+}
+
+.evidence {
+	width: 100%;
+}
+
+.upload-button {
+	min-height: 76px;
+	width: 100%;
+	padding: 13px;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	gap: 5px;
+	border: 1px dashed #b8d5ce;
+	border-radius: 9px;
+	color: #39826f;
+	background: #f2faf7;
+	font-size: 12px;
+}
+
+.el-drawer {
+	background: var(--panel);
+}
+
+.el-drawer__header {
+	padding: 20px 23px 16px;
+	margin-bottom: 0;
+	border-bottom: 1px solid var(--line);
+	color: var(--ink);
+}
+
+.hint {
+	margin-bottom: 15px;
+	padding: 11px 13px;
+	color: #53676a;
+	border-left: 3px solid var(--amber);
+	background: #fff8e8;
+	font-size: 12px;
+	line-height: 1.6;
+}
+
+.kpi-row {
+	display: grid;
+	grid-template-columns: 1.15fr 1fr 1fr;
+	gap: 14px;
+	margin-bottom: 16px;
+}
+
+.mini-card {
+	padding: 16px 18px;
+	border: 1px solid #e7ebe6;
+	border-radius: 13px;
+	background: var(--panel);
+	box-shadow: var(--shadow);
+}
+
+.mini-card span {
+	color: var(--muted);
+	font-size: 12px;
+}
+
+.mini-card b {
+	display: block;
+	margin-top: 7px;
+	font: 700 24px/1.1 Georgia, 'Songti SC', serif;
+}
+
+@media (max-width: 980px) {
+	.metrics {
+		grid-template-columns: repeat(2, minmax(0, 1fr));
+	}
+
+	.home-lower {
+		grid-template-columns: minmax(0, 1fr);
+	}
+
+	.shell {
+		grid-template-columns: 72px minmax(0, 1fr);
+	}
+
+	.sidebar {
+		width: auto;
+		padding: 20px 10px;
+	}
+
+	.brand {
+		justify-content: center;
+		padding: 0 0 26px;
+	}
+
+	.brand>div,
+	.warehouse span,
+	.nav-label,
+	.sidebar nav a span:not(.el-icon),
+	.sidebar-bottom {
+		display: none;
+	}
+
+	.warehouse {
+		display: flex;
+		justify-content: center;
+		padding: 10px;
+		margin: 0 0 20px;
+	}
+
+	.sidebar nav a {
+		justify-content: center;
+		padding: 12px;
+	}
+
+	.main {
+		margin-left: 0;
+	}
+}
+
+@media (max-width: 690px) {
+
+	.metrics,
+	.kpi-row {
+		grid-template-columns: repeat(2, minmax(0, 1fr));
+	}
+
+	.metric.featured {
+		grid-column: 1 / -1;
+	}
+
+	.metric {
+		padding: 20px;
+	}
+
+	.login-panel {
+		padding: 32px 24px;
+	}
+
+	.login-brand>div {
+		display: block;
+	}
+
+	.login-brand {
+		justify-content: flex-start;
+	}
+
+	.quick-grid {
+		padding-inline: 14px;
+	}
+
+	.quick {
+		padding: 12px;
+	}
+
+	.filters>.el-input,
+	.filters>.el-select {
+		flex-basis: 130px;
+	}
+
+	.form-grid {
+		grid-template-columns: minmax(0, 1fr);
+	}
+
+	.shell {
+		display: block;
+	}
+
+	.sidebar {
+		position: fixed;
+		z-index: 12;
+		inset: auto 0 0;
+		height: 56px;
+		width: 100%;
+		flex-direction: row;
+		align-items: center;
+		padding: 5px;
+		overflow: hidden;
+	}
+
+	.sidebar .brand,
+	.warehouse,
+	.sidebar-bottom,
+	.nav-label {
+		display: none;
+	}
+
+	.sidebar nav {
+		width: 100%;
+		grid-template-columns: repeat(6, minmax(0, 1fr));
+		gap: 2px;
+	}
+
+	.sidebar nav a {
+		height: 46px;
+		padding: 4px;
+	}
+
+	.topbar {
+		height: 62px;
+		padding: 0 17px;
+		gap: 8px;
+	}
+
+	.date,
+	.user span {
+		display: none;
+	}
+
+	.page {
+		padding: 22px 16px 82px;
+	}
+
+	.page-heading {
+		align-items: flex-start;
+		flex-direction: column;
+	}
+}

+ 296 - 0
src/style/style.css

@@ -0,0 +1,296 @@
+:root {
+  --text: #6b6375;
+  --text-h: #08060d;
+  --bg: #fff;
+  --border: #e5e4e7;
+  --code-bg: #f4f3ec;
+  --accent: #aa3bff;
+  --accent-bg: rgba(170, 59, 255, 0.1);
+  --accent-border: rgba(170, 59, 255, 0.5);
+  --social-bg: rgba(244, 243, 236, 0.5);
+  --shadow:
+    rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px;
+
+  --sans: system-ui, 'Segoe UI', Roboto, sans-serif;
+  --heading: system-ui, 'Segoe UI', Roboto, sans-serif;
+  --mono: ui-monospace, Consolas, monospace;
+
+  font: 18px/145% var(--sans);
+  letter-spacing: 0.18px;
+  color-scheme: light dark;
+  color: var(--text);
+  background: var(--bg);
+  font-synthesis: none;
+  text-rendering: optimizeLegibility;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+
+  @media (max-width: 1024px) {
+    font-size: 16px;
+  }
+}
+
+@media (prefers-color-scheme: dark) {
+  :root {
+    --text: #9ca3af;
+    --text-h: #f3f4f6;
+    --bg: #16171d;
+    --border: #2e303a;
+    --code-bg: #1f2028;
+    --accent: #c084fc;
+    --accent-bg: rgba(192, 132, 252, 0.15);
+    --accent-border: rgba(192, 132, 252, 0.5);
+    --social-bg: rgba(47, 48, 58, 0.5);
+    --shadow:
+      rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px;
+  }
+
+  #social .button-icon {
+    filter: invert(1) brightness(2);
+  }
+}
+
+body {
+  margin: 0;
+}
+
+h1,
+h2 {
+  font-family: var(--heading);
+  font-weight: 500;
+  color: var(--text-h);
+}
+
+h1 {
+  font-size: 56px;
+  letter-spacing: -1.68px;
+  margin: 32px 0;
+  @media (max-width: 1024px) {
+    font-size: 36px;
+    margin: 20px 0;
+  }
+}
+h2 {
+  font-size: 24px;
+  line-height: 118%;
+  letter-spacing: -0.24px;
+  margin: 0 0 8px;
+  @media (max-width: 1024px) {
+    font-size: 20px;
+  }
+}
+p {
+  margin: 0;
+}
+
+code,
+.counter {
+  font-family: var(--mono);
+  display: inline-flex;
+  border-radius: 4px;
+  color: var(--text-h);
+}
+
+code {
+  font-size: 15px;
+  line-height: 135%;
+  padding: 4px 8px;
+  background: var(--code-bg);
+}
+
+.counter {
+  font-size: 16px;
+  padding: 5px 10px;
+  border-radius: 5px;
+  color: var(--accent);
+  background: var(--accent-bg);
+  border: 2px solid transparent;
+  transition: border-color 0.3s;
+  margin-bottom: 24px;
+
+  &:hover {
+    border-color: var(--accent-border);
+  }
+  &:focus-visible {
+    outline: 2px solid var(--accent);
+    outline-offset: 2px;
+  }
+}
+
+.hero {
+  position: relative;
+
+  .base,
+  .framework,
+  .vite {
+    inset-inline: 0;
+    margin: 0 auto;
+  }
+
+  .base {
+    width: 170px;
+    position: relative;
+    z-index: 0;
+  }
+
+  .framework,
+  .vite {
+    position: absolute;
+  }
+
+  .framework {
+    z-index: 1;
+    top: 34px;
+    height: 28px;
+    transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg)
+      scale(1.4);
+  }
+
+  .vite {
+    z-index: 0;
+    top: 107px;
+    height: 26px;
+    width: auto;
+    transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg)
+      scale(0.8);
+  }
+}
+
+#app {
+  width: 1126px;
+  max-width: 100%;
+  margin: 0 auto;
+  text-align: center;
+  border-inline: 1px solid var(--border);
+  min-height: 100svh;
+  display: flex;
+  flex-direction: column;
+  box-sizing: border-box;
+}
+
+#center {
+  display: flex;
+  flex-direction: column;
+  gap: 25px;
+  place-content: center;
+  place-items: center;
+  flex-grow: 1;
+
+  @media (max-width: 1024px) {
+    padding: 32px 20px 24px;
+    gap: 18px;
+  }
+}
+
+#next-steps {
+  display: flex;
+  border-top: 1px solid var(--border);
+  text-align: left;
+
+  & > div {
+    flex: 1 1 0;
+    padding: 32px;
+    @media (max-width: 1024px) {
+      padding: 24px 20px;
+    }
+  }
+
+  .icon {
+    margin-bottom: 16px;
+    width: 22px;
+    height: 22px;
+  }
+
+  @media (max-width: 1024px) {
+    flex-direction: column;
+    text-align: center;
+  }
+}
+
+#docs {
+  border-right: 1px solid var(--border);
+
+  @media (max-width: 1024px) {
+    border-right: none;
+    border-bottom: 1px solid var(--border);
+  }
+}
+
+#next-steps ul {
+  list-style: none;
+  padding: 0;
+  display: flex;
+  gap: 8px;
+  margin: 32px 0 0;
+
+  .logo {
+    height: 18px;
+  }
+
+  a {
+    color: var(--text-h);
+    font-size: 16px;
+    border-radius: 6px;
+    background: var(--social-bg);
+    display: flex;
+    padding: 6px 12px;
+    align-items: center;
+    gap: 8px;
+    text-decoration: none;
+    transition: box-shadow 0.3s;
+
+    &:hover {
+      box-shadow: var(--shadow);
+    }
+    .button-icon {
+      height: 18px;
+      width: 18px;
+    }
+  }
+
+  @media (max-width: 1024px) {
+    margin-top: 20px;
+    flex-wrap: wrap;
+    justify-content: center;
+
+    li {
+      flex: 1 1 calc(50% - 8px);
+    }
+
+    a {
+      width: 100%;
+      justify-content: center;
+      box-sizing: border-box;
+    }
+  }
+}
+
+#spacer {
+  height: 88px;
+  border-top: 1px solid var(--border);
+  @media (max-width: 1024px) {
+    height: 48px;
+  }
+}
+
+.ticks {
+  position: relative;
+  width: 100%;
+
+  &::before,
+  &::after {
+    content: '';
+    position: absolute;
+    top: -4.5px;
+    border: 5px solid transparent;
+  }
+
+  &::before {
+    left: 0;
+    border-left-color: var(--border);
+  }
+  &::after {
+    right: 0;
+    border-right-color: var(--border);
+  }
+}

+ 152 - 0
src/utils/auth.ts

@@ -0,0 +1,152 @@
+import CryptoJS from 'crypto-js'
+import useStore from '../store'
+import type { AuthPayload } from '../store'
+export type { AuthPayload } from '../store'
+export { AUTH_TOKEN_KEY, AUTH_DISPLAY_NAME_KEY, AUTH_ROLE_KEY } from '../store'
+
+/** 认证工具:生成授权链接、管理 PKCE 临时会话,以及读写当前标签页的登录信息。 */
+/** 是否使用本地 Mock 账号登录,由构建环境配置决定。 */
+export const mockLogin = import.meta.env.VITE_USE_MOCK === 'true'
+
+/** 保留参考实现的原始值;不作为 PKCE verifier 或随机数种子使用。 */
+export const originRandomString = 'ABCDEFGHIJ'
+
+/** 使用密码学安全随机数生成字符串,默认 128 位,可用于 PKCE verifier。 */
+export function generateRandomString(length = 128) {
+	if (!Number.isInteger(length) || length < 1 || length > 1024) throw new Error('随机字符串长度必须为 1 到 1024 的整数')
+	const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
+	let result = ''
+	while (result.length < length) {
+		const bytes = crypto.getRandomValues(new Uint8Array(length - result.length))
+		for (const byte of bytes) {
+			if (byte < 248) result += characters[byte % characters.length]
+		}
+	}
+	return result
+}
+
+/** 登录成功后保存令牌、名称和角色,并解除自动认证暂停标记。 */
+export function setAuthInfo(payload : AuthPayload) {
+	useStore().SET_AUTH_INFO(payload)
+}
+
+/** 获取访问令牌并去掉已有的 Bearer 前缀,由请求拦截器统一添加前缀。 */
+export function getToken() {
+	const store = useStore()
+	return store.accessToken || store.token || ''
+}
+
+/** 获取用于界面展示的用户名称,未登录时返回空字符串。 */
+export const getDisplayName = () => useStore().displayName || ''
+
+/** 读取缓存的角色列表;仅供前端使用,实际操作权限必须由后端校验。 */
+export function getRole() : string[] {
+	return useStore().role
+}
+
+/** 清除本应用的登录信息,不会注销认证中心的单点登录会话。 */
+export function clearAuthInfo() {
+	useStore().CLEAR_AUTH_INFO()
+}
+
+/** 优先使用配置的回调地址,否则回到站点 /login;部署时需配置 SPA 路径回退。 */
+export function getOAuthRedirectUri() {
+	return String(import.meta.env.VITE_OAUTH_REDIRECT_URI || '').trim() || `${location.origin}/login`
+}
+
+/** 读取有效的 PKCE 缓存:verifier 长度为 43~128 位,不设置本地过期时间。 */
+function readTransaction() {
+	try {
+		const value = useStore().oauthTransaction
+		if (!value || typeof value.verifier !== 'string' || !/^[A-Za-z0-9._~-]{43,128}$/.test(value.verifier)) return null
+		return { ...value }
+	} catch { return null }
+}
+
+/** 获取缓存的 code_verifier,供回调换取令牌使用;无有效缓存时返回空字符串。 */
+export const getCachedVerifier = () => readTransaction()?.verifier || ''
+/** 获取发起授权时生成的 state,用于校验回调是否属于本次登录。 */
+export const getCachedOAuthState = () => readTransaction()?.state || ''
+export const getCachedOAuthNonce = () => readTransaction()?.nonce || ''
+
+/** 读取并校验授权地址、令牌地址、客户端 ID 和回调地址,缺少配置时停止登录。 */
+export function getOAuthConfig() {
+	const productionBase = String(import.meta.env.VITE_API_AUTH_URL || '').trim()
+	const baseURL = productionBase.replace(/\/+$/, '')
+	const authorizeURL = `${baseURL}/oauth2/authorize`
+	const tokenURL = `${baseURL}/oauth2/token`
+	const responseMode = String(import.meta.env.VITE_OAUTH_RESPONSE_MODE || 'form_post').trim()
+	if (!['query', 'form_post'].includes(responseMode)) throw new Error('回调模式仅支持 query 或 form_post')
+	const redirectURI = getOAuthRedirectUri()
+	if (!authorizeURL || !tokenURL) throw new Error('请配置认证地址、令牌地址')
+	for (const value of [authorizeURL, tokenURL, redirectURI]) {
+		const url = new URL(value, location.origin)
+		if (!['http:', 'https:'].includes(url.protocol)) throw new Error('认证地址必须使用 HTTP 或 HTTPS')
+	}
+	if (new URL(redirectURI).hash) throw new Error('OAuth 回调地址不能包含 #')
+	const clientId = 'client'
+	return { authorizeURL, tokenURL, clientId, redirectURI, responseMode }
+}
+
+/** 同步计算 SHA-256 摘要并编码为 Base64URL;不是可逆加密。 */
+export function sha256(message : string) {
+	return CryptoJS.SHA256(message).toString(CryptoJS.enc.Base64)
+		.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
+}
+
+/** 保留原有方法名称,与 sha256 使用同一套 S256 计算逻辑。 */
+export const createChallenge = sha256
+
+/** 生成新的 verifier 和 state,缓存本次授权会话并返回 challenge;再次调用会覆盖旧会话。 */
+export function generatePKCE() {
+	const verifier = generateRandomString()
+	const challenge = sha256(verifier)
+	useStore().SET_OAUTH_TRANSACTION({
+		verifier, state: generateRandomString(10),
+		nonce: generateRandomString(12),
+		redirectURI: getOAuthRedirectUri()
+	})
+	return challenge
+}
+
+/** 生成授权链接;form_post 模式必须有服务端接收并转交回调,静态前端默认使用 query。 */
+export function getOAuthLoginUrl() {
+	const config = getOAuthConfig()
+	const challenge = generatePKCE()
+	const state = getCachedOAuthState()
+	const nonce = getCachedOAuthNonce()
+	const url = new URL(config.authorizeURL, location.origin)
+	
+	url.search = new URLSearchParams({
+		client_id: config.clientId,
+		redirect_uri: config.redirectURI,
+		response_type: 'code',
+		response_mode: config.responseMode,
+		scope: 'openid',
+		state, 
+		nonce, 
+		code_challenge: challenge, 
+		code_challenge_method: 'S256'
+	}).toString()
+	return url.toString()
+}
+
+/** 兼容原有 URL 大写命名。 */
+export const getOAuthLoginURL = getOAuthLoginUrl
+
+/** 校验回调 state 和回调地址,返回换取令牌所需的 verifier 与回调地址。 */
+export function consumeOAuthTransaction(state : string | null) {
+	const transaction = readTransaction()
+	// 无论校验成功与否都删除缓存,禁止重复使用;失败后需重新发起认证。
+	useStore().CLEAR_OAUTH_TEMP()
+	if (!state || !transaction || transaction.state !== state || typeof transaction.verifier !== 'string'
+		|| transaction.redirectURI !== getOAuthConfig().redirectURI) {
+		throw new Error('登录会话已失效或状态校验失败,请重新认证')
+	}
+	return { verifier: transaction.verifier as string, redirectURI: transaction.redirectURI as string }
+}
+
+/** 清除 PKCE 临时会话,用于认证失败、退出登录或登录失效后的清理。 */
+export function clearOAuthTemp() {
+	useStore().CLEAR_OAUTH_TEMP()
+}

+ 6 - 0
src/utils/entry.ts

@@ -0,0 +1,6 @@
+import type { Direction, Entry } from '../api/indexType'
+
+/** 创建空白流水单,用于弹窗表单初始化。业务规则和库存计算由后端接口负责。 */
+export function blankEntry(direction: Direction): Entry {
+  return { id: '', direction, status: 'draft', source: 'normal', skuId: '', quantity: null, orderType: 'internal', so: '', logistics: 'express', company: '', tracking: '', evidence: [], orderEvidence: [], screenshots: [], remark: '', originalId: '', createdAt: '', completedAt: '', operator: '' }
+}

+ 96 - 0
src/utils/ui.ts

@@ -0,0 +1,96 @@
+import { nextTick } from 'vue'
+import { ElMessage } from 'element-plus'
+import type { WmsLedgerFlowVO, WmsReversalPreCheckResponse } from '../api/indexType'
+
+export async function refreshAfterWrite(refresh: () => Promise<unknown>) {
+	try { await refresh() }
+	catch { ElMessage.warning('操作已成功,但数据刷新失败,请刷新列表,勿重复提交') }
+}
+
+export function reversalLimit(check: WmsReversalPreCheckResponse | undefined, originType: number | undefined) {
+	const remaining = check?.remainReversalQty
+	if (typeof remaining !== 'number' || !Number.isSafeInteger(remaining) || remaining < 0) return 0
+	if (originType === 2) return remaining
+	const stock = check?.currentStock
+	if (originType !== 1 || typeof stock !== 'number' || !Number.isSafeInteger(stock) || stock < 0) return 0
+	return Math.min(remaining, stock)
+}
+
+/**
+ * 首页“今日操作轨迹” 展示
+ * 入库数量显示为正,出库数量显示为负;冲销优先按 IN/OUT 方向确定符号,未识别方向时保留原数量符号。
+ * 数量后追加 spec 单位;冲销详情展示关联原流水和反向操作,草稿仅展示 SKU 信息及备注。
+ * @param flow 接口返回的流水记录,包含类型、状态、SKU、数量、单位及冲销信息。
+ * @returns title 为操作状态文案,detail 为详情文案;流水编号由页面另行拼接。
+ */
+export function flowActivity(flow: WmsLedgerFlowVO) {
+	const reversal = flow.flowType === 3 || flow.flowSource === 'REVERSAL'
+	const names: Record<number, string> = { 1: '入库', 2: '出库', 3: '冲销' }
+	const title = flow.status === 0 ? '草稿保存' : `${reversal ? '冲销' : names[flow.flowType ?? 0] || '流水'}完成`
+	const direction = flow.reversalDirection === 1 ? 'IN' : flow.reversalDirection === 2 ? 'OUT' : String(flow.reversalDirection ?? '')
+	let signedQty = flow.qty
+	if (typeof signedQty === 'number' && Number.isFinite(signedQty)) {
+		if (reversal) {
+			if (direction === 'IN') signedQty = Math.abs(signedQty)
+			if (direction === 'OUT') signedQty = -Math.abs(signedQty)
+		} else {
+			if (flow.flowType === 1) signedQty = Math.abs(signedQty)
+			if (flow.flowType === 2) signedQty = -Math.abs(signedQty)
+		}
+	}
+	const unit = flow.spec?.trim()
+	const quantity = typeof signedQty === 'number' && Number.isFinite(signedQty)
+		? `${signedQty > 0 ? '+' : ''}${signedQty}${unit ? ` ${unit}` : ''}` : ''
+	const sku = [flow.skuId, flow.skuName].filter(Boolean).join(' · ')
+	if (flow.status === 0) return { title, detail: [sku, flow.remark].filter(Boolean).join(' · ') || '—' }
+	if (reversal) {
+		const action = direction === 'IN' ? '反向入库' : direction === 'OUT' ? '反向出库'
+			: (flow.qty ?? 0) > 0 ? '反向入库' : (flow.qty ?? 0) < 0 ? '反向出库' : '冲销'
+		return { title, detail: [`${flow.originFlowNo || '—'} ${action}`, quantity].filter(Boolean).join(' · ') }
+	}
+	return { title, detail: [sku, quantity].filter(Boolean).join(' · ') || '—' }
+}
+
+/** 将时间转换为中文相对描述:今天、昨天、前天,更早则显示 YYYY-MM-DD。 */
+export function relativeDay(value?: string) {
+	if (!value) return ''
+	const date = new Date(value)
+	if (Number.isNaN(date.getTime())) return ''
+	const now = new Date()
+	const sameDay = (first: Date, second: Date) =>
+		first.getFullYear() === second.getFullYear() && first.getMonth() === second.getMonth() && first.getDate() === second.getDate()
+	if (sameDay(date, now)) return '今天'
+	const yesterday = new Date(now)
+	yesterday.setDate(now.getDate() - 1)
+	if (sameDay(date, yesterday)) return '昨天'
+	const beforeYesterday = new Date(now)
+	beforeYesterday.setDate(now.getDate() - 2)
+	if (sameDay(date, beforeYesterday)) return '前天'
+	return dateTime(value, 'YYYY-MM-DD HH:mm:ss').slice(0, 10)
+}
+/** 按上海时区显示时间;默认返回年月日时分秒,也可指定 HH:mm;空值显示占位符。 */
+export function dateTime(value ?: string, format: 'YYYY-MM-DD HH:mm:ss' | 'HH:mm' = 'YYYY-MM-DD HH:mm:ss') {
+	if (!value) return '—'
+	const parts = new Intl.DateTimeFormat('zh-CN', { timeZone: 'Asia/Shanghai', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hourCycle: 'h23' }).formatToParts(new Date(value))
+	const fields = Object.fromEntries(parts.map(part => [part.type, part.value]))
+	const time = `${fields.hour}:${fields.minute}`
+	return format === 'HH:mm' ? time : `${fields.year}-${fields.month}-${fields.day} ${time}:${fields.second}`
+}
+/** 显示通用错误提示:优先使用接口错误信息,其次使用异常信息,最后使用默认文案。 */
+export function report(error : any) {
+	ElMessage.error(error.response?.data?.msg || error.response?.data?.message || error.message || '请求失败,请重试')
+}
+/**
+ * 处理表单提交失败,回填字段错误并定位弹窗内的第一个错误控件。
+ * @param error 请求异常,接口可通过 response.data.fields 返回字段名与错误文案的映射。
+ * @param target 表单字段错误的响应式状态,通常传入 ref<Record<string, string>>。
+ */
+export async function fieldErrors(error : any, target : { value : Record<string, string> }) {
+	target.value = error.response?.data?.fields || {}
+	ElMessage.error(Object.keys(target.value).length ? '提交失败,请修正标红字段后重试' : (error.response?.data?.msg || error.response?.data?.message || error.message || '操作失败,请重试'))
+	// 等待 Vue 将字段错误渲染到页面,再聚焦并滚动到首个错误控件。
+	await nextTick()
+	const element = document.querySelector<HTMLElement>('.el-dialog .is-error input, .el-dialog .is-error textarea, .el-dialog .is-error [tabindex], .el-dialog .is-error label.upload-button')
+	element?.focus(); 
+	element?.scrollIntoView({ block: 'center', behavior: 'smooth' })
+}

+ 81 - 0
src/utils/upload.ts

@@ -0,0 +1,81 @@
+import { api } from '../api'
+
+const DEFAULT_COS_UPLOAD_HOST = 'https://ranking-1309648802.cos.ap-shanghai.myqcloud.com'
+const imageExtensions: Record<string, string> = { 'image/png': '.png', 'image/jpeg': '.jpg', 'image/webp': '.webp' }
+const signatureFields = ['key', 'q-sign-algorithm', 'q-ak', 'q-key-time', 'q-signature', 'policy'] as const
+
+export function createUploadId(): string {
+	if (typeof globalThis.crypto.randomUUID === 'function') return globalThis.crypto.randomUUID()
+	const bytes = globalThis.crypto.getRandomValues(new Uint8Array(16))
+	bytes[6] = (bytes[6]! & 0x0f) | 0x40
+	bytes[8] = (bytes[8]! & 0x3f) | 0x80
+	const hex = Array.from(bytes, byte => byte.toString(16).padStart(2, '0')).join('')
+	return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`
+}
+
+/**
+ * 解析 COS 上传地址:优先使用凭证中的地址,其次使用环境变量,最后回退到默认地址。
+ * 仅接受 HTTP/HTTPS 地址,返回其源地址(协议、域名和端口),不保留路径、查询参数及片段。
+ * @param signature 后端返回的 COS 直传凭证。
+ * @returns 用于直传和拼接文件 URL 的源地址。
+ * @throws 地址无法解析或协议不受支持时抛出错误。
+ */
+function uploadHost(signature: Record<string, unknown>) {
+	const configured = import.meta.env.VITE_COS_UPLOAD_URL
+	const returned = ['uploadUrl', 'cosUrl', 'url', 'host', 'domain', 'bucketUrl', 'fileUrl'].map(field => signature[field]).find(value => typeof value === 'string' && value.trim())
+	const value = String(returned || configured || DEFAULT_COS_UPLOAD_HOST).trim()
+	try {
+		const url = new URL(value)
+		if (url.protocol !== 'https:' && url.protocol !== 'http:') throw new Error()
+		return url.origin
+	} catch {
+		throw new Error('上传签名中的 COS 地址格式不正确')
+	}
+}
+
+/**
+ * 获取后端签名后,将非空 PNG、JPEG 或 WebP 图片通过表单直接上传至 COS。
+ * @param file 待上传的图片文件,格式根据 MIME 类型校验。
+ * @param options 上传配置;keyPrefix 为对象路径前缀,默认使用 wms/INBOUND。
+ * @returns 上传成功后由 COS 源地址和签名 key 拼接的文件 URL,可用于业务附件提交。
+ * @throws 文件校验、签名解析、必填字段校验或上传失败时抛出错误,由调用方处理。
+ */
+export async function uploadImage(file: File, options: { keyPrefix?: string } = {}): Promise<string> {
+	const extension = imageExtensions[file.type]
+	if (!extension) throw new Error('仅支持 PNG、JPEG、WebP 图片')
+	if (!file.size) throw new Error('不能上传空图片')
+
+	// 按前缀、本地年月日和随机文件名申请签名,实际上传路径以后端返回的 key 为准。
+	const prefix = (options.keyPrefix ?? 'wms/INBOUND').replace(/^\/+|\/+$/g, '')
+	const now = new Date()
+	const date = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}`
+	const filename = [prefix, date, `${createUploadId()}${extension}`].filter(Boolean).join('/')
+	// 兼容接口直接返回凭证对象或返回 JSON 字符串两种格式。
+	let params: unknown = await api.wmsApi.file.oss(filename)
+	if (typeof params === 'string') {
+		try { params = JSON.parse(params) }
+		catch { throw new Error('上传签名格式不正确') }
+	}
+	if (!params || typeof params !== 'object' || Array.isArray(params)) throw new Error('上传签名格式不正确')
+	const signature = params as Record<string, unknown>
+	const cosUploadHost = uploadHost(signature)
+	const form = new FormData()
+	// 保留签名字段原值,并在使用临时凭证时附带安全令牌。
+	for (const field of signatureFields) {
+		const value = signature[field]
+		if (typeof value !== 'string' || !value.trim()) throw new Error(`上传签名缺少有效字段:${field}`)
+		form.append(field, value)
+	}
+	if (typeof signature['x-cos-security-token'] === 'string') {
+		form.append('x-cos-security-token', signature['x-cos-security-token'])
+	}
+	form.append('success_action_status', '200')
+	form.append('file', file, file.name)
+	// COS 直传不携带 Cookie,表单的 Content-Type 和边界由浏览器自动生成。
+	const response = await fetch(cosUploadHost, { method: 'POST', body: form, credentials: 'omit' })
+	if (!response.ok) throw new Error(`图片上传失败,状态码:${response.status}`)
+
+	// 逐段编码对象路径中的中文和空格,保留目录分隔符。
+	const encodedKey = (signature.key as string).replace(/^\/+/, '').split('/').map(encodeURIComponent).join('/')
+	return `${cosUploadHost}/${encodedKey}`
+}

+ 9 - 0
src/utils/uploads.ts

@@ -0,0 +1,9 @@
+import { provide, ref, type InjectionKey, type Ref } from 'vue'
+
+export const uploadsKey: InjectionKey<Ref<number>> = Symbol('uploads')
+
+export function useUploads() {
+  const pendingUploads = ref(0)
+  provide(uploadsKey, pendingUploads)
+  return pendingUploads
+}

+ 390 - 0
src/views/Dashboard.vue

@@ -0,0 +1,390 @@
+<script setup lang="ts">
+	import { onMounted, ref } from 'vue'
+	import { useRouter } from 'vue-router'
+	import { Download, Upload, Box, Collection, Plus } from '@element-plus/icons-vue'
+	import { api } from '../api'
+	import { useWarehouse } from '../store'
+	import type { WmsDashboardResponse, WmsDashboardTodayFlowVO } from '../api/indexType'
+	import { dateTime, report } from '../utils/ui'
+	const router = useRouter()
+	const warehouse = useWarehouse()
+	const summary = ref<WmsDashboardResponse | null>(null)
+	const updatedAt = ref<string>()
+	const todayFlows = ref<WmsDashboardTodayFlowVO[]>([])
+	const flowsLoading = ref(false)
+	const flowsFailed = ref(false)
+	async function loadTodayFlows() {
+		flowsLoading.value = true
+		flowsFailed.value = false
+		try {
+			todayFlows.value = await api.wmsApi.dashboard.todayTrace()
+		} catch (error) {
+			flowsFailed.value = true
+			report(error)
+		} finally {
+			flowsLoading.value = false
+		}
+	}
+	async function loadData() {
+		updatedAt.value = new Date().toISOString()
+		summary.value = await api.wmsApi.dashboard.summary()
+		warehouse.setWarehouseName(summary.value?.warehouseName)
+	}
+	onMounted(() => {
+		loadData().catch(report)
+		loadTodayFlows()
+	})
+	const shortcuts = [
+		{ title: '新建入库', note: '照片凭证与物流信息必填', path: '/inbound?new=1', icon: Download },
+		{ title: '新建出库', note: '记录出库数量、订单类型与物流', path: '/outbound?new=1', icon: Upload },
+		{ title: '维护 SKU', note: '规格不同,skuID 必须不同', path: '/sku', icon: Box },
+		{ title: '基础资料', note: '种类、运动类型、品牌', path: '/master', icon: Collection },
+	]
+</script>
+<template>
+	<div class="dashboard">
+		<div class="page-heading">
+			<div>
+				<h1>首页 / 工作台</h1>
+				<p>{{ warehouse.warehouseName }} · 实际库存台账 · 数据截至 {{ dateTime(updatedAt, 'HH:mm') }}</p>
+			</div><el-button type="primary" :icon="Plus" @click="router.push('/inbound?new=1')">新建入库</el-button>
+		</div>
+		<div class="metrics">
+			<article class="metric featured">
+				<span>已启用 SKU</span><strong>{{ summary?.skuCount ?? '0' }}
+				<small>个</small></strong><small>按规格独立核算,不跨单位汇总</small>
+			</article>
+			<article class="metric">
+				<span>今日入库</span><strong class="green">{{ summary?.todayInboundCount ?? '0' }}
+				<small>笔</small></strong><small>已完成流水,不含草稿</small>
+			</article>
+			<article class="metric">
+				<span>今日出库</span><strong class="amber">{{ summary?.todayOutboundCount ?? '0' }}
+				<small>笔</small></strong><small>含 {{ summary?.internalOrderCount ?? '0' }} 笔内部订单</small>
+			</article>
+			<article class="metric">
+				<span>待处理草稿</span><strong>{{ summary?.pendingDraftCount ?? '0' }}<small>笔</small></strong><small>不影响当前库存</small>
+			</article>
+		</div>
+		<div class="home-lower">
+			<section class="home-panel">
+				<div class="panel-head">
+					<div>
+						<h2>快捷作业</h2>
+						<p>先完成基础资料,再进行实物进出仓记录</p>
+					</div>
+				</div>
+				<div class="quick-grid">
+					<button v-for="item in shortcuts" :key="item.title" class="quick"
+						@click="router.push(item.path)"><el-icon>
+							<component :is="item.icon" />
+						</el-icon><strong>{{ item.title }}</strong><span>{{ item.note }}</span>
+					</button>
+				</div>
+			</section>
+			<section class="home-panel">
+				<div class="panel-head">
+					<div>
+						<h2>今日操作轨迹</h2>
+						<p>已完成流水</p>
+					</div>
+					<el-button link type="primary" @click="router.push('/ledger')">查看台账</el-button>
+				</div>
+				<div class="timeline">
+					<p v-if="flowsLoading" role="status">加载中...</p>
+					<p v-else-if="flowsFailed" role="status">操作轨迹加载失败<el-button link type="primary" @click="loadTodayFlows">重试</el-button></p>
+					<p v-else-if="!todayFlows.length">暂无操作轨迹数据</p>
+					<template v-else>
+						<div v-for="(flow, index) in todayFlows" :key="index" class="timeline-item">
+							<time>{{ flow.time || '—' }}</time>
+							<i class="timeline-dot" aria-hidden="true" />
+							<div>
+								<strong>{{ flow.label || '—' }}</strong>
+								<span>{{ flow.detail || '—' }}</span>
+							</div>
+							</div>
+					</template>
+				</div>
+			</section>
+		</div>
+	</div>
+</template>
+
+<style scoped>
+	.dashboard {
+		min-height: calc(100vh - 160px);
+		color: #23343c;
+		letter-spacing: 0;
+	}
+
+	.dashboard .page-heading {
+		margin-bottom: 20px;
+		gap: 16px;
+	}
+
+	.dashboard h1 {
+		font-size: 24px;
+		font-weight: 700;
+		margin: 0 0 6px;
+	}
+
+	.dashboard p {
+		margin: 0;
+		color: #809099;
+		font-size: 12px;
+	}
+
+	.mock-label {
+		margin-left: 8px;
+		font-size: 10px;
+		color: #81948d;
+	}
+
+	.dashboard .metrics {
+		display: grid;
+		grid-template-columns: 1.15fr repeat(3, minmax(0, 1fr));
+		gap: 14px;
+		border: 0;
+		background: none;
+		margin: 0 0 18px;
+	}
+
+	.dashboard .metric {
+		min-width: 0;
+		min-height: 120px;
+		padding: 20px 18px;
+		display: flex;
+		flex-direction: column;
+		gap: 5px;
+		border: 1px solid #e8ede9;
+		border-radius: 14px;
+		background: #fffefa;
+		box-shadow: 0 14px 30px #203e3410;
+	}
+
+	.dashboard .metric>span {
+		font-size: 12px;
+		color: #7e8e94;
+	}
+
+	.dashboard .metric strong {
+		font-size: 30px;
+		line-height: 1.15;
+	}
+
+	.dashboard .metric small {
+		font-size: 11px;
+		color: #7e8e94;
+	}
+
+	.dashboard .metric strong small {
+		font-size: 24px;
+		color: inherit;
+		font-weight: 400;
+	}
+
+	.dashboard .metric.featured {
+		background: linear-gradient(115deg, #1b3e45, #1a6054);
+		border: 0;
+		color: #fff;
+	}
+
+	.dashboard .metric.featured::after {
+		display: none;
+	}
+
+	.dashboard .metric.featured>span,
+	.dashboard .metric.featured>small {
+		color: #d1e7df;
+	}
+
+	.dashboard .green {
+		color: #17775e;
+	}
+
+	.dashboard .amber {
+		color: #b8790c;
+	}
+
+	.dashboard .home-lower {
+		display: grid;
+		grid-template-columns: minmax(0, 1.62fr) minmax(0, 1fr);
+		gap: 18px;
+	}
+
+	.dashboard .home-panel {
+		min-width: 0;
+		min-height: 350px;
+		padding: 20px;
+		background: #fffefa;
+		border: 1px solid #e8ede9;
+		border-radius: 14px;
+		box-shadow: 0 18px 32px #203e3410;
+	}
+
+	.dashboard .panel-head {
+		display: flex;
+		align-items: center;
+		justify-content: space-between;
+		gap: 12px;
+		margin-bottom: 14px;
+		padding: 0 0 14px;
+	}
+
+	.dashboard h2 {
+		font-size: 14px;
+		margin: 0 0 5px;
+	}
+
+	.dashboard .quick-grid {
+		display: grid;
+		grid-template-columns: repeat(2, minmax(0, 1fr));
+		gap: 10px;
+	}
+	.quick-grid{
+		padding: 0 0;
+	}
+
+	.dashboard .quick {
+		display: flex;
+		flex-direction: column;
+		align-items: flex-start;
+		min-width: 0;
+		min-height: 92px;
+		padding: 15px;
+		gap: 5px;
+		text-align: left;
+		background: #fff;
+		border: 1px solid #dce5e3;
+		border-radius: 8px;
+		color: #24343a;
+	}
+
+	.dashboard .quick .el-icon {
+		color: #6ba799;
+		font-size: 18px;
+		margin-bottom: 5px;
+	}
+
+	.dashboard .quick strong {
+		font-size: 12px;
+	}
+
+	.dashboard .quick span {
+		font-size: 11px;
+		color: #7c8d96;
+		overflow-wrap: anywhere;
+	}
+
+	.dashboard .quick:hover {
+		border-color: #4d9e88;
+		background: #f5faf7;
+	}
+
+	.dashboard .quick:focus-visible {
+		outline: 2px solid #17775e;
+		outline-offset: 2px;
+	}
+
+	.dashboard .timeline {
+		/* padding-top: 2px; */
+		padding: 0 0;
+	}
+
+	.dashboard .timeline-item {
+		position: relative;
+		display: grid;
+		grid-template-columns: 66px 10px minmax(0, 1fr);
+		gap: 14px;
+		min-height: 54px;
+		padding: 0 0 12px;
+		box-sizing: border-box;
+	}
+
+	.dashboard .timeline-item time {
+		font-size: 11px;
+		color: #8b9b9f;
+		padding-top: 2px;
+	}
+
+	.dashboard .timeline-dot {
+		width: 9px;
+		height: 9px;
+		border-radius: 50%;
+		background: #48b698;
+		margin-top: 5px;
+		position: relative;
+	}
+
+	.dashboard .timeline-item:not(:last-child)::before {
+		content: '';
+		position: absolute;
+		top: 16px;
+		bottom: -3px;
+		left: 84px;
+		width: 1px;
+		background: #d4e7df;
+	}
+
+	.dashboard .timeline-item strong {
+		display: block;
+		overflow-wrap: anywhere;
+		font-size: 12px;
+		line-height: 20px;
+	}
+
+	.dashboard .timeline-item span {
+		display: block;
+		color: #81919a;
+		font-size: 11px;
+		line-height: 18px;
+		overflow-wrap: anywhere;
+	}
+
+	@media (max-width: 1100px) {
+		.dashboard .home-lower {
+			grid-template-columns: 1fr;
+		}
+
+		.dashboard .home-panel {
+			min-height: auto;
+		}
+
+		.dashboard .metrics {
+			grid-template-columns: repeat(2, minmax(0, 1fr));
+		}
+	}
+
+	@media (max-width: 520px) {
+		.dashboard h1 {
+			font-size: 20px;
+		}
+
+		.dashboard .page-heading {
+			align-items: flex-start;
+			flex-wrap: wrap;
+		}
+
+		.dashboard .metric {
+			padding: 14px;
+		}
+
+		.dashboard .home-panel {
+			padding: 16px;
+		}
+
+		.dashboard .quick-grid {
+			grid-template-columns: 1fr;
+		}
+
+		.dashboard .timeline-item {
+			grid-template-columns: 40px 10px minmax(0, 1fr);
+			gap: 10px;
+			padding-bottom: 12px;
+		}
+
+		.dashboard .timeline-item:not(:last-child)::before {
+			left: 54px;
+		}
+	}
+</style>

+ 366 - 0
src/views/Inbound.vue

@@ -0,0 +1,366 @@
+<script setup lang="ts">
+import { computed, onMounted, reactive, ref } from 'vue'
+import { useRoute } from 'vue-router'
+import { Plus } from '@element-plus/icons-vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import type { FormInstance, FormRules } from 'element-plus'
+import EvidenceUpload from '../components/EvidenceUpload.vue'
+import EntryAudit from '../components/EntryAudit.vue'
+import { api } from '../api'
+import { blankEntry } from '../utils/entry'
+import { useUploads } from '../utils/uploads'
+import type { Attachment, Entry } from '../api/indexType'
+import type { WmsInboundRecordResponse, WmsInboundSaveRequest, WmsSkuResponse, WmsReversalPreCheckResponse } from '../api/indexType'
+import { dateTime, fieldErrors, report, reversalLimit, refreshAfterWrite } from '../utils/ui'
+
+type InboundEntry = Entry & { recordId?: number; alreadyReversedQty: number }
+const inboundRows = ref<InboundEntry[]>([])
+const skuOptions = ref<WmsSkuResponse[]>([])
+const pendingUploads = useUploads()
+let skuSearchVersion = 0
+const total = ref(0)
+const pageSize = 10
+const logisticsTypes: Record<string, number> = { express: 1, instant: 2, other: 3 }
+const preCheck = ref<WmsReversalPreCheckResponse>()
+function toEntry(item: WmsInboundRecordResponse): InboundEntry {
+  const files = (item.attachments || []).map((file, index) => ({ id: String(file.id ?? index), url: file.fileUrl || '', name: file.fileName || '凭证', fileType: file.fileType }))
+  return {
+    ...blankEntry('inbound'), recordId: item.id, id: item.flowNo || String(item.id ?? ''),
+    alreadyReversedQty: item.alreadyReversedQty ?? 0,
+    status: item.status === 1 ? 'completed' : 'draft', skuId: item.skuId || '', quantity: item.qty ?? null,
+    logistics: ({ 1: 'express', 2: 'instant', 3: 'other' } as Record<number, string>)[item.logisticsType ?? 1] || '',
+    company: item.expressCompany || '', tracking: item.expressNo || '', remark: item.remark || '',
+    createdAt: item.createTime || '', operator: item.updateUser || item.createUser || '',
+    evidence: files.filter(file => file.fileType !== 2),
+    screenshots: files.filter(file => file.fileType === 2),
+    snapshot: { id: item.skuId || '', name: item.skuNameSnapshot || '', spec: item.specSnapshot || '',
+      config: item.configDetailSnapshot || '', categoryId: '', sportId: '', brandId: '', remark: '',
+      enabled: true, createdAt: '', operator: '', category: '', sport: '', brand: '' },
+  }
+}
+function recordId(entry: InboundEntry) {
+  if (entry.recordId == null) throw new Error('接口未返回入库记录 ID')
+  return entry.recordId
+}
+async function loadSkus(keyword = '') {
+  const version = ++skuSearchVersion
+  const response = await api.wmsApi.sku.search({ keyword: keyword.trim() || undefined })
+  if (version !== skuSearchVersion) return
+  const records = Array.isArray(response) ? response : response.records ?? response.list
+  if (!Array.isArray(records)) throw new Error('SKU 搜索结果格式不符合预期')
+  const selected = skuOptions.value.find(item => item.skuId === form.value.skuId)
+  skuOptions.value = records.filter(item => item.status === 1)
+  if (selected && !skuOptions.value.some(item => item.skuId === selected.skuId)) skuOptions.value.unshift(selected)
+}
+function searchSkus(keyword: string) { loadSkus(keyword).catch(report) }
+const loading = ref(false)
+async function loadData() {
+  loading.value = true
+  try {
+    const filter = applied.value
+    let dates = filter.period === 'custom' ? filter.dates : []
+    if (filter.period === 'today' || filter.period === 'week') {
+      const now = new Date()
+      const end = dateTime(now.toISOString()).slice(0, 10)
+      if (filter.period === 'week') now.setDate(now.getDate() - 6)
+      dates = [dateTime(now.toISOString()).slice(0, 10), end]
+    }
+    const response = await api.wmsApi.inbound.page({
+      pageNo: page.value, 
+      pageSize,
+	  keyword: filter.keyword.trim() || undefined,
+      status: filter.status ? (filter.status === 'completed' ? 1 : 0) : undefined,
+      logisticsType: logisticsTypes[filter.logistics],
+      startTime: dates[0] ? `${dates[0]} 00:00:00` : undefined,
+      endTime: dates[1] ? `${dates[1]} 23:59:59` : undefined,
+    })
+    inboundRows.value = (response.rows as WmsInboundRecordResponse[]).map(toEntry)
+    total.value = response.total
+  }
+  finally { loading.value = false }
+}
+const route = useRoute()
+const filters = reactive({ period: '', keyword: '', status: '', logistics: '', dates: [] as string[] })
+const applied = ref({ ...filters })
+const page = ref(1)
+const dialog = ref(false)
+const form = ref<InboundEntry>({ ...blankEntry('inbound'), alreadyReversedQty: 0 })
+const errors = ref<Record<string, string>>({})
+const busy = ref(false)
+const formRef = ref<FormInstance>()
+const rules = computed<FormRules<InboundEntry>>(() => ({
+  skuId: [{ required: true, whitespace: true, message: '请选择 SKU', trigger: 'change' }],
+  quantity: [{ required: true, type: 'integer', min: 1, message: '请填写大于 0 的整数入库数量', trigger: 'change' }],
+  logistics: [{ required: true, message: '请选择物流方式', trigger: 'change' }],
+  company: form.value.logistics === 'express' ? [{ required: true, whitespace: true, message: '请填写快递公司', trigger: 'blur' }] : [],
+  tracking: form.value.logistics === 'express' ? [{ required: true, whitespace: true, message: '请填写快递单号', trigger: 'blur' }] : [],
+  evidence: form.value.logistics === 'express' ? [{ required: true, type: 'array', min: 1, message: '请上传照片 / 凭证', trigger: 'change' }] : [],
+  screenshots: ['instant', 'other'].includes(form.value.logistics) ? [{ required: true, type: 'array', min: 1, message: '请上传物流截图', trigger: 'change' }] : [],
+}))
+function validateAttachment(field: 'evidence' | 'screenshots') {
+  void formRef.value?.validateField(field).catch(() => false)
+}
+const detail = ref<InboundEntry>()
+const selectedSku = computed(() => skuOptions.value.find(item => item.skuId === form.value.skuId))
+const reverseDialog = ref(false)
+const original = ref<InboundEntry>()
+const reverseQty = ref<number | null>(null)
+const reverseEvidence = ref<Attachment[]>([])
+const reverseRemark = ref('')
+const reverseFormRef = ref<FormInstance>()
+const reverseForm = computed(() => ({ quantity: reverseQty.value, evidence: reverseEvidence.value }))
+const reverseMax = computed(() => reversalLimit(preCheck.value, 1))
+const reverseRules = computed<FormRules>(() => ({
+  quantity: [{ required: true, type: 'integer', min: 1, max: reverseMax.value, message: `冲销数量须为 1 至 ${reverseMax.value} 的整数`, trigger: 'change' }],
+  evidence: [{ required: true, type: 'array', min: 1, message: '请上传照片 / 凭证', trigger: 'change' }],
+}))
+function validateReverseEvidence() {
+  void reverseFormRef.value?.validateField('evidence').catch(() => false)
+}
+const logisticsName = (value: string) => ({ express: '快递', instant: '闪送', other: '其他' }[value] || '待填写')
+const entrySku = (entry: Entry) => entry.snapshot
+const attachments = (entry: Entry) => [...entry.evidence, ...entry.orderEvidence, ...entry.screenshots]
+const canReverse = (entry: InboundEntry) => entry.status === 'completed' && entry.alreadyReversedQty < Number(entry.quantity)
+const reversalStatus = (entry: InboundEntry) => entry.alreadyReversedQty === 0 ? '未冲销' : entry.alreadyReversedQty >= Number(entry.quantity) ? '已全部冲销' : '部分冲销'
+onMounted(async () => { try { await loadData(); if (route.query.new) open() } catch (error) { report(error) } })
+function filterRows() { applied.value = { ...filters, dates: [...(filters.dates || [])] }; page.value = 1; loadData().catch(report) }
+async function fetchDetail(entry: InboundEntry) {
+  return toEntry(await api.wmsApi.inbound.detail({ id: recordId(entry) }) as WmsInboundRecordResponse)
+}
+async function showDetail(entry: InboundEntry) {
+  try { detail.value = await fetchDetail(entry) } catch (error) { report(error) }
+}
+async function open(entry?: InboundEntry) {
+  try {
+    form.value = entry ? await fetchDetail(entry) : { ...blankEntry('inbound'), alreadyReversedQty: 0 }
+    if (entry) recordId(form.value)
+    skuOptions.value = form.value.skuId ? [{ skuId: form.value.skuId, productName: form.value.snapshot?.name, spec: form.value.snapshot?.spec, status: 1 }] : []
+    await loadSkus()
+    errors.value = {}; dialog.value = true
+  } catch (error) { report(error) }
+}
+function changeLogistics() { form.value.company = ''; form.value.tracking = ''; form.value.screenshots = []; form.value.evidence = []; errors.value = {}; formRef.value?.clearValidate(['company', 'tracking', 'evidence', 'screenshots']) }
+async function save(submit: boolean) {
+  if (busy.value || pendingUploads.value) return
+  busy.value = true; errors.value = {}
+  try {
+    formRef.value?.clearValidate()
+    const valid = await (submit ? formRef.value?.validate() : formRef.value?.validateField('skuId'))?.catch(() => false)
+    if (!valid) return
+    const files = [...form.value.evidence, ...form.value.screenshots]
+    const payload: WmsInboundSaveRequest = {
+      skuId: form.value.skuId || undefined, qty: form.value.quantity ?? undefined,
+      logisticsType: logisticsTypes[form.value.logistics], expressCompany: form.value.company,
+      expressNo: form.value.tracking, remark: form.value.remark,
+      fileUrls: files.map(file => file.url), fileNames: files.map(file => file.name),
+      fileTypes: [...form.value.evidence.map(() => 1), ...form.value.screenshots.map(() => 2)],
+    }
+    if (submit) await api.wmsApi.inbound.submit({ ...payload, draftId: form.value.id ? recordId(form.value) : undefined })
+    else if (form.value.id) await api.wmsApi.inbound.updateDraft({ ...payload, id: recordId(form.value) })
+    else await api.wmsApi.inbound.saveDraft(payload)
+    dialog.value = false
+    ElMessage.success(submit ? '入库提交成功' : '草稿已保存')
+    await refreshAfterWrite(loadData)
+  } catch (error) { await fieldErrors(error, errors) }
+  finally { busy.value = false }
+}
+async function remove(entry: InboundEntry) {
+  try {
+    await ElMessageBox.confirm(`确认删除草稿 ${entry.id}?`, '删除草稿', { type: 'warning', confirmButtonText: '删除', cancelButtonText: '取消' })
+    await api.wmsApi.inbound.deleteDraft({ id: recordId(entry) }); ElMessage.success('草稿已删除'); await refreshAfterWrite(loadData)
+  } catch (error) { if (error !== 'cancel' && error !== 'close') report(error) }
+}
+async function openReverse(entry: InboundEntry) {
+  try {
+    preCheck.value = await api.wmsApi.reversal.preCheck({ originFlowNo: entry.id, originFlowType: 1 }) as WmsReversalPreCheckResponse
+    if (!reverseMax.value) throw new Error('当前无可冲销数量,或预检数据不完整,请刷新后重试')
+    original.value = entry; reverseQty.value = reverseMax.value
+    reverseEvidence.value = []; reverseRemark.value = ''; errors.value = {}; detail.value = undefined; reverseDialog.value = true
+  } catch (error) { report(error) }
+}
+async function submitReverse() {
+  if (busy.value || pendingUploads.value || !original.value) return
+  busy.value = true; errors.value = {}
+  try {
+    const valid = await reverseFormRef.value?.validate().catch(() => false)
+    if (!valid) return
+    await api.wmsApi.reversal.submit({ originFlowNo: original.value.id, originFlowType: 1, qty: reverseQty.value ?? undefined,
+      fileUrls: reverseEvidence.value.map(file => file.url), fileNames: reverseEvidence.value.map(file => file.name), reversalReason: reverseRemark.value })
+    reverseDialog.value = false; ElMessage.success('冲销成功'); await refreshAfterWrite(loadData)
+  } catch (error) { await fieldErrors(error, errors) } finally { busy.value = false }
+}
+</script>
+
+<template>
+  <div v-loading="loading" class="inbound-page">
+    <div class="page-heading inbound-heading"><div><h1>入库管理</h1><p>提交即增加上海闵行仓库存;每笔完成单据均不可修改或删除。</p></div><el-button type="primary" :icon="Plus" @click="open()">新建入库</el-button></div>
+    <el-config-provider :empty-values="[null, undefined]">
+    <form class="inbound-filters" @submit.prevent="filterRows">
+      <el-select v-model="filters.period" aria-label="创建时间筛选"><el-option label="创建时间:全部" value="" /><el-option label="创建时间:今天" value="today" /><el-option label="创建时间:近 7 天" value="week" /><el-option label="自定义时间" value="custom" /></el-select>
+      <el-input v-model="filters.keyword" placeholder="skuID / 商品" aria-label="商品名称" clearable />
+	  <!-- <el-select v-model="filters.status" placeholder="单据状态" aria-label="单据状态筛选" clearable><el-option label="已完成" value="completed" /><el-option label="草稿" value="draft" /></el-select> -->
+      <!-- <el-select v-model="filters.logistics" placeholder="物流方式" aria-label="物流方式筛选" clearable><el-option v-for="value in ['express', 'instant', 'other']" :key="value" :value="value" :label="logisticsName(value)" /></el-select> -->
+	  <el-select v-model="filters.status" aria-label="单据状态筛选"><el-option label="单据状态:全部" value="" /><el-option label="已完成" value="completed" /><el-option label="草稿" value="draft" /></el-select>
+      <el-select v-model="filters.logistics" aria-label="物流方式筛选"><el-option label="物流方式:全部" value="" /><el-option v-for="value in ['express', 'instant', 'other']" :key="value" :value="value" :label="logisticsName(value)" /></el-select>
+      <el-button native-type="submit">筛选</el-button>
+      <el-date-picker v-if="filters.period === 'custom'" v-model="filters.dates" class="inbound-date-range" type="daterange" value-format="YYYY-MM-DD" start-placeholder="创建开始日期" end-placeholder="结束日期" />
+    </form>
+    </el-config-provider>
+    <div class="inbound-legend"><span>单据状态</span><span class="inbound-status">已完成</span><span class="inbound-status draft">草稿</span><span>草稿不进入库存台账</span></div>
+    <el-table :data="inboundRows" class="inbound-table" row-key="id" empty-text="暂无入库单据">
+      <el-table-column label="入库流水编号" width="120"><template #default="{ row }"><b class="inbound-code">{{ row.id }}</b></template></el-table-column>
+      <el-table-column label="单据状态" width="105"><template #default="{ row }"><span class="inbound-status" :class="{ draft: row.status === 'draft' }">{{ row.status === 'completed' ? '已完成' : '草稿' }}</span></template></el-table-column>
+      <el-table-column label="创建时间" width="180"><template #default="{ row }">{{ dateTime(row.createdAt) }}</template></el-table-column>
+      <el-table-column label="skuID / 商品" min-width="330"><template #default="{ row }"><strong class="inbound-product">{{ row.skuId || '待选择 SKU' }}<template v-if="entrySku(row)"> · {{ entrySku(row)?.name }}</template></strong></template></el-table-column>
+      <el-table-column label="规格" width="65"><template #default="{ row }">{{ entrySku(row)?.spec || '—' }}</template></el-table-column>
+      <el-table-column label="数量" width="80"><template #default="{ row }"><b v-if="row.quantity !== null">{{ row.quantity }}</b><span v-else>待填写</span></template></el-table-column>
+      <el-table-column label="物流信息" min-width="220"><template #default="{ row }">{{ row.tracking ? `${row.company || '—'} · ${row.tracking}` : (row.screenshots.length ? '物流截图' : '—') }}</template></el-table-column>
+      <el-table-column prop="operator" label="操作人" width="100"><template #default="{ row }">{{ row.operator || '—' }}</template></el-table-column>
+      <el-table-column prop="remark" label="备注" min-width="135" show-overflow-tooltip />
+      <el-table-column label="操作" width="190" fixed="right">
+			<template #default="{ row }">
+				<template v-if="row.status === 'draft'">
+					<el-button link type="primary" @click="open(row)">编辑</el-button>
+					<el-button link type="danger" @click="remove(row)">删除</el-button>
+				</template>
+					<template v-else>
+						<el-button link type="primary" @click="showDetail(row)">查看详情</el-button>
+						<!-- <el-button v-if="canReverse(row)" link type="warning" @click="openReverse(row)">冲销 / 更正</el-button> -->
+					</template>
+			</template>
+	  </el-table-column>
+    </el-table>
+    <el-pagination v-model:current-page="page" :total="total" :page-size="pageSize" layout="total, prev, pager, next" @current-change="loadData().catch(report)" />
+
+    <el-dialog v-model="dialog" class="inbound-dialog inbound-create" modal-class="inbound-overlay" width="680px" align-center :close-on-click-modal="false" :close-on-press-escape="!busy" :show-close="!busy" @closed="formRef?.clearValidate()">
+      <template #header="{ titleId, titleClass }"><h2 :id="titleId" :class="titleClass">{{ form.id ? '编辑入库草稿' : '新建入库' }}</h2><p class="inbound-subtitle">提交后立即增加上海闵行仓库存</p></template>
+      <el-alert v-if="errors.form" :title="errors.form" type="error" :closable="false" />
+      <el-form ref="formRef" :model="form" :rules="rules" :validate-on-rule-change="false" label-position="top" :disabled="busy || pendingUploads > 0">
+        <h3 class="inbound-section">基础信息</h3>
+        <div class="inbound-grid">
+          <el-form-item label="仓库"><el-input model-value="上海闵行仓" disabled /></el-form-item>
+          <el-form-item label="创建时间"><el-input :model-value="form.createdAt ? dateTime(form.createdAt) : '提交时由系统自动生成(精确到秒)'" disabled /></el-form-item>
+          <el-form-item label="选择 SKU" prop="skuId" required class="inbound-full" :error="errors.skuId">
+            <el-select v-model="form.skuId" filterable remote :remote-method="searchSkus" clearable placeholder="从系统 SKU 中搜索选择" no-data-text="暂无启用 SKU" no-match-text="未找到匹配的 SKU"><el-option v-for="sku in skuOptions.filter(item => item.status === 1)" :key="sku.skuId" :value="sku.skuId || ''" :label="`${sku.skuId} · ${sku.productName} · ${sku.spec}`" /></el-select>
+            <div class="inbound-field-hint">按系统生成的 skuID、商品名称或规格关键词搜索</div>
+          </el-form-item>
+          <el-form-item label="入库数量" prop="quantity" required :error="errors.quantity"><el-input-number v-model="form.quantity" :controls="false" :min="1" :precision="0" placeholder="填写大于 0 的整数" /></el-form-item>
+          <el-form-item label="商品规格"><el-input :model-value="selectedSku?.spec || ''" placeholder="选择 SKU 后自动带出" disabled /></el-form-item>
+        </div>
+        <h3 class="inbound-section">物流与凭证</h3>
+        <el-form-item label="物流方式" prop="logistics" required :error="errors.logistics"><el-select v-model="form.logistics" @change="changeLogistics"><el-option v-for="value in ['express', 'instant', 'other']" :key="value" :value="value" :label="logisticsName(value)" /></el-select></el-form-item>
+        <template v-if="form.logistics === 'express'">
+          <el-form-item label="快递公司" prop="company" required :error="errors.company"><el-input v-model="form.company" placeholder="例如:顺丰速运" /></el-form-item>
+          <el-form-item label="快递单号" prop="tracking" required :error="errors.tracking"><el-input v-model="form.tracking" placeholder="填写快递单号" /></el-form-item>
+          <el-form-item label="照片 / 凭证" prop="evidence" required :error="errors.evidence"><EvidenceUpload :key="form.logistics" :active="dialog" :disabled="busy" v-model="form.evidence" @update:model-value="validateAttachment('evidence')" /></el-form-item>
+        </template>
+        <el-form-item v-else label="物流截图" prop="screenshots" required :error="errors.screenshots"><EvidenceUpload :key="form.logistics" :active="dialog" :disabled="busy" v-model="form.screenshots" @update:model-value="validateAttachment('screenshots')" /></el-form-item>
+        <el-form-item label="备注"><el-input v-model="form.remark" type="textarea" :rows="3" placeholder="选填:填写本次入库说明" /></el-form-item>
+      </el-form>
+      <template #footer><el-button :disabled="busy" @click="dialog = false">取消</el-button><el-button type="warning" plain :disabled="busy || pendingUploads > 0" @click="save(false)">保存草稿</el-button><el-button type="primary" :loading="busy" :disabled="pendingUploads > 0" @click="save(true)">提交入库</el-button></template>
+    </el-dialog>
+
+    <el-dialog :model-value="!!detail" class="inbound-dialog" modal-class="inbound-overlay" width="760px" align-center @close="detail = undefined">
+      <template #header="{ titleId, titleClass }"><h2 :id="titleId" :class="titleClass">流水详情 · {{ detail?.id }}</h2><p v-if="detail" class="inbound-subtitle">已完成 · 原流水状态:{{ reversalStatus(detail) }} · 创建时间:{{ dateTime(detail.createdAt) }}</p></template>
+      <template v-if="detail">
+        <EntryAudit :entry="detail" :reversed-qty="detail.alreadyReversedQty" />
+        <div class="inbound-grid inbound-detail-fields">
+          <div><label>skuID / 商品</label><div class="inbound-readonly">{{ detail.skuId }} · {{ entrySku(detail)?.name }}</div></div>
+          <div><label>库存变动</label><div class="inbound-readonly">{{ detail.direction === 'inbound' ? '入库 +' : '出库 −' }}{{ detail.quantity }}</div></div>
+          <div><label>{{ detail.direction === 'inbound' ? '入库数量' : '出库数量' }}</label><div class="inbound-readonly">{{ detail.quantity }} {{ entrySku(detail)?.spec }}</div></div>
+        </div>
+        <div class="inbound-evidence-label">照片 / 凭证</div>
+        <div class="inbound-evidence">
+          <span v-if="!attachments(detail).length">暂无凭证</span>
+          <div v-for="(file, index) in attachments(detail)" :key="file.id" class="inbound-evidence-item"><el-image :src="file.url" :preview-src-list="attachments(detail).map(item => item.url)" :initial-index="index" preview-teleported fit="cover" /></div>
+          <span v-if="detail.tracking">{{ detail.company }} · 快递单号 {{ detail.tracking }}</span>
+        </div>
+      </template>
+      <template #footer><el-button @click="detail = undefined">关闭</el-button><el-button v-if="detail && canReverse(detail)" type="warning" plain @click="openReverse(detail)">冲销 / 更正</el-button></template>
+    </el-dialog>
+
+    <el-dialog v-model="reverseDialog" class="inbound-dialog inbound-reverse-dialog" modal-class="inbound-overlay" width="620px" align-center :close-on-click-modal="false" :close-on-press-escape="!busy" :show-close="!busy" @closed="reverseFormRef?.clearValidate()">
+      <template #header="{ titleId, titleClass }"><h2 :id="titleId" :class="titleClass">冲销 / 更正</h2><p class="inbound-subtitle">将生成与原流水方向相反的新流水,原记录不会被删除。</p></template>
+      <template v-if="original">
+        <!-- <EntryAudit :entry="original" reversal :reversed-qty="preCheck?.alreadyReversedQty ?? original.alreadyReversedQty" :current-stock="preCheck?.currentStock ?? null" /> -->
+        <el-alert class="inbound-reverse-notice" type="warning" :closable="false" show-icon :title="`原流水:${original.id} · 剩余可冲销数量 ${preCheck?.remainReversalQty ?? '—'}。冲销完成后,请另行录入正确的出入库单。`" />
+        <el-alert v-if="errors.form" :title="errors.form" type="error" :closable="false" />
+        <el-form ref="reverseFormRef" :model="reverseForm" :rules="reverseRules" label-position="top" :disabled="busy || pendingUploads > 0" class="inbound-reverse-form" @submit.prevent="submitReverse">
+          <div class="inbound-reverse-grid">
+            <el-form-item label="冲销方向"><el-input :model-value="original.direction === 'inbound' ? '出库(反向生成)' : '入库(反向生成)'" disabled /></el-form-item>
+            <el-form-item label="冲销数量" prop="quantity" required :error="errors.quantity"><el-input-number v-model="reverseQty" :controls="false" :min="1" :max="reverseMax" :precision="0" /></el-form-item>
+          </div>
+          <p v-if="original.direction === 'inbound'" class="inbound-stock-note">当前可用库存:{{ preCheck?.currentStock ?? '—' }} {{ entrySku(original)?.spec }}</p>
+          <el-form-item label="照片 / 凭证" prop="evidence" required :error="errors.evidence"><EvidenceUpload :active="reverseDialog" :disabled="busy" v-model="reverseEvidence" @update:model-value="validateReverseEvidence" /></el-form-item>
+          <el-form-item label="冲销原因"><el-input v-model="reverseRemark" type="textarea" :rows="3" placeholder="选填:说明冲销原因" /></el-form-item>
+        </el-form>
+      </template>
+      <template #footer><el-button :disabled="busy" @click="reverseDialog = false">取消</el-button><el-button type="primary" :loading="busy" :disabled="pendingUploads > 0" @click="submitReverse">提交冲销</el-button></template>
+    </el-dialog>
+  </div>
+</template>
+
+<style scoped>
+.inbound-heading { margin-bottom: 22px; }
+.inbound-heading h1 { font-size: 24px; letter-spacing: 0; }
+.inbound-heading p { margin-top: 8px; color: #7c8c92; font-size: 13px; }
+.inbound-heading > .el-button { height: 37px; border-radius: 8px; }
+.inbound-filters { display: grid; grid-template-columns: 1fr 1.7fr 1fr 1fr auto; align-items: center; gap: 10px; padding: 13px; border: 1px solid #e5ebe7; background: #fff; border-radius: 8px; }
+.inbound-filters > * { min-width: 0; }
+.inbound-date-range { grid-column: 1 / -1; max-width: 100%; }
+.inbound-legend { display: flex; flex-wrap: wrap; align-items: center; gap: 9px; margin: 15px 0 12px; font-size: 12px; color: #839196; }
+.inbound-status { display: inline-flex; align-items: center; gap: 4px; background: #e4f5ee; color: #218570; border-radius: 20px; padding: 3px 8px; font-size: 10px; font-weight: 600; line-height: 14px; white-space: nowrap; }
+.inbound-status::before { content: ''; width: 5px; height: 5px; border-radius: 50%; background: currentColor; }
+.inbound-status.draft { background: #fff1d4; color: #ac771c; }
+.inbound-table { --el-table-header-bg-color: #f6f8f6; --el-table-border-color: #edf1ed; --el-table-header-text-color: #819095; --el-table-text-color: #5c6e74; border: 1px solid #e8ede8; border-radius: 8px; box-shadow: 0 15px 30px #233b2d08; }
+.inbound-table :deep(th.el-table__cell) { height: 40px; font-size: 11px; }
+.inbound-table :deep(td.el-table__cell) { height: 53px; font-size: 12px; }
+.inbound-table :deep(.cell) { padding: 0 13px; }
+.inbound-table :deep(.el-button) { font-size: 12px; }
+.inbound-code { color: #486671; font-size: 11px; }
+.inbound-product { color: #26383e; overflow-wrap: anywhere; }
+.inbound-subtitle { margin: 5px 0 0; font-size: 12px; color: #84969c; line-height: 1.6; }
+.inbound-grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 0 14px; }
+.inbound-full { grid-column: 1 / -1; }
+.inbound-section { margin: 4px 0 12px; font-size: 12px; color: #7c9296; font-weight: 600; }
+.inbound-field-hint { margin-top: 6px; color: #84969c; font-size: 12px; line-height: 1.6; }
+.inbound-detail-fields { gap: 16px 14px; }
+.inbound-detail-fields label, .inbound-evidence-label { display: block; font-size: 12px; font-weight: 600; color: #556d74; margin-bottom: 6px; }
+.inbound-readonly { min-height: 38px; padding: 8px 10px; border: 1px solid #dce5e1; border-radius: 7px; background: #f2f6f4; color: #80969d; font-size: 14px; line-height: 20px; overflow-wrap: anywhere; }
+.inbound-evidence-label { margin-top: 16px; }
+.inbound-evidence { display: flex; justify-content: center; align-items: center; flex-wrap: wrap; gap: 12px; padding: 15px; min-height: 76px; border: 1px dashed #bdddd3; border-radius: 7px; background: #f1faf6; color: #458e7c; font-size: 12px; overflow-wrap: anywhere; }
+.inbound-evidence-item { display: flex; align-items: center; gap: 8px; max-width: 100%; }
+.inbound-evidence-item .el-image { width: 46px; height: 42px; border-radius: 4px; flex-shrink: 0; cursor: zoom-in; }
+.inbound-related, .inbound-reverse-form { margin-top: 16px; }
+.inbound-reverse-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
+.inbound-stock-note { margin: -2px 0 14px; color: #7b8d91; font-size: 12px; }
+@media (max-width: 1000px) { .inbound-filters { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
+@media (max-width: 600px) { .inbound-heading { flex-direction: column; align-items: flex-start; gap: 14px; } .inbound-grid { grid-template-columns: minmax(0, 1fr); } .inbound-heading h1 { font-size: 22px; } }
+</style>
+
+<style>
+.inbound-overlay { backdrop-filter: blur(3px); background: #1c2f3566; }
+.el-dialog.inbound-dialog { padding: 0; max-width: calc(100vw - 32px); border-radius: 16px; overflow: hidden; background: #fffefb; box-shadow: 0 28px 70px #12252233; }
+.inbound-dialog .el-dialog__header { padding: 20px 22px 14px; margin: 0; border-bottom: 1px solid #dfe7e3; }
+.inbound-dialog .el-dialog__title { margin: 0; padding-right: 32px; font-size: 18px; font-weight: 700; color: #1c3039; line-height: 26px; }
+.inbound-dialog .el-dialog__headerbtn { top: 24px; right: 22px; width: 28px; height: 28px; border-radius: 7px; background: #eef3f1; }
+.inbound-dialog .el-dialog__body { padding: 20px 22px; max-height: calc(100dvh - 220px); overflow-y: auto; }
+.inbound-dialog .el-form-item { margin-bottom: 12px; }
+.inbound-dialog .el-form-item__label { font-size: 12px; font-weight: 600; color: #566d73; margin-bottom: 5px; line-height: 18px; }
+.inbound-dialog .el-input__wrapper, .inbound-dialog .el-select__wrapper { min-height: 34px; border-radius: 7px; box-shadow: 0 0 0 1px #dce5e1 inset; }
+.inbound-dialog .el-input.is-disabled .el-input__wrapper { background: #f2f6f4; }
+.inbound-dialog .el-input.is-disabled .el-input__inner { -webkit-text-fill-color: #82959c; }
+.inbound-dialog .el-input-number { width: 100%; }
+.inbound-dialog .el-input-number .el-input__inner { text-align: left; }
+.inbound-dialog .el-textarea__inner { border-radius: 7px; }
+.inbound-dialog .el-dialog__footer { border-top: 1px solid #dfe7e3; padding: 14px 22px; }
+.inbound-dialog .el-dialog__footer .el-button { height: 36px; border-radius: 8px; }
+.inbound-dialog .el-alert { margin-bottom: 14px; }
+.inbound-reverse-dialog .el-dialog__body { padding-top: 20px; }
+.inbound-reverse-dialog .inbound-reverse-notice { border: 1px solid #ffe0a5; background: #fffaec; }
+.inbound-reverse-dialog .inbound-reverse-notice .el-alert__title { font-size: 12px; line-height: 1.6; }
+.inbound-reverse-dialog .inbound-reverse-form { margin-top: 18px; }
+.inbound-dialog .evidence { width: 100%; }
+.inbound-dialog .upload-button { display: flex; justify-content: center; align-items: center; gap: 7px; width: 100%; min-height: 66px; box-sizing: border-box; border: 1px dashed #bdddd3; border-radius: 7px; background: #f1faf6; color: #458e7c; font-size: 12px; }
+@media (max-width: 600px) { .inbound-reverse-grid { grid-template-columns: minmax(0, 1fr); gap: 0; } }
+</style>

+ 283 - 0
src/views/Ledger.vue

@@ -0,0 +1,283 @@
+<script setup lang="ts">
+import { computed, onMounted, reactive, ref } from 'vue'
+import { ElMessage } from 'element-plus'
+import type { FormInstance, FormRules } from 'element-plus'
+import EvidenceUpload from '../components/EvidenceUpload.vue'
+import { api } from '../api'
+import { blankEntry } from '../utils/entry'
+import { useUploads } from '../utils/uploads'
+import type { Attachment, Entry } from '../api/indexType'
+import type { WmsLedgerAdminSummaryResponse, WmsLedgerFlowVO, WmsReversalPreCheckResponse, WmsReversalRecordResponse } from '../api/indexType'
+import { dateTime, fieldErrors, report, reversalLimit, refreshAfterWrite } from '../utils/ui'
+
+type LedgerEntry = Entry & { recordId?: number; flowType?: number; alreadyReversedQty: number; reversalState?: number; originFlowDetail?: string }
+type LedgerDetail = WmsLedgerFlowVO
+const entries = ref<LedgerEntry[]>([])
+const pendingUploads = useUploads()
+const summary = ref<WmsLedgerAdminSummaryResponse>()
+async function loadSummary() {
+  summary.value = undefined
+  summary.value = await api.wmsApi.ledger.adminSummary()
+}
+const total = ref(0)
+const pageSize = 10
+const preCheck = ref<WmsReversalPreCheckResponse>()
+const reversalStates: Record<string, number> = { '未冲销': 0, '部分冲销': 1, '已全额冲销': 2 }
+function toEntry(item: LedgerDetail): LedgerEntry {
+  const direction = item.flowType === 1 ? 'inbound' : item.flowType === 2 ? 'outbound'
+    : item.reversalDirection === 1 ? 'inbound' : item.reversalDirection === 2 ? 'outbound'
+    : (item.qty ?? 0) >= 0 ? 'inbound' : 'outbound'
+  return {
+    ...blankEntry(direction), id: item.flowNo || '', recordId: item.recordId, flowType: item.flowType,
+    status: item.status === 1 ? 'completed' : 'draft', source: item.flowType === 3 || item.flowSource === 'REVERSAL' ? 'reversal' : 'normal',
+    skuId: item.skuId || '', quantity: item.qty == null ? null : Math.abs(item.qty),
+    alreadyReversedQty: item.alreadyReversedQty ?? 0, reversalState: item.reversalStatus,
+    originalId: item.originFlowNo || '', createdAt: item.createTime || '', operator: item.updateUser || item.createUser || '',
+    originFlowDetail: item.originFlowDetail,
+    remark: item.reversalReason || item.remark || '', orderType: item.orderType === 1 ? 'internal' : item.orderType === 2 ? 'external' : '',
+    so: item.soNo || '', company: item.expressCompany || '', tracking: item.expressNo || '',
+    evidence: (item.attachments || []).map((file, index) => ({ id: String(file.id ?? index), url: file.fileUrl || '', name: file.fileName || '凭证' })),
+    snapshot: { id: item.skuId || '', name: item.skuName || '', spec: item.spec || '', config: item.configDetail || '',
+      categoryId: '', sportId: '', brandId: '', category: '', sport: '', brand: '', remark: '', enabled: true, createdAt: '', operator: '' },
+  }
+}
+const loading = ref(false)
+async function loadData() {
+  loading.value = true
+  try {
+    const filter = applied.value
+    const result = await api.wmsApi.ledger.flowList({
+      pageNo: page.value, 
+	  pageSize,
+	  status: 1,
+      keyword: filter.keyword.trim() || undefined,
+      flowType: filter.direction ? Number(filter.direction) : undefined,
+      flowSource: filter.source || undefined, reversalStatus: reversalStates[filter.reversal],
+    })
+    entries.value = (result.rows as WmsLedgerFlowVO[]).map(toEntry)
+    total.value = result.total
+  }
+  finally { loading.value = false }
+}
+const filters = reactive({ keyword: '', direction: '', reversal: '', source: '' })
+const applied = ref({ ...filters })
+const page = ref(1)
+const detail = ref<LedgerEntry>()
+const reverseDialog = ref(false)
+const original = ref<LedgerEntry>()
+const reverseQty = ref<number | null>(1)
+const reverseEvidence = ref<Attachment[]>([])
+const reverseRemark = ref('')
+const reverseFormRef = ref<FormInstance>()
+const reverseForm = computed(() => ({ quantity: reverseQty.value, evidence: reverseEvidence.value }))
+const reverseMax = computed(() => reversalLimit(preCheck.value, original.value?.flowType))
+const reverseRules = computed<FormRules>(() => ({
+  quantity: [{ required: true, type: 'integer', min: 1, max: reverseMax.value, message: `冲销数量须为 1 至 ${reverseMax.value} 的整数`, trigger: 'change' }],
+  evidence: [{ required: true, type: 'array', min: 1, message: '请上传照片 / 凭证', trigger: 'change' }],
+}))
+function validateReverseEvidence() {
+  void reverseFormRef.value?.validateField('evidence').catch(() => false)
+}
+const errors = ref<Record<string, string>>({})
+const busy = ref(false)
+const entrySku = (entry: Entry) => entry.snapshot
+const attachments = (entry: Entry) => [...entry.evidence, ...entry.orderEvidence, ...entry.screenshots]
+const remaining = (entry: LedgerEntry) => Number(entry.quantity) - entry.alreadyReversedQty
+const canReverse = (entry: LedgerEntry) => entry.status === 'completed' && entry.source === 'normal' && entry.reversalState !== 2 && remaining(entry) > 0
+const reversalStatus = (entry: LedgerEntry) => entry.source === 'reversal' ? '—' : entry.reversalState === 2 ? '已全额冲销' : entry.reversalState === 1 ? '部分冲销' : '未冲销'
+const directionName = (entry: Entry) => entry.direction === 'inbound' ? '入库' : '出库'
+onMounted(() => {
+  loadData().catch(report)
+  loadSummary().catch(report)
+})
+function filterRows() { applied.value = { ...filters }; page.value = 1; loadData().catch(report) }
+async function showDetail(entry: LedgerEntry) {
+  try {
+    if (entry.recordId == null || entry.flowType == null) throw new Error('接口未返回流水类型或记录 ID')
+    if (entry.flowType === 3 || entry.source === 'reversal') {
+      const result = await api.wmsApi.reversal.detail({ id: entry.recordId }) as WmsReversalRecordResponse
+      if (!result || typeof result !== 'object') throw new Error('冲销详情格式不符合预期')
+      const direction = result.reversalDirection === 1 ? 'inbound' : result.reversalDirection === 2 ? 'outbound'
+        : result.originFlowType === 1 ? 'outbound' : result.originFlowType === 2 ? 'inbound' : entry.direction
+      detail.value = {
+        ...toEntry({
+          ...result, flowNo: result.flowNo || entry.id, recordId: result.id ?? entry.recordId,
+          flowType: 3, flowSource: 'REVERSAL', status: result.status ?? 1,
+          skuName: result.skuNameSnapshot ?? entry.snapshot?.name,
+          spec: result.specSnapshot ?? entry.snapshot?.spec,
+          configDetail: result.configDetailSnapshot ?? entry.snapshot?.config,
+        }), direction,
+      }
+      return
+    }
+    const result = await api.wmsApi.ledger.flowDetail({ flowNo: entry.id, flowType: entry.flowType, recordId: entry.recordId })
+    if (!result || typeof result !== 'object') throw new Error('流水详情格式不符合预期')
+    detail.value = toEntry({ flowNo: entry.id, recordId: entry.recordId, flowType: entry.flowType, status: 1, ...result } as LedgerDetail)
+  } catch (error) { report(error) }
+}
+async function openReverse(entry: LedgerEntry) {
+  if (!canReverse(entry)) return
+  try {
+    preCheck.value = await api.wmsApi.reversal.preCheck({ originFlowNo: entry.id, originFlowType: entry.flowType }) as WmsReversalPreCheckResponse
+    original.value = entry
+    if (!reverseMax.value) throw new Error('当前无可冲销数量,或预检数据不完整,请刷新后重试')
+    reverseQty.value = reverseMax.value; reverseEvidence.value = []; reverseRemark.value = ''
+    errors.value = {}; detail.value = undefined; reverseDialog.value = true
+  } catch (error) { report(error) }
+}
+async function submitReverse() {
+  if (busy.value || pendingUploads.value || !original.value) return
+  busy.value = true; errors.value = {}
+  try {
+    const valid = await reverseFormRef.value?.validate().catch(() => false)
+    if (!valid) return
+    await api.wmsApi.reversal.submit({ originFlowNo: original.value.id, originFlowType: original.value.flowType,
+      qty: reverseQty.value ?? undefined, fileUrls: reverseEvidence.value.map(file => file.url),
+      fileNames: reverseEvidence.value.map(file => file.name), reversalReason: reverseRemark.value })
+    reverseDialog.value = false
+    ElMessage.success('冲销成功')
+    await refreshAfterWrite(() => Promise.all([loadData(), loadSummary()]))
+  } catch (error) { await fieldErrors(error, errors) } finally { busy.value = false }
+}
+</script>
+
+<template>
+  <div v-loading="loading" class="ledger-page">
+    <div class="page-heading ledger-heading"><div><h1>库存台账</h1><p>只展示已完成流水;实际库存按已完成入库减已完成出库计算。</p></div><el-button @click="$router.push('/sku')">查看 SKU</el-button></div>
+    <div class="ledger-stats"><article><span>当前查询范围</span><strong>{{ summary?.warehouseName ?? summary?.queryScope ?? '—' }}</strong></article><article><span>库存总记录</span><strong>{{ summary?.totalStockCount ?? '—' }} SKU</strong></article><article><span>今日变动流水</span><strong class="ledger-positive">{{ summary?.todayFlowCount ?? '—' }} 笔</strong></article></div>
+    <el-config-provider :empty-values="[null, undefined]">
+      <form class="ledger-filters" @submit.prevent="filterRows">
+        <el-input v-model="filters.keyword" placeholder="skuID / 商品 / 订单号" aria-label="skuID / 商品名称 / 订单号" clearable />
+        <el-select v-model="filters.direction" aria-label="流水类型"><el-option label="流水类型:全部" value="" /><el-option label="入库" value="1" /><el-option label="出库" value="2" /><el-option label="冲销" value="3" /></el-select>
+        <el-select model-value="completed" aria-label="单据状态"><el-option label="单据状态:已完成" value="completed" /></el-select>
+        <el-select v-model="filters.reversal" aria-label="原流水状态"><el-option label="原流水状态:全部" value="" /><el-option v-for="value in ['未冲销', '部分冲销', '已全额冲销']" :key="value" :value="value" :label="value" /></el-select>
+        <el-select v-model="filters.source" aria-label="流水来源"><el-option label="流水来源:全部" value="" /><el-option label="正常录入" value="NORMAL" /><el-option label="冲销" value="REVERSAL" /></el-select>
+        <el-button native-type="submit">筛选</el-button>
+      </form>
+    </el-config-provider>
+    <p class="ledger-note">“原流水状态”用于查看冲销进度;冲销流水自身显示为“—”,通过流水来源和原流水编号追溯。</p>
+    <el-table :data="entries" row-key="id" class="ledger-table" empty-text="暂无已完成流水">
+      <el-table-column label="流水编号" width="108"><template #default="{ row }"><b class="ledger-code">{{ row.id }}</b></template></el-table-column>
+      <el-table-column label="创建时间" width="164"><template #default="{ row }">{{ dateTime(row.createdAt) }}</template></el-table-column>
+      <el-table-column label="入 / 出库" width="82"><template #default="{ row }">{{ directionName(row) }}</template></el-table-column>
+      <el-table-column label="单据状态" width="96"><template #default><span class="ledger-pill">已完成</span></template></el-table-column>
+      <el-table-column label="原流水状态" width="110"><template #default="{ row }"><span v-if="row.source === 'normal'" class="ledger-pill" :class="{ warning: reversalStatus(row) === '部分冲销', muted: reversalStatus(row) === '已全额冲销' }">{{ reversalStatus(row) }}</span><span v-else>—</span></template></el-table-column>
+      <el-table-column label="来源" width="104"><template #default="{ row }"><span class="ledger-pill blue">{{ row.source === 'normal' ? '正常录入' : '冲销' }}</span></template></el-table-column>
+      <el-table-column label="skuID / 商品" min-width="270"><template #default="{ row }"><strong class="ledger-product">{{ row.skuId }} · {{ entrySku(row)?.name }}</strong></template></el-table-column>
+      <el-table-column prop="quantity" label="数量" width="58" />
+      <el-table-column label="库存变动" width="82"><template #default="{ row }"><b :class="row.direction === 'inbound' ? 'ledger-positive' : 'ledger-negative'">{{ row.direction === 'inbound' ? '+' : '−' }} {{ row.quantity }}</b></template></el-table-column>
+      <el-table-column label="关联原流水" width="110"><template #default="{ row }"><span class="ledger-code">{{ row.originalId || '—' }}</span></template></el-table-column>
+      <el-table-column label="操作" fixed="right" width="152"><template #default="{ row }"><el-button link type="primary" @click="showDetail(row)">详情</el-button><el-button v-if="canReverse(row)" link type="primary" @click="openReverse(row)">冲销 / 更正</el-button></template></el-table-column>
+    </el-table>
+    <el-pagination v-model:current-page="page" :total="total" :page-size="pageSize" layout="total, prev, pager, next" @current-change="loadData().catch(report)" />
+
+    <el-dialog :model-value="!!detail" class="ledger-dialog" modal-class="ledger-overlay" width="760px" align-center @close="detail = undefined">
+      <template #header="{ titleId, titleClass }"><h2 :id="titleId" :class="titleClass">流水详情 · {{ detail?.id }}</h2><p v-if="detail" class="ledger-subtitle">已完成 · 原流水状态:{{ reversalStatus(detail) }} · 创建时间:{{ dateTime(detail.createdAt) }}</p></template>
+      <template v-if="detail">
+        <div class="ledger-grid ledger-detail-fields">
+          <div><label>skuID / 商品</label><div class="ledger-readonly">{{ detail.skuId }} · {{ entrySku(detail)?.name }}</div></div>
+          <div><label>库存变动</label><div class="ledger-readonly">{{ directionName(detail) }} {{ detail.direction === 'inbound' ? '+' : '−' }}{{ detail.quantity }}</div></div>
+          <div><label>{{ directionName(detail) }}数量</label><div class="ledger-readonly">{{ detail.quantity }} {{ entrySku(detail)?.spec }}</div></div>
+          <div v-if="detail.source === 'normal' && detail.direction === 'outbound'"><label>订单类型</label><div class="ledger-readonly">{{ detail.orderType === 'internal' ? '内部订单' : '外部订单' }}</div></div>
+        </div>
+        <template v-if="detail.source === 'normal' && detail.orderType === 'internal'"><div class="ledger-evidence-label">内部订单追溯信息</div><div class="ledger-evidence">内部订单:{{ detail.so }};内部订单不展示物流、照片/凭证</div></template>
+        <template v-else>
+          <div class="ledger-evidence-label">照片 / 凭证</div>
+          <div class="ledger-evidence"><span v-if="!attachments(detail).length">暂无凭证</span><div v-for="(file, index) in attachments(detail)" :key="file.id" class="ledger-evidence-item"><el-image :src="file.url" :preview-src-list="attachments(detail).map(item => item.url)" :initial-index="index" preview-teleported fit="cover" /></div><span v-if="detail.tracking">{{ detail.company }} · 快递单号 {{ detail.tracking }}</span></div>
+        </template>
+        <div v-if="detail.remark" class="ledger-related">备注 / 冲销原因:{{ detail.remark }}</div>
+        <div v-if="detail.originFlowDetail">
+          <div class="ledger-evidence-label">关联原流水</div>
+          <div class="ledger-readonly">{{ detail.originFlowDetail }}</div>
+        </div>
+      </template>
+      <template #footer><el-button @click="detail = undefined">关闭</el-button><el-button v-if="detail && canReverse(detail)" type="warning" plain @click="openReverse(detail)">冲销 / 更正</el-button></template>
+    </el-dialog>
+
+    <el-dialog v-model="reverseDialog" class="ledger-dialog" modal-class="ledger-overlay" width="548px" align-center :close-on-click-modal="false" :close-on-press-escape="!busy" :show-close="!busy" @closed="reverseFormRef?.clearValidate()">
+      <template #header="{ titleId, titleClass }"><h2 :id="titleId" :class="titleClass">冲销 / 更正</h2><p class="ledger-subtitle">将生成与原流水方向相反的新流水,原记录不会被删除</p></template>
+      <template v-if="original">
+        <el-alert class="ledger-reverse-notice" type="warning" :closable="false" show-icon :title="`原流水:${original.id} · 剩余可冲销数量 ${preCheck?.remainReversalQty ?? '—'}。冲销完成后,请另行录入正确的出入库单。`" />
+        <el-alert v-if="errors.form" :title="errors.form" type="error" :closable="false" />
+        <el-form ref="reverseFormRef" :model="reverseForm" :rules="reverseRules" label-position="top" :disabled="busy || pendingUploads > 0">
+          <div class="ledger-grid"><el-form-item label="冲销方向"><el-input :model-value="original.direction === 'inbound' ? '出库(反向生成)' : '入库(反向生成)'" disabled /></el-form-item><el-form-item label="冲销数量" prop="quantity" required :error="errors.quantity"><el-input-number v-model="reverseQty" :controls="false" :min="1" :max="reverseMax" :precision="0" /></el-form-item></div>
+          <p v-if="original.direction === 'inbound'" class="ledger-stock-note">当前可用库存:{{ preCheck?.currentStock ?? '—' }} {{ entrySku(original)?.spec }}</p>
+          <el-form-item label="照片 / 凭证" prop="evidence" required :error="errors.evidence"><EvidenceUpload :active="reverseDialog" :disabled="busy" v-model="reverseEvidence" @update:model-value="validateReverseEvidence" /></el-form-item>
+          <el-form-item label="冲销原因"><el-input v-model="reverseRemark" type="textarea" :rows="3" placeholder="选填:说明冲销原因" /></el-form-item>
+        </el-form>
+      </template>
+      <template #footer><el-button :disabled="busy" @click="reverseDialog = false">取消</el-button><el-button type="primary" :loading="busy" :disabled="pendingUploads > 0" @click="submitReverse">提交冲销</el-button></template>
+    </el-dialog>
+  </div>
+</template>
+
+<style scoped>
+.ledger-heading { margin-bottom: 18px; }
+.ledger-heading h1 { font-size: 24px; letter-spacing: 0; }
+.ledger-heading p { margin-top: 7px; color: #7c8c92; font-size: 12px; }
+.ledger-heading > .el-button { height: 34px; border-radius: 8px; }
+.ledger-stats { display: grid; grid-template-columns: 1.15fr 1fr 1fr; gap: 12px; margin-bottom: 14px; }
+.ledger-stats article { padding: 16px; border: 1px solid #e5ebe7; border-radius: 8px; background: #fffefb; box-shadow: 0 10px 24px #233b2d06; }
+.ledger-stats span { display: block; margin-bottom: 6px; color: #829095; font-size: 11px; }
+.ledger-stats strong { font-size: 22px; line-height: 26px; }
+.ledger-filters { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; padding: 12px; border: 1px solid #e5ebe7; background: #fffefb; border-radius: 8px; }
+.ledger-filters > * { flex: 1 1 0; }
+.ledger-filters > .el-button { flex: 0 0 auto; }
+.ledger-filters > .ledger-search { flex: 1 1 260px; }
+.ledger-search :deep(.el-input__wrapper) { min-height: 36px; border-radius: 8px; }
+.ledger-filter-break { flex-basis: 100%; width: 0; height: 0; }
+.ledger-filters > :nth-child(8) { flex-grow: 1.35; }
+.ledger-filters > * { min-width: 0; }
+.ledger-note { font-size: 11px; color: #829095; line-height: 1.6; margin: 12px 4px; }
+.ledger-pill { display: inline-flex; align-items: center; gap: 4px; padding: 2px 7px; border-radius: 20px; background: #e4f5ee; color: #218570; font-size: 10px; line-height: 14px; white-space: nowrap; font-weight: 600; }
+.ledger-pill::before { content: ''; width: 4px; height: 4px; border-radius: 50%; background: currentColor; }
+.ledger-pill.warning { background: #fff1d4; color: #ad751c; }
+.ledger-pill.muted { background: #eef1f0; color: #74837f; }
+.ledger-pill.blue { background: #e8f2fc; color: #4685b5; }
+.ledger-positive { color: #17836d; }
+.ledger-negative { color: #dc685d; }
+.ledger-table { --el-table-header-bg-color: #f6f8f6; --el-table-border-color: #edf1ed; --el-table-header-text-color: #819095; --el-table-text-color: #5c6e74; border: 1px solid #e8ede8; border-radius: 8px; box-shadow: 0 15px 30px #233b2d08; }
+.ledger-table :deep(th.el-table__cell) { height: 36px; font-size: 10px; }
+.ledger-table :deep(td.el-table__cell) { height: 47px; font-size: 11px; }
+.ledger-table :deep(.cell) { padding: 0 11px; }
+.ledger-table :deep(.el-button) { font-size: 11px; }
+.ledger-table :deep(.el-button + .el-button) { margin-left: 10px; }
+.ledger-code { color: #486671; font-size: 10px; }
+.ledger-product { color: #26383e; overflow-wrap: anywhere; }
+.ledger-subtitle { margin: 5px 0 0; font-size: 12px; color: #84969c; line-height: 1.6; }
+.ledger-grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 0 14px; }
+.ledger-detail-fields { gap: 16px 14px; }
+.ledger-detail-fields label, .ledger-evidence-label { display: block; font-size: 12px; font-weight: 600; color: #556d74; margin-bottom: 6px; }
+.ledger-readonly { min-height: 38px; padding: 8px 10px; border: 1px solid #dce5e1; border-radius: 7px; background: #f2f6f4; color: #80969d; font-size: 14px; line-height: 20px; overflow-wrap: anywhere; }
+.ledger-evidence-label { margin-top: 16px; }
+.ledger-evidence { display: flex; justify-content: center; align-items: center; flex-wrap: wrap; gap: 10px; padding: 15px; min-height: 76px; border: 1px dashed #bdddd3; border-radius: 7px; background: #f1faf6; color: #458e7c; font-size: 12px; overflow-wrap: anywhere; }
+.ledger-evidence-item { display: flex; align-items: center; gap: 8px; max-width: 100%; }
+.ledger-evidence-item .el-image { width: 44px; height: 40px; border-radius: 4px; flex-shrink: 0; cursor: zoom-in; }
+.ledger-related { margin-top: 16px; overflow-wrap: anywhere; }
+.ledger-stock-note { font-size: 12px; color: #7b8d91; margin: 0 0 14px; }
+@media (max-width: 1100px) { .ledger-filters > * { flex-basis: calc(33.333% - 8px); } .ledger-filters > :nth-child(8) { flex-grow: 1; } }
+@media (max-width: 600px) { .ledger-heading { align-items: flex-start; gap: 12px; } .ledger-heading h1 { font-size: 22px; } .ledger-stats { grid-template-columns: minmax(0, 1fr); } .ledger-filters > * { flex-basis: calc(50% - 8px); } .ledger-filters > .ledger-search { flex-basis: 100%; } .ledger-grid { grid-template-columns: minmax(0, 1fr); } }
+</style>
+
+<style>
+.ledger-overlay { backdrop-filter: blur(3px); background: #1c2f3566; }
+.el-dialog.ledger-dialog { padding: 0; max-width: calc(100vw - 32px); border-radius: 16px; overflow: hidden; background: #fffefb; box-shadow: 0 28px 70px #12252233; }
+.ledger-dialog .el-dialog__header { padding: 20px 22px 14px; margin: 0; border-bottom: 1px solid #dfe7e3; }
+.ledger-dialog .el-dialog__title { margin: 0; padding-right: 32px; font-size: 18px; font-weight: 700; color: #1c3039; line-height: 26px; }
+.ledger-dialog .el-dialog__headerbtn { top: 24px; right: 22px; width: 28px; height: 28px; border-radius: 7px; background: #eef3f1; }
+.ledger-dialog .el-dialog__body { padding: 22px; max-height: calc(100dvh - 220px); overflow-y: auto; }
+.ledger-dialog .el-form-item { margin-bottom: 16px; }
+.ledger-dialog .el-form-item__label { font-size: 12px; font-weight: 600; color: #566d73; margin-bottom: 5px; line-height: 18px; }
+.ledger-dialog .el-input__wrapper { min-height: 36px; border-radius: 7px; box-shadow: 0 0 0 1px #dce5e1 inset; }
+.ledger-dialog .el-input.is-disabled .el-input__wrapper { background: #f2f6f4; }
+.ledger-dialog .el-input.is-disabled .el-input__inner { -webkit-text-fill-color: #82959c; }
+.ledger-dialog .el-input-number { width: 100%; }
+.ledger-dialog .el-input-number .el-input__inner { text-align: left; }
+.ledger-dialog .el-dialog__footer { border-top: 1px solid #dfe7e3; padding: 14px 22px; }
+.ledger-dialog .el-dialog__footer .el-button { height: 36px; border-radius: 8px; }
+.ledger-dialog .el-alert { margin-bottom: 18px; }
+.ledger-dialog .ledger-reverse-notice { border: 1px solid #ffe0a5; background: #fffaec; }
+.ledger-dialog .ledger-reverse-notice .el-alert__title { font-size: 12px; line-height: 1.6; }
+.ledger-dialog .evidence { width: 100%; }
+.ledger-dialog .upload-button { display: flex; justify-content: center; align-items: center; gap: 7px; width: 100%; min-height: 68px; box-sizing: border-box; border: 1px dashed #bdddd3; border-radius: 7px; background: #f1faf6; color: #458e7c; font-size: 12px; }
+</style>

+ 79 - 0
src/views/Login.vue

@@ -0,0 +1,79 @@
+<script setup lang="ts">
+import { reactive, ref } from 'vue'
+import { useRouter } from 'vue-router'
+import { ElMessage } from 'element-plus'
+import { setAuthInfo } from '../utils/auth'
+
+const router = useRouter()
+const form = reactive({ username: 'admin', password: 'admin123' })
+const busy = ref(false)
+const showPassword = ref(false)
+
+async function submit() {
+	if (busy.value) return
+	busy.value = true
+	try {
+		// 跳过登录验证,直接写入模拟令牌进入系统
+		await setAuthInfo({ accessToken: 'mock-token-' + Date.now(), displayName: form.username || '管理员' })
+		ElMessage.success('登录成功')
+		await router.replace('/dashboard')
+	} finally {
+		busy.value = false
+	}
+}
+</script>
+
+<template>
+	<div class="mock-login">
+		<section class="login-panel" aria-labelledby="login-title">
+			<div class="brand login-brand"><span class="brand-mark">H</span>
+				<div class="brand-text">
+					<strong>HS 仓储运营台</strong>
+					<small>WAREHOUSE OPS · 上海闵行仓</small>
+				</div>
+			</div>
+			<h1 id="login-title">登录系统</h1>
+			<p class="login-desc">使用内部账号进入仓储基础 ERP,登录后所有操作将自动记录操作人。</p>
+			<form class="login-form" @submit.prevent="submit">
+				<div class="form-field">
+					<label class="field-label">用户名</label>
+					<input v-model="form.username" class="field-input" placeholder="请输入用户名" autocomplete="username" />
+				</div>
+				<div class="form-field">
+					<label class="field-label">密码</label>
+					<input v-model="form.password" :type="showPassword ? 'text' : 'password'" class="field-input" placeholder="请输入密码" autocomplete="current-password" />
+					<button type="button" class="toggle-password" @click="showPassword = !showPassword" :aria-label="showPassword ? '隐藏密码' : '显示密码'">
+						<svg v-if="showPassword" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
+						<svg v-else viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/><line x1="1" y1="1" x2="23" y2="23"/></svg>
+					</button>
+				</div>
+				<el-button type="primary" class="login-submit" :loading="busy" native-type="submit">继 续</el-button>
+			</form>
+			<footer>仅限内部操作 · 登录后进入上海闵行仓工作台</footer>
+		</section>
+	</div>
+</template>
+
+<style scoped>
+.mock-login { min-height: 100vh; display: flex; align-items: center; justify-content: center; background: linear-gradient(135deg, #f0f7ff 0%, #e8f4f8 100%); padding: 24px; }
+.login-panel { width: 100%; max-width: 480px; background: #fff; border-radius: 24px; padding: 48px 40px 36px; box-shadow: 0 8px 40px rgba(0, 0, 0, 0.06); }
+.login-brand { display: flex; align-items: center; gap: 14px; margin-bottom: 32px; }
+.brand-mark { width: 52px; height: 52px; border-radius: 14px; background: #3b82f6; color: #fff; display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 26px; }
+.brand-text { display: flex; flex-direction: column; gap: 4px; }
+.brand-text strong { font-size: 18px; font-weight: 600; color: #111827; }
+.brand-text small { font-size: 11px; color: #6b7280; letter-spacing: 0.3px; }
+.login-panel h1 { font-size: 25px; font-weight: 700; margin: 0 0 12px; color: #111827; }
+.login-desc { font-size: 13px; color: #6b7280; line-height: 1.7; margin: 0 0 32px; }
+.login-form { display: flex; flex-direction: column; gap: 20px; }
+.form-field { position: relative; display: flex; align-items: center; border: 1px solid #dbeafe; border-radius: 20px; padding: 0 24px; height: 64px; background: #fff; transition: border-color 0.2s; }
+.form-field:focus-within { border-color: #3b82f6; }
+.field-label { flex-shrink: 0; width: 100px; font-size: 13px; color: #6b7280; font-weight: 500; }
+.field-input { flex: 1; border: none; outline: none; font-size: 14px; color: #111827; background: transparent; }
+.field-input::placeholder { color: #9ca3af; }
+.toggle-password { position: absolute; right: 24px; top: 50%; transform: translateY(-50%); background: none; border: none; cursor: pointer; color: #9ca3af; padding: 4px; display: flex; align-items: center; justify-content: center; }
+.toggle-password:hover { color: #6b7280; }
+.toggle-password svg { width: 22px; height: 22px; }
+.login-submit { width: 100%; height: 56px; font-size: 18px; font-weight: 600; border-radius: 14px; margin-top: 12px; background: #3b82f6; border: none; }
+.login-submit:hover { background: #2563eb; }
+.login-panel footer { margin-top: 28px; text-align: center; font-size: 11px; color: #9ca3af; }
+</style>

+ 199 - 0
src/views/Login/index.vue

@@ -0,0 +1,199 @@
+<script setup lang="ts">
+import { onMounted, onBeforeUnmount, ref } from 'vue'
+import { useRouter } from 'vue-router'
+import { ElMessage } from 'element-plus'
+import { Loading, Right, RefreshRight } from '@element-plus/icons-vue'
+import { jwtDecode } from 'jwt-decode'
+import MockLogin from '../Login.vue'
+import { api } from '../../api'
+import {
+	clearAuthInfo, clearOAuthTemp, consumeOAuthTransaction,
+	getCachedOAuthState, getCachedVerifier, getOAuthLoginUrl,
+	getToken, mockLogin, setAuthInfo
+} from '../../utils/auth'
+
+const router = useRouter()
+const busy = ref(true)
+const message = ref('正在检查登录状态...')
+const failed = ref(false)
+const manualRetryVisible = ref(false)
+let cancelled = false
+onBeforeUnmount(() => { cancelled = true })
+
+interface DecodedAccessToken {
+	displayName?: string
+	role?: string | string[]
+}
+
+let exchangingCode: string | null = null
+let exchangingPromise: Promise<any> | null = null
+
+/** 同一 code 并发换取令牌时复用请求,避免重复调用。 */
+function exchangeTokenOnce(code: string, verifier: string, redirectURI: string) {
+	if (exchangingPromise && exchangingCode === code) return exchangingPromise
+	exchangingCode = code
+	exchangingPromise = api.exchangeToken(code, verifier, redirectURI).finally(() => {
+		exchangingCode = null
+		exchangingPromise = null
+	})
+	return exchangingPromise
+}
+
+async function authorize() {
+	busy.value = true
+	failed.value = false
+	manualRetryVisible.value = false
+	message.value = '正在跳转认证中心...'
+	try {
+		const url = getOAuthLoginUrl()
+		if (cancelled) return
+		sessionStorage.removeItem('warehouse-auth-paused')
+		sessionStorage.removeItem('oauth_no_auto_redirect')
+		location.assign(url)
+	} catch (error) {
+		showError(error)
+	}
+}
+
+function handleRetryAuth() {
+	sessionStorage.removeItem('oauth_no_auto_redirect')
+	authorize()
+}
+
+function showError(error: unknown) {
+	clearOAuthTemp()
+	if (cancelled) return
+	busy.value = false
+	failed.value = true
+	manualRetryVisible.value = true
+	message.value = error instanceof Error ? error.message : '登录失败,请重新认证'
+}
+
+async function handleCallback(code: string, state: string | null, authError: string | null) {
+	// 清理 URL 中的 OAuth 临时参数,避免刷新页面重复使用
+	const query = new URLSearchParams(location.search)
+	for (const key of ['code', 'state', 'error', 'error_description', 'session_state', 'iss']) {
+		query.delete(key)
+	}
+	const remaining = query.toString()
+	history.replaceState(history.state, '', `${location.pathname}${remaining ? `?${remaining}` : ''}`)
+
+	sessionStorage.removeItem('oauth_no_auto_redirect')
+	clearAuthInfo()
+
+	const cachedState = getCachedOAuthState()
+	if (!state || !cachedState || state !== cachedState) {
+		ElMessage.warning('登录状态校验失败,准备重新发起认证')
+		clearOAuthTemp()
+		authorize()
+		return
+	}
+
+	const codeVerifier = getCachedVerifier()
+	if (!codeVerifier) {
+		ElMessage.warning('登录会话已失效,准备重新发起认证')
+		clearOAuthTemp()
+		authorize()
+		return
+	}
+
+	try {
+		message.value = '正在换取访问令牌...'
+		busy.value = true
+		manualRetryVisible.value = false
+
+		const transaction = consumeOAuthTransaction(state)
+		if (authError) throw new Error('认证被取消或拒绝,请重新认证')
+
+		const result = await exchangeTokenOnce(code, transaction.verifier, transaction.redirectURI)
+		if (typeof result.access_token !== 'string' || !result.access_token.trim()) {
+			throw new Error('认证服务未返回有效访问令牌')
+		}
+		if (result.token_type?.toLowerCase() !== 'bearer') {
+			throw new Error('认证服务返回了不支持的令牌类型')
+		}
+		if (cancelled) return
+
+		// 解析 JWT 提取用户信息
+		let decodedDisplayName = ''
+		let decodedRole: string[] = []
+		try {
+			const decoded = jwtDecode<DecodedAccessToken>(result.access_token)
+			decodedDisplayName = decoded?.displayName || ''
+			if (Array.isArray(decoded?.role)) {
+				decodedRole = decoded.role
+			} else if (typeof decoded?.role === 'string' && decoded.role) {
+				decodedRole = [decoded.role]
+			}
+		} catch (decodeError) {
+			console.warn('access_token 解析失败:', decodeError)
+		}
+
+		await setAuthInfo({
+			accessToken: result.access_token,
+			displayName: decodedDisplayName || result.displayName,
+			role: decodedRole
+		})
+
+		clearOAuthTemp()
+		await router.replace('/dashboard')
+	} catch (error) {
+		showError(error)
+	}
+}
+
+onMounted(async () => {
+	if (mockLogin) return
+
+	const token = getToken()
+	if (token) {
+		await router.replace('/dashboard')
+		return
+	}
+
+	const searchParams = new URLSearchParams(location.search)
+	const code = searchParams.get('code')
+	const state = searchParams.get('state')
+	const authError = searchParams.get('error')
+	const noAutoRedirect = sessionStorage.getItem('oauth_no_auto_redirect') === '1'
+
+	if (code || authError) {
+		await handleCallback(code || '', state, authError)
+		return
+	}
+
+	if (noAutoRedirect || sessionStorage.getItem('warehouse-auth-paused')) {
+		busy.value = false
+		manualRetryVisible.value = true
+		message.value = '登录已退出或会话已失效,请重新认证。'
+		return
+	}
+
+	await authorize()
+})
+</script>
+
+<template>
+	<MockLogin v-if="mockLogin" />
+	<div v-else class="login-screen">
+		<section class="login-panel" aria-labelledby="login-title">
+			<div class="brand login-brand"><span class="brand-mark">H</span>
+				<div><strong>HS 仓储运营台</strong><small>WAREHOUSE OPS · 上海闵行仓</small></div>
+			</div>
+			<h1 id="login-title">统一身份认证</h1>
+			<div class="auth-status" role="status" aria-live="polite" :aria-busy="busy">
+				<el-icon v-if="busy" class="is-loading" :size="28"><Loading /></el-icon>
+				<p :class="{ 'auth-error': failed }">{{ message }}</p>
+			</div>
+			<el-button v-if="!busy && manualRetryVisible" type="primary" size="large" class="login-submit"
+				:icon="failed ? RefreshRight : Right" @click="handleRetryAuth">{{ failed ? '重新发起认证' : '前往认证中心' }}</el-button>
+			<footer>仅限内部授权账号访问</footer>
+		</section>
+	</div>
+</template>
+
+<style scoped>
+.auth-status { display: flex; flex-direction: column; align-items: center; gap: 16px; padding: 24px 0; min-height: 128px; }
+.auth-status p { margin: 0; line-height: 1.7; overflow-wrap: anywhere; text-align: center; }
+.auth-error { color: var(--el-color-danger); }
+</style>

+ 224 - 0
src/views/Master.vue

@@ -0,0 +1,224 @@
+<script setup lang="ts">
+import { computed, onMounted, ref, watch } from 'vue'
+import { useRouter } from 'vue-router'
+import { Plus } from '@element-plus/icons-vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import type { FormInstance, FormRules } from 'element-plus'
+import { api } from '../api'
+import type { Master, MasterKind } from '../api/indexType'
+import type { WmsBasicDataSaveRequest } from '../api/indexType'
+import { fieldErrors, relativeDay, report, refreshAfterWrite } from '../utils/ui'
+
+const dataTypes = { category: 1, sport: 2, brand: 3 } satisfies Record<MasterKind, NonNullable<WmsBasicDataSaveRequest['dataType']>>
+const masters = ref<Master[]>([])
+const pendingLoads = ref<Record<MasterKind, number>>({ category: 0, sport: 0, brand: 0 })
+const pages = ref<Record<MasterKind, number>>({ category: 1, sport: 1, brand: 1 })
+const totals = ref<Record<MasterKind, number>>({ category: 0, sport: 0, brand: 0 })
+const pageSize = 10
+async function loadData(masterKind: MasterKind = kind.value) {
+  pendingLoads.value[masterKind] += 1
+  try {
+      const response = await api.wmsApi.basicData.list({ dataType: dataTypes[masterKind], pageNo: pages.value[masterKind], pageSize })
+      const records = response.rows
+      totals.value[masterKind] = response.total
+      const lastPage = Math.max(1, Math.ceil(response.total / pageSize))
+      if (!records.length && pages.value[masterKind] > lastPage) {
+        pages.value[masterKind] = lastPage
+        await loadData(masterKind)
+        return
+      }
+      const loadedMasters = records.map(item => {
+        return { id: String(item.id ?? ''), kind: masterKind, name: item.dataName || '', enabled: item.status === 1, order: item.sortOrder ?? 99, operator: item.updateUser || item.createUser || '', card: masterKind === 'category' && item.dataName === '球星卡', skuCount: item.skuCount ?? 0, updatedAt: item.updateTime || item.createTime || '' }
+      })
+      masters.value = [...masters.value.filter(item => item.kind !== masterKind), ...loadedMasters]
+  }
+  finally { pendingLoads.value[masterKind] -= 1 }
+}
+const router = useRouter()
+const kind = ref<MasterKind>('category')
+const titles = { category: '产品种类', sport: '产品运动类型', brand: '产品品牌' }
+const dialog = ref(false)
+const busy = ref(false)
+const errors = ref<Record<string, string>>({})
+const master = ref<Master>(emptyMaster())
+const formRef = ref<FormInstance>()
+const rules: FormRules<Master> = {
+  name: [{ required: true, whitespace: true, message: '请填写名称', trigger: 'blur' }],
+  enabled: [{ required: true, type: 'boolean', message: '请选择状态', trigger: 'change' }],
+}
+const rows = computed(() => masters.value.filter((item: Master) => item.kind === kind.value).sort((first, second) => first.order - second.order))
+onMounted(() => loadData().catch(report))
+watch(kind, masterKind => {
+  pages.value[masterKind] = 1
+  loadData(masterKind).catch(report)
+})
+function emptyMaster(): Master {
+  return { id: '', kind: kind.value, name: '', enabled: true, order: 99, operator: '' }
+}
+function viewLinkedSkus(item: Master) {
+  router.push({ path: '/sku', query: { kind: item.kind, id: item.id } })
+}
+function open(item?: Master) {
+  errors.value = {}
+  master.value = item ? { ...item } : emptyMaster()
+  dialog.value = true
+}
+async function save() {
+  if (busy.value) return
+  busy.value = true
+  errors.value = {}
+  try {
+    const valid = await formRef.value?.validate().catch(() => false)
+    if (!valid) return
+    const savedKind = master.value.kind
+    const payload: WmsBasicDataSaveRequest = { dataName: master.value.name.trim(), dataType: dataTypes[savedKind], status: master.value.enabled ? 1 : 0, sortOrder: master.value.order }
+    if (master.value.id) payload.id = Number(master.value.id)
+    await api.wmsApi.basicData.save(payload)
+    dialog.value = false
+    ElMessage.success('保存成功')
+    await refreshAfterWrite(() => loadData(savedKind))
+  } catch (error) {
+    await fieldErrors(error, errors)
+  } finally {
+    busy.value = false
+  }
+}
+async function toggle(item: Master) {
+  try {
+    await ElMessageBox.confirm(`确认${item.enabled ? '停用' : '启用'} ${item.name}?`, '状态变更', { type: 'warning', confirmButtonText: '确认', cancelButtonText: '取消' })
+    await api.wmsApi.basicData.updateStatus({ id: Number(item.id), status: item.enabled ? 0 : 1 })
+    ElMessage.success('状态已更新')
+    await refreshAfterWrite(() => loadData())
+  } catch (error) {
+    if (error !== 'cancel' && error !== 'close') report(error)
+  }
+}
+async function remove(item: Master) {
+  try {
+    await ElMessageBox.confirm(`确认删除 ${item.name}?`, '删除基础资料', { type: 'warning', confirmButtonText: '删除', cancelButtonText: '取消' })
+    await api.wmsApi.basicData.delete({ id: Number(item.id) })
+    ElMessage.success('删除成功')
+    await refreshAfterWrite(() => loadData())
+  } catch (error) {
+    if (error !== 'cancel' && error !== 'close') report(error)
+  }
+}
+</script>
+
+<template>
+  <div class="master-page">
+    <div class="page-heading master-heading">
+      <div><h1>基础资料管理</h1><p>作为 SKU 下拉选项来源;已被使用的数据只能停用。</p></div>
+      <el-button type="primary" :icon="Plus" @click="open()">新增{{ titles[kind] }}</el-button>
+    </div>
+    <div class="master-notice">产品种类初始预置为「球星卡、周边、其他」。选择球星卡后,SKU 页面才显示并要求选择产品运动类型、产品品牌。</div>
+    <el-tabs v-model="kind" class="master-tabs" aria-label="基础资料类型">
+      <el-tab-pane v-for="(title, key) in titles" :key="key" :name="key" :label="title" />
+    </el-tabs>
+    <el-table v-loading="pendingLoads[kind] > 0" :data="rows" class="master-table" empty-text="暂无资料" row-key="id">
+      <el-table-column label="名称" min-width="160">
+        <template #default="{ row }"><span class="master-name">{{ row.name }}</span><span v-if="row.card" class="master-note">(球星卡联动)</span></template>
+      </el-table-column>
+      <el-table-column label="状态">
+        <template #default="{ row }"><span class="master-status" :class="{ disabled: !row.enabled }"><i />{{ row.enabled ? '启用' : '停用' }}</span></template>
+      </el-table-column>
+      <el-table-column prop="order" label="排序" />
+      <el-table-column label="已关联 SKU">
+        <template #default="{ row }"><button v-if="row.skuCount" type="button" class="master-count master-count-link" :aria-label="`查看 ${row.name} 已关联的 ${row.skuCount} 个 SKU`" @click="viewLinkedSkus(row)">{{ row.skuCount }} 个</button><span v-else class="master-count master-count-empty">0 个</span></template>
+      </el-table-column>
+      <el-table-column label="最近操作人">
+        <template #default="{ row }">
+          <span v-if="row.operator">{{ row.operator }}<span v-if="relativeDay(row.updatedAt)"> · {{ relativeDay(row.updatedAt) }}</span></span>
+          <span v-else>—</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" width="210" fixed="right">
+        <template #default="{ row }">
+          <el-button link type="primary" @click="open(row)">编辑</el-button>
+          <el-button link :type="row.enabled ? 'info' : 'primary'" @click="toggle(row)">{{ row.enabled ? '停用' : '启用' }}</el-button>
+          <el-button v-if="!row.skuCount" link type="danger" @click="remove(row)">删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <el-pagination v-model:current-page="pages[kind]" :total="totals[kind]" :page-size="pageSize" layout="total, prev, pager, next" @current-change="loadData().catch(report)" />
+    <el-dialog v-model="dialog" class="master-dialog" modal-class="master-overlay" width="480px" align-center :close-on-click-modal="false" :close-on-press-escape="!busy" :show-close="!busy" @closed="formRef?.clearValidate()">
+      <template #header="{ titleId, titleClass }">
+        <h2 :id="titleId" :class="titleClass">{{ master.id ? '编辑' : '新增' }}{{ titles[master.kind] }}</h2>
+        <p class="master-dialog-subtitle">保存后可在 SKU 下拉选项中使用。</p>
+      </template>
+      <el-alert v-if="errors.form" :title="errors.form" type="error" :closable="false" show-icon />
+      <el-form ref="formRef" :model="master" :rules="rules" label-position="top" :disabled="busy" @submit.prevent="save">
+        <el-form-item label="名称" prop="name" required :error="errors.name"><el-input v-model="master.name" placeholder="填写名称" autofocus /></el-form-item>
+        <div class="master-form-row">
+          <el-form-item label="状态" prop="enabled" required :error="errors.enabled"><el-select v-model="master.enabled" aria-label="状态"><el-option :value="true" label="启用" /><el-option :value="false" label="停用" /></el-select></el-form-item>
+          <el-form-item label="排序" :error="errors.order"><el-input-number v-model="master.order" :min="1" :precision="0" controls-position="right" aria-label="排序" /></el-form-item>
+		  <!-- <el-form-item label="排序" :error="errors.order"><el-input-number v-model="master.order" :min="1" :precision="0" controls="true" aria-label="排序" /></el-form-item> -->
+        </div>
+      </el-form>
+      <template #footer><el-button :disabled="busy" @click="dialog = false">取消</el-button><el-button type="primary" :loading="busy" @click="save">保存</el-button></template>
+    </el-dialog>
+  </div>
+</template>
+
+<style scoped>
+.master-heading { margin-bottom: 22px; }
+.master-heading h1 { font-size: 24px; letter-spacing: 0; }
+.master-heading p { color: #7b8885; margin-top: 8px; font-size: 13px; }
+.master-heading > .el-button { min-height: 38px; padding: 0 17px; border-radius: 7px; }
+.master-notice { padding: 13px 16px; border-left: 3px solid #e4b949; background: #fff9e9; color: #806b31; font-size: 12px; line-height: 1.8; border-radius: 4px; margin-bottom: 22px; }
+.master-tabs { width: fit-content; max-width: 100%; margin-bottom: 18px; }
+.master-tabs :deep(.el-tabs__header) { margin: 0; }
+.master-tabs :deep(.el-tabs__nav-wrap::after), .master-tabs :deep(.el-tabs__active-bar) { display: none; }
+.master-tabs :deep(.el-tabs__nav) { background: #fff; border-radius: 8px; }
+.master-tabs :deep(.el-tabs__item) { padding: 0 20px !important; height: 34px; color: #6b7875; font-size: 13px; }
+.master-tabs :deep(.el-tabs__item.is-active) { background: #e8f3ec; color: #277452; border-radius: 5px; font-weight: 600; }
+.master-table { --el-table-header-bg-color: #f3f6f4; --el-table-border-color: #edf1ef; --el-table-header-text-color: #77847f; --el-table-text-color: #46564f; border: 1px solid #e5ebe7; border-radius: 8px; box-shadow: 0 3px 12px #233b2d05; }
+.master-table :deep(th.el-table__cell) { height: 45px; font-size: 12px; font-weight: 500; }
+.master-table :deep(td.el-table__cell) { height: 59px; font-size: 13px; }
+.master-table :deep(.cell) { padding-left: 20px; padding-right: 20px; }
+.master-name { font-weight: 600; color: #30483c; }
+.master-note { font-size: 11px; color: #96a09b; margin-left: 6px; }
+.master-status { display: inline-flex; align-items: center; gap: 5px; padding: 3px 9px; border-radius: 20px; background: #eaf5ed; color: #3d8059; font-size: 11px; line-height: 18px; }
+.master-status i { width: 5px; height: 5px; border-radius: 50%; background: currentColor; }
+.master-status.disabled { background: #f0f2f1; color: #8b9590; }
+.master-count { color: #44845e; }
+.master-count-link { background: none; border: none; padding: 0; cursor: pointer; font: inherit; color: #277452; font-weight: 600; }
+.master-count-link:hover { color: #1c5d3e; }
+.master-count-link:focus-visible { outline: 2px solid #277452; outline-offset: 2px; border-radius: 3px; }
+.master-count-empty { color: #aab2ae; }
+.master-form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }
+.master-form-row .el-input-number { width: 100%; }
+.master-dialog-subtitle { margin: 8px 0 0; color: #8d9792; font-size: 12px; }
+@media (max-width: 600px) {
+  .master-heading { align-items: flex-start; gap: 14px; flex-direction: column; }
+  .master-heading h1 { font-size: 22px; }
+  .master-tabs :deep(.el-tabs__item) { padding: 0 12px !important; font-size: 12px; }
+}
+</style>
+
+<style>
+.master-overlay { backdrop-filter: blur(3px); background: #18282066; }
+.el-dialog.master-dialog { max-width: calc(100vw - 32px); padding: 0; border-radius: 16px; overflow: hidden; background: #fefefd; box-shadow: 0 24px 70px #10281b33; }
+.master-dialog .el-dialog__header { padding: 23px 26px 21px; margin: 0; border-bottom: 1px solid #edf0ed; }
+.master-dialog .el-dialog__title { margin: 0; font-size: 19px; font-weight: 600; line-height: 26px; color: #30443a; padding-right: 30px; }
+.master-dialog .el-dialog__headerbtn { top: 22px; right: 22px; width: 28px; height: 28px; border-radius: 7px; background: #f1f4f1; }
+.master-dialog .el-dialog__body { padding: 24px 26px 10px; }
+.master-dialog .el-form-item { margin-bottom: 22px; }
+.master-dialog .el-form-item__label { color: #526158; font-size: 12px; margin-bottom: 8px; }
+.master-dialog .el-input__wrapper, .master-dialog .el-select__wrapper { min-height: 39px; border-radius: 7px; box-shadow: 0 0 0 1px #dfe6df inset; background: #fff; }
+.master-dialog .el-input-number .el-input__inner { text-align: left; }
+.master-dialog .el-input-number.is-controls-right .el-input__wrapper { padding-left: 12px; padding-right: 32px; }
+.master-dialog .el-input-number.is-controls-right .el-input-number__increase,
+.master-dialog .el-input-number.is-controls-right .el-input-number__decrease { right: 7px; width: 20px; height: 12px; line-height: 12px; border: 0; border-radius: 3px; background: transparent; color: #8d9792; }
+.master-dialog .el-input-number.is-controls-right .el-input-number__increase { top: calc(50% - 12px); bottom: auto; }
+.master-dialog .el-input-number.is-controls-right .el-input-number__decrease { top: 50%; bottom: auto; }
+.master-dialog .el-input-number.is-controls-right .el-input-number__increase .el-icon,
+.master-dialog .el-input-number.is-controls-right .el-input-number__decrease .el-icon { width: 10px; height: 10px; font-size: 10px; }
+.master-dialog .el-input-number.is-controls-right .el-input-number__increase:hover,
+.master-dialog .el-input-number.is-controls-right .el-input-number__decrease:hover { background: #edf5ef; color: #277452; }
+.master-dialog .el-input-number.is-controls-right .is-disabled,
+.master-dialog .el-input-number.is-controls-right .is-disabled:hover { background: transparent; color: #d0d7d2; }
+.master-dialog .el-dialog__footer { padding: 16px 26px; border-top: 1px solid #edf0ed; }
+.master-dialog .el-dialog__footer .el-button { min-width: 68px; height: 36px; border-radius: 7px; }
+.master-dialog .el-alert { margin-bottom: 16px; }
+</style>

+ 386 - 0
src/views/Outbound.vue

@@ -0,0 +1,386 @@
+<script setup lang="ts">
+import { computed, onMounted, reactive, ref } from 'vue'
+import { useRoute } from 'vue-router'
+import { Plus, Tickets } from '@element-plus/icons-vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import type { FormInstance, FormRules } from 'element-plus'
+import EvidenceUpload from '../components/EvidenceUpload.vue'
+import EntryAudit from '../components/EntryAudit.vue'
+import { api } from '../api'
+import { blankEntry } from '../utils/entry'
+import { useUploads } from '../utils/uploads'
+import type { Attachment, Entry } from '../api/indexType'
+import type { WmsOutboundRecordResponse, WmsOutboundSaveRequest, WmsSkuResponse, WmsReversalPreCheckResponse } from '../api/indexType'
+import { dateTime, fieldErrors, report, reversalLimit, refreshAfterWrite } from '../utils/ui'
+
+type OutboundEntry = Entry & { recordId?: number; alreadyReversedQty: number }
+type OutboundDetail = WmsOutboundRecordResponse
+const outboundRows = ref<OutboundEntry[]>([])
+const total = ref(0)
+const pageSize = 10
+const skuOptions = ref<WmsSkuResponse[]>([])
+const pendingUploads = useUploads()
+let skuSearchVersion = 0
+const preCheck = ref<WmsReversalPreCheckResponse>()
+const logisticsTypes: Record<string, number> = { express: 1, instant: 2, other: 3 }
+const orderTypes: Record<string, number> = { internal: 1, external: 2 }
+function toEntry(item: OutboundDetail): OutboundEntry {
+  const files = (item.attachments || []).map((file, index) => ({ id: String(file.id ?? index), url: file.fileUrl || '', name: file.fileName || '凭证', fileType: file.fileType }))
+  return {
+    ...blankEntry('outbound'), recordId: item.id, id: item.flowNo || String(item.id ?? ''),
+    alreadyReversedQty: item.alreadyReversedQty ?? 0, status: item.status === 1 ? 'completed' : 'draft',
+    skuId: item.skuId || '', quantity: item.qty ?? null, orderType: item.orderType === 1 ? 'internal' : item.orderType === 2 ? 'external' : '',
+    so: item.soNo || '', logistics: Object.keys(logisticsTypes).find(key => logisticsTypes[key] === item.logisticsType) || '',
+    company: item.expressCompany || '', tracking: item.expressNo || '', remark: item.remark || '',
+    createdAt: item.createTime || '', operator: item.updateUser || item.createUser || '',
+    orderEvidence: files.filter(file => file.fileType === 3),
+    screenshots: files.filter(file => file.fileType === 2),
+    snapshot: { id: item.skuId || '', name: item.skuNameSnapshot || '', spec: item.specSnapshot || '',
+      config: item.configDetailSnapshot || '', categoryId: '', sportId: '', brandId: '', remark: '', enabled: true,
+      createdAt: '', operator: '', category: '', sport: '', brand: '' },
+  }
+}
+function recordId(entry: OutboundEntry) {
+  if (entry.recordId == null) throw new Error('接口未返回出库记录 ID')
+  return entry.recordId
+}
+async function loadSkus(keyword = '') {
+  const version = ++skuSearchVersion
+  const response = await api.wmsApi.sku.search({ keyword: keyword.trim() || undefined })
+  if (version !== skuSearchVersion) return
+  const records = Array.isArray(response) ? response : response.records ?? response.list
+  if (!Array.isArray(records)) throw new Error('SKU 搜索结果格式不符合预期')
+  const selected = skuOptions.value.find(item => item.skuId === form.value.skuId)
+  skuOptions.value = records.filter(item => item.status === 1)
+  if (selected && !skuOptions.value.some(item => item.skuId === selected.skuId)) skuOptions.value.unshift(selected)
+}
+function searchSkus(keyword: string) { loadSkus(keyword).catch(report) }
+const loading = ref(false)
+async function loadData() {
+  loading.value = true
+  try {
+    const filter = applied.value
+    let dates = filter.period === 'custom' ? filter.dates : []
+    if (filter.period === 'today' || filter.period === 'week') {
+      const now = new Date()
+      const end = dateTime(now.toISOString()).slice(0, 10)
+      if (filter.period === 'week') now.setDate(now.getDate() - 6)
+      dates = [dateTime(now.toISOString()).slice(0, 10), end]
+    }
+    const result = await api.wmsApi.outbound.page({
+      pageNo: page.value, 
+      pageSize,
+      keyword: filter.keyword.trim() || undefined,
+      status: filter.status ? (filter.status === 'completed' ? 1 : 0) : undefined,
+      orderType: orderTypes[filter.orderType],
+      startTime: dates[0] ? `${dates[0]} 00:00:00` : undefined,
+      endTime: dates[1] ? `${dates[1]} 23:59:59` : undefined,
+    })
+    outboundRows.value = (result.rows as WmsOutboundRecordResponse[]).map(toEntry)
+    total.value = result.total
+  }
+  finally { loading.value = false }
+}
+const route = useRoute()
+const filters = reactive({ period: '', keyword: '', status: '', orderType: '', dates: [] as string[] })
+const applied = ref({ ...filters })
+const page = ref(1)
+const dialog = ref(false)
+const form = ref<OutboundEntry>({ ...blankEntry('outbound'), alreadyReversedQty: 0 })
+const errors = ref<Record<string, string>>({})
+const busy = ref(false)
+const formRef = ref<FormInstance>()
+const rules = computed<FormRules<OutboundEntry>>(() => {
+  const external = form.value.orderType === 'external'
+  const express = external && form.value.logistics === 'express'
+  return {
+    skuId: [{ required: true, whitespace: true, message: '请选择 SKU', trigger: 'change' }],
+    quantity: [{ required: true, type: 'integer', min: 1, message: '请填写大于 0 的整数出库数量', trigger: 'change' }],
+    orderType: [{ required: true, message: '请选择订单类型', trigger: 'change' }],
+    so: form.value.orderType === 'internal' ? [{ required: true, whitespace: true, message: '请填写 SO 编号', trigger: 'blur' }] : [],
+    orderEvidence: external ? [{ required: true, type: 'array', min: 1, message: '请上传外部订单凭证', trigger: 'change' }] : [],
+    logistics: external ? [{ required: true, message: '请选择物流方式', trigger: 'change' }] : [],
+    company: express ? [{ required: true, whitespace: true, message: '请填写快递公司', trigger: 'blur' }] : [],
+    tracking: express ? [{ required: true, whitespace: true, message: '请填写快递单号', trigger: 'blur' }] : [],
+    screenshots: external && ['instant', 'other'].includes(form.value.logistics) ? [{ required: true, type: 'array', min: 1, message: '请上传物流截图', trigger: 'change' }] : [],
+  }
+})
+function validateAttachment(field: 'orderEvidence' | 'screenshots') {
+  void formRef.value?.validateField(field).catch(() => false)
+}
+const detail = ref<OutboundEntry>()
+const selectedSku = computed(() => skuOptions.value.find(item => item.skuId === form.value.skuId))
+const reverseDialog = ref(false)
+const original = ref<OutboundEntry>()
+const reverseQty = ref<number | null>(null)
+const reverseEvidence = ref<Attachment[]>([])
+const reverseRemark = ref('')
+const reverseFormRef = ref<FormInstance>()
+const reverseForm = computed(() => ({ quantity: reverseQty.value, evidence: reverseEvidence.value }))
+const reverseMax = computed(() => reversalLimit(preCheck.value, 2))
+const reverseRules = computed<FormRules>(() => ({
+  quantity: [{ required: true, type: 'integer', min: 1, max: reverseMax.value, message: `冲销数量须为 1 至 ${reverseMax.value} 的整数`, trigger: 'change' }],
+  evidence: [{ required: true, type: 'array', min: 1, message: '请上传照片 / 凭证', trigger: 'change' }],
+}))
+function validateReverseEvidence() {
+  void reverseFormRef.value?.validateField('evidence').catch(() => false)
+}
+const orderName = (value: string) => ({ internal: '内部订单', external: '外部订单' }[value] || '待填写')
+const logisticsName = (value: string) => ({ express: '快递', instant: '闪送', other: '其他' }[value] || '待填写')
+const entrySku = (entry: Entry) => entry.snapshot
+const attachments = (entry: Entry) => [...entry.orderEvidence, ...entry.evidence, ...entry.screenshots]
+const canReverse = (entry: OutboundEntry) => entry.status === 'completed' && entry.alreadyReversedQty < Number(entry.quantity)
+const reversalStatus = (entry: OutboundEntry) => entry.alreadyReversedQty === 0 ? '未冲销' : entry.alreadyReversedQty >= Number(entry.quantity) ? '已全部冲销' : '部分冲销'
+const logisticsText = (entry: Entry) => entry.orderType === 'internal' ? '—' : entry.logistics === 'express' ? [entry.company, entry.tracking].filter(Boolean).join(' · ') || '待填写' : entry.screenshots.length ? `${logisticsName(entry.logistics)} · 物流截图` : '待填写'
+onMounted(async () => { try { await loadData(); if (route.query.new) open() } catch (error) { report(error) } })
+function filterRows() { applied.value = { ...filters, dates: [...(filters.dates || [])] }; page.value = 1; loadData().catch(report) }
+async function fetchDetail(entry: OutboundEntry) {
+  const result = toEntry(await api.wmsApi.outbound.detail({ id: recordId(entry) }) as OutboundDetail)
+  recordId(result)
+  return result
+}
+async function showDetail(entry: OutboundEntry) {
+  try { detail.value = await fetchDetail(entry) } catch (error) { report(error) }
+}
+async function open(entry?: OutboundEntry) {
+  try {
+    form.value = entry ? await fetchDetail(entry) : { ...blankEntry('outbound'), alreadyReversedQty: 0 }
+    skuOptions.value = form.value.skuId ? [{ skuId: form.value.skuId, productName: form.value.snapshot?.name, spec: form.value.snapshot?.spec, status: 1 }] : []
+    await loadSkus()
+    errors.value = {}; dialog.value = true
+  } catch (error) { report(error) }
+}
+function clearBranches() { form.value.so = ''; form.value.orderEvidence = []; form.value.evidence = []; form.value.screenshots = []; form.value.company = ''; form.value.tracking = ''; form.value.logistics = 'express'; errors.value = {}; formRef.value?.clearValidate(['so', 'orderEvidence', 'logistics', 'company', 'tracking', 'screenshots']) }
+function changeLogistics() { form.value.company = ''; form.value.tracking = ''; form.value.screenshots = []; form.value.evidence = []; errors.value = {}; formRef.value?.clearValidate(['company', 'tracking', 'screenshots']) }
+async function save(submit: boolean) {
+  if (busy.value || pendingUploads.value) return
+  busy.value = true; errors.value = {}
+  try {
+    const external = form.value.orderType === 'external'
+    formRef.value?.clearValidate()
+    const valid = await (submit ? formRef.value?.validate() : formRef.value?.validateField('skuId'))?.catch(() => false)
+    if (!valid) return
+    const screenshots = external && form.value.logistics !== 'express' ? form.value.screenshots : []
+    const orderEvidence = external ? form.value.orderEvidence : []
+    const files = [...orderEvidence, ...screenshots]
+    const payload: WmsOutboundSaveRequest = {
+      skuId: form.value.skuId || undefined, qty: form.value.quantity ?? undefined,
+      orderType: orderTypes[form.value.orderType], soNo: form.value.orderType === 'internal' ? form.value.so : undefined,
+      logisticsType: external ? logisticsTypes[form.value.logistics] : undefined,
+      expressCompany: external && form.value.logistics === 'express' ? form.value.company : undefined,
+      expressNo: external && form.value.logistics === 'express' ? form.value.tracking : undefined,
+      remark: form.value.remark, fileUrls: files.map(file => file.url), fileNames: files.map(file => file.name),
+      fileTypes: [...orderEvidence.map(() => 3), ...screenshots.map(() => 2)],
+    }
+    if (submit) await api.wmsApi.outbound.submit({ ...payload, draftId: form.value.recordId })
+    else if (form.value.recordId != null) await api.wmsApi.outbound.updateDraft({ ...payload, id: recordId(form.value) })
+    else await api.wmsApi.outbound.saveDraft(payload)
+    dialog.value = false
+    ElMessage.success(submit ? '出库提交成功' : '草稿已保存')
+    await refreshAfterWrite(loadData)
+  } catch (error) { await fieldErrors(error, errors) }
+  finally { busy.value = false }
+}
+async function remove(entry: OutboundEntry) {
+  try {
+    await ElMessageBox.confirm(`确认删除草稿 ${entry.id}?`, '删除草稿', { type: 'warning', confirmButtonText: '删除', cancelButtonText: '取消' })
+    await api.wmsApi.outbound.deleteDraft({ id: recordId(entry) }); ElMessage.success('草稿已删除'); await refreshAfterWrite(loadData)
+  } catch (error) { if (error !== 'cancel' && error !== 'close') report(error) }
+}
+async function openReverse(entry: OutboundEntry) {
+  try {
+    preCheck.value = await api.wmsApi.reversal.preCheck({ originFlowNo: entry.id, originFlowType: 2 }) as WmsReversalPreCheckResponse
+    if (!reverseMax.value) throw new Error('当前无可冲销数量,或预检数据不完整,请刷新后重试')
+    original.value = entry; reverseQty.value = reverseMax.value
+    reverseEvidence.value = []; reverseRemark.value = ''; errors.value = {}; detail.value = undefined; reverseDialog.value = true
+  } catch (error) { report(error) }
+}
+async function submitReverse() {
+  if (busy.value || pendingUploads.value || !original.value) return
+  busy.value = true; errors.value = {}
+  try {
+    const valid = await reverseFormRef.value?.validate().catch(() => false)
+    if (!valid) return
+    await api.wmsApi.reversal.submit({ originFlowNo: original.value.id, originFlowType: 2, qty: reverseQty.value ?? undefined,
+      fileUrls: reverseEvidence.value.map(file => file.url), fileNames: reverseEvidence.value.map(file => file.name), reversalReason: reverseRemark.value })
+    reverseDialog.value = false; ElMessage.success('冲销成功'); await refreshAfterWrite(loadData)
+  } catch (error) { await fieldErrors(error, errors) } finally { busy.value = false }
+}
+</script>
+
+<template>
+  <div v-loading="loading" class="outbound-page">
+    <div class="page-heading outbound-heading"><div><h1>出库管理</h1><p>内部订单填写 SO;外部订单上传订单凭证并填写物流。ERP 第一版只记录出库数量。</p></div><el-button type="primary" :icon="Plus" @click="open()">新建出库</el-button></div>
+    <el-config-provider :empty-values="[null, undefined]">
+    <form class="outbound-filters" @submit.prevent="filterRows">
+      <el-select v-model="filters.period" aria-label="创建时间筛选"><el-option label="创建时间:全部" value="" /><el-option label="创建时间:今天" value="today" /><el-option label="创建时间:近 7 天" value="week" /><el-option label="自定义时间" value="custom" /></el-select>
+      <el-input v-model="filters.keyword" placeholder="skuID / 商品 / SO 编号" aria-label="skuID / 商品名称 / SO 编号" clearable />
+      <el-select v-model="filters.status" aria-label="单据状态筛选"><el-option label="单据状态:全部" value="" /><el-option label="已完成" value="completed" /><el-option label="草稿" value="draft" /></el-select>
+      <el-select v-model="filters.orderType" aria-label="订单类型筛选"><el-option label="订单类型:全部" value="" /><el-option label="内部订单" value="internal" /><el-option label="外部订单" value="external" /></el-select>
+      <el-button native-type="submit">筛选</el-button>
+      <el-date-picker v-if="filters.period === 'custom'" v-model="filters.dates" class="outbound-date-range" type="daterange" value-format="YYYY-MM-DD" start-placeholder="创建开始日期" end-placeholder="结束日期" />
+    </form>
+    </el-config-provider>
+    <el-table :data="outboundRows" class="outbound-table" row-key="id" empty-text="暂无出库单据">
+      <el-table-column label="出库流水编号" width="124"><template #default="{ row }"><b class="outbound-code">{{ row.id }}</b></template></el-table-column>
+      <el-table-column label="单据状态" width="104"><template #default="{ row }"><span class="outbound-status" :class="{ draft: row.status === 'draft' }">{{ row.status === 'completed' ? '已完成' : '草稿' }}</span></template></el-table-column>
+      <el-table-column label="创建时间" width="182"><template #default="{ row }">{{ dateTime(row.createdAt) }}</template></el-table-column>
+      <el-table-column label="skuID / 商品" min-width="300"><template #default="{ row }"><strong class="outbound-product">{{ row.skuId || '待选择 SKU' }}<template v-if="entrySku(row)"> · {{ entrySku(row)?.name }}</template></strong></template></el-table-column>
+      <el-table-column label="规格" width="100"><template #default="{ row }">{{ entrySku(row)?.spec || '—' }}</template></el-table-column>
+      <el-table-column label="出库数量" width="94"><template #default="{ row }"><b v-if="row.quantity !== null">{{ row.quantity }}</b><span v-else>待填写</span></template></el-table-column>
+      <el-table-column label="订单类型" width="100"><template #default="{ row }">{{ orderName(row.orderType) }}</template></el-table-column>
+      <el-table-column label="SO 编号" width="164"><template #default="{ row }">{{ row.so || (row.orderType === 'external' ? '—' : '待填写') }}</template></el-table-column>
+      <el-table-column label="物流信息" min-width="210"><template #default="{ row }">{{ logisticsText(row) }}</template></el-table-column>
+      <el-table-column prop="operator" label="操作人" width="100"><template #default="{ row }">{{ row.operator || '—' }}</template></el-table-column>
+      <el-table-column prop="remark" label="备注" min-width="150" show-overflow-tooltip><template #default="{ row }">{{ row.remark || '—' }}</template></el-table-column>
+      <el-table-column label="操作" width="190" fixed="right">
+		  <template #default="{ row }">
+			<template v-if="row.status === 'draft'">
+				<el-button link type="primary" @click="open(row)">编辑</el-button>
+				<el-button link type="danger" @click="remove(row)">删除</el-button>
+			</template>
+			<template v-else>
+				<el-button link type="primary" @click="showDetail(row)">查看详情</el-button>
+				<!-- <el-button v-if="canReverse(row)" link type="warning" @click="openReverse(row)">冲销 / 更正</el-button> -->
+			</template>
+		</template>
+	  </el-table-column>
+    </el-table>
+    <el-pagination v-model:current-page="page" :total="total" :page-size="pageSize" layout="total, prev, pager, next" @current-change="loadData().catch(report)" />
+
+    <el-dialog v-model="dialog" class="outbound-dialog" modal-class="outbound-overlay" width="728px" align-center :close-on-click-modal="false" :close-on-press-escape="!busy" :show-close="!busy" @closed="formRef?.clearValidate()">
+      <template #header="{ titleId, titleClass }"><h2 :id="titleId" :class="titleClass">{{ form.id ? '编辑出库草稿' : '新建出库' }}</h2><p class="outbound-subtitle">提交后立即扣减上海闵行仓库存</p></template>
+      <el-alert v-if="errors.form" :title="errors.form" type="error" :closable="false" />
+      <el-form ref="formRef" :model="form" :rules="rules" :validate-on-rule-change="false" label-position="top" :disabled="busy || pendingUploads > 0">
+        <h3 class="outbound-section">基础信息</h3>
+        <div class="outbound-grid">
+          <el-form-item label="仓库"><el-input model-value="上海闵行仓" disabled /></el-form-item>
+          <el-form-item label="创建时间"><el-input :model-value="form.createdAt ? dateTime(form.createdAt) : '提交时由系统自动生成(精确到秒)'" disabled /></el-form-item>
+          <el-form-item label="选择 SKU" prop="skuId" required class="outbound-full" :error="errors.skuId">
+            <el-select v-model="form.skuId" filterable remote :remote-method="searchSkus" clearable placeholder="从系统 SKU 中搜索选择" no-data-text="暂无启用 SKU" no-match-text="未找到匹配的 SKU"><el-option v-for="sku in skuOptions.filter(item => item.status === 1)" :key="sku.skuId" :value="sku.skuId || ''" :label="`${sku.skuId} · ${sku.productName} · ${sku.spec}`" /></el-select>
+            <div class="outbound-field-hint">按系统生成的 skuID、商品名称或规格关键词搜索</div>
+          </el-form-item>
+          <el-form-item label="出库数量" prop="quantity" required :error="errors.quantity"><el-input-number v-model="form.quantity" :controls="false" :min="1" :precision="0" placeholder="填写大于 0 的整数" /></el-form-item>
+          <el-form-item label="商品规格"><el-input :model-value="selectedSku?.spec || ''" placeholder="选择 SKU 后自动带出" disabled /></el-form-item>
+        </div>
+        <h3 class="outbound-section">订单信息</h3>
+        <el-form-item label="订单类型" prop="orderType" required :error="errors.orderType"><el-select v-model="form.orderType" placeholder="请选择订单类型" @change="clearBranches"><el-option label="内部订单" value="internal" /><el-option label="外部订单" value="external" /></el-select></el-form-item>
+        <el-form-item v-if="form.orderType === 'internal'" label="SO 编号" prop="so" required :error="errors.so"><el-input v-model="form.so" placeholder="填写内部订单 SO 编号" /></el-form-item>
+        <template v-if="form.orderType === 'external'">
+          <el-form-item label="外部订单凭证" prop="orderEvidence" required :error="errors.orderEvidence"><EvidenceUpload :key="form.logistics" :active="dialog" :disabled="busy" v-model="form.orderEvidence" @update:model-value="validateAttachment('orderEvidence')" /></el-form-item>
+          <h3 class="outbound-section">物流信息</h3>
+          <el-form-item label="物流方式" prop="logistics" required :error="errors.logistics"><el-select v-model="form.logistics" @change="changeLogistics"><el-option v-for="value in ['express', 'instant', 'other']" :key="value" :value="value" :label="logisticsName(value)" /></el-select></el-form-item>
+          <div v-if="form.logistics === 'express'" class="outbound-grid"><el-form-item label="快递公司" prop="company" required :error="errors.company"><el-input v-model="form.company" placeholder="例如:顺丰速运" /></el-form-item><el-form-item label="快递单号" prop="tracking" required :error="errors.tracking"><el-input v-model="form.tracking" placeholder="填写快递单号" /></el-form-item></div>
+          <el-form-item v-else label="物流截图" prop="screenshots" required :error="errors.screenshots"><EvidenceUpload :key="form.logistics" :active="dialog" :disabled="busy" v-model="form.screenshots" @update:model-value="validateAttachment('screenshots')" /></el-form-item>
+        </template>
+        <el-form-item label="备注"><el-input v-model="form.remark" type="textarea" :rows="2" /></el-form-item>
+      </el-form>
+      <template #footer><el-button :disabled="busy" @click="dialog = false">取消</el-button><el-button type="warning" plain :disabled="busy || pendingUploads > 0" @click="save(false)">保存草稿</el-button><el-button type="primary" :loading="busy" :disabled="pendingUploads > 0" @click="save(true)">提交出库</el-button></template>
+    </el-dialog>
+
+    <el-dialog :model-value="!!detail" class="outbound-dialog" modal-class="outbound-overlay" width="760px" align-center @close="detail = undefined">
+      <template #header="{ titleId, titleClass }"><h2 :id="titleId" :class="titleClass">流水详情 · {{ detail?.id }}</h2><p v-if="detail" class="outbound-subtitle">已完成 · 原流水状态:{{ reversalStatus(detail) }} · 创建时间:{{ dateTime(detail.createdAt) }}</p></template>
+      <template v-if="detail">
+        <EntryAudit :entry="detail" :reversed-qty="detail.alreadyReversedQty" />
+        <div class="outbound-grid outbound-detail-fields">
+          <div><label>skuID / 商品</label><div class="outbound-readonly">{{ detail.skuId }} · {{ entrySku(detail)?.name }}</div></div>
+          <div><label>库存变动</label><div class="outbound-readonly">{{ detail.direction === 'outbound' ? '出库 −' : '入库 +' }}{{ detail.quantity }}</div></div>
+          <div><label>{{ detail.direction === 'outbound' ? '出库数量' : '入库数量' }}</label><div class="outbound-readonly">{{ detail.quantity }} {{ entrySku(detail)?.spec }}</div></div>
+          <div v-if="detail.source === 'normal'"><label>订单类型</label><div class="outbound-readonly">{{ orderName(detail.orderType) }}</div></div>
+        </div>
+        <template v-if="detail.orderType === 'internal' && detail.source === 'normal'"><div class="outbound-evidence-label">内部订单追溯信息</div><div class="outbound-evidence"><el-icon><Tickets /></el-icon><span>内部订单:{{ detail.so }};内部订单不展示物流、照片/凭证</span></div></template>
+        <template v-else>
+          <div class="outbound-evidence-label">{{ detail.source === 'normal' ? '订单凭证 / 物流截图' : '冲销凭证' }}</div>
+          <div class="outbound-evidence">
+            <span v-if="!attachments(detail).length">暂无凭证</span>
+            <div v-for="(file, index) in attachments(detail)" :key="file.id" class="outbound-evidence-item"><el-image :src="file.url" :preview-src-list="attachments(detail).map(item => item.url)" :initial-index="index" preview-teleported fit="cover" /></div>
+            <span v-if="detail.source === 'normal'">物流信息:{{ logisticsText(detail) }}</span>
+          </div>
+        </template>
+        <div v-if="detail.remark" class="outbound-related">备注 / 冲销原因:{{ detail.remark }}</div>
+      </template>
+      <template #footer><el-button @click="detail = undefined">关闭</el-button><el-button v-if="detail && canReverse(detail)" type="warning" plain @click="openReverse(detail)">冲销 / 更正</el-button></template>
+    </el-dialog>
+
+    <el-dialog v-model="reverseDialog" class="outbound-dialog outbound-reverse-dialog" modal-class="outbound-overlay" width="620px" align-center :close-on-click-modal="false" :close-on-press-escape="!busy" :show-close="!busy" @closed="reverseFormRef?.clearValidate()">
+      <template #header="{ titleId, titleClass }"><h2 :id="titleId" :class="titleClass">冲销 / 更正</h2><p class="outbound-subtitle">将生成与原流水方向相反的新流水,原记录不会被删除。</p></template>
+      <template v-if="original">
+        <!-- <EntryAudit :entry="original" reversal :reversed-qty="preCheck?.alreadyReversedQty ?? original.alreadyReversedQty" /> -->
+        <el-alert class="outbound-reverse-notice" type="warning" :closable="false" show-icon :title="`原流水:${original.id} · 剩余可冲销数量 ${preCheck?.remainReversalQty ?? '—'}。冲销完成后,请另行录入正确的出入库单。`" />
+        <el-alert v-if="errors.form" :title="errors.form" type="error" :closable="false" />
+        <el-form ref="reverseFormRef" :model="reverseForm" :rules="reverseRules" label-position="top" :disabled="busy || pendingUploads > 0" class="outbound-reverse-form">
+          <div class="outbound-reverse-grid">
+            <el-form-item label="冲销方向"><el-input model-value="入库(反向生成)" disabled /></el-form-item>
+            <el-form-item label="冲销数量" prop="quantity" required :error="errors.quantity"><el-input-number v-model="reverseQty" :controls="false" :min="1" :max="reverseMax" :precision="0" /></el-form-item>
+          </div>
+          <el-form-item label="照片 / 凭证" prop="evidence" required :error="errors.evidence"><EvidenceUpload :active="reverseDialog" :disabled="busy" v-model="reverseEvidence" @update:model-value="validateReverseEvidence" /></el-form-item>
+          <el-form-item label="冲销原因"><el-input v-model="reverseRemark" type="textarea" :rows="3" placeholder="选填:说明冲销原因" /></el-form-item>
+        </el-form>
+      </template>
+      <template #footer><el-button :disabled="busy" @click="reverseDialog = false">取消</el-button><el-button type="primary" :loading="busy" :disabled="pendingUploads > 0" @click="submitReverse">提交冲销</el-button></template>
+    </el-dialog>
+  </div>
+</template>
+
+<style scoped>
+.outbound-heading { margin-bottom: 22px; }
+.outbound-heading h1 { font-size: 24px; letter-spacing: 0; }
+.outbound-heading p { margin-top: 8px; color: #7c8c92; font-size: 13px; }
+.outbound-heading > .el-button { height: 37px; border-radius: 8px; }
+.outbound-filters { display: grid; grid-template-columns: 1fr 1.7fr 1fr 1fr auto; align-items: center; gap: 10px; padding: 13px; margin-bottom: 14px; border: 1px solid #e5ebe7; background: #fff; border-radius: 8px; }
+.outbound-filters > * { min-width: 0; }
+.outbound-date-range { grid-column: 1 / -1; max-width: 100%; }
+.outbound-status { display: inline-flex; align-items: center; gap: 4px; background: #e4f5ee; color: #218570; border-radius: 20px; padding: 3px 8px; font-size: 10px; font-weight: 600; line-height: 14px; white-space: nowrap; }
+.outbound-status::before { content: ''; width: 5px; height: 5px; border-radius: 50%; background: currentColor; }
+.outbound-status.draft { background: #fff1d4; color: #ac771c; }
+.outbound-table { --el-table-header-bg-color: #f6f8f6; --el-table-border-color: #edf1ed; --el-table-header-text-color: #819095; --el-table-text-color: #5c6e74; border: 1px solid #e8ede8; border-radius: 8px; box-shadow: 0 15px 30px #233b2d08; }
+.outbound-table :deep(th.el-table__cell) { height: 40px; font-size: 11px; }
+.outbound-table :deep(td.el-table__cell) { height: 53px; font-size: 12px; }
+.outbound-table :deep(.cell) { padding: 0 13px; }
+.outbound-table :deep(.el-button) { font-size: 12px; }
+.outbound-code { color: #486671; font-size: 11px; }
+.outbound-product { color: #26383e; overflow-wrap: anywhere; }
+.outbound-subtitle { margin: 5px 0 0; font-size: 12px; color: #84969c; line-height: 1.6; }
+.outbound-grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 0 14px; }
+.outbound-full { grid-column: 1 / -1; }
+.outbound-section { margin: 4px 0 12px; font-size: 12px; color: #7c9296; font-weight: 600; }
+.outbound-field-hint { margin-top: 6px; color: #84969c; font-size: 12px; line-height: 1.6; }
+.outbound-stock { font-size: 12px; color: #6b8584; }
+.outbound-detail-fields { gap: 16px 14px; }
+.outbound-detail-fields label, .outbound-evidence-label { display: block; font-size: 12px; font-weight: 600; color: #556d74; margin-bottom: 6px; }
+.outbound-readonly { min-height: 38px; padding: 8px 10px; border: 1px solid #dce5e1; border-radius: 7px; background: #f2f6f4; color: #80969d; font-size: 14px; line-height: 20px; overflow-wrap: anywhere; }
+.outbound-evidence-label { margin-top: 16px; }
+.outbound-evidence { display: flex; justify-content: center; align-items: center; flex-wrap: wrap; gap: 12px; padding: 15px; min-height: 76px; border: 1px dashed #bdddd3; border-radius: 7px; background: #f1faf6; color: #458e7c; font-size: 12px; overflow-wrap: anywhere; }
+.outbound-evidence-item { display: flex; align-items: center; gap: 8px; max-width: 100%; }
+.outbound-evidence-item .el-image { width: 46px; height: 42px; border-radius: 4px; flex-shrink: 0; cursor: zoom-in; }
+.outbound-related, .outbound-reverse-form { margin-top: 16px; overflow-wrap: anywhere; }
+.outbound-reverse-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
+@media (max-width: 1000px) { .outbound-filters { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
+@media (max-width: 600px) { .outbound-heading { flex-direction: column; align-items: flex-start; gap: 14px; } .outbound-grid { grid-template-columns: minmax(0, 1fr); } .outbound-heading h1 { font-size: 22px; } }
+</style>
+
+<style>
+.outbound-overlay { backdrop-filter: blur(3px); background: #1c2f3566; }
+.el-dialog.outbound-dialog { padding: 0; max-width: calc(100vw - 32px); border-radius: 16px; overflow: hidden; background: #fffefb; box-shadow: 0 28px 70px #12252233; }
+.outbound-dialog .el-dialog__header { padding: 20px 22px 14px; margin: 0; border-bottom: 1px solid #dfe7e3; }
+.outbound-dialog .el-dialog__title { margin: 0; padding-right: 32px; font-size: 18px; font-weight: 700; color: #1c3039; line-height: 26px; }
+.outbound-dialog .el-dialog__headerbtn { top: 24px; right: 22px; width: 28px; height: 28px; border-radius: 7px; background: #eef3f1; }
+.outbound-dialog .el-dialog__body { padding: 24px 22px 20px; max-height: calc(100dvh - 220px); overflow-y: auto; }
+.outbound-dialog .el-form-item { margin-bottom: 14px; }
+.outbound-dialog .el-form-item__label { font-size: 12px; font-weight: 600; color: #566d73; margin-bottom: 5px; line-height: 18px; }
+.outbound-dialog .el-input__wrapper, .outbound-dialog .el-select__wrapper { min-height: 36px; border-radius: 7px; box-shadow: 0 0 0 1px #dce5e1 inset; }
+.outbound-dialog .el-input.is-disabled .el-input__wrapper { background: #f2f6f4; }
+.outbound-dialog .el-input.is-disabled .el-input__inner { -webkit-text-fill-color: #82959c; }
+.outbound-dialog .el-input-number { width: 100%; }
+.outbound-dialog .el-input-number .el-input__inner { text-align: left; }
+.outbound-dialog .el-dialog__footer { border-top: 1px solid #dfe7e3; padding: 14px 22px; }
+.outbound-dialog .el-dialog__footer .el-button { height: 36px; border-radius: 8px; }
+.outbound-dialog .el-alert { margin-bottom: 14px; }
+.outbound-reverse-dialog .el-dialog__body { padding-top: 20px; }
+.outbound-reverse-dialog .outbound-reverse-notice { border: 1px solid #ffe0a5; background: #fffaec; }
+.outbound-reverse-dialog .outbound-reverse-notice .el-alert__title { font-size: 12px; line-height: 1.6; }
+.outbound-reverse-dialog .outbound-reverse-form { margin-top: 18px; }
+.outbound-dialog .evidence { width: 100%; }
+.outbound-dialog .upload-button { display: flex; justify-content: center; align-items: center; gap: 7px; width: 100%; min-height: 66px; box-sizing: border-box; border: 1px dashed #bdddd3; border-radius: 7px; background: #f1faf6; color: #458e7c; font-size: 12px; }
+@media (max-width: 600px) { .outbound-reverse-grid { grid-template-columns: minmax(0, 1fr); gap: 0; } }
+</style>

+ 290 - 0
src/views/Sku.vue

@@ -0,0 +1,290 @@
+<script setup lang="ts">
+import { computed, onMounted, reactive, ref } from 'vue'
+import { useRoute } from 'vue-router'
+import { Plus } from '@element-plus/icons-vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import type { FormInstance, FormRules } from 'element-plus'
+import { api } from '../api'
+import type { Master, MasterKind, Sku } from '../api/indexType'
+import type { WmsSkuResponse, WmsBasicDataResponse } from '../api/indexType'
+import { dateTime, fieldErrors, report, refreshAfterWrite } from '../utils/ui'
+
+const route = useRoute()
+const masters = ref<Master[]>([])
+const loading = ref(false)
+type SkuRow = Sku & { recordId?: number; lastFlowTime?: string; stock?: number; recentFlows?: string[] }
+const skuRows = ref<SkuRow[]>([])
+const total = ref(0)
+const pageSize = 10
+function toSku(item: WmsSkuResponse): SkuRow {
+  return {
+    recordId: item.id, id: item.skuId || '', categoryId: String(item.categoryId ?? ''),
+    sportId: String(item.sportTypeId ?? ''), brandId: String(item.brandId ?? ''),
+    name: item.productName || '', spec: item.spec || '', config: item.configDetail || '',
+    remark: item.remark || '', enabled: item.status === 1, createdAt: item.createTime || '',
+    operator: item.updateUser || item.createUser || '', lastFlowTime: item.lastFlowTime || item.updateTime, stock: item.stock,
+    recentFlows: item.recentFlows ? [...item.recentFlows].reverse() : undefined,
+  }
+}
+function recordId(item: SkuRow) {
+  if (item.recordId == null) throw new Error('接口未返回 SKU 记录 ID')
+  return item.recordId
+}
+async function loadData() {
+  loading.value = true
+  try {
+    const filter = applied.value
+      const response = await api.wmsApi.sku.page({
+        pageNo: page.value, pageSize,
+        keyword: filter.keyword.trim() || undefined,
+        categoryId: filter.category ? Number(filter.category) : undefined,
+        sportTypeId: filter.sport ? Number(filter.sport) : undefined,
+        brandId: filter.brand ? Number(filter.brand) : undefined,
+      })
+      skuRows.value = (response.rows as WmsSkuResponse[]).map(toSku)
+      total.value = response.total
+  }
+  finally { loading.value = false }
+}
+async function loadMasters() {
+  const types: Record<MasterKind, number> = { category: 1, sport: 2, brand: 3 }
+  const responses = await Promise.all((Object.keys(types) as MasterKind[]).map(async kind => {
+    const items: WmsBasicDataResponse[] = []
+    let pageNo = 1
+    while (true) {
+      const response = await api.wmsApi.basicData.list({ dataType: types[kind], pageNo, pageSize: 100 })
+      items.push(...response.rows)
+      if (items.length >= response.total) break
+      if (!response.rows.length) throw new Error('基础资料分页不完整,请刷新重试')
+      pageNo += 1
+    }
+    return items.map(item => ({
+      id: String(item.id ?? ''), kind, name: item.dataName || '', enabled: item.status === 1,
+      order: item.sortOrder ?? 99, operator: item.updateUser || item.createUser || '',
+      card: kind === 'category' && item.dataName === '球星卡',
+    }))
+  }))
+  masters.value = responses.flat()
+}
+const filters = reactive({ keyword: '', category: '', sport: '', brand: '' })
+const applied = ref({ ...filters })
+const page = ref(1)
+const dialog = ref(false)
+const detail = ref<SkuRow>()
+const busy = ref(false)
+const errors = ref<Record<string, string>>({})
+const sku = ref<SkuRow>(emptySku())
+const name = (id: string) => masters.value.find(item => item.id === id)?.name || '—'
+const options = (kind: MasterKind, current = '') => masters.value.filter(item => item.kind === kind && (item.enabled || item.id === current)).sort((first, second) => first.order - second.order)
+const filterOptions = (kind: MasterKind) => masters.value.filter(item => item.kind === kind).sort((first, second) => first.order - second.order)
+const card = computed(() => masters.value.find(item => item.id === sku.value.categoryId)?.card)
+const locked = computed(() => sku.value.recordId != null)
+const formRef = ref<FormInstance>()
+const rules = computed<FormRules<SkuRow>>(() => ({
+  name: [{ required: true, whitespace: true, message: '请填写商品名称', trigger: 'blur' }],
+  config: [{ required: true, whitespace: true, message: '请填写配置明细', trigger: 'blur' }],
+  categoryId: locked.value ? [] : [{ required: true, message: '请选择产品种类', trigger: 'change' }],
+  spec: locked.value ? [] : [{ required: true, whitespace: true, message: '请填写规格', trigger: 'blur' }],
+  sportId: !locked.value && card.value ? [{ required: true, message: '请选择运动类型', trigger: 'change' }] : [],
+  brandId: !locked.value && card.value ? [{ required: true, message: '请选择品牌', trigger: 'change' }] : [],
+}))
+function changePage() {
+  loadData().catch(report)
+}
+onMounted(() => {
+  applyMasterQuery()
+  Promise.all([loadMasters(), loadData()]).catch(report)
+})
+function applyMasterQuery() {
+  const kind = String(route.query.kind || '')
+  const id = String(route.query.id || '')
+  if (!id) return
+  if (kind === 'category') filters.category = id
+  else if (kind === 'sport') filters.sport = id
+  else if (kind === 'brand') filters.brand = id
+  else return
+  applied.value = { ...filters }
+}
+function emptySku(): Sku {
+  return { id: '', categoryId: '', sportId: '', brandId: '', name: '', spec: '', config: '', remark: '', enabled: true, createdAt: '', operator: '' }
+}
+function filterRows() { applied.value = { ...filters }; page.value = 1; loadData().catch(report) }
+async function showDetail(item: SkuRow) {
+  try {
+    detail.value = toSku(await api.wmsApi.sku.detail({ id: recordId(item) }) as WmsSkuResponse)
+  } catch (error) { report(error) }
+}
+async function open(item?: SkuRow) {
+  try {
+  errors.value = {}
+  sku.value = item ? toSku(await api.wmsApi.sku.detail({ id: recordId(item) }) as WmsSkuResponse) : { ...emptySku(), categoryId: options('category').find(category => category.card)?.id || options('category')[0]?.id || '' }
+  if (item) recordId(sku.value)
+  detail.value = undefined
+  dialog.value = true
+  } catch (error) { report(error) }
+}
+function categoryChanged() { sku.value.sportId = ''; sku.value.brandId = '' }
+async function save() {
+  if (busy.value) return
+  busy.value = true
+  errors.value = {}
+  try {
+    const valid = await formRef.value?.validate().catch(() => false)
+    if (!valid) return
+    if (locked.value) {
+      await api.wmsApi.sku.update({ id: recordId(sku.value), productName: sku.value.name, configDetail: sku.value.config, remark: sku.value.remark })
+    } else {
+      await api.wmsApi.sku.save({
+        categoryId: sku.value.categoryId ? Number(sku.value.categoryId) : undefined,
+        sportTypeId: card.value && sku.value.sportId ? Number(sku.value.sportId) : undefined,
+        brandId: card.value && sku.value.brandId ? Number(sku.value.brandId) : undefined,
+        productName: sku.value.name, spec: sku.value.spec, configDetail: sku.value.config, remark: sku.value.remark,
+      })
+    }
+    dialog.value = false
+    ElMessage.success('保存成功')
+    await refreshAfterWrite(loadData)
+  } catch (error) { await fieldErrors(error, errors) }
+  finally { busy.value = false }
+}
+async function toggle(item: SkuRow) {
+  try {
+    await ElMessageBox.confirm(`确认${item.enabled ? '停用' : '启用'} ${item.name}?`, '状态变更', { type: 'warning', confirmButtonText: '确认', cancelButtonText: '取消' })
+    await api.wmsApi.sku.updateStatus({ id: recordId(item), status: item.enabled ? 0 : 1 })
+    ElMessage.success('状态已更新')
+    await refreshAfterWrite(loadData)
+  } catch (error) { if (error !== 'cancel' && error !== 'close') report(error) }
+}
+</script>
+
+<template>
+  <div v-loading="loading" class="sku-page">
+    <div class="page-heading sku-heading">
+      <div><h1>商品 / SKU 管理</h1><p>skuID 是库存最小单位;编辑仅支持修改商品名称、配置明细和备注。</p></div>
+      <el-button type="primary" :icon="Plus" @click="open()">新建 SKU</el-button>
+    </div>
+    <el-config-provider :empty-values="[null, undefined]">
+    <form class="sku-filters" @submit.prevent="filterRows">
+      <el-input v-model="filters.keyword" placeholder="搜索 skuID / 商品名称" aria-label="搜索 skuID / 商品名称" clearable />
+      
+	  <el-select v-model="filters.category" aria-label="产品种类筛选"><el-option label="产品种类:全部" value="" /><el-option v-for="item in filterOptions('category')" :key="item.id" :value="item.id" :label="item.name" /></el-select>
+      <el-select v-model="filters.sport"  aria-label="运动类型筛选"><el-option label="运动类型:全部" value="" /><el-option v-for="item in filterOptions('sport')" :key="item.id" :value="item.id" :label="item.name" /></el-select>
+      <el-select v-model="filters.brand" aria-label="品牌筛选"><el-option label="品牌类型:全部" value="" /><el-option v-for="item in filterOptions('brand')" :key="item.id" :value="item.id" :label="item.name" /></el-select>
+      
+	  <!-- <el-select v-model="filters.category" placeholder="产品种类" aria-label="产品种类筛选" clearable><el-option v-for="item in filterOptions('category')" :key="item.id" :value="item.id" :label="item.name" /></el-select> -->
+      <!-- <el-select v-model="filters.sport" placeholder="运动类型" aria-label="运动类型筛选" clearable><el-option v-for="item in filterOptions('sport')" :key="item.id" :value="item.id" :label="item.name" /></el-select> -->
+      <!-- <el-select v-model="filters.brand" placeholder="产品品牌" aria-label="品牌筛选" clearable><el-option v-for="item in filterOptions('brand')" :key="item.id" :value="item.id" :label="item.name" /></el-select> -->
+      <el-button native-type="submit">筛选</el-button>
+    </form>
+    </el-config-provider>
+    <el-table :data="skuRows" class="sku-table" row-key="id" empty-text="暂无匹配的 SKU">
+      <el-table-column prop="id" label="skuID" width="112"><template #default="{ row }"><span class="sku-code">{{ row.id }}</span></template></el-table-column>
+      <el-table-column label="产品种类" width="105"><template #default="{ row }">{{ name(row.categoryId) }}</template></el-table-column>
+      <el-table-column label="运动类型" width="105"><template #default="{ row }">{{ name(row.sportId) }}</template></el-table-column>
+      <el-table-column label="产品品牌" width="115"><template #default="{ row }">{{ name(row.brandId) }}</template></el-table-column>
+      <el-table-column label="商品名称" min-width="300"><template #default="{ row }"><strong class="sku-product">{{ row.name }}</strong><span v-if="!row.enabled" class="sku-inactive">已停用</span></template></el-table-column>
+      <el-table-column prop="spec" label="规格" width="70" />
+      <el-table-column label="闵行仓库存" width="120"><template #default="{ row }"><span class="sku-stock">{{ row.stock ?? '—' }}</span></template></el-table-column>
+      <el-table-column label="最近变动" width="185"><template #default="{ row }">{{ dateTime(row.lastFlowTime) }}</template></el-table-column>
+      <el-table-column label="操作" width="165" fixed="right"><template #default="{ row }">
+        <el-button link type="primary" @click="showDetail(row)">查看</el-button><el-button link @click="open(row)">编辑</el-button><el-button link @click="toggle(row)">{{ row.enabled ? '停用' : '启用' }}</el-button>
+      </template></el-table-column>
+    </el-table>
+    <el-pagination v-model:current-page="page" :total="total" :page-size="pageSize" layout="total, prev, pager, next" @current-change="changePage" />
+
+    <el-dialog v-model="dialog" class="sku-dialog" modal-class="sku-overlay" :width="sku.id ? '620px' : '710px'" align-center :close-on-click-modal="false" :close-on-press-escape="!busy" :show-close="!busy" @closed="formRef?.clearValidate()">
+      <template #header="{ titleId, titleClass }"><h2 :id="titleId" :class="titleClass">{{ sku.id ? `编辑 SKU · ${sku.id}` : '新建 SKU' }}</h2><p class="sku-subtitle">{{ locked ? '仅可修改商品名称、配置明细和备注。' : '产品种类为球星卡时,运动类型、品牌必须选择。' }}</p></template>
+      <el-alert v-if="errors.form" :title="errors.form" type="error" :closable="false" />
+      <el-form ref="formRef" :model="sku" :rules="rules" :validate-on-rule-change="false" label-position="top" :disabled="busy" @submit.prevent="save">
+        <div class="sku-form-grid">
+          <el-form-item label="skuID"><el-input :model-value="sku.id || '保存后由系统自动生成'" disabled /></el-form-item>
+          <el-form-item label="产品种类" prop="categoryId" :required="!locked" :error="errors.categoryId">
+            <el-input v-if="locked" :model-value="`${name(sku.categoryId)}(已锁定)`" disabled />
+            <el-select v-else v-model="sku.categoryId" placeholder="请选择产品种类" no-data-text="请先至基础资料管理维护" @change="categoryChanged"><el-option v-for="item in options('category', sku.categoryId)" :key="item.id" :value="item.id" :label="item.name" /></el-select>
+          </el-form-item>
+          <template v-if="card">
+            <el-form-item label="产品运动类型" prop="sportId" :required="!locked" :error="errors.sportId"><el-input v-if="locked" :model-value="`${name(sku.sportId)}(已锁定)`" disabled /><el-select v-else v-model="sku.sportId" placeholder="请选择运动类型" no-data-text="请先至基础资料管理维护"><el-option v-for="item in options('sport', sku.sportId)" :key="item.id" :value="item.id" :label="item.name" /></el-select></el-form-item>
+            <el-form-item label="产品品牌" prop="brandId" :required="!locked" :error="errors.brandId"><el-input v-if="locked" :model-value="`${name(sku.brandId)}(已锁定)`" disabled /><el-select v-else v-model="sku.brandId" placeholder="请选择品牌" no-data-text="请先至基础资料管理维护"><el-option v-for="item in options('brand', sku.brandId)" :key="item.id" :value="item.id" :label="item.name" /></el-select></el-form-item>
+          </template>
+          <el-form-item label="商品名称" prop="name" class="sku-full" required :error="errors.name"><el-input v-model="sku.name" placeholder="填写商品完整名称" /></el-form-item>
+          <el-form-item label="规格" prop="spec" :required="!locked" :error="errors.spec"><el-input v-if="locked" :model-value="`${sku.spec}(已锁定)`" disabled /><el-input v-else v-model="sku.spec" placeholder="例如:盒、箱、个" /></el-form-item>
+          <el-form-item label="配置明细" prop="config" required :error="errors.config"><el-input v-model="sku.config" placeholder="例如:1盒/2包" /></el-form-item>
+          <el-form-item label="备注" class="sku-full"><el-input v-model="sku.remark" type="textarea" :rows="3" placeholder="选填:填写补充说明" /></el-form-item>
+        </div>
+      </el-form>
+      <template #footer><el-button :disabled="busy" @click="dialog = false">取消</el-button><el-button type="primary" :loading="busy" @click="save">{{ sku.id ? '保存修改' : '保存 SKU' }}</el-button></template>
+    </el-dialog>
+
+    <el-dialog :model-value="!!detail" class="sku-dialog sku-detail" modal-class="sku-overlay" width="760px" align-center @close="detail = undefined">
+      <template #header="{ titleId, titleClass }"><h2 :id="titleId" :class="titleClass">SKU 详情 · {{ detail?.id }}</h2></template>
+      <template v-if="detail">
+        <div class="sku-form-grid sku-detail-fields">
+          <div><label>商品名称</label><div class="sku-readonly">{{ detail.name }}</div></div>
+          <div><label>状态</label><div class="sku-readonly">{{ detail.enabled ? '启用' : '停用' }}</div></div>
+          <div><label>规格</label><div class="sku-readonly">{{ detail.spec || '—' }}</div></div>
+          <div><label>创建时间</label><div class="sku-readonly">{{ dateTime(detail.createdAt) }}</div></div>
+          <div class="sku-full"><label>配置明细</label><div class="sku-readonly">{{ detail.config || '—' }}</div></div>
+          <div v-if="detail.remark?.trim()" class="sku-full"><label>备注</label><div class="sku-readonly">{{ detail.remark }}</div></div>
+        </div>
+        <template v-if="detail.recentFlows?.length">
+          <label class="sku-history-label">最近流水</label>
+          <div class="sku-history"><div v-for="(flow, index) in detail.recentFlows" :key="index" class="sku-history-entry">{{ flow }}</div></div>
+        </template>
+      </template>
+      <template #footer><el-button @click="detail = undefined">关闭</el-button><el-button v-if="false" @click="$router.push({ path: '/ledger', query: { skuId: detail?.id || '' } })">查看全部流水</el-button><el-button type="primary" @click="open(detail)">编辑 SKU</el-button></template>
+    </el-dialog>
+  </div>
+</template>
+
+<style scoped>
+.sku-heading { margin-bottom: 22px; }
+.sku-heading h1 { font-size: 24px; letter-spacing: 0; }
+.sku-heading p { margin-top: 8px; color: #7a8a8e; font-size: 13px; }
+.sku-heading > .el-button { height: 37px; border-radius: 8px; }
+.sku-filters { display: grid; grid-template-columns: minmax(0, 1.65fr) repeat(3, minmax(0, 1fr)) auto; align-items: center; gap: 10px; padding: 13px; background: #fff; border: 1px solid #e5ebe7; border-radius: 8px; margin-bottom: 15px; }
+.sku-filters > * { min-width: 0; }
+.sku-filters :deep(.el-input__wrapper), .sku-filters :deep(.el-select__wrapper) { min-height: 36px; box-sizing: border-box; border-radius: 8px; }
+.sku-filters > .el-button { height: 32px; padding: 0 12px; border-radius: 8px; }
+.sku-table { --el-table-header-bg-color: #f6f8f6; --el-table-border-color: #edf1ed; --el-table-header-text-color: #819095; --el-table-text-color: #5c6e74; border: 1px solid #e8ede8; border-radius: 8px; box-shadow: 0 15px 30px #233b2d08; }
+.sku-table :deep(th.el-table__cell) { height: 40px; font-size: 11px; font-weight: 600; }
+.sku-table :deep(td.el-table__cell) { height: 53px; font-size: 12px; }
+.sku-table :deep(.cell) { padding: 0 13px; }
+.sku-table :deep(.el-button) { font-size: 12px; }
+.sku-table :deep(.el-button + .el-button) { margin-left: 12px; }
+.sku-code { color: #42616b; font-size: 11px; font-weight: 700; }
+.sku-product { color: #23353c; overflow-wrap: anywhere; }
+.sku-stock { color: #19644f; font-size: 15px; }
+.sku-inactive { margin-left: 8px; color: #939b9c; font-size: 11px; }
+.sku-form-grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 0 14px; }
+.sku-full { grid-column: 1 / -1; }
+.sku-subtitle { font-size: 12px; color: #7f9097; margin: 5px 0 0; line-height: 1.6; }
+.sku-detail-fields { gap: 16px 14px; }
+.sku-detail-fields label, .sku-history-label { display: block; font-size: 12px; font-weight: 600; color: #546a70; margin-bottom: 6px; }
+.sku-readonly { min-height: 38px; padding: 8px 10px; background: #f2f6f4; border: 1px solid #d8e2df; border-radius: 7px; color: #7a9099; font-size: 14px; line-height: 20px; overflow-wrap: anywhere; }
+.sku-history-label { margin-top: 16px; }
+.sku-history { display: flex; flex-direction: column; align-items: center; gap: 4px; padding: 15px; background: #f2faf7; border: 1px dashed #b8d9cd; border-radius: 7px; color: #328b78; font-size: 12px; max-height: 250px; overflow: auto; }
+.sku-history-entry { display: flex; align-items: baseline; gap: 8px; max-width: 100%; overflow-wrap: anywhere; }
+.sku-history-entry .el-icon { flex-shrink: 0; }
+.sku-result { margin-top: 3px; text-align: center; overflow-wrap: anywhere; }
+@media (max-width: 1000px) { .sku-filters { grid-template-columns: repeat(2, minmax(0, 1fr)); } .sku-filters > .el-input { grid-column: 1 / -1; } }
+@media (max-width: 600px) { .sku-heading { flex-direction: column; align-items: flex-start; gap: 14px; } .sku-form-grid { grid-template-columns: minmax(0, 1fr); } .sku-heading h1 { font-size: 22px; } }
+</style>
+
+<style>
+.sku-overlay { backdrop-filter: blur(3px); background: #1c2f3566; }
+.el-dialog.sku-dialog { padding: 0; max-width: calc(100vw - 32px); border-radius: 16px; overflow: hidden; background: #fffefb; box-shadow: 0 28px 70px #12252233; }
+.sku-dialog .el-dialog__header { padding: 20px 22px 14px; margin: 0; border-bottom: 1px solid #dfe7e3; }
+.sku-dialog .el-dialog__title { margin: 0; padding-right: 34px; font-size: 18px; font-weight: 700; color: #1c3039; line-height: 26px; }
+.sku-dialog .el-dialog__headerbtn { top: 24px; right: 22px; width: 28px; height: 28px; border-radius: 7px; background: #eef3f1; }
+.sku-dialog .el-dialog__body { padding: 20px 22px 6px; }
+.sku-dialog .el-form-item { margin-bottom: 15px; }
+.sku-dialog .el-form-item__label { font-size: 12px; font-weight: 600; color: #566d73; margin-bottom: 6px; line-height: 18px; }
+.sku-dialog .el-input__wrapper, .sku-dialog .el-select__wrapper { min-height: 36px; border-radius: 7px; box-shadow: 0 0 0 1px #dce5e1 inset; }
+.sku-dialog .el-input.is-disabled .el-input__wrapper { background: #f2f6f4; }
+.sku-dialog .el-input.is-disabled .el-input__inner { -webkit-text-fill-color: #82959c; }
+.sku-dialog .el-textarea__inner { border-radius: 7px; box-shadow: 0 0 0 1px #dce5e1 inset; }
+.sku-dialog .el-dialog__footer { border-top: 1px solid #dfe7e3; padding: 14px 22px; }
+.sku-dialog .el-dialog__footer .el-button { height: 37px; border-radius: 8px; }
+.sku-dialog .el-alert { margin-bottom: 14px; }
+.sku-detail .el-dialog__body { padding-bottom: 22px; }
+</style>

+ 15 - 0
tsconfig.app.json

@@ -0,0 +1,15 @@
+{
+  "extends": "@vue/tsconfig/tsconfig.dom.json",
+  "compilerOptions": {
+    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+    "types": ["vite/client"],
+    "allowArbitraryExtensions": true,
+
+    /* Linting */
+    "noUnusedLocals": true,
+    "noUnusedParameters": true,
+    "erasableSyntaxOnly": true,
+    "noFallthroughCasesInSwitch": true
+  },
+  "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
+}

+ 7 - 0
tsconfig.json

@@ -0,0 +1,7 @@
+{
+  "files": [],
+  "references": [
+    { "path": "./tsconfig.app.json" },
+    { "path": "./tsconfig.node.json" }
+  ]
+}

+ 23 - 0
tsconfig.node.json

@@ -0,0 +1,23 @@
+{
+  "compilerOptions": {
+    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+    "target": "es2023",
+    "lib": ["ES2023"],
+    "types": ["node"],
+    "skipLibCheck": true,
+
+    /* Bundler mode */
+    "module": "nodenext",
+    "allowImportingTsExtensions": true,
+    "verbatimModuleSyntax": true,
+    "moduleDetection": "force",
+    "noEmit": true,
+
+    /* Linting */
+    "noUnusedLocals": true,
+    "noUnusedParameters": true,
+    "erasableSyntaxOnly": true,
+    "noFallthroughCasesInSwitch": true
+  },
+  "include": ["vite.config.ts"]
+}

+ 13 - 0
vite.config.ts

@@ -0,0 +1,13 @@
+import vue from '@vitejs/plugin-vue'
+import { defineConfig } from 'vite'
+
+// https://vite.dev/config/
+export default defineConfig({
+  plugins: [vue()],
+  server: {
+    host: '0.0.0.0',
+  },
+  preview: {
+    host: '0.0.0.0',
+  },
+})

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно