Просмотр исходного кода

Initial commit: CrossBorder_OS project

docker 3 месяцев назад
Сommit
a5442d747f
46 измененных файлов с 8655 добавлено и 0 удалено
  1. 7 0
      .claude/settings.local.json
  2. 29 0
      .gitignore
  3. 1015 0
      CrossBorder_OS_PRD.md
  4. 45 0
      README.md
  5. 231 0
      TODO.md
  6. 12 0
      index.html
  7. 2281 0
      package-lock.json
  8. 26 0
      package.json
  9. 7 0
      src/App.vue
  10. 34 0
      src/api/request.ts
  11. 145 0
      src/api/services.ts
  12. 77 0
      src/components/SpecPageRenderer.vue
  13. 148 0
      src/config/page-specs.ts
  14. 234 0
      src/layout/AppLayout.vue
  15. 15 0
      src/main.ts
  16. 713 0
      src/mock/data.ts
  17. 410 0
      src/mock/routes.ts
  18. 32 0
      src/router/index.ts
  19. 75 0
      src/router/menu.ts
  20. 136 0
      src/router/routes.ts
  21. 12 0
      src/stores/app.ts
  22. 59 0
      src/stores/auth.ts
  23. 247 0
      src/styles/global.scss
  24. 404 0
      src/types/page.ts
  25. 149 0
      src/views/auth/LoginView.vue
  26. 50 0
      src/views/channel/ChannelConfigView.vue
  27. 72 0
      src/views/dashboard/ReportDashboardView.vue
  28. 49 0
      src/views/inventory/InventoryOverviewView.vue
  29. 44 0
      src/views/inventory/ShippingWorkView.vue
  30. 255 0
      src/views/order/OrderDetailView.vue
  31. 157 0
      src/views/order/OrderListView.vue
  32. 330 0
      src/views/product/ProductEditorView.vue
  33. 207 0
      src/views/product/ProductListView.vue
  34. 221 0
      src/views/product/ProductMappingView.vue
  35. 218 0
      src/views/product/ProductPricingView.vue
  36. 57 0
      src/views/report/ReportCenterView.vue
  37. 28 0
      src/views/shared/ForbiddenView.vue
  38. 12 0
      src/views/shared/SpecOnlyPage.vue
  39. 58 0
      src/views/supplier/PurchaseOrderView.vue
  40. 57 0
      src/views/supplier/SupplierListView.vue
  41. 49 0
      src/views/system/ApiKeyView.vue
  42. 43 0
      src/views/system/OperationLogView.vue
  43. 69 0
      src/views/system/RolePermissionView.vue
  44. 1 0
      src/vite-env.d.ts
  45. 21 0
      tsconfig.json
  46. 114 0
      vite.config.ts

+ 7 - 0
.claude/settings.local.json

@@ -0,0 +1,7 @@
+{
+  "permissions": {
+    "allow": [
+      "Bash(ls:*)"
+    ]
+  }
+}

+ 29 - 0
.gitignore

@@ -0,0 +1,29 @@
+# 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?
+
+# Environment variables
+.env
+.env.local
+.env.*.local

+ 1015 - 0
CrossBorder_OS_PRD.md

@@ -0,0 +1,1015 @@
+# CrossBorder OS(跨境运营中台)产品需求文档(PRD)
+
+| 字段       | 内容                            |
+| ---------- | ------------------------------- |
+| 产品名称   | CrossBorder OS(跨境运营中台)  |
+| 文档版本   | V1.0                            |
+| 创建日期   | 2026-04-19                      |
+| 文档状态   | Draft                           |
+| 文档作者   | 产品团队                        |
+| 适用范围   | 首期(PIM / OMS / SRM / 渠道适配) |
+
+---
+
+## 修订记录
+
+| 版本 | 日期       | 修改人   | 修改内容     |
+| ---- | ---------- | -------- | ------------ |
+| V1.0 | 2026-04-19 | 产品团队 | 初始版本创建 |
+
+---
+
+## 一、产品概述
+
+### 1.1 产品定位
+
+CrossBorder OS 是一套面向跨境电商企业的**运营中台系统**,核心目标为打通**商品、订单、供应商**三大业务域,统一接入 **Shopify** 与 **TikTok Shop** 两大销售渠道,形成完整的业务闭环:
+
+> **商品 → 上架 → 下单 → 履约 → 回传 → 复盘**
+
+### 1.2 项目背景与意义
+
+#### 1.2.1 行业痛点
+
+跨境电商企业日常运营面临以下核心问题:
+
+| 痛点         | 具体表现                                                     |
+| ------------ | ------------------------------------------------------------ |
+| 重复劳动     | 同一商品需在 Shopify、TikTok Shop 分别手动上架、改价、管理库存 |
+| 数据割裂     | 订单散落在各平台后台,无法统一查看和处理                     |
+| 库存超卖     | 多渠道卖同一个 SKU,无统一库存锁定机制,极易超卖             |
+| 采购盲目     | 库存预警靠人工盯,补货靠经验判断,容易断货或积压             |
+| 财务糊涂     | 采购成本、物流成本、平台佣金分散在各个系统,利润算不清       |
+
+#### 1.2.2 项目价值
+
+| 维度       | 没有这套系统                      | 有了这套系统                        |
+| ---------- | --------------------------------- | ----------------------------------- |
+| 上架效率   | 一个商品在两个平台各录一次        | 录一次,一键发布到多渠道            |
+| 订单处理   | 登录多个平台后台分别发货          | 一个看板处理所有渠道订单            |
+| 库存管理   | Excel 记账,手动同步各平台库存    | 系统自动锁库存、同步、预警          |
+| 补货决策   | 靠经验,容易断货或压货            | 安全库存预警 → 自动触发采购流程     |
+| 数据复盘   | 各平台数据导出 Excel 手动汇总     | 统一报表,按渠道/SKU/国家多维分析   |
+| 人力成本   | 运营、采购、仓库各干各的,信息滞后 | 全链路打通,减少沟通成本与出错率    |
+
+#### 1.2.3 核心理念
+
+> **这是一套让跨境卖家从"多平台手动搬砖"升级为"中台自动化运营"的基础设施。**
+>
+> 把分散在 Shopify、TikTok Shop 等平台的商品、订单、库存、采购数据统一收敛到一个中台,用标准化流程替代人工操作,让企业可以**规模化地多渠道经营**而不会被人效拖垮。
+
+#### 1.2.4 商业价值
+
+- **小团队做大生意** — 用系统替代人力,3-5 人团队也能管理多渠道、多国家的业务
+- **从卖货到做品牌** — 系统兜底运营效率,团队精力释放到品牌建设和产品开发上
+- **数据资产沉淀** — 全链路数字化意味着数据可追溯、业务可审计,为融资/并购提供数据基础
+
+### 1.3 产品目标
+
+| 目标维度     | 描述                                                              |
+| ------------ | ----------------------------------------------------------------- |
+| **统一管理** | 一套商品数据驱动多渠道上架,避免多平台重复维护                    |
+| **流程闭环** | 从采购补货到订单履约全链路数字化,降低人工干预                    |
+| **数据驱动** | 通过核心指标与报表辅助运营决策,提升库存周转与发货时效            |
+| **可扩展性** | 架构层面预留渠道扩展能力,后续可快速接入新平台                    |
+
+### 1.4 用户角色
+
+| 角色   | 职责描述                           | 核心使用场景                     |
+| ------ | ---------------------------------- | -------------------------------- |
+| 运营   | 负责商品上架、价格调整、活动管理   | 商品发布、渠道映射、定价管理     |
+| 采购   | 负责供应商对接、下单补货           | 采购单管理、到货确认、成本记录   |
+| 仓库   | 负责拣货、打包、发货               | 发货单处理、运单录入、库存盘点   |
+| 客服   | 负责订单查询、售后处理             | 订单状态查询、退款/退货/换货处理 |
+| 管理者 | 负责全盘数据监控与决策             | 报表查看、指标分析               |
+
+---
+
+## 二、范围定义
+
+### 2.1 In Scope(首期交付)
+
+| 模块               | 说明                                        |
+| ------------------ | ------------------------------------------- |
+| PIM(商品中台)    | SPU/SKU 建模、多语言、渠道映射、定价        |
+| OMS(订单中台)    | 多渠道订单接入、拆合单、状态机、售后        |
+| SRM(供应商中心)  | 供应商档案、采购单、供货能力管理            |
+| Channel Adapter    | Shopify / TikTok Shop 商品与订单双向同步    |
+| 基础 WMS 对接      | 库存读写、库存锁定与释放                    |
+| 发货回传           | 发货状态与物流单号回传至销售平台            |
+
+### 2.2 Out of Scope(后续迭代)
+
+- 财务结算(ERP 对接)
+- 广告投放管理
+- 全量海外仓系统(首期仅对接国内仓)
+
+---
+
+## 三、功能模块总览
+
+```
+CrossBorder OS
+├── 1. 商品中心(PIM)
+│   ├── 1.1 商品建模
+│   ├── 1.2 商品管理
+│   ├── 1.3 渠道映射
+│   └── 1.4 定价与库存
+├── 2. 订单中心(OMS)
+│   ├── 2.1 订单接入
+│   ├── 2.2 订单处理
+│   ├── 2.3 订单状态机
+│   └── 2.4 售后管理
+├── 3. 供应商中心(SRM)
+│   ├── 3.1 供应商管理
+│   ├── 3.2 采购管理
+│   └── 3.3 供货能力
+├── 4. 渠道中心(Adapter)
+│   ├── 4.1 Shopify Adapter
+│   └── 4.2 TikTok Shop Adapter
+├── 5. 库存与履约
+│   ├── 5.1 库存管理
+│   └── 5.2 发货管理
+├── 6. 数据与报表
+│   ├── 6.1 核心指标
+│   └── 6.2 报表中心
+└── 7. 系统与权限
+    ├── 7.1 RBAC 权限
+    ├── 7.2 操作日志
+    └── 7.3 API Key 管理
+```
+
+### 3.1 页面拆分说明
+
+为避免“功能模块”与“实际界面”之间的理解偏差,首期产品建议按页面级进行拆分。  
+基于当前 In Scope 范围,本系统首期建议设计为 **18 个界面**,其中:
+
+- **15 个业务操作界面**
+- **3 个系统管理界面**
+
+页面拆分的原则如下:
+
+- 一个模块可以对应多个界面,例如“订单中心”会拆分为订单列表、订单详情/处理、售后处理等界面
+- 技术能力型模块不强制独立成界面,例如状态机、Webhook、库存锁定更适合作为后台能力体现
+- 页面设计优先围绕用户任务展开,确保运营、采购、仓库、客服、管理者都能按流程完成工作
+
+### 3.2 页面清单与功能说明
+
+| 序号 | 界面名称 | 所属模块 | 主要功能说明 |
+| ---- | -------- | -------- | ------------ |
+| 1 | 商品列表页 | 商品中心(PIM) | 查看商品列表,按类目、状态、渠道筛选,支持批量上下架、批量导入、快速进入编辑页 |
+| 2 | 商品新建/编辑页 | 商品中心(PIM) | 创建 SPU/SKU,维护标题、属性、图片、视频、多语言信息,配置规格组合 |
+| 3 | 渠道映射页 | 商品中心(PIM) | 将内部 SKU 映射到 Shopify/TikTok 渠道 SKU,配置类目映射、属性映射,执行发布前校验 |
+| 4 | 定价与库存规则页 | 商品中心(PIM) | 配置多币种价格、渠道价、安全库存阈值,维护商品维度的价格与库存规则 |
+| 5 | 订单列表页 | 订单中心(OMS) | 汇总查看所有渠道订单,按订单状态、渠道、异常标记筛选,支持进入订单处理 |
+| 6 | 订单详情/处理页 | 订单中心(OMS) | 查看订单明细、商品、收货信息、支付信息,支持拆单、合单、备注、手动处理异常订单 |
+| 7 | 售后处理页 | 订单中心(OMS) | 处理退款、退货、换货申请,记录审核结果,并联动渠道退款或补发流程 |
+| 8 | 供应商管理页 | 供应商中心(SRM) | 维护供应商档案、联系人、合作状态、合同信息、评级信息 |
+| 9 | 采购单管理页 | 供应商中心(SRM) | 创建采购单(PO),关联供应商与 SKU,管理采购状态、到货确认、差异记录、采购成本 |
+| 10 | 供货能力配置页 | 供应商中心(SRM) | 维护供应商对 SKU 的交期、MOQ、阶梯价格,为补货与成本分析提供依据 |
+| 11 | 渠道接入配置页 | 渠道中心(Adapter) | 配置 Shopify、TikTok Shop 店铺授权、API 参数、Webhook、同步状态与异常提示 |
+| 12 | 库存总览页 | 库存与履约 | 查看可用库存、锁定库存、安全库存、预警状态,支持按仓库、SKU、渠道维度查询 |
+| 13 | 发货作业页 | 库存与履约 | 生成和查看发货单,支持仓库拣货、打包、录入物流单号与物流公司,并触发发货回传 |
+| 14 | 运营看板页 | 数据与报表 | 查看 GMV、订单量、库存周转、发货时效等核心经营指标,供管理层和运营快速复盘 |
+| 15 | 报表中心页 | 数据与报表 | 按渠道、SKU、国家、时间范围查看和导出报表,用于经营分析与复盘 |
+| 16 | 角色权限页 | 系统与权限 | 按角色配置模块权限和操作权限,控制不同岗位可见范围与可操作能力 |
+| 17 | 操作日志页 | 系统与权限 | 查询系统内关键操作记录,按模块、操作人、时间范围进行追溯 |
+| 18 | API Key 管理页 | 系统与权限 | 管理系统对接所需的 API Key,支持创建、停用、轮换与权限范围控制 |
+
+### 3.3 页面之间的衔接关系
+
+为了让模块衔接更清楚,可以把核心业务理解为以下三条页面链路:
+
+**1. 商品上架链路**
+
+`供货能力配置页/供应商管理页 → 商品新建/编辑页 → 渠道映射页 → 渠道接入配置页 → 商品列表页(查看发布结果)`
+
+说明:
+
+- 采购先维护供应商与 SKU 供货条件
+- 运营创建商品并完善商品资料
+- 运营完成渠道映射并执行平台校验
+- 系统通过渠道接入配置完成发布
+- 发布后可回到商品列表页查看上架状态
+
+**2. 订单履约链路**
+
+`渠道接入配置页 → 订单列表页 → 订单详情/处理页 → 库存总览页 → 发货作业页 → 订单列表页/订单详情页`
+
+说明:
+
+- 订单先通过渠道接入能力进入系统
+- 运营或客服在订单列表中识别待处理订单
+- 在订单详情页完成异常处理、拆单、合单等动作
+- 系统联动库存能力锁定或释放库存
+- 仓库在发货作业页完成发货并录入运单
+- 发货结果回写后,订单状态在订单列表和详情页可继续跟踪
+
+**3. 补货链路**
+
+`库存总览页(预警) → 采购单管理页 → 供应商管理页/供货能力配置页 → 到货入库 → 库存总览页`
+
+说明:
+
+- 系统在库存总览页中呈现低库存预警
+- 采购根据预警创建采购单
+- 采购过程中需要结合供应商信息与供货能力参数决策
+- 到货确认后库存更新,回到库存总览页查看预警是否解除
+
+### 3.4 页面结构建议
+
+从导航设计角度,建议左侧一级菜单直接按页面聚合为以下结构:
+
+- 商品中心:商品列表、商品新建/编辑、渠道映射、定价与库存规则
+- 订单中心:订单列表、订单详情/处理、售后处理
+- 供应商中心:供应商管理、采购单管理、供货能力配置
+- 渠道中心:渠道接入配置
+- 库存与履约:库存总览、发货作业
+- 数据与报表:运营看板、报表中心
+- 系统与权限:角色权限、操作日志、API Key 管理
+
+以上拆分可作为后续原型、菜单结构、权限点设计和开发任务拆分的直接依据。
+
+### 3.5 页面级功能明细
+
+以下内容用于把“页面”进一步细化到原型和开发可执行层面。每个页面至少需要明确页面目标、核心区域、关键字段和主要操作按钮。
+
+#### 3.5.1 商品中心(PIM)
+
+**界面 1:商品列表页**
+
+- 页面目标:集中查看商品状态,并作为商品编辑、上下架、导入、发布的统一入口
+- 核心区域:顶部筛选区、商品列表区、批量操作区、快捷统计区
+- 关键字段:SPU 编号、SKU 编号、商品标题、类目、渠道状态、上架状态、价格、库存、更新时间
+- 主要按钮:新建商品、批量导入、批量上架、批量下架、导出、查看详情、编辑、进入渠道映射
+
+**界面 2:商品新建/编辑页**
+
+- 页面目标:完成商品主数据录入,生成可用于渠道发布的标准化 SPU/SKU 数据
+- 核心区域:基础信息区、规格组合区、图片视频区、多语言区、物流属性区、保存发布区
+- 关键字段:商品标题、商品副标题、类目、品牌、属性模板、规格项、SKU 编码、条码、重量、尺寸、主图、详情图、视频、多语言文案
+- 主要按钮:保存草稿、保存并继续编辑、新增 SKU、删除 SKU、上传图片、上传视频、预览、提交校验
+
+**界面 3:渠道映射页**
+
+- 页面目标:建立内部商品与外部渠道商品的映射关系,并在发布前完成类目和属性校验
+- 核心区域:商品选择区、渠道选择区、类目映射区、属性映射区、校验结果区、发布记录区
+- 关键字段:内部 SKU、目标渠道、渠道店铺、渠道类目、渠道属性、渠道标题、渠道图片、渠道 SKU 编码、映射状态、校验结果
+- 主要按钮:新增映射、自动映射、保存映射、执行校验、发布到渠道、同步更新、查看失败原因
+
+**界面 4:定价与库存规则页**
+
+- 页面目标:维护商品在不同渠道和币种下的价格规则,以及安全库存和预警参数
+- 核心区域:价格规则区、渠道定价区、汇率参考区、安全库存区、规则变更记录区
+- 关键字段:SKU、币种、基准售价、渠道售价、折扣价、汇率、仓库、安全库存阈值、预警开关、生效时间
+- 主要按钮:新增价格规则、批量调价、保存规则、启用、停用、导入规则、查看变更记录
+
+#### 3.5.2 订单中心(OMS)
+
+**界面 5:订单列表页**
+
+- 页面目标:统一查看各渠道订单,并识别待处理、异常、待发货等重点订单
+- 核心区域:筛选检索区、订单列表区、异常标记区、批量操作区
+- 关键字段:订单号、渠道订单号、渠道来源、买家信息、订单状态、支付状态、发货状态、金额、下单时间、异常标签
+- 主要按钮:查看详情、批量标记、导出订单、刷新同步、分配处理人、进入售后
+
+**界面 6:订单详情/处理页**
+
+- 页面目标:处理单笔订单的履约动作和异常动作,沉淀订单全生命周期信息
+- 核心区域:订单摘要区、商品明细区、收货信息区、支付信息区、状态流转区、操作记录区、备注区
+- 关键字段:订单状态、订单商品、SKU、数量、收货人、地址、联系方式、支付方式、支付时间、仓库、运费、备注、风险标识
+- 主要按钮:拆单、合单、锁定库存、释放库存、修改备注、取消订单、提交仓库、查看日志
+
+**界面 7:售后处理页**
+
+- 页面目标:统一处理退款、退货、换货请求,并联动订单和渠道状态
+- 核心区域:售后单列表区、申请信息区、审核处理区、退款信息区、物流跟踪区
+- 关键字段:售后单号、关联订单号、售后类型、申请原因、申请金额、审核状态、退款方式、退货物流单号、入库状态
+- 主要按钮:审核通过、审核拒绝、发起退款、确认收货、生成补发单、关闭售后、查看原订单
+
+#### 3.5.3 供应商中心(SRM)
+
+**界面 8:供应商管理页**
+
+- 页面目标:维护供应商基础档案,作为采购、补货、成本核算的基础数据入口
+- 核心区域:供应商列表区、基础信息区、联系人区、合同信息区、评级记录区
+- 关键字段:供应商名称、联系人、联系方式、地址、合作状态、结算方式、合同编号、合同有效期、评级结果
+- 主要按钮:新建供应商、编辑、启用、停用、上传合同、更新评级、查看关联采购单
+
+**界面 9:采购单管理页**
+
+- 页面目标:完成采购单创建、跟进、到货确认和采购成本记录
+- 核心区域:采购单列表区、采购单表头区、SKU 明细区、到货确认区、成本记录区、状态流转区
+- 关键字段:采购单号、供应商、采购 SKU、采购数量、采购单价、预计交期、实到数量、差异数量、运费、关税、采购状态
+- 主要按钮:创建采购单、提交审批、通知供应商、确认到货、录入差异、关闭采购单、导出采购单
+
+**界面 10:供货能力配置页**
+
+- 页面目标:维护供应商对商品的供货约束条件,为补货决策提供参数支持
+- 核心区域:供应商选择区、SKU 供货能力列表区、交期配置区、MOQ 配置区、阶梯价格区
+- 关键字段:供应商、SKU、标准交期、最小起订量、价格区间、采购单位、默认供货标记、最近更新时间
+- 主要按钮:新增配置、批量导入、保存、设为默认供应商、停用配置、查看历史报价
+
+#### 3.5.4 渠道中心(Adapter)
+
+**界面 11:渠道接入配置页**
+
+- 页面目标:管理外部渠道授权、接口配置、Webhook 状态和同步任务状态
+- 核心区域:渠道账户列表区、授权信息区、接口配置区、Webhook 配置区、同步状态区、异常提示区
+- 关键字段:渠道名称、店铺名称、授权状态、API Key、Webhook 地址、最后同步时间、同步结果、错误信息
+- 主要按钮:新增店铺授权、重新授权、保存配置、测试连接、启用同步、停用同步、查看同步日志
+
+#### 3.5.5 库存与履约
+
+**界面 12:库存总览页**
+
+- 页面目标:统一查看库存分布、锁定情况和预警情况,作为补货和履约判断依据
+- 核心区域:库存筛选区、库存列表区、预警区、库存变动记录区
+- 关键字段:SKU、仓库、可用库存、锁定库存、在途库存、安全库存、预警状态、最近出入库时间
+- 主要按钮:查看明细、手动调整、导出库存、创建补货建议、查看库存日志
+
+**界面 13:发货作业页**
+
+- 页面目标:让仓库完成发货单处理,并把物流结果回传至订单和渠道
+- 核心区域:待发货列表区、发货单详情区、拣货信息区、打包信息区、物流录入区、回传结果区
+- 关键字段:发货单号、关联订单号、仓库、SKU、应发数量、实发数量、物流公司、运单号、发货状态、回传状态
+- 主要按钮:生成发货单、开始拣货、确认打包、录入运单号、确认发货、回传平台、打印面单
+
+#### 3.5.6 数据与报表
+
+**界面 14:运营看板页**
+
+- 页面目标:给运营和管理层提供经营总览,快速识别订单、库存、履约等关键异常
+- 核心区域:指标卡片区、趋势图区、渠道对比区、预警提示区、快捷入口区
+- 关键字段:GMV、订单量、支付转化、发货时效、退款率、库存周转天数、低库存商品数、异常订单数
+- 主要按钮:切换时间范围、切换渠道、查看明细报表、导出截图、跳转订单列表、跳转库存页
+
+**界面 15:报表中心页**
+
+- 页面目标:支持经营数据按不同维度分析、筛选、导出
+- 核心区域:报表目录区、查询条件区、结果表格区、图表区、导出区
+- 关键字段:报表类型、时间范围、渠道、国家、SKU、供应商、仓库、指标项、汇总口径
+- 主要按钮:生成报表、保存筛选条件、导出 Excel、导出 CSV、订阅报表
+
+#### 3.5.7 系统与权限
+
+**界面 16:角色权限页**
+
+- 页面目标:按角色控制用户可访问页面和可执行操作,保障岗位分工和操作安全
+- 核心区域:角色列表区、权限矩阵区、用户绑定区、变更记录区
+- 关键字段:角色名称、角色描述、模块权限、操作权限、绑定用户数、启用状态
+- 主要按钮:新建角色、编辑权限、复制角色、启用、停用、绑定用户、保存权限
+
+**界面 17:操作日志页**
+
+- 页面目标:追溯关键业务和系统操作,满足审计与问题定位需求
+- 核心区域:筛选区、日志列表区、日志详情侧栏
+- 关键字段:操作时间、操作人、所属模块、操作类型、对象 ID、变更前内容、变更后内容、来源 IP
+- 主要按钮:查询、重置筛选、导出日志、查看详情
+
+**界面 18:API Key 管理页**
+
+- 页面目标:统一管理内部或外部系统调用所需的 API Key,并控制安全策略
+- 核心区域:Key 列表区、Key 详情区、权限范围区、调用记录区
+- 关键字段:Key 名称、所属系统、权限范围、状态、创建时间、过期时间、最近调用时间
+- 主要按钮:创建 Key、复制 Key、停用、启用、轮换 Key、删除、查看调用记录
+
+### 3.6 原型输出建议
+
+为了便于后续推进,建议原型阶段至少对以下内容逐页产出:
+
+- 页面名称与页面目标
+- 页面布局草图
+- 筛选区字段
+- 列表区字段
+- 详情区字段
+- 主要按钮与操作说明
+- 页面跳转关系
+- 异常状态与空状态说明
+
+如需进一步细化,下一版可继续补充每个页面的字段校验规则、按钮权限规则和状态变化规则。
+
+### 3.7 页面字段级交互说明
+
+本节用于补充页面原型设计时的字段级交互要求。字段级说明重点覆盖 4 类内容:
+
+- 筛选字段:用户如何查找目标数据
+- 列表字段:用户在列表页最需要快速判断的信息
+- 表单字段:用户录入或修改信息时的输入项
+- 交互规则:按钮触发、字段联动、校验提示、状态反馈
+
+#### 3.7.1 商品列表页
+
+- 筛选字段:商品标题/关键词、SPU 编号、SKU 编号、类目、品牌、渠道、上架状态、创建时间、更新时间
+- 列表字段:主图、商品标题、SPU、SKU 数量、类目、渠道发布状态、总库存、价格区间、负责人、更新时间
+- 行内操作:查看、编辑、复制商品、上下架、进入映射、查看发布日志
+- 交互规则:选择类目后保留其他筛选条件;批量上架/下架仅对勾选数据生效;存在未完善字段的商品显示“待完善”标签;导入完成后展示成功/失败数量并支持下载失败明细
+
+#### 3.7.2 商品新建/编辑页
+
+- 基础字段:商品标题、商品副标题、类目、品牌、商品标签、属性模板、商品简介
+- SKU 字段:SKU 编码、规格值、条码、成本价、建议售价、重量、尺寸、包装尺寸
+- 媒体字段:主图、附图、详情图、视频、图片排序、图片 alt 文案
+- 多语言字段:语言版本、标题翻译、卖点翻译、详情翻译
+- 交互规则:选择类目后自动带出属性模板;新增规格后自动生成 SKU 组合;删除规格值时提示影响的 SKU 数量;主图必填;保存草稿允许缺少非必填字段,提交校验必须通过必填校验;离开未保存页面时弹出确认提示
+
+#### 3.7.3 渠道映射页
+
+- 筛选字段:商品标题、内部 SKU、渠道、店铺、映射状态、校验状态
+- 列表字段:内部 SKU、渠道 SKU、渠道类目、店铺、映射状态、最近同步时间、最近校验结果
+- 表单字段:目标渠道、目标店铺、渠道类目、渠道属性映射、渠道标题、渠道描述、渠道主图、渠道价格
+- 交互规则:切换渠道后动态刷新类目和属性模板;未完成类目映射前不可提交发布;校验失败字段需在字段旁展示错误原因;支持“自动映射后人工修正”;发布成功后记录平台返回 ID
+
+#### 3.7.4 定价与库存规则页
+
+- 筛选字段:SKU、渠道、币种、仓库、规则状态
+- 列表字段:SKU、币种、基准售价、渠道售价、折扣价、生效时间、安全库存、规则状态、最近修改人
+- 表单字段:币种、基础售价、渠道加价/减价规则、促销价、汇率来源、仓库、安全库存阈值、预警接收人、生效时间、失效时间
+- 交互规则:选择渠道后展示该渠道专属价格字段;促销价不得高于渠道售价;生效时间晚于当前时间时进入“待生效”状态;保存时生成变更记录;批量调价支持按百分比或固定金额调整
+
+#### 3.7.5 订单列表页
+
+- 筛选字段:订单号、渠道订单号、渠道、买家姓名、订单状态、支付状态、发货状态、异常标签、下单时间、仓库
+- 列表字段:订单号、渠道、买家、订单金额、商品件数、支付状态、订单状态、发货状态、仓库、异常标签、下单时间
+- 行内操作:查看详情、分配处理人、添加标签、导出、进入售后
+- 交互规则:异常订单高亮显示;筛选条件支持保存为常用视图;刷新同步后提示新增/更新订单数;勾选多条订单后才显示批量操作栏
+
+#### 3.7.6 订单详情/处理页
+
+- 展示字段:订单基础信息、支付信息、商品清单、仓库分配、收货地址、物流信息、订单备注、操作日志
+- 操作字段:取消原因、异常标签、人工备注、仓库选择、拆单方案、合单目标订单、客服备注
+- 交互规则:订单状态不同,按钮权限不同;取消订单需二次确认并填写原因;拆单时必须明确 SKU 与数量归属;修改地址后需记录修改前后值;锁定库存成功后状态更新为 allocated;所有人工处理动作写入日志
+
+#### 3.7.7 售后处理页
+
+- 筛选字段:售后单号、订单号、售后类型、审核状态、退款状态、申请时间
+- 列表字段:售后单号、订单号、买家、售后类型、申请金额、审核状态、退款状态、最近更新时间
+- 表单字段:售后原因、责任归属、退款金额、退款方式、退货物流单号、补发仓库、补发 SKU、审核备注
+- 交互规则:退款金额不得超过实付金额;部分退款需显示剩余可退额度;退货入库后才可完结“退货退款”类售后;换货通过后自动生成补发任务;拒绝售后必须填写原因
+
+#### 3.7.8 供应商管理页
+
+- 筛选字段:供应商名称、联系人、合作状态、评级、创建时间
+- 列表字段:供应商名称、联系人、手机号、合作状态、结算方式、评级、关联 SKU 数、最后更新时间
+- 表单字段:供应商名称、公司名称、联系人、电话、邮箱、地址、开户信息、税号、合同编号、合同附件、结算方式、备注
+- 交互规则:供应商名称不可重复;手机号和邮箱需格式校验;停用供应商前需检查是否存在未完成采购单;上传合同后保留版本记录
+
+#### 3.7.9 采购单管理页
+
+- 筛选字段:采购单号、供应商、采购状态、SKU、创建时间、预计交期
+- 列表字段:采购单号、供应商、SKU 数、采购总额、预计交期、到货进度、采购状态、创建人
+- 表单字段:供应商、仓库、采购 SKU、采购数量、采购单价、币种、税费、运费、预计交期、备注
+- 到货字段:实到数量、良品数量、差异数量、差异原因、入库时间、签收人
+- 交互规则:选定供应商后带出默认交期和 MOQ;采购数量低于 MOQ 时提示但允许提交审批;确认到货后自动更新库存;关闭采购单需校验未完成到货记录
+
+#### 3.7.10 供货能力配置页
+
+- 筛选字段:供应商、SKU、默认供货标记、状态
+- 列表字段:供应商、SKU、标准交期、MOQ、采购单位、阶梯价格、默认供货标记、状态
+- 表单字段:标准交期、MOQ、阶梯价格起始数量、阶梯价格、结算币种、采购单位、备注
+- 交互规则:同一供应商同一 SKU 允许存在多档阶梯价格;设置默认供货后需取消其他默认项;交期和 MOQ 不可小于 0;保存后同步用于补货建议计算
+
+#### 3.7.11 渠道接入配置页
+
+- 列表字段:渠道名称、店铺名称、授权状态、同步状态、最近同步时间、异常状态
+- 表单字段:店铺名称、App Key、App Secret、Access Token、Webhook 地址、Webhook Secret、同步开关、默认仓库、备注
+- 交互规则:测试连接成功后才允许启用同步;重新授权前保留旧配置快照;Webhook 地址支持一键复制;接口报错需展示最近一次错误信息和发生时间
+
+#### 3.7.12 库存总览页
+
+- 筛选字段:SKU、仓库、渠道、预警状态、库存状态
+- 列表字段:SKU、商品标题、仓库、可用库存、锁定库存、在途库存、安全库存、预警状态、最近变动时间
+- 详情字段:出入库来源、关联订单/采购单、操作人、变动数量、变动后库存
+- 交互规则:低于安全库存时显示红色预警;点击库存数字进入变动明细;手动调整库存需填写原因;创建补货建议时自动带入默认供应商与建议数量
+
+#### 3.7.13 发货作业页
+
+- 筛选字段:发货单号、订单号、仓库、发货状态、回传状态、创建时间
+- 列表字段:发货单号、订单号、仓库、SKU 数、应发件数、实发件数、物流公司、运单号、发货状态、回传状态
+- 表单字段:拣货人、打包人、物流公司、运单号、包裹重量、包裹尺寸、备注
+- 交互规则:确认发货前必须填写物流公司和运单号;实发数量与应发数量不一致时需给出差异提示;回传失败支持重试;打印面单前校验地址完整性
+
+#### 3.7.14 运营看板页
+
+- 过滤字段:时间范围、渠道、国家、店铺、仓库
+- 展示字段:GMV、支付订单数、客单价、退款率、发货及时率、低库存商品数、异常订单数
+- 交互规则:指标卡支持点击下钻;趋势图默认展示近 7 天,可切换近 30 天;异常指标高亮;点击低库存商品数跳转库存总览页并自动带入筛选
+
+#### 3.7.15 报表中心页
+
+- 查询字段:报表类型、统计口径、时间范围、渠道、店铺、SKU、供应商、仓库、国家
+- 结果字段:指标名称、指标值、环比、同比、维度分组值
+- 交互规则:切换报表类型时动态加载对应筛选项;导出前保留当前筛选条件;超大报表使用异步导出并在完成后通知用户;支持保存个人常用报表视图
+
+#### 3.7.16 角色权限页
+
+- 列表字段:角色名称、角色描述、用户数、启用状态、更新时间
+- 权限字段:页面访问权限、按钮权限、数据范围权限、特殊操作权限
+- 交互规则:复制角色时带出原权限配置;停用角色前校验是否仍绑定用户;保存权限后实时生效或提示重新登录策略;高风险权限需二次确认
+
+#### 3.7.17 操作日志页
+
+- 筛选字段:操作人、模块、操作类型、时间范围、对象 ID
+- 列表字段:操作时间、操作人、模块、操作动作、对象 ID、结果状态、来源 IP
+- 详情字段:变更前值、变更后值、请求来源、备注
+- 交互规则:支持按对象 ID 快速定位单据全链路日志;删除、取消、退款类操作应标红显示;日志详情优先展示关键变更字段
+
+#### 3.7.18 API Key 管理页
+
+- 列表字段:Key 名称、所属系统、权限范围、状态、创建时间、过期时间、最近调用时间
+- 表单字段:Key 名称、所属系统、权限范围、IP 白名单、过期时间、备注
+- 交互规则:Key 明文仅在创建成功时展示一次;复制 Key 需再次身份确认;轮换 Key 后旧 Key 可设置过渡失效时间;停用/删除操作需二次确认
+
+### 3.8 通用交互与状态说明
+
+为保证所有界面体验一致,建议全系统遵循以下通用交互规则:
+
+- 必填字段使用统一标识,失焦后即时校验,提交时再次统一校验
+- 列表页筛选条件在用户返回页面时默认保留
+- 所有批量操作必须显示影响条数和结果反馈
+- 高风险操作如删除、取消、退款、停用必须二次确认
+- 异步任务如导入、导出、发布、同步需提供处理状态和失败原因
+- 空状态页面应提供下一步建议,例如“去创建商品”“去配置店铺授权”
+- 权限不足时按钮置灰,并提示无权限原因
+- 关键业务动作写入操作日志,便于追溯
+- 表格列支持基础排序与自定义显示/隐藏
+- 重要状态使用统一颜色语义:成功、进行中、警告、失败
+
+以上字段级说明可作为低保真原型、UI 设计稿、接口设计和测试用例编写的共同基线。
+
+---
+
+## 四、详细功能设计
+
+### 4.1 商品中心(PIM)
+
+#### 4.1.1 商品建模
+
+| 功能点         | 说明                                                       | 优先级 |
+| -------------- | ---------------------------------------------------------- | ------ |
+| SPU/SKU 结构   | SPU 为商品聚合层,SKU 为最小售卖单元(颜色+尺码等维度组合) | P0     |
+| 属性模型       | 基于类目模板定义通用属性与特有属性,支持自定义属性扩展      | P0     |
+| 多语言支持     | 商品信息支持 EN / JP / EU 多语言,按渠道/地区下发对应语言   | P1     |
+
+**数据模型(简化):**
+
+```
+SPU
+├── spu_id           (SPU唯一标识)
+├── title            (多语言)
+├── category_id      (类目)
+├── attributes       (通用属性集合)
+└── sku_list[]
+    ├── sku_id       (SKU唯一标识)
+    ├── spec_values  (规格值,如 color=Red, size=M)
+    ├── barcode      (条形码)
+    ├── weight       (重量)
+    └── dimensions   (尺寸)
+```
+
+#### 4.1.2 商品管理
+
+| 功能点       | 说明                                                              | 优先级 |
+| ------------ | ----------------------------------------------------------------- | ------ |
+| 创建/编辑    | 支持新建商品、编辑商品信息、修改 SKU 规格                         | P0     |
+| 上下架       | 支持商品级别的上下架操作,可批量处理                               | P0     |
+| 图片/视频    | 支持上传多张图片与视频,按顺序排列,支持设置主图                   | P0     |
+| 批量导入     | 支持通过 CSV 文件批量导入商品数据,提供标准模板下载                 | P1     |
+
+#### 4.1.3 渠道映射
+
+| 功能点              | 说明                                                              | 优先级 |
+| ------------------- | ----------------------------------------------------------------- | ------ |
+| SKU → Shopify 映射  | 将内部 SKU 映射为 Shopify Product Variant,同步标题/图片/价格/库存 | P0     |
+| SKU → TikTok 映射   | 将内部 SKU 映射为 TikTok SKU,同步商品属性                        | P0     |
+| 类目映射            | 内部类目与平台类目映射,TikTok 需强校验类目合规性                  | P0     |
+
+**映射规则说明:**
+
+- 一个内部 SKU 可映射到多个渠道 SKU
+- 映射关系建立后,商品信息的变更(标题/图片/价格)可自动或手动同步至对应渠道
+- TikTok Shop 对类目与属性有严格校验,映射前需完成合规检查
+
+#### 4.1.4 定价与库存
+
+| 功能点     | 说明                                                              | 优先级 |
+| ---------- | ----------------------------------------------------------------- | ------ |
+| 多币种价格 | SKU 支持设置多币种售价(USD / EUR / JPY 等),基于汇率换算        | P0     |
+| 渠道价     | 同一 SKU 可按渠道设置不同售价,支持独立定价策略                    | P1     |
+| 安全库存   | 每个仓库/SKU 可设置安全库存阈值,低于阈值触发补货预警              | P1     |
+
+---
+
+### 4.2 订单中心(OMS)
+
+#### 4.2.1 订单接入
+
+| 功能点            | 说明                                                              | 优先级 |
+| ----------------- | ----------------------------------------------------------------- | ------ |
+| Shopify Webhook   | 接收 Shopify 订单创建/更新/取消 Webhook,自动同步至 OMS           | P0     |
+| TikTok Webhook/API | 通过 TikTok Shop API 拉取订单,支持 Webhook 实时推送              | P0     |
+| 幂等处理          | 基于 Order ID + Channel 做幂等校验,防止重复订单                  | P0     |
+
+#### 4.2.2 订单处理
+
+| 功能点   | 说明                                                              | 优先级 |
+| -------- | ----------------------------------------------------------------- | ------ |
+| 拆单     | 一个订单中包含多个仓库的 SKU 时,自动拆分为多个发货单             | P0     |
+| 合单     | 同一用户、同一收货地址的多个订单可合并发货,降低物流成本           | P1     |
+| 风控标记 | 系统自动标记异常订单(地址异常、金额异常、高频下单等)             | P1     |
+| 手动处理 | 运营/客服可手动介入处理异常订单,添加备注与标签                    | P0     |
+
+#### 4.2.3 订单状态机(核心)
+
+订单状态流转是 OMS 的核心逻辑,定义如下:
+
+```
+                        ┌──────────┐
+                        │ cancelled│
+                        └────▲─────┘
+                             │ (任意阶段可取消)
+┌─────────┐    ┌──────┐    ┌┴────────┐    ┌────────┐    ┌──────────┐    ┌──────────┐
+│ created │───▶│ paid │───▶│allocated│───▶│shipped │───▶│ delivered│───▶│completed │
+└─────────┘    └──────┘    └─────────┘    └────────┘    └──────────┘    └──────────┘
+                               │                                              │
+                               │         ┌───────────┐                        │
+                               └────────▶│ refunded  │◀───────────────────────┘
+                                         └───────────┘
+```
+
+| 状态      | 触发条件                       | 说明             |
+| --------- | ------------------------------ | ---------------- |
+| created   | 接收到渠道订单                 | 订单创建,待支付 |
+| paid      | 支付回调确认                   | 支付完成         |
+| allocated | 库存锁定成功                   | 已分配库存       |
+| shipped   | 物流单号录入并发货             | 已发货           |
+| delivered | 物流状态更新为已签收           | 已签收           |
+| completed | 签收后超时无售后请求或手动确认 | 订单完结         |
+| cancelled | 取消操作(任意阶段)           | 已取消           |
+| refunded  | 退款/退货审核通过             | 已退款           |
+
+**状态流转规则:**
+
+- 状态只能按定义路径单向流转(cancelled/refunded 除外)
+- 任意状态均可流转至 cancelled(需权限校验)
+- completed 状态可流转至 refunded(售后场景)
+- 每次状态变更需记录操作人、时间、来源
+
+#### 4.2.4 售后管理
+
+| 功能点 | 说明                                                              | 优先级 |
+| ------ | ----------------------------------------------------------------- | ------ |
+| 退款   | 支持全额/部分退款,审核通过后调用渠道退款 API                     | P0     |
+| 退货   | 支持退货申请、退货物流跟踪、入库确认                              | P1     |
+| 换货   | 支持换货申请,生成新发货单,关联原订单                            | P1     |
+
+---
+
+### 4.3 供应商中心(SRM)
+
+#### 4.3.1 供应商管理
+
+| 功能点   | 说明                                                              | 优先级 |
+| -------- | ----------------------------------------------------------------- | ------ |
+| 基本信息 | 供应商名称、联系人、联系方式、地址、合作状态等                    | P0     |
+| 合同管理 | 合同编号、有效期、结算条款、附件存储                              | P1     |
+| 评级     | 基于交期准时率、质量合格率、响应速度等维度评级(A/B/C/D)         | P1     |
+
+#### 4.3.2 采购管理
+
+| 功能点   | 说明                                                              | 优先级 |
+| -------- | ----------------------------------------------------------------- | ------ |
+| 采购单(PO) | 创建采购单,关联供应商与 SKU,记录采购数量、单价、交期        | P0     |
+| 到货确认 | 采购到货后确认入库数量,记录差异(短交/多交)                     | P0     |
+| 成本记录 | 记录采购成本(含运费、关税等),为利润分析提供数据支撑            | P1     |
+
+#### 4.3.3 供货能力
+
+| 功能点         | 说明                                                              | 优先级 |
+| -------------- | ----------------------------------------------------------------- | ------ |
+| 交期(Lead Time) | 记录供应商每个 SKU 的标准交货周期(天),用于补货计算           | P0     |
+| 最小起订量(MOQ) | 记录每个 SKU 的最小采购数量                                     | P0     |
+| 价格梯度       | 按采购量区间设定阶梯价格(如 100-500: $5, 500-1000: $4.5)        | P1     |
+
+---
+
+### 4.4 渠道中心(Adapter)
+
+#### 4.4.1 Shopify Adapter
+
+| 功能点   | 方向         | 说明                                                              | 优先级 |
+| -------- | ------------ | ----------------------------------------------------------------- | ------ |
+| 商品同步 | PIM → Shopify | 调用 Shopify Product API 创建/更新商品(Product + Variant)      | P0     |
+| 订单接入 | Shopify → OMS | 监听 Shopify Webhook,接收订单创建/更新/取消事件                  | P0     |
+| 发货回传 | OMS → Shopify | 调用 Shopify Fulfillment API 回传发货状态与物流单号               | P0     |
+| 库存同步 | 双向         | Shopify 库存变更同步至中台,中台库存变更同步至 Shopify            | P0     |
+
+**接口映射:**
+
+| 业务操作 | Shopify API                     |
+| -------- | ------------------------------- |
+| 创建商品 | `POST /admin/api/products.json` |
+| 更新商品 | `PUT /admin/api/products/{id}.json` |
+| 创建履约 | `POST /admin/api/fulfillments.json` |
+| 订单回调 | Webhook: `orders/create`        |
+
+#### 4.4.2 TikTok Shop Adapter
+
+| 功能点   | 方向            | 说明                                                              | 优先级 |
+| -------- | --------------- | ----------------------------------------------------------------- | ------ |
+| 商品同步 | PIM → TikTok    | 调用 TikTok Shop Product API 发布商品,需通过类目与属性强校验     | P0     |
+| 订单接入 | TikTok → OMS    | 通过 TikTok Shop API 拉取订单,支持 Webhook 实时通知              | P0     |
+| 发货回传 | OMS → TikTok    | 回传发货信息,需满足 SLA 时效要求,面单需符合平台规范             | P0     |
+| 类目校验 | 上架前          | 商品发布前自动校验 TikTok 类目合规性、必填属性完整性              | P0     |
+| 属性校验 | 上架前          | 校验商品属性是否符合 TikTok Shop 要求(标题长度、图片尺寸等)     | P0     |
+
+**TikTok Shop 特殊要求:**
+
+- 发货 SLA:订单支付后需在规定时间内发货(通常 48h-72h),超时将影响店铺评分
+- 面单规范:物流面单需包含 TikTok 规定的必填信息
+- 类目强校验:商品必须匹配 TikTok 官方类目树,属性需满足必填要求
+
+---
+
+### 4.5 库存与履约
+
+#### 4.5.1 库存管理
+
+| 功能点   | 说明                                                              | 优先级 |
+| -------- | ----------------------------------------------------------------- | ------ |
+| 实时库存 | 展示每个仓库/SKU 的当前可用库存、锁定库存、在途库存              | P0     |
+| 锁库存   | 订单支付后锁定对应 SKU 库存,防止超卖                            | P0     |
+| 释放库存 | 订单取消/超时未支付时释放已锁定库存                              | P0     |
+| 库存预警 | 库存低于安全阈值时自动触发预警通知,联动采购模块                  | P1     |
+
+**库存数据模型:**
+
+```
+Inventory
+├── warehouse_id      (仓库标识)
+├── sku_id            (SKU标识)
+├── available_qty     (可用库存)
+├── locked_qty        (锁定库存)
+├── in_transit_qty    (在途库存)
+├── safety_stock      (安全库存阈值)
+└── updated_at        (最后更新时间)
+```
+
+#### 4.5.2 发货管理
+
+| 功能点   | 说明                                                              | 优先级 |
+| -------- | ----------------------------------------------------------------- | ------ |
+| 发货单   | 根据订单自动生成发货单,包含 SKU 明细、收货地址、物流要求         | P0     |
+| 运单号   | 录入物流运单号,关联发货单与订单                                  | P0     |
+| 物流公司 | 支持对接主流跨境物流(DHL、FedEx 等),获取物流轨迹               | P1     |
+| 批量发货 | 支持批量录入运单号并批量回传至销售渠道                            | P1     |
+
+---
+
+### 4.6 数据与报表
+
+#### 4.6.1 核心指标
+
+| 指标       | 计算方式                                     | 维度               |
+| ---------- | -------------------------------------------- | ------------------ |
+| GMV        | 订单金额总和(含退款扣减)                   | 日/周/月、渠道、SKU |
+| 订单量     | 有效订单数量                                 | 日/周/月、渠道、国家 |
+| 发货时效   | 支付时间 → 发货时间的平均耗时                | 渠道、仓库         |
+| 库存周转   | 销售成本 / 平均库存价值                      | SKU、类目          |
+| 退款率     | 退款订单数 / 总订单数                        | 渠道、SKU          |
+
+#### 4.6.2 报表中心
+
+| 报表类型   | 说明                                              | 优先级 |
+| ---------- | ------------------------------------------------- | ------ |
+| 渠道报表   | 按 Shopify / TikTok 分别统计销售额、订单量、利润   | P1     |
+| SKU 报表   | 按单品维度统计销量、库存、周转率                   | P1     |
+| 国家报表   | 按收货国家统计订单分布、物流时效                   | P1     |
+| 自定义报表 | 支持自定义维度与指标组合,支持导出 CSV             | P2     |
+
+---
+
+### 4.7 系统与权限
+
+#### 4.7.1 RBAC 权限
+
+| 功能点   | 说明                                                              | 优先级 |
+| -------- | ----------------------------------------------------------------- | ------ |
+| 角色管理 | 预设角色(运营/采购/仓库/客服/管理者),支持自定义角色             | P0     |
+| 权限控制 | 基于功能模块 + 操作类型(查看/编辑/删除)进行细粒度权限控制       | P0     |
+| 数据权限 | 按渠道、仓库等维度控制数据可见范围                                | P1     |
+
+**预设角色权限矩阵:**
+
+| 功能模块   | 运营 | 采购 | 仓库 | 客服 | 管理者 |
+| ---------- | ---- | ---- | ---- | ---- | ------ |
+| 商品中心   | 读写 | 读   | -    | 读   | 读     |
+| 订单中心   | 读   | -    | 读写 | 读写 | 读     |
+| 供应商中心 | -    | 读写 | -    | -    | 读     |
+| 渠道中心   | 读写 | -    | -    | 读   | 读     |
+| 库存与履约 | 读   | 读   | 读写 | 读   | 读     |
+| 数据报表   | 读   | -    | -    | -    | 读写   |
+| 系统权限   | -    | -    | -    | -    | 读写   |
+
+#### 4.7.2 操作日志
+
+| 功能点     | 说明                                                              | 优先级 |
+| ---------- | ----------------------------------------------------------------- | ------ |
+| 日志记录   | 记录所有关键操作(创建/修改/删除/状态变更),含操作人、时间、内容 | P0     |
+| 日志查询   | 支持按模块、操作人、时间范围查询操作日志                          | P1     |
+| 日志导出   | 支持导出操作日志,便于审计                                        | P2     |
+
+#### 4.7.3 API Key 管理
+
+| 功能点     | 说明                                                              | 优先级 |
+| ---------- | ----------------------------------------------------------------- | ------ |
+| Key 管理   | 统一管理各渠道 API Key(Shopify AccessToken、TikTok AccessToken) | P0     |
+| Key 轮换   | 支持 API Key 的更新与轮换,不影响在线服务                         | P1     |
+| 权限绑定   | API Key 与系统账号绑定,操作通过 Key 执行时可追溯至具体人员       | P1     |
+
+---
+
+## 五、关键业务流程
+
+### 5.1 流程一:商品上架
+
+```
+┌──────────┐     ┌──────────┐     ┌──────────┐     ┌──────────┐     ┌──────────┐
+│ SRM 供货价│────▶│ PIM 建 SKU│────▶│ 渠道映射  │────▶│ 合规校验  │────▶│ 发布到渠道│
+└──────────┘     └──────────┘     └──────────┘     └──────────┘     └──────────┘
+                                                         │
+                                                         ▼
+                                               ┌──────────────────┐
+                                               │ Shopify Product   │
+                                               │ TikTok Product    │
+                                               └──────────────────┘
+```
+
+**流程说明:**
+
+1. 采购在 SRM 中录入供应商供货价格
+2. 运营在 PIM 中创建 SPU/SKU,完善商品信息(标题、图片、属性、多语言)
+3. 在渠道映射模块中,将 SKU 映射到 Shopify Variant / TikTok SKU
+4. 系统执行合规校验(TikTok 类目校验、属性校验等)
+5. 校验通过后,发布至对应渠道
+
+### 5.2 流程二:订单履约
+
+```
+┌──────────┐     ┌──────────┐     ┌──────────┐     ┌──────────┐     ┌──────────┐
+│ 用户下单  │────▶│ Webhook   │────▶│ OMS 接单  │────▶│ 锁定库存  │────▶│ 生成发货单│
+└──────────┘     │ 推送      │     └──────────┘     └──────────┘     └──────────┘
+                 └──────────┘                                           │
+                                                                        ▼
+                                                                 ┌──────────┐
+                              ┌──────────┐    ┌──────────┐       │ 仓库拣货  │
+                              │回传平台   │◀───│录入运单号 │◀──────│ 打包发货  │
+                              └──────────┘    └──────────┘       └──────────┘
+                                   │
+                                   ▼
+                          ┌──────────────────┐
+                          │ Shopify Fulfillment│
+                          │ TikTok Shipment   │
+                          └──────────────────┘
+```
+
+**流程说明:**
+
+1. 用户在 Shopify/TikTok 下单
+2. 渠道通过 Webhook 推送订单至 OMS
+3. OMS 接收订单,创建订单记录(状态:created → paid)
+4. 系统自动锁定对应 SKU 库存(allocated)
+5. 生成发货单,仓库进行拣货、打包、发货
+6. 仓库录入运单号与物流公司
+7. 系统自动回传发货状态至对应渠道(shipped)
+8. 物流签收后更新为 delivered,超时确认后 completed
+
+### 5.3 流程三:补货
+
+```
+┌──────────┐     ┌──────────┐     ┌──────────┐     ┌──────────┐     ┌──────────┐
+│ 库存预警  │────▶│ 生成采购单│────▶│ SRM 确认  │────▶│ 供应商发货│────▶│ 到货入库  │
+└──────────┘     └──────────┘     └──────────┘     └──────────┘     └──────────┘
+     │                                                                   │
+     ▼                                                                   ▼
+┌──────────────────┐                                          ┌──────────────┐
+│ 触发条件:        │                                          │ 库存更新       │
+│ 可用库存 < 安全库存│                                          │ 预警解除       │
+└──────────────────┘                                          └──────────────┘
+```
+
+**流程说明:**
+
+1. 系统监测到 SKU 可用库存低于安全库存阈值,触发预警
+2. 采购根据预警信息创建采购单(PO),关联供应商与 SKU
+3. 采购单确认后通知供应商
+4. 供应商按交期发货
+5. 到货后仓库确认入库,系统更新库存,预警解除
+
+---
+
+## 六、非功能性需求
+
+### 6.1 性能要求
+
+| 指标             | 要求               |
+| ---------------- | ------------------ |
+| 页面响应时间     | ≤ 2s(常规操作)   |
+| API 响应时间     | ≤ 500ms            |
+| Webhook 处理延迟 | ≤ 3s               |
+| 并发订单处理     | 支持 100+ TPS      |
+
+### 6.2 可用性要求
+
+| 指标     | 要求    |
+| -------- | ------- |
+| 系统可用性 | ≥ 99.9% |
+| 数据备份   | 每日全量备份 + 实时增量 |
+| 故障恢复   | RTO ≤ 1h, RPO ≤ 5min |
+
+### 6.3 安全要求
+
+- 所有 API 通信强制 HTTPS
+- API Key 加密存储,不可明文展示
+- 敏感操作(删除/取消/退款)需二次确认
+- 操作日志不可篡改,保留至少 180 天
+
+---
+
+## 七、集成接口清单
+
+### 7.1 外部接口
+
+| 渠道        | 接口类型   | 用途                    | 优先级 |
+| ----------- | ---------- | ----------------------- | ------ |
+| Shopify     | REST API   | 商品 CRUD               | P0     |
+| Shopify     | Webhook    | 订单事件通知            | P0     |
+| Shopify     | REST API   | 履约回传                | P0     |
+| TikTok Shop | REST API   | 商品发布与管理          | P0     |
+| TikTok Shop | Webhook    | 订单事件通知            | P0     |
+| TikTok Shop | REST API   | 发货回传                | P0     |
+| DHL         | REST API   | 物流轨迹查询            | P1     |
+| FedEx       | REST API   | 物流轨迹查询            | P1     |
+
+### 7.2 内部模块间接口
+
+| 源模块    | 目标模块    | 接口用途               |
+| --------- | ----------- | ---------------------- |
+| PIM       | Adapter     | 商品数据同步           |
+| Adapter   | OMS         | 订单数据写入           |
+| OMS       | Inventory   | 库存锁定/释放          |
+| OMS       | Adapter     | 发货状态回传           |
+| Inventory | SRM         | 库存预警触发采购       |
+| SRM       | Inventory   | 采购入库更新库存       |
+
+---
+
+## 八、优先级与里程碑
+
+| 阶段   | 交付内容                                         | 优先级覆盖 |
+| ------ | ------------------------------------------------ | ---------- |
+| Phase 1 | PIM 核心 + Shopify Adapter + OMS 基础 + 库存管理 | P0 核心功能 |
+| Phase 2 | TikTok Adapter + 售后 + SRM 采购流程              | P0 + P1    |
+| Phase 3 | 报表中心 + 批量操作 + 合单/拆单优化              | P1         |
+| Phase 4 | 自定义报表 + API Key 轮换 + 高级风控              | P2         |
+
+---
+
+## 九、风险与假设
+
+### 9.1 假设
+
+- Shopify 与 TikTok Shop API 稳定可用,无重大 Breaking Change
+- 客户已拥有 Shopify 店铺与 TikTok Shop 卖家账号
+- 首期对接国内单一仓库,后续扩展海外仓
+
+### 9.2 风险
+
+| 风险                          | 影响 | 缓解措施                                      |
+| ----------------------------- | ---- | --------------------------------------------- |
+| TikTok Shop API 频繁变动      | 高   | 封装 Adapter 层,变更隔离在 Adapter 内部      |
+| 跨渠道库存超卖                | 高   | 下单即时锁库存,库存扣减采用乐观锁/分布式锁    |
+| TikTok 类目审核不通过         | 中   | 上架前强校验,预审核机制                       |
+| 大促期间订单量激增            | 中   | 消息队列削峰,Webhook 异步处理                  |
+
+---
+
+## 附录
+
+### A. 术语表
+
+| 术语     | 全称                    | 说明                              |
+| -------- | ----------------------- | --------------------------------- |
+| PIM      | Product Information Management | 商品信息管理              |
+| OMS      | Order Management System | 订单管理系统                      |
+| SRM      | Supplier Relationship Management | 供应商关系管理       |
+| SPU      | Standard Product Unit   | 标准产品单元                      |
+| SKU      | Stock Keeping Unit      | 最小库存单元                      |
+| PO       | Purchase Order          | 采购订单                          |
+| MOQ      | Minimum Order Quantity  | 最小起订量                        |
+| GMV      | Gross Merchandise Volume | 成交总额                         |
+| RBAC     | Role-Based Access Control | 基于角色的访问控制               |
+| SLA      | Service Level Agreement | 服务等级协议                      |
+| TPS      | Transactions Per Second | 每秒事务数                        |
+| RTO      | Recovery Time Objective | 恢复时间目标                      |
+| RPO      | Recovery Point Objective | 恢复点目标                       |

