编辑 | blame | 历史 | 原始文档

质检报告「表达力缺口」设计:可自定列的检验表 + 可配字段的样品信息

状态:**待确认**。本文只做方案,不动代码。
触发背景: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.tsCOLUMNS 常量(7 项)
  • mom-pro2-before/src/components/quality/header/sample-info.tsSAMPLE_FIELDS 常量(6 项)

2. 两条硬约束(先讲清楚,它们决定方案形状)

2.1 属性协议只认标量——不能用「数组属性」

QualityProps = Record<string, boolean | number | string | undefined>
QualityFieldType 只有 boolean | enum | number | string | text 五种。
属性最终落到画布节点的 attributes 上(写作 data-qc-<key>),而
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 这两个通用属性驱动。

本轮不需要改 HtmlRendererBASE_CSSCanvasSafety,也不需要改 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. 标题里不允许出现 |;需要竖线时用全角

纵向合并列(分组表专用):列标题前加 # 表示「这一列要跨住整组」,沿用现有
GroupedQualityTablerowspan / 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.tsReportContextItem:镜像同名字段
  6. 对拍 fixture 与相关单测

注意resultText(判定)是**引擎算出来的**(InspectionItem.applyResult),不是 MES 给的;
「结论」列如果要的是人工拍的结论,还要确认它是否已有下发(MesQcReportContextMapper 注释提到
「人工拍的检验判定,与引擎算出的 result/resultText/conclusion 分列两处」)。这一条**待确认后再定**。


5. 被否的备选

备选 否掉的原因
列的列定义用 JSON 字符串 模型要输出转义后的嵌套 JSON("columns":"[{\"label\":…}]"),实测这类转义是模型出错的重灾区;属性面板里人也没法手改。单行语法信息量相同而两边都好写。
QualityFieldType 加数组类型 要动协议、属性面板、画布序列化、schema 契约测试,代价远超收益(见 2.1)。
新增独立的「可自定列检验表」组件 QualityTable / GroupedQualityTable 三方竞争,选型稳定性风险大(见 3.1);用户还要维护两套同类组件。
做一个可视化的列编辑器(增删拖拽) 收益确实更高,但成本是前者的数倍,且需要新的属性面板控件类型。**建议先用手填字符串验证价值**,真有需求再迭代——那时字符串语法已经是现成的存储格式,不用迁移。
SampleInfodataSchema 改成动态 绑定选择器只能枚举静态 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.tspropertySchemacolumns / headerSpanstype: 'text');
    buildContent 里解析,缺席回落现有 COLUMNSvalidate 加跨列数校验
  • header/sample-info.tspropertySchemafieldstype: 'text');buildContent 同理回落
  • 新增一个**共用的解析函数**(列说明串 → 列定义数组),两个组件共用,避免两份解析逻辑漂移
  • aiHintvalidate 的文案

后端(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.1ReportTemplateSchemaCompatibilityTest 守的是「旧 JSON 读得出」,
    加属性不影响;QualityComponentNode.attributes 是字符串映射,新键天然兼容。
  • 已发布版本(冻结快照)不重算,不受影响。

9. 验证计划

  1. 单测:两类组件各覆盖「属性缺席 → 产物与改前逐字一致」(防回归的**第一优先级**用例)、
    「属性在场 → 列数/列序/表头跨列正确」、跨列数不匹配的报错文案。
  2. 解析函数的纯函数单测:非法输入(缺 =、空标题、| 出现在标题里)各自报什么错。
  3. 前后端一致性:新列产出的 HTML 要过 FrontendConformanceTestCanvasSafetyTest
    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 项)?改了默认值会让**新建**的样品信息块与现在不同。
    建议保持不变,只让「可配」成为用户能改的能力。