/**
|
* 组件列定义 / 表头跨列的字符串解析。
|
* <p>
|
* 「列」这件事必须能被编码成**一个标量字符串**:组件业务属性的类型只有
|
* boolean / enum / number / string / text(见 {@link QualityFieldSchema}),
|
* 最终还会落到画布节点的 attributes 上,而那里也是字符串映射。
|
* 用 JSON 编码列数组会要求模型输出转义后的嵌套 JSON,是它出错的重灾区;
|
* 一行 `标题=绑定|标题=绑定` 信息量相同,模型、人都好写。
|
* <p>
|
* 必须单行:这串东西最终是 HTML 属性值,换行在属性里合法但容易被
|
* 编辑器/序列化环节改写,统一按单行处理。
|
* <p>
|
* 解析放在这里而不是各组件内部,是因为校验列与表头跨列都依赖同一套语法,
|
* 两份实现迟早漂移,而漂移的表现是「列数对不上」这种很难查的渲染事故。
|
*/
|
|
/** 一列:表头文字 + 行内取值表达式 */
|
export interface QualityColumnSpec {
|
/** 表头文字,已剥掉合并标记前缀 */
|
label: string;
|
/** 行内取值表达式,如 {{item.itemName}} */
|
expr: string;
|
/** 是否纵向合并跨住整组(标题前写了 #) */
|
merge: boolean;
|
}
|
|
/** 上层表头的一个单元格 */
|
export interface QualityHeaderSpan {
|
label: string;
|
span: number;
|
}
|
|
/** 解析结果:能解析出多少算多少,错误逐条回报,由调用方决定是否拦下保存 */
|
export interface QualitySpecParseResult<T> {
|
items: T[];
|
errors: string[];
|
}
|
|
/** 列与列之间的分隔符 */
|
const SEGMENT_SEPARATOR = '|';
|
|
/** 合并列的标记前缀 */
|
const MERGE_MARK = '#';
|
|
/** 表头跨列的计数标记 */
|
const SPAN_MARK = '^';
|
|
/**
|
* 解析列定义串。
|
*
|
* @param spec 形如 `序号={{index}}|#检验项目={{item.itemName}}|子项={{item.group.childName}}`
|
* @returns 列定义与错误清单;空串返回空数组,由调用方回落到组件默认列
|
*/
|
export function parseColumnSpec(spec: string | undefined): QualitySpecParseResult<QualityColumnSpec> {
|
const errors: string[] = [];
|
const items: QualityColumnSpec[] = [];
|
const segments = splitSegments(spec);
|
segments.forEach((segment, index) => {
|
const position = `第 ${index + 1} 段「${segment}」`;
|
const separatorAt = segment.indexOf('=');
|
if (separatorAt < 0) {
|
errors.push(`${position}缺少「=」,每一列都要写成「列标题=绑定表达式」`);
|
return;
|
}
|
const rawLabel = segment.slice(0, separatorAt).trim();
|
const expr = segment.slice(separatorAt + 1).trim();
|
const merge = rawLabel.startsWith(MERGE_MARK);
|
const label = merge ? rawLabel.slice(MERGE_MARK.length).trim() : rawLabel;
|
if (!label) {
|
errors.push(`${position}的列标题是空的,请补上打印在表头上的文字`);
|
return;
|
}
|
if (!expr) {
|
errors.push(`${position}的绑定表达式是空的,请补上「=」后面的内容,如 {{item.itemName}}`);
|
return;
|
}
|
items.push({ label, expr, merge });
|
});
|
return { items, errors };
|
}
|
|
/**
|
* 解析上层表头串。
|
*
|
* @param spec 形如 `检验结果^5|结论^1`;写不出 `^N` 的按 1 列算,靠总数校验兜底
|
* @param columnCount 下层实际列数,用来校验各段跨列数之和
|
*/
|
export function parseHeaderSpans(
|
spec: string | undefined,
|
columnCount: number,
|
): QualitySpecParseResult<QualityHeaderSpan> {
|
const errors: string[] = [];
|
const items: QualityHeaderSpan[] = [];
|
const segments = splitSegments(spec);
|
// 没填就是「不要上层表头」,不是「跨列数合计为 0」——不能拿它去撞总数校验
|
if (segments.length === 0) {
|
return { items, errors };
|
}
|
segments.forEach((segment, index) => {
|
const position = `第 ${index + 1} 段「${segment}」`;
|
const markAt = segment.lastIndexOf(SPAN_MARK);
|
const rawLabel = (markAt < 0 ? segment : segment.slice(0, markAt)).trim();
|
const rawSpan = markAt < 0 ? '' : segment.slice(markAt + 1).trim();
|
if (!rawLabel) {
|
errors.push(`${position}的表头文字是空的,请补上跨越这几列的分组标题`);
|
return;
|
}
|
const span = rawSpan ? Number(rawSpan) : 1;
|
if (!Number.isInteger(span) || span < 1) {
|
errors.push(`${position}的跨列数「${rawSpan}」不是正整数,请写成如 检验结果^5 的形式`);
|
return;
|
}
|
items.push({ label: rawLabel, span });
|
});
|
if (errors.length > 0) {
|
return { items, errors };
|
}
|
const total = items.reduce((sum, item) => sum + item.span, 0);
|
if (total !== columnCount) {
|
errors.push(
|
`表头跨列数合计 ${total},检验表的实际列数为 ${columnCount},两者必须相等。` +
|
`请检查「表头跨列」里各段末尾 ^ 后面的数字`,
|
);
|
}
|
return { items, errors };
|
}
|
|
/** 按 `|` 切段:空段(首尾多写了分隔符)直接跳过,不当作错误,避免手滑就报错 */
|
function splitSegments(spec: string | undefined): string[] {
|
if (!spec) {
|
return [];
|
}
|
return spec
|
.split(SEGMENT_SEPARATOR)
|
.map((segment) => segment.trim())
|
.filter((segment) => segment.length > 0);
|
}
|