yeg
2026-09-08 dc1c47ea58be0c321c7a62d4435c3bbd498c2948
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
import { init, use } from 'echarts/core'
import { LineChart } from 'echarts/charts'
import { GridComponent } from 'echarts/components'
import { SVGRenderer } from 'echarts/renderers'
import { compareSelectionKey, getComparePrintVersion, type CompareItem, type SavedCompareReport } from '@/api/compareReports'
import type { CompareMetricKey } from '@/stores/compareReports'
import { compareChartOption, compareSeriesName } from './compareReportChart'
import { compareFields, compareFieldLabel, compareBusinessFieldValue } from './compareReportFields'
import { openSavedReportPrint } from './savedReportPrint'
import { compareSourceKey, compareDetailSources, compareSourceFields, compareSourceValue, compareHasRangeDetailValues } from './compareSourceDetails'
 
use([LineChart, GridComponent, SVGRenderer])
 
export interface CompareMetricDefinition {
  key: CompareMetricKey
  title: string
  unit: string
  values: number[][]
}
 
// 未交付的其他页面仍复用演示数组;正式对比页面与打印只读取后端测量。
export const compareMetricDefinitions: CompareMetricDefinition[] = [
  { key: 'voltage', title: '电压', unit: 'V', values: [[1.25, 1.34, 1.46, 1.42, 1.48, 1.55, 1.58], [1.12, 1.20, 1.31, 1.28, 1.36, 1.31, 1.28], [1.38, 1.43, 1.49, 1.47, 1.51, 1.56, 1.62]] },
  { key: 'current', title: '电流', unit: 'mA', values: [[1.10, 1.15, 1.23, 1.20, 1.26, 1.30, 1.38], [0.94, 1.02, 1.11, 1.06, 1.10, 1.08, 1.04], [1.18, 1.24, 1.29, 1.27, 1.34, 1.39, 1.48]] },
  { key: 'positiveVoltage', title: '正电位', unit: 'V', values: [[1.30, 1.37, 1.48, 1.45, 1.52, 1.57, 1.61], [1.16, 1.25, 1.34, 1.30, 1.39, 1.35, 1.30], [1.40, 1.45, 1.50, 1.48, 1.54, 1.59, 1.65]] },
  { key: 'negativeVoltage', title: '负电位', unit: 'V', values: [[0.82, 0.90, 0.98, 0.95, 1.02, 1.08, 1.11], [0.66, 0.74, 0.82, 0.78, 0.86, 0.83, 0.80], [0.90, 0.96, 1.04, 1.01, 1.08, 1.13, 1.18]] },
  { key: 'temperature', title: '温度', unit: '℃', values: [[25, 27, 30, 29, 32, 35, 38], [25, 26, 29, 31, 34, 37, 41], [25, 28, 31, 30, 33, 36, 39]] },
]
 
