yeg
2026-09-08 0c7ab59fe6f7c91280665533cdaa57530a70dc6b
src/utils/spliceSavedReport.ts
@@ -2,7 +2,7 @@
import { LineChart } from 'echarts/charts'
import { GridComponent, MarkLineComponent } from 'echarts/components'
import { SVGRenderer } from 'echarts/renderers'
import { getSplicePrintVersion, spliceFields, spliceFieldValue, spliceStatisticKey, type SavedSpliceReport } from '@/api/spliceReports'
import { getSplicePrintVersion, spliceFields, spliceFieldValue, spliceStatisticKey, spliceSourceValue, spliceExperiment, spliceSourceFields, spliceDetailValue, type SavedSpliceReport } from '@/api/spliceReports'
import { openSavedReportPrint } from './savedReportPrint'
import css from './spliceSavedReport.css?inline'
@@ -20,7 +20,8 @@
const display = (value: unknown) => { const number = numeric(value); return number === null ? '缺失' : String(Number(number.toFixed(4))) }
/** 重开与打印共用冻结统计及人工字段,绝不重算或读取来源测试。 */
export function spliceSavedReportBody(record: SavedSpliceReport) {
export function spliceSavedReportBody(record: SavedSpliceReport, chartsOnly = false) {
  if (record.spliceMode === 'battery-segments') return segmentReportBody(record, chartsOnly)
  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) => {
@@ -88,6 +89,48 @@
  return `<div class="splice-snapshot"><header><h1>${cell(record.name)}</h1>${heading}</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>`
}
/** 新版每指标一条完整接续线;旧版仍走原快照呈现,不静默重算历史偏移。 */
function segmentReportBody(record: SavedSpliceReport, chartsOnly: boolean) {
  const segments = record.segments
  const cycles = record.cycleNos
  const savedColor = record.settings.seriesColors?.[0]
  const color = savedColor && /^#[\da-f]{6}$/i.test(savedColor) ? savedColor : '#3978f6'
  const sourceName = (index: number) => `来源${index + 1} · ${segments[index]!.source.name} · ${segments[index]!.source.group} · ${segments[index]!.source.batteryNo}`
  const charts = metrics.filter(metric => !record.settings.metrics || record.settings.metrics.includes(metric.key)).map(metric => {
    if (!cycles.length) return `<article class="splice-chart"><h3>${metric.title}</h3><p>无已保存循环统计</p></article>`
    const chart = init(null, undefined, { renderer: 'svg', ssr: true, width: 900, height: 320 })
    let svg: string
    try {
      chart.setOption({ animation: false, grid: { top: 42, right: 35, bottom: 48, left: 70 },
        xAxis: { type: 'value', min: cycles[0]! - 0.5, max: cycles.at(-1)! + 0.5, minInterval: 1, name: '拼接循环号', nameLocation: 'middle', nameGap: 30, axisLabel: { hideOverlap: true, formatter: (value: number) => Number.isInteger(value) ? String(value) : '' } },
        yAxis: { type: 'value', scale: true, name: metric.unit },
        series: [{ name: '接续曲线', type: 'line', connectNulls: false, showSymbol: true, symbolSize: 5, itemStyle: { color },
          // 人工表格值不进入计算原值曲线;缺值保留断点,全部指标共用循环偏移。
          data: record.settings.hiddenSeries ? [] : segments.flatMap((segment, index) => segment.originalCycleNos.map(no => ({ name: `${sourceName(index)} · 原循环${no} · 拼接循环${no + segment.offset}`, value: [no + segment.offset, numeric(segment.statistics[String(no)]?.[metric.key])] }))),
          markLine: { silent: true, symbol: 'none', lineStyle: { color: '#82909b', type: 'dashed' }, label: { show: false }, data: segments.flatMap((segment, index) => segment.originalCycleNos.length ? [{ name: `来源${index + 1}`, xAxis: segment.originalCycleNos[0]! + segment.offset }] : []) },
        }],
      })
      svg = chart.renderToSVGString()
    } finally { chart.dispose() }
    return `<article class="splice-chart" data-series-count="1"><h3>${metric.title} (${metric.unit})</h3><div class="splice-chart-plot">${svg}</div><div class="splice-legend"><span><i style="background:${color}"></i>接续曲线${record.settings.hiddenSeries ? '(已隐藏)' : ''}</span></div><div class="splice-legend">${segments.map((segment, index) => `<span>${cell(sourceName(index))} · ${segment.originalCycleNos.length ? `循环${segment.originalCycleNos[0]! + segment.offset}~${segment.originalCycleNos.at(-1)! + segment.offset}` : '无已保存循环'}</span>`).join('')}</div></article>`
  }).join('')
  if (chartsOnly) return `<div class="splice-snapshot">${charts}</div>`
  const fields = spliceFields(record.settings)
  const headers = segments.map((_, index) => `<th>${cell(sourceName(index))}</th>`).join('')
  const overrides = record.settings.sourceDetailOverrides
  const sourceDetails = overrides === undefined ? '' : `<section class="source-detail-section"><h2>来源测试详情</h2><table><thead><tr><th>字段</th>${headers}</tr></thead><tbody><tr><td>实验</td>${segments.map(segment => `<td>${escapeHtml(spliceDetailValue(segment, overrides))}</td>`).join('')}</tr>${spliceSourceFields(segments, overrides).map(field => `<tr><td>${cell(field.name)}(${field.category === 'battery' ? '电池字段' : '特殊字段'})</td>${segments.map(segment => `<td>${escapeHtml(spliceDetailValue(segment, overrides, field))}</td>`).join('')}</tr>`).join('')}</tbody></table></section>`
  // 无独立详情设置的旧版仍使用其原字段结构,不猜补来源字段或改变历史行含义。
  const legacyExperiment = overrides === undefined ? `<tr><td>实验</td>${segments.map(segment => `<td>${escapeHtml(spliceExperiment(segment, record.settings))}</td>`).join('')}</tr>` : ''
  const details = `${sourceDetails}<section class="report-field-section"><h2>${overrides === undefined ? '测试详情' : '报表字段'}</h2><table><thead><tr><th>字段</th>${headers}</tr></thead><tbody>${legacyExperiment}${fields.map(field => `<tr><td>${cell(field.displayName)}${field.unit ? ` (${cell(field.unit)})` : ''}</td>${segments.map(segment => `<td>${escapeHtml(spliceSourceValue(segment, record.settings, field))}</td>`).join('')}</tr>`).join('')}</tbody></table></section>`
  const sources = `<section><h2>来源顺序与循环对应</h2><table><thead><tr><th>来源</th><th>原循环号 → 拼接循环号</th></tr></thead><tbody>${segments.map((segment, index) => `<tr><td>${cell(sourceName(index))}</td><td>${segment.originalCycleNos.map(no => `${no} → ${no + segment.offset}`).join('、') || '无已保存循环'}</td></tr>`).join('')}</tbody></table></section>`
  const statistics = segments.map((segment, index) => `<section class="splice-statistics"><h2>${cell(sourceName(index))}</h2><table><thead><tr><th>原循环号</th><th>拼接循环号</th>${metrics.map(metric => `<th>${metric.title} (${metric.unit})</th>`).join('')}</tr></thead><tbody>${segment.originalCycleNos.map(no => `<tr><td>${no}</td><td>${no + segment.offset}</td>${metrics.map(metric => {
    const original = segment.statistics[String(no)]?.[metric.key]
    const override = record.settings.statisticValues?.[spliceStatisticKey(segment, no, metric.key)]
    return `<td>${override === undefined ? display(original) : `${escapeHtml(override)}<br><small>计算原值:${display(original)}</small>`}</td>`
  }).join('')}</tr>`).join('') || '<tr><td colspan="4">无已保存循环</td></tr>'}</tbody></table></section>`).join('')
  return `<div class="splice-snapshot"><header><h1>${cell(record.name)}</h1><p>拼接报表 #${record.id} · 版本 V${record.versionNo}</p><p>创建人 ${cell(record.creator)} · 版本操作人 ${cell(record.operatorName)}</p><p>修改说明 ${cell(record.changeSummary)}</p></header>${sources}${details}<section><h2>统计图表</h2>${charts || '<p>当前未启用统计图指标</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>`)