修复: 08-07-business-routing-regression-repair 关闭资产创建跳转回归

这个提交包含在:
2026-08-07 14:53:55 +08:00
父节点 892b313888
当前提交 c14b40a00a
共修改 15 个文件,包含 450 行新增和 21 行删除
@@ -1185,6 +1185,9 @@ validation prevent repeated re-entry and reduce support/debugging cost.
- Reload/close guard: `beforeunload` while `isDirty === true`.
- Shared UI contract: one unsaved-change dialog with continue-editing and
discard actions.
- Successful-save navigation: `usePostSaveNavigation()` opens one synchronous
allow-next window around the canonical navigation action; its signature is
`(action: () => void) => void`.
### 3. Contracts
@@ -1194,6 +1197,12 @@ validation prevent repeated re-entry and reduce support/debugging cost.
is called only after explicit discard confirmation.
- Record switching and panel closing use the same pending-action contract as
route navigation. Successful save or explicit discard clears dirty state.
- When a successful server save immediately replaces a create route with its
canonical detail, use the dedicated post-save navigation entry after a valid
entity ID is available. Do not depend on React state cleanup completing
before `navigate(...)`, and do not expose a persistent guard-disable switch.
- Failed, timed-out or invalid save responses never use the post-save bypass;
they retain the form values and normal dirty protection.
- Register `beforeunload` only while dirty and remove it during cleanup.
### 4. Validation & Error Matrix
@@ -1202,21 +1211,31 @@ validation prevent repeated re-entry and reduce support/debugging cost.
- Dirty in-app navigation or browser back -> block and show one dialog.
- Continue editing -> URL, record selection and values remain unchanged.
- Discard -> execute exactly one pending navigation/selection and clear dirty.
- Successful create -> replace to canonical detail without an unsaved dialog,
even when the clean-state update has not committed yet.
- Save failure or missing entity ID -> remain on the form with dirty protection;
never bypass navigation protection to hide the failure.
- Reload/close while dirty -> browser-level unload warning remains active.
### 5. Good/Base/Bad Cases
- Good: reimbursement, procurement, contract and asset forms reuse one guard
while keeping their own save and dirty-field logic.
- Good: an asset POST succeeds, then the page wraps one `replace` navigation in
`usePostSaveNavigation()` before refreshing non-critical list caches.
- Base: a short non-editable filter form does not register the guard.
- Bad: listen to history after navigation, then push the previous URL back; the
user can see state flicker or lose form data before confirmation.
- Bad: call `setSavedSignature(...)` and immediately navigate while relying on
React to commit the clean state before `useBlocker` evaluates.
### 6. Tests Required
- Playwright covers link navigation, browser back, reload and record switch.
- Assert cancel preserves URL and values; discard changes target once; save
removes the warning.
- A create-route regression covers successful canonical `replace`, absence of
the unsaved dialog and browser history that does not reopen the submitted form.
- Frontend typecheck and build after router or shared guard changes.
### 7. Wrong vs Correct
@@ -1227,6 +1246,11 @@ validation prevent repeated re-entry and reduce support/debugging cost.
window.addEventListener("popstate", () => navigate(previousUrl));
```
```ts
setSavedSignature(nextSignature);
navigate(canonicalDetailPath, { replace: true }); // dirty ref may still be true
```
#### Correct
```ts
@@ -1234,6 +1258,13 @@ const blocker = useBlocker(isDirty);
// reset to keep editing; proceed only after explicit discard.
```
```ts
const runAfterSuccessfulSave = usePostSaveNavigation();
// Call only after the API returned a valid entity ID.
runAfterSuccessfulSave(() => navigate(canonicalDetailPath, { replace: true }));
void refreshNonCriticalCaches();
```
## Scenario: Business Ledger List-To-Detail Route Migration
### 1. Scope / Trigger