Archived
评审复现了一个真泄漏:selectCurrent 用 /\bselected\b/.test(block) 当「行内那一遍 处理过没有」的代理 —— 而一行被正确处理过、且合法地落在「没有选中项」上(值不在 选项里,或这一格还没填),跟「压根没被处理过」在字面上是同一个样子。renderView 收尾那一遍拿外层作用域重新处理这块,若外层恰好有个同名裸键命中某个选项,就会把 它错误地渗进这一行 —— 界面撒谎:显示一个这一行数据里根本不存在的选中态。 改记一个跟内容无关的标记属性 data-sel-scoped:处理完(不管有没有真标出 selected) 都打上,下一遍见到标记就跳过;renderView 收尾前把它整个剥掉,不留进交给沙箱的成品。 顺带治了评审点的 Minor #1:旧写法还会被 <option> 显示文字里恰好出现的 "selected" 误判(枚举标签写成 "Selected Value" 那种)。 三条新用例:行内下拉值不在选项里 / 新加的空行,均验证不再被外层同名值渗透 (RED 证实了泄漏确实发生过);行外独立下拉仍正确标好 selected(防重归防重, 不能把本该工作的那条也防没了)。 docs/HTML报告生成规范.md 补一句:data-options 与 data-slot 不能共存,后者会被 fillOptions 摘掉不起作用(评审 Minor #2)。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet
338 lines
27 KiB
Markdown
338 lines
27 KiB
Markdown
# HTML 报告生成规范(活动视图模版)
|
|
|
|
> 2026-08-02 初版,从《活动开发规范》拆出成册。基准:`activities/quote-intake@42` 的三张模版
|
|
> (`intake-overview` / `intake-classified` / `intake-release`)—— 用户验收通过的那套。
|
|
> 渲染引擎:`apps/api/src/modules/interaction/internal/domain/view-render.ts`;
|
|
> 容器:`packages/ui-kit/src/ViewContainer.tsx`;保存链路:`apps/api/src/app/routes/desk.ts`。
|
|
> **本文与代码冲突时以代码为准。**
|
|
|
|
---
|
|
|
|
## 1. 报告是什么:同一份 HTML 的三个身份
|
|
|
|
| 身份 | 在哪 | 状态 |
|
|
|---|---|---|
|
|
| **件**(给人看) | 工作区 · 待处理 | 锁定态 —— 进屏先是一份能读的报告,不是一张待填的表 |
|
|
| **表单**(给人改) | 工作区 · 待处理,按「编辑」后 | 可改格子现出输入件,能增删行 |
|
|
| **归档件**(存档) | 工作区 · 工作成果 | 同一份 HTML 原样定稿,零输入框零外壳 |
|
|
|
|
**为什么必须是同一份**:渲成两三套就是存两三份,存多份就会分叉,分叉之后没人看得出
|
|
以哪份为准。**看与改是两件事**(整份件一进屏就是编辑态,人还没决定改什么,
|
|
眼前已经是 54 个亮着的输入框 —— 踩过),但**看的与改的必须是同一份东西**。
|
|
|
|
三条硬规矩:
|
|
|
|
1. **每份件两态 1:1,不同工作各有各的件**(2026-08-02/03 用户两次校准):
|
|
「1:1」的落点是**每份件与它的办结快照**(在办可编辑 ↔ 回执收下那一刻存
|
|
`finalHtml`,成果页静态重放,数据/内容/布局一个字不差)—— 不是全程只许一张。
|
|
作业的件(如 A2 选型 One Pager)与收口的件(整合清单)是两件工作两份件:
|
|
「清单是所有选型完成后收口才整合的东西」。别再把两态统一误执行成一张到底
|
|
(踩过:验收工单打开还是选型页,用户当场指出)。
|
|
2. **剧本每步的 `view` 定推哪张**,写清单时定死,不归模型临场挑;放行那一步的
|
|
`view` 必须指向验收目标件 —— 最后摆上去的那张就是归档件。
|
|
3. **数据变了基座重渲**(repush):人在件上保存、在卡上回填,落草稿层后服务端拿
|
|
新数据重渲同一张 —— 由数据算出来的东西(类徽、分布条、去向)跟着一起变。
|
|
**手改的全部意义就是纠正 AI 的错,改了数而画面不动,这个兜底功能就是假的**(用户原话级教训)。
|
|
|
|
## 2. 报告与卡(表单)的关系 —— 这套里最容易做错的一章
|
|
|
|
三个区各归各位(2026-08-01 用户定,别再混):
|
|
|
|
| 区 | 放什么 | 不放什么 |
|
|
|---|---|---|
|
|
| **工作区 · 待处理** | **只放那份件**(活动的 HTML 模版),整篇铺满工作区宽度、可就地改 | 题头 / 编号带 / 依据带 / 判据表 / 指导书 / 提交钮 —— 一样都不要 |
|
|
| **工作区 · 工作成果** | 同一份件,定稿归档 | 同上 |
|
|
| **对话区的卡** | 作业指导书(steps/standard)· 依据 · 判据 · **整单级表单字段** · **提交与裁决** · 落地结果 | 件本身(给一枚「执行工单/验收工单」四字钮跳过去就够) |
|
|
|
|
分工的原则:**逐行数据归件,整单级归卡;编辑在件,提交在卡。**
|
|
|
|
- 一张 9 行的清单,人要逐格核、逐格改 —— 那是件的事(报告即表单);
|
|
- 「结果说明」这种整单一格的字段、「先受理还是退回」这种裁决 —— 那是卡的事;
|
|
- 卡上那枚提交钮是**唯一的交卷动作**;件上的保存只落草稿层,**工单不办结、
|
|
放行门的位置不动**。
|
|
|
|
**同一个数管三处(同源纪律)**:台账声明 `required` 产出的 `域_对象_missing`——
|
|
|
|
```
|
|
件上的缺项红带(gapBar) ←┐
|
|
卡上的提交闸(gate) ←┼── 同一个基座读数、同一套算法
|
|
放行门的 L1 机检 ←┘
|
|
```
|
|
|
|
三处各算各的,就会出现「件上说齐了、卡上说不能交、放行门又说行」——
|
|
那种矛盾人是判不出对错的。所以**模版与卡都不算这个数,只摆基座算好的**。
|
|
|
|
**保存链路(件 → 库 → 屏)**:
|
|
|
|
```
|
|
人按「保存」→ 前端按件自己说的话收值(data-edit 地址 + data-col 列名 + drops)
|
|
→ POST /tasks/:id/table {tables, drops}
|
|
→ 服务端按【活动清单的台账声明】过授权门,归型,只写真改了的行,
|
|
自增主键补号,draftDrop 撤显式点名的行
|
|
→ 留一条人痕(改了几行/新增几行/撤掉哪几行 —— 那条痕是下一拍模型的行动指南)
|
|
→ repush 重渲 → 闸、判据、类徽、分布条、去向全部跟着变
|
|
```
|
|
|
|
三条实测教训,每条都别再犯:
|
|
- **授权面是台账声明,不是「当下那张卡长什么样」** —— 从前保存借道工单 `blocks[i]`
|
|
取列名,熔断卡(没有 blocks)开着时,人补的那一格一个字都送不出去,
|
|
而他要补的恰恰是把这一单卡住的必填格;
|
|
- **删要说出来**(载荷 `drops` 点名主键),不靠「没回传就是删了」—— 那种口径会把
|
|
一次不完整的回传变成一次删表;
|
|
- **只写真改了的行** —— 件上按一次保存整张表都回传,照单全写会留「修订 4 行」的痕,
|
|
而人只改了一格;日志是给人看的,那句话就成了假的。
|
|
|
|
**卡侧还有一条老路**(与件侧并存,别混):卡面 `blocks` 里绑了 `ledger`+`fields` 的表,
|
|
由**基座铺行**(发卡那一刻从草稿层铺,活动别自己抄行 —— 回复有长度上限,铺长了截断
|
|
整拍作废)、人提交时由**基座写回**(`table:N` 载荷)。验收卡的 `editableAtRelease`
|
|
成果表走的就是它。两条路的授权面是同一份台账声明。
|
|
|
|
## 3. 沙箱与安全
|
|
|
|
- 容器:`sandbox="allow-same-origin"` 的 iframe,**永不给 `allow-scripts`**
|
|
(两者同给才是破沙箱)。父文档读得到也改得动里面的 DOM:开编辑 = 加 class、
|
|
收值 = 读 input,全在父文档侧;**iframe 里始终一行脚本都没有**。
|
|
- 模版含 `<script` 或外链,安装期即拒。样式全部自带(内联 `<style>`)。
|
|
- ⚠️ **跨 realm 陷阱**:父文档里 `event.target instanceof Element` 判 iframe 元素
|
|
**恒为 false**(Element 是父文档的构造器)。症状:点「编辑」毫无反应而 class/CSS/
|
|
选择器逐个查都对。**认能力不认血统**(有 `closest` 就当元素);监听挂载写成幂等的,
|
|
每次有机会就挂一遍(srcDoc 换文档的时机抓不准)。
|
|
- 内容更新**就地换 `body.innerHTML`,不换 srcDoc** —— srcDoc 一变整个文档重载,
|
|
高度掉回兜底再涨回来,人刚按下保存件就「闪黑」一下。
|
|
|
|
## 4. 模版语言参考(绑定全表)
|
|
|
|
渲染 = 纯取值绑定。基座三遍扫描:槽位填值 → 输入件填 value → 独立 class 投影。
|
|
|
|
| 属性 | 写在哪 | 语义 |
|
|
|---|---|---|
|
|
| `data-slot="键"` | 任意元素 | 取值填为文本。数值整数原样、小数截 4 位;数组给长度 |
|
|
| `data-rows="mine:域.对象"` | 行容器(tbody 等) | 按本单草稿行逐行复制内部结构;行内 `data-slot` 相对该行取值;序号交 CSS 计数器 |
|
|
| `data-by="子列"` / `data-by="子列=父列"` | 与 `data-rows` 同元素(**嵌套**在另一个行模版里) | 只留子列值 = 外层那一行父列值的行 —— 「一个候选一张表」(外层按候选复制 `<section>`,里层只列它名下的明细);也能当查表用(`<span data-rows="parts.profile" data-by="internal_pn=candidate_internal_pn">` 从整表里挑这一颗料那一行摆品牌)。父值缺席一行不留。行模版可以嵌套:里层先按本行展开,再填本行标量;**外层容器的标签名不得在行模版内部再出现**(用 `<article>` 包 `<section>`) |
|
|
| `data-blank` | 与 `data-slot` 同元素 | 空值**真空**(不写「无」)—— 空格子的胶囊、占位交给 CSS `:empty::after` |
|
|
| `data-class-of="键"` | 任意元素 | 把值消毒成 `v-<小写值>` 追加进 class(数值就是 `v-0`/`v-1`/…)。**判断留在样式表** |
|
|
| `data-col="台账列名"` | 可改格子(与 `data-slot` 同元素) | 渲染时多渲一个孪生 `<input class="jc-cell">`;写的是**台账列名**不是表头文字(表头改说法不该让写回错位) |
|
|
| `data-options="域.对象"`(配 `data-option-value` / `data-option-label`) | `<select>` | **选项从台账现读**:一行一个 `<option>`,`value` 取值列、显示取名列,前面固定留一个空选项(「还没选」得说得出来)。地址写法与 `data-rows` 一致。选中哪一个仍由 `selectCurrent` 按这一行的值定 —— 模版这一侧照旧只取值不判断 |
|
|
| `data-edit="域.对象"` | 可改表的行容器 | 这组行可就地改;保存按它认地址 |
|
|
| `data-value` | `<input>` | 值进 value 属性(孪生件用,模版一般不手写) |
|
|
| `data-file-of="主键列"` | 行内任意元素 | 这一格**点开是一份资料文件**:渲成 `data-file="域.对象/主键值"`,外层接住就跳到查看面。那一列空着就整个属性去掉 —— 没有文件的行不该长得像能点开 |
|
|
|
|
**`data-options` 与 `data-slot` 不能共存**(2026-08-29):同一个 `<select>` 上若还写了
|
|
`data-slot`,渲染时会被 `fillOptions` 一并摘掉、不起作用 —— `data-slot` 换的是**文本**,
|
|
而 `<select>` 的内容是一串 `<option>`,留着它会被后面的槽位填值当成普通文本槽,把刚铺好的
|
|
选项整段冲成一行数字/文字。**下拉的「当下值」由 `data-col` 决定**(`selectCurrent` 认它),
|
|
不必也不该再写 `data-slot`。
|
|
|
|
**资料文件**(2026-08-23):一行台账还可以是一份**真文件**。台账声明里写
|
|
`file: { titleColumn, bodyColumn, formatColumn }`(它同时是授权面 —— 不声明就点不开),
|
|
正文一律 Markdown 原文写在 `bodyColumn`,格式列写 `md` / `html` / `docx` / `xlsx`;
|
|
基座按声明**现造**字节(`file-compose.ts`),再读回来渲(`file-doc.ts`)——
|
|
下载到的是真 .docx / .xlsx,屏上渲的就是那份字节读回来的样子。
|
|
件这一侧只管挂 `data-file-of`,「点了去哪」不在 HTML 里。
|
|
|
|
**在办中现造,放行那一刻存档**(2026-08-23 补齐):在办期间文件是台账那一行的投影,
|
|
台账改一格文件跟着变 —— 那时这样最好,两处永远一致。**放行落地之前**基座把每一份
|
|
造成字节冻进 `sys.task_asset_file`,取文件那条路**存档优先**:归档件上点开的必须是
|
|
他签字那一刻的那一份,而不是台账后来被下一单改成的样子。
|
|
非存不可还有一条硬理由:资料常落在**主数据**上,而试跑单放行时主数据只留在活动缓存、
|
|
不落正式表 —— 不存这一份,试跑单一放行,工作成果那一屏就什么都点不开。
|
|
工作成果里「产出的资料」自成一摞(与定稿报告分开:报告是「叫人办的那件事办完长什么样」,
|
|
资料是**办出来的东西本身**)。
|
|
|
|
取数约定:`mine:域.对象` = 本单草稿行(成果表**只用它** —— 别单放行过的记录不混进来);
|
|
裸键 = 摊平字段 + **基座读数**(`域_对象_rows` / `_missing` / `_missing_keys` /
|
|
`_<列>_<值>`…);`case:openedAt / case:origin / case:owner / case:routeKey / routePort /
|
|
routeTo / routeWhy` = 引擎注入的案情与算出的去向。
|
|
|
|
**语言纪律**:报告上不摆系统内英文名 —— 状态名、表名、列名、口名一律人读名;
|
|
数值列**与表头同侧左对齐**(右对齐是给要竖着相加的数用的;一行一个料、彼此不相加的表,
|
|
右对齐只让数和自己的标题分家),等宽字体留着。
|
|
|
|
### 4.1 运营中心沙箱看板:`viewTemplates[].data` 取数声明(2026-08-27)
|
|
|
|
工单件的行由引擎按**本单草稿层**给(`mine:`);运营中心活动详情底部那张沙箱看板摆的是**最近一次已放行实例**
|
|
的落账快照 —— 草稿层放行后已清空,行只能按案号从**正式表**圈。「哪几行是这一单的」是活动自己的知识
|
|
(案号前缀、哪张表按哪一列圈),所以写在模版旁边的 `data` 里,基座只照它查:
|
|
|
|
```ts
|
|
{ name: 'activity-ledger', html: LEDGER_BOARD, data: {
|
|
case: { ledger: 'sub_eval.verdict', key: 'eval_ref', prefix: 'W05-' }, // 案号 = prefix + 开案时刻 14 位数字(与模型铸主键那串同源)
|
|
rows: [
|
|
{ as: 'sub_eval.pn_check', ledger: 'sub_eval.pn_check', by: { column: 'eval_ref', from: 'case' }, orderBy: [{ field: 'role', dir: 'desc' }] },
|
|
{ as: 'parts.profile', ledger: 'parts.profile', by: { column: 'internal_pn', from: 'sub_eval.pn_check.internal_pn' } },
|
|
{ as: 'validation.history', ledger: 'validation.record', by: { column: 'validation_internal_pn', from: 'sub_eval.pn_check.internal_pn' },
|
|
not: { column: 'request_id', from: 'sub_eval.verdict.request_id' } },
|
|
] } }
|
|
```
|
|
|
|
- `rows` **按序**求值:`by.column ∈ values(by.from)`,`from` 只能是 `case` 或**前面**某个 `<as>.<列>`;每个值单独一次
|
|
等值读(正式表里历次运行的行并排躺着),结果按 `from` 值的顺序拼接 —— 想让原 PN 排第一,就让核对表按 `role desc` 排;
|
|
`not` 排除一批(同料号历史 = 按料号圈的减去本单挂的);同一张表圈两批就另起 `as`。
|
|
- 作用域:`<as>` 行数组(`data-rows="<as>"`,嵌套行照旧 `data-by`)、`<as>.<列>` 首行标量(一单一行的 verdict / demand
|
|
直接 `data-slot="demand.request.request_id"`)、`<as 去符号>_rows` 行数、`<as 去符号>_<列>_<值>` tally 计数(先铺 0)、
|
|
`case:instanceNo / openedAt / releasedAt / owner / ref`、`board:state`(`ready | missing | none`,挂在 `.page` 上
|
|
`data-class-of` 投成 `v-ready` 等,横幅与区块的显隐交样式表)。
|
|
- 表未物化 / 查询失败:那一块空着、stderr 一行,整屏不 500;案号那一行查无 → `missing`;没放行过 → `none`。
|
|
- 看板**只读**:没有 `data-edit` / `data-col`;仍不算不判(MATCH 行淡显、放弃的候选只在有 abandoned 行时露面,
|
|
都是 `data-class-of` 投出的 class 交样式表;计数摆基座读数)。渲染在 `apps/api/src/app/activity-ops/view-scope.ts`。
|
|
|
|
## 5. 模版不许算,但基座算过的可以摆
|
|
|
|
两件事别混:
|
|
|
|
- 报告**自己算**(把判据抄成静态文字、拿宽度骗人、JS 拼 class)—— 不行。
|
|
抄本与判据是两份东西,改了一处另一处就一直指着旧规则。
|
|
- 摆**基座算好的那一次** —— 该做,而且要做足。三个惯用法:
|
|
- **按值上色**:`data-class-of="type_tag"` → `.v-a/.v-b/.v-c` 三色徽;
|
|
- **按条数分宽**(分布条):`data-class-of="quote_intake_record_type_tag_a"` →
|
|
样式表备一把 `v-1…v-20` 的宽度梯子,`flex` 按档取;零条那段自己消失;
|
|
- **按数开关**(缺项红带):`data-class-of="quote_intake_record_missing"` →
|
|
`v-0` 那档 `display:none` ——「一格不缺就别出这条带」这个判断在样式表里,
|
|
模版仍只做取值。
|
|
- 去向那行:基座按契约各出口 `when` 序求值后注入 `case:route*`(与放行发口是
|
|
**同一次判定**),报告只摆;算不出来键就是空的,一行都不高亮,不冒充判过。
|
|
- 搬不过来的如实空着:编数据是撒谎,画空轨像组件坏了 —— 那一格不画,
|
|
把「为什么没有」写在卡脚。
|
|
|
|
## 6. 编辑态(报告即表单)
|
|
|
|
**默认锁着,按「编辑」才开。** 每张可改的表配三样(A1 `report.ts` 有现成实现):
|
|
|
|
1. **闸**(`editGate`):`[data-edit-for="域.对象"]` + 三钮
|
|
`data-edit-open / -cancel / -save`。锁着只出「编辑」;开了换「取消/保存」;
|
|
闸只在这一屏可改时现身(根节点 `editable`),归档件里一个字都没有。
|
|
2. **孪生输入件**:挂了 `data-col` 的格子渲染时自动配一个 `<input>`,
|
|
只读那份与输入那份由样式表按表上的 `on` 二选一 —— 同一份 HTML 两态复用。
|
|
编辑态下必填空格红框(`:placeholder-shown`)、整行浅红打底。
|
|
3. **增删行**:序号格编辑态换成 ×(一列两用);表尾一条「加一行」
|
|
(照末行克隆、值清空,主键不填不落库)。**删是挂记号不当场抹掉**
|
|
(`jc-row-gone` 划线摆着 —— 看得见、可后悔);「取消」把加的行撤掉、
|
|
把删记号摘掉、把值放回快照,**三样一起才叫还原**。
|
|
|
|
为什么必须有增删:手改是**兜 AI 的错**的那一手,而错不止「这一格填错了」——
|
|
「这一行根本不该在」(客户点名跳过的料被写进有效清单)与「漏了一行」是同一类错,
|
|
只给改格子就没有出口(实测那一单 13 次拒回、234k token 才熔断)。
|
|
|
|
4. **缺项红带**(`gapBar`):表上方一条红带点名「还差 N 格必填,分别是哪一行哪一列」
|
|
(`_missing` + `_missing_keys`,与提交闸、放行门同源),并告诉人怎么办
|
|
(「按右上角编辑开表,红框那几格就是要填的」)。只在可改屏出、缺口为 0 自动收
|
|
(§5 的按数开关)。小胶囊淹在九行表里,人得逐行扫 —— 而他打开这一屏就是来填的。
|
|
|
|
## 7. 有目标件(设计稿)时:搬,不要照着做
|
|
|
|
A1 两份报告是 1:1 平移 PRD 目标件:样式表整段搬(`prd-style.ts` 的 `PRD_CSS` +
|
|
`PRD_SPRITE`,一个数值都没改),标记逐块搬,只把写死的数据换成绑定。
|
|
**不用拼装件复述** —— 那套外壳有自己的版面语言,「照着做」出来的是第二种版面。
|
|
|
|
搬的时候四样替换(原型是带脚本的演示页,这里一行脚本跑不了):
|
|
|
|
| 原型怎么做 | 搬过来怎么做 |
|
|
|---|---|
|
|
| JS 造表、写死的行 | `data-rows` 行模版 + CSS 计数器序号 |
|
|
| JS 算的计数 | 台账汇总列或**基座读数**,`data-slot` 取 |
|
|
| JS 判空插胶囊 | `data-blank` 真空 + CSS `:empty::after` |
|
|
| JS 拼 class / `style="flex:N"` | `data-class-of` 投影 + 样式表分档(§5) |
|
|
|
|
**验收只认并排截图比对** —— 「25 块到位」是结构断言,对版面零约束(栽过三轮)。
|
|
|
|
## 8. 版式纪律
|
|
|
|
- **空占位三级统一**(2026-08-02 用户定,全局口径):**必填(红)· 建议填(橘)·
|
|
选填(灰)** —— 与声明的 `required` / `expect` / 其余一一对应,胶囊文案就写这三个词。
|
|
归档静态态它们如实呈现「没有」。
|
|
- **选择列直接选**(如 A2 替代等级):三枚自绘 radio(`input[type=radio].jc-cell[data-col]`,
|
|
radio 盖满选项格可点、`:has(:checked)` 高亮),**不写 checked** —— 当前值由
|
|
`data-class-of` 投影 v-* 描虚线;基座对未选中的 radio 整列不交(「没点」=「不改」)。
|
|
收值按「input 的祖父」归行:一张卡里所有输入格的祖父必须是同一个元素。
|
|
|
|
- **铺满工作区**:去掉外层宽度上限与横向留白、卸掉外框圆角边 —— 件就是这一屏的正文,
|
|
不是嵌在里面的另一个页面(`jc-stage { max-width:none }`)。
|
|
- 必填/选填胶囊只在**在办态**摆;归档件一律 `dimempty`(那时空格的意思不再是
|
|
「你还能填」而是「客户没给」,摆红胶囊是在催一件办完的事)。
|
|
- 别摆恒常说「没有」的横幅(「本轮不报 0 行:—」),别把同一个数摆两处
|
|
(人得停下来对一遍两处一不一样);已在别处说过的结论不再复述一遍。
|
|
- 标记里不写 `**` 星号这类记号 —— 卡面与件都是纯文本/纯 HTML,记号不会变粗体。
|
|
- **概念图形是报告的第一语言**(A2 迭代周用户反复要「图形占比大一点」):
|
|
芯片小像、流光走线、引脚对照、漏斗 —— 内联线稿 SVG(stroke currentColor,
|
|
配色走样式表)把「这是件什么事」画出来;数据格是第二语言。一份让人愿意看的
|
|
报告先立视觉隐喻,再摆数。
|
|
- **收值三铁律**(件上就地改的结构约束,违者行互相覆盖或值丢失):
|
|
① 同一行所有输入格的**祖父必须是同一个元素**(collect 按祖父归行);
|
|
② 主键格必挂 `data-col`(行的钥匙,可以是隐藏格);
|
|
③ radio 不写 `checked`,「没点」=「不改」(当前值靠 `data-class-of` 投影描样式)。
|
|
|
|
## 8.5 一页汇报(slide 语言)—— 强交互全走纯 CSS
|
|
|
|
用户对 A2 选型件的要求(2026-08-02):「不够华丽,要强交互的汇报形式,
|
|
直接拿去当一页 PPT 的程度」。沙箱零脚本不变 —— **「强交互」全部由样式表承担**,
|
|
数据仍只经 `data-slot` / `data-class-of` 投影,动效是呈现,不是判断。
|
|
参考实现:`activities/quote-alternative/src/views/report.ts` 的 `ALT_WORKSHEET`。
|
|
|
|
**构图**:主视觉(hero)+ 编号小节(slide part)推进。
|
|
|
|
- **hero 深空底**:`radial-gradient` 两团光 + `linear-gradient` 深空 +
|
|
`::before` 电路点阵(radial-gradient 1px 点)+ `::after` 顶部一道光。
|
|
hero 内配色自成一组(亮字/暗丸/信标),别把 PRD 亮色变量直接用在深底上。
|
|
- **slide 小节头**(`jc-sp`):`01/02/…` 编号丸 + 题 + 一句注 + 延伸线 ——
|
|
编号是版面语言,不是数据。
|
|
- **逐段入场**(`jc-rise` + `.d1…d6` 延迟):`opacity/transform` 动画,
|
|
**全部包在 `@media (prefers-reduced-motion: no-preference)` 里**(连 `@keyframes`
|
|
一起包 —— 引用未定义的动画名等于没有动画,减弱设置下自然全静)。
|
|
- **流光走线**:两条同 path 的 SVG(`wire` 底线 + `pulse` 虚线),
|
|
`stroke-dasharray` + `stroke-dashoffset` 动画 + `drop-shadow` 发光。
|
|
- **悬停揭示**:卡片 hover 浮起(`transform + box-shadow` 过渡);
|
|
等级卡的口径段 `max-height:0 → hover/focus-within 展开`,加 `tabindex="0"`
|
|
让键盘也进得去;**触屏常开**(`@media (hover: none)` 里展开、藏掉「悬停展开」hint)。
|
|
- **空节自折**:整节只在有数据时出现 —— 包一层 wrapper,
|
|
`:not(:has(.行.v-xxx)) { display:none }`(如 nowwrap/donewrap)。
|
|
没有 NOW 行时「当前这一颗」整节(含小节头)不出,不留空壳。
|
|
|
|
**三条工程账**(都踩过):
|
|
|
|
1. **媒体查询要排在被它覆盖的基础规则之后** —— 同权重下后定义胜。
|
|
竖排断点里写了 `.jc-trace{display:none}`,而 `.jc-trace{display:flex}` 基础规则
|
|
定义在媒体块之后,于是竖排下走线照显:横着一根流光指向右边的空气。
|
|
2. **动效只动 `transform`/`opacity`/`box-shadow`**,别动布局属性;
|
|
`jc-rise` 会在每次 srcdoc 重渲后重播(保存一次播一次)—— 半秒内是 slide 感,
|
|
长了就是折磨,入场动画别超过 0.6s。
|
|
3. 队列丸/信标这类**状态灯**照旧走 `data-class-of`(`v-now` 信标呼吸、`v-done` 绿、
|
|
`v-wait` 暗)—— 「哪颗在办」是数据说的,动画只是把它说响一点。
|
|
|
|
## 9. 自测与浏览器走查
|
|
|
|
包内自测(结构层):模版无 `<script` 无外链;`data-class-of` 不以 `data-slot` 为前提
|
|
(纯色块曾整条静默失色);剧本每个 `view` 都在 viewTemplates 里;绑定键都是
|
|
声明列 / 基座读数 / `case:*`。
|
|
|
|
浏览器走查(版面与交互层,结构断言替代不了):
|
|
|
|
```
|
|
锁定态整篇可读 → 编辑 → 改一格 → 取消(还原) → 再改 + 删一行 + 加一行 → 保存
|
|
→ 件上新值现出、红带收起、类徽/分布条/去向重算 → 卡上提交闸开 → 提交 → 放行
|
|
→ 工作成果里同一份件零输入框零外壳 → 与目标件并排截图比对
|
|
```
|
|
|
|
## 10. 陷阱表
|
|
|
|
| 症状 | 根因 | 出路 |
|
|
|---|---|---|
|
|
| 点「编辑」毫无反应,一切选择器都对 | `instanceof Element` 跨 realm 恒 false;或监听挂在 about:blank 上 | 认能力不认血统;幂等重挂(§3) |
|
|
| 保存时件「闪黑」一下 | srcDoc 换文档整页重载 | 就地换 body.innerHTML(§3) |
|
|
| 就地换了还是「闪一下」(2026-08-03 再报) | innerHTML 仍是**整树重建**:47KB 样式在 body 再解析一遍(head 那份还在,一文档两份)、SVG 全重建、CSS 动画回第 0 帧重起 | 第二层:逐节点 morph(`ViewContainer.replaceInPlace`)—— 一样的节点不碰,样式归位 head 去重,涟漪只落在真改了的格子上;任何一步栽了退回整树重建(闪但不错) |
|
|
| **保存后旧值还摆着一会儿**(真正让人以为没生效的那个) | 保存要跑个来回(POST 落草稿 → 失效 → 重取详情 → 新件推回),而闸在按下那一刻就关了 —— 这段时间屏上是只读那份的**旧值** | 按下保存**先把新值写进只读孪生件**再关闸(`mirrorTwins`),回声到达时 morph 一比已经一样;只镜像格子自己的值,红带/分布条/去向等基座算的数一律等回声 |
|
|
| 镜像/取值找不到孪生件而无声失效 | 渲染那一遍把 `data-slot` **连同槽位一起消费掉**,活 DOM 里两份都没有这个属性 | 认**位置**不认属性:`twinInputs` 把 input 紧接着只读那份写出,`previousElementSibling` 就是孪生件;**验证要在真单的 DOM 上做** —— 合成 fixture 里两份都写了 `data-slot`,照样绿 |
|
|
| 人保存了,一格都没落库 | 保存借道当下那张卡的 blocks | 按 `data-edit` 地址收,授权=台账声明(§2) |
|
|
| 改了类标,分布条/去向不动 | 汇总数是自报的没人重算;或没 repush | 分类列标 `tally` 让基座数;保存后必 repush(§1) |
|
|
| 纯色块整条灰着像没算出来 | `data-class-of` 曾以 `data-slot` 为前提 | 已修;自测钉住 |
|
|
| 屏上多了一行删不掉 | 只给改格子没给删行 | §6-3 增删行 |
|
|
| 空格子显示「无」/上一行的值 | 没挂 `data-blank`;或加行时只清了 input 没清只读份 | §4;addRow 两份都清 |
|
|
| 报告与卡说的判据不一致 | 两处各算各的 | 全部读基座读数,同源(§2) |
|
|
| 归档件上还有编辑闸/胶囊 | 闸没锁在 `editable` 根类下;胶囊没分态 | §6-1、§8 |
|
|
| 版面「像又不像」目标件 | 用拼装件照着做而不是搬 | §7;截图比对 |
|
|
| 人保存后一行的值被另一行盖掉 | **主键格没挂 `data-col`** —— 收上去的行没有钥匙,服务端按「全表一致列」补键,多行对到同一行 | 主键列必挂 `data-col`(它不是编辑面,是行的钥匙);e2e 走 API 自带主键测不出,只有件上真按保存才现形(A2 走查逮过) |
|
|
| 逐行卡片克隆成套娃(第二行长进第一行里) | 渲染器按**第一个同名闭合标签**截行内容 —— `<div data-rows>` 的行模版里嵌了别的 `<div>`,行在半截被断开 | **行容器的标签名不得在行模版内部再出现**:容器换 `<section>`/`<tbody>` 这类行内不会嵌的标签(A2 选型档案卡逮过) |
|
|
| 空格胶囊比同行的数字**往下坠几像素** | 裸 inline-flex 胶囊按 baseline 挂:盒(字 + 内距 + 边框)比字的基线深,行高也随之忽高忽低 | 空格子(`:empty`)转 flex + `align-items:center` + `min-height` 撑住文字那份盒高,胶囊中线对数字中线(A1 逮过,2026-08-03) |
|
|
| 查看面的灰底**滚过一屏就变白** | 画背景的那一格是弹性子项(`flex:0 1 auto`),高度被「看得见的那一屏」封住;内容比一屏长时溢出盒子继续排,而**背景只画在盒子里** | `flex: 1 0 auto`(不许被压 → 盒子至少和内容一样高;内容短时能长满 → 下方不留白边)。同一症状可能有两条独立的路(沙箱内的底色、父文档里的底色),治完一条把另一条也走一遍(2026-08-23 用户报了两次) |
|
|
| 表宽超出屏宽,列被裁掉且**滑不动** | `.jc-tbl-wrap` 写成 `overflow: hidden`(本意是让圆角裁住表头)—— 横向一并封死;列一多、或某列为放下下拉框加宽,人就再也看不到右边那几列 | `overflow-x: auto; overflow-y: hidden`(件是整篇跟着外层滚的,竖向不该再套一层)。抄别包版式时最容易抄丢这一条 —— quote-* 六包一直是 auto,E04/I01 抄漏(2026-08-23 用户报) |
|
|
| 改了 `prd-style.ts`,esbuild 报 `Expected ";" but found …`,报错行离改动处很远 | 整段 CSS 是 **TS 模版字符串**,注释里写了一个反引号就把字符串在那儿截断,后面的 CSS 变成 TS 代码 | **视图文件的注释里不写反引号**(要引 CSS 属性名就裸写 `overflow-x: auto`)。本轮同一处踩了三次 |
|