+ 45 - 0
README.md

@@ -0,0 +1,45 @@
+# CrossBorder OS Console
+
+基于 `Vue 3 + TypeScript + Vite + Element Plus` 的跨境运营中台前端原型项目,按 PRD 中的 18 个页面进行结构化拆分,并使用本地 mock API 提供演示数据。
+
+## 为什么选 Element Plus
+
+- 更适合中后台业务场景:表格、表单、抽屉、弹窗、权限配置类页面成熟
+- Vue 3 + TypeScript 生态稳定,中文资料和组件案例丰富
+- 对这份 PRD 里的订单、库存、采购、权限这类管理页面匹配度高
+
+## 已实现内容
+
+- 18 个页面对应的路由与侧边导航结构
+- 基础中后台布局与视觉风格
+- 关键页面的表格/卡片/表单骨架
+- 字段级页面说明面板,可直接对照 PRD 继续扩展
+- 本地 mock backend:开发环境下拦截 `/api/*`
+
+## 启动命令
+
+```bash
+npm install
+npm run dev
+```
+
+生产构建:
+
+```bash
+npm run build
+```
+
+## 当前目录说明
+
+- `src/layout`:中后台主布局
+- `src/router`:路由和菜单配置
+- `src/mock`:mock 数据与 mock 路由
+- `src/config/page-specs.ts`:按 PRD 整理的字段级页面说明
+- `src/views`:页面实现
+
+## 下一步建议
+
+- 接入真实登录与权限控制
+- 将 `SpecOnlyPage` 中的页面逐步替换成真实业务页面
+- 引入图表库补充运营看板与报表中心
+- 将 mock API 切换为真实后端接口

+ 231 - 0
TODO.md

@@ -0,0 +1,231 @@
+# CrossBorder OS 前端实施计划
+
+## 现状总结
+
+### 已完成的基础设施(100%)
+- Vue 3 + TS + Vite + Element Plus + Pinia + Vue Router
+- 中后台布局(侧边栏、顶栏、角色菜单过滤)
+- Mock API 系统(完整的 mock 数据 + API 路由)
+- 登录认证 + RBAC 权限控制
+- API 请求封装层(request.ts + services.ts)
+- TypeScript 类型定义(page.ts)
+- 全局样式系统
+
+### 各页面实现程度
+
+| 页面 | 实现度 | 说明 |
+|------|--------|------|
+| 登录页 | 100% | 完整的登录 + 角色切换 |
+| 运营看板 | 100% | 指标卡片 + 告警 + 工作提示 |
+| 商品编辑 | 60% | 分步表单骨架,缺实际保存 |
+| 订单详情 | 70% | 有拆单/合单弹窗,缺真实逻辑 |
+| 其他列表页 | 30-40% | 只有基础表格 + SpecPageRenderer 占位 |
+
+---
+
+## 实施任务
+
+### 一、商品中心(4 页)
+
+- [ ] **商品列表页(ProductListView.vue)**
+  - [ ] 完整筛选区:关键词、SPU 编号、SKU 编号、类目、品牌、渠道、上架状态、更新时间
+  - [ ] 表格列:主图、商品标题、SPU、SKU 数量、类目、渠道发布状态、总库存、价格区间、负责人、更新时间
+  - [ ] 行内操作:查看、编辑、复制商品、上下架、进入映射、查看发布日志
+  - [ ] 批量操作:批量上架、批量下架、批量导出
+  - [ ] 批量导入弹窗(CSV 上传 + 结果反馈)
+  - [ ] 移除底部 SpecPageRenderer 占位
+
+- [ ] **商品编辑页(ProductEditorView.vue)**
+  - [ ] 完善基础信息表单:商品标题、副标题、类目、品牌、标签、简介
+  - [ ] 类目选择后自动带出属性模板
+  - [ ] 规格组合区:新增规格项 → 自动生成 SKU 组合表格
+  - [ ] SKU 编辑:编码、条码、成本价、建议售价、重量、尺寸
+  - [ ] 图片/视频上传区:主图、附图、详情图排序、视频
+  - [ ] 多语言区:语言版本选择、标题翻译、卖点翻译、详情翻译
+  - [ ] 保存草稿 / 保存并继续编辑 / 提交校验
+  - [ ] 离开未保存页面弹出确认提示
+  - [ ] 必填字段校验(主图必填)
+
+- [ ] **渠道映射页(新建 ProductMappingView.vue)**
+  - [ ] 映射列表:内部 SKU、渠道 SKU、渠道类目、店铺、映射状态、最近同步时间、校验结果
+  - [ ] 筛选区:商品标题、内部 SKU、渠道、店铺、映射状态、校验状态
+  - [ ] 新增/编辑映射弹窗:目标渠道、店铺、类目、属性映射、标题、描述、主图、价格
+  - [ ] 渠道切换后动态刷新类目和属性模板
+  - [ ] 自动映射按钮 + 人工修正
+  - [ ] 执行校验 → 展示校验结果(失败字段旁显示错误原因)
+  - [ ] 发布到渠道操作
+
+- [ ] **定价与库存规则页(新建 ProductPricingView.vue)**
+  - [ ] 规则列表:SKU、币种、基准售价、渠道售价、折扣价、生效时间、安全库存、规则状态、最近修改人
+  - [ ] 筛选区:SKU、渠道、币种、仓库、规则状态
+  - [ ] 新增/编辑规则弹窗:币种、基础售价、渠道加减价规则、促销价、汇率来源、仓库、安全库存阈值、预警接收人、生效/失效时间
+  - [ ] 批量调价:按百分比或固定金额
+  - [ ] 促销价不可高于渠道售价校验
+  - [ ] 启用/停用规则
+  - [ ] 每次保存生成变更记录
+
+### 二、订单中心(3 页)
+
+- [ ] **订单列表页(OrderListView.vue)**
+  - [ ] 完整筛选区:订单号、渠道订单号、渠道、买家姓名、订单状态、支付状态、发货状态、异常标签、下单时间、仓库
+  - [ ] 表格列:订单号、渠道、买家、订单金额、商品件数、支付状态、订单状态、发货状态、仓库、异常标签、下单时间
+  - [ ] 异常订单行高亮显示
+  - [ ] 行内操作:查看详情、分配处理人、添加标签、导出、进入售后
+  - [ ] 勾选后显示批量操作栏(批量标记、导出)
+  - [ ] 保存常用视图功能
+  - [ ] 刷新同步提示新增/更新数量
+  - [ ] 移除 SpecPageRenderer 占位
+
+- [ ] **订单详情页(OrderDetailView.vue)**
+  - [ ] 订单摘要区:订单号、状态、渠道、金额、时间
+  - [ ] 商品明细区:商品列表、SKU、数量、单价
+  - [ ] 收货信息区:收货人、地址、联系方式
+  - [ ] 支付信息区:支付方式、支付时间、金额
+  - [ ] 物流信息区:物流公司、运单号、状态
+  - [ ] 状态流转时间线
+  - [ ] 拆单弹窗:选择 SKU + 数量归属
+  - [ ] 合单弹窗:选择目标订单
+  - [ ] 取消订单:二次确认 + 填写原因
+  - [ ] 锁定/释放库存
+  - [ ] 修改备注
+  - [ ] 操作日志展示
+  - [ ] 不同状态显示不同操作按钮
+
+- [ ] **售后处理页(新建 OrderAfterSaleView.vue)**
+  - [ ] 售后单列表:售后单号、订单号、买家、售后类型、申请金额、审核状态、退款状态、最近更新时间
+  - [ ] 筛选区:售后单号、订单号、售后类型(退款/退货/换货)、审核状态、退款状态、申请时间
+  - [ ] 审核弹窗:通过/拒绝 + 审核备注
+  - [ ] 退款处理:退款金额(不超过实付)、退款方式、部分退款显示剩余额度
+  - [ ] 退货处理:退货物流单号、入库状态确认
+  - [ ] 换货处理:补发仓库、补发 SKU 选择
+  - [ ] 拒绝售后必须填写原因
+  - [ ] 关联原订单跳转
+
+### 三、供应商中心(3 页)
+
+- [ ] **供应商管理页(SupplierListView.vue)**
+  - [ ] 完整列表:供应商名称、联系人、手机号、合作状态、结算方式、评级、关联 SKU 数、最后更新时间
+  - [ ] 筛选区:供应商名称、联系人、合作状态、评级、创建时间
+  - [ ] 新建/编辑抽屉:供应商名称、公司名称、联系人、电话、邮箱、地址、开户信息、税号、合同编号、结算方式、备注
+  - [ ] 名称不可重复校验
+  - [ ] 手机号和邮箱格式校验
+  - [ ] 启用/停用(停用前检查未完成采购单)
+  - [ ] 合同附件上传
+  - [ ] 移除 SpecPageRenderer 占位
+
+- [ ] **采购单管理页(PurchaseOrderView.vue)**
+  - [ ] 采购单列表:采购单号、供应商、SKU 数、采购总额、预计交期、到货进度、采购状态、创建人
+  - [ ] 筛选区:采购单号、供应商、采购状态、SKU、创建时间、预计交期
+  - [ ] 创建采购单弹窗:供应商(带出默认交期和 MOQ)、仓库、采购 SKU、数量、单价、币种、税费、运费、预计交期、备注
+  - [ ] 低于 MOQ 提示但允许提交
+  - [ ] 到货确认弹窗:实到数量、良品数量、差异、原因、入库时间
+  - [ ] 到货后自动更新库存
+  - [ ] 状态流转:草稿 → 待审批 → 已审批 → 部分到货 → 已完成 → 已关闭
+  - [ ] 关闭前校验未完成到货记录
+  - [ ] 移除 SpecPageRenderer 占位
+
+- [ ] **供货能力配置页(新建 SupplyCapabilityView.vue)**
+  - [ ] 列表:供应商、SKU、标准交期、MOQ、采购单位、阶梯价格、默认供货标记、状态
+  - [ ] 筛选区:供应商、SKU、默认供货标记、状态
+  - [ ] 新增/编辑配置弹窗:标准交期、MOQ、阶梯价格(起始数量 + 价格,支持多档)、结算币种、采购单位、备注
+  - [ ] 设置默认供货后取消其他默认项
+  - [ ] 交期和 MOQ 不可小于 0
+  - [ ] 停用配置
+  - [ ] 移除 SpecPageRenderer 占位
+
+### 四、渠道中心(1 页)
+
+- [ ] **渠道接入配置页(ChannelConfigView.vue)**
+  - [ ] 渠道账户卡片列表:渠道名称、店铺名称、授权状态、同步状态、最近同步时间、异常状态
+  - [ ] 新增店铺授权弹窗/抽屉
+  - [ ] 配置表单:店铺名称、App Key、App Secret、Access Token、Webhook 地址、Webhook Secret、同步开关、默认仓库、备注
+  - [ ] 测试连接按钮(成功后才允许启用同步)
+  - [ ] 重新授权(保留旧配置快照)
+  - [ ] Webhook 地址一键复制
+  - [ ] 同步状态展示 + 错误信息
+  - [ ] 启用/停用同步
+  - [ ] 移除 SpecPageRenderer 占位
+
+### 五、库存与履约(2 页)
+
+- [ ] **库存总览页(InventoryOverviewView.vue)**
+  - [ ] 完整列表:SKU、商品标题、仓库、可用库存、锁定库存、在途库存、安全库存、预警状态、最近变动时间
+  - [ ] 筛选区:SKU、仓库、渠道、预警状态、库存状态
+  - [ ] 低于安全库存红色预警高亮
+  - [ ] 点击库存数字进入变动明细抽屉:出入库来源、关联单号、操作人、变动数量、变动后库存
+  - [ ] 手动调整库存弹窗(必须填写原因)
+  - [ ] 创建补货建议(自动带入默认供应商和建议数量)
+  - [ ] 导出库存
+  - [ ] 移除 SpecPageRenderer 占位
+
+- [ ] **发货作业页(ShippingWorkView.vue)**
+  - [ ] 发货单列表:发货单号、订单号、仓库、SKU 数、应发件数、实发件数、物流公司、运单号、发货状态、回传状态
+  - [ ] 筛选区:发货单号、订单号、仓库、发货状态、回传状态、创建时间
+  - [ ] 发货单详情弹窗/抽屉
+  - [ ] 拣货/打包/录入运单号流程
+  - [ ] 确认发货前必须填写物流公司和运单号
+  - [ ] 实发和应发不一致时差异提示
+  - [ ] 确认发货 → 回传平台
+  - [ ] 回传失败支持重试
+  - [ ] 移除 SpecPageRenderer 占位
+
+### 六、数据与报表(2 页)
+
+- [ ] **运营看板页(ReportDashboardView.vue)**
+  - [ ] 补充趋势图(引入 ECharts):GMV、订单量趋势
+  - [ ] 7 天 / 30 天切换
+  - [ ] 指标卡点击下钻
+  - [ ] 异常指标高亮
+  - [ ] 点击低库存商品数跳转库存页(带筛选)
+
+- [ ] **报表中心页(ReportCenterView.vue)**
+  - [ ] 报表类型选择(销售、库存、采购、售后等)
+  - [ ] 切换报表类型时动态加载筛选项
+  - [ ] 查询条件区:统计口径、时间范围、渠道、店铺、SKU、供应商、仓库、国家
+  - [ ] 结果表格:指标名称、指标值、环比、同比、维度分组值
+  - [ ] 导出 Excel / CSV(保留当前筛选条件)
+  - [ ] 保存个人常用视图
+  - [ ] 移除 SpecPageRenderer 占位
+
+### 七、系统与权限(3 页)
+
+- [ ] **角色权限页(RolePermissionView.vue)**
+  - [ ] 角色列表:角色名称、角色描述、用户数、启用状态、更新时间
+  - [ ] 筛选区:角色名称、角色状态、更新时间
+  - [ ] 新建角色 / 编辑权限弹窗
+  - [ ] 权限矩阵树:页面访问权限、按钮权限、数据范围权限、特殊操作权限
+  - [ ] 复制角色(带出原权限配置)
+  - [ ] 停用前校验绑定用户
+  - [ ] 高风险权限二次确认
+  - [ ] 移除 SpecPageRenderer 占位
+
+- [ ] **操作日志页(OperationLogView.vue)**
+  - [ ] 完整列表:操作时间、操作人、模块、操作动作、对象 ID、结果状态、来源 IP
+  - [ ] 筛选区:操作人、模块、操作类型、时间范围、对象 ID
+  - [ ] 日志详情侧栏:变更前值、变更后值、请求来源、备注
+  - [ ] 删除/取消/退款类操作标红
+  - [ ] 按对象 ID 快速定位单据全链路日志
+  - [ ] 导出日志(保留筛选条件)
+  - [ ] 移除 SpecPageRenderer 占位
+
+- [ ] **API Key 管理页(ApiKeyView.vue)**
+  - [ ] Key 列表:Key 名称、所属系统、权限范围、状态、创建时间、过期时间、最近调用时间
+  - [ ] 创建 Key 弹窗:Key 名称、所属系统、权限范围、IP 白名单、过期时间、备注
+  - [ ] 明文 Key 仅创建成功时展示一次
+  - [ ] 复制 Key 需再次确认
+  - [ ] 停用/启用 Key
+  - [ ] 轮换 Key(支持过渡失效时间)
+  - [ ] 删除 Key 二次确认
+  - [ ] 移除 SpecPageRenderer 占位
+
+---
+
+## 跨页面通用工作
+
+- [ ] **补充 Mock API 路由** — 为所有 CRUD 操作补充 POST/PUT/DELETE 路由(在 mock/routes.ts 中)
+- [ ] **补充 API Services** — 在 api/services.ts 中补充所有新增的接口调用方法
+- [ ] **补充 TypeScript 类型** — 在 types/page.ts 中补充缺失的类型定义(渠道映射、定价规则、售后单、供货能力等)
+- [ ] **引入 ECharts** — 为运营看板和报表中心添加图表支持(package.json 添加 echarts 依赖)
+- [ ] **新增页面注册路由** — 在 router/routes.ts 中注册新建的页面路由(渠道映射、定价、售后、供货能力)
+- [ ] **更新菜单配置** — 在 router/menu.ts 中添加新页面的菜单项
+- [ ] **移除 SpecPageRenderer** — 所有页面完成后移除 SpecPageRenderer 组件及相关引用

