import { init, use } from 'echarts/core'
|
import { LineChart } from 'echarts/charts'
|
import { GridComponent, MarkLineComponent } from 'echarts/components'
|
import { SVGRenderer } from 'echarts/renderers'
|
import { getSplicePrintVersion, type SavedSpliceReport } from '@/api/spliceReports'
|
import { openSavedReportPrint } from './savedReportPrint'
|
import css from './spliceSavedReport.css?inline'
|
|
use([LineChart, GridComponent, MarkLineComponent, SVGRenderer])
|
const metrics = [
|
{ key: 'finalVoltage1', title: '最终电压1', unit: 'V' },
|
{ key: 'energyEfficiency', title: '能量效率', unit: '%' },
|
]
|
function escapeHtml(value: unknown) {
|
const entities: Record<string, string> = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }
|
return String(value ?? '').replace(/[&<>"']/g, character => entities[character]!)
|
}
|
const cell = (value: unknown) => escapeHtml(value == null || value === '' ? '缺失' : value)
|
const numeric = (value: unknown) => typeof value === 'number' && Number.isFinite(value) ? value : null
|
const display = (value: unknown) => { const number = numeric(value); return number === null ? '缺失' : String(Number(number.toFixed(4))) }
|
|
/** 重开与打印共用冻结统计及人工字段,绝不重算或读取来源测试。 */
|
export function spliceSavedReportBody(record: SavedSpliceReport) {
|
const sources = [...new Set(record.segments.map(segment => segment.source.selection.testId))]
|
const batteries = [...new Set(record.segments.map(segment => segment.source.batteryNo))]
|
const color = (battery: string) => {
|
const index = batteries.indexOf(battery)
|
const saved = record.settings.seriesColors?.[index]
|
return saved && /^#[\da-f]{6}$/i.test(saved) ? saved : ['#3978f6', '#109854', '#d25b45', '#7754a8'][index % 4]!
|
}
|
const label = (testId: number) => `来源${sources.indexOf(testId) + 1}`
|
const fields = [...(record.settings.batteryFields ?? []), ...(record.settings.specialFields ?? [])]
|
// 早期版本的五类实时显隐键不适用于循环统计,不能据此补造测量曲线。
|
const savedMetrics = record.settings.metrics
|
const visibleMetrics = savedMetrics?.some(key => metrics.some(metric => metric.key === key)) || savedMetrics?.length === 0
|
? metrics.filter(metric => savedMetrics!.includes(metric.key)) : metrics
|
const sourceRows = record.segments.map(segment => `<tr><td>${label(segment.source.selection.testId)}</td><td>${cell(segment.source.name)}</td><td>${cell(segment.source.group)}</td><td>${cell(segment.source.batteryNo)}</td><td>${segment.originalCycleNos.map(no => `${no} → ${segment.offset + no}`).join('、') || '无已保存循环'}</td></tr>`).join('')
|
const fieldRows = batteries.flatMap(battery => fields.map(field => `<tr><td>${cell(battery)}</td><td>${cell(field)}</td><td>${cell(record.settings.detailFieldValues?.[field]?.[battery])}</td></tr>`)).join('')
|
const statistics = sources.map(testId => {
|
const segments = record.segments.filter(segment => segment.source.selection.testId === testId)
|
const rows = segments.flatMap(segment => segment.originalCycleNos.map(no => {
|
const stat = segment.statistics[String(no)] ?? {}
|
return `<tr><td>${cell(segment.source.batteryNo)}</td><td>${no}</td><td>${segment.offset + no}</td>${metrics.map(metric => `<td>${display(stat[metric.key])}</td>`).join('')}</tr>`
|
})).join('')
|
const name = `${label(testId)} · ${segments[0]!.source.name}`
|
return `<section class="splice-statistics"><h2>${cell(name)}</h2><table><thead><tr><th colspan="5">${cell(name)}</th></tr><tr><th>电池号</th><th>原循环号</th><th>拼接循环号</th><th>最终电压1 (V)</th><th>能量效率 (%)</th></tr></thead><tbody>${rows || '<tr><td colspan="5">无已保存循环</td></tr>'}</tbody></table></section>`
|
}).join('')
|
const cycles = [...new Set(record.segments.flatMap(segment => segment.originalCycleNos.map(no => segment.offset + no)))].sort((a, b) => a - b)
|
const charts: string[] = []
|
// 长分段按实际统计点分图,多电池按四条分组;不补缺失循环,所有分组均保留来源标识。
|
for (let start = 0; start < cycles.length; start += 40) {
|
const range = cycles.slice(start, start + 40)
|
for (let batteryStart = 0; batteryStart < batteries.length; batteryStart += 4) {
|
const group = batteries.slice(batteryStart, batteryStart + 4)
|
const segments = record.segments.filter(segment => group.includes(segment.source.batteryNo) && segment.originalCycleNos.some(no => range.includes(segment.offset + no)))
|
const boundaries = [...new Map(segments.map(segment => [segment.source.selection.testId, { name: label(segment.source.selection.testId), xAxis: Math.max(range[0]!, segment.offset + segment.originalCycleNos[0]!) }])).values()]
|
const legend = group.map(battery => `<span><i style="background:${color(battery)}"></i>${cell(battery)}</span>`).join('')
|
const sourceLegend = [...new Set(segments.map(segment => `${label(segment.source.selection.testId)} · ${segment.source.name}`))].map(name => `<span>${cell(name)}</span>`).join('')
|
for (const metric of visibleMetrics) {
|
const chart = init(null, undefined, { renderer: 'svg', ssr: true, width: 900, height: 300 })
|
let svg: string
|
try {
|
chart.setOption({ animation: false, grid: { top: 42, right: 28, bottom: 45, left: 72 },
|
xAxis: { type: 'value', min: range[0]! - 0.5, max: range.at(-1)! + 0.5, minInterval: 1, name: '拼接循环号', nameLocation: 'middle', nameGap: 28, axisLabel: { hideOverlap: true, formatter: (value: number) => Number.isInteger(value) ? String(value) : '' } },
|
yAxis: { type: 'value', scale: true, name: metric.unit },
|
series: group.map((battery, index) => ({ name: battery, type: 'line', connectNulls: false, showSymbol: true, symbolSize: 5, itemStyle: { color: color(battery) },
|
data: segments.filter(segment => segment.source.batteryNo === battery).flatMap(segment => segment.originalCycleNos.filter(no => range.includes(segment.offset + no)).map(no => [segment.offset + no, numeric(segment.statistics[String(no)]?.[metric.key])])),
|
markLine: index === 0 ? { silent: true, symbol: 'none', lineStyle: { color: '#82909b', type: 'dashed' }, label: { formatter: '{b}', position: 'end' }, data: boundaries } : undefined,
|
})),
|
})
|
svg = chart.renderToSVGString()
|
} finally { chart.dispose() }
|
charts.push(`<article class="splice-chart"><h3>${metric.title} (${metric.unit}) · 拼接循环 ${range[0]}–${range.at(-1)}</h3>${svg}<div class="splice-legend">${legend}</div><div class="splice-legend">${sourceLegend}</div></article>`)
|
}
|
}
|
}
|
const date = (value: unknown) => escapeHtml(new Date(Number(value)).toLocaleString())
|
return `<div class="splice-snapshot"><header><h1>${cell(record.name)}</h1><p>拼接报表 #${record.id} · 版本 V${record.versionNo}</p><p>创建人 ${cell(record.creator)} · 创建时间 ${date(record.createdAt)}</p><p>版本操作人 ${cell(record.operatorName)} · 版本时间 ${date(record.versionCreatedAt)}</p><p>修改说明 ${cell(record.changeSummary)}</p></header><section><h2>来源测试顺序与循环对应</h2><table><thead><tr><th>顺序</th><th>来源测试</th><th>电池组</th><th>参与电池号</th><th>原循环号 → 拼接循环号</th></tr></thead><tbody>${sourceRows}</tbody></table></section><section><h2>报表字段</h2>${fieldRows ? `<table><thead><tr><th>电池号</th><th>字段</th><th>保存值</th></tr></thead><tbody>${fieldRows}</tbody></table>` : '<p>无已保存字段</p>'}</section><section><h2>统计图表</h2>${charts.join('') || `<p>${cycles.length ? '当前未启用统计图指标' : '无已保存循环统计'}</p>`}</section>${statistics}<section><h2>报表说明</h2><div class="splice-note">${cell(record.description)}</div></section></div>`
|
}
|
|
/** 先打开窗口,再使用独立导出权限读取指定版本;页面草稿不进入打印内容。 */
|
export function openSpliceSavedReportPrint(id: number, versionNo: number) {
|
return openSavedReportPrint(() => getSplicePrintVersion(id, versionNo), record => `<!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}body{margin:0;padding:16px;background:white}@media print{body{padding:0}}${css}</style></head><body>${spliceSavedReportBody(record)}</body></html>`)
|
}
|