import type { QualityComponentDefinition, QualityProps } from '../core/types';
|
|
import { parseColumnSpec } from '../core/column-spec';
|
import { cssStyle, escapeHtml } from '../core/html';
|
import { QUALITY_ICONS } from '../core/icons';
|
import { QUALITY_CATEGORY } from '../core/types';
|
|
/** 样品信息默认展示的字段,值全部来自报告上下文 */
|
const SAMPLE_FIELDS: { expr: string; label: string }[] = [
|
{ label: '样品编号', expr: '{{report.sampleNo}}' },
|
{ label: '产品名称', expr: '{{report.productName}}' },
|
{ label: '规格型号', expr: '{{report.spec}}' },
|
{ label: '批次号', expr: '{{report.batchNo}}' },
|
{ label: '检验日期', expr: '{{report.inspectDate}}' },
|
{ label: '检验员', expr: '{{report.inspector}}' },
|
];
|
|
const CELL_BORDER = '1px solid #d9d9d9';
|
|
function textOf(value: unknown): string {
|
return String(value ?? '').trim();
|
}
|
|
/**
|
* 出表用的字段:填了 fields 就整块照它来,否则用默认字段。
|
* <p>
|
* 与分组检验项表共用同一套 `字段名=绑定表达式` 语法(见 {@link parseColumnSpec}),
|
* 免得同一个「键值对列表」长出两种写法。合并标记 `#` 在这里没有意义,解析出来也只是被丢掉,
|
* 所以 validate 里明确拦下,不让用户以为自己写对了。
|
*/
|
function effectiveFields(properties: QualityProps): { expr: string; label: string }[] {
|
const custom = parseColumnSpec(textOf(properties.fields));
|
return custom.items.length > 0
|
? custom.items.map((field) => ({ label: field.label, expr: field.expr }))
|
: SAMPLE_FIELDS;
|
}
|
|
function buildRows(
|
pairsPerRow: number,
|
showBorder: boolean,
|
fields: { expr: string; label: string }[],
|
): string {
|
const perRow = Math.max(pairsPerRow, 1);
|
const border = showBorder ? CELL_BORDER : 'none';
|
const rows: string[] = [];
|
for (let index = 0; index < fields.length; index += perRow) {
|
const rowFields = fields.slice(index, index + perRow);
|
// 末行凑不满一组时,让最后一个值格横向吃掉余下的列。照原样铺满格子的话,
|
// 右侧会秃出几个只有边框的空格(字段数不是「每行组数」整数倍时必然出现)。
|
const tailColspan = rowFields.length < perRow ? (perRow - rowFields.length) * 2 + 1 : 1;
|
const cells = rowFields
|
.map((field, position) => {
|
const span = position === rowFields.length - 1 && tailColspan > 1 ? ` colspan="${tailColspan}"` : '';
|
return (
|
`<td style="${cssStyle({
|
border,
|
padding: '5px 8px',
|
background: '#fafafa',
|
'font-weight': 600,
|
width: '90px',
|
})}">${escapeHtml(field.label)}</td>` +
|
`<td${span} style="${cssStyle({ border, padding: '5px 8px' })}">${escapeHtml(field.expr)}</td>`
|
);
|
})
|
.join('');
|
rows.push(`<tr>${cells}</tr>`);
|
}
|
return rows.join('');
|
}
|
|
export const sampleInfoDefinition: QualityComponentDefinition = {
|
type: 'SampleInfo',
|
name: 'sampleInfo',
|
label: '样品信息',
|
category: QUALITY_CATEGORY.HEADER,
|
icon: QUALITY_ICONS.sample,
|
aiHint: '样品与物料的基本信息块:产品名称、规格、批号、数量、生产日期、有效日期等键值对。',
|
defaults: { title: '样品信息', pairsPerRow: 2, showBorder: true },
|
propertySchema: [
|
{ key: 'title', label: '区块标题', type: 'string', defaultValue: '样品信息' },
|
{
|
key: 'pairsPerRow',
|
label: '每行字段数',
|
type: 'number',
|
defaultValue: 2,
|
tip: '一行放几组「字段名 + 值」',
|
},
|
{ key: 'showBorder', label: '显示边框', type: 'boolean', defaultValue: true },
|
{
|
key: 'fields',
|
label: '字段定义(覆盖默认字段)',
|
type: 'text',
|
tip:
|
'留空则用默认的六项。格式:字段名=绑定表达式,多项用 | 分隔,' +
|
'如 样品编号={{report.sampleNo}}|产品名称={{report.productName}}|生产日期={{report.produceDate}}。' +
|
'每行放几组仍由上面的「每行字段数」控制;这里不支持 # 合并标记',
|
},
|
],
|
dataSchema: SAMPLE_FIELDS.map((field) => ({
|
// 表达式形如 {{report.sampleNo}},去掉两层花括号即绑定路径
|
key: field.expr.slice(2, -2),
|
label: field.label,
|
type: 'string' as const,
|
bindable: true,
|
})),
|
buildContent: (properties) => {
|
const rawTitle = String(properties.title ?? '').trim();
|
const heading = rawTitle
|
? `<div style="${cssStyle({
|
'font-size': '13px',
|
'font-weight': 600,
|
margin: '0 0 4px',
|
})}">${escapeHtml(rawTitle)}</div>`
|
: '';
|
const table = `<table style="${cssStyle({
|
width: '100%',
|
'border-collapse': 'collapse',
|
'font-size': '12px',
|
})}">${buildRows(
|
Number(properties.pairsPerRow ?? 2),
|
properties.showBorder !== false,
|
effectiveFields(properties),
|
)}</table>`;
|
return {
|
tagName: 'div',
|
components: heading + table,
|
style: { margin: '0 0 10px' },
|
};
|
},
|
validate: (properties) => {
|
const parsed = parseColumnSpec(textOf(properties.fields));
|
const errors = [...parsed.errors];
|
if (parsed.items.some((field) => field.merge)) {
|
errors.push(
|
'样品信息的「字段定义」不支持 # 合并标记(那是分组检验项表标组名用的),请去掉字段名前的 #',
|
);
|
}
|
return errors;
|
},
|
};
|