文件
hyb-oa/.trellis/tasks/archive/2026-08/08-05-business-detail-routing/design.md
T

85 行
9.0 KiB
Markdown

# 设计说明
## 1. 架构边界与目标路由
本任务只迁移采购、资产已有详情/编辑能力,并为“提交前没有实体 ID”的资产创建表单提供独立路由。列表数据、详情 API、mutation payload、权限数据源、审计和状态机保持原合同,不新增后端 endpoint 或数据库字段。
| 模块 | 列表 | 创建 | 详情 |
| --- | --- | --- | --- |
| 采购 | `/purchase-requests` | 不新增;列表先创建草稿 | `/purchase-requests/:purchaseRequestId` |
| 资产 | `/assets` | `/assets/new` | `/assets/:assetId` |
路由注册与页面元数据必须把 `/assets/new` 作为静态路径处理,不能落入 `:assetId`。`PAGE_TITLES` 增加采购详情、资产新建、资产详情层级,导航仍按父模块高亮。
## 2. 页面与数据职责拆分
- `PurchaseRequestsPage` 收敛为采购列表页:持有 URL 筛选、分页、列表 query、创建草稿入口和行级详情入口。
- `PurchaseRequestDetailPage` 承接现有采购 detail query、表单 hydration、附件、采购单归档、提交/撤回、审计/状态动作和 dirty 状态;不得为渲染详情请求完整采购列表。
- `AssetRegistryPage` 收敛为资产列表页:持有 URL 筛选、资产列表 query、资产分类管理、移动扫码入口、创建入口和行级详情入口。
- `AssetDetailPage` 与 `AssetCreatePage` 使用同一资产编辑工作区/section 集合:详情模式加载实体并按现有权限编辑与流转;创建模式使用初始表单并只执行 POST,避免复制两份长表单。
- 列表页不再加载详情专用 options;详情/创建页只加载自身表单所需的分类、公司、成本中心、候选采购等数据。React Query 可复用缓存,但职责不能靠隐藏 JSX 维持。
- 优先提取页面内类型、纯函数和有明确领域含义的 section;不建立跨采购/资产的“万能详情页”。
## 3. 列表筛选 URL 合同
筛选参数由 URL 直接派生,避免“URL 一份、本地 state 一份”的双向同步 effect。筛选输入使用 `replace` 更新当前列表 history entry;采购翻页同样保留当前筛选。资产文本搜索维持现有即时查询体验,但用 deferred/debounce 值触发请求,避免每次键入造成不必要的渲染/请求抖动。
| 模块 | 查询参数 | 默认与约束 |
| --- | --- | --- |
| 采购 | `status`、`scope`、`supplierId`、`legalEntityId`、`costCenterId`、`page` | 无 `status` 时为 `draft`;`status=all` 表示全部;无 `scope` 时为 `own`;`scope=all` 仅 `canViewAll` 生效;`page` 只接受正整数 |
| 资产 | `q`、`status`、`assetType`、`sensitivity`、`categoryId`、`legalEntityId`、`costCenterId`、`assigneeId`、`scope` | 无 `scope` 时保留现有按权限决定的默认范围;显式 `own/all` 仅在有全量权限时可切换,否则强制 own |
联动筛选一次性更新:公司变化且原成本中心不再属于该公司时,同一次 replace 删除 `costCenterId`;采购非页码筛选变化同时把 `page` 归一到 1。未知枚举、无效页码或越权范围只在读取层回落,不进入 API 查询条件。
## 4. 导航、返回与创建完成
- 列表进入详情使用 history push,并把 `pathname + search` 作为 `location.state.from`;浏览器后退自然回到原列表 entry,显式返回使用经校验的来源。
- 扩展 `pcDetailNavigation` 为显式 allowlist 合同,不把任意字符串当 return URL。采购详情允许采购列表来源以及受控的项目详情来源;资产详情/创建只允许资产列表来源。绝对 URL、协议相对 URL、hash、当前页和其他模块来源全部拒绝。
- 无安全来源时,采购详情回到 `/purchase-requests`,资产详情/创建回到 `/assets`。从全局搜索或通知进入时,浏览器后退仍按 history 返回原页面,显式返回使用安全列表兜底。
- 采购创建保留现有“先 POST 空草稿”合同;成功后 push 到采购详情并携带当前列表来源。
- 资产列表的“新增资产”push 到 `/assets/new` 并携带列表来源。创建成功后使用 replace 进入 `/assets/:assetId`,继续携带原列表来源,避免返回到已提交的创建表单。
- 项目详情中的采购链接携带项目详情作为安全来源;详情返回文案按来源显示“返回项目详情”或“返回采购列表”。
## 5. 路径构造与现有入口
- 新增单一的采购/资产详情路径构造函数,对实体 ID 使用路径段编码;列表、创建成功、项目关联、通知和搜索解析都消费该函数,避免分散拼接。
- `GlobalSearchDialog` 对 `purchase_request` 与 `asset` 使用 `entityType + entityId` 构造 canonical 路径,不继续信任旧服务端 `href` 的 `?id=` 或列表级值;其他实体仍沿用原合同。本任务因此无需修改全局搜索 API shape。
- PC `notificationLinks` 将 `purchase_request`、`asset`/`asset_card` 直接解析为新详情路径,停止生成未消费的 `highlight`;移动通知解析保持现有移动路由边界。
- `ProjectDetailPage` 的采购关联改为新详情路径;没有现有入口的模块不为满足“路由统一”额外新增按钮。
- `client/src` 中旧 `?id=` 只允许出现在明确命名的 legacy redirect/parser 与兼容测试中。服务端历史集成审计字段或未被前端消费的 routePath 不在本任务内,不能借机改 API 或集成逻辑。
## 6. 旧链接兼容
- `/purchase-requests?id=<id>` 与 `/assets?id=<id>` 在对应列表入口、列表 query 启动前解析;有非空 ID 时 replace 到 canonical 详情路径,避免先闪现列表或同时请求列表/详情。
- redirect 从查询参数中删除 `id`,将剩余且仍属于对应列表的参数归一化后写入安全 `state.from`;canonical 详情 URL 不携带列表筛选噪音。
- 空 ID、只有空白字符或缺少 ID 时不重定向,列表按其余有效筛选正常展示。未知实体 ID 进入详情后的统一错误态。
- redirect 使用 replace,历史栈不保留旧 URL;仓库内新入口全部使用 canonical 路径,旧格式仅作为外部收藏、旧 witness 和历史链接兼容层。
## 7. Dirty guard、错误态与权限
- 采购详情、资产详情和资产创建页面继续注册 `useUnsavedChangesGuard`;显式返回、浏览器后退、跨模块链接和直接路由离开均由现有 provider 阻断。
- 用户取消离开时不执行 navigate,保留当前 URL、实体和全部输入;确认后才执行原导航意图。资产创建成功后的 replace 只在 mutation 成功后发生。
- 403/404 统一映射为普通中文安全错误态:“无法查看该记录,记录可能不存在或你无权访问”;不渲染已取得的局部摘要,不区分可用于资源枚举的原因。
- 采购列表与详情沿用原 `RequirePermission` anyOf;资产列表与详情沿用原资产 anyOf。`/assets/new` 单独要求 `asset.manage`,页面内编辑、流转、敏感字段和附件权限继续使用现有细粒度判断。
- API endpoint、请求 payload、审计、高风险确认、附件下载原因和状态矩阵均不改变。
## 8. 移动兼容
- 现有 `/mobile/purchase-requests/:id` 不变;`runtimeContext` 继续把 PC 采购详情映射到对应移动详情。
- 本任务不新增移动资产详情或创建能力。移动工作台偏好环境访问 `/assets`、`/assets/new` 或 `/assets/:assetId` 时统一回落到既有 `/mobile/assets/scan`,保持旧 `/assets?id=` 在移动端的能力边界。
- 新 PC 详情路由不得改变移动底部导航、企业微信入口或资产扫码/盘点流程。
## 9. 测试策略
- 为两个模块建立同构路由合同:列表不发详情 query、详情不发完整列表 query、列表进入、直接深链、刷新、返回恢复、旧链 replace、无效/无权限、dirty 取消/确认。
- 单独覆盖资产 `/assets/new`:静态路由优先、`asset.manage` guard、直接访问、取消返回、创建成功 replace、失败保留输入、成功后后退不回创建页。
- 覆盖 URL 筛选事实源:直接打开筛选 URL、筛选 replace、采购 page 重置/恢复、范围权限收紧、公司/成本中心联动归一。
- 覆盖项目关联、全局搜索、PC 通知的 canonical URL;全局搜索 purchase/asset 不受服务端旧 `href` 影响。
- 更新 `procurement-flow.spec.ts`、`assets-flow.spec.ts`、`r103-cross-page-ui-consistency.spec.ts`、`pc-detail-mobile-visual-wave3.spec.ts`、`global-search.spec.ts` 及通知/移动路由用例,必要时增加专门的 `business-detail-routing.spec.ts`。
- 测试使用完整 URL 与页面 marker 断言,不用父列表路径前缀误认动态详情;403 与 404 都断言同一安全文案。
## 10. 回滚
- 新路由、页面拆分、筛选 URL、入口消费者和 legacy redirect 作为同一任务提交;回滚时整体恢复 `?id=` 页面形态,避免消费者指向不存在的页面。
- 无 schema、API 或数据迁移。应用回滚不处理数据库;若需部分回滚,必须至少同时回滚 route registry、lazy exports、canonical path builder 和所有前端消费者。