import type { EChartsOption } from 'echarts';
|
|
import type { StabilityAnalysisRow, StabilityExpectedPeriod } from '#/api/x/lims/wendingxing';
|
|
export interface StabilityChartSeries {
|
data: Array<null | number>;
|
id: string;
|
kind: 'limit' | 'record';
|
name: string;
|
points: Array<null | StabilityAnalysisRow>;
|
}
|
|
export interface StabilityChartModel {
|
categories: string[];
|
item_code: string;
|
item_id: string;
|
item_name: string;
|
key: string;
|
lower_limit: string;
|
series: StabilityChartSeries[];
|
standard_id: string;
|
text_only: boolean;
|
unit: string;
|
upper_limit: string;
|
}
|
|
interface CategoryEntry {
|
key: string;
|
label: string;
|
order: number;
|
}
|
|
export function buildStabilityChartModels(rows: StabilityAnalysisRow[], _expectedPeriods: StabilityExpectedPeriod[] = []): StabilityChartModel[] {
|
const groups = new Map<string, StabilityAnalysisRow[]>();
|
for (const row of rows) {
|
const signature = [row.xiangmu_bianma || row.xiangmu_id, row.biaozhun_id || '', row.xiaxian || '', row.shangxian || '', row.unit || ''].join('|');
|
groups.set(signature, [...(groups.get(signature) || []), row]);
|
}
|
|
return [...groups.entries()].map(([key, groupRows]) => buildChartModel(key, groupRows));
|
}
|
|
function buildChartModel(key: string, rows: StabilityAnalysisRow[]): StabilityChartModel {
|
const first = rows[0]!;
|
const categories = buildCategories(rows);
|
const textOnly = isTextDataType(first.shuju_leixing);
|
if (textOnly) {
|
return {
|
categories: categories.map((category) => category.label),
|
item_code: first.xiangmu_bianma,
|
item_id: first.xiangmu_id,
|
item_name: first.xiangmu_mingcheng,
|
key,
|
lower_limit: first.xiaxian || '',
|
series: [],
|
standard_id: first.biaozhun_id || '',
|
text_only: true,
|
unit: first.unit || '',
|
upper_limit: first.shangxian || '',
|
};
|
}
|
|
const categoryIndex = new Map(categories.map((category, index) => [category.key, index]));
|
const series: StabilityChartSeries[] = [];
|
const limitSeries: StabilityChartSeries[] = [];
|
if (first.xiaxian_value !== null && Number.isFinite(first.xiaxian_value)) {
|
limitSeries.push(createLimitSeries('lower-limit', '下限', first.xiaxian_value, categories.length));
|
}
|
if (first.shangxian_value !== null && Number.isFinite(first.shangxian_value)) {
|
limitSeries.push(createLimitSeries('upper-limit', '上限', first.shangxian_value, categories.length));
|
}
|
|
const duplicateBatches = duplicateBatchNumbers(rows);
|
const byBatch = new Map<string, StabilityAnalysisRow[]>();
|
for (const row of rows) {
|
byBatch.set(row.batch_key, [...(byBatch.get(row.batch_key) || []), row]);
|
}
|
for (const [batchKey, batchRows] of byBatch) {
|
const values: Array<null | number> = categories.map(() => null);
|
const points: Array<null | StabilityAnalysisRow> = categories.map(() => null);
|
const latestByCategory = new Map<string, StabilityAnalysisRow>();
|
for (const row of batchRows) {
|
const periodKey = categoryKey(row);
|
const current = latestByCategory.get(periodKey);
|
if (!current || (row.jieguo_luru_riqi || '').localeCompare(current.jieguo_luru_riqi || '') >= 0) {
|
latestByCategory.set(periodKey, row);
|
}
|
}
|
for (const [periodKey, row] of latestByCategory) {
|
const index = categoryIndex.get(periodKey);
|
if (index === undefined) continue;
|
values[index] = row.numeric_result;
|
points[index] = row;
|
}
|
const sample = batchRows[0]!;
|
series.push({
|
data: values,
|
id: batchKey,
|
kind: 'record',
|
name: duplicateBatches.has(sample.pihao) ? `${sample.jihua_bianhao} / ${sample.pihao}` : sample.pihao,
|
points,
|
});
|
}
|
series.push(...limitSeries);
|
|
return {
|
categories: categories.map((category) => category.label),
|
item_code: first.xiangmu_bianma,
|
item_id: first.xiangmu_id,
|
item_name: first.xiangmu_mingcheng,
|
key,
|
lower_limit: first.xiaxian || '',
|
series,
|
standard_id: first.biaozhun_id || '',
|
text_only: false,
|
unit: first.unit || '',
|
upper_limit: first.shangxian || '',
|
};
|
}
|
|
function buildCategories(rows: StabilityAnalysisRow[]): CategoryEntry[] {
|
const entries = new Map<string, CategoryEntry>();
|
for (const row of rows) {
|
const key = categoryKey(row);
|
if (!entries.has(key)) {
|
entries.set(key, {
|
key,
|
label: formatPeriod(row),
|
order: Number.isFinite(row.zhouqi_order) ? row.zhouqi_order : Number.MAX_SAFE_INTEGER,
|
});
|
}
|
}
|
return [...entries.values()].sort((left, right) => left.order - right.order || left.key.localeCompare(right.key));
|
}
|
|
function categoryKey(row: StabilityAnalysisRow) {
|
return `${row.zhouqi}|${row.zhouqi_danwei}`;
|
}
|
|
function formatPeriod(row: StabilityAnalysisRow) {
|
return formatPeriodValues(row.zhouqi, row.zhouqi_danwei);
|
}
|
|
function formatPeriodValues(period: string, unit: string) {
|
return Number(period) === 0 ? '0' : `${formatDisplayNumber(period)}${formatPeriodUnit(unit)}`;
|
}
|
|
export function formatDisplayNumber(value: string) {
|
const normalized = String(value ?? '').trim();
|
if (!normalized) return '-';
|
if (!/^[+-]?\d+(?:\.\d+)?$/.test(normalized)) return normalized;
|
return normalized.replace(/(\.\d*?[1-9])0+$|\.0+$/, '$1');
|
}
|
|
export function formatPeriodUnit(value: string) {
|
const normalized = String(value ?? '').trim();
|
const unitMap: Record<string, string> = {
|
d: '天',
|
day: '天',
|
days: '天',
|
h: '小时',
|
hour: '小时',
|
hours: '小时',
|
m: '月',
|
month: '月',
|
months: '月',
|
w: '周',
|
week: '周',
|
weeks: '周',
|
y: '年',
|
year: '年',
|
years: '年',
|
};
|
return unitMap[normalized.toLocaleLowerCase()] || normalized || '/';
|
}
|
|
function createLimitSeries(id: string, name: string, value: number, length: number): StabilityChartSeries {
|
return {
|
data: Array.from({ length }, () => value),
|
id,
|
kind: 'limit',
|
name,
|
points: Array.from({ length }, () => null),
|
};
|
}
|
|
function duplicateBatchNumbers(rows: StabilityAnalysisRow[]) {
|
const plansByBatch = new Map<string, Set<string>>();
|
for (const row of rows) {
|
const plans = plansByBatch.get(row.pihao) || new Set<string>();
|
plans.add(row.jihua_id);
|
plansByBatch.set(row.pihao, plans);
|
}
|
return new Set([...plansByBatch.entries()].filter(([, plans]) => plans.size > 1).map(([batch]) => batch));
|
}
|
|
function isTextDataType(dataType: string) {
|
return /文本|文字|定性|字符|string|text/i.test(dataType || '');
|
}
|
|
export function createStabilityChartOption(model: StabilityChartModel, title: string): EChartsOption {
|
const seriesById = new Map(model.series.map((series) => [series.id, series]));
|
return {
|
animationDuration: 300,
|
grid: { bottom: 76, containLabel: true, left: 28, right: 44, top: 72 },
|
legend: { bottom: 8, type: 'scroll' },
|
series: model.series.map((series) => {
|
if (series.kind === 'limit') {
|
const color = series.id === 'upper-limit' ? '#ef4444' : '#f59e0b';
|
return {
|
connectNulls: false,
|
data: series.data,
|
endLabel: { color, formatter: ({ value }: { value: unknown }) => String(value), show: true },
|
id: series.id,
|
itemStyle: { color },
|
lineStyle: { color, type: 'dashed', width: 1.5 },
|
name: series.name,
|
showSymbol: false,
|
type: 'line',
|
};
|
}
|
return {
|
connectNulls: false,
|
data: series.data,
|
id: series.id,
|
lineStyle: { width: 2 },
|
name: series.name,
|
showSymbol: true,
|
symbol: 'circle',
|
symbolSize: 7,
|
type: 'line',
|
};
|
}),
|
title: { left: 'center', text: title, textStyle: { fontSize: 18, fontWeight: 600 } },
|
tooltip: {
|
trigger: 'axis',
|
formatter: (params: any) => {
|
const list = Array.isArray(params) ? params : [params];
|
const lines = [`周期:${list[0]?.axisValueLabel || ''}`];
|
for (const item of list) {
|
const source = seriesById.get(String(item.seriesId));
|
if (!source || source.kind === 'limit') {
|
lines.push(`${item.marker || ''}${item.seriesName}:${item.value ?? '-'}`);
|
continue;
|
}
|
const point = source.points[item.dataIndex];
|
if (!point) continue;
|
lines.push(
|
`${item.marker || ''}${item.seriesName}:${point.raw_result || '-'}`,
|
`考察编号:${point.jihua_bianhao || '-'}`,
|
`品名:${point.yangpin_mingcheng || '-'}`,
|
`上下限:${point.xiaxian || '-'} ~ ${point.shangxian || '-'}`,
|
`单位:${point.unit || '-'}`,
|
`录入日期:${point.jieguo_luru_riqi || '-'}`,
|
);
|
}
|
return lines.join('<br/>');
|
},
|
},
|
xAxis: { boundaryGap: false, data: model.categories, type: 'category' },
|
yAxis: { name: model.unit, scale: true, type: 'value' },
|
};
|
}
|