| | |
| | | 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 { getFormationPrintVersion, type FormationItem, type FormationField, type SavedFormationReport } from '@/api/formationReports' |
| | | import { formationFieldLabel, formationSourceValue } from './formationReportFields' |
| | | import { formationCellKey, formationInputValue, formationNumberText } from './formationReportCalculations' |
| | | import { formationCurveMetrics } from './formationCycleChart' |
| | | import { formationChargeSteps, formationStepGroups, formationStepSeries, formationStepChartOption } from './formationStepChart' |
| | | import { openSavedReportPrint } from './savedReportPrint' |
| | | |
| | | use([LineChart, GridComponent, LegendComponent, SVGRenderer]) |
| | |
| | | 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) => { |
| | | 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 ? [ |
| | | ['充电工步号', ...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))], |
| | | ] : [] |
| | | for (const field of record.settings.fields.filter(field => field.scope === (cycle ? 'cycle' : 'detail'))) { |
| | | rows.push([formationFieldLabel(field), ...group.map(item => value(item, () => |
| | | record.settings.values[formationValueKey(item, cycle, field)] ?? formationSourceValue(item, cycle, field)))]) |
| | | /** 打印只读指定版本的字段、人工输入及计算结果,绝不向当前来源补数。 */ |
| | | export function formationSavedReportHtml(record: SavedFormationReport) { |
| | | const items = record.items |
| | | const value = (item:FormationItem,cycle:number,field:FormationField) => { |
| | | if(cycle && !item.cycleNos.includes(cycle))return '无该循环' |
| | | if(field.kind==='calculated')return formationNumberText(record.calculatedValues?.[formationCellKey(item,cycle,field.key)]) |
| | | return field.kind==='metric'?formationSourceValue(item,cycle,field):formationInputValue(item,field,record.settings.values,cycle) |
| | | } |
| | | const tables = (cycle:number) => { |
| | | const result:string[]=[] |
| | | // 每组最多四个来源列,完整输出所有电池;动态终止电压行保持全表相同序号。 |
| | | const stepCount=cycle?Math.max(0,...items.map(item=>formationChargeSteps(item,cycle).length)):0 |
| | | for(let start=0;start<items.length;start+=4){ |
| | | const group=items.slice(start,start+4),rows:unknown[][]=[] |
| | | const fields=record.settings.fields.filter(field=>field.scope===(cycle?'cycle':'detail')) |
| | | const append=(field:FormationField)=>rows.push([formationFieldLabel(field),...group.map(item=>value(item,cycle,field))]) |
| | | if(cycle){ |
| | | fields.filter(field=>['method','method2'].includes(field.key)).forEach(append) |
| | | for(let index=0;index<stepCount;index++)rows.push(['终止电压',...group.map(item=>{ |
| | | if(!item.cycleNos.includes(cycle))return '无该循环' |
| | | const step=formationChargeSteps(item,cycle)[index] |
| | | return step?`${formationNumberText(step.endVoltage)}${step.endVoltage==null?'':' V'}(工步${step.stepNo})`:'—' |
| | | })]) |
| | | } |
| | | 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(cell).join('')}</tr>`).join('')}</tbody></table>`) |
| | | fields.filter(field=>!cycle||!['method','method2'].includes(field.key)).forEach(append) |
| | | result.push(`<table><thead><tr><th>${cycle?`循环${cycle}`:'报表数据'}</th>${group.map(item=>`<th>${escapeHtml(item.source.name)}<br>${escapeHtml(item.source.batteryGroup)} / ${escapeHtml(item.source.batteryNo)}</th>`).join('')}</tr></thead><tbody>${rows.map(row=>`<tr>${row.map(cell=>`<td>${escapeHtml(cell)}</td>`).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 => { |
| | | 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>` |
| | | const cycles=record.cycleNos.map(cycle=>{ |
| | | const groups=formationStepGroups.map(group=>{ |
| | | const series=formationStepSeries(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)}</span>`).join('') |
| | | const charts=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({...formationStepChartOption(metric,items,cycle,group.key,record.settings),tooltip:{show:false}});svg=chart.renderToSVGString()}finally{chart.dispose()} |
| | | return `<article class="chart-card"><h3>${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>当前未启用曲线指标</p>'}</div></section>` |
| | | }).join('') |
| | | const unknown=items.flatMap(item=>(item.chargeSteps?.[cycle]??[]).filter(step=>!step.direction||step.direction==='UNKNOWN').map(step=>`${item.source.name}-${item.source.batteryNo} 工步${step.stepNo}`)) |
| | | return `<section data-cycle="${cycle}"><h2>循环${cycle}</h2>${tables(cycle)}${unknown.length?`<p>方向未识别,未纳入统计:${escapeHtml(unknown.join(';'))}</p>`:''}${groups}</section>` |
| | | }).join('') |
| | | const date = (time: number) => escapeHtml(new Date(time).toLocaleString()) |
| | | 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}.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>` |
| | | @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;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;gap:6px;overflow-wrap:anywhere;max-width:100%}.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 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} · 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><h2>报表数据</h2>${tables(0)}<p>拐点容量、拐点完整容量为演示占位。</p>${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) |
| | | return openSavedReportPrint(() => getFormationPrintVersion(id, versionNo), formationSavedReportHtml) |
| | | } |