import { init, use } from 'echarts/core'
|
import { LineChart } from 'echarts/charts'
|
import { GridComponent, LegendComponent } from 'echarts/components'
|
import { SVGRenderer } from 'echarts/renderers'
|
import { formationValueKey, getFormationPrintVersion, type FormationItem, type SavedFormationReport } from '@/api/formationReports'
|
import { formationChargeChoice, formationFieldLabel, formationSourceValue } from './formationReportFields'
|
import { formationChartOption, formationCurveMetrics, formationSeriesColor, formationSeriesName } from './formationCycleChart'
|
import { formationChargeSteps, formationStepGroups, formationStepSeries, formationStepChartOption } from './formationStepChart'
|
import { openSavedReportPrint } from './savedReportPrint'
|
import { reportDetailColumn, reportDetailsHtml } from './reportTestDetails'
|
|
use([LineChart, GridComponent, LegendComponent, SVGRenderer])
|
function escapeHtml(value: unknown) {
|
const entities: Record<string, string> = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }
|
return String(value ?? '').replace(/[&<>"']/g, character => entities[character]!)
|
}
|
|
function printHtml(record: SavedFormationReport) {
|
const colors = record.settings.seriesColors ?? {}
|
const items = [...new Set(record.items.map(item => item.source.selection.testId))].flatMap(id => record.items.filter(item => item.source.selection.testId === id))
|
const cell = (value: unknown) => `<td>${escapeHtml(value == null || value === '' ? '缺失' : value)}</td>`
|
const tables = (cycle: number) => {
|
if (!cycle) return reportDetailsHtml(items.map(item => reportDetailColumn(item.source)), record.settings.sourceDetailOverrides)
|
const result: string[] = []
|
// 每组最多四列,按原测试分组顺序打印全部矩阵;长表重复列头并允许跨页。
|
for (let start = 0; start < items.length; start += 4) {
|
const group = items.slice(start, start + 4)
|
const value = (item: FormationItem, read: () => unknown) => cycle && !item.cycleNos.includes(cycle) ? '无该循环' : read()
|
const rows: unknown[][] = cycle && record.curveMode !== 'actual-steps' ? [
|
['充电工步号', ...group.map(item => value(item, () => formationChargeChoice(item, cycle, record.settings.chargeStepSelections ?? {})?.stepNo ?? '未识别'))],
|
['充电工步名称', ...group.map(item => value(item, () => formationChargeChoice(item, cycle, record.settings.chargeStepSelections ?? {})?.name || '未选择'))],
|
['终止电压 (V)', ...group.map(item => value(item, () => formationChargeChoice(item, cycle, record.settings.chargeStepSelections ?? {})?.endVoltage))],
|
] : []
|
if (cycle && record.curveMode === 'actual-steps') {
|
const steps = group.map(item => formationChargeSteps(item, cycle, record.settings.manualChargeSteps))
|
const count = Math.max(1, ...steps.map(values => values.length))
|
for (let index = 0; index < count; index++) rows.push([`充电工步${index + 1} / 终止电压 (V)`, ...group.map((item, column) => value(item, () => {
|
const step = steps[column]?.[index]
|
if (!step) return index ? '—' : (item.chargeSteps?.[cycle]?.length ? '未识别充电工步' : '缺工步')
|
return `工步${step.stepNo} · ${step.name || '未命名工步'} · ${step.endVoltage ?? '缺失'}${step.endVoltage == null ? '' : ' V'}`
|
}))])
|
}
|
for (const field of record.settings.fields.filter(field => field.scope === 'cycle')) {
|
rows.push([formationFieldLabel(field), ...group.map(item => value(item, () =>
|
record.settings.values[formationValueKey(item, cycle, field)] ?? formationSourceValue(item, cycle, field)))])
|
}
|
// 循环指标缺失继续使用缺失标记,人工详情由独立详情表保留空字符串。
|
result.push(`<table class="matrix-table"><thead><tr><th>${cycle ? `循环${cycle} / 测试` : '测试'}</th>${group.map(item => `<th>${escapeHtml(item.source.name)}</th>`).join('')}</tr><tr><th>电池组 / 电池号</th>${group.map(item => `<th>${escapeHtml(item.source.batteryGroup)} / ${escapeHtml(item.source.batteryNo)}</th>`).join('')}</tr></thead><tbody>${rows.map(row => `<tr>${row.map(value => cell(value)).join('')}</tr>`).join('')}</tbody></table>`)
|
}
|
return result.join('')
|
}
|
const legend = items.map(item => `<span><i style="background:${formationSeriesColor(item, record.items.indexOf(item), colors)}"></i>${escapeHtml(formationSeriesName(item))}</span>`).join('')
|
const cycles = record.cycleNos.map(cycle => {
|
if (record.curveMode === 'actual-steps') {
|
const groups = formationStepGroups.map(group => {
|
const series = formationStepSeries(record.items, cycle, group.key, record.settings)
|
const legend = series.map(item => `<span${item.hidden ? ' class="hidden-series"' : ''}><i style="background:${escapeHtml(item.color)}"></i>${escapeHtml(item.name)}${item.hidden ? '(已隐藏)' : ''}</span>`).join('')
|
const available = series.some(item => item.records.length)
|
const charts = available ? formationCurveMetrics.filter(metric => (record.settings.metrics ?? formationCurveMetrics.map(value => value.key)).includes(metric.key)).map(metric => {
|
const chart = init(null, undefined, { renderer: 'svg', ssr: true, width: 900, height: 300 })
|
let svg: string
|
try { chart.setOption({ ...formationStepChartOption(metric, record.items, cycle, group.key, record.settings), tooltip: { show: false } }); svg = chart.renderToSVGString() }
|
finally { chart.dispose() }
|
return `<article class="chart-card"><h3>循环${cycle} · ${group.title}工步 · ${escapeHtml(metric.title)} (${escapeHtml(metric.unit)})</h3>${svg}<div class="legend">${legend}</div></article>`
|
}).join('') : ''
|
return `<section data-group="${group.key}"><h3>${group.title}工步曲线</h3><div class="charts">${charts || `<p>${available ? '当前未启用曲线指标' : '所选工步暂无已保存实时数据'}</p>`}</div></section>`
|
}).join('')
|
return `<section class="cycle-section" data-cycle="${cycle}"><h2>循环${cycle}</h2>${tables(cycle)}${groups}</section>`
|
}
|
const available = record.items.some(item => item.curves?.[cycle]?.records.length)
|
const charts = available ? formationCurveMetrics.filter(metric => (record.settings.metrics ?? formationCurveMetrics.map(item => item.key)).includes(metric.key)).map(metric => {
|
const chart = init(null, undefined, { renderer: 'svg', ssr: true, width: 900, height: 300 })
|
let svg: string
|
try { chart.setOption({ ...formationChartOption(metric, record.items, cycle, colors), tooltip: { show: false } }); svg = chart.renderToSVGString() }
|
finally { chart.dispose() }
|
return `<article class="chart-card"><h3>循环${cycle} · ${escapeHtml(metric.title)} (${escapeHtml(metric.unit)})</h3>${svg}<div class="legend">${legend}</div></article>`
|
}).join('') : ''
|
return `<section class="cycle-section" data-cycle="${cycle}"><h2>循环${cycle}</h2>${tables(cycle)}<div class="charts">${charts || `<p>${available ? '当前未启用曲线指标' : record.items.some(item => item.curves) ? '该循环暂无已保存实时数据' : '该版本未保存曲线数据'}</p>`}</div></section>`
|
}).join('')
|
const date = (time: number) => escapeHtml(new Date(time).toLocaleString())
|
return `<!doctype html><html lang="zh-CN"><head><meta charset="UTF-8"><title>${escapeHtml(record.name)} - V${record.versionNo}</title><style>
|
@page{size:A4 landscape;margin:10mm}*{box-sizing:border-box}body{margin:0;padding:16px;font:11px "Microsoft YaHei",sans-serif;color:#26333f;background:#fff;letter-spacing:0;print-color-adjust:exact;-webkit-print-color-adjust:exact}
|
.report-header{display:flex;flex-wrap:wrap;gap:16px;justify-content:space-between;border-bottom:2px solid #2b78a3;padding-bottom:12px;margin-bottom:16px}.report-header>div{min-width:0;max-width:100%}h1{font-size:22px;margin:0;overflow-wrap:anywhere}.saved-meta{display:flex;flex-wrap:wrap;gap:8px 18px;max-width:50%;overflow-wrap:anywhere}h2{font-size:14px;background:#f4f8fb;padding:10px;border:1px solid #d8e1e9;break-after:avoid}h3{margin:0;padding:8px 12px;font-size:13px}table{width:100%;table-layout:fixed;border-collapse:collapse;margin:0 0 16px}thead{display:table-header-group}tr{break-inside:avoid}td,th{border:1px solid #cbd8e4;padding:7px;white-space:pre-wrap;overflow-wrap:anywhere;text-align:center}th{background:#3c6497;color:#fff}td:first-child,th:first-child{width:150px;text-align:left}td:first-child{background:#f4f8fb}.charts{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}.chart-card{min-width:0;border:1px solid #d8e1e9;break-inside:avoid}.chart-card svg{display:block;width:100%;height:auto}.legend{display:flex;flex-wrap:wrap;gap:8px 16px;padding:10px}.legend span{display:flex;align-items:flex-start;gap:6px;overflow-wrap:anywhere;max-width:100%;break-inside:avoid}.hidden-series{text-decoration:line-through;opacity:.55}.legend i{flex:0 0 12px;height:12px;margin-top:2px}.note{white-space:pre-wrap;overflow-wrap:anywhere}
|
@media(max-width:680px){.charts{grid-template-columns:1fr}.saved-meta{max-width:100%}}@media print{body{padding:0}.charts{display:block}.chart-card{margin-bottom:12px}.report-header{break-inside:avoid}}
|
</style></head><body><header class="report-header"><div><h1>${escapeHtml(record.name)}</h1><p>检测室数据可视化系统 · 化成报表</p></div><div class="saved-meta"><span>报表ID #${record.id}</span><span>版本 V${record.versionNo}</span><span>创建人 ${escapeHtml(record.creator)}</span><span>创建时间 ${date(record.createdAt)}</span><span>版本操作人 ${escapeHtml(record.operatorName)}</span><span>版本时间 ${date(record.versionCreatedAt)}</span><span>修改说明 ${escapeHtml(record.changeSummary)}</span></div></header><section><h2>测试详情</h2>${tables(0)}</section>${cycles}<section><h2>报表说明</h2><div class="note">${escapeHtml(record.description || '-')}</div></section></body></html>`
|
}
|
|
/** 指定当前或历史保存版本打印,不读取页面草稿和来源测试。 */
|
export function openFormationSavedReportPrint(id: number, versionNo: number) {
|
return openSavedReportPrint(() => getFormationPrintVersion(id, versionNo), printHtml)
|
}
|