实现 B-2b 统一报销工作台迁移(reimbursement-form-shared-component-migration)
这个提交包含在:
@@ -489,70 +489,82 @@ reference, not a copy of Feishu or Ant Design branding.
|
||||
| `请假记录` / `考勤` | 业务台账列表 or record view | Enterprise WeChat-owned records, mirror status, exceptions and audit | Ordinary users do not see OA daily leave/punch approval creation; OA emergency entry only for authorized emergency/admin scope. |
|
||||
| Mobile routes | Mobile expression of the same template | Top status/focus, card list or sectioned detail | One bottom safe-area primary action; secondary actions in `更多` or confirmation. |
|
||||
|
||||
## Scenario: Reimbursement Approval Content Workbench
|
||||
## Scenario: Unified Reimbursement Object Workbench
|
||||
|
||||
### 1. Scope / Trigger
|
||||
|
||||
- Trigger: a reimbursement approval task must expose the evidence needed for a decision without forcing the approver into a second page.
|
||||
- Trigger: changing PC/mobile reimbursement edit, read-only or approval detail, or changing workflow task routing for a reimbursement entity.
|
||||
- Purpose: keep one interactive business-object structure while allowing different desktop and mobile layout skeletons and different injected capabilities.
|
||||
|
||||
### 2. Signatures
|
||||
|
||||
- Mobile reimbursement target: `/mobile/reimbursements/:reimbursementId`.
|
||||
- PC reimbursement target: `/reimbursements/:reimbursementId`.
|
||||
- Mobile source state: `<Link state={{ from: currentMobileSource }}>`.
|
||||
- PC source state: `<Link state={{ from: currentPcSource }}>`; `currentPcSource` is the trusted internal `pathname + search` of the current reimbursement list or approval task.
|
||||
- Business routes: `/reimbursements/:reimbursementId` and `/mobile/reimbursements/:reimbursementId`.
|
||||
- Approval routes: `/workflow/tasks/:taskId` and `/mobile/approvals/:taskId`.
|
||||
- Dispatchers: `WorkflowTaskRoutePage` and `MobileWorkflowTaskRoutePage` fetch one authorized `WorkflowTaskDetail`, then select the reimbursement adapter or the generic non-reimbursement detail.
|
||||
- Pure layout shells: `ReimbursementDesktopWorkspaceShell` and `ReimbursementMobileWorkspaceShell`.
|
||||
- Shared model/capability types: `ReimbursementWorkspaceModel`, `ReimbursementWorkspaceSlots` and `ReimbursementEvidenceCapabilities`.
|
||||
- Structural preflight: `node scripts/qa/check-ui-design-rules.mjs --strict`.
|
||||
|
||||
### 3. Contracts
|
||||
|
||||
- The pending-task list card continues to open `/mobile/approvals/:taskId`; do not confuse task selection with the separate business-detail exit inside the task detail.
|
||||
- When `WorkflowTaskDetail.reimbursement` exists, PC and mobile task details load the current reimbursement detail and render the shared `ReimbursementItemEvidenceList`; the complete original form remains a secondary exit rather than the only evidence path.
|
||||
- The shared component receives a minimal projection. Review/open/reference and future edit/delete/link behavior are explicit callbacks; it never infers capabilities from roles.
|
||||
- Approval links keep the task URL. A dispatcher recognizes `entityType=reimbursement` and renders the reimbursement approval adapter directly; it never mounts the generic detail first and never requires a `查看报销单` detour for core evidence.
|
||||
- A task dispatcher owns the single task-detail query and passes the same result plus controlled refetch to the selected renderer. The reimbursement adapter may fetch current reimbursement detail but must not fetch the same task detail again.
|
||||
- Desktop and mobile shells share the same model, section order, `ReimbursementItemEvidenceList` and capability contract. They may use different DOM/layout skeletons: desktop main evidence plus one sticky command panel; mobile focus cards plus a bottom safe-area action surface.
|
||||
- Shells compose ready data and slots only. They contain no `useQuery`, `useMutation`, API/fetch call, role/permission inference, workflow/reimbursement state-machine decision, `mode === ...` comparison or `isMobile` business branch. `check-ui-design-rules.mjs --strict` enforces this boundary and rejects a dormant reimbursement branch in the generic approval pages.
|
||||
- The shared evidence component receives a minimal projection. Open/reference/edit/delete/link/unlink behavior exists only through callbacks; the component never infers capabilities from roles. Approval injects view/reference/decision only, read-only injects view only, and edit injects maintenance/submission capabilities.
|
||||
- Submission-time fee-standard and budget facts come from the workflow context snapshot. Current item/invoice grouping, amount evidence and attachments come from the reimbursement detail. Approval pages do not call reimbursement precheck or present current budget data as a submission snapshot.
|
||||
- Reimbursement approval actions fail closed until workflow snapshot parsing and current reimbursement evidence are ready. Historical missing budget snapshots are explained; malformed snapshots disable all decision paths.
|
||||
- Mobile workflow caching uses an explicit persisted projection. Reimbursement items, invoices, attachments, amount evidence, material access data, workflow snapshot checks and shared component projections do not enter IndexedDB. Offline reimbursement approve/reject/return/draft is forbidden, while other workflow entity offline behavior remains unchanged.
|
||||
- Mobile navigation passes the current approval task URL through the existing `state.from` convention so the business detail back button returns to that task. This is a one-level source contract, not a new nested return stack.
|
||||
- PC reimbursement list and task-detail exits use the same one-level `state.from` contract. The detail page accepts only `/reimbursements` list URLs and `/workflow/tasks/:id`; absolute/protocol-relative URLs, hashes, other modules and the current page are rejected. A direct open or refresh safely falls back to `/reimbursements`.
|
||||
- PC detail return is a visible text action immediately before the detail hero, uses `replace`, and is not duplicated inside the business primary-action group or `更多`.
|
||||
- Approval actions remain on the approval task page. Opening the business detail does not approve, reject, return or mutate the task.
|
||||
- The information order is compact header and reason, item with owned invoices, unlinked invoices when present, supporting attachments, real issues/differences when present, then history/progress. Normal state creates no `0 条` check card. Issue copy identifies the object, reason, impact and next step rather than exposing internal counters alone.
|
||||
- `MobileStickyActionBar` and `MOBILE_STICKY_ACTION_PAGE_CLASS` are one contract. Add the page class only when `mobileActions` exists; omitting it lets `page-scaffold` transform capture the fixed bar, while adding it without a bar leaves false bottom padding.
|
||||
- `/reimbursements/:id/print` remains a purpose-built print/PDF view. It is not a fourth interactive workbench and receives no workbench mutation.
|
||||
|
||||
### 4. Validation & Error Matrix
|
||||
|
||||
- Task has a reimbursement -> show the reimbursement-detail link.
|
||||
- Task is another entity type -> do not show a reimbursement link; add that entity's route only in a scoped task.
|
||||
- Reimbursement summary is absent -> render no broken or guessed target.
|
||||
- Current or historical assigned workflow reviewers may read the reimbursement evidence projection through server-side authorization; unrelated employees remain forbidden. Preview/download endpoints continue their own permission, governance, audit and short-lived URL checks.
|
||||
- Authorized task resolves to reimbursement -> dispatcher renders the approval adapter from the one task-detail result.
|
||||
- Task resolves to leave/attendance/purchase/travel -> generic detail remains active, including its existing mobile offline approve/reject draft behavior.
|
||||
- Current reimbursement detail, critical check or snapshot parsing is absent/invalid -> all reimbursement decision entry points are disabled and the action handler rejects a direct call with natural Chinese recovery copy.
|
||||
- Mobile reimbursement approval is offline -> no evidence is loaded from IndexedDB and no approval draft is allowed; an explained disabled state remains visible.
|
||||
- Mobile edit/approval has a bottom action -> fixed bar is visible at the viewport bottom and final evidence remains reachable above it. No bottom action -> no fixed-action page padding.
|
||||
- Current or historical assigned reviewers may read the authorized current evidence; unrelated employees remain forbidden. Material endpoints continue their own permission, governance, audit and short-lived WeCom grant checks.
|
||||
|
||||
### 5. Good/Base/Bad Cases
|
||||
|
||||
- Good: approver opens the task, reviews each item with its invoices and inline submission warnings, references an object into the return comment, then acts from the single action panel.
|
||||
- Base: a completed reimbursement task shows the current read-only evidence with a clear warning that it may contain post-return changes; historical comments and submission snapshots retain historical meaning.
|
||||
- Bad: the task shows four summary fields plus a mandatory `查看报销单` detour, caches invoice evidence offline, or enables a decision when current evidence failed to load.
|
||||
- Bad: make a static-looking summary card secretly clickable, change the pending-list click handler, or copy a partial invoice/attachment viewer into the task page.
|
||||
- Good: edit, read-only and approval display the same item/invoice grouping; only the adapter-provided actions differ. The approver reads evidence and acts from one task URL.
|
||||
- Base: a completed task shows the latest authorized read-only evidence with a clear time-semantics note while history and submission snapshots remain historical.
|
||||
- Bad: keep a dormant reimbursement branch in `WorkflowTaskDetailPage`/`MobileApprovalsPage`, query task detail in both dispatcher and adapter, or put `if (mode === "approval")` around API calls in a shell.
|
||||
- Bad: cache evidence/context data offline, show normal `0 条` summaries, render two copies of the reimbursement header, or leave a mobile fixed action captured by an animated ancestor.
|
||||
|
||||
### 6. Tests Required
|
||||
|
||||
- Mobile E2E: task detail -> reimbursement detail -> items/evidence visible -> back to the same task -> approval actions still visible.
|
||||
- PC E2E: filtered reimbursement list -> task/detail -> visible source-aware return; the list path retains its query string, task detail returns to the same task, and direct-open detail falls back to the reimbursement list.
|
||||
- Assert non-reimbursement task details do not receive a reimbursement target.
|
||||
- Run `cd client && npm run typecheck && npm run build`.
|
||||
- UI strict/self-test proves shell and legacy-branch rules are active and reports zero findings.
|
||||
- Source/structure tests prove both task dispatchers contain one `fetchWorkflowTaskDetail` call, the approval adapter contains none, and both shells share the model/slots/capability contract and section order.
|
||||
- PC/mobile E2E covers edit/read-only/approval, no-issue suppression, fail-closed, item-local invoice recovery, submit-confirm failure recovery, task deep links/back behavior and mobile fixed-action geometry.
|
||||
- IndexedDB tests inspect the stored record and reject reimbursement item/invoice/attachment/context/material fields plus synthetic sentinel values. Non-reimbursement mobile offline draft create/sync/conflict remains green.
|
||||
- Run `cd client && npm run typecheck && npm run build`, focused workflow/reimbursement Playwright, route/flow gates, Trellis governance and `git diff --check`.
|
||||
|
||||
### 7. Wrong vs Correct
|
||||
|
||||
#### Wrong
|
||||
|
||||
```tsx
|
||||
<div onClick={() => handleTaskSelection(task.id)}>报销摘要</div>
|
||||
export function ReimbursementWorkspaceShell({ mode, taskId }) {
|
||||
const task = useQuery({ queryFn: () => fetchWorkflowTaskDetail(taskId) });
|
||||
return mode === "approval" ? <ApprovalContent task={task.data} /> : <EditContent />;
|
||||
}
|
||||
```
|
||||
|
||||
#### Correct
|
||||
|
||||
```tsx
|
||||
<Link
|
||||
to={`/mobile/reimbursements/${encodeURIComponent(reimbursement.id)}`}
|
||||
state={{ from: currentMobileSource }}
|
||||
>
|
||||
查看完整报销单
|
||||
</Link>
|
||||
const taskQuery = useQuery({
|
||||
queryKey: ["workflow", "task", taskId],
|
||||
queryFn: () => fetchWorkflowTaskDetail(taskId),
|
||||
});
|
||||
|
||||
return taskQuery.data.workflowInstance?.entityType === "reimbursement"
|
||||
? <ReimbursementApprovalAdapter detail={taskQuery.data} refetchTask={taskQuery.refetch} />
|
||||
: <WorkflowTaskDetailPage detail={taskQuery.data} refetchTask={taskQuery.refetch} />;
|
||||
```
|
||||
|
||||
**Implementation order for design refactors**:
|
||||
|
||||
在新工单中引用
屏蔽一个用户