+ 12 - 0
index.html

@@ -0,0 +1,12 @@
+<!doctype html>
+<html lang="zh-CN">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>CrossBorder OS Console</title>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script type="module" src="/src/main.ts"></script>
+  </body>
+  </html>

+ 2281 - 0
package-lock.json

@@ -0,0 +1,2281 @@
+{
+  "name": "crossborder-os-console",
+  "version": "0.1.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "crossborder-os-console",
+      "version": "0.1.0",
+      "dependencies": {
+        "@element-plus/icons-vue": "^2.3.1",
+        "element-plus": "^2.10.0",
+        "pinia": "^3.0.1",
+        "vue": "^3.5.13",
+        "vue-router": "^4.5.1"
+      },
+      "devDependencies": {
+        "@types/node": "^22.15.3",
+        "@vitejs/plugin-vue": "^5.2.3",
+        "sass": "^1.87.0",
+        "typescript": "^5.8.3",
+        "vite": "^6.3.3",
+        "vue-tsc": "^2.2.8"
+      }
+    },
+    "node_modules/@babel/helper-string-parser": {
+      "version": "7.27.1",
+      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+      "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-validator-identifier": {
+      "version": "7.28.5",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+      "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/parser": {
+      "version": "7.29.2",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz",
+      "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.29.0"
+      },
+      "bin": {
+        "parser": "bin/babel-parser.js"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@babel/types": {
+      "version": "7.29.0",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
+      "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-string-parser": "^7.27.1",
+        "@babel/helper-validator-identifier": "^7.28.5"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@ctrl/tinycolor": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.2.0.tgz",
+      "integrity": "sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A==",
+      "license": "MIT",
+      "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==",
+      "license": "MIT",
+      "peerDependencies": {
+        "vue": "^3.2.0"
+      }
+    },
+    "node_modules/@esbuild/aix-ppc64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
+      "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "aix"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-arm": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
+      "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-arm64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
+      "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-x64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
+      "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/darwin-arm64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
+      "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/darwin-x64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
+      "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/freebsd-arm64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
+      "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/freebsd-x64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
+      "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-arm": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
+      "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-arm64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
+      "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-ia32": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
+      "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-loong64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
+      "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-mips64el": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
+      "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
+      "cpu": [
+        "mips64el"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-ppc64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
+      "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-riscv64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
+      "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-s390x": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
+      "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-x64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
+      "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/netbsd-arm64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
+      "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/netbsd-x64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
+      "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openbsd-arm64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
+      "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openbsd-x64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
+      "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openharmony-arm64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
+      "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openharmony"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/sunos-x64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
+      "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "sunos"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-arm64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
+      "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-ia32": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
+      "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-x64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
+      "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@floating-ui/core": {
+      "version": "1.7.5",
+      "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz",
+      "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@floating-ui/utils": "^0.2.11"
+      }
+    },
+    "node_modules/@floating-ui/dom": {
+      "version": "1.7.6",
+      "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz",
+      "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@floating-ui/core": "^1.7.5",
+        "@floating-ui/utils": "^0.2.11"
+      }
+    },
+    "node_modules/@floating-ui/utils": {
+      "version": "0.2.11",
+      "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz",
+      "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==",
+      "license": "MIT"
+    },
+    "node_modules/@jridgewell/sourcemap-codec": {
+      "version": "1.5.5",
+      "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+      "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+      "license": "MIT"
+    },
+    "node_modules/@parcel/watcher": {
+      "version": "2.5.6",
+      "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz",
+      "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "detect-libc": "^2.0.3",
+        "is-glob": "^4.0.3",
+        "node-addon-api": "^7.0.0",
+        "picomatch": "^4.0.3"
+      },
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      },
+      "optionalDependencies": {
+        "@parcel/watcher-android-arm64": "2.5.6",
+        "@parcel/watcher-darwin-arm64": "2.5.6",
+        "@parcel/watcher-darwin-x64": "2.5.6",
+        "@parcel/watcher-freebsd-x64": "2.5.6",
+        "@parcel/watcher-linux-arm-glibc": "2.5.6",
+        "@parcel/watcher-linux-arm-musl": "2.5.6",
+        "@parcel/watcher-linux-arm64-glibc": "2.5.6",
+        "@parcel/watcher-linux-arm64-musl": "2.5.6",
+        "@parcel/watcher-linux-x64-glibc": "2.5.6",
+        "@parcel/watcher-linux-x64-musl": "2.5.6",
+        "@parcel/watcher-win32-arm64": "2.5.6",
+        "@parcel/watcher-win32-ia32": "2.5.6",
+        "@parcel/watcher-win32-x64": "2.5.6"
+      }
+    },
+    "node_modules/@parcel/watcher-android-arm64": {
+      "version": "2.5.6",
+      "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz",
+      "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-darwin-arm64": {
+      "version": "2.5.6",
+      "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz",
+      "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-darwin-x64": {
+      "version": "2.5.6",
+      "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz",
+      "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-freebsd-x64": {
+      "version": "2.5.6",
+      "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz",
+      "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-linux-arm-glibc": {
+      "version": "2.5.6",
+      "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz",
+      "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-linux-arm-musl": {
+      "version": "2.5.6",
+      "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz",
+      "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-linux-arm64-glibc": {
+      "version": "2.5.6",
+      "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz",
+      "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-linux-arm64-musl": {
+      "version": "2.5.6",
+      "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz",
+      "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-linux-x64-glibc": {
+      "version": "2.5.6",
+      "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz",
+      "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-linux-x64-musl": {
+      "version": "2.5.6",
+      "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz",
+      "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-win32-arm64": {
+      "version": "2.5.6",
+      "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz",
+      "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-win32-ia32": {
+      "version": "2.5.6",
+      "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz",
+      "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-win32-x64": {
+      "version": "2.5.6",
+      "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz",
+      "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "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==",
+      "license": "MIT",
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/popperjs"
+      }
+    },
+    "node_modules/@rollup/rollup-android-arm-eabi": {
+      "version": "4.60.2",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz",
+      "integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ]
+    },
+    "node_modules/@rollup/rollup-android-arm64": {
+      "version": "4.60.2",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz",
+      "integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ]
+    },
+    "node_modules/@rollup/rollup-darwin-arm64": {
+      "version": "4.60.2",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz",
+      "integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@rollup/rollup-darwin-x64": {
+      "version": "4.60.2",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz",
+      "integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@rollup/rollup-freebsd-arm64": {
+      "version": "4.60.2",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz",
+      "integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-freebsd-x64": {
+      "version": "4.60.2",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz",
+      "integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+      "version": "4.60.2",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz",
+      "integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+      "version": "4.60.2",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz",
+      "integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm64-gnu": {
+      "version": "4.60.2",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz",
+      "integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm64-musl": {
+      "version": "4.60.2",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz",
+      "integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-loong64-gnu": {
+      "version": "4.60.2",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz",
+      "integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-loong64-musl": {
+      "version": "4.60.2",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz",
+      "integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+      "version": "4.60.2",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz",
+      "integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-ppc64-musl": {
+      "version": "4.60.2",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz",
+      "integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+      "version": "4.60.2",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz",
+      "integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-riscv64-musl": {
+      "version": "4.60.2",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz",
+      "integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-s390x-gnu": {
+      "version": "4.60.2",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz",
+      "integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-x64-gnu": {
+      "version": "4.60.2",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz",
+      "integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-x64-musl": {
+      "version": "4.60.2",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz",
+      "integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-openbsd-x64": {
+      "version": "4.60.2",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz",
+      "integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-openharmony-arm64": {
+      "version": "4.60.2",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz",
+      "integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openharmony"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-arm64-msvc": {
+      "version": "4.60.2",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz",
+      "integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-ia32-msvc": {
+      "version": "4.60.2",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz",
+      "integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-x64-gnu": {
+      "version": "4.60.2",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz",
+      "integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-x64-msvc": {
+      "version": "4.60.2",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz",
+      "integrity": "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@types/estree": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+      "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/lodash": {
+      "version": "4.17.24",
+      "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz",
+      "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==",
+      "license": "MIT"
+    },
+    "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==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/lodash": "*"
+      }
+    },
+    "node_modules/@types/node": {
+      "version": "22.19.17",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz",
+      "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "undici-types": "~6.21.0"
+      }
+    },
+    "node_modules/@types/web-bluetooth": {
+      "version": "0.0.20",
+      "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz",
+      "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==",
+      "license": "MIT"
+    },
+    "node_modules/@vitejs/plugin-vue": {
+      "version": "5.2.4",
+      "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz",
+      "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^18.0.0 || >=20.0.0"
+      },
+      "peerDependencies": {
+        "vite": "^5.0.0 || ^6.0.0",
+        "vue": "^3.2.25"
+      }
+    },
+    "node_modules/@volar/language-core": {
+      "version": "2.4.15",
+      "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.15.tgz",
+      "integrity": "sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@volar/source-map": "2.4.15"
+      }
+    },
+    "node_modules/@volar/source-map": {
+      "version": "2.4.15",
+      "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.15.tgz",
+      "integrity": "sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@volar/typescript": {
+      "version": "2.4.15",
+      "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.15.tgz",
+      "integrity": "sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@volar/language-core": "2.4.15",
+        "path-browserify": "^1.0.1",
+        "vscode-uri": "^3.0.8"
+      }
+    },
+    "node_modules/@vue/compiler-core": {
+      "version": "3.5.32",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.32.tgz",
+      "integrity": "sha512-4x74Tbtqnda8s/NSD6e1Dr5p1c8HdMU5RWSjMSUzb8RTcUQqevDCxVAitcLBKT+ie3o0Dl9crc/S/opJM7qBGQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.29.2",
+        "@vue/shared": "3.5.32",
+        "entities": "^7.0.1",
+        "estree-walker": "^2.0.2",
+        "source-map-js": "^1.2.1"
+      }
+    },
+    "node_modules/@vue/compiler-dom": {
+      "version": "3.5.32",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.32.tgz",
+      "integrity": "sha512-ybHAu70NtiEI1fvAUz3oXZqkUYEe5J98GjMDpTGl5iHb0T15wQYLR4wE3h9xfuTNA+Cm2f4czfe8B4s+CCH57Q==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-core": "3.5.32",
+        "@vue/shared": "3.5.32"
+      }
+    },
+    "node_modules/@vue/compiler-sfc": {
+      "version": "3.5.32",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.32.tgz",
+      "integrity": "sha512-8UYUYo71cP/0YHMO814TRZlPuUUw3oifHuMR7Wp9SNoRSrxRQnhMLNlCeaODNn6kNTJsjFoQ/kqIj4qGvya4Xg==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.29.2",
+        "@vue/compiler-core": "3.5.32",
+        "@vue/compiler-dom": "3.5.32",
+        "@vue/compiler-ssr": "3.5.32",
+        "@vue/shared": "3.5.32",
+        "estree-walker": "^2.0.2",
+        "magic-string": "^0.30.21",
+        "postcss": "^8.5.8",
+        "source-map-js": "^1.2.1"
+      }
+    },
+    "node_modules/@vue/compiler-ssr": {
+      "version": "3.5.32",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.32.tgz",
+      "integrity": "sha512-Gp4gTs22T3DgRotZ8aA/6m2jMR+GMztvBXUBEUOYOcST+giyGWJ4WvFd7QLHBkzTxkfOt8IELKNdpzITLbA2rw==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-dom": "3.5.32",
+        "@vue/shared": "3.5.32"
+      }
+    },
+    "node_modules/@vue/compiler-vue2": {
+      "version": "2.7.16",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz",
+      "integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "de-indent": "^1.0.2",
+        "he": "^1.2.0"
+      }
+    },
+    "node_modules/@vue/devtools-api": {
+      "version": "7.7.9",
+      "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.9.tgz",
+      "integrity": "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/devtools-kit": "^7.7.9"
+      }
+    },
+    "node_modules/@vue/devtools-kit": {
+      "version": "7.7.9",
+      "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.9.tgz",
+      "integrity": "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/devtools-shared": "^7.7.9",
+        "birpc": "^2.3.0",
+        "hookable": "^5.5.3",
+        "mitt": "^3.0.1",
+        "perfect-debounce": "^1.0.0",
+        "speakingurl": "^14.0.1",
+        "superjson": "^2.2.2"
+      }
+    },
+    "node_modules/@vue/devtools-shared": {
+      "version": "7.7.9",
+      "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.9.tgz",
+      "integrity": "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==",
+      "license": "MIT",
+      "dependencies": {
+        "rfdc": "^1.4.1"
+      }
+    },
+    "node_modules/@vue/language-core": {
+      "version": "2.2.12",
+      "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.2.12.tgz",
+      "integrity": "sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@volar/language-core": "2.4.15",
+        "@vue/compiler-dom": "^3.5.0",
+        "@vue/compiler-vue2": "^2.7.16",
+        "@vue/shared": "^3.5.0",
+        "alien-signals": "^1.0.3",
+        "minimatch": "^9.0.3",
+        "muggle-string": "^0.4.1",
+        "path-browserify": "^1.0.1"
+      },
+      "peerDependencies": {
+        "typescript": "*"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@vue/reactivity": {
+      "version": "3.5.32",
+      "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.32.tgz",
+      "integrity": "sha512-/ORasxSGvZ6MN5gc+uE364SxFdJ0+WqVG0CENXaGW58TOCdrAW76WWaplDtECeS1qphvtBZtR+3/o1g1zL4xPQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/shared": "3.5.32"
+      }
+    },
+    "node_modules/@vue/runtime-core": {
+      "version": "3.5.32",
+      "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.32.tgz",
+      "integrity": "sha512-pDrXCejn4UpFDFmMd27AcJEbHaLemaE5o4pbb7sLk79SRIhc6/t34BQA7SGNgYtbMnvbF/HHOftYBgFJtUoJUQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/reactivity": "3.5.32",
+        "@vue/shared": "3.5.32"
+      }
+    },
+    "node_modules/@vue/runtime-dom": {
+      "version": "3.5.32",
+      "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.32.tgz",
+      "integrity": "sha512-1CDVv7tv/IV13V8Nip1k/aaObVbWqRlVCVezTwx3K07p7Vxossp5JU1dcPNhJk3w347gonIUT9jQOGutyJrSVQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/reactivity": "3.5.32",
+        "@vue/runtime-core": "3.5.32",
+        "@vue/shared": "3.5.32",
+        "csstype": "^3.2.3"
+      }
+    },
+    "node_modules/@vue/server-renderer": {
+      "version": "3.5.32",
+      "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.32.tgz",
+      "integrity": "sha512-IOjm2+JQwRFS7W28HNuJeXQle9KdZbODFY7hFGVtnnghF51ta20EWAZJHX+zLGtsHhaU6uC9BGPV52KVpYryMQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-ssr": "3.5.32",
+        "@vue/shared": "3.5.32"
+      },
+      "peerDependencies": {
+        "vue": "3.5.32"
+      }
+    },
+    "node_modules/@vue/shared": {
+      "version": "3.5.32",
+      "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.32.tgz",
+      "integrity": "sha512-ksNyrmRQzWJJ8n3cRDuSF7zNNontuJg1YHnmWRJd2AMu8Ij2bqwiiri2lH5rHtYPZjj4STkNcgcmiQqlOjiYGg==",
+      "license": "MIT"
+    },
+    "node_modules/@vueuse/core": {
+      "version": "12.0.0",
+      "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-12.0.0.tgz",
+      "integrity": "sha512-C12RukhXiJCbx4MGhjmd/gH52TjJsc3G0E0kQj/kb19H3Nt6n1CA4DRWuTdWWcaFRdlTe0npWDS942mvacvNBw==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/web-bluetooth": "^0.0.20",
+        "@vueuse/metadata": "12.0.0",
+        "@vueuse/shared": "12.0.0",
+        "vue": "^3.5.13"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      }
+    },
+    "node_modules/@vueuse/metadata": {
+      "version": "12.0.0",
+      "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-12.0.0.tgz",
+      "integrity": "sha512-Yzimd1D3sjxTDOlF05HekU5aSGdKjxhuhRFHA7gDWLn57PRbBIh+SF5NmjhJ0WRgF3my7T8LBucyxdFJjIfRJQ==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      }
+    },
+    "node_modules/@vueuse/shared": {
+      "version": "12.0.0",
+      "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-12.0.0.tgz",
+      "integrity": "sha512-3i6qtcq2PIio5i/vVYidkkcgvmTjCqrf26u+Fd4LhnbBmIT6FN8y6q/GJERp8lfcB9zVEfjdV0Br0443qZuJpw==",
+      "license": "MIT",
+      "dependencies": {
+        "vue": "^3.5.13"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      }
+    },
+    "node_modules/alien-signals": {
+      "version": "1.0.13",
+      "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-1.0.13.tgz",
+      "integrity": "sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "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==",
+      "license": "MIT"
+    },
+    "node_modules/balanced-match": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+      "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "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==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      }
+    },
+    "node_modules/brace-expansion": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz",
+      "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^1.0.0"
+      }
+    },
+    "node_modules/chokidar": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
+      "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "readdirp": "^4.0.1"
+      },
+      "engines": {
+        "node": ">= 14.16.0"
+      },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
+    "node_modules/copy-anything": {
+      "version": "4.0.5",
+      "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz",
+      "integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==",
+      "license": "MIT",
+      "dependencies": {
+        "is-what": "^5.2.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/mesqueeb"
+      }
+    },
+    "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==",
+      "license": "MIT"
+    },
+    "node_modules/dayjs": {
+      "version": "1.11.20",
+      "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz",
+      "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==",
+      "license": "MIT"
+    },
+    "node_modules/de-indent": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
+      "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "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==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "optional": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/element-plus": {
+      "version": "2.13.7",
+      "resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.13.7.tgz",
+      "integrity": "sha512-XdHATFZOyzVFL1DaHQ90IOJQSg9UnSAV+bhDW+YB5UoZ0Hxs50mwqjqfwXkuwpSag+VXXizVcErBR6Movo5daw==",
+      "license": "MIT",
+      "dependencies": {
+        "@ctrl/tinycolor": "^4.2.0",
+        "@element-plus/icons-vue": "^2.3.2",
+        "@floating-ui/dom": "^1.0.1",
+        "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7",
+        "@types/lodash": "^4.17.20",
+        "@types/lodash-es": "^4.17.12",
+        "@vueuse/core": "12.0.0",
+        "async-validator": "^4.2.5",
+        "dayjs": "^1.11.19",
+        "lodash": "^4.17.23",
+        "lodash-es": "^4.17.23",
+        "lodash-unified": "^1.0.3",
+        "memoize-one": "^6.0.0",
+        "normalize-wheel-es": "^1.2.0",
+        "vue-component-type-helpers": "^3.2.4"
+      },
+      "peerDependencies": {
+        "vue": "^3.3.0"
+      }
+    },
+    "node_modules/entities": {
+      "version": "7.0.1",
+      "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
+      "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=0.12"
+      },
+      "funding": {
+        "url": "https://github.com/fb55/entities?sponsor=1"
+      }
+    },
+    "node_modules/esbuild": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
+      "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "bin": {
+        "esbuild": "bin/esbuild"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "optionalDependencies": {
+        "@esbuild/aix-ppc64": "0.25.12",
+        "@esbuild/android-arm": "0.25.12",
+        "@esbuild/android-arm64": "0.25.12",
+        "@esbuild/android-x64": "0.25.12",
+        "@esbuild/darwin-arm64": "0.25.12",
+        "@esbuild/darwin-x64": "0.25.12",
+        "@esbuild/freebsd-arm64": "0.25.12",
+        "@esbuild/freebsd-x64": "0.25.12",
+        "@esbuild/linux-arm": "0.25.12",
+        "@esbuild/linux-arm64": "0.25.12",
+        "@esbuild/linux-ia32": "0.25.12",
+        "@esbuild/linux-loong64": "0.25.12",
+        "@esbuild/linux-mips64el": "0.25.12",
+        "@esbuild/linux-ppc64": "0.25.12",
+        "@esbuild/linux-riscv64": "0.25.12",
+        "@esbuild/linux-s390x": "0.25.12",
+        "@esbuild/linux-x64": "0.25.12",
+        "@esbuild/netbsd-arm64": "0.25.12",
+        "@esbuild/netbsd-x64": "0.25.12",
+        "@esbuild/openbsd-arm64": "0.25.12",
+        "@esbuild/openbsd-x64": "0.25.12",
+        "@esbuild/openharmony-arm64": "0.25.12",
+        "@esbuild/sunos-x64": "0.25.12",
+        "@esbuild/win32-arm64": "0.25.12",
+        "@esbuild/win32-ia32": "0.25.12",
+        "@esbuild/win32-x64": "0.25.12"
+      }
+    },
+    "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==",
+      "license": "MIT"
+    },
+    "node_modules/fdir": {
+      "version": "6.5.0",
+      "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+      "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "peerDependencies": {
+        "picomatch": "^3 || ^4"
+      },
+      "peerDependenciesMeta": {
+        "picomatch": {
+          "optional": true
+        }
+      }
+    },
+    "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,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+      }
+    },
+    "node_modules/he": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
+      "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "he": "bin/he"
+      }
+    },
+    "node_modules/hookable": {
+      "version": "5.5.3",
+      "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
+      "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
+      "license": "MIT"
+    },
+    "node_modules/immutable": {
+      "version": "5.1.5",
+      "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.5.tgz",
+      "integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/is-extglob": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+      "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-glob": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+      "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "is-extglob": "^2.1.1"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-what": {
+      "version": "5.5.0",
+      "resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz",
+      "integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/mesqueeb"
+      }
+    },
+    "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==",
+      "license": "MIT"
+    },
+    "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==",
+      "license": "MIT"
+    },
+    "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==",
+      "license": "MIT",
+      "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==",
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/sourcemap-codec": "^1.5.5"
+      }
+    },
+    "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==",
+      "license": "MIT"
+    },
+    "node_modules/minimatch": {
+      "version": "9.0.9",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
+      "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "brace-expansion": "^2.0.2"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/mitt": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
+      "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
+      "license": "MIT"
+    },
+    "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==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/nanoid": {
+      "version": "3.3.11",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+      "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "bin": {
+        "nanoid": "bin/nanoid.cjs"
+      },
+      "engines": {
+        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+      }
+    },
+    "node_modules/node-addon-api": {
+      "version": "7.1.1",
+      "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
+      "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
+      "dev": true,
+      "license": "MIT",
+      "optional": 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==",
+      "license": "BSD-3-Clause"
+    },
+    "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,
+      "license": "MIT"
+    },
+    "node_modules/perfect-debounce": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
+      "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
+      "license": "MIT"
+    },
+    "node_modules/picocolors": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+      "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+      "license": "ISC"
+    },
+    "node_modules/picomatch": {
+      "version": "4.0.4",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+      "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/pinia": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.4.tgz",
+      "integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/devtools-api": "^7.7.7"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/posva"
+      },
+      "peerDependencies": {
+        "typescript": ">=4.5.0",
+        "vue": "^3.5.11"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/postcss": {
+      "version": "8.5.10",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz",
+      "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==",
+      "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"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "nanoid": "^3.3.11",
+        "picocolors": "^1.1.1",
+        "source-map-js": "^1.2.1"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      }
+    },
+    "node_modules/readdirp": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
+      "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 14.18.0"
+      },
+      "funding": {
+        "type": "individual",
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
+    "node_modules/rfdc": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
+      "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
+      "license": "MIT"
+    },
+    "node_modules/rollup": {
+      "version": "4.60.2",
+      "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz",
+      "integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/estree": "1.0.8"
+      },
+      "bin": {
+        "rollup": "dist/bin/rollup"
+      },
+      "engines": {
+        "node": ">=18.0.0",
+        "npm": ">=8.0.0"
+      },
+      "optionalDependencies": {
+        "@rollup/rollup-android-arm-eabi": "4.60.2",
+        "@rollup/rollup-android-arm64": "4.60.2",
+        "@rollup/rollup-darwin-arm64": "4.60.2",
+        "@rollup/rollup-darwin-x64": "4.60.2",
+        "@rollup/rollup-freebsd-arm64": "4.60.2",
+        "@rollup/rollup-freebsd-x64": "4.60.2",
+        "@rollup/rollup-linux-arm-gnueabihf": "4.60.2",
+        "@rollup/rollup-linux-arm-musleabihf": "4.60.2",
+        "@rollup/rollup-linux-arm64-gnu": "4.60.2",
+        "@rollup/rollup-linux-arm64-musl": "4.60.2",
+        "@rollup/rollup-linux-loong64-gnu": "4.60.2",
+        "@rollup/rollup-linux-loong64-musl": "4.60.2",
+        "@rollup/rollup-linux-ppc64-gnu": "4.60.2",
+        "@rollup/rollup-linux-ppc64-musl": "4.60.2",
+        "@rollup/rollup-linux-riscv64-gnu": "4.60.2",
+        "@rollup/rollup-linux-riscv64-musl": "4.60.2",
+        "@rollup/rollup-linux-s390x-gnu": "4.60.2",
+        "@rollup/rollup-linux-x64-gnu": "4.60.2",
+        "@rollup/rollup-linux-x64-musl": "4.60.2",
+        "@rollup/rollup-openbsd-x64": "4.60.2",
+        "@rollup/rollup-openharmony-arm64": "4.60.2",
+        "@rollup/rollup-win32-arm64-msvc": "4.60.2",
+        "@rollup/rollup-win32-ia32-msvc": "4.60.2",
+        "@rollup/rollup-win32-x64-gnu": "4.60.2",
+        "@rollup/rollup-win32-x64-msvc": "4.60.2",
+        "fsevents": "~2.3.2"
+      }
+    },
+    "node_modules/sass": {
+      "version": "1.99.0",
+      "resolved": "https://registry.npmjs.org/sass/-/sass-1.99.0.tgz",
+      "integrity": "sha512-kgW13M54DUB7IsIRM5LvJkNlpH+WhMpooUcaWGFARkF1Tc82v9mIWkCbCYf+MBvpIUBSeSOTilpZjEPr2VYE6Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "chokidar": "^4.0.0",
+        "immutable": "^5.1.5",
+        "source-map-js": ">=0.6.2 <2.0.0"
+      },
+      "bin": {
+        "sass": "sass.js"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      },
+      "optionalDependencies": {
+        "@parcel/watcher": "^2.4.1"
+      }
+    },
+    "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==",
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/speakingurl": {
+      "version": "14.0.1",
+      "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz",
+      "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==",
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/superjson": {
+      "version": "2.2.6",
+      "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz",
+      "integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==",
+      "license": "MIT",
+      "dependencies": {
+        "copy-anything": "^4"
+      },
+      "engines": {
+        "node": ">=16"
+      }
+    },
+    "node_modules/tinyglobby": {
+      "version": "0.2.16",
+      "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
+      "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "fdir": "^6.5.0",
+        "picomatch": "^4.0.4"
+      },
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/SuperchupuDev"
+      }
+    },
+    "node_modules/typescript": {
+      "version": "5.9.3",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+      "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+      "devOptional": true,
+      "license": "Apache-2.0",
+      "bin": {
+        "tsc": "bin/tsc",
+        "tsserver": "bin/tsserver"
+      },
+      "engines": {
+        "node": ">=14.17"
+      }
+    },
+    "node_modules/undici-types": {
+      "version": "6.21.0",
+      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+      "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/vite": {
+      "version": "6.4.2",
+      "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz",
+      "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "esbuild": "^0.25.0",
+        "fdir": "^6.4.4",
+        "picomatch": "^4.0.2",
+        "postcss": "^8.5.3",
+        "rollup": "^4.34.9",
+        "tinyglobby": "^0.2.13"
+      },
+      "bin": {
+        "vite": "bin/vite.js"
+      },
+      "engines": {
+        "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/vitejs/vite?sponsor=1"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.3"
+      },
+      "peerDependencies": {
+        "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+        "jiti": ">=1.21.0",
+        "less": "*",
+        "lightningcss": "^1.21.0",
+        "sass": "*",
+        "sass-embedded": "*",
+        "stylus": "*",
+        "sugarss": "*",
+        "terser": "^5.16.0",
+        "tsx": "^4.8.1",
+        "yaml": "^2.4.2"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        },
+        "jiti": {
+          "optional": true
+        },
+        "less": {
+          "optional": true
+        },
+        "lightningcss": {
+          "optional": true
+        },
+        "sass": {
+          "optional": true
+        },
+        "sass-embedded": {
+          "optional": true
+        },
+        "stylus": {
+          "optional": true
+        },
+        "sugarss": {
+          "optional": true
+        },
+        "terser": {
+          "optional": true
+        },
+        "tsx": {
+          "optional": true
+        },
+        "yaml": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/vscode-uri": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz",
+      "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/vue": {
+      "version": "3.5.32",
+      "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.32.tgz",
+      "integrity": "sha512-vM4z4Q9tTafVfMAK7IVzmxg34rSzTFMyIe0UUEijUCkn9+23lj0WRfA83dg7eQZIUlgOSGrkViIaCfqSAUXsMw==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-dom": "3.5.32",
+        "@vue/compiler-sfc": "3.5.32",
+        "@vue/runtime-dom": "3.5.32",
+        "@vue/server-renderer": "3.5.32",
+        "@vue/shared": "3.5.32"
+      },
+      "peerDependencies": {
+        "typescript": "*"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/vue-component-type-helpers": {
+      "version": "3.2.7",
+      "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-3.2.7.tgz",
+      "integrity": "sha512-+gPp5YGmhfsj1IN+xUo7y0fb4clfnOiiUA39y07yW1VzCRjzVgwLbtmdWlghh7mXrPsEaYc7rrIir/HT6C8vYQ==",
+      "license": "MIT"
+    },
+    "node_modules/vue-router": {
+      "version": "4.6.4",
+      "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz",
+      "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/devtools-api": "^6.6.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/posva"
+      },
+      "peerDependencies": {
+        "vue": "^3.5.0"
+      }
+    },
+    "node_modules/vue-router/node_modules/@vue/devtools-api": {
+      "version": "6.6.4",
+      "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
+      "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
+      "license": "MIT"
+    },
+    "node_modules/vue-tsc": {
+      "version": "2.2.12",
+      "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.2.12.tgz",
+      "integrity": "sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@volar/typescript": "2.4.15",
+        "@vue/language-core": "2.2.12"
+      },
+      "bin": {
+        "vue-tsc": "bin/vue-tsc.js"
+      },
+      "peerDependencies": {
+        "typescript": ">=5.0.0"
+      }
+    }
+  }
+}

+ 26 - 0
package.json

@@ -0,0 +1,26 @@
+{
+  "name": "crossborder-os-console",
+  "private": true,
+  "version": "0.1.0",
+  "type": "module",
+  "scripts": {
+    "dev": "vite",
+    "build": "vue-tsc --noEmit && vite build",
+    "preview": "vite preview"
+  },
+  "dependencies": {
+    "@element-plus/icons-vue": "^2.3.1",
+    "element-plus": "^2.10.0",
+    "pinia": "^3.0.1",
+    "vue": "^3.5.13",
+    "vue-router": "^4.5.1"
+  },
+  "devDependencies": {
+    "@types/node": "^22.15.3",
+    "@vitejs/plugin-vue": "^5.2.3",
+    "sass": "^1.87.0",
+    "typescript": "^5.8.3",
+    "vite": "^6.3.3",
+    "vue-tsc": "^2.2.8"
+  }
+}

+ 7 - 0
src/App.vue

@@ -0,0 +1,7 @@
+<template>
+  <RouterView />
+</template>
+
+<script setup lang="ts">
+import { RouterView } from 'vue-router';
+</script>

+ 34 - 0
src/api/request.ts

@@ -0,0 +1,34 @@
+export interface ApiResponse<T> {
+  code: number;
+  data: T;
+  message: string;
+}
+
+export const request = async <T>(url: string, init?: RequestInit): Promise<T> => {
+  const token = localStorage.getItem('cbos-token');
+  const headers = new Headers(init?.headers);
+
+  if (!headers.has('Content-Type') && !(init?.body instanceof FormData)) {
+    headers.set('Content-Type', 'application/json');
+  }
+
+  if (token) {
+    headers.set('Authorization', `Bearer ${token}`);
+  }
+
+  const response = await fetch(url, {
+    ...init,
+    headers
+  });
+
+  if (!response.ok) {
+    throw new Error(`Request failed: ${response.status}`);
+  }
+
+  const payload = (await response.json()) as ApiResponse<T>;
+  if (payload.code !== 0) {
+    throw new Error(payload.message || 'Request failed');
+  }
+
+  return payload.data;
+};

+ 145 - 0
src/api/services.ts

