13 小时以前 35722562e9e13f0504acc15b740d042ecb810199
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
/**
 * 组件列定义 / 表头跨列的字符串解析。
 * <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);
}