function escapeHtml(value: unknown) {
  const entities: Record<string, string> = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }
  return String(value ?? '').replace(/[&<>"']/g, character => entities[character])
}
 
function chartSvg(metric: CompareMetricDefinition, record: SavedCompareReport) {
  const chart = init(null, undefined, { renderer: 'svg', ssr: true, width: 900, height: 300 })
  try {
    chart.setOption({ ...compareChartOption(metric, record.items, record.settings.seriesColors, record.settings.hiddenSeriesKeys), tooltip: { show: false } })
    return chart.renderToSVGString()
  } finally { chart.dispose() }
}
 
function printHtml(record: SavedCompareReport) {
  const cell = (value: unknown) => `<td>${escapeHtml(value === '' || value == null ? '-' : value)}</td>`
  const fields = compareFields(record.settings)
  const showSource = record.settings.sourceDetailOverrides !== undefined
  const bySource = !compareHasRangeDetailValues(record.settings)
  const detailSources = bySource ? compareDetailSources(record.items) : record.items.map(item => item.source)
  const sources = compareDetailSources(record.items)
  const sourceFields = compareSourceFields(sources, record.settings.sourceDetailOverrides)
  const compareRows = record.items.map(item => {
    const source = item.source
    return `<tr>${[source.name, source.type, source.batteryGroup, source.batteryNo, source.process, source.selection.cycleNo].map(cell).join('')}</tr>`
  }).join('')
  const fieldValue = (field: typeof fields[number], item: CompareItem) => {
    const value = record.settings.detailFieldValues[field.name]?.[bySource ? compareSourceKey(item.source) : compareSelectionKey(item.source.selection)]
    // 与重开表单相同:未填写才使用冻结业务字段,显式清空仍为空。
    return value === undefined ? compareBusinessFieldValue(field, item) : value
  }
  // 横向每组最多四项,所有组顺序输出,长表可跨页并重复列头。
  const detailTables: string[] = []
  if (showSource) for (let start = 0; start < sources.length; start += 4) {
    const columns = sources.slice(start, start + 4)
    const rows = [
      ['测试', ...columns.map(source => source.name)],
      ['电池组', ...columns.map(source => source.batteryGroup)],
      ['电池号', ...columns.map(source => source.batteryNo)],
      ['实验', ...columns.map(source => compareSourceValue(source, record.settings.sourceDetailOverrides))],
      ...sourceFields.map(field => [`${field.name}(${field.category === 'battery' ? '电池组详情' : '特殊详情'})`, ...columns.map(source => compareSourceValue(source, record.settings.sourceDetailOverrides, field))]),
    ]
    detailTables.push(`<table class="detail-table source-detail-table"><thead><tr><th>字段</th>${columns.map((_, index) => `<th>来源电池 ${start + index + 1}</th>`).join('')}</tr></thead><tbody>${rows.map(row => `<tr>${row.map(cell).join('')}</tr>`).join('')}</tbody></table>`)
  }
  for (let start = 0; (fields.length || !showSource) && start < detailSources.length; start += 4) {
    const items = detailSources.slice(start, start + 4).map(source => record.items.find(item => compareSelectionKey(item.source.selection) === compareSelectionKey(source.selection))!)
    const rows = [
      ['测试', ...items.map(item => item.source.name)],
      ['电池组', ...items.map(item => item.source.batteryGroup)],
      ['电池号', ...items.map(item => item.source.batteryNo)],
      ...(!bySource ? [['循环 / 工步', ...items.map(item => `${item.source.selection.cycleNo} / ${item.source.process}`)]] : []),
      ...fields.map(field => [compareFieldLabel(field), ...items.map(item => fieldValue(field, item))]),
    ]
    detailTables.push(`<table class="detail-table"><thead><tr><th>字段</th>${items.map((_, index) => `<th>${bySource ? '来源电池' : '对比项'} ${start + index + 1}</th>`).join('')}</tr></thead><tbody>${rows.map(row => `<tr>${row.map(cell).join('')}</tr>`).join('')}</tbody></table>`)
  }
  const legend = record.items.map((item, index) => {
    const color = record.settings.seriesColors[index % record.settings.seriesColors.length]
    const safeColor = color && /^#[0-9a-f]{6}$/i.test(color) ? color : '#52606d'
    const hidden = record.settings.hiddenSeriesKeys?.includes(compareSelectionKey(item.source.selection))
    return `<span${hidden ? ' style="text-decoration:line-through;color:#8795a3"' : ''}><i style="background:${safeColor}"></i>${escapeHtml(compareSeriesName(item))}</span>`
  }).join('')
  const charts = compareMetricDefinitions.filter(metric => record.settings.metrics.includes(metric.key)).map(metric =>
    `<article class="chart-card"><header><strong>${metric.title} (${escapeHtml(record.displayUnits[metric.key] || metric.unit)})</strong></header>${chartSvg(metric, record)}<div class="chart-inner-legend">${legend}</div></article>`).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;color:#26333f;background:#fff;font-family:"Microsoft YaHei",sans-serif;font-size:11px;letter-spacing:0;-webkit-print-color-adjust:exact;print-color-adjust:exact}
    .report-header{display:flex;align-items:flex-start;justify-content:space-between;gap:20px;margin-bottom:16px;padding-bottom:12px;border-bottom:2px solid #2b78a3}.report-header>div{min-width:0}.report-header h1{margin:0;font-size:22px;overflow-wrap:anywhere}.report-header p{margin:5px 0;color:#71808d}.saved-meta{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:8px 18px;max-width:50%;overflow-wrap:anywhere}.saved-meta strong{margin-left:5px}
    .section{margin:0 0 16px}.section-title{margin:0;padding:9px 10px;border:1px solid #d8e1e9;background:#f4f8fb;font-size:13px;break-after:avoid}.experiment,.note{padding:10px 12px;white-space:pre-wrap;overflow-wrap:anywhere}.experiment strong{margin-left:12px}
    table{width:100%;border-collapse:collapse;table-layout:fixed;margin-bottom:16px}thead{display:table-header-group}tr{break-inside:avoid}th,td{padding:6px 9px;border:1px solid #cbd8e4;text-align:center;overflow-wrap:anywhere;white-space:pre-wrap}th{color:#fff;background:#3c6497;font-weight:600}.detail-table th:first-child,.detail-table td:first-child{width:130px;text-align:left}.detail-table td:first-child{background:#f7fafc;font-weight:600}
    .legend,.chart-inner-legend{display:flex;flex-wrap:wrap;gap:10px 18px;padding:12px 0}.legend span,.chart-inner-legend span{display:flex;align-items:flex-start;gap:6px;max-width:100%;overflow-wrap:anywhere;break-inside:avoid}.legend i,.chart-inner-legend i{flex:0 0 12px;height:12px;margin-top:2px}.chart-inner-legend{padding:10px;font-size:10px}
    .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 header{padding:9px 10px;border-bottom:1px solid #edf1f5}.chart-card svg{display:block;width:100%;height:auto}.empty{padding:30px;color:#8795a3}
    @media(max-width:680px){.report-header{flex-direction:column}.saved-meta{max-width:100%;justify-content:flex-start}.charts{grid-template-columns:1fr}}
    @media print{body{padding:0}.charts{display:block}.chart-card{margin:0 0 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 <strong>#${record.id}</strong></span><span>版本 <strong>V${record.versionNo}</strong></span><span>创建人 <strong>${escapeHtml(record.creator)}</strong></span><span>创建时间 <strong>${date(record.createdAt)}</strong></span><span>版本操作人 <strong>${escapeHtml(record.operatorName)}</strong></span><span>版本时间 <strong>${date(record.versionCreatedAt)}</strong></span><span>修改说明 <strong>${escapeHtml(record.changeSummary)}</strong></span></div></header>
    <section class="section"><h2 class="section-title">对比信息</h2><table class="compare-table"><thead><tr><th>测试名称</th><th>测试类型</th><th>电池组</th><th>电池号</th><th>工步</th><th>循环</th></tr></thead><tbody>${compareRows}</tbody></table></section>
    <section class="section"><h2 class="section-title">测试详情</h2>${!showSource || record.experimentName ? `<div class="experiment">实验<strong>${escapeHtml(record.experimentName || '-')}</strong></div>` : ''}${detailTables.join('')}</section>
    <section class="section"><h2 class="section-title">对比曲线</h2><div class="legend">${legend}</div><div class="charts">${charts || '<div class="empty">当前未启用曲线指标</div>'}</div></section>
    <section class="section"><h2 class="section-title">报表说明</h2><div class="note">${escapeHtml(record.description || '-')}</div></section>
  </body></html>`
}
 
/** 点击时先打开窗口,再经后端导出权限读取指定保存版本;打开或取消打印不代表 PDF 保存成功。 */
export async function openCompareReportPrint(id: number, versionNo: number) {
  return openSavedReportPrint(() => getComparePrintVersion(id, versionNo), printHtml)
}