背景:#2257 修的是实例,这里要消灭的是 bug 类
framework#2604 / #2256 / #2257 / #2267 的 dogfood 过程暴露了一个结构性问题:这套 UI 把"数据刷新"和"UI 重建"绑在一起,又没有"什么状态该放哪"的成文规则。tab 重置只是它露出的角;同一根因还在持续产生:
现有代码里其实已经有正确模式的先例:notifyRelatedChanged 事件(#2256,子表保存只刷 related list、父详情原地不动)和 RelatedCountStore(模块级订阅式计数失效)。本 issue = 把先例推广成制度。
方案(三层,按投入产出排序)
P1 — 客户端失效总线,删掉两个 key(核心工作项)
把 RelatedCountStore / related-changed 的模式推广为统一的数据失效层:
- 写路径(create/update/delete/action 成功)按
(objectName, recordId?) 精确发失效;
- 数据消费方(详情主体 fetch、
RelatedList、计数徽章、审批状态等)各自订阅、各自重取;组件树永不因数据刷新而重建;
- 完成后直接删除
key={refreshKey} 与 key={actionRefreshKey}(及其 bump 源),crud_success 只负责 toast + 关 overlay + 发失效;
- 兼容期:
notifyRelatedChanged 收敛为总线的一个别名调用,RelatedCountStore 接到同一总线上,消灭三套并存。
收益 = 正确性(滚动/折叠/就地编辑草稿在保存后幸存)× 性能(定向重取取代全量重取)双赢。
P2 — UI 状态分类规则写进 AGENTS.md + review 检查项(半天)
| 状态类别 |
判据 |
归属 |
| 可寻址状态 |
想分享 / 刷新后想回到 / Back 应尊重 |
URL(?recordId= ?form= ?tab=) |
| 用户偏好 |
跨记录、跨会话稳定 |
localStorage / 服务端偏好 |
| 真·瞬态 |
丢了没人心疼(hover、下拉开合) |
组件内部 state |
一句话规则:"任何需要在数据刷新中幸存的用户状态,不允许只活在非受控组件里。" 不开新 ADR(ADR-0054 C3 已画边界),落 AGENTS.md 约定区即可。
P3 — URL 保留参数注册表(半天,趁参数还是个位数)
app-shell 内建单一导出模块登记所有保留 query 参数:语义、push 还是 replace、谁读谁写;所有读写经它。TSDoc 即文档——AI 作者(北极星)将来消费 URL 契约时有单一学习点。
非目标 / 挂账说明
验收
Refs #2257 · #2267 · #2256 · framework#2604 · ADR-0054 C3
背景:#2257 修的是实例,这里要消灭的是 bug 类
framework#2604 / #2256 / #2257 / #2267 的 dogfood 过程暴露了一个结构性问题:这套 UI 把"数据刷新"和"UI 重建"绑在一起,又没有"什么状态该放哪"的成文规则。tab 重置只是它露出的角;同一根因还在持续产生:
AppContent的<RecordDetailView key={refreshKey}>(每次crud_successbump)、RecordDetailView的<DetailView key={actionRefreshKey}>(每次记录级 action bump)。语义 = "不知道哪些数据脏了,把整页重建"——顺带摧毁滚动位置、分组折叠态、进行中的就地编辑(潜在数据丢失),并触发全量重取(Dashboard-level filters follow-ups: showcase, more examples, docs, spec pairing (#2576 / framework#2501) #2578 时代实测过一个记录页 ~50 个并发请求的教训);?tab=URL 化治好,但规则不落地,下一个非受控状态(related list 折叠、面板开合)会重演同样的 bug;recordId/form/formObject/formLink/tab/palette/ …,无单点定义,撞名/语义漂移只是时间问题。现有代码里其实已经有正确模式的先例:
notifyRelatedChanged事件(#2256,子表保存只刷 related list、父详情原地不动)和RelatedCountStore(模块级订阅式计数失效)。本 issue = 把先例推广成制度。方案(三层,按投入产出排序)
P1 — 客户端失效总线,删掉两个 key(核心工作项)
把
RelatedCountStore/related-changed的模式推广为统一的数据失效层:(objectName, recordId?)精确发失效;RelatedList、计数徽章、审批状态等)各自订阅、各自重取;组件树永不因数据刷新而重建;key={refreshKey}与key={actionRefreshKey}(及其 bump 源),crud_success只负责 toast + 关 overlay + 发失效;notifyRelatedChanged收敛为总线的一个别名调用,RelatedCountStore接到同一总线上,消灭三套并存。收益 = 正确性(滚动/折叠/就地编辑草稿在保存后幸存)× 性能(定向重取取代全量重取)双赢。
P2 — UI 状态分类规则写进 AGENTS.md + review 检查项(半天)
?recordId=?form=?tab=)一句话规则:"任何需要在数据刷新中幸存的用户状态,不允许只活在非受控组件里。" 不开新 ADR(ADR-0054 C3 已画边界),落 AGENTS.md 约定区即可。
P3 — URL 保留参数注册表(半天,趁参数还是个位数)
app-shell 内建单一导出模块登记所有保留 query 参数:语义、
push还是replace、谁读谁写;所有读写经它。TSDoc 即文档——AI 作者(北极星)将来消费 URL 契约时有单一学习点。非目标 / 挂账说明
RecordDetailView的机理(疑 lazy + Suspense + router transition 组合):dev-only 伪影(Detail-page tab strip resets to "Details" on any URL search-param change (uncontrolled PageTabsRenderer) — make tab state URL-driven (ADR-0054 C3) #2257 评论有 A/B 实证),P1 落地后自然无害化;保留 StrictMode——它的"噪音"正是提前暴露了重建不健壮。deriveRecordSurface/deriveRecordFlowSurface的 plugin-view 本地镜像回收:等@objectstack/spec下一次发版后按代码 TODO 换回@objectstack/spec/dataimport(机械活,别拖过两个版本)。验收
key={refreshKey}/key={actionRefreshKey}(及等价的"数据刷新即重挂载"模式);notifyRelatedChanged/RelatedCountStore收敛到同一失效层,无三套并存。Refs #2257 · #2267 · #2256 · framework#2604 · ADR-0054 C3