# 质检报告「表达力缺口」设计:可自定列的检验表 + 可配字段的样品信息 > 状态:**待确认**。本文只做方案,不动代码。 > 触发背景:AI 导入已能正确选到 `GroupedQualityTable`(选型问题已修,见开发进度 §二十四), > 但用户比对原件后指出「草稿仍不像原件」。经核查,剩下的差距**全部是「可用积木的表达力不够」**, > 不是识别问题——文字都抽出来了,是组件装不下。 --- ## 1. 证据:差距到底在哪 用户原件《百事模版.docx》(142373 字节)与当前草稿的逐项对比: | 原件里有的 | 草稿里的下场 | 原因 | |---|---|---| | 表格列「检测方法」(`GB 5009.3-2016` / `ANA.MTH-000034`) | **整列丢弃** | `GroupedQualityTable` 的 7 列写死在代码常量里 | | 表格列「结论」(`合格Checkout`) | 整列丢弃 | 同上 | | 两级表头「检验结果 \| 结论」 | 拍平 | 组件只支持单层表头 | | 样品信息 7 项(产品名称 / 产品数量 / 规格 / 生产日期 / 批号 / 土豆品种 / 有效日期) | 只剩 6 项,且与原件的 6 项**不是同一批** | `SampleInfo` 的字段写死在代码常量里 | | 首行栏目名 `产品名称Material Names` | 模型拿通用组件硬凑成独立一行 | 没有任何组件能承接「表头里的栏目名」 | 两处「写死」的位置: - `mom-pro2-before/src/components/quality/inspection/grouped-quality-table.ts` 的 `COLUMNS` 常量(7 项) - `mom-pro2-before/src/components/quality/header/sample-info.ts` 的 `SAMPLE_FIELDS` 常量(6 项) --- ## 2. 两条硬约束(先讲清楚,它们决定方案形状) ### 2.1 属性协议只认标量——不能用「数组属性」 `QualityProps = Record`, `QualityFieldType` 只有 `boolean | enum | number | string | text` 五种。 属性最终落到画布节点的 `attributes` 上(写作 `data-qc-`),而 `ReportTemplateSchema.QualityComponentNode.attributes` 也是字符串映射。 ⇒ **新增一个「列数组」属性类型要动协议本身**(属性面板控件、画布序列化、schema 契约测试、 两侧渲染器),代价远超收益。列定义必须能被编码成**一个标量字符串**。 ### 2.2 渲染器不认识组件类型——不需要动渲染器 这是好消息,已核查: - 后端 `HtmlRenderer.renderAttributes` 只做两件与组件无关的事:跳过 `data-quality-type` / `data-qc-*`、 拦掉 `on*` 与危险协议;**没有任何按组件类型分支的逻辑**,也没有属性白名单。 - 后端 `CanvasSafety.validate` 是黑名单(`script` 等可执行标签、事件属性、整段 CSS), 不是标签/属性白名单 ⇒ `colspan` / `rowspan` 本来就放行。 - 行循环靠 `data-qc-repeat` / `data-qc-repeat-row` 这两个通用属性驱动。 ⇒ **本轮不需要改 `HtmlRenderer`、`BASE_CSS`、`CanvasSafety`**,也不需要改 schema 版本号。 组件侧改动集中在「注册表里的一个 definition」。这也正是质量组件协议的设计初衷 (`core/types.ts` 开头:「新增质量组件=注册一个新 definition,不需要改动 GrapesJS 集成代码」)。 --- ## 3. 方案 ### 3.1 总原则:不新增组件,给现有组件加「可选覆盖」属性 **推荐做法**:给 `GroupedQualityTable` 加两个**可选**属性 `columns` / `headerSpans`;给 `SampleInfo` 加一个**可选**属性 `fields`。 - 属性**缺席时行为逐字不变**(`buildContent` 里回落到现有常量)⇒ 存量和已发布模板零影响, 不需要数据迁移,不需要改 `defaults`。 - **不新增第三个检验表组件**。当前 13 个组件里已经有两个检验表(`QualityTable` / `GroupedQualityTable`), 再加一个就是三个同类竞争,**极可能把刚刚修好的选型稳定性再次打破**(参见 §二十四: 选型不稳的根因就是提示词里多了一处指向平铺表的措辞,组件数量增加是同一个机理)。 用户的收益来自「列能改」,不是「多一个组件」。 ### 3.2 列/字段描述的语法 **单行字符串**,列与列之间用 `|` 分隔,每列写作 `列标题=绑定表达式`: ``` 序号={{index}}|检验项目={{item.itemName}}|检测方法={{item.checkMethod}}|标准要求={{item.standardValue}}|实测值={{item.actualValue}}|单位={{item.unit}}|判定={{item.resultText}} ``` 三条约束,都来自 2.1: 1. **必须单行**:这串东西最终是画布节点的一个 HTML 属性值,换行在属性里是合法的但极易被 编辑器/序列化环节改写,统一按单行处理。 2. **`=` 只作第一个分隔符**:标题与表达式都不允许含 `=`,表达式里的 `{{...}}` 原样保留。 3. **标题里不允许出现 `|`**;需要竖线时用全角 `|`。 **纵向合并列(分组表专用)**:列标题前加 `#` 表示「这一列要跨住整组」,沿用现有 `GroupedQualityTable` 的 `rowspan` / `hidden` 机制: ``` #检验项目={{item.itemName}}|子项={{item.group.childName}}|... ``` ### 3.3 两级表头(可选) `headerSpans` 描述**上面那一层**表头,单元格写作 `标题^跨越列数`: ``` 检验结果^3|结论^2 ``` 下层表头由 `columns` 里各列的标题自动拼出(顺序即列顺序)。约束: **各单元格跨越列数之和必须等于 `columns` 的列数**,否则保存时给出精确报错: > 表头跨列数合计 5,检验表的实际列数为 7,两者必须相等。请检查「表头跨列」里各段末尾的 `^` 数字。 (按 `error-message-precision`:给出具体维度 + 两侧实际值 + 可执行动作。) ### 3.4 样品信息字段可配 同一个语法,用在 `SampleInfo` 上: ``` 样品编号={{report.sampleNo}}|产品名称={{report.productName}}|规格={{report.spec}}|生产日期={{report.produceDate}}|批号={{report.batchNo}}|土豆品种={{report.potatoVariety}}|有效日期={{report.expireDate}} ``` 每行放几组仍由现有的 `pairsPerRow` 属性控制,布局逻辑不变。 --- ## 4. 必须一并修的数据侧缺口:「检测方法」目前不在渲染作用域 这是本轮最容易漏掉、且**不修则新功能形同虚设**的一条。 | 层 | 现状 | |---|---| | MES 单据侧 `MesQcReportItemRespDTO` | **已有** `checkMethod`(检测方法)、`tool`(检测工具) | | qcreport 引擎 `InspectionItem` | **没有**这两个字段 | | `ReportContext.itemMap()` 下发的前端作用域 | 只有 `index / itemCode / itemName / standardValue / actualValue / unit / requirement / group.* / upperLimit / lowerLimit / result / resultText / remark` | ⇒ 就算列定义里写了 `检测方法={{item.checkMethod}}`,渲染期也取不到值,只会按「绑定取不到值」 记一条缺口、留一片空白。 **须补的链路**(缺一不可,且两侧字段名必须逐字一致): 1. `MesQcReportContextMapper`:把 DTO 的 `checkMethod`(必要时含 `tool`)映射进 `InspectionItem` 2. `InspectionItem`:加字段 + 进 `copy()` 3. `ReportContext.itemMap()`:下发到前端作用域 4. `ReportContextCodec`:冻结快照的读写(否则历史报告重渲染时丢值) 5. 前端 `engine/context.ts` 的 `ReportContextItem`:镜像同名字段 6. 对拍 fixture 与相关单测 **注意**:`resultText`(判定)是**引擎算出来的**(`InspectionItem.applyResult`),不是 MES 给的; 「结论」列如果要的是人工拍的结论,还要确认它是否已有下发(`MesQcReportContextMapper` 注释提到 「人工拍的检验判定,与引擎算出的 result/resultText/conclusion 分列两处」)。这一条**待确认后再定**。 --- ## 5. 被否的备选 | 备选 | 否掉的原因 | |---|---| | 列的列定义用 JSON 字符串 | 模型要输出转义后的嵌套 JSON(`"columns":"[{\"label\":…}]"`),实测这类转义是模型出错的重灾区;属性面板里人也没法手改。单行语法信息量相同而两边都好写。 | | 给 `QualityFieldType` 加数组类型 | 要动协议、属性面板、画布序列化、schema 契约测试,代价远超收益(见 2.1)。 | | 新增独立的「可自定列检验表」组件 | 与 `QualityTable` / `GroupedQualityTable` 三方竞争,选型稳定性风险大(见 3.1);用户还要维护两套同类组件。 | | 做一个可视化的列编辑器(增删拖拽) | 收益确实更高,但成本是前者的数倍,且需要新的属性面板控件类型。**建议先用手填字符串验证价值**,真有需求再迭代——那时字符串语法已经是现成的存储格式,不用迁移。 | | 把 `SampleInfo` 的 `dataSchema` 改成动态 | 绑定选择器只能枚举静态 `dataSchema`,动态化会让「选数据字段」入口失效。**保持静态**:`columns`/`fields` 属性设为**不可绑定**(`bindable` 不声明),由人手写完整串。 | --- ## 6. AI 侧的影响(本轮的重点风险) 新属性必须让模型能填,否则 AI 导入仍产不出这些列。 1. **清单自动带上**:`buildQualityComponentCatalog()` 把 `propertySchema` 原样映射成 `fields`, 新属性声明为 `type: 'text'` 即自动进提示词,**不需要改 `catalog.ts`**。 用现有 `text` 类型(而不是自造一个新类型)还能让属性面板直接渲染成多行输入框,零改动。 2. **提示词要补规则**:需要新增一条抽取规则,说明「原件里检验表除标准列之外还有别的列 (检测方法、结论、备注等)时,用 `columns` 把这些列按 `标题=绑定` 写进去」, 并给出可用绑定路径白名单。 3. **`aiHint` 必须重写**:`GroupedQualityTable` 的 hint 要说明「列与本组件默认不同时用 `columns` 覆盖」, 且**不能**让模型以为「列不一致就要换组件」——否则又会回到选型不稳。 4. **验收口径**:AI 导入的验收必须包含「列是否与原件的列**逐列对齐**」, 而不只是「type 选对了」。这是本轮从用户反馈里学到的:`GroupedQualityTable` 选对不等于报告对了。 --- ## 7. 影响面(预估改动清单) **前端(`mom-pro2-before/src/components/quality/`)** - `inspection/grouped-quality-table.ts`:`propertySchema` 加 `columns` / `headerSpans`(`type: 'text'`); `buildContent` 里解析,缺席回落现有 `COLUMNS`;`validate` 加跨列数校验 - `header/sample-info.ts`:`propertySchema` 加 `fields`(`type: 'text'`);`buildContent` 同理回落 - 新增一个**共用的解析函数**(列说明串 → 列定义数组),两个组件共用,避免两份解析逻辑漂移 - `aiHint` 与 `validate` 的文案 **后端(`yudao-module-qcreport`)** - 数据侧 6 处,见 §4 - 提示词:`QcReportTemplatePromptBuilder.EXTRACTION_RULES` 加一条规则 - **不需要**动 `HtmlRenderer` / `BASE_CSS` / `CanvasSafety` / schema 版本号 **不动**:`docs/sql/config_export_all_*.sql`(无表结构、无配置数据变更)。 --- ## 8. 兼容性 - 新属性都是**可选**且**不写进 `defaults`** ⇒ 已保存模板的 `attributes` 里没有这个键, `buildContent` 走回落分支,产物逐字不变。 - `schemaVersion` 保持 `1.1`:`ReportTemplateSchemaCompatibilityTest` 守的是「旧 JSON 读得出」, 加属性不影响;`QualityComponentNode.attributes` 是字符串映射,新键天然兼容。 - 已发布版本(冻结快照)不重算,不受影响。 --- ## 9. 验证计划 1. 单测:两类组件各覆盖「属性缺席 → 产物与改前逐字一致」(防回归的**第一优先级**用例)、 「属性在场 → 列数/列序/表头跨列正确」、跨列数不匹配的报错文案。 2. 解析函数的纯函数单测:非法输入(缺 `=`、空标题、`|` 出现在标题里)各自报什么错。 3. 前后端一致性:新列产出的 HTML 要过 `FrontendConformanceTest` 与 `CanvasSafetyTest`; `rowspan`/`hidden` 的分组机制在覆盖列之后仍要工作。 4. 数据侧:`{{item.checkMethod}}` 在真实质检单上取得到值(不起缺口告警)。 5. AI 导入:真实文件 + 空 hint,检查**列逐列对齐**,不只看 type。 6. 存量回归:`mvn -o -pl yudao-module-qcreport -am test` 全绿;前端 `pnpm typecheck`。 --- ## 10. 明确不做 - 不做可视化列编辑器(先验证字符串方案的价值) - 不新增检验表组件 - 不改属性协议(不加数组类型) - 不改 `HtmlRenderer` / `BASE_CSS` / `CanvasSafety` - 不动 schema 版本号、不做数据迁移 - 不改 `docs/sql/config_export_all_*.sql` --- ## 11. 待裁决 1. **`headerSpans`(两级表头)是否本轮就做**?不做的话「检验结果 | 结论」这层仍还原不了, 但列覆盖已经能解决「检测方法/结论列丢失」这个更大的缺口。 2. **「结论」列取哪个值**?人工拍的判定(`MesQcReportContextMapper` 里提到的那一处)还是 引擎算的 `resultText`?取错会让报告结论与单据结论不一致,属于业务口径,需你确认。 3. **`tool`(检测工具)是否一并下发**?原件没有这一列,先不下发,需要时再加。 4. **样品信息的默认字段要不要保持现状**(6 项)?改了默认值会让**新建**的样品信息块与现在不同。 建议保持不变,只让「可配」成为用户能改的能力。