import type { FormationReportCycle, FormationReportMetricKey, FormationReportRecord } from '@/stores/formationReports'
|
|
export type FormationMetricKey = FormationReportMetricKey
|
|
export interface FormationMetricDefinition {
|
key: FormationMetricKey
|
title: string
|
unit: string
|
values: number[][]
|
}
|
|
export interface FormationCycleFieldDefinition {
|
key: string
|
label: string
|
mode: 'input' | 'raw' | 'auto'
|
}
|
|
export interface FormationReportPrintData {
|
name: string
|
reportId?: number
|
creator?: string
|
createdAt?: string
|
testName: string
|
batteryGroup: string
|
samples: string[]
|
cycles: FormationReportCycle[]
|
cycleFieldValues: Record<string, string>
|
reportFieldValues: Record<string, string>
|
batteryFields: string[]
|
specialFields: string[]
|
metrics: FormationMetricKey[]
|
seriesColors: string[]
|
description: string
|
}
|
|
export const formationMetricDefinitions: FormationMetricDefinition[] = [
|
{ key: 'voltage', title: '电压', unit: 'V', values: [[3.18, 3.35, 3.62, 3.86, 4.02, 4.18, 4.2], [3.2, 3.37, 3.64, 3.88, 4.04, 4.2, 4.22], [3.16, 3.33, 3.6, 3.84, 4.0, 4.16, 4.18]] },
|
{ key: 'current', title: '电流', unit: 'A', values: [[1.2, 1.18, 1.14, 1.08, 0.92, 0.6, 0.2], [1.24, 1.21, 1.17, 1.11, 0.95, 0.63, 0.23], [1.18, 1.16, 1.12, 1.06, 0.9, 0.58, 0.18]] },
|
{ key: 'positiveVoltage', title: '正电位', unit: 'V', values: [[1.82, 1.94, 2.18, 2.42, 2.66, 2.88, 3.02], [1.84, 1.96, 2.2, 2.44, 2.68, 2.9, 3.04], [1.8, 1.92, 2.16, 2.4, 2.64, 2.86, 3.0]] },
|
{ key: 'negativeVoltage', title: '负电位', unit: 'V', values: [[1.36, 1.41, 1.44, 1.46, 1.48, 1.5, 1.52], [1.38, 1.43, 1.46, 1.48, 1.5, 1.52, 1.54], [1.34, 1.39, 1.42, 1.44, 1.46, 1.48, 1.5]] },
|
{ key: 'temperature', title: '温度', unit: '℃', values: [[25, 26, 27, 29, 31, 33, 34], [25, 26.5, 27.5, 29.5, 31.5, 33.5, 34.5], [24.5, 25.5, 26.5, 28.5, 30.5, 32.5, 33.5]] },
|
]
|
|
export const formationFixedReportFields = [
|
{ key: 'Z', label: 'Z', mode: 'raw' as const },
|
{ key: 'H', label: 'H', mode: 'raw' as const },
|
{ key: 'F', label: 'F', mode: 'raw' as const },
|
{ key: 'A', label: 'A', mode: 'raw' as const },
|
{ key: 'B', label: 'B', mode: 'raw' as const },
|
{ key: 'FN', label: 'F/N', mode: 'auto' as const },
|
{ key: 'C', label: 'C', mode: 'auto' as const },
|
]
|
|
export const formationCycleFields: FormationCycleFieldDefinition[] = [
|
{ key: 'method2', label: '测试方法2', mode: 'input' },
|
{ key: 'charge005', label: '0.05C充电终止电压', mode: 'raw' },
|
{ key: 'charge01', label: '0.1C充电终止电压', mode: 'raw' },
|
{ key: 'manual', label: '数字', mode: 'input' },
|
{ key: 'chargeEnergy', label: '每克瓦充电量', mode: 'auto' },
|
{ key: 'chargeEnergyTotal', label: '每克瓦充电量', mode: 'auto' },
|
{ key: 'capacity1v', label: '1.0V容量', mode: 'raw' },
|
{ key: 'capacity1vf', label: '1.0VF完整容量', mode: 'auto' },
|
{ key: 'discharge', label: '拐点容量', mode: 'raw' },
|
{ key: 'dischargeTotal', label: '拐点完整容量', mode: 'auto' },
|
]
|
|
const defaultSeriesColors = ['#3978f6', '#7d42d8', '#f08a3e']
|
|
export function createFormationReportPrintData(record: FormationReportRecord): FormationReportPrintData {
|
return {
|
name: record.name,
|
reportId: record.id,
|
creator: record.creator,
|
createdAt: record.createdAt,
|
testName: record.testName,
|
batteryGroup: record.batteryGroup,
|
samples: [...record.samples],
|
cycles: record.cycles.map(cycle => ({ ...cycle })),
|
cycleFieldValues: { ...(record.cycleFieldValues || {}) },
|
reportFieldValues: { ...(record.reportFieldValues || {}) },
|
batteryFields: [...(record.batteryFields || [])],
|
specialFields: [...(record.specialFields || [])],
|
metrics: record.metrics ? [...record.metrics] : formationMetricDefinitions.map(metric => metric.key),
|
seriesColors: record.seriesColors?.length ? [...record.seriesColors] : [...defaultSeriesColors],
|
description: record.description,
|
}
|
}
|
|
function escapeHtml(value: unknown) {
|
const entities: Record<string, string> = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }
|
return String(value ?? '').replace(/[&<>"']/g, character => entities[character])
|
}
|
|
function safeColor(value: string | undefined, index: number) {
|
return value && /^#[0-9a-f]{6}$/i.test(value) ? value : defaultSeriesColors[index % defaultSeriesColors.length]
|
}
|
|
function timeLabel(index: number) {
|
return `${Math.floor(index / 2)}:${index % 2 ? '30' : '00'}`
|
}
|
|
function rawReportValue(fieldIndex: number, sampleIndex: number) {
|
return (1.02 + fieldIndex * 0.06 + sampleIndex * 0.03).toFixed(2)
|
}
|
|
function rawCycleValue(cycle: number, fieldIndex: number, sampleIndex: number) {
|
return (3.76 + cycle * 0.03 + fieldIndex * 0.018 + sampleIndex * 0.012).toFixed(3)
|
}
|
|
function createChartSvg(metric: FormationMetricDefinition, cycle: FormationReportCycle, data: FormationReportPrintData) {
|
const samples = (data.samples.length ? data.samples : ['样品1', '样品2', '样品3']).slice(0, 3)
|
const cycleOffset = Math.max(0, cycle.cycle - 1) * 0.012
|
const series = samples.map((sample, index) => ({
|
name: sample,
|
color: safeColor(data.seriesColors[index], index),
|
values: (metric.values[index] || metric.values[0]).map(value => Number((value + cycleOffset).toFixed(3))),
|
}))
|
const values = series.flatMap(item => item.values)
|
const dataMax = Math.max(0, ...values)
|
const axisMax = Math.max(metric.unit === '℃' ? Math.ceil(dataMax / 10) * 10 : Math.ceil(dataMax * 10) / 10, 1)
|
const width = 680
|
const height = 260
|
const left = 54
|
const right = 626
|
const top = 32
|
const bottom = 214
|
const plotWidth = right - left
|
const plotHeight = bottom - top
|
const pointCount = Math.max(1, ...series.map(item => item.values.length))
|
const yTicks = Array.from({ length: 6 }, (_, index) => axisMax * index / 5)
|
const yCoordinate = (value: number) => bottom - (value / axisMax) * plotHeight
|
const xCoordinate = (index: number) => pointCount === 1 ? left : left + index * plotWidth / (pointCount - 1)
|
const gridLines = yTicks.map(value => {
|
const y = yCoordinate(value)
|
const label = metric.unit === '℃' ? Math.round(value) : Number(value.toFixed(2))
|
return `<line x1="${left}" y1="${y}" x2="${right}" y2="${y}" stroke="#e8eef4"/><text x="${left - 8}" y="${y + 4}" text-anchor="end">${label}</text><text x="${right + 8}" y="${y + 4}" text-anchor="start">${label}</text>`
|
}).join('')
|
const xLabels = Array.from({ length: pointCount }, (_, index) => `<text x="${xCoordinate(index)}" y="${bottom + 20}" text-anchor="middle">${timeLabel(index)}</text>`).join('')
|
const lines = series.map(item => {
|
const points = item.values.map((value, index) => `${xCoordinate(index)},${yCoordinate(value)}`).join(' ')
|
return `<polyline points="${points}" fill="none" stroke="${item.color}" stroke-width="2.2" stroke-linejoin="round" stroke-linecap="round"/>`
|
}).join('')
|
return `<svg viewBox="0 0 ${width} ${height}" role="img" aria-label="${escapeHtml(metric.title)}曲线">
|
<g fill="#7b8794" font-family="Microsoft YaHei, sans-serif" font-size="10">${gridLines}${xLabels}<text x="${left}" y="18">${escapeHtml(metric.unit)}</text><text x="${right}" y="18" text-anchor="end">${escapeHtml(metric.unit)}</text><text x="${(left + right) / 2}" y="252" text-anchor="middle" fill="#405261">时间/h:mm</text></g>
|
<line x1="${left}" y1="${top}" x2="${left}" y2="${bottom}" stroke="#cbd8e4"/><line x1="${right}" y1="${top}" x2="${right}" y2="${bottom}" stroke="#cbd8e4"/><line x1="${left}" y1="${bottom}" x2="${right}" y2="${bottom}" stroke="#cbd8e4"/>${lines}
|
</svg>`
|
}
|
|
function createPrintHtml(data: FormationReportPrintData) {
|
const samples = data.samples.length ? data.samples : ['样品1', '样品2', '样品3']
|
const columns = [
|
{ id: 1, name: data.testName, samples },
|
{ id: 2, name: `${data.testName} · 对照`, samples: [...samples] },
|
]
|
const reportSamples = columns.flatMap(column => column.samples)
|
const detailFields = [...formationFixedReportFields, ...data.batteryFields.map(field => ({ key: `battery:${field}`, label: field, mode: 'input' as const })), ...data.specialFields.map(field => ({ key: `special:${field}`, label: field, mode: 'input' as const }))]
|
const reportHead = `<tr><th>电池组</th>${columns.map(column => `<th colspan="${column.samples.length}"><span class="group-index">${column.id}</span>${escapeHtml(column.name)}</th>`).join('')}</tr><tr><th>电池号</th>${reportSamples.map(sample => `<th>${escapeHtml(sample)}</th>`).join('')}</tr>`
|
const reportRows = detailFields.map((field, fieldIndex) => {
|
const cells = columns.flatMap((column, columnIndex) => column.samples.map((sample, sampleIndex) => {
|
if (field.mode === 'raw') return `<td>${rawReportValue(fieldIndex, sampleIndex + columnIndex)}</td>`
|
if (field.mode === 'auto') return '<td class="auto-value">自动计算</td>'
|
const key = `${field.key}|${column.id}-${sample}`
|
return `<td>${escapeHtml(data.reportFieldValues[key] || '-')}</td>`
|
}))
|
return `<tr><td>${escapeHtml(field.label)}</td>${cells.join('')}</tr>`
|
}).join('')
|
const samplesHeader = reportSamples.map(sample => `<th>${escapeHtml(sample)}</th>`).join('')
|
const cycleSections = data.cycles.map(cycle => {
|
const cycleRows = [
|
`<tr><td>测试方法</td>${reportSamples.map(() => `<td>${escapeHtml(cycle.method || '-')}</td>`).join('')}</tr>`,
|
...formationCycleFields.map((field, fieldIndex) => `<tr><td>${escapeHtml(field.label)}</td>${reportSamples.map((sample, sampleIndex) => {
|
if (field.mode === 'raw') return `<td>${rawCycleValue(cycle.cycle, fieldIndex, sampleIndex)}</td>`
|
if (field.mode === 'auto') return '<td class="auto-value">自动计算</td>'
|
const key = `${cycle.cycle}|${field.key}|${sample}-${sampleIndex}`
|
return `<td>${escapeHtml(data.cycleFieldValues[key] || '-')}</td>`
|
}).join('')}</tr>`),
|
].join('')
|
const enabledMetrics = formationMetricDefinitions.filter(metric => data.metrics.includes(metric.key))
|
const legend = samples.slice(0, 3).map((sample, index) => `<span><i style="background:${safeColor(data.seriesColors[index], index)}"></i>${escapeHtml(sample)}</span>`).join('')
|
const charts = enabledMetrics.map(metric => `<article class="chart-card"><header><strong>${escapeHtml(metric.title)}</strong><span>${escapeHtml(metric.unit)}</span></header>${createChartSvg(metric, cycle, data)}<div class="chart-inner-legend">${legend}</div></article>`).join('')
|
return `<section class="cycle-section"><h3>循环${cycle.cycle}</h3><table class="cycle-table"><thead><tr><th>指标字段</th>${samplesHeader}</tr></thead><tbody>${cycleRows}</tbody></table><div class="curve-header"><strong>循环${cycle.cycle}曲线</strong><div class="legend">${legend}</div></div><div class="charts">${charts || '<div class="no-chart">当前未启用曲线指标</div>'}</div></section>`
|
}).join('')
|
const savedMeta = data.reportId ? `<span>报表ID <strong>#${data.reportId}</strong></span><span>创建人 <strong>${escapeHtml(data.creator || '-')}</strong></span><span>创建时间 <strong>${escapeHtml(data.createdAt || '-')}</strong></span>` : ''
|
return `<!doctype html><html lang="zh-CN"><head><meta charset="UTF-8"><title>${escapeHtml(data.name)}</title><style>
|
@page{size:A4 landscape;margin:10mm}*{box-sizing:border-box}body{margin:0;color:#26333f;background:#fff;font-family:"Microsoft YaHei",sans-serif;font-size:11px;-webkit-print-color-adjust:exact;print-color-adjust:exact}.report-header{display:flex;align-items:flex-end;justify-content:space-between;gap:20px;margin-bottom:16px;padding-bottom:12px;border-bottom:2px solid #2b78a3}.report-header h1{margin:0;font-size:22px}.report-header p{margin:5px 0 0;color:#71808d}.saved-meta{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:8px 18px;color:#7b8794}.saved-meta strong{margin-left:5px;color:#405261}.section-title,.cycle-section h3{display:flex;align-items:center;gap:8px;height:34px;margin:0;padding:0 10px;border:1px solid #d8e1e9;border-bottom:0;background:#f4f8fb;font-size:13px}.section-title:before,.cycle-section h3:before{width:3px;height:15px;background:#2b78a3;content:""}.section{margin:0 0 16px}.experiment{padding:10px 12px;border:1px solid #d8e1e9;border-bottom:0;color:#52606d}.experiment strong{margin-left:12px;color:#26333f}table{width:100%;border-collapse:collapse;table-layout:fixed}th,td{height:31px;padding:5px 8px;border:1px solid #cbd8e4;text-align:center;overflow-wrap:anywhere}th{color:#fff;background:#3c6497;font-weight:600}.report-table th:first-child,.report-table td:first-child,.cycle-table th:first-child,.cycle-table td:first-child{width:150px;text-align:left}.report-table td:first-child,.cycle-table td:first-child{color:#31404d;background:#f2f7fb;font-weight:600}.group-index{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;margin-right:7px;border:1px solid #cbd8e4;border-radius:50%;color:#647380;background:#fff;font-size:9px}.auto-value{color:#2188f5;font-weight:500}.cycle-section{margin:0 0 18px;break-inside:auto}.cycle-section h3{border-bottom:1px solid #d8e1e9}.curve-header{display:flex;align-items:center;gap:18px;min-height:38px;padding:8px 10px;border:1px solid #d8e1e9;border-bottom:0}.curve-header strong{font-size:13px}.legend{display:flex;flex-wrap:wrap;gap:8px 18px}.legend span,.chart-inner-legend span{display:inline-flex;align-items:center;gap:6px;color:#52606d}.legend i,.chart-inner-legend i{width:12px;height:12px;border:1px solid rgba(38,51,63,.18);display:inline-block}.charts{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;padding:10px;border:1px solid #d8e1e9}.chart-card{position:relative;min-width:0;border:1px solid #d8e1e9;break-inside:avoid}.chart-card header{height:30px;display:flex;align-items:center;justify-content:space-between;padding:0 10px;border-bottom:1px solid #edf1f5}.chart-card header span{color:#8795a3;font-size:10px}.chart-card svg{display:block;width:100%;height:auto}.chart-inner-legend{display:flex;flex-wrap:wrap;gap:5px 12px;padding:0 10px 8px;font-size:9px}.no-chart{grid-column:1/-1;padding:30px;text-align:center;color:#8795a3}.note{padding:10px 12px;border:1px solid #d8e1e9;background:#f7fafc;white-space:pre-wrap}@media print{.section-title,.cycle-section h3,th,.report-table td:first-child,.cycle-table td:first-child{print-color-adjust:exact;-webkit-print-color-adjust:exact}}
|
</style></head><body><header class="report-header"><div><h1>${escapeHtml(data.name)}</h1><p>检测室数据可视化系统 · 化成报表</p></div><div class="saved-meta">${savedMeta}</div></header><section class="section"><h2 class="section-title">报表数据</h2><div class="experiment">测试<strong>${escapeHtml(data.testName)}</strong><span class="experiment-gap">电池组<strong>${escapeHtml(data.batteryGroup)}</strong></span></div><table class="report-table"><thead>${reportHead}</thead><tbody>${reportRows || '<tr><td colspan="2" class="empty">暂无报表字段</td></tr>'}</tbody></table></section>${cycleSections || '<section class="section"><div class="note">暂无化成循环数据</div></section>'}<section class="section"><h2 class="section-title">报表说明</h2><div class="note">${escapeHtml(data.description || '暂无说明')}</div></section></body></html>`
|
}
|
|
export function openFormationReportPrint(data: FormationReportPrintData) {
|
const popup = window.open('', '_blank', 'width=1200,height=820')
|
if (!popup) return false
|
popup.document.open()
|
popup.document.write(createPrintHtml(data))
|
popup.document.close()
|
window.setTimeout(() => {
|
popup.focus()
|
popup.print()
|
}, 250)
|
return true
|
}
|