@@ -0,0 +1,145 @@
+import { request } from './request';
+import type {
+  AfterSaleItem,
+  ApiKeyItem,
+  ChannelItem,
+  DashboardAlert,
+  DashboardStat,
+  InventoryItem,
+  InventoryLogItem,
+  LogItem,
+  MappingItem,
+  OrderItem,
+  PricingRuleItem,
+  ProductItem,
+  PurchaseOrderItem,
+  ReportDataItem,
+  ReportItem,
+  RoleItem,
+  ShippingItem,
+  SupplierItem,
+  SupplyCapabilityItem,
+  UserProfile
+} from '@/types/page';
+
+export interface DashboardOverviewResponse {
+  alerts: DashboardAlert[];
+  stats: DashboardStat[];
+  workflowTips: string[];
+}
+
+export interface LoginPayload {
+  password: string;
+  username: string;
+}
+
+export interface LoginResponse {
+  token: string;
+  user: UserProfile;
+}
+
+export const api = {
+  /* Auth */
+  login: (payload: LoginPayload) =>
+    request<LoginResponse>('/api/auth/login', { method: 'POST', body: JSON.stringify(payload) }),
+  getCurrentUser: () => request<UserProfile>('/api/auth/me'),
+
+  /* Dashboard */
+  getDashboardOverview: () => request<DashboardOverviewResponse>('/api/dashboard/overview'),
+
+  /* Products */
+  getProducts: () => request<{ items: ProductItem[] }>('/api/products'),
+  createProduct: (data: Partial<ProductItem>) =>
+    request<ProductItem>('/api/products', { method: 'POST', body: JSON.stringify(data) }),
+  updateProduct: (id: string, data: Partial<ProductItem>) =>
+    request<ProductItem>(`/api/products/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
+  deleteProduct: (id: string) =>
+    request<{ success: boolean }>(`/api/products/${id}`, { method: 'DELETE' }),
+
+  /* Channel Mappings */
+  getMappings: () => request<{ items: MappingItem[] }>('/api/channel-mappings'),
+  createMapping: (data: Partial<MappingItem>) =>
+    request<MappingItem>('/api/channel-mappings', { method: 'POST', body: JSON.stringify(data) }),
+  updateMapping: (id: string, data: Partial<MappingItem>) =>
+    request<MappingItem>(`/api/channel-mappings/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
+
+  /* Pricing Rules */
+  getPricingRules: () => request<{ items: PricingRuleItem[] }>('/api/pricing-rules'),
+  createPricingRule: (data: Partial<PricingRuleItem>) =>
+    request<PricingRuleItem>('/api/pricing-rules', { method: 'POST', body: JSON.stringify(data) }),
+  updatePricingRule: (id: string, data: Partial<PricingRuleItem>) =>
+    request<PricingRuleItem>(`/api/pricing-rules/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
+
+  /* Orders */
+  getOrders: () => request<{ items: OrderItem[] }>('/api/orders'),
+  getOrder: (id: string) => request<OrderItem>(`/api/orders/${id}`),
+  updateOrder: (id: string, data: Partial<OrderItem>) =>
+    request<OrderItem>(`/api/orders/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
+
+  /* After Sale */
+  getAfterSales: () => request<{ items: AfterSaleItem[] }>('/api/after-sale'),
+  updateAfterSale: (id: string, data: Partial<AfterSaleItem>) =>
+    request<AfterSaleItem>(`/api/after-sale/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
+
+  /* Suppliers */
+  getSuppliers: () => request<{ items: SupplierItem[] }>('/api/suppliers'),
+  createSupplier: (data: Partial<SupplierItem>) =>
+    request<SupplierItem>('/api/suppliers', { method: 'POST', body: JSON.stringify(data) }),
+  updateSupplier: (id: string, data: Partial<SupplierItem>) =>
+    request<SupplierItem>(`/api/suppliers/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
+
+  /* Purchase Orders */
+  getPurchaseOrders: () => request<{ items: PurchaseOrderItem[] }>('/api/purchase-orders'),
+  createPurchaseOrder: (data: Partial<PurchaseOrderItem>) =>
+    request<PurchaseOrderItem>('/api/purchase-orders', { method: 'POST', body: JSON.stringify(data) }),
+  updatePurchaseOrder: (id: string, data: Partial<PurchaseOrderItem>) =>
+    request<PurchaseOrderItem>(`/api/purchase-orders/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
+
+  /* Supply Capabilities */
+  getSupplyCapabilities: () => request<{ items: SupplyCapabilityItem[] }>('/api/supply-capabilities'),
+  createSupplyCapability: (data: Partial<SupplyCapabilityItem>) =>
+    request<SupplyCapabilityItem>('/api/supply-capabilities', { method: 'POST', body: JSON.stringify(data) }),
+  updateSupplyCapability: (id: string, data: Partial<SupplyCapabilityItem>) =>
+    request<SupplyCapabilityItem>(`/api/supply-capabilities/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
+
+  /* Channels */
+  getChannels: () => request<{ items: ChannelItem[] }>('/api/channels'),
+  createChannel: (data: Partial<ChannelItem>) =>
+    request<ChannelItem>('/api/channels', { method: 'POST', body: JSON.stringify(data) }),
+  updateChannel: (id: string, data: Partial<ChannelItem>) =>
+    request<ChannelItem>(`/api/channels/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
+
+  /* Inventory */
+  getInventory: () => request<{ items: InventoryItem[] }>('/api/inventory'),
+  getInventoryLogs: () => request<{ items: InventoryLogItem[] }>('/api/inventory-logs'),
+  updateInventory: (id: string, data: Partial<InventoryItem>) =>
+    request<InventoryItem>(`/api/inventory/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
+
+  /* Shipping */
+  getShippingOrders: () => request<{ items: ShippingItem[] }>('/api/shipping-orders'),
+  updateShippingOrder: (id: string, data: Partial<ShippingItem>) =>
+    request<ShippingItem>(`/api/shipping-orders/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
+
+  /* Reports */
+  getReports: () => request<{ items: ReportItem[] }>('/api/reports'),
+  getReportData: () => request<{ items: ReportDataItem[] }>('/api/report-data'),
+
+  /* System: Roles */
+  getRoles: () => request<{ items: RoleItem[] }>('/api/system/roles'),
+  createRole: (data: Partial<RoleItem>) =>
+    request<RoleItem>('/api/system/roles', { method: 'POST', body: JSON.stringify(data) }),
+  updateRole: (id: string, data: Partial<RoleItem>) =>
+    request<RoleItem>(`/api/system/roles/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
+
+  /* System: Logs */
+  getLogs: () => request<{ items: LogItem[] }>('/api/system/logs'),
+
+  /* System: API Keys */
+  getApiKeys: () => request<{ items: ApiKeyItem[] }>('/api/system/api-keys'),
+  createApiKey: (data: Partial<ApiKeyItem>) =>
+    request<ApiKeyItem & { keyValue?: string }>('/api/system/api-keys', { method: 'POST', body: JSON.stringify(data) }),
+  updateApiKey: (id: string, data: Partial<ApiKeyItem>) =>
+    request<ApiKeyItem>(`/api/system/api-keys/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
+  deleteApiKey: (id: string) =>
+    request<{ success: boolean }>(`/api/system/api-keys/${id}`, { method: 'DELETE' })
+};

+ 77 - 0
src/components/SpecPageRenderer.vue

@@ -0,0 +1,77 @@
+<template>
+  <div class="app-page">
+    <section class="glass-card page-hero">
+      <div class="page-hero__meta">
+        <span class="page-hero__eyebrow">Field-Level Spec</span>
+        <h2>{{ spec.title }}</h2>
+        <p>{{ spec.description }}</p>
+      </div>
+      <div class="chip-list">
+        <span class="chip">字段级说明</span>
+        <span class="chip">原型可参考</span>
+        <span class="chip">支持开发拆分</span>
+      </div>
+    </section>
+
+    <section class="page-grid page-grid--two">
+      <article class="glass-card section-card">
+        <div class="section-card__title">
+          <div>
+            <h3>筛选字段</h3>
+            <p>帮助业务人员快速定位目标数据。</p>
+          </div>
+        </div>
+        <div class="chip-list">
+          <span v-for="field in spec.filters" :key="field" class="chip">{{ field }}</span>
+        </div>
+      </article>
+
+      <article class="glass-card section-card">
+        <div class="section-card__title">
+          <div>
+            <h3>列表字段</h3>
+            <p>优先展示用于判断状态与推进流程的信息。</p>
+          </div>
+        </div>
+        <div class="chip-list">
+          <span v-for="field in spec.listFields" :key="field" class="chip">{{ field }}</span>
+        </div>
+      </article>
+
+      <article class="glass-card section-card">
+        <div class="section-card__title">
+          <div>
+            <h3>表单字段</h3>
+            <p>录入、修改和审批动作需要关注的输入项。</p>
+          </div>
+        </div>
+        <ul class="tight-list">
+          <li v-for="field in spec.formFields" :key="field">{{ field }}</li>
+        </ul>
+      </article>
+
+      <article class="glass-card section-card">
+        <div class="section-card__title">
+          <div>
+            <h3>交互规则</h3>
+            <p>按钮触发、状态反馈和字段联动建议。</p>
+          </div>
+        </div>
+        <ul class="tight-list">
+          <li v-for="item in spec.interactions" :key="item">{{ item }}</li>
+        </ul>
+      </article>
+    </section>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { computed } from 'vue';
+import { pageSpecs } from '@/config/page-specs';
+
+const props = defineProps<{
+  pageKey: string;
+}>();
+
+const spec = computed(() => pageSpecs[props.pageKey]);
+</script>

+ 148 - 0
src/config/page-specs.ts

@@ -0,0 +1,148 @@
+import type { PageSpec } from '@/types/page';
+
+export const pageSpecs: Record<string, PageSpec> = {
+  'product-list': {
+    title: '商品列表页',
+    description: '统一查看商品状态,并承接编辑、发布、导入、批量上下架等核心操作。',
+    filters: ['商品标题/关键词', 'SPU 编号', 'SKU 编号', '类目', '品牌', '渠道', '上架状态', '更新时间'],
+    listFields: ['主图', '商品标题', 'SPU', 'SKU 数量', '类目', '渠道发布状态', '总库存', '价格区间', '负责人', '更新时间'],
+    formFields: ['不以表单为主,顶部仅保留筛选条件与批量操作配置'],
+    interactions: ['批量上架/下架仅对勾选行生效', '导入后展示成功失败结果', '待完善商品显示醒目标记', '筛选条件返回页面时保留']
+  },
+  'product-editor': {
+    title: '商品新建/编辑页',
+    description: '完成 SPU/SKU 主数据录入,保证商品信息能够被渠道发布与库存履约模块复用。',
+    filters: ['无固定筛选区,以分步表单录入为主'],
+    listFields: ['SKU 规格组合预览', '媒体素材预览', '多语言内容预览'],
+    formFields: ['商品标题', '商品副标题', '类目', '品牌', '属性模板', '规格项', 'SKU 编码', '条码', '重量尺寸', '主图/附图/视频', '多语言文案'],
+    interactions: ['选择类目后自动带出属性模板', '新增规格后自动生成 SKU 组合', '提交校验前校验主图和必填字段', '离开未保存页面弹出提示']
+  },
+  'product-mapping': {
+    title: '渠道映射页',
+    description: '建立内部 SKU 与 Shopify/TikTok 渠道 SKU、类目、属性之间的映射关系。',
+    filters: ['商品标题', '内部 SKU', '渠道', '店铺', '映射状态', '校验状态'],
+    listFields: ['内部 SKU', '渠道 SKU', '渠道类目', '店铺', '映射状态', '最近同步时间', '最近校验结果'],
+    formFields: ['目标渠道', '目标店铺', '渠道类目', '属性映射', '渠道标题', '渠道描述', '渠道主图', '渠道价格'],
+    interactions: ['切换渠道动态刷新类目与属性模板', '未完成类目映射前不可发布', '校验失败在字段旁展示错误原因', '支持自动映射后人工修正']
+  },
+  'product-pricing': {
+    title: '定价与库存规则页',
+    description: '维护多渠道、多币种定价和安全库存阈值,让价格策略和补货逻辑可配置。',
+    filters: ['SKU', '渠道', '币种', '仓库', '规则状态'],
+    listFields: ['SKU', '币种', '基准售价', '渠道售价', '折扣价', '生效时间', '安全库存', '规则状态', '最近修改人'],
+    formFields: ['币种', '基础售价', '渠道加减价规则', '促销价', '汇率来源', '仓库', '安全库存阈值', '预警接收人', '生效/失效时间'],
+    interactions: ['促销价不可高于渠道售价', '支持按百分比和固定金额批量调价', '未来生效规则进入待生效状态', '每次保存生成变更记录']
+  },
+  'order-list': {
+    title: '订单列表页',
+    description: '汇总所有渠道订单,优先发现待处理、待发货和异常订单。',
+    filters: ['订单号', '渠道订单号', '渠道', '买家姓名', '订单状态', '支付状态', '发货状态', '异常标签', '下单时间', '仓库'],
+    listFields: ['订单号', '渠道', '买家', '订单金额', '商品件数', '支付状态', '订单状态', '发货状态', '仓库', '异常标签', '下单时间'],
+    formFields: ['筛选区和批量操作区'],
+    interactions: ['异常订单高亮显示', '可保存常用视图', '刷新同步提示新增/更新数量', '批量操作仅在勾选数据后出现']
+  },
+  'order-detail': {
+    title: '订单详情/处理页',
+    description: '沉淀订单全生命周期信息,并承接拆单、合单、取消、库存分配等动作。',
+    filters: ['无固定筛选,围绕单据详情展示'],
+    listFields: ['订单商品清单', '支付信息', '收货地址', '物流信息', '操作日志'],
+    formFields: ['取消原因', '异常标签', '人工备注', '仓库选择', '拆单方案', '合单目标订单', '客服备注'],
+    interactions: ['不同状态开放不同按钮权限', '取消订单必须二次确认并填写原因', '拆单需要明确 SKU 与数量归属', '所有人工处理动作写入日志']
+  },
+  'order-after-sale': {
+    title: '售后处理页',
+    description: '统一处理退款、退货、换货申请,并与订单状态、渠道退款动作联动。',
+    filters: ['售后单号', '订单号', '售后类型', '审核状态', '退款状态', '申请时间'],
+    listFields: ['售后单号', '订单号', '买家', '售后类型', '申请金额', '审核状态', '退款状态', '最近更新时间'],
+    formFields: ['售后原因', '责任归属', '退款金额', '退款方式', '退货物流单号', '补发仓库', '补发 SKU', '审核备注'],
+    interactions: ['退款金额不可超过实付金额', '部分退款要展示剩余可退额度', '退货入库后才能完结退货退款', '拒绝售后必须填写原因']
+  },
+  'supplier-list': {
+    title: '供应商管理页',
+    description: '维护供应商基础档案,给采购、补货、成本分析提供统一主数据。',
+    filters: ['供应商名称', '联系人', '合作状态', '评级', '创建时间'],
+    listFields: ['供应商名称', '联系人', '手机号', '合作状态', '结算方式', '评级', '关联 SKU 数', '最后更新时间'],
+    formFields: ['供应商名称', '公司名称', '联系人', '电话', '邮箱', '地址', '开户信息', '税号', '合同编号', '结算方式', '备注'],
+    interactions: ['供应商名称不可重复', '手机号和邮箱格式校验', '停用前校验未完成采购单', '合同上传保留版本记录']
+  },
+  'purchase-orders': {
+    title: '采购单管理页',
+    description: '管理采购单创建、审批、到货确认、差异处理与成本记录。',
+    filters: ['采购单号', '供应商', '采购状态', 'SKU', '创建时间', '预计交期'],
+    listFields: ['采购单号', '供应商', 'SKU 数', '采购总额', '预计交期', '到货进度', '采购状态', '创建人'],
+    formFields: ['供应商', '仓库', '采购 SKU', '采购数量', '采购单价', '币种', '税费', '运费', '预计交期', '备注'],
+    interactions: ['选择供应商后带出默认交期和 MOQ', '低于 MOQ 给出提示但允许提交审批', '确认到货后自动更新库存', '关闭前校验到货记录']
+  },
+  'supply-capability': {
+    title: '供货能力配置页',
+    description: '维护供应商对 SKU 的交期、MOQ、阶梯价格等供货参数。',
+    filters: ['供应商', 'SKU', '默认供货标记', '状态'],
+    listFields: ['供应商', 'SKU', '标准交期', 'MOQ', '采购单位', '阶梯价格', '默认供货标记', '状态'],
+    formFields: ['标准交期', 'MOQ', '阶梯价格起始数量', '阶梯价格', '结算币种', '采购单位', '备注'],
+    interactions: ['同一 SKU 支持多档阶梯价格', '设置默认供货后取消其他默认项', '交期与 MOQ 不可小于 0', '保存后同步补货建议计算']
+  },
+  'channel-config': {
+    title: '渠道接入配置页',
+    description: '管理 Shopify/TikTok 店铺授权、接口配置、Webhook 状态与同步任务。',
+    filters: ['无传统筛选区,以渠道账户卡片和状态筛选为主'],
+    listFields: ['渠道名称', '店铺名称', '授权状态', '同步状态', '最近同步时间', '异常状态'],
+    formFields: ['店铺名称', 'App Key', 'App Secret', 'Access Token', 'Webhook 地址', 'Webhook Secret', '同步开关', '默认仓库', '备注'],
+    interactions: ['测试连接成功后才允许启用同步', '重新授权前保留旧配置快照', 'Webhook 地址支持一键复制', '接口报错展示最近错误时间']
+  },
+  'inventory-overview': {
+    title: '库存总览页',
+    description: '统一查看库存分布、锁定情况与预警状态,作为履约与补货依据。',
+    filters: ['SKU', '仓库', '渠道', '预警状态', '库存状态'],
+    listFields: ['SKU', '商品标题', '仓库', '可用库存', '锁定库存', '在途库存', '安全库存', '预警状态', '最近变动时间'],
+    formFields: ['手动调整库存原因', '补货建议参数'],
+    interactions: ['低于安全库存显示红色预警', '点击库存数字进入变动明细', '手动调整库存必须填写原因', '创建补货建议时自动带出默认供应商']
+  },
+  'shipping-work': {
+    title: '发货作业页',
+    description: '支撑仓库拣货、打包、录入运单号,并将发货结果回传至渠道和订单。',
+    filters: ['发货单号', '订单号', '仓库', '发货状态', '回传状态', '创建时间'],
+    listFields: ['发货单号', '订单号', '仓库', 'SKU 数', '应发件数', '实发件数', '物流公司', '运单号', '发货状态', '回传状态'],
+    formFields: ['拣货人', '打包人', '物流公司', '运单号', '包裹重量', '包裹尺寸', '备注'],
+    interactions: ['确认发货前必须填写物流公司和运单号', '实发和应发不一致时给出差异提示', '回传失败支持重试', '打印面单前校验地址完整性']
+  },
+  'report-dashboard': {
+    title: '运营看板页',
+    description: '给运营和管理层提供经营总览,快速发现订单、库存、履约等风险点。',
+    filters: ['时间范围', '渠道', '国家', '店铺', '仓库'],
+    listFields: ['GMV', '支付订单数', '客单价', '退款率', '发货及时率', '低库存商品数', '异常订单数'],
+    formFields: ['看板不以表单为主,以指标卡和趋势图为主'],
+    interactions: ['指标卡支持下钻', '趋势图支持 7 天和 30 天切换', '异常指标高亮', '点击低库存商品数跳转库存页并带筛选']
+  },
+  'report-center': {
+    title: '报表中心页',
+    description: '支持经营数据按渠道、国家、SKU、供应商等维度筛选、分析和导出。',
+    filters: ['报表类型', '统计口径', '时间范围', '渠道', '店铺', 'SKU', '供应商', '仓库', '国家'],
+    listFields: ['指标名称', '指标值', '环比', '同比', '维度分组值'],
+    formFields: ['报表类型', '筛选条件', '导出格式', '订阅频率'],
+    interactions: ['切换报表类型动态加载筛选项', '导出保留当前筛选条件', '超大报表异步导出', '支持保存个人常用视图']
+  },
+  'role-permission': {
+    title: '角色权限页',
+    description: '按角色管理页面访问、按钮操作与数据范围权限。',
+    filters: ['角色名称', '角色状态', '更新时间'],
+    listFields: ['角色名称', '角色描述', '用户数', '启用状态', '更新时间'],
+    formFields: ['页面访问权限', '按钮权限', '数据范围权限', '特殊操作权限'],
+    interactions: ['复制角色时带出原权限配置', '停用前校验绑定用户', '保存后提示实时生效或重新登录策略', '高风险权限二次确认']
+  },
+  'operation-log': {
+    title: '操作日志页',
+    description: '追踪关键业务与系统操作,满足审计和问题排查需要。',
+    filters: ['操作人', '模块', '操作类型', '时间范围', '对象 ID'],
+    listFields: ['操作时间', '操作人', '模块', '操作动作', '对象 ID', '结果状态', '来源 IP'],
+    formFields: ['无独立表单,以筛选和详情抽屉为主'],
+    interactions: ['支持按对象 ID 快速定位单据全链路日志', '删除/取消/退款类操作标红', '日志详情优先展示关键变更字段', '导出时保留筛选条件']
+  },
+  'api-key': {
+    title: 'API Key 管理页',
+    description: '统一管理外部系统调用凭据,控制安全策略和过期时间。',
+    filters: ['所属系统', '状态', '权限范围', '到期时间'],
+    listFields: ['Key 名称', '所属系统', '权限范围', '状态', '创建时间', '过期时间', '最近调用时间'],
+    formFields: ['Key 名称', '所属系统', '权限范围', 'IP 白名单', '过期时间', '备注'],
+    interactions: ['明文仅创建时展示一次', '复制 Key 需再次身份确认', '轮换时支持过渡失效时间', '停用/删除需二次确认']
+  }
+};

+ 234 - 0
src/layout/AppLayout.vue

@@ -0,0 +1,234 @@
+<template>
+  <el-container class="app-layout">
+    <el-aside :width="appStore.menuCollapsed ? '84px' : '280px'" class="app-layout__aside">
+      <div class="brand glass-card">
+        <div class="brand__mark">CB</div>
+        <div v-if="!appStore.menuCollapsed" class="brand__meta">
+          <strong>CrossBorder OS</strong>
+          <span>Vue 3 + TS + Mock</span>
+        </div>
+      </div>
+
+      <div class="menu-groups glass-card">
+        <el-menu :default-active="route.path" :collapse="appStore.menuCollapsed" router class="menu-groups__menu">
+          <template v-for="group in filteredGroups" :key="group.key">
+            <el-menu-item-group :title="appStore.menuCollapsed ? '' : group.title">
+              <el-menu-item v-for="item in group.items" :key="item.key" :index="item.path">
+                <span>{{ item.title }}</span>
+              </el-menu-item>
+            </el-menu-item-group>
+          </template>
+        </el-menu>
+      </div>
+    </el-aside>
+
+    <el-container>
+      <el-header class="app-layout__header">
+        <div class="header-panel glass-card">
+          <div>
+            <div class="header-panel__breadcrumb">跨境运营中台 / {{ currentTitle }}</div>
+            <h2>{{ currentTitle }}</h2>
+          </div>
+
+          <div class="header-panel__actions">
+            <el-button plain @click="appStore.toggleMenu()">
+              {{ appStore.menuCollapsed ? '展开菜单' : '收起菜单' }}
+            </el-button>
+            <el-tag effect="dark" type="success">Mock 环境</el-tag>
+            <el-dropdown>
+              <div class="header-user glass-card">
+                <div class="header-user__avatar">{{ authStore.user?.avatar || 'CB' }}</div>
+                <div class="header-user__meta">
+                  <strong>{{ authStore.user?.name }}</strong>
+                  <span>{{ authStore.user?.roleLabel }}</span>
+                </div>
+              </div>
+              <template #dropdown>
+                <el-dropdown-menu>
+                  <el-dropdown-item disabled>{{ authStore.user?.workspace }}</el-dropdown-item>
+                  <el-dropdown-item @click="goLogin">切换账号</el-dropdown-item>
+                  <el-dropdown-item divided @click="logout">退出登录</el-dropdown-item>
+                </el-dropdown-menu>
+              </template>
+            </el-dropdown>
+          </div>
+        </div>
+      </el-header>
+
+      <el-main class="app-layout__main">
+        <RouterView />
+      </el-main>
+    </el-container>
+  </el-container>
+</template>
+
+<script setup lang="ts">
+import { computed } from 'vue';
+import { RouterView, useRoute, useRouter } from 'vue-router';
+import { menuGroups } from '@/router/menu';
+import { useAppStore } from '@/stores/app';
+import { useAuthStore } from '@/stores/auth';
+
+const appStore = useAppStore();
+const authStore = useAuthStore();
+const route = useRoute();
+const router = useRouter();
+
+const currentTitle = computed(() => (route.meta.title as string) || 'CrossBorder OS');
+const filteredGroups = computed(() =>
+  menuGroups
+    .map((group) => ({
+      ...group,
+      items: group.items.filter((item) => !authStore.role || item.roles.includes(authStore.role))
+    }))
+    .filter((group) => group.items.length > 0)
+);
+
+const goLogin = () => {
+  authStore.logout();
+  router.push('/login');
+};
+
+const logout = () => {
+  authStore.logout();
+  router.push('/login');
+};
+</script>
+
+<style scoped lang="scss">
+.app-layout {
+  min-height: 100vh;
+}
+
+.app-layout__aside {
+  padding: 18px 12px 18px 18px;
+}
+
+.brand {
+  display: flex;
+  align-items: center;
+  gap: 14px;
+  padding: 18px;
+  margin-bottom: 16px;
+}
+
+.brand__mark {
+  width: 46px;
+  height: 46px;
+  border-radius: 16px;
+  display: grid;
+  place-items: center;
+  background: linear-gradient(135deg, #0f766e, #d97706);
+  color: white;
+  font-weight: 800;
+}
+
+.brand__meta {
+  display: flex;
+  flex-direction: column;
+  gap: 4px;
+}
+
+.brand__meta span {
+  color: var(--cb-text-soft);
+  font-size: 13px;
+}
+
+.menu-groups {
+  height: calc(100vh - 120px);
+  padding: 14px 10px;
+  overflow: auto;
+}
+
+.menu-groups__menu {
+  border-right: none;
+  background: transparent;
+}
+
+:deep(.el-menu-item-group__title) {
+  font-weight: 700;
+  color: var(--cb-text-soft);
+}
+
+:deep(.el-menu-item) {
+  margin: 6px 0;
+  border-radius: 14px;
+}
+
+:deep(.el-menu-item.is-active) {
+  background: rgba(15, 118, 110, 0.14);
+}
+
+.app-layout__header {
+  padding: 18px 18px 0 6px;
+  height: auto;
+}
+
+.header-panel {
+  display: flex;
+  justify-content: space-between;
+  gap: 16px;
+  align-items: center;
+  padding: 20px 24px;
+}
+
+.header-panel__breadcrumb {
+  color: var(--cb-text-soft);
+  font-size: 13px;
+}
+
+.header-panel h2 {
+  margin: 8px 0 0;
+  font-size: 24px;
+}
+
+.header-panel__actions {
+  display: flex;
+  gap: 10px;
+  align-items: center;
+}
+
+.header-user {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  padding: 8px 12px;
+  cursor: pointer;
+}
+
+.header-user__avatar {
+  width: 38px;
+  height: 38px;
+  border-radius: 14px;
+  display: grid;
+  place-items: center;
+  background: linear-gradient(135deg, rgba(15, 118, 110, 0.15), rgba(217, 119, 6, 0.18));
+  font-weight: 700;
+  color: var(--cb-primary);
+}
+
+.header-user__meta {
+  display: flex;
+  flex-direction: column;
+}
+
+.header-user__meta span {
+  color: var(--cb-text-soft);
+  font-size: 12px;
+}
+
+.app-layout__main {
+  padding: 18px 18px 18px 6px;
+}
+
+@media (max-width: 900px) {
+  .app-layout__aside {
+    display: none;
+  }
+
+  .header-panel {
+    flex-direction: column;
+    align-items: flex-start;
+  }
+}
+</style>

+ 15 - 0
src/main.ts

@@ -0,0 +1,15 @@
+import { createApp } from 'vue';
+import ElementPlus from 'element-plus';
+import { createPinia } from 'pinia';
+import 'element-plus/dist/index.css';
+import App from './App.vue';
+import router from './router';
+import './styles/global.scss';
+
+const app = createApp(App);
+
+app.use(createPinia());
+app.use(router);
+app.use(ElementPlus);
+
+app.mount('#app');

+ 713 - 0
src/mock/data.ts

@@ -0,0 +1,713 @@
+export const dashboardOverview = {
+  stats: [
+    { label: 'GMV(近 7 天)', value: '$128,430', trend: '+12.6% vs 上周期', tone: 'primary' },
+    { label: '支付订单数', value: '1,842', trend: '+8.3% vs 上周期', tone: 'success' },
+    { label: '客单价', value: '$69.74', trend: '-2.1% vs 上周期', tone: 'info' },
+    { label: '退款率', value: '3.2%', trend: '+0.4% vs 上周期', tone: 'warning' },
+    { label: '发货及时率', value: '96.8%', trend: '+1.9% vs 上周期', tone: 'success' },
+    { label: '低库存商品数', value: '42', trend: '建议本周发起补货', tone: 'danger' },
+    { label: '异常订单数', value: '18', trend: '其中 6 单高风险', tone: 'warning' },
+    { label: '库存周转天数', value: '28.5', trend: '-3.2 天 vs 上周期', tone: 'primary' }
+  ],
+  alerts: [
+    { title: 'TikTok 美国店铺授权即将过期', summary: '建议今天内完成重新授权,避免订单推送中断。', level: 'warning' },
+    { title: '3 个 SKU 已跌破安全库存', summary: '默认供应商已可匹配,建议直接生成采购单。', level: 'critical' },
+    { title: 'Shopify 发货回传成功率稳定', summary: '最近 24 小时回传成功率 99.4%,无阻塞项。', level: 'info' }
+  ],
+  workflowTips: [
+    '商品创建后先走渠道映射校验,再触发发布,能减少 TikTok 类目校验失败。',
+    '订单详情页适合做异常订单闭环,建议优先承接取消、改址、拆单、备注操作。',
+    '库存总览与采购单管理页要保持强链接,低库存预警建议一键跳采购单创建。'
+  ]
+};
+
+export const products = [
+  {
+    id: 'p1',
+    title: 'TravelFlex Expandable Carry-On',
+    spu: 'SPU-TR-1001',
+    skuCount: 3,
+    category: 'Luggage',
+    brand: 'NomadPeak',
+    channelStatus: 'Shopify 已发布 / TikTok 待校验',
+    inventory: 268,
+    priceRange: '$79 - $95',
+    owner: '运营组 A',
+    status: '已上架',
+    updatedAt: '2026-04-19 21:18',
+    image: 'https://picsum.photos/seed/luggage/80/80'
+  },
+  {
+    id: 'p2',
+    title: 'Commuter Sling Bag',
+    spu: 'SPU-BG-2044',
+    skuCount: 2,
+    category: 'Bags',
+    brand: 'UrbanTrail',
+    channelStatus: '双渠道已发布',
+    inventory: 114,
+    priceRange: '$32 - $38',
+    owner: '运营组 B',
+    status: '已上架',
+    updatedAt: '2026-04-19 19:42',
+    image: 'https://picsum.photos/seed/bag/80/80'
+  },
+  {
+    id: 'p3',
+    title: 'AeroDry Gym Towel Set',
+    spu: 'SPU-GM-0312',
+    skuCount: 4,
+    category: 'Sports',
+    brand: 'AeroDry',
+    channelStatus: '待完善主图',
+    inventory: 86,
+    priceRange: '$12 - $18',
+    owner: '运营组 C',
+    status: '草稿',
+    updatedAt: '2026-04-18 17:05',
+    image: 'https://picsum.photos/seed/towel/80/80'
+  },
+  {
+    id: 'p4',
+    title: 'Ultralight Hiking Poles',
+    spu: 'SPU-HK-0087',
+    skuCount: 2,
+    category: 'Outdoor',
+    brand: 'NomadPeak',
+    channelStatus: 'TikTok 已发布',
+    inventory: 43,
+    priceRange: '$45 - $55',
+    owner: '运营组 A',
+    status: '已上架',
+    updatedAt: '2026-04-17 14:30',
+    image: 'https://picsum.photos/seed/poles/80/80'
+  },
+  {
+    id: 'p5',
+    title: 'Insulated Water Bottle 750ml',
+    spu: 'SPU-BT-0520',
+    skuCount: 5,
+    category: 'Sports',
+    brand: 'AeroDry',
+    channelStatus: 'Shopify 已发布',
+    inventory: 312,
+    priceRange: '$18 - $24',
+    owner: '运营组 B',
+    status: '已上架',
+    updatedAt: '2026-04-16 11:20',
+    image: 'https://picsum.photos/seed/bottle/80/80'
+  }
+];
+
+export const mappingItems = [
+  {
+    id: 'm1',
+    internalSku: 'SKU-LUGG-20-BLK',
+    channelSku: 'SHOP-Variant-1024',
+    channel: 'Shopify',
+    shopName: 'NomadPeak US',
+    channelCategory: 'Luggage > Carry-On',
+    mappingStatus: '已映射',
+    validateStatus: '通过',
+    lastSyncAt: '2026-04-19 22:05',
+    lastValidateResult: '校验通过',
+    productTitle: 'TravelFlex Expandable Carry-On / Black'
+  },
+  {
+    id: 'm2',
+    internalSku: 'SKU-LUGG-24-BLU',
+    channelSku: 'TK-SKU-8801',
+    channel: 'TikTok Shop',
+    shopName: 'NomadPeak Global',
+    channelCategory: 'Luggage > Suitcases',
+    mappingStatus: '已映射',
+    validateStatus: '失败',
+    lastSyncAt: '2026-04-19 21:48',
+    lastValidateResult: '类目属性"材质"未填写',
+    productTitle: 'TravelFlex Expandable Carry-On / Blue'
+  },
+  {
+    id: 'm3',
+    internalSku: 'SKU-BAG-08-GRY',
+    channelSku: 'SHOP-Variant-2048',
+    channel: 'Shopify',
+    shopName: 'UrbanTrail JP',
+    channelCategory: 'Bags > Sling Bags',
+    mappingStatus: '已映射',
+    validateStatus: '通过',
+    lastSyncAt: '2026-04-19 20:30',
+    lastValidateResult: '校验通过',
+    productTitle: 'Commuter Sling Bag / Gray'
+  },
+  {
+    id: 'm4',
+    internalSku: 'SKU-TOWEL-SET-MIX',
+    channelSku: '',
+    channel: 'TikTok Shop',
+    shopName: 'AeroDry UK',
+    channelCategory: '',
+    mappingStatus: '未映射',
+    validateStatus: '未校验',
+    lastSyncAt: '-',
+    lastValidateResult: '-',
+    productTitle: 'AeroDry Gym Towel Set / Mixed'
+  }
+];
+
+export const pricingRules = [
+  {
+    id: 'pr1',
+    sku: 'SKU-LUGG-20-BLK',
+    productTitle: 'TravelFlex Expandable Carry-On / Black',
+    currency: 'USD',
+    basePrice: '$79.00',
+    channelPrice: '$85.00',
+    discountPrice: '$75.00',
+    effectiveTime: '2026-04-15',
+    safeStock: 30,
+    status: '生效中',
+    modifiedBy: '运营组 A',
+    updatedAt: '2026-04-15 10:20'
+  },
+  {
+    id: 'pr2',
+    sku: 'SKU-BAG-08-GRY',
+    productTitle: 'Commuter Sling Bag / Gray',
+    currency: 'USD',
+    basePrice: '$32.00',
+    channelPrice: '$35.00',
+    discountPrice: '-',
+    effectiveTime: '2026-04-01',
+    safeStock: 20,
+    status: '生效中',
+    modifiedBy: '运营组 B',
+    updatedAt: '2026-04-10 09:15'
+  },
+  {
+    id: 'pr3',
+    sku: 'SKU-BAG-08-GRY',
+    productTitle: 'Commuter Sling Bag / Gray',
+    currency: 'JPY',
+    basePrice: '¥4,800',
+    channelPrice: '¥5,200',
+    discountPrice: '¥4,600',
+    effectiveTime: '2026-05-01',
+    safeStock: 20,
+    status: '待生效',
+    modifiedBy: '运营组 B',
+    updatedAt: '2026-04-18 16:00'
+  },
+  {
+    id: 'pr4',
+    sku: 'SKU-TOWEL-SET-MIX',
+    productTitle: 'AeroDry Gym Towel Set / Mixed',
+    currency: 'USD',
+    basePrice: '$12.00',
+    channelPrice: '$14.00',
+    discountPrice: '$10.00',
+    effectiveTime: '2026-03-20',
+    safeStock: 25,
+    status: '已停用',
+    modifiedBy: '运营组 C',
+    updatedAt: '2026-04-05 14:30'
+  }
+];
+
+export const orders = [
+  {
+    id: 'o1',
+    orderNo: 'OMS-20260419-0012',
+    channelOrderNo: 'SH-ORD-4521',
+    channel: 'Shopify US',
+    buyer: 'Olivia Zhang',
+    amount: '$128.00',
+    itemCount: 2,
+    paymentStatus: '已支付',
+    orderStatus: 'allocated',
+    shippingStatus: '待发货',
+    warehouse: '深圳仓',
+    exceptionTag: '地址需复核',
+    createdAt: '2026-04-19 20:42'
+  },
+  {
+    id: 'o2',
+    orderNo: 'OMS-20260419-0017',
+    channelOrderNo: 'TK-ORD-77832',
+    channel: 'TikTok UK',
+    buyer: 'Noah Smith',
+    amount: '$64.90',
+    itemCount: 1,
+    paymentStatus: '已支付',
+    orderStatus: 'paid',
+    shippingStatus: '待分配仓库',
+    warehouse: '待分配',
+    exceptionTag: '高频下单',
+    createdAt: '2026-04-19 21:08'
+  },
+  {
+    id: 'o3',
+    orderNo: 'OMS-20260419-0022',
+    channelOrderNo: 'SH-ORD-4580',
+    channel: 'Shopify JP',
+    buyer: 'Liam Chen',
+    amount: '$219.00',
+    itemCount: 3,
+    paymentStatus: '已支付',
+    orderStatus: 'shipped',
+    shippingStatus: '运输中',
+    warehouse: '义乌仓',
+    exceptionTag: '正常',
+    createdAt: '2026-04-19 21:31'
+  },
+  {
+    id: 'o4',
+    orderNo: 'OMS-20260418-0008',
+    channelOrderNo: 'SH-ORD-4301',
+    channel: 'Shopify US',
+    buyer: 'Emma Wilson',
+    amount: '$76.00',
+    itemCount: 1,
+    paymentStatus: '未支付',
+    orderStatus: 'created',
+    shippingStatus: '未发货',
+    warehouse: '深圳仓',
+    exceptionTag: '正常',
+    createdAt: '2026-04-18 15:20'
+  },
+  {
+    id: 'o5',
+    orderNo: 'OMS-20260418-0015',
+    channelOrderNo: 'TK-ORD-76450',
+    channel: 'TikTok UK',
+    buyer: 'Sophie Brown',
+    amount: '$156.50',
+    itemCount: 2,
+    paymentStatus: '已支付',
+    orderStatus: 'delivered',
+    shippingStatus: '已签收',
+    warehouse: '深圳仓',
+    exceptionTag: '正常',
+    createdAt: '2026-04-18 10:45'
+  }
+];
+
+export const afterSaleOrders = [
+  {
+    id: 'as1',
+    afterSaleNo: 'AS-20260419-001',
+    orderNo: 'OMS-20260417-0035',
+    buyer: 'James Lee',
+    type: '退款',
+    amount: '$45.00',
+    auditStatus: '待审核',
+    refundStatus: '未退款',
+    reason: '商品与描述不符',
+    updatedAt: '2026-04-19 22:10'
+  },
+  {
+    id: 'as2',
+    afterSaleNo: 'AS-20260419-002',
+    orderNo: 'OMS-20260416-0028',
+    buyer: 'Mia Wang',
+    type: '退货退款',
+    amount: '$128.00',
+    auditStatus: '已通过',
+    refundStatus: '已退款',
+    reason: '尺码不合适',
+    updatedAt: '2026-04-19 18:30'
+  },
+  {
+    id: 'as3',
+    afterSaleNo: 'AS-20260418-003',
+    orderNo: 'OMS-20260415-0021',
+    buyer: 'Lucas Garcia',
+    type: '换货',
+    amount: '$0.00',
+    auditStatus: '已通过',
+    refundStatus: '补发中',
+    reason: '商品损坏',
+    updatedAt: '2026-04-18 14:55'
+  },
+  {
+    id: 'as4',
+    afterSaleNo: 'AS-20260417-004',
+    orderNo: 'OMS-20260414-0012',
+    buyer: 'Ava Martinez',
+    type: '退款',
+    amount: '$32.50',
+    auditStatus: '已拒绝',
+    refundStatus: '未退款',
+    reason: '不想要了',
+    updatedAt: '2026-04-17 09:20'
+  }
+];
+
+export const suppliers = [
+  {
+    id: 's1',
+    name: '深圳市赛峰箱包有限公司',
+    companyName: '深圳市赛峰箱包有限公司',
+    contact: '刘楠',
+    phone: '138****2210',
+    email: 'liunan@saifeng.com',
+    address: '深圳市宝安区沙井街道新沙路 88 号',
+    settlement: '月结 30 天',
+    rating: 'A',
+    status: '合作中',
+    relatedSkuCount: 84,
+    updatedAt: '2026-04-18 16:42'
+  },
+  {
+    id: 's2',
+    name: '东莞市恒驰纺织厂',
+    companyName: '东莞市恒驰纺织厂',
+    contact: '王敏',
+    phone: '139****3045',
+    email: 'wangmin@hengchi.com',
+    address: '东莞市虎门镇北栅工业区',
+    settlement: '预付 50%',
+    rating: 'B',
+    status: '合作中',
+    relatedSkuCount: 39,
+    updatedAt: '2026-04-17 11:13'
+  },
+  {
+    id: 's3',
+    name: '宁波远帆塑胶制品',
+    companyName: '宁波远帆塑胶制品有限公司',
+    contact: '赵坤',
+    phone: '137****8901',
+    email: 'zhaokun@yuanfan.com',
+    address: '宁波市鄞州区姜山镇科技路 128 号',
+    settlement: '月结 45 天',
+    rating: 'A',
+    status: '待续约',
+    relatedSkuCount: 57,
+    updatedAt: '2026-04-16 09:22'
+  }
+];
+
+export const purchaseOrders = [
+  {
+    id: 'po1',
+    poNo: 'PO-20260419-001',
+    supplier: '深圳市赛峰箱包有限公司',
+    skuCount: 12,
+    amount: '$9,840',
+    expectedDate: '2026-04-26',
+    arrivalProgress: '4 / 12 SKU 已到货',
+    status: '部分到货',
+    creator: '采购组 A',
+    createdAt: '2026-04-12 10:30'
+  },
+  {
+    id: 'po2',
+    poNo: 'PO-20260418-007',
+    supplier: '宁波远帆塑胶制品',
+    skuCount: 6,
+    amount: '$4,320',
+    expectedDate: '2026-04-24',
+    arrivalProgress: '待发货',
+    status: '待供应商确认',
+    creator: '采购组 B',
+    createdAt: '2026-04-11 14:20'
+  },
+  {
+    id: 'po3',
+    poNo: 'PO-20260417-011',
+    supplier: '东莞市恒驰纺织厂',
+    skuCount: 18,
+    amount: '$6,760',
+    expectedDate: '2026-04-29',
+    arrivalProgress: '0 / 18 SKU 已到货',
+    status: '采购中',
+    creator: '采购组 C',
+    createdAt: '2026-04-10 09:00'
+  },
+  {
+    id: 'po4',
+    poNo: 'PO-20260415-005',
+    supplier: '深圳市赛峰箱包有限公司',
+    skuCount: 8,
+    amount: '$5,200',
+    expectedDate: '2026-04-22',
+    arrivalProgress: '8 / 8 SKU 已到货',
+    status: '已完成',
+    creator: '采购组 A',
+    createdAt: '2026-04-08 11:00'
+  }
+];
+
+export const supplyCapabilities = [
+  {
+    id: 'sc1',
+    supplier: '深圳市赛峰箱包有限公司',
+    sku: 'SKU-LUGG-20-BLK',
+    productTitle: 'TravelFlex Expandable Carry-On / Black',
+    leadTime: 7,
+    moq: 100,
+    unit: '个',
+    tierPrices: [
+      { from: 100, to: 499, price: '$6.50' },
+      { from: 500, to: 999, price: '$5.80' },
+      { from: 1000, to: 9999, price: '$5.20' }
+    ],
+    isDefault: true,
+    status: '启用',
+    updatedAt: '2026-04-15 10:00'
+  },
+  {
+    id: 'sc2',
+    supplier: '东莞市恒驰纺织厂',
+    sku: 'SKU-BAG-08-GRY',
+    productTitle: 'Commuter Sling Bag / Gray',
+    leadTime: 10,
+    moq: 200,
+    unit: '个',
+    tierPrices: [
+      { from: 200, to: 499, price: '$3.20' },
+      { from: 500, to: 999, price: '$2.80' }
+    ],
+    isDefault: true,
+    status: '启用',
+    updatedAt: '2026-04-12 14:30'
+  },
+  {
+    id: 'sc3',
+    supplier: '宁波远帆塑胶制品',
+    sku: 'SKU-TOWEL-SET-MIX',
+    productTitle: 'AeroDry Gym Towel Set / Mixed',
+    leadTime: 5,
+    moq: 500,
+    unit: '套',
+    tierPrices: [
+      { from: 500, to: 1999, price: '$1.80' },
+      { from: 2000, to: 9999, price: '$1.50' }
+    ],
+    isDefault: false,
+    status: '启用',
+    updatedAt: '2026-04-10 16:00'
+  }
+];
+
+export const channels = [
+  {
+    id: 'ch1',
+    channel: 'Shopify',
+    shopName: 'NomadPeak US',
+    tokenStatus: '已授权',
+    syncStatus: '运行中',
+    lastSyncAt: '2026-04-19 22:05',
+    errorMessage: '无',
+    appKey: 'shpa_3k2x***',
+    appSecret: 'shss_***',
+    accessToken: 'shpat_1a2b***',
+    webhookUrl: 'https://cbos.example.com/webhook/shopify/us',
+    webhookSecret: 'whsec_***',
+    syncEnabled: true,
+    defaultWarehouse: '深圳仓',
+    remark: '美国站主店铺'
+  },
+  {
+    id: 'ch2',
+    channel: 'Shopify',
+    shopName: 'UrbanTrail JP',
+    tokenStatus: '即将过期',
+    syncStatus: '运行中',
+    lastSyncAt: '2026-04-19 21:55',
+    errorMessage: 'Token 3 天后到期',
+    appKey: 'shpa_7m4y***',
+    appSecret: 'shss_***',
+    accessToken: 'shpat_9c0d***',
+    webhookUrl: 'https://cbos.example.com/webhook/shopify/jp',
+    webhookSecret: 'whsec_***',
+    syncEnabled: true,
+    defaultWarehouse: '义乌仓',
+    remark: '日本站'
+  },
+  {
+    id: 'ch3',
+    channel: 'TikTok Shop',
+    shopName: 'AeroDry UK',
+    tokenStatus: '已授权',
+    syncStatus: '告警中',
+    lastSyncAt: '2026-04-19 21:48',
+    errorMessage: '部分商品类目校验失败',
+    appKey: 'tt_5n8p***',
+    appSecret: 'ttss_***',
+    accessToken: 'ttat_2e4f***',
+    webhookUrl: 'https://cbos.example.com/webhook/tiktok/uk',
+    webhookSecret: 'whsec_***',
+    syncEnabled: true,
+    defaultWarehouse: '深圳仓',
+    remark: '英国站'
+  }
+];
+
+export const inventory = [
+  {
+    id: 'inv1',
+    sku: 'SKU-LUGG-20-BLK',
+    productTitle: 'TravelFlex Expandable Carry-On / Black',
+    warehouse: '深圳仓',
+    available: 22,
+    locked: 15,
+    inbound: 60,
+    safeStock: 30,
+    warningStatus: '低库存预警',
+    lastChangeAt: '2026-04-19 21:12'
+  },
+  {
+    id: 'inv2',
+    sku: 'SKU-BAG-08-GRY',
+    productTitle: 'Commuter Sling Bag / Gray',
+    warehouse: '义乌仓',
+    available: 88,
+    locked: 9,
+    inbound: 0,
+    safeStock: 20,
+    warningStatus: '正常',
+    lastChangeAt: '2026-04-19 20:44'
+  },
+  {
+    id: 'inv3',
+    sku: 'SKU-TOWEL-SET-MIX',
+    productTitle: 'AeroDry Gym Towel Set / Mixed',
+    warehouse: '深圳仓',
+    available: 11,
+    locked: 4,
+    inbound: 120,
+    safeStock: 25,
+    warningStatus: '紧急补货',
+    lastChangeAt: '2026-04-19 19:20'
+  },
+  {
+    id: 'inv4',
+    sku: 'SKU-HK-POLE-01',
+    productTitle: 'Ultralight Hiking Poles / Telescopic',
+    warehouse: '深圳仓',
+    available: 38,
+    locked: 5,
+    inbound: 0,
+    safeStock: 15,
+    warningStatus: '正常',
+    lastChangeAt: '2026-04-18 16:30'
+  }
+];
+
+export const inventoryLogs = [
+  { id: 'il1', time: '2026-04-19 21:12', source: '订单锁定', relatedOrder: 'OMS-20260419-0012', operator: '系统', quantity: -2, afterQty: 22 },
+  { id: 'il2', time: '2026-04-19 18:30', source: '采购入库', relatedOrder: 'PO-20260415-005', operator: '采购组 A', quantity: +30, afterQty: 24 },
+  { id: 'il3', time: '2026-04-18 14:20', source: '订单释放', relatedOrder: 'OMS-20260418-0008', operator: '系统', quantity: +1, afterQty: 23 },
+  { id: 'il4', time: '2026-04-17 09:00', source: '手动调整', relatedOrder: '-', operator: '运营组 A / 陈欣', quantity: -5, afterQty: 22 }
+];
+
+export const shippingOrders = [
+  {
+    id: 'sh1',
+    shipmentNo: 'SHIP-20260419-0008',
+    orderNo: 'OMS-20260419-0012',
+    warehouse: '深圳仓',
+    skuCount: 2,
+    expectedQty: 2,
+    actualQty: 2,
+    carrier: 'DHL',
+    trackingNo: 'DHL-8845123690',
+    shippingStatus: '待发货',
+    returnStatus: '未回传',
+    createdAt: '2026-04-19 20:52'
+  },
+  {
+    id: 'sh2',
+    shipmentNo: 'SHIP-20260419-0011',
+    orderNo: 'OMS-20260419-0022',
+    warehouse: '义乌仓',
+    skuCount: 3,
+    expectedQty: 3,
+    actualQty: 3,
+    carrier: 'FedEx',
+    trackingNo: 'FDX-7720345891',
+    shippingStatus: '已发货',
+    returnStatus: '已回传',
+    createdAt: '2026-04-19 21:36'
+  },
+  {
+    id: 'sh3',
+    shipmentNo: 'SHIP-20260419-0016',
+    orderNo: 'OMS-20260419-0017',
+    warehouse: '深圳仓',
+    skuCount: 1,
+    expectedQty: 1,
+    actualQty: 0,
+    carrier: '待选择',
+    trackingNo: '',
+    shippingStatus: '待拣货',
+    returnStatus: '未回传',
+    createdAt: '2026-04-19 21:43'
+  }
+];
+
+export const reports = [
+  { id: 'r1', reportType: '渠道销售日报', dimensions: '渠道 / 店铺 / 国家', owner: '管理层', updatedAt: '2026-04-19 22:00' },
+  { id: 'r2', reportType: 'SKU 利润分析', dimensions: 'SKU / 供应商 / 仓库', owner: '运营 + 采购', updatedAt: '2026-04-19 21:48' },
+  { id: 'r3', reportType: '履约时效周报', dimensions: '仓库 / 物流商 / 渠道', owner: '仓配负责人', updatedAt: '2026-04-19 21:30' }
+];
+
+export const reportData = [
+  { metric: 'GMV', value: '$128,430', mom: '+12.6%', yoy: '+35.2%', dimension: '全渠道汇总' },
+  { metric: '订单量', value: '1,842', mom: '+8.3%', yoy: '+28.4%', dimension: '全渠道汇总' },
+  { metric: '客单价', value: '$69.74', mom: '-2.1%', yoy: '+5.8%', dimension: '全渠道汇总' },
+  { metric: '退款率', value: '3.2%', mom: '+0.4%', yoy: '-1.1%', dimension: '全渠道汇总' },
+  { metric: 'GMV', value: '$89,200', mom: '+15.2%', yoy: '+40.1%', dimension: 'Shopify US' },
+  { metric: 'GMV', value: '$24,630', mom: '+6.8%', yoy: '+22.5%', dimension: 'Shopify JP' },
+  { metric: 'GMV', value: '$14,600', mom: '+9.1%', yoy: '+31.7%', dimension: 'TikTok UK' }
+];
+
+export const roles = [
+  { id: 'role1', name: '运营', description: '负责商品管理、渠道操作和订单查看处理', boundUsers: 14, status: '启用', updatedAt: '2026-04-18 18:10', permissions: ['product:read', 'product:write', 'channel:read', 'channel:write', 'order:read', 'inventory:read', 'report:read'] },
+  { id: 'role2', name: '采购', description: '负责供应商管理、采购操作和库存预警', boundUsers: 8, status: '启用', updatedAt: '2026-04-18 17:42', permissions: ['supplier:read', 'supplier:write', 'purchase:read', 'purchase:write', 'inventory:read', 'report:read'] },
+  { id: 'role3', name: '仓库', description: '负责发货操作、库存调整和物流录入', boundUsers: 11, status: '启用', updatedAt: '2026-04-17 09:20', permissions: ['order:read', 'order:write', 'inventory:read', 'inventory:write', 'shipping:read', 'shipping:write'] },
+  { id: 'role4', name: '客服', description: '负责订单查询和售后处理', boundUsers: 6, status: '启用', updatedAt: '2026-04-16 14:00', permissions: ['order:read', 'order:write', 'aftersale:read', 'aftersale:write'] },
+  { id: 'role5', name: '管理者', description: '查看所有数据和报表', boundUsers: 3, status: '启用', updatedAt: '2026-04-15 10:30', permissions: ['product:read', 'order:read', 'supplier:read', 'channel:read', 'inventory:read', 'report:read', 'report:write', 'system:read'] }
+];
+
+export const logs = [
+  { id: 'l1', time: '2026-04-19 21:58:12', actor: '运营组 A / 陈欣', module: '订单中心', type: '取消订单', objectId: 'OMS-20260419-0010', result: '成功', sourceIp: '10.10.2.18', beforeValue: '状态: paid', afterValue: '状态: cancelled', remark: '买家要求取消' },
+  { id: 'l2', time: '2026-04-19 21:49:43', actor: '采购组 B / 周明', module: '供应商中心', type: '确认到货', objectId: 'PO-20260419-001', result: '成功', sourceIp: '10.10.3.07', beforeValue: '到货进度: 0/12', afterValue: '到货进度: 4/12', remark: '' },
+  { id: 'l3', time: '2026-04-19 21:36:28', actor: '仓库组 A / 李涛', module: '库存与履约', type: '确认发货', objectId: 'SHIP-20260419-0011', result: '成功', sourceIp: '10.10.4.21', beforeValue: '状态: 待发货', afterValue: '状态: 已发货', remark: '' },
+  { id: 'l4', time: '2026-04-19 20:15:00', actor: '运营组 A / 陈欣', module: '商品中心', type: '编辑商品', objectId: 'SPU-TR-1001', result: '成功', sourceIp: '10.10.2.18', beforeValue: '标题: TravelFlex Carry-On', afterValue: '标题: TravelFlex Expandable Carry-On', remark: '补充标题' },
+  { id: 'l5', time: '2026-04-19 18:30:00', actor: '系统', module: '库存与履约', type: '自动锁定库存', objectId: 'OMS-20260419-0012', result: '成功', sourceIp: '系统', beforeValue: '可用库存: 24', afterValue: '可用库存: 22', remark: '订单支付后自动锁定' }
+];
+
+export const apiKeys = [
+  { id: 'ak1', name: 'shopify-erp-sync', system: 'ERP Bridge', scope: '订单同步 / 发货回传', status: '启用', expireAt: '2026-07-31', lastUsedAt: '2026-04-19 21:52', ipWhitelist: '10.10.*.*', remark: 'ERP 系统对接' },
+  { id: 'ak2', name: 'analytics-reporter', system: 'BI Center', scope: '报表读取', status: '启用', expireAt: '2026-06-30', lastUsedAt: '2026-04-19 20:10', ipWhitelist: '10.20.*.*', remark: 'BI 平台数据拉取' },
+  { id: 'ak3', name: 'partner-warehouse', system: 'WMS Partner', scope: '库存读写', status: '待轮换', expireAt: '2026-05-15', lastUsedAt: '2026-04-18 16:34', ipWhitelist: '10.30.*.*', remark: '仓库系统对接' }
+];
+
+export const mockUsers = [
+  {
+    username: 'admin',
+    password: '123456',
+    token: 'token-admin',
+    user: { name: '周岚', role: 'admin', roleLabel: '系统管理员', workspace: '全球运营指挥台', avatar: 'CB' }
+  },
+  {
+    username: 'operator',
+    password: '123456',
+    token: 'token-operator',
+    user: { name: '陈欣', role: 'operator', roleLabel: '运营负责人', workspace: '商品与渠道工作台', avatar: 'YX' }
+  },
+  {
+    username: 'procurement',
+    password: '123456',
+    token: 'token-procurement',
+    user: { name: '周明', role: 'procurement', roleLabel: '采购专员', workspace: '供应链协同台', avatar: 'CG' }
+  },
+  {
+    username: 'warehouse',
+    password: '123456',
+    token: 'token-warehouse',
+    user: { name: '李涛', role: 'warehouse', roleLabel: '仓配主管', workspace: '履约作业台', avatar: 'WK' }
+  }
+];

+ 410 - 0
src/mock/routes.ts

@@ -0,0 +1,410 @@
+import {
+  afterSaleOrders,
+  apiKeys,
+  channels,
+  dashboardOverview,
+  inventory,
+  inventoryLogs,
+  logs,
+  mappingItems,
+  mockUsers,
+  orders,
+  pricingRules,
+  products,
+  purchaseOrders,
+  reportData,
+  reports,
+  roles,
+  shippingOrders,
+  supplyCapabilities,
+  suppliers
+} from './data';
+
+interface MockRoute {
+  handler: (context?: {
+    body?: unknown;
+    headers?: Record<string, string | string[] | undefined>;
+    method?: string;
+    query?: Record<string, string>;
+    url?: string;
+  }) => { code: number; data: unknown; message: string } | Promise<{ code: number; data: unknown; message: string }>;
+  method: 'DELETE' | 'GET' | 'POST' | 'PUT';
+  path: RegExp | string;
+}
+
+const ok = (data: unknown) => ({ code: 0, data, message: 'ok' });
+const fail = (message: string, code = 1) => ({ code, data: null, message });
+
+/* helper: simulate a delay-free immediate response */
+const genId = () => 'id-' + Math.random().toString(36).slice(2, 10);
+
+export const mockRoutes: MockRoute[] = [
+  /* ── Auth ── */
+  {
+    method: 'POST',
+    path: '/api/auth/login',
+    handler: async (context) => {
+      const body = (context?.body || {}) as { password?: string; username?: string };
+      const matched = mockUsers.find((item) => item.username === body.username && item.password === body.password);
+      if (!matched) return fail('用户名或密码错误');
+      return ok({ token: matched.token, user: matched.user });
+    }
+  },
+  {
+    method: 'GET',
+    path: '/api/auth/me',
+    handler: async (context) => {
+      const authorization = context?.headers?.authorization;
+      const token = Array.isArray(authorization) ? authorization[0] : authorization;
+      const normalized = token?.replace('Bearer ', '');
+      const matched = mockUsers.find((item) => item.token === normalized);
+      if (!matched) return fail('登录状态已失效', 401);
+      return ok(matched.user);
+    }
+  },
+
+  /* ── Dashboard ── */
+  { method: 'GET', path: '/api/dashboard/overview', handler: async () => ok(dashboardOverview) },
+
+  /* ── Products ── */
+  { method: 'GET', path: '/api/products', handler: async () => ok({ items: products }) },
+  {
+    method: 'POST',
+    path: '/api/products',
+    handler: async (context) => {
+      const body = context?.body as Record<string, unknown>;
+      const newProduct = { id: genId(), ...body, updatedAt: new Date().toISOString().slice(0, 16).replace('T', ' ') };
+      products.push(newProduct as never);
+      return ok(newProduct);
+    }
+  },
+  {
+    method: 'PUT',
+    path: /^\/api\/products\/(.+)$/,
+    handler: async (context) => {
+      const url = context?.url || '';
+      const id = url.split('/').pop();
+      const body = context?.body as Record<string, unknown>;
+      const idx = products.findIndex((p) => p.id === id);
+      if (idx === -1) return fail('商品不存在');
+      Object.assign(products[idx], body, { updatedAt: new Date().toISOString().slice(0, 16).replace('T', ' ') });
+      return ok(products[idx]);
+    }
+  },
+  {
+    method: 'DELETE',
+    path: /^\/api\/products\/(.+)$/,
+    handler: async (context) => {
+      const url = context?.url || '';
+      const id = url.split('/').pop();
+      const idx = products.findIndex((p) => p.id === id);
+      if (idx === -1) return fail('商品不存在');
+      products.splice(idx, 1);
+      return ok({ success: true });
+    }
+  },
+
+  /* ── Channel Mappings ── */
+  { method: 'GET', path: '/api/channel-mappings', handler: async () => ok({ items: mappingItems }) },
+  {
+    method: 'POST',
+    path: '/api/channel-mappings',
+    handler: async (context) => {
+      const body = context?.body as Record<string, unknown>;
+      const item = { id: genId(), ...body };
+      mappingItems.push(item as never);
+      return ok(item);
+    }
+  },
+  {
+    method: 'PUT',
+    path: /^\/api\/channel-mappings\/(.+)$/,
+    handler: async (context) => {
+      const url = context?.url || '';
+      const id = url.split('/').pop();
+      const body = context?.body as Record<string, unknown>;
+      const idx = mappingItems.findIndex((m) => m.id === id);
+      if (idx === -1) return fail('映射不存在');
+      Object.assign(mappingItems[idx], body);
+      return ok(mappingItems[idx]);
+    }
+  },
+
+  /* ── Pricing Rules ── */
+  { method: 'GET', path: '/api/pricing-rules', handler: async () => ok({ items: pricingRules }) },
+  {
+    method: 'POST',
+    path: '/api/pricing-rules',
+    handler: async (context) => {
+      const body = context?.body as Record<string, unknown>;
+      const item = { id: genId(), ...body, updatedAt: new Date().toISOString().slice(0, 16).replace('T', ' ') };
+      pricingRules.push(item as never);
+      return ok(item);
+    }
+  },
+  {
+    method: 'PUT',
+    path: /^\/api\/pricing-rules\/(.+)$/,
+    handler: async (context) => {
+      const url = context?.url || '';
+      const id = url.split('/').pop();
+      const body = context?.body as Record<string, unknown>;
+      const idx = pricingRules.findIndex((r) => r.id === id);
+      if (idx === -1) return fail('规则不存在');
+      Object.assign(pricingRules[idx], body, { updatedAt: new Date().toISOString().slice(0, 16).replace('T', ' ') });
+      return ok(pricingRules[idx]);
+    }
+  },
+
+  /* ── Orders ── */
+  { method: 'GET', path: '/api/orders', handler: async () => ok({ items: orders }) },
+  {
+    method: 'GET',
+    path: /^\/api\/orders\/(.+)$/,
+    handler: async (context) => {
+      const url = context?.url || '';
+      const id = url.split('/').pop();
+      const order = orders.find((o) => o.id === id || o.orderNo === id);
+      if (!order) return fail('订单不存在');
+      return ok(order);
+    }
+  },
+  {
+    method: 'PUT',
+    path: /^\/api\/orders\/(.+)$/,
+    handler: async (context) => {
+      const url = context?.url || '';
+      const id = url.split('/').pop();
+      const body = context?.body as Record<string, unknown>;
+      const idx = orders.findIndex((o) => o.id === id || o.orderNo === id);
+      if (idx === -1) return fail('订单不存在');
+      Object.assign(orders[idx], body);
+      return ok(orders[idx]);
+    }
+  },
+
+  /* ── After Sale ── */
+  { method: 'GET', path: '/api/after-sale', handler: async () => ok({ items: afterSaleOrders }) },
+  {
+    method: 'PUT',
+    path: /^\/api\/after-sale\/(.+)$/,
+    handler: async (context) => {
+      const url = context?.url || '';
+      const id = url.split('/').pop();
+      const body = context?.body as Record<string, unknown>;
+      const idx = afterSaleOrders.findIndex((a) => a.id === id);
+      if (idx === -1) return fail('售后单不存在');
+      Object.assign(afterSaleOrders[idx], body, { updatedAt: new Date().toISOString().slice(0, 16).replace('T', ' ') });
+      return ok(afterSaleOrders[idx]);
+    }
+  },
+
+  /* ── Suppliers ── */
+  { method: 'GET', path: '/api/suppliers', handler: async () => ok({ items: suppliers }) },
+  {
+    method: 'POST',
+    path: '/api/suppliers',
+    handler: async (context) => {
+      const body = context?.body as Record<string, unknown>;
+      const item = { id: genId(), ...body, updatedAt: new Date().toISOString().slice(0, 16).replace('T', ' ') };
+      suppliers.push(item as never);
+      return ok(item);
+    }
+  },
+  {
+    method: 'PUT',
+    path: /^\/api\/suppliers\/(.+)$/,
+    handler: async (context) => {
+      const url = context?.url || '';
+      const id = url.split('/').pop();
+      const body = context?.body as Record<string, unknown>;
+      const idx = suppliers.findIndex((s) => s.id === id);
+      if (idx === -1) return fail('供应商不存在');
+      Object.assign(suppliers[idx], body, { updatedAt: new Date().toISOString().slice(0, 16).replace('T', ' ') });
+      return ok(suppliers[idx]);
+    }
+  },
+
+  /* ── Purchase Orders ── */
+  { method: 'GET', path: '/api/purchase-orders', handler: async () => ok({ items: purchaseOrders }) },
+  {
+    method: 'POST',
+    path: '/api/purchase-orders',
+    handler: async (context) => {
+      const body = context?.body as Record<string, unknown>;
+      const item = { id: genId(), ...body, createdAt: new Date().toISOString().slice(0, 16).replace('T', ' ') };
+      purchaseOrders.push(item as never);
+      return ok(item);
+    }
+  },
+  {
+    method: 'PUT',
+    path: /^\/api\/purchase-orders\/(.+)$/,
+    handler: async (context) => {
+      const url = context?.url || '';
+      const id = url.split('/').pop();
+      const body = context?.body as Record<string, unknown>;
+      const idx = purchaseOrders.findIndex((p) => p.id === id);
+      if (idx === -1) return fail('采购单不存在');
+      Object.assign(purchaseOrders[idx], body);
+      return ok(purchaseOrders[idx]);
+    }
+  },
+
+  /* ── Supply Capability ── */
+  { method: 'GET', path: '/api/supply-capabilities', handler: async () => ok({ items: supplyCapabilities }) },
+  {
+    method: 'POST',
+    path: '/api/supply-capabilities',
+    handler: async (context) => {
+      const body = context?.body as Record<string, unknown>;
+      const item = { id: genId(), ...body, updatedAt: new Date().toISOString().slice(0, 16).replace('T', ' ') };
+      supplyCapabilities.push(item as never);
+      return ok(item);
+    }
+  },
+  {
+    method: 'PUT',
+    path: /^\/api\/supply-capabilities\/(.+)$/,
+    handler: async (context) => {
+      const url = context?.url || '';
+      const id = url.split('/').pop();
+      const body = context?.body as Record<string, unknown>;
+      const idx = supplyCapabilities.findIndex((s) => s.id === id);
+      if (idx === -1) return fail('配置不存在');
+      Object.assign(supplyCapabilities[idx], body, { updatedAt: new Date().toISOString().slice(0, 16).replace('T', ' ') });
+      return ok(supplyCapabilities[idx]);
+    }
+  },
+
+  /* ── Channels ── */
+  { method: 'GET', path: '/api/channels', handler: async () => ok({ items: channels }) },
+  {
+    method: 'POST',
+    path: '/api/channels',
+    handler: async (context) => {
+      const body = context?.body as Record<string, unknown>;
+      const item = { id: genId(), ...body };
+      channels.push(item as never);
+      return ok(item);
+    }
+  },
+  {
+    method: 'PUT',
+    path: /^\/api\/channels\/(.+)$/,
+    handler: async (context) => {
+      const url = context?.url || '';
+      const id = url.split('/').pop();
+      const body = context?.body as Record<string, unknown>;
+      const idx = channels.findIndex((c) => c.id === id);
+      if (idx === -1) return fail('渠道不存在');
+      Object.assign(channels[idx], body);
+      return ok(channels[idx]);
+    }
+  },
+
+  /* ── Inventory ── */
+  { method: 'GET', path: '/api/inventory', handler: async () => ok({ items: inventory }) },
+  { method: 'GET', path: '/api/inventory-logs', handler: async () => ok({ items: inventoryLogs }) },
+  {
+    method: 'PUT',
+    path: /^\/api\/inventory\/(.+)$/,
+    handler: async (context) => {
+      const url = context?.url || '';
+      const id = url.split('/').pop();
+      const body = context?.body as Record<string, unknown>;
+      const idx = inventory.findIndex((i) => i.id === id);
+      if (idx === -1) return fail('库存记录不存在');
+      Object.assign(inventory[idx], body, { lastChangeAt: new Date().toISOString().slice(0, 16).replace('T', ' ') });
+      return ok(inventory[idx]);
+    }
+  },
+
+  /* ── Shipping ── */
+  { method: 'GET', path: '/api/shipping-orders', handler: async () => ok({ items: shippingOrders }) },
+  {
+    method: 'PUT',
+    path: /^\/api\/shipping-orders\/(.+)$/,
+    handler: async (context) => {
+      const url = context?.url || '';
+      const id = url.split('/').pop();
+      const body = context?.body as Record<string, unknown>;
+      const idx = shippingOrders.findIndex((s) => s.id === id);
+      if (idx === -1) return fail('发货单不存在');
+      Object.assign(shippingOrders[idx], body);
+      return ok(shippingOrders[idx]);
+    }
+  },
+
+  /* ── Reports ── */
+  { method: 'GET', path: '/api/reports', handler: async () => ok({ items: reports }) },
+  { method: 'GET', path: '/api/report-data', handler: async () => ok({ items: reportData }) },
+
+  /* ── System: Roles ── */
+  { method: 'GET', path: '/api/system/roles', handler: async () => ok({ items: roles }) },
+  {
+    method: 'POST',
+    path: '/api/system/roles',
+    handler: async (context) => {
+      const body = context?.body as Record<string, unknown>;
+      const item = { id: genId(), ...body, updatedAt: new Date().toISOString().slice(0, 16).replace('T', ' ') };
+      roles.push(item as never);
+      return ok(item);
+    }
+  },
+  {
+    method: 'PUT',
+    path: /^\/api\/system\/roles\/(.+)$/,
+    handler: async (context) => {
+      const url = context?.url || '';
+      const id = url.split('/').pop();
+      const body = context?.body as Record<string, unknown>;
+      const idx = roles.findIndex((r) => r.id === id);
+      if (idx === -1) return fail('角色不存在');
+      Object.assign(roles[idx], body, { updatedAt: new Date().toISOString().slice(0, 16).replace('T', ' ') });
+      return ok(roles[idx]);
+    }
+  },
+
+  /* ── System: Logs ── */
+  { method: 'GET', path: '/api/system/logs', handler: async () => ok({ items: logs }) },
+
+  /* ── System: API Keys ── */
+  { method: 'GET', path: '/api/system/api-keys', handler: async () => ok({ items: apiKeys }) },
+  {
+    method: 'POST',
+    path: '/api/system/api-keys',
+    handler: async (context) => {
+      const body = context?.body as Record<string, unknown>;
+      const key = 'cbos_' + Math.random().toString(36).slice(2, 18);
+      const item = { id: genId(), ...body, keyValue: key, lastUsedAt: '-', updatedAt: new Date().toISOString().slice(0, 16).replace('T', ' ') };
+      apiKeys.push(item as never);
+      return ok(item);
+    }
+  },
+  {
+    method: 'PUT',
+    path: /^\/api\/system\/api-keys\/(.+)$/,
+    handler: async (context) => {
+      const url = context?.url || '';
+      const id = url.split('/').pop();
+      const body = context?.body as Record<string, unknown>;
+      const idx = apiKeys.findIndex((k) => k.id === id);
+      if (idx === -1) return fail('API Key 不存在');
+      Object.assign(apiKeys[idx], body);
+      return ok(apiKeys[idx]);
+    }
+  },
+  {
+    method: 'DELETE',
+    path: /^\/api\/system\/api-keys\/(.+)$/,
+    handler: async (context) => {
+      const url = context?.url || '';
+      const id = url.split('/').pop();
+      const idx = apiKeys.findIndex((k) => k.id === id);
+      if (idx === -1) return fail('API Key 不存在');
+      apiKeys.splice(idx, 1);
+      return ok({ success: true });
+    }
+  }
+];

+ 32 - 0
src/router/index.ts

@@ -0,0 +1,32 @@
+import { createRouter, createWebHistory } from 'vue-router';
+import { routes } from './routes';
+import { useAuthStore } from '@/stores/auth';
+import type { UserRole } from '@/types/page';
+
+const router = createRouter({
+  history: createWebHistory(),
+  routes
+});
+
+router.beforeEach(async (to) => {
+  const authStore = useAuthStore();
+  await authStore.bootstrap();
+
+  if (to.meta.public && to.path === '/login' && authStore.isAuthenticated) {
+    return '/report/dashboard';
+  }
+
+  if (!to.meta.public && !authStore.isAuthenticated) {
+    return {
+      path: '/login',
+      query: { redirect: to.fullPath }
+    };
+  }
+
+  const roles = to.meta.roles as UserRole[] | undefined;
+  if (roles?.length && authStore.role && !roles.includes(authStore.role)) {
+    return '/403';
+  }
+});
+
+export default router;

+ 75 - 0
src/router/menu.ts

@@ -0,0 +1,75 @@
+import type { UserRole } from '@/types/page';
+
+export interface MenuItem {
+  key: string;
+  path: string;
+  roles: UserRole[];
+  title: string;
+}
+
+export interface MenuGroup {
+  key: string;
+  title: string;
+  items: MenuItem[];
+}
+
+export const menuGroups: MenuGroup[] = [
+  {
+    key: 'reports',
+    title: '数据与报表',
+    items: [
+      { key: 'report-dashboard', title: '运营看板', path: '/report/dashboard', roles: ['admin', 'manager', 'operator', 'procurement'] },
+      { key: 'report-center', title: '报表中心', path: '/report/center', roles: ['admin', 'manager', 'operator', 'procurement'] }
+    ]
+  },
+  {
+    key: 'product',
+    title: '商品中心',
+    items: [
+      { key: 'product-list', title: '商品列表', path: '/product/list', roles: ['admin', 'manager', 'operator'] },
+      { key: 'product-editor', title: '商品新建/编辑', path: '/product/editor', roles: ['admin', 'manager', 'operator'] },
+      { key: 'product-mapping', title: '渠道映射', path: '/product/mapping', roles: ['admin', 'manager', 'operator'] },
+      { key: 'product-pricing', title: '定价与库存规则', path: '/product/pricing', roles: ['admin', 'manager', 'operator'] }
+    ]
+  },
+  {
+    key: 'orders',
+    title: '订单中心',
+    items: [
+      { key: 'order-list', title: '订单列表', path: '/order/list', roles: ['admin', 'manager', 'operator', 'customer_service', 'warehouse'] },
+      { key: 'order-detail', title: '订单详情/处理', path: '/order/detail', roles: ['admin', 'manager', 'operator', 'customer_service', 'warehouse'] },
+      { key: 'order-after-sale', title: '售后处理', path: '/order/after-sale', roles: ['admin', 'manager', 'operator', 'customer_service'] }
+    ]
+  },
+  {
+    key: 'supplier',
+    title: '供应商中心',
+    items: [
+      { key: 'supplier-list', title: '供应商管理', path: '/supplier/list', roles: ['admin', 'manager', 'procurement'] },
+      { key: 'purchase-orders', title: '采购单管理', path: '/supplier/purchase-orders', roles: ['admin', 'manager', 'procurement'] },
+      { key: 'supply-capability', title: '供货能力配置', path: '/supplier/capability', roles: ['admin', 'manager', 'procurement'] }
+    ]
+  },
+  {
+    key: 'channel',
+    title: '渠道中心',
+    items: [{ key: 'channel-config', title: '渠道接入配置', path: '/channel/config', roles: ['admin', 'manager', 'operator'] }]
+  },
+  {
+    key: 'inventory',
+    title: '库存与履约',
+    items: [
+      { key: 'inventory-overview', title: '库存总览', path: '/inventory/overview', roles: ['admin', 'manager', 'operator', 'procurement', 'warehouse'] },
+      { key: 'shipping-work', title: '发货作业', path: '/inventory/shipping', roles: ['admin', 'manager', 'warehouse'] }
+    ]
+  },
+  {
+    key: 'system',
+    title: '系统与权限',
+    items: [
+      { key: 'role-permission', title: '角色权限', path: '/system/roles', roles: ['admin'] },
+      { key: 'operation-log', title: '操作日志', path: '/system/logs', roles: ['admin', 'manager'] },
+      { key: 'api-key', title: 'API Key 管理', path: '/system/api-keys', roles: ['admin'] }
+    ]
+  }
+];

+ 136 - 0
src/router/routes.ts

@@ -0,0 +1,136 @@
+import type { RouteRecordRaw } from 'vue-router';
+import AppLayout from '@/layout/AppLayout.vue';
+import type { UserRole } from '@/types/page';
+
+const allRoles: UserRole[] = ['admin', 'manager', 'operator', 'procurement', 'warehouse', 'customer_service'];
+
+export const routes: RouteRecordRaw[] = [
+  {
+    path: '/login',
+    name: 'login',
+    component: () => import('@/views/auth/LoginView.vue'),
+    meta: { public: true, title: '登录' }
+  },
+  {
+    path: '/',
+    component: AppLayout,
+    redirect: '/report/dashboard',
+    meta: { requiresAuth: true },
+    children: [
+      {
+        path: '/report/dashboard',
+        name: 'report-dashboard',
+        component: () => import('@/views/dashboard/ReportDashboardView.vue'),
+        meta: { title: '运营看板', pageKey: 'report-dashboard', roles: ['admin', 'manager', 'operator', 'procurement'] }
+      },
+      {
+        path: '/report/center',
+        name: 'report-center',
+        component: () => import('@/views/report/ReportCenterView.vue'),
+        meta: { title: '报表中心', pageKey: 'report-center', roles: ['admin', 'manager', 'operator', 'procurement'] }
+      },
+      {
+        path: '/product/list',
+        name: 'product-list',
+        component: () => import('@/views/product/ProductListView.vue'),
+        meta: { title: '商品列表', pageKey: 'product-list', roles: ['admin', 'manager', 'operator'] }
+      },
+      {
+        path: '/product/editor',
+        name: 'product-editor',
+        component: () => import('@/views/product/ProductEditorView.vue'),
+        meta: { title: '商品新建/编辑', pageKey: 'product-editor', roles: ['admin', 'manager', 'operator'] }
+      },
+      {
+        path: '/product/mapping',
+        name: 'product-mapping',
+        component: () => import('@/views/product/ProductMappingView.vue'),
+        meta: { title: '渠道映射', pageKey: 'product-mapping', roles: ['admin', 'manager', 'operator'] }
+      },
+      {
+        path: '/product/pricing',
+        name: 'product-pricing',
+        component: () => import('@/views/product/ProductPricingView.vue'),
+        meta: { title: '定价与库存规则', pageKey: 'product-pricing', roles: ['admin', 'manager', 'operator'] }
+      },
+      {
+        path: '/order/list',
+        name: 'order-list',
+        component: () => import('@/views/order/OrderListView.vue'),
+        meta: { title: '订单列表', pageKey: 'order-list', roles: ['admin', 'manager', 'operator', 'customer_service', 'warehouse'] }
+      },
+      {
+        path: '/order/detail',
+        name: 'order-detail',
+        component: () => import('@/views/order/OrderDetailView.vue'),
+        meta: { title: '订单详情/处理', pageKey: 'order-detail', roles: ['admin', 'manager', 'operator', 'customer_service', 'warehouse'] }
+      },
+      {
+        path: '/order/after-sale',
+        name: 'order-after-sale',
+        component: () => import('@/views/shared/SpecOnlyPage.vue'),
+        meta: { title: '售后处理', pageKey: 'order-after-sale', roles: ['admin', 'manager', 'operator', 'customer_service'] }
+      },
+      {
+        path: '/supplier/list',
+        name: 'supplier-list',
+        component: () => import('@/views/supplier/SupplierListView.vue'),
+        meta: { title: '供应商管理', pageKey: 'supplier-list', roles: ['admin', 'manager', 'procurement'] }
+      },
+      {
+        path: '/supplier/purchase-orders',
+        name: 'purchase-orders',
+        component: () => import('@/views/supplier/PurchaseOrderView.vue'),
+        meta: { title: '采购单管理', pageKey: 'purchase-orders', roles: ['admin', 'manager', 'procurement'] }
+      },
+      {
+        path: '/supplier/capability',
+        name: 'supply-capability',
+        component: () => import('@/views/shared/SpecOnlyPage.vue'),
+        meta: { title: '供货能力配置', pageKey: 'supply-capability', roles: ['admin', 'manager', 'procurement'] }
+      },
+      {
+        path: '/channel/config',
+        name: 'channel-config',
+        component: () => import('@/views/channel/ChannelConfigView.vue'),
+        meta: { title: '渠道接入配置', pageKey: 'channel-config', roles: ['admin', 'manager', 'operator'] }
+      },
+      {
+        path: '/inventory/overview',
+        name: 'inventory-overview',
+        component: () => import('@/views/inventory/InventoryOverviewView.vue'),
+        meta: { title: '库存总览', pageKey: 'inventory-overview', roles: ['admin', 'manager', 'operator', 'procurement', 'warehouse'] }
+      },
+      {
+        path: '/inventory/shipping',
+        name: 'shipping-work',
+        component: () => import('@/views/inventory/ShippingWorkView.vue'),
+        meta: { title: '发货作业', pageKey: 'shipping-work', roles: ['admin', 'manager', 'warehouse'] }
+      },
+      {
+        path: '/system/roles',
+        name: 'role-permission',
+        component: () => import('@/views/system/RolePermissionView.vue'),
+        meta: { title: '角色权限', pageKey: 'role-permission', roles: ['admin'] }
+      },
+      {
+        path: '/system/logs',
+        name: 'operation-log',
+        component: () => import('@/views/system/OperationLogView.vue'),
+        meta: { title: '操作日志', pageKey: 'operation-log', roles: ['admin', 'manager'] }
+      },
+      {
+        path: '/system/api-keys',
+        name: 'api-key',
+        component: () => import('@/views/system/ApiKeyView.vue'),
+        meta: { title: 'API Key 管理', pageKey: 'api-key', roles: ['admin'] }
+      }
+    ]
+  },
+  {
+    path: '/403',
+    name: '403',
+    component: () => import('@/views/shared/ForbiddenView.vue'),
+    meta: { public: true, title: '无权限访问', roles: allRoles }
+  }
+];

+ 12 - 0
src/stores/app.ts

@@ -0,0 +1,12 @@
+import { defineStore } from 'pinia';
+
+export const useAppStore = defineStore('app', {
+  state: () => ({
+    menuCollapsed: false
+  }),
+  actions: {
+    toggleMenu() {
+      this.menuCollapsed = !this.menuCollapsed;
+    }
+  }
+});

+ 59 - 0
src/stores/auth.ts

@@ -0,0 +1,59 @@
+import { defineStore } from 'pinia';
+import { api } from '@/api/services';
+import type { UserProfile, UserRole } from '@/types/page';
+
+const TOKEN_KEY = 'cbos-token';
+
+export const useAuthStore = defineStore('auth', {
+  state: () => ({
+    bootstrapped: false,
+    loading: false,
+    token: localStorage.getItem(TOKEN_KEY) || '',
+    user: null as UserProfile | null
+  }),
+  getters: {
+    isAuthenticated: (state) => Boolean(state.token && state.user),
+    role: (state): UserRole | '' => state.user?.role || '',
+    roleLabel: (state) => state.user?.roleLabel || '未登录'
+  },
+  actions: {
+    async login(username: string, password: string) {
+      this.loading = true;
+      try {
+        const response = await api.login({ username, password });
+        this.token = response.token;
+        this.user = response.user;
+        localStorage.setItem(TOKEN_KEY, response.token);
+        this.bootstrapped = true;
+      } finally {
+        this.loading = false;
+      }
+    },
+    async bootstrap() {
+      if (this.bootstrapped) {
+        return;
+      }
+
+      if (!this.token) {
+        this.bootstrapped = true;
+        return;
+      }
+
+      this.loading = true;
+      try {
+        this.user = await api.getCurrentUser();
+      } catch {
+        this.logout();
+      } finally {
+        this.loading = false;
+        this.bootstrapped = true;
+      }
+    },
+    logout() {
+      this.token = '';
+      this.user = null;
+      this.bootstrapped = true;
+      localStorage.removeItem(TOKEN_KEY);
+    }
+  }
+});

+ 247 - 0
src/styles/global.scss

@@ -0,0 +1,247 @@
+:root {
+  color-scheme: light;
+  --cb-bg: #f4efe6;
+  --cb-bg-soft: #fbf8f2;
+  --cb-panel: rgba(255, 255, 255, 0.82);
+  --cb-panel-strong: #ffffff;
+  --cb-border: rgba(33, 53, 71, 0.12);
+  --cb-text: #213547;
+  --cb-text-soft: #597188;
+  --cb-primary: #0f766e;
+  --cb-primary-soft: rgba(15, 118, 110, 0.12);
+  --cb-accent: #d97706;
+  --cb-danger: #b42318;
+  --cb-shadow: 0 20px 50px rgba(33, 53, 71, 0.08);
+  --el-color-primary: #0f766e;
+  --el-border-radius-base: 14px;
+  --el-font-family: "HarmonyOS Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;
+}
+
+* {
+  box-sizing: border-box;
+}
+
+html,
+body,
+#app {
+  margin: 0;
+  min-height: 100%;
+  color: var(--cb-text);
+  background:
+    radial-gradient(circle at top left, rgba(217, 119, 6, 0.12), transparent 32%),
+    radial-gradient(circle at top right, rgba(15, 118, 110, 0.16), transparent 26%),
+    linear-gradient(180deg, #fbf8f2 0%, #f2ece1 100%);
+}
+
+body {
+  font-family: var(--el-font-family);
+}
+
+a {
+  color: inherit;
+  text-decoration: none;
+}
+
+.app-page {
+  display: flex;
+  flex-direction: column;
+  gap: 20px;
+}
+
+.glass-card {
+  border: 1px solid var(--cb-border);
+  border-radius: 22px;
+  background: var(--cb-panel);
+  box-shadow: var(--cb-shadow);
+  backdrop-filter: blur(14px);
+}
+
+.page-hero {
+  display: flex;
+  flex-wrap: wrap;
+  justify-content: space-between;
+  gap: 20px;
+  padding: 24px 28px;
+}
+
+.page-hero__meta {
+  max-width: 760px;
+}
+
+.page-hero__eyebrow {
+  display: inline-flex;
+  align-items: center;
+  gap: 8px;
+  margin-bottom: 10px;
+  padding: 6px 12px;
+  border-radius: 999px;
+  background: var(--cb-primary-soft);
+  color: var(--cb-primary);
+  font-size: 12px;
+  font-weight: 700;
+  letter-spacing: 0.08em;
+  text-transform: uppercase;
+}
+
+.page-hero h1,
+.page-hero h2 {
+  margin: 0 0 8px;
+  font-size: 28px;
+  line-height: 1.18;
+}
+
+.page-hero p {
+  margin: 0;
+  color: var(--cb-text-soft);
+  line-height: 1.7;
+}
+
+.page-grid {
+  display: grid;
+  gap: 20px;
+}
+
+.page-grid--two {
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+}
+
+.page-grid--three {
+  grid-template-columns: repeat(3, minmax(0, 1fr));
+}
+
+.section-card {
+  padding: 22px 24px;
+}
+
+.section-card__title {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 12px;
+  margin-bottom: 16px;
+}
+
+.section-card__title h3 {
+  margin: 0;
+  font-size: 18px;
+}
+
+.section-card__title p {
+  margin: 8px 0 0;
+  color: var(--cb-text-soft);
+}
+
+.chip-list {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 10px;
+}
+
+.chip {
+  display: inline-flex;
+  align-items: center;
+  gap: 8px;
+  padding: 8px 12px;
+  border: 1px solid rgba(15, 118, 110, 0.14);
+  border-radius: 999px;
+  background: rgba(255, 255, 255, 0.78);
+  color: var(--cb-text);
+  font-size: 13px;
+}
+
+.muted {
+  color: var(--cb-text-soft);
+}
+
+.tight-list {
+  display: grid;
+  gap: 10px;
+  margin: 0;
+  padding-left: 18px;
+  color: var(--cb-text-soft);
+  line-height: 1.7;
+}
+
+.stat-grid {
+  display: grid;
+  grid-template-columns: repeat(4, minmax(0, 1fr));
+  gap: 16px;
+}
+
+.stat-card {
+  padding: 18px 20px;
+  border: 1px solid var(--cb-border);
+  border-radius: 18px;
+  background: rgba(255, 255, 255, 0.78);
+}
+
+.stat-card__label {
+  color: var(--cb-text-soft);
+  font-size: 13px;
+}
+
+.stat-card__value {
+  margin-top: 10px;
+  font-size: 28px;
+  font-weight: 700;
+}
+
+.stat-card__trend {
+  margin-top: 8px;
+  color: var(--cb-primary);
+  font-size: 13px;
+}
+
+.table-toolbar {
+  display: flex;
+  flex-wrap: wrap;
+  justify-content: space-between;
+  gap: 12px;
+  margin-bottom: 16px;
+}
+
+.drawer-preview {
+  display: grid;
+  gap: 14px;
+}
+
+.drawer-preview__item {
+  padding: 14px 16px;
+  border: 1px solid var(--cb-border);
+  border-radius: 16px;
+  background: rgba(255, 255, 255, 0.62);
+}
+
+.drawer-preview__item strong {
+  display: block;
+  margin-bottom: 6px;
+}
+
+.status-dot {
+  display: inline-flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.status-dot::before {
+  content: "";
+  width: 8px;
+  height: 8px;
+  border-radius: 999px;
+  background: currentColor;
+}
+
+@media (max-width: 1200px) {
+  .stat-grid,
+  .page-grid--three {
+    grid-template-columns: repeat(2, minmax(0, 1fr));
+  }
+}
+
+@media (max-width: 900px) {
+  .page-grid--two,
+  .page-grid--three,
+  .stat-grid {
+    grid-template-columns: 1fr;
+  }
+}

+ 404 - 0
src/types/page.ts

@@ -0,0 +1,404 @@
+export interface PageSpec {
+  description: string;
+  filters?: string[];
+  formFields?: string[];
+  interactions?: string[];
+  listFields?: string[];
+  title: string;
+}
+
+export interface DashboardStat {
+  label: string;
+  tone: 'danger' | 'info' | 'primary' | 'success' | 'warning';
+  trend: string;
+  value: string;
+}
+
+export interface DashboardAlert {
+  level: 'critical' | 'info' | 'warning';
+  summary: string;
+  title: string;
+}
+
+/* ───── Product ───── */
+export interface ProductItem {
+  id: string;
+  title: string;
+  spu: string;
+  skuCount: number;
+  category: string;
+  brand: string;
+  channelStatus: string;
+  inventory: number;
+  priceRange: string;
+  owner: string;
+  status: string;
+  updatedAt: string;
+  image?: string;
+}
+
+export interface ProductFormData {
+  title: string;
+  subtitle: string;
+  category: string;
+  brand: string;
+  tags: string[];
+  description: string;
+  specs: SpecItem[];
+  skus: SkuItem[];
+  images: MediaItem[];
+  videos: MediaItem[];
+  translations: TranslationItem[];
+}
+
+export interface SpecItem {
+  name: string;
+  values: string[];
+}
+
+export interface SkuItem {
+  id: string;
+  specCombo: string;
+  barcode: string;
+  costPrice: string;
+  suggestPrice: string;
+  weight: string;
+  length: string;
+  width: string;
+  height: string;
+}
+
+export interface MediaItem {
+  url: string;
+  name: string;
+  type: 'image' | 'video';
+}
+
+export interface TranslationItem {
+  lang: string;
+  title: string;
+  sellingPoint: string;
+  description: string;
+}
+
+/* ───── Channel Mapping ───── */
+export interface MappingItem {
+  id: string;
+  internalSku: string;
+  channelSku: string;
+  channel: string;
+  shopName: string;
+  channelCategory: string;
+  mappingStatus: string;
+  validateStatus: string;
+  lastSyncAt: string;
+  lastValidateResult: string;
+  productTitle: string;
+}
+
+export interface MappingFormData {
+  channel: string;
+  shopName: string;
+  channelCategory: string;
+  channelTitle: string;
+  channelDescription: string;
+  channelPrice: string;
+  channelImage: string;
+  attributeMappings: { internal: string; channel: string }[];
+}
+
+/* ───── Pricing ───── */
+export interface PricingRuleItem {
+  id: string;
+  sku: string;
+  productTitle: string;
+  currency: string;
+  basePrice: string;
+  channelPrice: string;
+  discountPrice: string;
+  effectiveTime: string;
+  safeStock: number;
+  status: string;
+  modifiedBy: string;
+  updatedAt: string;
+}
+
+export interface PricingRuleFormData {
+  sku: string;
+  currency: string;
+  basePrice: string;
+  channelAdjustRule: string;
+  discountPrice: string;
+  exchangeSource: string;
+  warehouse: string;
+  safeStockThreshold: number;
+  warningRecipients: string;
+  effectiveTime: string;
+  expireTime: string;
+}
+
+/* ───── Order ───── */
+export interface OrderItem {
+  id: string;
+  orderNo: string;
+  channelOrderNo: string;
+  channel: string;
+  buyer: string;
+  amount: string;
+  itemCount: number;
+  paymentStatus: string;
+  orderStatus: string;
+  shippingStatus: string;
+  warehouse: string;
+  exceptionTag: string;
+  createdAt: string;
+}
+
+export interface OrderDetail extends OrderItem {
+  receiver: string;
+  phone: string;
+  address: string;
+  payMethod: string;
+  payTime: string;
+  carrier: string;
+  trackingNo: string;
+  items: OrderProductItem[];
+  timeline: StatusEvent[];
+  logs: string[];
+  remark: string;
+}
+
+export interface OrderProductItem {
+  sku: string;
+  title: string;
+  image: string;
+  qty: number;
+  unitPrice: string;
+  subtotal: string;
+}
+
+export interface StatusEvent {
+  status: string;
+  time: string;
+  operator: string;
+}
+
+/* ───── After Sale ───── */
+export interface AfterSaleItem {
+  id: string;
+  afterSaleNo: string;
+  orderNo: string;
+  buyer: string;
+  type: string;
+  amount: string;
+  auditStatus: string;
+  refundStatus: string;
+  reason: string;
+  updatedAt: string;
+}
+
+export interface AfterSaleFormData {
+  type: string;
+  reason: string;
+  responsibility: string;
+  refundAmount: string;
+  refundMethod: string;
+  returnTrackingNo: string;
+  resendWarehouse: string;
+  resendSku: string;
+  auditRemark: string;
+}
+
+/* ───── Supplier ───── */
+export interface SupplierItem {
+  id: string;
+  name: string;
+  companyName: string;
+  contact: string;
+  phone: string;
+  email: string;
+  address: string;
+  settlement: string;
+  rating: string;
+  status: string;
+  relatedSkuCount: number;
+  updatedAt: string;
+}
+
+export interface SupplierFormData {
+  name: string;
+  companyName: string;
+  contact: string;
+  phone: string;
+  email: string;
+  address: string;
+  bankInfo: string;
+  taxNo: string;
+  contractNo: string;
+  settlement: string;
+  remark: string;
+}
+
+/* ───── Purchase Order ───── */
+export interface PurchaseOrderItem {
+  id: string;
+  poNo: string;
+  supplier: string;
+  skuCount: number;
+  amount: string;
+  expectedDate: string;
+  arrivalProgress: string;
+  status: string;
+  creator: string;
+  createdAt: string;
+}
+
+export interface PurchaseOrderFormData {
+  supplier: string;
+  warehouse: string;
+  items: { sku: string; qty: number; price: string }[];
+  currency: string;
+  tax: string;
+  freight: string;
+  expectedDate: string;
+  remark: string;
+}
+
+/* ───── Supply Capability ───── */
+export interface SupplyCapabilityItem {
+  id: string;
+  supplier: string;
+  sku: string;
+  productTitle: string;
+  leadTime: number;
+  moq: number;
+  unit: string;
+  tierPrices: { from: number; to: number; price: string }[];
+  isDefault: boolean;
+  status: string;
+  updatedAt: string;
+}
+
+/* ───── Channel ───── */
+export interface ChannelItem {
+  id: string;
+  channel: string;
+  shopName: string;
+  tokenStatus: string;
+  syncStatus: string;
+  lastSyncAt: string;
+  errorMessage: string;
+  appKey?: string;
+  appSecret?: string;
+  accessToken?: string;
+  webhookUrl?: string;
+  webhookSecret?: string;
+  syncEnabled?: boolean;
+  defaultWarehouse?: string;
+  remark?: string;
+}
+
+/* ───── Inventory ───── */
+export interface InventoryItem {
+  id: string;
+  sku: string;
+  productTitle: string;
+  warehouse: string;
+  available: number;
+  locked: number;
+  inbound: number;
+  safeStock: number;
+  warningStatus: string;
+  lastChangeAt: string;
+}
+
+export interface InventoryLogItem {
+  id: string;
+  time: string;
+  source: string;
+  relatedOrder: string;
+  operator: string;
+  quantity: number;
+  afterQty: number;
+}
+
+/* ───── Shipping ───── */
+export interface ShippingItem {
+  id: string;
+  shipmentNo: string;
+  orderNo: string;
+  warehouse: string;
+  skuCount: number;
+  expectedQty: number;
+  actualQty: number;
+  carrier: string;
+  trackingNo: string;
+  shippingStatus: string;
+  returnStatus: string;
+  createdAt: string;
+}
+
+/* ───── Report ───── */
+export interface ReportItem {
+  id: string;
+  reportType: string;
+  dimensions: string;
+  owner: string;
+  updatedAt: string;
+}
+
+export interface ReportDataItem {
+  metric: string;
+  value: string;
+  mom: string;
+  yoy: string;
+  dimension: string;
+}
+
+/* ───── System ───── */
+export interface RoleItem {
+  id: string;
+  name: string;
+  description: string;
+  boundUsers: number;
+  status: string;
+  updatedAt: string;
+  permissions?: string[];
+}
+
+export interface LogItem {
+  id: string;
+  time: string;
+  actor: string;
+  module: string;
+  type: string;
+  objectId: string;
+  result: string;
+  sourceIp: string;
+  beforeValue?: string;
+  afterValue?: string;
+  remark?: string;
+}
+
+export interface ApiKeyItem {
+  id: string;
+  name: string;
+  system: string;
+  scope: string;
+  status: string;
+  expireAt: string;
+  lastUsedAt: string;
+  ipWhitelist?: string;
+  remark?: string;
+}
+
+/* ───── User ───── */
+export type UserRole = 'admin' | 'customer_service' | 'manager' | 'operator' | 'procurement' | 'warehouse';
+
+export interface UserProfile {
+  avatar: string;
+  name: string;
+  role: UserRole;
+  roleLabel: string;
+  workspace: string;
+}

+ 149 - 0
src/views/auth/LoginView.vue

@@ -0,0 +1,149 @@
+<template>
+  <div class="login-page">
+    <div class="login-shell">
+      <section class="login-hero glass-card">
+        <span class="page-hero__eyebrow">CrossBorder OS</span>
+        <h1>跨境运营中台登录</h1>
+        <p>先登录再进入商品、订单、供应链和履约模块。当前提供 4 套 mock 账号,方便你验证角色权限差异。</p>
+
+        <div class="login-accounts">
+          <div v-for="account in demoAccounts" :key="account.username" class="login-account" @click="fillAccount(account.username)">
+            <strong>{{ account.label }}</strong>
+            <span>{{ account.username }} / 123456</span>
+          </div>
+        </div>
+      </section>
+
+      <section class="login-form glass-card">
+        <div class="section-card__title">
+          <div>
+            <h3>欢迎回来</h3>
+            <p>请输入账号密码,系统会按角色展示菜单与页面权限。</p>
+          </div>
+        </div>
+
+        <el-form :model="form" label-position="top" @submit.prevent="submit">
+          <el-form-item label="用户名">
+            <el-input v-model="form.username" placeholder="admin / operator / procurement / warehouse" />
+          </el-form-item>
+          <el-form-item label="密码">
+            <el-input v-model="form.password" type="password" show-password placeholder="123456" />
+          </el-form-item>
+          <el-alert v-if="errorMessage" :title="errorMessage" type="error" :closable="false" style="margin-bottom: 16px" />
+          <el-button type="primary" :loading="authStore.loading" style="width: 100%" @click="submit">登录并进入系统</el-button>
+        </el-form>
+      </section>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { reactive, ref } from 'vue';
+import { useRoute, useRouter } from 'vue-router';
+import { ElMessage } from 'element-plus';
+import { useAuthStore } from '@/stores/auth';
+
+const router = useRouter();
+const route = useRoute();
+const authStore = useAuthStore();
+
+const demoAccounts = [
+  { label: '系统管理员', username: 'admin' },
+  { label: '运营负责人', username: 'operator' },
+  { label: '采购专员', username: 'procurement' },
+  { label: '仓配主管', username: 'warehouse' }
+];
+
+const form = reactive({
+  username: 'admin',
+  password: '123456'
+});
+
+const errorMessage = ref('');
+
+const fillAccount = (username: string) => {
+  form.username = username;
+  form.password = '123456';
+  errorMessage.value = '';
+};
+
+const submit = async () => {
+  errorMessage.value = '';
+  try {
+    await authStore.login(form.username, form.password);
+    ElMessage.success(`欢迎回来,${authStore.user?.name}`);
+    const redirect = typeof route.query.redirect === 'string' ? route.query.redirect : '/report/dashboard';
+    await router.push(redirect);
+  } catch (error) {
+    errorMessage.value = error instanceof Error ? error.message : '登录失败';
+  }
+};
+</script>
+
+<style scoped lang="scss">
+.login-page {
+  min-height: 100vh;
+  display: grid;
+  place-items: center;
+  padding: 28px;
+}
+
+.login-shell {
+  display: grid;
+  grid-template-columns: 1.2fr 0.9fr;
+  gap: 22px;
+  width: min(1120px, 100%);
+}
+
+.login-hero,
+.login-form {
+  padding: 28px;
+}
+
+.login-hero h1 {
+  margin: 0 0 12px;
+  font-size: 40px;
+  line-height: 1.1;
+}
+
+.login-hero p {
+  margin: 0;
+  color: var(--cb-text-soft);
+  line-height: 1.8;
+}
+
+.login-accounts {
+  display: grid;
+  gap: 12px;
+  margin-top: 28px;
+}
+
+.login-account {
+  padding: 16px 18px;
+  border: 1px solid var(--cb-border);
+  border-radius: 18px;
+  background: rgba(255, 255, 255, 0.68);
+  cursor: pointer;
+  transition: transform 0.2s ease, box-shadow 0.2s ease;
+}
+
+.login-account:hover {
+  transform: translateY(-2px);
+  box-shadow: var(--cb-shadow);
+}
+
+.login-account strong {
+  display: block;
+  margin-bottom: 6px;
+}
+
+.login-account span {
+  color: var(--cb-text-soft);
+}
+
+@media (max-width: 900px) {
+  .login-shell {
+    grid-template-columns: 1fr;
+  }
+}
+</style>

+ 50 - 0
src/views/channel/ChannelConfigView.vue

@@ -0,0 +1,50 @@
+<template>
+  <div class="app-page">
+    <section class="glass-card page-hero">
+      <div class="page-hero__meta">
+        <span class="page-hero__eyebrow">Adapter</span>
+        <h1>渠道接入配置与同步状态</h1>
+        <p>统一维护店铺授权、Webhook、同步状态和异常提示,是订单接入和商品发布前的基础能力页。</p>
+      </div>
+      <div class="chip-list">
+        <span class="chip">测试连接后启用</span>
+        <span class="chip">保留授权快照</span>
+      </div>
+    </section>
+
+    <section class="glass-card section-card">
+      <div class="table-toolbar">
+        <div class="chip-list">
+          <el-button type="primary">新增店铺授权</el-button>
+          <el-button>测试连接</el-button>
+          <el-button>查看同步日志</el-button>
+        </div>
+      </div>
+
+      <el-table :data="items" stripe style="width: 100%">
+        <el-table-column prop="channel" label="渠道" width="130" />
+        <el-table-column prop="shopName" label="店铺名称" min-width="220" />
+        <el-table-column prop="tokenStatus" label="授权状态" width="120" />
+        <el-table-column prop="syncStatus" label="同步状态" width="120" />
+        <el-table-column prop="lastSyncAt" label="最近同步时间" width="170" />
+        <el-table-column prop="errorMessage" label="异常提示" min-width="220" />
+      </el-table>
+    </section>
+
+    <SpecPageRenderer page-key="channel-config" />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { onMounted, ref } from 'vue';
+import { api } from '@/api/services';
+import SpecPageRenderer from '@/components/SpecPageRenderer.vue';
+import type { ChannelItem } from '@/types/page';
+
+const items = ref<ChannelItem[]>([]);
+
+onMounted(async () => {
+  const response = await api.getChannels();
+  items.value = response.items;
+});
+</script>

+ 72 - 0
src/views/dashboard/ReportDashboardView.vue

@@ -0,0 +1,72 @@
+<template>
+  <div class="app-page">
+    <section class="glass-card page-hero">
+      <div class="page-hero__meta">
+        <span class="page-hero__eyebrow">Executive Snapshot</span>
+        <h1>跨境业务一屏掌控</h1>
+        <p>围绕 GMV、异常订单、库存预警和发货时效做统一洞察,适合作为每日经营例会的第一屏。</p>
+      </div>
+      <div class="chip-list">
+        <span class="chip">PIM → OMS → SRM → 履约</span>
+        <span class="chip">Mock 数据联动</span>
+      </div>
+    </section>
+
+    <section class="stat-grid">
+      <article v-for="item in overview?.stats" :key="item.label" class="glass-card stat-card">
+        <div class="stat-card__label">{{ item.label }}</div>
+        <div class="stat-card__value">{{ item.value }}</div>
+        <div class="stat-card__trend">{{ item.trend }}</div>
+      </article>
+    </section>
+
+    <section class="page-grid page-grid--two">
+      <article class="glass-card section-card">
+        <div class="section-card__title">
+          <div>
+            <h3>今日重点预警</h3>
+            <p>帮助运营、采购、仓配团队快速对齐优先级。</p>
+          </div>
+        </div>
+        <el-timeline>
+          <el-timeline-item v-for="alert in overview?.alerts" :key="alert.title" :type="timelineType(alert.level)">
+            <strong>{{ alert.title }}</strong>
+            <div class="muted" style="margin-top: 6px">{{ alert.summary }}</div>
+          </el-timeline-item>
+        </el-timeline>
+      </article>
+
+      <article class="glass-card section-card">
+        <div class="section-card__title">
+          <div>
+            <h3>流程落地建议</h3>
+            <p>基于 PRD 把页面串成连续闭环。</p>
+          </div>
+        </div>
+        <ul class="tight-list">
+          <li v-for="tip in overview?.workflowTips" :key="tip">{{ tip }}</li>
+        </ul>
+      </article>
+    </section>
+
+    <SpecPageRenderer page-key="report-dashboard" />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { onMounted, ref } from 'vue';
+import { api, type DashboardOverviewResponse } from '@/api/services';
+import SpecPageRenderer from '@/components/SpecPageRenderer.vue';
+
+const overview = ref<DashboardOverviewResponse>();
+
+const timelineType = (level: string) => {
+  if (level === 'critical') return 'danger';
+  if (level === 'warning') return 'warning';
+  return 'primary';
+};
+
+onMounted(async () => {
+  overview.value = await api.getDashboardOverview();
+});
+</script>

+ 49 - 0
src/views/inventory/InventoryOverviewView.vue

@@ -0,0 +1,49 @@
+<template>
+  <div class="app-page">
+    <section class="glass-card page-hero">
+      <div class="page-hero__meta">
+        <span class="page-hero__eyebrow">Inventory</span>
+        <h1>库存总览与预警</h1>
+        <p>通过可用库存、锁定库存、在途库存和安全库存阈值来判断是否需要补货,并为订单分配提供依据。</p>
+      </div>
+      <div class="chip-list">
+        <span class="chip">低库存高亮</span>
+        <span class="chip">一键创建补货建议</span>
+      </div>
+    </section>
+
+    <section class="glass-card section-card">
+      <el-table :data="items" stripe style="width: 100%">
+        <el-table-column prop="sku" label="SKU" width="180" />
+        <el-table-column prop="productTitle" label="商品标题" min-width="250" />
+        <el-table-column prop="warehouse" label="仓库" width="110" />
+        <el-table-column prop="available" label="可用库存" width="110" />
+        <el-table-column prop="locked" label="锁定库存" width="110" />
+        <el-table-column prop="inbound" label="在途库存" width="110" />
+        <el-table-column prop="safeStock" label="安全库存" width="110" />
+        <el-table-column label="预警状态" width="140">
+          <template #default="{ row }">
+            <el-tag :type="row.warningStatus === '正常' ? 'success' : 'danger'">{{ row.warningStatus }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="lastChangeAt" label="最近变动时间" width="170" />
+      </el-table>
+    </section>
+
+    <SpecPageRenderer page-key="inventory-overview" />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { onMounted, ref } from 'vue';
+import { api } from '@/api/services';
+import SpecPageRenderer from '@/components/SpecPageRenderer.vue';
+import type { InventoryItem } from '@/types/page';
+
+const items = ref<InventoryItem[]>([]);
+
+onMounted(async () => {
+  const response = await api.getInventory();
+  items.value = response.items;
+});
+</script>

+ 44 - 0
src/views/inventory/ShippingWorkView.vue

@@ -0,0 +1,44 @@
+<template>
+  <div class="app-page">
+    <section class="glass-card page-hero">
+      <div class="page-hero__meta">
+        <span class="page-hero__eyebrow">Fulfillment</span>
+        <h1>发货作业与回传状态</h1>
+        <p>承接待发货单据、物流录入、回传重试和面单打印,是仓配角色的日常工作台。</p>
+      </div>
+      <div class="chip-list">
+        <span class="chip">运单号必填</span>
+        <span class="chip">回传失败支持重试</span>
+      </div>
+    </section>
+
+    <section class="glass-card section-card">
+      <el-table :data="items" stripe style="width: 100%">
+        <el-table-column prop="shipmentNo" label="发货单号" width="170" />
+        <el-table-column prop="orderNo" label="订单号" width="180" />
+        <el-table-column prop="warehouse" label="仓库" width="110" />
+        <el-table-column prop="packageCount" label="包裹数" width="90" />
+        <el-table-column prop="carrier" label="物流公司" width="120" />
+        <el-table-column prop="shippingStatus" label="发货状态" width="110" />
+        <el-table-column prop="returnStatus" label="回传状态" width="110" />
+        <el-table-column prop="createdAt" label="创建时间" width="170" />
+      </el-table>
+    </section>
+
+    <SpecPageRenderer page-key="shipping-work" />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { onMounted, ref } from 'vue';
+import { api } from '@/api/services';
+import SpecPageRenderer from '@/components/SpecPageRenderer.vue';
+import type { ShippingItem } from '@/types/page';
+
+const items = ref<ShippingItem[]>([]);
+
+onMounted(async () => {
+  const response = await api.getShippingOrders();
+  items.value = response.items;
+});
+</script>

+ 255 - 0
src/views/order/OrderDetailView.vue

@@ -0,0 +1,255 @@
+<template>
+  <div class="app-page">
+    <!-- 订单摘要 -->
+    <section class="glass-card section-card">
+      <div class="table-toolbar">
+        <h2 style="margin:0">订单详情 — {{ order.orderNo }}</h2>
+        <div class="chip-list">
+          <el-button @click="$router.back()">返回列表</el-button>
+        </div>
+      </div>
+      <div class="stat-grid" style="margin-top:16px">
+        <article class="stat-card">
+          <div class="stat-card__label">订单状态</div>
+          <div class="stat-card__value" style="font-size:22px">{{ statusLabel(order.orderStatus) }}</div>
+        </article>
+        <article class="stat-card">
+          <div class="stat-card__label">支付金额</div>
+          <div class="stat-card__value" style="font-size:22px">{{ order.amount }}</div>
+        </article>
+        <article class="stat-card">
+          <div class="stat-card__label">发货状态</div>
+          <div class="stat-card__value" style="font-size:22px">{{ order.shippingStatus }}</div>
+        </article>
+        <article class="stat-card">
+          <div class="stat-card__label">异常标签</div>
+          <div class="stat-card__value" style="font-size:22px">
+            <el-tag :type="order.exceptionTag === '正常' ? 'success' : 'warning'">{{ order.exceptionTag }}</el-tag>
+          </div>
+        </article>
+      </div>
+    </section>
+
+    <section class="page-grid page-grid--two">
+      <!-- 收货 + 支付信息 -->
+      <article class="glass-card section-card">
+        <h3 style="margin:0 0 16px">收货 & 支付信息</h3>
+        <el-descriptions :column="1" border>
+          <el-descriptions-item label="收货人">{{ order.receiver }}</el-descriptions-item>
+          <el-descriptions-item label="电话">{{ order.phone }}</el-descriptions-item>
+          <el-descriptions-item label="地址">{{ order.address }}</el-descriptions-item>
+          <el-descriptions-item label="支付方式">{{ order.payMethod }}</el-descriptions-item>
+          <el-descriptions-item label="支付时间">{{ order.payTime }}</el-descriptions-item>
+          <el-descriptions-item label="渠道">{{ order.channel }}</el-descriptions-item>
+          <el-descriptions-item label="仓库">{{ order.warehouse }}</el-descriptions-item>
+        </el-descriptions>
+      </article>
+
+      <!-- 操作区 -->
+      <article class="glass-card section-card">
+        <h3 style="margin:0 0 16px">操作</h3>
+        <div class="chip-list" style="margin-bottom:16px">
+          <el-button type="primary" @click="assignDrawer = true" :disabled="order.orderStatus === 'cancelled'">提交仓库</el-button>
+          <el-button @click="lockInventory" :disabled="order.orderStatus === 'cancelled'">锁定库存</el-button>
+          <el-button @click="splitDialog = true" :disabled="order.orderStatus === 'cancelled'">拆单</el-button>
+          <el-button @click="mergeDialog = true" :disabled="order.orderStatus === 'cancelled'">合单</el-button>
+          <el-button type="danger" plain @click="cancelDialog = true" :disabled="order.orderStatus === 'cancelled'">取消订单</el-button>
+        </div>
+        <el-form label-position="top">
+          <el-form-item label="订单备注">
+            <el-input v-model="order.remark" type="textarea" :rows="3" placeholder="客服备注" />
+          </el-form-item>
+        </el-form>
+      </article>
+    </section>
+
+    <!-- 商品明细 -->
+    <section class="glass-card section-card">
+      <h3 style="margin:0 0 16px">商品明细</h3>
+      <el-table :data="lineItems" stripe>
+        <el-table-column prop="sku" label="SKU" width="180" />
+        <el-table-column prop="title" label="商品" min-width="250" />
+        <el-table-column prop="qty" label="数量" width="80" />
+        <el-table-column prop="unitPrice" label="单价" width="100" />
+        <el-table-column prop="subtotal" label="小计" width="100" />
+      </el-table>
+    </section>
+
+    <!-- 状态时间线 -->
+    <section class="glass-card section-card">
+      <h3 style="margin:0 0 16px">订单轨迹</h3>
+      <el-timeline>
+        <el-timeline-item v-for="item in timeline" :key="item.time" :timestamp="item.time" :type="item.type">
+          <strong>{{ item.title }}</strong>
+          <div style="color:var(--cb-text-soft);margin-top:4px">{{ item.summary }}</div>
+        </el-timeline-item>
+      </el-timeline>
+    </section>
+
+    <!-- 取消弹窗 -->
+    <el-dialog v-model="cancelDialog" title="取消订单" width="520px">
+      <el-alert title="取消订单需二次确认,操作不可撤销" type="warning" :closable="false" style="margin-bottom:16px" />
+      <el-form label-position="top">
+        <el-form-item label="取消原因" required>
+          <el-select v-model="cancelReason" style="width:100%">
+            <el-option label="买家申请取消" value="买家申请取消" />
+            <el-option label="地址异常无法发货" value="地址异常无法发货" />
+            <el-option label="库存不足" value="库存不足" />
+            <el-option label="其他原因" value="其他原因" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="补充说明">
+          <el-input v-model="cancelRemark" type="textarea" :rows="3" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="cancelDialog = false">关闭</el-button>
+        <el-button type="danger" @click="confirmCancel">确认取消</el-button>
+      </template>
+    </el-dialog>
+
+    <!-- 拆单弹窗 -->
+    <el-dialog v-model="splitDialog" title="拆单处理" width="620px">
+      <el-table :data="lineItems">
+        <el-table-column prop="sku" label="SKU" min-width="180" />
+        <el-table-column prop="title" label="商品" min-width="200" />
+        <el-table-column prop="qty" label="原数量" width="90" />
+        <el-table-column label="拆单数量" width="140">
+          <template #default="{ row }">
+            <el-input-number v-model="row.splitQty" :min="0" :max="row.qty" size="small" />
+          </template>
+        </el-table-column>
+      </el-table>
+      <template #footer>
+        <el-button @click="splitDialog = false">取消</el-button>
+        <el-button type="primary" @click="confirmSplit">生成拆单方案</el-button>
+      </template>
+    </el-dialog>
+
+    <!-- 合单弹窗 -->
+    <el-dialog v-model="mergeDialog" title="合单处理" width="480px">
+      <el-form label-position="top">
+        <el-form-item label="目标订单号">
+          <el-input v-model="mergeTarget" placeholder="请输入待合并的订单号" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="mergeDialog = false">取消</el-button>
+        <el-button type="primary" @click="confirmMerge">确认合单</el-button>
+      </template>
+    </el-dialog>
+
+    <!-- 分配仓库抽屉 -->
+    <el-drawer v-model="assignDrawer" title="分配仓库" size="400px">
+      <el-form label-position="top">
+        <el-form-item label="选择仓库">
+          <el-select v-model="order.warehouse" style="width:100%">
+            <el-option label="深圳仓" value="深圳仓" />
+            <el-option label="义乌仓" value="义乌仓" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="仓库备注">
+          <el-input v-model="warehouseRemark" type="textarea" :rows="4" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="assignDrawer = false">取消</el-button>
+        <el-button type="primary" @click="submitWarehouse">确认提交</el-button>
+      </template>
+    </el-drawer>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { onMounted, reactive, ref } from 'vue';
+import { useRoute } from 'vue-router';
+import { ElMessage, ElMessageBox } from 'element-plus';
+import { api } from '@/api/services';
+import type { OrderItem } from '@/types/page';
+
+const route = useRoute();
+const cancelDialog = ref(false);
+const splitDialog = ref(false);
+const mergeDialog = ref(false);
+const assignDrawer = ref(false);
+const cancelReason = ref('');
+const cancelRemark = ref('');
+const mergeTarget = ref('');
+const warehouseRemark = ref('');
+
+const order = reactive<Partial<OrderItem> & { address?: string; payMethod?: string; payTime?: string; phone?: string; receiver?: string; remark?: string }>({
+  id: '', orderNo: '', channel: '', buyer: '', amount: '', paymentStatus: '', orderStatus: '', shippingStatus: '', warehouse: '', exceptionTag: '', createdAt: '',
+  receiver: '', phone: '', address: '', payMethod: '', payTime: '', remark: ''
+});
+
+const lineItems = ref<{ sku: string; title: string; qty: number; splitQty: number; unitPrice: string; subtotal: string }[]>([]);
+
+const timeline = ref<{ time: string; title: string; summary: string; type: string }[]>([]);
+
+const statusLabel = (s: string) => {
+  const map: Record<string, string> = { created: '待支付', paid: '已支付', allocated: '已分配', shipped: '已发货', delivered: '已签收', completed: '已完成', cancelled: '已取消', refunded: '已退款' };
+  return map[s] || s;
+};
+
+const loadData = async () => {
+  const id = route.query.id as string;
+  if (!id) return;
+  const res = await api.getOrder(id);
+  Object.assign(order, res);
+
+  // mock detail data
+  order.receiver = 'Olivia Zhang';
+  order.phone = '+1 213 **** 4401';
+  order.address = '1234 Main St, Apt 5B, Los Angeles, CA 90001, US';
+  order.payMethod = '信用卡';
+  order.payTime = '2026-04-19 20:43';
+  order.remark = '地址楼栋不清晰,需要客服复核后再提交仓库。';
+
+  lineItems.value = [
+    { sku: 'SKU-LUGG-20-BLK', title: 'TravelFlex Expandable Carry-On / Black', qty: 1, splitQty: 0, unitPrice: '$89.00', subtotal: '$89.00' },
+    { sku: 'SKU-TAG-SET-GRY', title: 'Travel Tag Set / Gray', qty: 2, splitQty: 0, unitPrice: '$19.50', subtotal: '$39.00' }
+  ];
+
+  timeline.value = [
+    { time: '2026-04-19 21:16', title: '地址校验告警', summary: '地址楼栋信息不完整,标记为"地址需复核"', type: 'warning' },
+    { time: '2026-04-19 21:10', title: '库存锁定完成', summary: '系统自动锁定 SKU-LUGG-20-BLK × 1, SKU-TAG-SET-GRY × 2', type: 'success' },
+    { time: '2026-04-19 20:43', title: '支付确认', summary: '状态 created → paid,金额 $128.00', type: 'primary' },
+    { time: '2026-04-19 20:42', title: '订单接入 OMS', summary: `来自 ${order.channel} 的订单推送`, type: 'primary' }
+  ];
+};
+
+const lockInventory = () => { ElMessage.success('库存已锁定'); };
+
+const confirmCancel = async () => {
+  if (!cancelReason.value) { ElMessage.warning('请选择取消原因'); return; }
+  await ElMessageBox.confirm('确认取消此订单?操作不可撤销。', '二次确认', { type: 'warning' });
+  await api.updateOrder(order.id!, { orderStatus: 'cancelled' } as Partial<OrderItem>);
+  order.orderStatus = 'cancelled';
+  timeline.value.unshift({ time: '刚刚', title: '订单已取消', summary: cancelReason.value, type: 'danger' });
+  cancelDialog.value = false;
+  ElMessage.success('订单已取消');
+};
+
+const confirmSplit = () => {
+  const hasSplit = lineItems.value.some(i => i.splitQty > 0);
+  if (!hasSplit) { ElMessage.warning('请至少为一个 SKU 设置拆单数量'); return; }
+  splitDialog.value = false;
+  timeline.value.unshift({ time: '刚刚', title: '拆单方案已生成', summary: '待确认后生成新发货单', type: 'primary' });
+  ElMessage.success('拆单方案已生成');
+};
+
+const confirmMerge = () => {
+  if (!mergeTarget.value) { ElMessage.warning('请输入目标订单号'); return; }
+  mergeDialog.value = false;
+  ElMessage.success(`已提交与 ${mergeTarget.value} 的合单申请`);
+};
+
+const submitWarehouse = async () => {
+  await api.updateOrder(order.id!, { warehouse: order.warehouse } as Partial<OrderItem>);
+  assignDrawer.value = false;
+  timeline.value.unshift({ time: '刚刚', title: '已提交仓库', summary: `分配至 ${order.warehouse}`, type: 'success' });
+  ElMessage.success('已提交仓库');
+};
+
+onMounted(loadData);
+</script>

+ 157 - 0
src/views/order/OrderListView.vue

@@ -0,0 +1,157 @@
+<template>
+  <div class="app-page">
+    <!-- 筛选区 -->
+    <section class="glass-card section-card">
+      <el-form :model="filters" inline class="filter-form">
+        <el-form-item label="订单号">
+          <el-input v-model="filters.orderNo" placeholder="OMS-xxx 或渠道订单号" clearable style="width:200px" @keyup.enter="loadData" />
+        </el-form-item>
+        <el-form-item label="渠道">
+          <el-select v-model="filters.channel" placeholder="全部渠道" clearable style="width:140px">
+            <el-option label="Shopify US" value="Shopify US" />
+            <el-option label="Shopify JP" value="Shopify JP" />
+            <el-option label="TikTok UK" value="TikTok UK" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="订单状态">
+          <el-select v-model="filters.orderStatus" placeholder="全部" clearable style="width:130px">
+            <el-option v-for="s in orderStatuses" :key="s" :label="s" :value="s" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="异常标签">
+          <el-select v-model="filters.exceptionTag" placeholder="全部" clearable style="width:130px">
+            <el-option label="正常" value="正常" />
+            <el-option label="地址需复核" value="地址需复核" />
+            <el-option label="高频下单" value="高频下单" />
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" @click="loadData">查询</el-button>
+          <el-button @click="resetFilters">重置</el-button>
+        </el-form-item>
+      </el-form>
+    </section>
+
+    <!-- 工具栏 -->
+    <section class="glass-card section-card" style="padding:12px 24px">
+      <div class="table-toolbar" style="margin-bottom:0">
+        <div class="chip-list">
+          <el-button type="primary" @click="refreshSync">刷新同步</el-button>
+          <el-button @click="doExport">导出订单</el-button>
+          <el-button v-if="selected.length" @click="batchMark">批量标记 ({{ selected.length }})</el-button>
+        </div>
+        <el-select v-model="savedView" placeholder="常用视图" clearable style="width:150px">
+          <el-option label="待处理订单" value="pending" />
+          <el-option label="异常订单" value="exception" />
+          <el-option label="今日订单" value="today" />
+        </el-select>
+      </div>
+    </section>
+
+    <!-- 订单列表 -->
+    <section class="glass-card section-card">
+      <el-table :data="filteredItems" stripe style="width:100%" @selection-change="onSelection" :row-class-name="rowClass">
+        <el-table-column type="selection" width="45" />
+        <el-table-column prop="orderNo" label="订单号" width="190" />
+        <el-table-column prop="channel" label="渠道" width="120" />
+        <el-table-column prop="buyer" label="买家" width="130" />
+        <el-table-column prop="amount" label="金额" width="90" />
+        <el-table-column prop="itemCount" label="件数" width="60" />
+        <el-table-column prop="paymentStatus" label="支付" width="80">
+          <template #default="{ row }">
+            <el-tag :type="row.paymentStatus === '已支付' ? 'success' : 'info'" size="small">{{ row.paymentStatus }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="orderStatus" label="订单状态" width="100">
+          <template #default="{ row }">
+            <el-tag :type="statusType(row.orderStatus)" size="small">{{ statusLabel(row.orderStatus) }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="shippingStatus" label="发货状态" width="110" />
+        <el-table-column prop="warehouse" label="仓库" width="90" />
+        <el-table-column label="异常" width="110">
+          <template #default="{ row }">
+            <el-tag v-if="row.exceptionTag !== '正常'" type="warning" size="small">{{ row.exceptionTag }}</el-tag>
+            <span v-else style="color:var(--cb-text-soft)">正常</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="createdAt" label="下单时间" width="160" />
+        <el-table-column label="操作" width="180" fixed="right">
+          <template #default="{ row }">
+            <el-button link type="primary" @click="$router.push(`/order/detail?id=${row.id}`)">详情</el-button>
+            <el-button link type="primary" @click="$router.push('/order/after-sale')">售后</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+    </section>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { computed, onMounted, ref } from 'vue';
+import { ElMessage } from 'element-plus';
+import { api } from '@/api/services';
+import type { OrderItem } from '@/types/page';
+
+const items = ref<OrderItem[]>([]);
+const selected = ref<OrderItem[]>([]);
+const savedView = ref('');
+const orderStatuses = ['created', 'paid', 'allocated', 'shipped', 'delivered', 'completed', 'cancelled', 'refunded'];
+
+const filters = ref({ orderNo: '', channel: '', orderStatus: '', exceptionTag: '' });
+
+const filteredItems = computed(() => {
+  return items.value.filter((item) => {
+    if (filters.value.orderNo && !item.orderNo.includes(filters.value.orderNo) && !(item.channelOrderNo || '').includes(filters.value.orderNo)) return false;
+    if (filters.value.channel && item.channel !== filters.value.channel) return false;
+    if (filters.value.orderStatus && item.orderStatus !== filters.value.orderStatus) return false;
+    if (filters.value.exceptionTag && item.exceptionTag !== filters.value.exceptionTag) return false;
+    return true;
+  });
+});
+
+const statusType = (s: string) => {
+  const map: Record<string, string> = { created: 'info', paid: '', allocated: 'warning', shipped: '', delivered: 'success', completed: 'success', cancelled: 'danger', refunded: 'danger' };
+  return map[s] || '';
+};
+
+const statusLabel = (s: string) => {
+  const map: Record<string, string> = { created: '待支付', paid: '已支付', allocated: '已分配', shipped: '已发货', delivered: '已签收', completed: '已完成', cancelled: '已取消', refunded: '已退款' };
+  return map[s] || s;
+};
+
+const rowClass = ({ row }: { row: OrderItem }) => {
+  return row.exceptionTag !== '正常' ? 'exception-row' : '';
+};
+
+const loadData = async () => {
+  const res = await api.getOrders();
+  items.value = res.items;
+};
+
+const resetFilters = () => {
+  filters.value = { orderNo: '', channel: '', orderStatus: '', exceptionTag: '' };
+};
+
+const onSelection = (rows: OrderItem[]) => { selected.value = rows; };
+
+const refreshSync = () => {
+  ElMessage.success('同步完成:新增 2 单,更新 1 单');
+  loadData();
+};
+
+const doExport = () => {
+  ElMessage.info('导出已开始,完成后将自动下载');
+};
+
+const batchMark = () => {
+  ElMessage.success(`已标记 ${selected.value.length} 个订单`);
+};
+
+onMounted(loadData);
+</script>
+
+<style scoped>
+.filter-form :deep(.el-form-item) { margin-bottom: 0; }
+:deep(.exception-row) { background-color: rgba(217, 119, 6, 0.06) !important; }
+</style>

+ 330 - 0
src/views/product/ProductEditorView.vue

@@ -0,0 +1,330 @@
+<template>
+  <div class="app-page">
+    <section class="glass-card section-card">
+      <el-steps :active="step" finish-status="success" simple>
+        <el-step title="基础信息" />
+        <el-step title="SKU 规格" />
+        <el-step title="媒体素材" />
+        <el-step title="多语言" />
+        <el-step title="预览提交" />
+      </el-steps>
+    </section>
+
+    <!-- Step 0: 基础信息 -->
+    <section class="page-grid page-grid--two" v-if="step === 0">
+      <article class="glass-card section-card">
+        <h3 style="margin:0 0 16px">基础信息</h3>
+        <el-form label-position="top" :model="form" :rules="baseRules" ref="baseFormRef">
+          <el-form-item label="商品标题" prop="title">
+            <el-input v-model="form.title" placeholder="请输入商品标题" />
+          </el-form-item>
+          <el-form-item label="商品副标题">
+            <el-input v-model="form.subtitle" placeholder="可选" />
+          </el-form-item>
+          <el-form-item label="类目" prop="category">
+            <el-select v-model="form.category" placeholder="选择类目" style="width:100%" @change="onCategoryChange">
+              <el-option label="Luggage" value="Luggage" />
+              <el-option label="Bags" value="Bags" />
+              <el-option label="Sports" value="Sports" />
+              <el-option label="Outdoor" value="Outdoor" />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="品牌" prop="brand">
+            <el-input v-model="form.brand" placeholder="品牌名称" />
+          </el-form-item>
+          <el-form-item label="商品标签">
+            <el-select v-model="form.tags" multiple filterable allow-create placeholder="输入标签" style="width:100%">
+              <el-option label="轻量" value="轻量" />
+              <el-option label="可扩展" value="可扩展" />
+              <el-option label="登机友好" value="登机友好" />
+              <el-option label="旅行收纳" value="旅行收纳" />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="商品简介">
+            <el-input v-model="form.description" type="textarea" :rows="4" placeholder="商品卖点描述" />
+          </el-form-item>
+        </el-form>
+      </article>
+      <article class="glass-card section-card">
+        <h3 style="margin:0 0 16px">属性模板</h3>
+        <p style="color:var(--cb-text-soft);margin:0 0 16px">{{ form.category }} 类目自动带出的属性字段</p>
+        <div class="drawer-preview">
+          <div class="drawer-preview__item" v-for="attr in categoryAttrs" :key="attr">
+            <strong>{{ attr }}</strong>
+            <el-input :placeholder="'请输入' + attr" size="small" style="margin-top:8px" />
+          </div>
+        </div>
+        <div v-if="!form.category" style="color:var(--cb-text-soft)">请先选择类目</div>
+      </article>
+    </section>
+
+    <!-- Step 1: SKU 规格 -->
+    <section class="page-grid page-grid--two" v-else-if="step === 1">
+      <article class="glass-card section-card">
+        <h3 style="margin:0 0 16px">规格配置</h3>
+        <p style="color:var(--cb-text-soft);margin:0 0 16px">输入规格值后点击更新,自动生成 SKU 组合</p>
+        <el-form label-position="top">
+          <el-form-item label="颜色(逗号分隔)">
+            <el-input v-model="colorInput" placeholder="Black, Sand, Olive" />
+          </el-form-item>
+          <el-form-item label="尺寸(逗号分隔)">
+            <el-input v-model="sizeInput" placeholder="20 inch, 24 inch" />
+          </el-form-item>
+          <el-button type="primary" @click="applySpecs">更新 SKU 组合</el-button>
+        </el-form>
+      </article>
+      <article class="glass-card section-card">
+        <h3 style="margin:0 0 16px">SKU 组合表 <el-tag size="small" style="margin-left:8px">{{ skuRows.length }} 个</el-tag></h3>
+        <el-table :data="skuRows" stripe size="small" style="margin-top:12px">
+          <el-table-column prop="sku" label="SKU 编码" min-width="180" />
+          <el-table-column prop="color" label="颜色" width="100" />
+          <el-table-column prop="size" label="尺寸" width="100" />
+          <el-table-column label="条码" width="140">
+            <template #default="{ row }"><el-input v-model="row.barcode" size="small" placeholder="条码" /></template>
+          </el-table-column>
+          <el-table-column label="成本价" width="100">
+            <template #default="{ row }"><el-input v-model="row.costPrice" size="small" placeholder="$0" /></template>
+          </el-table-column>
+          <el-table-column label="售价" width="100">
+            <template #default="{ row }"><el-input v-model="row.suggestPrice" size="small" placeholder="$0" /></template>
+          </el-table-column>
+          <el-table-column label="重量(kg)" width="100">
+            <template #default="{ row }"><el-input v-model="row.weight" size="small" placeholder="0" /></template>
+          </el-table-column>
+          <el-table-column label="操作" width="60" fixed="right">
+            <template #default="{ $index }">
+              <el-button link type="danger" size="small" @click="skuRows.splice($index, 1)">删除</el-button>
+            </template>
+          </el-table-column>
+        </el-table>
+      </article>
+    </section>
+
+    <!-- Step 2: 媒体素材 -->
+    <section class="page-grid page-grid--two" v-else-if="step === 2">
+      <article class="glass-card section-card">
+        <h3 style="margin:0 0 16px">媒体素材</h3>
+        <el-form label-position="top">
+          <el-form-item label="主图(必填)" required>
+            <el-upload action="#" :auto-upload="false" list-type="picture-card" :limit="1" :on-change="(f: File) => form.mainImage = f.name">
+              <el-icon><Plus /></el-icon>
+            </el-upload>
+          </el-form-item>
+          <el-form-item label="附图">
+            <el-upload action="#" :auto-upload="false" list-type="picture-card" :limit="5" multiple>
+              <el-icon><Plus /></el-icon>
+            </el-upload>
+          </el-form-item>
+          <el-form-item label="详情图">
+            <el-input v-model="detailImagesText" type="textarea" :rows="4" placeholder="每行一个图片 URL" />
+          </el-form-item>
+          <el-form-item label="视频 URL">
+            <el-input v-model="form.videoUrl" placeholder="可选" />
+          </el-form-item>
+        </el-form>
+      </article>
+      <article class="glass-card section-card">
+        <h3 style="margin:0 0 16px">素材预览</h3>
+        <div class="drawer-preview">
+          <div class="drawer-preview__item"><strong>主图</strong>{{ form.mainImage || '未上传' }}</div>
+          <div class="drawer-preview__item"><strong>详情图</strong>{{ detailImages.length }} 张</div>
+          <div class="drawer-preview__item"><strong>视频</strong>{{ form.videoUrl || '暂未配置' }}</div>
+        </div>
+      </article>
+    </section>
+
+    <!-- Step 3: 多语言 -->
+    <section class="page-grid page-grid--two" v-else-if="step === 3">
+      <article class="glass-card section-card">
+        <h3 style="margin:0 0 16px">多语言配置</h3>
+        <el-tabs v-model="activeLocale">
+          <el-tab-pane label="English" name="en">
+            <el-form label-position="top">
+              <el-form-item label="英文标题"><el-input v-model="form.locales.en.title" /></el-form-item>
+              <el-form-item label="英文卖点"><el-input v-model="form.locales.en.highlight" type="textarea" :rows="3" /></el-form-item>
+              <el-form-item label="英文详情"><el-input v-model="form.locales.en.description" type="textarea" :rows="3" /></el-form-item>
+            </el-form>
+          </el-tab-pane>
+          <el-tab-pane label="Japanese" name="jp">
+            <el-form label-position="top">
+              <el-form-item label="日文标题"><el-input v-model="form.locales.jp.title" /></el-form-item>
+              <el-form-item label="日文卖点"><el-input v-model="form.locales.jp.highlight" type="textarea" :rows="3" /></el-form-item>
+              <el-form-item label="日文详情"><el-input v-model="form.locales.jp.description" type="textarea" :rows="3" /></el-form-item>
+            </el-form>
+          </el-tab-pane>
+        </el-tabs>
+      </article>
+      <article class="glass-card section-card">
+        <h3 style="margin:0 0 16px">翻译完整度</h3>
+        <div class="stat-grid" style="grid-template-columns:repeat(2,1fr)">
+          <article class="stat-card">
+            <div class="stat-card__label">中文</div>
+            <div class="stat-card__value" style="color:var(--cb-primary)">100%</div>
+          </article>
+          <article class="stat-card">
+            <div class="stat-card__label">English</div>
+            <div class="stat-card__value" :style="{ color: form.locales.en.title ? 'var(--cb-primary)' : 'var(--cb-danger)' }">{{ form.locales.en.title ? '100%' : '40%' }}</div>
+          </article>
+          <article class="stat-card">
+            <div class="stat-card__label">日本語</div>
+            <div class="stat-card__value" :style="{ color: form.locales.jp.title ? 'var(--cb-primary)' : 'var(--cb-danger)' }">{{ form.locales.jp.title ? '80%' : '20%' }}</div>
+          </article>
+          <article class="stat-card">
+            <div class="stat-card__label">可发布性</div>
+            <div class="stat-card__value" :style="{ color: readyForPublish ? 'var(--cb-primary)' : 'var(--cb-accent)' }">{{ readyForPublish ? '可进入映射' : '待完善' }}</div>
+          </article>
+        </div>
+      </article>
+    </section>
+
+    <!-- Step 4: 预览提交 -->
+    <section class="page-grid page-grid--two" v-else>
+      <article class="glass-card section-card">
+        <h3 style="margin:0 0 16px">发布前预览</h3>
+        <div class="drawer-preview">
+          <div class="drawer-preview__item"><strong>标题</strong>{{ form.title }}</div>
+          <div class="drawer-preview__item"><strong>副标题</strong>{{ form.subtitle || '-' }}</div>
+          <div class="drawer-preview__item"><strong>类目 / 品牌</strong>{{ form.category }} / {{ form.brand }}</div>
+          <div class="drawer-preview__item"><strong>标签</strong>{{ form.tags.join(', ') || '-' }}</div>
+          <div class="drawer-preview__item"><strong>SKU 数量</strong>{{ skuRows.length }}</div>
+          <div class="drawer-preview__item"><strong>主图</strong>{{ form.mainImage || '未上传' }}</div>
+          <div class="drawer-preview__item"><strong>详情图</strong>{{ detailImages.length }} 张</div>
+          <div class="drawer-preview__item"><strong>EN 标题</strong>{{ form.locales.en.title || '未填写' }}</div>
+          <div class="drawer-preview__item"><strong>JP 标题</strong>{{ form.locales.jp.title || '未填写' }}</div>
+        </div>
+      </article>
+      <article class="glass-card section-card">
+        <h3 style="margin:0 0 16px">提交操作</h3>
+        <div class="chip-list" style="flex-direction:column;align-items:flex-start">
+          <el-button type="primary" size="large" @click="saveDraft" style="width:200px">保存草稿</el-button>
+          <el-button size="large" :disabled="!readyForPublish" @click="submitForValidation" style="width:200px">提交校验</el-button>
+          <el-button size="large" @click="$router.push('/product/mapping')" style="width:200px">进入渠道映射</el-button>
+          <el-button size="large" @click="step = 0" style="width:200px">返回修改</el-button>
+        </div>
+      </article>
+    </section>
+
+    <!-- 底部导航 -->
+    <section class="glass-card section-card" style="padding:12px 24px">
+      <div class="table-toolbar" style="margin-bottom:0">
+        <div class="muted">当前步骤:{{ step + 1 }} / 5</div>
+        <div class="chip-list">
+          <el-button :disabled="step === 0" @click="step--">上一步</el-button>
+          <el-button type="primary" :disabled="step === 4" @click="nextStep">下一步</el-button>
+        </div>
+      </div>
+    </section>
+  </div>
+</template>
+
+<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, type FormInstance, type FormRules } from 'element-plus';
+import { api } from '@/api/services';
+import type { ProductItem } from '@/types/page';
+
+const route = useRoute();
+const step = ref(0);
+const activeLocale = ref('en');
+const colorInput = ref('Black, Sand');
+const sizeInput = ref('20 inch, 24 inch');
+const detailImagesText = ref('https://img.mock/detail-1.jpg\nhttps://img.mock/detail-2.jpg\nhttps://img.mock/detail-3.jpg');
+const baseFormRef = ref<FormInstance>();
+
+const isEdit = computed(() => !!route.query.id);
+
+const form = reactive({
+  title: '',
+  subtitle: '',
+  category: '',
+  brand: '',
+  tags: [] as string[],
+  description: '',
+  mainImage: '',
+  videoUrl: '',
+  locales: {
+    en: { title: '', highlight: '', description: '' },
+    jp: { title: '', highlight: '', description: '' }
+  }
+});
+
+const baseRules: FormRules = {
+  title: [{ required: true, message: '请输入商品标题', trigger: 'blur' }],
+  category: [{ required: true, message: '请选择类目', trigger: 'change' }],
+  brand: [{ required: true, message: '请输入品牌', trigger: 'blur' }]
+};
+
+const categoryAttrs = computed(() => {
+  const map: Record<string, string[]> = {
+    Luggage: ['材质', '尺寸', '重量', '箱体类型', '适用场景'],
+    Bags: ['材质', '容量', '风格', '适用性别', '闭合方式'],
+    Sports: ['材质', '功能', '适用运动', '尺寸'],
+    Outdoor: ['材质', '重量', '折叠长度', '最大承重']
+  };
+  return map[form.category] || [];
+});
+
+const skuRows = ref<{ sku: string; color: string; size: string; barcode: string; costPrice: string; suggestPrice: string; weight: string }[]>([]);
+
+const detailImages = computed(() => detailImagesText.value.split('\n').map(s => s.trim()).filter(Boolean));
+
+const readyForPublish = computed(() =>
+  Boolean(form.title && form.mainImage && form.locales.en.title && skuRows.value.length)
+);
+
+const onCategoryChange = () => {
+  /* 类目切换后清空属性模板相关 */
+};
+
+const applySpecs = () => {
+  const colors = colorInput.value.split(',').map(s => s.trim()).filter(Boolean);
+  const sizes = sizeInput.value.split(',').map(s => s.trim()).filter(Boolean);
+  skuRows.value = colors.flatMap((color, ci) =>
+    sizes.map((size, si) => ({
+      sku: `SKU-${form.category?.slice(0, 3).toUpperCase() || 'NEW'}-${size.replace(/\s/g, '')}-${color.toUpperCase().slice(0, 3)}-${ci * sizes.length + si + 1}`,
+      color, size,
+      barcode: '', costPrice: '', suggestPrice: '', weight: ''
+    }))
+  );
+  ElMessage.success(`已生成 ${skuRows.value.length} 个 SKU 组合`);
+};
+
+const nextStep = async () => {
+  if (step.value === 0 && baseFormRef.value) {
+    const valid = await baseFormRef.value.validate().catch(() => false);
+    if (!valid) return;
+  }
+  step.value++;
+};
+
+const saveDraft = async () => {
+  if (isEdit.value) {
+    await api.updateProduct(route.query.id as string, { title: form.title, status: '草稿' } as Partial<ProductItem>);
+  } else {
+    await api.createProduct({ title: form.title, category: form.category, brand: form.brand, status: '草稿' } as Partial<ProductItem>);
+  }
+  ElMessage.success('草稿已保存');
+};
+
+const submitForValidation = () => {
+  if (!readyForPublish.value) {
+    ElMessage.warning('请先补齐主图、英文标题和 SKU 组合');
+    return;
+  }
+  ElMessage.success('校验通过,可进入渠道映射页发布');
+};
+
+onMounted(async () => {
+  if (isEdit.value) {
+    const res = await api.getProducts();
+    const product = res.items.find(p => p.id === route.query.id);
+    if (product) {
+      form.title = product.title;
+      form.category = product.category;
+      form.brand = product.brand;
+    }
+  }
+});
+</script>

+ 207 - 0
src/views/product/ProductListView.vue

@@ -0,0 +1,207 @@
+<template>
+  <div class="app-page">
+    <!-- 筛选区 -->
+    <section class="glass-card section-card">
+      <el-form :model="filters" inline class="filter-form">
+        <el-form-item label="关键词">
+          <el-input v-model="filters.keyword" placeholder="商品标题/SPU/SKU" clearable style="width:180px" @keyup.enter="loadData" />
+        </el-form-item>
+        <el-form-item label="类目">
+          <el-select v-model="filters.category" placeholder="全部类目" clearable style="width:140px">
+            <el-option v-for="c in categories" :key="c" :label="c" :value="c" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="渠道">
+          <el-select v-model="filters.channel" placeholder="全部渠道" clearable style="width:140px">
+            <el-option label="Shopify" value="Shopify" />
+            <el-option label="TikTok Shop" value="TikTok Shop" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="上架状态">
+          <el-select v-model="filters.status" placeholder="全部状态" clearable style="width:130px">
+            <el-option label="已上架" value="已上架" />
+            <el-option label="草稿" value="草稿" />
+            <el-option label="已下架" value="已下架" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="品牌">
+          <el-select v-model="filters.brand" placeholder="全部品牌" clearable style="width:130px">
+            <el-option v-for="b in brands" :key="b" :label="b" :value="b" />
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" @click="loadData">查询</el-button>
+          <el-button @click="resetFilters">重置</el-button>
+        </el-form-item>
+      </el-form>
+    </section>
+
+    <!-- 工具栏 -->
+    <section class="glass-card section-card" style="padding:12px 24px">
+      <div class="table-toolbar" style="margin-bottom:0">
+        <div class="chip-list">
+          <el-button type="primary" @click="$router.push('/product/editor')">新建商品</el-button>
+          <el-button @click="showImport = true">批量导入</el-button>
+          <el-button :disabled="!selected.length" @click="batchAction('上架')">批量上架</el-button>
+          <el-button :disabled="!selected.length" @click="batchAction('下架')">批量下架</el-button>
+        </div>
+        <el-button @click="loadData">刷新</el-button>
+      </div>
+    </section>
+
+    <!-- 列表 -->
+    <section class="glass-card section-card">
+      <el-table :data="filteredItems" stripe style="width:100%" @selection-change="onSelection">
+        <el-table-column type="selection" width="45" />
+        <el-table-column label="主图" width="72">
+          <template #default="{ row }">
+            <el-image :src="row.image" style="width:48px;height:48px;border-radius:8px" fit="cover">
+              <template #error><div class="image-placeholder">IMG</div></template>
+            </el-image>
+          </template>
+        </el-table-column>
+        <el-table-column prop="title" label="商品标题" min-width="220">
+          <template #default="{ row }">
+            <div>{{ row.title }}</div>
+            <div style="color:var(--cb-text-soft);font-size:12px">{{ row.spu }}</div>
+          </template>
+        </el-table-column>
+        <el-table-column prop="skuCount" label="SKU 数" width="80" />
+        <el-table-column prop="category" label="类目" width="100" />
+        <el-table-column prop="channelStatus" label="渠道发布状态" min-width="180">
+          <template #default="{ row }">
+            <span :class="{ 'text-warning': row.channelStatus.includes('待') }">{{ row.channelStatus }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="inventory" label="总库存" width="90">
+          <template #default="{ row }">
+            <span :class="{ 'text-danger': row.inventory < 50 }">{{ row.inventory }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="priceRange" label="价格区间" width="120" />
+        <el-table-column prop="owner" label="负责人" width="100" />
+        <el-table-column prop="status" label="上架状态" width="90">
+          <template #default="{ row }">
+            <el-tag :type="statusType(row.status)" size="small">{{ row.status }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="updatedAt" label="更新时间" width="160" />
+        <el-table-column label="操作" width="240" fixed="right">
+          <template #default="{ row }">
+            <el-button link type="primary" @click="$router.push(`/product/editor?id=${row.id}`)">编辑</el-button>
+            <el-button link type="primary" @click="$router.push('/product/mapping')">映射</el-button>
+            <el-button link type="primary" @click="toggleStatus(row)">{{ row.status === '已上架' ? '下架' : '上架' }}</el-button>
+            <el-button link type="danger" @click="handleDelete(row)">删除</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+    </section>
+
+    <!-- 批量导入弹窗 -->
+    <el-dialog v-model="showImport" title="批量导入商品" width="500px">
+      <el-upload drag action="#" :auto-upload="false" accept=".csv" :on-change="onFileChange">
+        <el-icon style="font-size:40px;color:var(--cb-primary)"><UploadFilled /></el-icon>
+        <div style="margin-top:8px">将 CSV 文件拖到此处,或 <em>点击上传</em></div>
+      </el-upload>
+      <div style="margin-top:12px;color:var(--cb-text-soft);font-size:13px">
+        <el-button link type="primary">下载导入模板</el-button>
+      </div>
+      <template #footer>
+        <el-button @click="showImport = false">取消</el-button>
+        <el-button type="primary" @click="doImport">确认导入</el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { computed, onMounted, ref } from 'vue';
+import { UploadFilled } from '@element-plus/icons-vue';
+import { ElMessage, ElMessageBox } from 'element-plus';
+import { api } from '@/api/services';
+import type { ProductItem } from '@/types/page';
+
+const items = ref<ProductItem[]>([]);
+const selected = ref<ProductItem[]>([]);
+const showImport = ref(false);
+
+const filters = ref({
+  keyword: '',
+  category: '',
+  channel: '',
+  status: '',
+  brand: ''
+});
+
+const categories = ['Luggage', 'Bags', 'Sports', 'Outdoor'];
+const brands = ['NomadPeak', 'UrbanTrail', 'AeroDry'];
+
+const filteredItems = computed(() => {
+  return items.value.filter((item) => {
+    if (filters.value.keyword && !item.title.includes(filters.value.keyword) && !item.spu.includes(filters.value.keyword)) return false;
+    if (filters.value.category && item.category !== filters.value.category) return false;
+    if (filters.value.status && item.status !== filters.value.status) return false;
+    if (filters.value.brand && item.brand !== filters.value.brand) return false;
+    if (filters.value.channel && !item.channelStatus.includes(filters.value.channel)) return false;
+    return true;
+  });
+});
+
+const statusType = (s: string) => {
+  if (s === '已上架') return 'success';
+  if (s === '草稿') return 'info';
+  return 'warning';
+};
+
+const loadData = async () => {
+  const res = await api.getProducts();
+  items.value = res.items;
+};
+
+const resetFilters = () => {
+  filters.value = { keyword: '', category: '', channel: '', status: '', brand: '' };
+};
+
+const onSelection = (rows: ProductItem[]) => {
+  selected.value = rows;
+};
+
+const batchAction = async (action: string) => {
+  await ElMessageBox.confirm(`确认将选中的 ${selected.value.length} 个商品${action}?`, '批量操作');
+  for (const item of selected.value) {
+    await api.updateProduct(item.id, { status: action === '上架' ? '已上架' : '已下架' });
+  }
+  ElMessage.success(`${action}成功`);
+  loadData();
+};
+
+const toggleStatus = async (row: ProductItem) => {
+  const newStatus = row.status === '已上架' ? '已下架' : '已上架';
+  await ElMessageBox.confirm(`确认${newStatus === '已上架' ? '上架' : '下架'}「${row.title}」?`, '操作确认');
+  await api.updateProduct(row.id, { status: newStatus });
+  ElMessage.success('操作成功');
+  loadData();
+};
+
+const handleDelete = async (row: ProductItem) => {
+  await ElMessageBox.confirm(`确认删除「${row.title}」?此操作不可恢复。`, '删除确认', { type: 'warning' });
+  await api.deleteProduct(row.id);
+  ElMessage.success('已删除');
+  loadData();
+};
+
+const onFileChange = () => { /* placeholder */ };
+const doImport = () => {
+  ElMessage.info('导入功能演示:文件解析中...');
+  showImport.value = false;
+};
+
+onMounted(loadData);
+</script>
+
+<style scoped>
+.filter-form :deep(.el-form-item) { margin-bottom: 0; }
+.image-placeholder { width: 48px; height: 48px; border-radius: 8px; background: var(--cb-primary-soft); display: flex; align-items: center; justify-content: center; color: var(--cb-primary); font-size: 12px; font-weight: 700; }
+.text-danger { color: var(--cb-danger); font-weight: 600; }
+.text-warning { color: var(--cb-accent); }
+</style>

+ 221 - 0
src/views/product/ProductMappingView.vue

@@ -0,0 +1,221 @@
+<template>
+  <div class="app-page">
+    <!-- 筛选区 -->
+    <section class="glass-card section-card">
+      <el-form :model="filters" inline class="filter-form">
+        <el-form-item label="商品标题">
+          <el-input v-model="filters.keyword" placeholder="商品标题/SKU" clearable style="width:180px" @keyup.enter="loadData" />
+        </el-form-item>
+        <el-form-item label="渠道">
+          <el-select v-model="filters.channel" placeholder="全部渠道" clearable style="width:140px">
+            <el-option label="Shopify" value="Shopify" />
+            <el-option label="TikTok Shop" value="TikTok Shop" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="映射状态">
+          <el-select v-model="filters.mappingStatus" placeholder="全部" clearable style="width:130px">
+            <el-option label="已映射" value="已映射" />
+            <el-option label="未映射" value="未映射" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="校验状态">
+          <el-select v-model="filters.validateStatus" placeholder="全部" clearable style="width:130px">
+            <el-option label="通过" value="通过" />
+            <el-option label="失败" value="失败" />
+            <el-option label="未校验" value="未校验" />
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" @click="loadData">查询</el-button>
+          <el-button @click="resetFilters">重置</el-button>
+        </el-form-item>
+      </el-form>
+    </section>
+
+    <!-- 工具栏 -->
+    <section class="glass-card section-card" style="padding:12px 24px">
+      <div class="table-toolbar" style="margin-bottom:0">
+        <div class="chip-list">
+          <el-button type="primary" @click="openDialog()">新增映射</el-button>
+          <el-button @click="autoMap">自动映射</el-button>
+        </div>
+        <el-button @click="loadData">刷新</el-button>
+      </div>
+    </section>
+
+    <!-- 映射列表 -->
+    <section class="glass-card section-card">
+      <el-table :data="filteredItems" stripe style="width:100%">
+        <el-table-column prop="internalSku" label="内部 SKU" width="160" />
+        <el-table-column prop="productTitle" label="商品" min-width="200" />
+        <el-table-column prop="channel" label="渠道" width="120" />
+        <el-table-column prop="shopName" label="店铺" width="140" />
+        <el-table-column prop="channelSku" label="渠道 SKU" width="160">
+          <template #default="{ row }">
+            {{ row.channelSku || '-' }}
+          </template>
+        </el-table-column>
+        <el-table-column prop="channelCategory" label="渠道类目" min-width="180">
+          <template #default="{ row }">
+            {{ row.channelCategory || '-' }}
+          </template>
+        </el-table-column>
+        <el-table-column prop="mappingStatus" label="映射状态" width="90">
+          <template #default="{ row }">
+            <el-tag :type="row.mappingStatus === '已映射' ? 'success' : 'info'" size="small">{{ row.mappingStatus }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="validateStatus" label="校验状态" width="90">
+          <template #default="{ row }">
+            <el-tag :type="row.validateStatus === '通过' ? 'success' : row.validateStatus === '失败' ? 'danger' : 'info'" size="small">{{ row.validateStatus }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="lastSyncAt" label="最近同步" width="160" />
+        <el-table-column label="操作" width="220" fixed="right">
+          <template #default="{ row }">
+            <el-button link type="primary" @click="openDialog(row)">编辑</el-button>
+            <el-button link type="primary" @click="validateMapping(row)">校验</el-button>
+            <el-button link type="primary" :disabled="row.mappingStatus !== '已映射' || row.validateStatus === '失败'" @click="publishMapping(row)">发布</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+    </section>
+
+    <!-- 新增/编辑映射弹窗 -->
+    <el-dialog v-model="dialogVisible" :title="editingItem ? '编辑映射' : '新增映射'" width="600px" destroy-on-close>
+      <el-form :model="formData" label-width="100px">
+        <el-form-item label="目标渠道" required>
+          <el-select v-model="formData.channel" placeholder="选择渠道" style="width:100%" @change="onChannelChange">
+            <el-option label="Shopify" value="Shopify" />
+            <el-option label="TikTok Shop" value="TikTok Shop" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="目标店铺" required>
+          <el-select v-model="formData.shopName" placeholder="选择店铺" style="width:100%">
+            <el-option v-for="shop in shopOptions" :key="shop" :label="shop" :value="shop" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="渠道类目" required>
+          <el-select v-model="formData.channelCategory" placeholder="选择类目" style="width:100%">
+            <el-option v-for="cat in channelCategories" :key="cat" :label="cat" :value="cat" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="内部 SKU" required>
+          <el-input v-model="formData.internalSku" placeholder="如 SKU-LUGG-20-BLK" />
+        </el-form-item>
+        <el-form-item label="渠道标题">
+          <el-input v-model="formData.channelTitle" placeholder="可不同于内部标题" />
+        </el-form-item>
+        <el-form-item label="渠道描述">
+          <el-input v-model="formData.channelDescription" type="textarea" :rows="3" />
+        </el-form-item>
+        <el-form-item label="渠道价格">
+          <el-input v-model="formData.channelPrice" placeholder="$0.00" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="dialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="saveMapping">保存</el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { computed, onMounted, reactive, ref } from 'vue';
+import { ElMessage } from 'element-plus';
+import { api } from '@/api/services';
+import type { MappingItem } from '@/types/page';
+
+const items = ref<MappingItem[]>([]);
+const dialogVisible = ref(false);
+const editingItem = ref<MappingItem | null>(null);
+
+const filters = ref({ keyword: '', channel: '', mappingStatus: '', validateStatus: '' });
+const formData = reactive<{ channel: string; channelCategory: string; channelDescription: string; channelPrice: string; channelTitle: string; internalSku: string; shopName: string }>({
+  channel: '', channelCategory: '', channelDescription: '', channelPrice: '', channelTitle: '', internalSku: '', shopName: ''
+});
+
+const shopOptions = computed(() => {
+  if (formData.channel === 'Shopify') return ['NomadPeak US', 'UrbanTrail JP'];
+  if (formData.channel === 'TikTok Shop') return ['AeroDry UK', 'NomadPeak Global'];
+  return [];
+});
+
+const channelCategories = computed(() => {
+  if (formData.channel === 'Shopify') return ['Luggage > Carry-On', 'Bags > Sling Bags', 'Sports > Accessories'];
+  if (formData.channel === 'TikTok Shop') return ['Luggage > Suitcases', 'Bags > Crossbody', 'Sports > Towels'];
+  return [];
+});
+
+const filteredItems = computed(() => {
+  return items.value.filter((item) => {
+    if (filters.value.keyword && !item.productTitle.includes(filters.value.keyword) && !item.internalSku.includes(filters.value.keyword)) return false;
+    if (filters.value.channel && item.channel !== filters.value.channel) return false;
+    if (filters.value.mappingStatus && item.mappingStatus !== filters.value.mappingStatus) return false;
+    if (filters.value.validateStatus && item.validateStatus !== filters.value.validateStatus) return false;
+    return true;
+  });
+});
+
+const loadData = async () => {
+  const res = await api.getMappings();
+  items.value = res.items;
+};
+
+const resetFilters = () => {
+  filters.value = { keyword: '', channel: '', mappingStatus: '', validateStatus: '' };
+};
+
+const onChannelChange = () => {
+  formData.shopName = '';
+  formData.channelCategory = '';
+};
+
+const openDialog = (item?: MappingItem) => {
+  editingItem.value = item || null;
+  if (item) {
+    Object.assign(formData, { channel: item.channel, shopName: item.shopName, channelCategory: item.channelCategory, internalSku: item.internalSku, channelTitle: '', channelDescription: '', channelPrice: '' });
+  } else {
+    Object.assign(formData, { channel: '', shopName: '', channelCategory: '', internalSku: '', channelTitle: '', channelDescription: '', channelPrice: '' });
+  }
+  dialogVisible.value = true;
+};
+
+const saveMapping = async () => {
+  if (editingItem.value) {
+    await api.updateMapping(editingItem.value.id, { ...formData, mappingStatus: '已映射', validateStatus: '未校验', lastSyncAt: new Date().toISOString().slice(0, 16).replace('T', ' ') } as Partial<MappingItem>);
+    ElMessage.success('映射已更新');
+  } else {
+    await api.createMapping({ ...formData, mappingStatus: '已映射', validateStatus: '未校验', lastSyncAt: '-', lastValidateResult: '-', channelSku: '', productTitle: formData.internalSku } as Partial<MappingItem>);
+    ElMessage.success('映射已创建');
+  }
+  dialogVisible.value = false;
+  loadData();
+};
+
+const autoMap = () => {
+  ElMessage.info('自动映射完成,请检查结果并修正');
+  loadData();
+};
+
+const validateMapping = async (row: MappingItem) => {
+  const passed = Math.random() > 0.3;
+  const result = passed ? '校验通过' : '类目属性"材质"未填写';
+  await api.updateMapping(row.id, { validateStatus: passed ? '通过' : '失败', lastValidateResult: result } as Partial<MappingItem>);
+  ElMessage[passed ? 'success' : 'warning'](result);
+  loadData();
+};
+
+const publishMapping = async (row: MappingItem) => {
+  await api.updateMapping(row.id, { lastSyncAt: new Date().toISOString().slice(0, 16).replace('T', ' ') } as Partial<MappingItem>);
+  ElMessage.success('发布成功');
+  loadData();
+};
+
+onMounted(loadData);
+</script>
+
+<style scoped>
+.filter-form :deep(.el-form-item) { margin-bottom: 0; }
+</style>

+ 218 - 0
src/views/product/ProductPricingView.vue

@@ -0,0 +1,218 @@
+<template>
+  <div class="app-page">
+    <!-- 筛选区 -->
+    <section class="glass-card section-card">
+      <el-form :model="filters" inline class="filter-form">
+        <el-form-item label="SKU">
+          <el-input v-model="filters.sku" placeholder="SKU 编号" clearable style="width:180px" @keyup.enter="loadData" />
+        </el-form-item>
+        <el-form-item label="渠道">
+          <el-select v-model="filters.channel" placeholder="全部渠道" clearable style="width:130px">
+            <el-option label="Shopify" value="Shopify" />
+            <el-option label="TikTok Shop" value="TikTok Shop" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="币种">
+          <el-select v-model="filters.currency" placeholder="全部" clearable style="width:110px">
+            <el-option label="USD" value="USD" />
+            <el-option label="EUR" value="EUR" />
+            <el-option label="JPY" value="JPY" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="规则状态">
+          <el-select v-model="filters.status" placeholder="全部" clearable style="width:110px">
+            <el-option label="生效中" value="生效中" />
+            <el-option label="待生效" value="待生效" />
+            <el-option label="已停用" value="已停用" />
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" @click="loadData">查询</el-button>
+          <el-button @click="resetFilters">重置</el-button>
+        </el-form-item>
+      </el-form>
+    </section>
+
+    <!-- 工具栏 -->
+    <section class="glass-card section-card" style="padding:12px 24px">
+      <div class="table-toolbar" style="margin-bottom:0">
+        <div class="chip-list">
+          <el-button type="primary" @click="openDialog()">新增价格规则</el-button>
+          <el-button @click="batchPriceDialog = true">批量调价</el-button>
+        </div>
+        <el-button @click="loadData">刷新</el-button>
+      </div>
+    </section>
+
+    <!-- 列表 -->
+    <section class="glass-card section-card">
+      <el-table :data="filteredItems" stripe style="width:100%">
+        <el-table-column prop="sku" label="SKU" width="160" />
+        <el-table-column prop="productTitle" label="商品" min-width="220" />
+        <el-table-column prop="currency" label="币种" width="70" />
+        <el-table-column prop="basePrice" label="基准售价" width="100" />
+        <el-table-column prop="channelPrice" label="渠道售价" width="100" />
+        <el-table-column prop="discountPrice" label="折扣价" width="90">
+          <template #default="{ row }">
+            <span :class="{ 'text-danger': row.discountPrice !== '-' }">{{ row.discountPrice }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="effectiveTime" label="生效时间" width="110" />
+        <el-table-column prop="safeStock" label="安全库存" width="90" />
+        <el-table-column prop="status" label="规则状态" width="90">
+          <template #default="{ row }">
+            <el-tag :type="row.status === '生效中' ? 'success' : row.status === '待生效' ? 'warning' : 'info'" size="small">{{ row.status }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="modifiedBy" label="修改人" width="100" />
+        <el-table-column prop="updatedAt" label="更新时间" width="160" />
+        <el-table-column label="操作" width="150" fixed="right">
+          <template #default="{ row }">
+            <el-button link type="primary" @click="openDialog(row)">编辑</el-button>
+            <el-button link :type="row.status === '已停用' ? 'success' : 'warning'" @click="toggleRuleStatus(row)">
+              {{ row.status === '已停用' ? '启用' : '停用' }}
+            </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+    </section>
+
+    <!-- 新增/编辑弹窗 -->
+    <el-dialog v-model="dialogVisible" :title="editingItem ? '编辑价格规则' : '新增价格规则'" width="550px" destroy-on-close>
+      <el-form :model="formData" label-width="110px">
+        <el-form-item label="SKU" required>
+          <el-input v-model="formData.sku" placeholder="如 SKU-LUGG-20-BLK" />
+        </el-form-item>
+        <el-form-item label="币种" required>
+          <el-select v-model="formData.currency" style="width:100%">
+            <el-option label="USD" value="USD" />
+            <el-option label="EUR" value="EUR" />
+            <el-option label="JPY" value="JPY" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="基础售价" required>
+          <el-input v-model="formData.basePrice" placeholder="$0.00" />
+        </el-form-item>
+        <el-form-item label="渠道加减价">
+          <el-input v-model="formData.channelAdjustRule" placeholder="如 +10% 或 +$5" />
+        </el-form-item>
+        <el-form-item label="促销价">
+          <el-input v-model="formData.discountPrice" placeholder="留空表示无折扣" />
+        </el-form-item>
+        <el-form-item label="安全库存阈值">
+          <el-input-number v-model="formData.safeStockThreshold" :min="0" style="width:100%" />
+        </el-form-item>
+        <el-form-item label="预警接收人">
+          <el-input v-model="formData.warningRecipients" placeholder="多人用逗号分隔" />
+        </el-form-item>
+        <el-form-item label="生效时间">
+          <el-date-picker v-model="formData.effectiveTime" type="date" placeholder="选择日期" style="width:100%" value-format="YYYY-MM-DD" />
+        </el-form-item>
+        <el-form-item label="失效时间">
+          <el-date-picker v-model="formData.expireTime" type="date" placeholder="留空表示长期有效" style="width:100%" value-format="YYYY-MM-DD" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="dialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="saveRule">保存</el-button>
+      </template>
+    </el-dialog>
+
+    <!-- 批量调价弹窗 -->
+    <el-dialog v-model="batchPriceDialog" title="批量调价" width="450px">
+      <el-form label-width="100px">
+        <el-form-item label="调价方式">
+          <el-radio-group>
+            <el-radio value="percent">按百分比</el-radio>
+            <el-radio value="fixed">按固定金额</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="调整值">
+          <el-input placeholder="如 +10% 或 -$5" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="batchPriceDialog = false">取消</el-button>
+        <el-button type="primary" @click="doBatchPrice">确认调价</el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { computed, onMounted, reactive, ref } from 'vue';
+import { ElMessage, ElMessageBox } from 'element-plus';
+import { api } from '@/api/services';
+import type { PricingRuleItem } from '@/types/page';
+
+const items = ref<PricingRuleItem[]>([]);
+const dialogVisible = ref(false);
+const batchPriceDialog = ref(false);
+const editingItem = ref<PricingRuleItem | null>(null);
+
+const filters = ref({ sku: '', channel: '', currency: '', status: '' });
+const formData = reactive({
+  sku: '', currency: 'USD', basePrice: '', channelAdjustRule: '', discountPrice: '',
+  safeStockThreshold: 20, warningRecipients: '', effectiveTime: '', expireTime: ''
+});
+
+const filteredItems = computed(() => {
+  return items.value.filter((item) => {
+    if (filters.value.sku && !item.sku.includes(filters.value.sku)) return false;
+    if (filters.value.currency && item.currency !== filters.value.currency) return false;
+    if (filters.value.status && item.status !== filters.value.status) return false;
+    return true;
+  });
+});
+
+const loadData = async () => {
+  const res = await api.getPricingRules();
+  items.value = res.items;
+};
+
+const resetFilters = () => {
+  filters.value = { sku: '', channel: '', currency: '', status: '' };
+};
+
+const openDialog = (item?: PricingRuleItem) => {
+  editingItem.value = item || null;
+  if (item) {
+    Object.assign(formData, { sku: item.sku, currency: item.currency, basePrice: item.basePrice, channelAdjustRule: '', discountPrice: item.discountPrice === '-' ? '' : item.discountPrice, safeStockThreshold: item.safeStock, warningRecipients: '', effectiveTime: item.effectiveTime, expireTime: '' });
+  } else {
+    Object.assign(formData, { sku: '', currency: 'USD', basePrice: '', channelAdjustRule: '', discountPrice: '', safeStockThreshold: 20, warningRecipients: '', effectiveTime: '', expireTime: '' });
+  }
+  dialogVisible.value = true;
+};
+
+const saveRule = async () => {
+  if (editingItem.value) {
+    await api.updatePricingRule(editingItem.value.id, { ...formData, discountPrice: formData.discountPrice || '-' } as Partial<PricingRuleItem>);
+    ElMessage.success('规则已更新');
+  } else {
+    await api.createPricingRule({ ...formData, productTitle: formData.sku, channelPrice: formData.basePrice, discountPrice: formData.discountPrice || '-', status: '生效中', modifiedBy: '当前用户' } as Partial<PricingRuleItem>);
+    ElMessage.success('规则已创建');
+  }
+  dialogVisible.value = false;
+  loadData();
+};
+
+const toggleRuleStatus = async (row: PricingRuleItem) => {
+  const newStatus = row.status === '已停用' ? '生效中' : '已停用';
+  await ElMessageBox.confirm(`确认${newStatus === '生效中' ? '启用' : '停用'}该规则?`);
+  await api.updatePricingRule(row.id, { status: newStatus } as Partial<PricingRuleItem>);
+  ElMessage.success('操作成功');
+  loadData();
+};
+
+const doBatchPrice = () => {
+  ElMessage.info('批量调价已提交');
+  batchPriceDialog.value = false;
+};
+
+onMounted(loadData);
+</script>
+
+<style scoped>
+.filter-form :deep(.el-form-item) { margin-bottom: 0; }
+.text-danger { color: var(--cb-danger); font-weight: 600; }
+</style>

+ 57 - 0
src/views/report/ReportCenterView.vue

@@ -0,0 +1,57 @@
+<template>
+  <div class="app-page">
+    <section class="glass-card page-hero">
+      <div class="page-hero__meta">
+        <span class="page-hero__eyebrow">BI Center</span>
+        <h1>报表中心</h1>
+        <p>当前把报表目录、筛选条件和导出动作先放进统一框架,适合后续接真实图表和异步导出能力。</p>
+      </div>
+      <div class="chip-list">
+        <span class="chip">支持异步导出</span>
+        <span class="chip">可保存常用视图</span>
+      </div>
+    </section>
+
+    <section class="glass-card section-card">
+      <div class="table-toolbar">
+        <el-form inline>
+          <el-form-item label="报表类型">
+            <el-select model-value="渠道销售日报" style="width: 170px">
+              <el-option label="渠道销售日报" value="渠道销售日报" />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="时间范围">
+            <el-date-picker type="daterange" start-placeholder="开始日期" end-placeholder="结束日期" />
+          </el-form-item>
+        </el-form>
+        <div class="chip-list">
+          <el-button type="primary">生成报表</el-button>
+          <el-button>导出 Excel</el-button>
+        </div>
+      </div>
+
+      <el-table :data="items" stripe style="width: 100%">
+        <el-table-column prop="reportType" label="报表类型" min-width="200" />
+        <el-table-column prop="dimensions" label="维度" min-width="220" />
+        <el-table-column prop="owner" label="使用角色" width="160" />
+        <el-table-column prop="updatedAt" label="最近更新时间" width="170" />
+      </el-table>
+    </section>
+
+    <SpecPageRenderer page-key="report-center" />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { onMounted, ref } from 'vue';
+import { api } from '@/api/services';
+import SpecPageRenderer from '@/components/SpecPageRenderer.vue';
+import type { ReportItem } from '@/types/page';
+
+const items = ref<ReportItem[]>([]);
+
+onMounted(async () => {
+  const response = await api.getReports();
+  items.value = response.items;
+});
+</script>

+ 28 - 0
src/views/shared/ForbiddenView.vue

@@ -0,0 +1,28 @@
+<template>
+  <div class="app-page">
+    <section class="glass-card page-hero">
+      <div class="page-hero__meta">
+        <span class="page-hero__eyebrow">Permission Required</span>
+        <h1>当前角色没有访问这个页面的权限</h1>
+        <p>你可以返回首页,或者切换登录账号验证不同角色下的导航和页面可见范围。</p>
+      </div>
+      <div class="chip-list">
+        <el-button type="primary" @click="router.push('/report/dashboard')">返回首页</el-button>
+        <el-button @click="switchAccount">切换账号</el-button>
+      </div>
+    </section>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { useRouter } from 'vue-router';
+import { useAuthStore } from '@/stores/auth';
+
+const router = useRouter();
+const authStore = useAuthStore();
+
+const switchAccount = () => {
+  authStore.logout();
+  router.push('/login');
+};
+</script>

+ 12 - 0
src/views/shared/SpecOnlyPage.vue

@@ -0,0 +1,12 @@
+<template>
+  <SpecPageRenderer :page-key="pageKey" />
+</template>
+
+<script setup lang="ts">
+import { computed } from 'vue';
+import { useRoute } from 'vue-router';
+import SpecPageRenderer from '@/components/SpecPageRenderer.vue';
+
+const route = useRoute();
+const pageKey = computed(() => String(route.meta.pageKey || ''));
+</script>

+ 58 - 0
src/views/supplier/PurchaseOrderView.vue

@@ -0,0 +1,58 @@
+<template>
+  <div class="app-page">
+    <section class="glass-card page-hero">
+      <div class="page-hero__meta">
+        <span class="page-hero__eyebrow">Purchase Flow</span>
+        <h1>采购单管理与到货确认</h1>
+        <p>从低库存预警跳入创建采购单,再到确认到货更新库存,这页承担补货闭环的核心任务。</p>
+      </div>
+      <div class="chip-list">
+        <span class="chip">支持带出默认供货参数</span>
+        <span class="chip">到货后自动回写库存</span>
+      </div>
+    </section>
+
+    <section class="glass-card section-card">
+      <div class="table-toolbar">
+        <el-form inline>
+          <el-form-item label="采购状态">
+            <el-select model-value="全部状态" style="width: 140px">
+              <el-option label="全部状态" value="全部状态" />
+            </el-select>
+          </el-form-item>
+        </el-form>
+        <div class="chip-list">
+          <el-button type="primary">创建采购单</el-button>
+          <el-button>确认到货</el-button>
+        </div>
+      </div>
+
+      <el-table :data="items" stripe style="width: 100%">
+        <el-table-column prop="poNo" label="采购单号" width="170" />
+        <el-table-column prop="supplier" label="供应商" min-width="240" />
+        <el-table-column prop="skuCount" label="SKU 数" width="90" />
+        <el-table-column prop="amount" label="采购总额" width="120" />
+        <el-table-column prop="expectedDate" label="预计交期" width="120" />
+        <el-table-column prop="arrivalProgress" label="到货进度" min-width="150" />
+        <el-table-column prop="status" label="采购状态" width="130" />
+        <el-table-column prop="creator" label="创建人" width="120" />
+      </el-table>
+    </section>
+
+    <SpecPageRenderer page-key="purchase-orders" />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { onMounted, ref } from 'vue';
+import { api } from '@/api/services';
+import SpecPageRenderer from '@/components/SpecPageRenderer.vue';
+import type { PurchaseOrderItem } from '@/types/page';
+
+const items = ref<PurchaseOrderItem[]>([]);
+
+onMounted(async () => {
+  const response = await api.getPurchaseOrders();
+  items.value = response.items;
+});
+</script>

+ 57 - 0
src/views/supplier/SupplierListView.vue

@@ -0,0 +1,57 @@
+<template>
+  <div class="app-page">
+    <section class="glass-card page-hero">
+      <div class="page-hero__meta">
+        <span class="page-hero__eyebrow">SRM</span>
+        <h1>供应商管理与档案维护</h1>
+        <p>供应商页是采购和补货链路的入口,建议与采购单、供货能力配置页保持互跳。</p>
+      </div>
+      <div class="chip-list">
+        <span class="chip">支持合同版本管理</span>
+        <span class="chip">停用前校验在途采购</span>
+      </div>
+    </section>
+
+    <section class="glass-card section-card">
+      <div class="table-toolbar">
+        <el-form inline>
+          <el-form-item label="状态">
+            <el-select model-value="全部供应商" style="width: 150px">
+              <el-option label="全部供应商" value="全部供应商" />
+            </el-select>
+          </el-form-item>
+        </el-form>
+        <div class="chip-list">
+          <el-button type="primary">新建供应商</el-button>
+          <el-button>导出</el-button>
+        </div>
+      </div>
+
+      <el-table :data="items" stripe style="width: 100%">
+        <el-table-column prop="name" label="供应商名称" min-width="260" />
+        <el-table-column prop="contact" label="联系人" width="180" />
+        <el-table-column prop="settlement" label="结算方式" width="140" />
+        <el-table-column prop="rating" label="评级" width="90" />
+        <el-table-column prop="status" label="合作状态" width="110" />
+        <el-table-column prop="relatedSkuCount" label="关联 SKU 数" width="120" />
+        <el-table-column prop="updatedAt" label="更新时间" width="170" />
+      </el-table>
+    </section>
+
+    <SpecPageRenderer page-key="supplier-list" />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { onMounted, ref } from 'vue';
+import { api } from '@/api/services';
+import SpecPageRenderer from '@/components/SpecPageRenderer.vue';
+import type { SupplierItem } from '@/types/page';
+
+const items = ref<SupplierItem[]>([]);
+
+onMounted(async () => {
+  const response = await api.getSuppliers();
+  items.value = response.items;
+});
+</script>

+ 49 - 0
src/views/system/ApiKeyView.vue

@@ -0,0 +1,49 @@
+<template>
+  <div class="app-page">
+    <section class="glass-card page-hero">
+      <div class="page-hero__meta">
+        <span class="page-hero__eyebrow">Credential</span>
+        <h1>API Key 管理</h1>
+        <p>用于统一管理对外接口凭据、轮换策略和过期时间,并保留安全操作提示。</p>
+      </div>
+      <div class="chip-list">
+        <span class="chip">明文只展示一次</span>
+        <span class="chip">轮换支持过渡时间</span>
+      </div>
+    </section>
+
+    <section class="glass-card section-card">
+      <div class="table-toolbar">
+        <div class="chip-list">
+          <el-button type="primary">创建 Key</el-button>
+          <el-button>轮换 Key</el-button>
+        </div>
+      </div>
+
+      <el-table :data="items" stripe style="width: 100%">
+        <el-table-column prop="name" label="Key 名称" width="180" />
+        <el-table-column prop="system" label="所属系统" width="160" />
+        <el-table-column prop="scope" label="权限范围" min-width="220" />
+        <el-table-column prop="status" label="状态" width="110" />
+        <el-table-column prop="expireAt" label="过期时间" width="130" />
+        <el-table-column prop="lastUsedAt" label="最近调用时间" width="170" />
+      </el-table>
+    </section>
+
+    <SpecPageRenderer page-key="api-key" />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { onMounted, ref } from 'vue';
+import { api } from '@/api/services';
+import SpecPageRenderer from '@/components/SpecPageRenderer.vue';
+import type { ApiKeyItem } from '@/types/page';
+
+const items = ref<ApiKeyItem[]>([]);
+
+onMounted(async () => {
+  const response = await api.getApiKeys();
+  items.value = response.items;
+});
+</script>

+ 43 - 0
src/views/system/OperationLogView.vue

@@ -0,0 +1,43 @@
+<template>
+  <div class="app-page">
+    <section class="glass-card page-hero">
+      <div class="page-hero__meta">
+        <span class="page-hero__eyebrow">Audit</span>
+        <h1>操作日志</h1>
+        <p>聚焦对象 ID、操作动作和结果状态,方便排查问题和审计关键操作。</p>
+      </div>
+      <div class="chip-list">
+        <span class="chip">支持对象 ID 快速检索</span>
+        <span class="chip">高风险动作标红</span>
+      </div>
+    </section>
+
+    <section class="glass-card section-card">
+      <el-table :data="items" stripe style="width: 100%">
+        <el-table-column prop="time" label="操作时间" width="180" />
+        <el-table-column prop="actor" label="操作人" width="180" />
+        <el-table-column prop="module" label="模块" width="130" />
+        <el-table-column prop="type" label="操作动作" width="120" />
+        <el-table-column prop="objectId" label="对象 ID" width="180" />
+        <el-table-column prop="result" label="结果状态" width="100" />
+        <el-table-column prop="sourceIp" label="来源 IP" width="130" />
+      </el-table>
+    </section>
+
+    <SpecPageRenderer page-key="operation-log" />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { onMounted, ref } from 'vue';
+import { api } from '@/api/services';
+import SpecPageRenderer from '@/components/SpecPageRenderer.vue';
+import type { LogItem } from '@/types/page';
+
+const items = ref<LogItem[]>([]);
+
+onMounted(async () => {
+  const response = await api.getLogs();
+  items.value = response.items;
+});
+</script>

+ 69 - 0
src/views/system/RolePermissionView.vue

@@ -0,0 +1,69 @@
+<template>
+  <div class="app-page">
+    <section class="glass-card page-hero">
+      <div class="page-hero__meta">
+        <span class="page-hero__eyebrow">RBAC</span>
+        <h1>角色权限配置</h1>
+        <p>这一版先把角色列表、权限矩阵说明和操作动作放进页面骨架,后续适合继续扩展成树形权限矩阵。</p>
+      </div>
+      <div class="chip-list">
+        <span class="chip">页面权限 + 按钮权限</span>
+        <span class="chip">高风险权限二次确认</span>
+      </div>
+    </section>
+
+    <section class="page-grid page-grid--two">
+      <article class="glass-card section-card">
+        <div class="section-card__title">
+          <div>
+            <h3>角色列表</h3>
+            <p>当前以角色概览表为主。</p>
+          </div>
+        </div>
+        <el-table :data="items" stripe style="width: 100%">
+          <el-table-column prop="name" label="角色名称" width="120" />
+          <el-table-column prop="scope" label="权限范围" min-width="220" />
+          <el-table-column prop="boundUsers" label="绑定用户数" width="120" />
+          <el-table-column prop="status" label="状态" width="100" />
+          <el-table-column prop="updatedAt" label="更新时间" width="170" />
+        </el-table>
+      </article>
+
+      <article class="glass-card section-card">
+        <div class="section-card__title">
+          <div>
+            <h3>权限矩阵建议</h3>
+            <p>可继续扩成树形矩阵 + 数据范围抽屉。</p>
+          </div>
+        </div>
+        <ul class="tight-list">
+          <li>页面访问权限:按模块控制菜单可见。</li>
+          <li>按钮权限:控制新建、编辑、删除、退款、停用等操作。</li>
+          <li>数据范围权限:按仓库、店铺、国家、渠道限制可见数据。</li>
+          <li>特殊操作权限:取消订单、退款、删除 Key 等需额外校验。</li>
+        </ul>
+        <div class="chip-list" style="margin-top: 18px">
+          <el-button type="primary">新建角色</el-button>
+          <el-button>复制角色</el-button>
+          <el-button>保存权限</el-button>
+        </div>
+      </article>
+    </section>
+
+    <SpecPageRenderer page-key="role-permission" />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { onMounted, ref } from 'vue';
+import { api } from '@/api/services';
+import SpecPageRenderer from '@/components/SpecPageRenderer.vue';
+import type { RoleItem } from '@/types/page';
+
+const items = ref<RoleItem[]>([]);
+
+onMounted(async () => {
+  const response = await api.getRoles();
+  items.value = response.items;
+});
+</script>

+ 1 - 0
src/vite-env.d.ts

@@ -0,0 +1 @@
+/// <reference types="vite/client" />

+ 21 - 0
tsconfig.json

@@ -0,0 +1,21 @@
+{
+  "compilerOptions": {
+    "target": "ES2020",
+    "useDefineForClassFields": true,
+    "module": "ESNext",
+    "moduleResolution": "Bundler",
+    "strict": true,
+    "jsx": "preserve",
+    "resolveJsonModule": true,
+    "isolatedModules": true,
+    "esModuleInterop": true,
+    "lib": ["ES2020", "DOM", "DOM.Iterable"],
+    "skipLibCheck": true,
+    "types": ["vite/client", "node"],
+    "baseUrl": ".",
+    "paths": {
+      "@/*": ["src/*"]
+    }
+  },
+  "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue", "vite.config.ts"]
+}

+ 114 - 0
vite.config.ts

@@ -0,0 +1,114 @@
+import type { IncomingHttpHeaders } from 'node:http';
+import { fileURLToPath, URL } from 'node:url';
+import type { Connect, Plugin } from 'vite';
+import { defineConfig } from 'vite';
+import vue from '@vitejs/plugin-vue';
+import { mockRoutes } from './src/mock/routes';
+
+interface MockContext {
+  body: unknown;
+  headers: IncomingHttpHeaders;
+  method: string;
+  query: Record<string, string>;
+  url: string;
+}
+
+const createMockMiddleware = (): Connect.NextHandleFunction => {
+  return async (req, res, next) => {
+    if (!req.url || !req.method) {
+      next();
+      return;
+    }
+
+    const method = req.method.toUpperCase();
+    const requestUrl = new URL(req.url, 'http://localhost');
+    const route = mockRoutes.find((item) => {
+      const methodMatched = item.method === method;
+      const pathMatched =
+        typeof item.path === 'string' ? item.path === requestUrl.pathname : item.path.test(requestUrl.pathname);
+
+      return methodMatched && pathMatched;
+    });
+
+    if (!route) {
+      next();
+      return;
+    }
+
+    const body = await new Promise<string>((resolve, reject) => {
+      if (method === 'GET') {
+        resolve('');
+        return;
+      }
+
+      let raw = '';
+      req.on('data', (chunk: Buffer) => {
+        raw += chunk.toString();
+      });
+      req.on('end', () => resolve(raw));
+      req.on('error', reject);
+    });
+
+    let parsedBody: unknown = {};
+    if (body) {
+      try {
+        parsedBody = JSON.parse(body);
+      } catch {
+        parsedBody = body;
+      }
+    }
+
+    const payload = await route.handler({
+      body: parsedBody,
+      headers: req.headers,
+      method,
+      query: Object.fromEntries(requestUrl.searchParams.entries()),
+      url: requestUrl.pathname
+    } as MockContext);
+
+    res.statusCode = 200;
+    res.setHeader('Content-Type', 'application/json; charset=utf-8');
+    res.end(JSON.stringify(payload));
+  };
+};
+
+const mockApiPlugin = (): Plugin => {
+  const middleware = createMockMiddleware();
+
+  return {
+    name: 'crossborder-os-mock-api',
+    configureServer(server) {
+      server.middlewares.use(middleware);
+    },
+    configurePreviewServer(server) {
+      server.middlewares.use(middleware);
+    }
+  };
+};
+
+export default defineConfig({
+  plugins: [vue(), mockApiPlugin()],
+  resolve: {
+    alias: {
+      '@': fileURLToPath(new URL('./src', import.meta.url))
+    }
+  },
+  server: {
+    host: '0.0.0.0',
+    port: 5173
+  },
+  build: {
+    rollupOptions: {
+      output: {
+        manualChunks(id) {
+          if (id.includes('node_modules/element-plus') || id.includes('@element-plus')) {
+            return 'element-plus';
+          }
+          if (id.includes('node_modules/vue') || id.includes('node_modules/pinia') || id.includes('node_modules/vue-router')) {
+            return 'vue-vendor';
+          }
+        }
+      }
+    }
+  }
+});