yeg
5 天以前 9dfc7d64b00a704b7e77a6000c1dcadd66b0d345
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
import { init, use } from 'echarts/core'
import { LineChart } from 'echarts/charts'
import { GridComponent, LegendComponent, TooltipComponent } from 'echarts/components'
import { SVGRenderer } from 'echarts/renderers'
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, TooltipComponent, SVGRenderer])
function escapeHtml(value: unknown) {
  const entities: Record<string, string> = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }
  return String(value ?? '').replace(/[&<>"']/g, character => entities[character]!)
}
 
/** 打印只读指定版本的字段、人工输入及计算结果,绝不向当前来源补数。 */
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})`:'—'
        })])
      }
      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 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())
  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;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), formationSavedReportHtml)
}