From 4b6fbf192b8e3c97ba279c96a085b98d734b8afd Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期二, 08 九月 2026 15:42:56 +0800
Subject: [PATCH] 完善逐电池拼接报表页面验收
---
src/utils/spliceSavedReport.ts | 43 ++++++
src/views/SpliceReportForm.vue | 51 +++++---
tests/e2e/splice-battery-segments.spec.ts | 134 ++++++++++++++++++++++
docs/pages/report-splice.md | 6
tests/e2e/splice-saved-version-print.spec.ts | 8
tests/e2e/data-analysis-integrated-acceptance.spec.ts | 38 +++---
src/api/spliceReports.ts | 20 ++
src/components/SpliceSavedReport.vue | 4
8 files changed, 252 insertions(+), 52 deletions(-)
diff --git a/docs/pages/report-splice.md b/docs/pages/report-splice.md
index 1d3e106..4b64254 100644
--- a/docs/pages/report-splice.md
+++ b/docs/pages/report-splice.md
@@ -7,15 +7,15 @@
## 来源与统计
-添加弹窗按电池组、测试名称或 ID 筛选检测完成测试;选择同组至少两个不同测试及其共同参与电池子集。默认按开始时间排序,可上下调整或删除任一未保存来源。后端再次验证来源身份、状态、电池组和共同集合。
+添加弹窗按电池组、测试名称或 ID 筛选检测完成测试中的电池;至少选择两个不同测试电池段,允许同测试及跨测试、跨组、跨电池号。默认按开始时间排序,同测试按选入顺序,可上下调整或删除任一未保存来源。后端再次验证来源身份、状态和电池归属,拒绝重复段。
-各测试全部参与电池共用循环偏移,首个有数据段保留原循环号,后段首循环接续前段末循环并保留段内跳号。不以日期空档补循环。来源表保留原号与拼接号;不同电池循环覆盖范围不同时,来源分界仍按该测试最早循环确定。
+每个测试电池独立计算循环偏移,首个有数据段保留原循环号,后段首循环接续前段末循环并保留段内跳号。空段不占用虚构循环。每指标一条完整接续线,各指标共用段顺序和循环定位,页面不再按40点或4个来源拆图。
统计图只展示最终电压1和能量效率。最终电压1取末实际工步终止电压,缺失不回退;能量效率为同循环放电能量除以充电能量乘100%,缺失或非正充电能量不计算,超过100%不截断。原五类演示实时曲线及未定义自动计算不再用于拼接。
## 字段、模板与版本
-字段设置支持电池组共享字段、人工项、顺序、展示名称及单位。共享字段从后端来源预览读取;正式保存只冻结已选字段。拼接模板独立存储,只保存字段结构,不带来源或填写值,后续模板变化不修改报表。
+测试详情按来源列带入控制台保存的实验、两类字段列表和逐电池字符串,包含空字符串。来源人工字段不凭名称推断共享身份或单位。报表人工调整以测试和测试电池身份保存,重排后不串列。拼接模板独立存储,只保存字段结构、顺序、展示名称与单位,不带来源或填写值。
表格指标可人工调整并显示计算原值;统计图保持冻结的原始计算结果。取消电池或移除来源时清理失效人工值,保留其他有效修改。报表修改不反写测试事实。
diff --git a/src/api/spliceReports.ts b/src/api/spliceReports.ts
index a3bb6f7..6359577 100644
--- a/src/api/spliceReports.ts
+++ b/src/api/spliceReports.ts
@@ -3,11 +3,11 @@
export interface SpliceCandidate { selection: SpliceSelection; name: string; type: string; groupId: number; group: string; batteryNo: string; startedAt: number }
export interface SpliceField { name: string; category: 'battery' | 'special'; displayName: string; unit: string; fieldDefinitionId?: number | null }
export interface SpliceBusinessField { fieldDefinitionId: number; fieldName: string; fieldValue: string | null }
-export interface SpliceSegment { source: SpliceCandidate; offset: number; originalCycleNos: number[]; statistics: Record<string, Record<string, number | null>>; businessFields?: SpliceBusinessField[] }
+export interface SpliceSegment { source: SpliceCandidate; offset: number; originalCycleNos: number[]; statistics: Record<string, Record<string, number | null>>; businessFields?: SpliceBusinessField[]; consoleDetail?: { experimentName?: string; batteryFields: string[]; specialFields: string[]; fieldValues: Record<string, string> } }
export interface SpliceMatrix { segments: SpliceSegment[]; cycleNos: number[] }
-export interface SpliceSettings { fieldStructure?: SpliceField[]; statisticValues?: Record<string, string>; batteryFields?: string[]; specialFields?: string[]; detailFieldValues?: Record<string, Record<string, string>>; metrics?: string[]; seriesColors?: string[] }
+export interface SpliceSettings { sourceExperimentNames?: Record<string,string>; hiddenSeries?: boolean; fieldStructure?: SpliceField[]; statisticValues?: Record<string, string>; batteryFields?: string[]; specialFields?: string[]; detailFieldValues?: Record<string, Record<string, string>>; metrics?: string[]; seriesColors?: string[] }
export interface SpliceSummary { id: number; name: string; creator: string; createdAt: number }
-export interface SavedSpliceReport extends SpliceSummary { description?: string; selections: SpliceSelection[]; segments: SpliceSegment[]; cycleNos: number[]; settings: SpliceSettings; versionNo: number; operatorName: string; versionCreatedAt: number; changeSummary: string }
+export interface SavedSpliceReport extends SpliceSummary { spliceMode?: 'battery-segments'; description?: string; selections: SpliceSelection[]; segments: SpliceSegment[]; cycleNos: number[]; settings: SpliceSettings; versionNo: number; operatorName: string; versionCreatedAt: number; changeSummary: string }
const root='/system/splice-report'
async function data<T>(path:string, method='GET', body?:unknown){return (await request<ApiResponse & {data:T}>(`${root}${path}`,{method,...(body===undefined?{}:{body:JSON.stringify(body)})})).data}
export function listSpliceReports(){return request<ApiResponse & {rows:SpliceSummary[]}>(`${root}/list`).then(r=>r.rows)}
@@ -41,6 +41,20 @@
field.fieldDefinitionId ? field.fieldDefinitionId === value.fieldDefinitionId : field.name === value.fieldName)
return field.category === 'battery' ? business?.fieldValue ?? '' : ''
}
+/** 新版列身份包含测试与测试电池;空字符串不回退为组字段或其他列。 */
+export function spliceSourceKey(source: SpliceCandidate) { return `${source.selection.testId}|${source.selection.testBatteryId}` }
+export function spliceSourceValue(segment: SpliceSegment, settings: SpliceSettings, field: SpliceField) {
+ const manual = settings.detailFieldValues?.[field.name]?.[spliceSourceKey(segment.source)]
+ if (manual !== undefined) return manual
+ const detail = segment.consoleDetail?.fieldValues[field.name]
+ if (detail !== undefined) return detail
+ if (segment.consoleDetail?.batteryFields.includes(field.name) || segment.consoleDetail?.specialFields.includes(field.name)) return ''
+ // 用户明确选择的共享字段仍按其身份提供组值,不冒充缺失的控制台人工字段。
+ return field.fieldDefinitionId ? segment.businessFields?.find(value => value.fieldDefinitionId === field.fieldDefinitionId)?.fieldValue ?? '' : ''
+}
+export function spliceExperiment(segment: SpliceSegment, settings: SpliceSettings) {
+ return settings.sourceExperimentNames?.[spliceSourceKey(segment.source)] ?? segment.consoleDetail?.experimentName ?? ''
+}
/** 人工统计绑定测试电池和原循环身份,来源重排不改变归属。 */
export function spliceStatisticKey(segment: SpliceSegment, cycle: number, metric: string) {
return `${segment.source.selection.testId}|${segment.source.selection.testBatteryId}|${cycle}|${metric}`
diff --git a/src/components/SpliceSavedReport.vue b/src/components/SpliceSavedReport.vue
index 511d626..92e5b40 100644
--- a/src/components/SpliceSavedReport.vue
+++ b/src/components/SpliceSavedReport.vue
@@ -3,8 +3,8 @@
import type { SavedSpliceReport } from '@/api/spliceReports'
import { spliceSavedReportBody } from '@/utils/spliceSavedReport'
import '@/utils/spliceSavedReport.css'
-const props = defineProps<{ report: SavedSpliceReport }>()
-const content = computed(() => spliceSavedReportBody(props.report))
+const props = defineProps<{ report: SavedSpliceReport; chartsOnly?: boolean }>()
+const content = computed(() => spliceSavedReportBody(props.report, props.chartsOnly))
</script>
<template>
diff --git a/src/utils/spliceSavedReport.ts b/src/utils/spliceSavedReport.ts
index 44916e4..a12a399 100644
--- a/src/utils/spliceSavedReport.ts
+++ b/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, 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,44 @@
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 details = `<section><h2>测试详情</h2><table><thead><tr><th>字段</th>${headers}</tr></thead><tbody><tr><td>实验</td>${segments.map(segment => `<td>${escapeHtml(spliceExperiment(segment, record.settings))}</td>`).join('')}</tr>${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>`)
diff --git a/src/views/SpliceReportForm.vue b/src/views/SpliceReportForm.vue
index 4859963..3721796 100644
--- a/src/views/SpliceReportForm.vue
+++ b/src/views/SpliceReportForm.vue
@@ -4,7 +4,7 @@
import { useRoute, useRouter } from 'vue-router'
import { getUserInfo } from '@/api/auth'
import { getSpliceSources, previewSpliceReport, createSpliceReport, editSpliceReport, refreshSpliceSource,
- getSpliceReport, getSpliceVersion, listSpliceVersions, spliceFields, spliceFieldValue, spliceStatisticKey,
+ getSpliceReport, getSpliceVersion, listSpliceVersions, spliceFields, spliceFieldValue, spliceStatisticKey, spliceSourceKey, spliceSourceValue, spliceExperiment,
type SpliceCandidate, type SpliceMatrix, type SpliceField, type SpliceSettings, type SavedSpliceReport } from '@/api/spliceReports'
import { openSpliceSavedReportPrint } from '@/utils/spliceSavedReport'
import SpliceSavedReport from '@/components/SpliceSavedReport.vue'
@@ -34,6 +34,8 @@
const fields = ref<SpliceField[]>([])
const detailFieldValues = ref<Record<string, Record<string, string>>>({})
const statisticValues = ref<Record<string, string>>({})
+const sourceExperimentNames = ref<Record<string,string>>({})
+const hiddenSeries = ref(false)
const statisticMetrics = [{ key: 'finalVoltage1', title: '最终电压1', unit: 'V' }, { key: 'energyEfficiency', title: '能量效率', unit: '%' }] as const
const statisticVisible = reactive({ finalVoltage1: true, energyEfficiency: true })
const seriesColors = ref(['#3978f6', '#109854', '#d25b45', '#7754a8'])
@@ -46,22 +48,22 @@
const selectedCandidateId = ref<string | null>(null)
const candidateGroupId = ref<number | null>(null)
const candidateQuery = ref('')
-const batteryGroup = computed(() => selectedTests.value[0]?.group || '')
+const batteryGroup = computed(() => [...new Set(selectedTests.value.map(item => item.group))].join('、'))
const candidateTests = computed(() => sources.value.filter(source =>
(!candidateGroupId.value || source.groupId === candidateGroupId.value) &&
(source.name.includes(candidateQuery.value) || String(source.selection.testId).includes(candidateQuery.value) || source.batteryNo.includes(candidateQuery.value))))
const candidateGroups = computed(() => [...new Map(sources.value.map(source => [source.groupId, source.group])).entries()])
-const commonBatteries = computed(() => selectedTests.value.length ? [...new Set(sources.value
- .filter(source => source.selection.testId === selectedTests.value[0]!.selection.testId).map(source => source.batteryNo))]
- .filter(battery => selectedTests.value.every(test => sources.value.some(source =>
- source.selection.testId === test.selection.testId && source.batteryNo === battery))) : [])
const selections = computed(() => selectedTests.value.map(item => item.selection))
const matrix = computed(() => savedReport.value || draftMatrix.value)
+const segmentMode = computed(() => !savedReport.value || savedReport.value.spliceMode === 'battery-segments')
+const detailColumns = computed(() => matrix.value?.segments || [])
const statisticSettings = computed<SpliceSettings>(() => ({
+ sourceExperimentNames: sourceExperimentNames.value,
+ hiddenSeries: hiddenSeries.value,
fieldStructure: fields.value,
batteryFields: fields.value.filter(field => field.category === 'battery').map(field => field.name),
specialFields: fields.value.filter(field => field.category === 'special').map(field => field.name),
- detailFieldValues: Object.fromEntries(fields.value.map(field => [field.name, Object.fromEntries(samples.value
+ detailFieldValues: Object.fromEntries(fields.value.map(field => [field.name, Object.fromEntries((segmentMode.value ? selectedTests.value.map(spliceSourceKey) : samples.value)
.filter(battery => detailFieldValues.value[field.name]?.[battery] !== undefined)
.map(battery => [battery, detailFieldValues.value[field.name]![battery]!]))])),
statisticValues: statisticValues.value,
@@ -69,6 +71,7 @@
}))
const statisticPreview = computed<SavedSpliceReport | null>(() => matrix.value ? {
id: 0, createdAt: 0, creator: '', versionNo: 0, operatorName: '', versionCreatedAt: 0, changeSummary: '',
+ spliceMode: 'battery-segments',
selections: selections.value, ...savedReport.value, ...matrix.value,
name: reportName.value, description: experimentName.value, settings: statisticSettings.value,
} : null)
@@ -80,8 +83,10 @@
fields.value = spliceFields(saved.settings).map(field => ({ ...field }))
detailFieldValues.value = structuredClone(saved.settings.detailFieldValues || {})
statisticValues.value = { ...saved.settings.statisticValues }
+ sourceExperimentNames.value = { ...saved.settings.sourceExperimentNames }
+ hiddenSeries.value = saved.settings.hiddenSeries === true
seriesColors.value = [...(saved.settings.seriesColors || ['#3978f6', '#109854', '#d25b45', '#7754a8'])]
- selectedTests.value = [...new Map(saved.segments.map(segment => [segment.source.selection.testId, segment.source])).values()]
+ selectedTests.value = saved.segments.map(segment => segment.source)
samples.value = [...new Set(saved.segments.map(segment => segment.source.batteryNo))]
const keys = saved.settings.metrics
statisticMetrics.forEach(metric => { statisticVisible[metric.key] = !keys || (keys.length > 0 && !keys.some(key => statisticMetrics.some(item => item.key === key))) || keys.includes(metric.key) })
@@ -103,6 +108,7 @@
if (!canCreate.value) throw new Error('无新增拼接报表权限')
selectedTests.value = []; samples.value = []; reportName.value = ''; experimentName.value = ''
fields.value = []; detailFieldValues.value = {}; statisticValues.value = {}; reordered = false
+ sourceExperimentNames.value = {}; hiddenSeries.value = false
sources.value = await getSpliceSources()
savedContent.value = formContent()
}
@@ -127,6 +133,12 @@
statisticMetrics.map(metric => spliceStatisticKey(segment, cycle, metric.key)))))
statisticValues.value = Object.fromEntries(Object.entries(statisticValues.value).filter(([key]) => allowed.has(key)))
draftMatrix.value = result
+ // 控制台只提供字段名,带入时不猜单位;已有人工结构与值保持不变。
+ for (const segment of result.segments) for (const category of ['battery', 'special'] as const) {
+ for (const name of segment.consoleDetail?.[category === 'battery' ? 'batteryFields' : 'specialFields'] || []) {
+ if (!fields.value.some(field => field.name === name)) fields.value.push({ name, category, displayName: name, unit: '' })
+ }
+ }
}
} catch (error) {
if (request === previewRequest) previewError.value = error instanceof Error ? error.message : '统计预览失败'
@@ -194,7 +206,7 @@
}
async function saveReport() {
if (!reportName.value.trim()) { ElMessage.warning('请输入拼接报表名称'); return }
- if (!editingId.value && (!draftMatrix.value || previewing.value)) { ElMessage.warning('请选择至少两个测试及共同电池,并等待统计预览'); return }
+ if (!editingId.value && (!draftMatrix.value || previewing.value)) { ElMessage.warning('请选择至少两个测试电池段,并等待统计预览'); return }
saving.value = true
try {
const payload = { name: reportName.value.trim(), description: experimentName.value, settings: statisticSettings.value }
@@ -219,22 +231,24 @@
<section class="report-name-panel"><label><span>拼接报表名称</span><el-input v-model="reportName" clearable placeholder="请输入拼接报表名称" /></label></section>
<section class="test-panel">
- <header class="panel-heading"><div><strong>拼接测试</strong></div><small>{{ selectedTests.length }} 个测试</small></header>
- <div class="test-table-wrap"><table class="test-table"><thead><tr><th>顺序</th><th>测试名称</th><th>测试类型</th><th>电池组</th><th>开始时间</th><th v-if="!editingId">操作</th></tr></thead><tbody>
- <tr v-for="(item, index) in selectedTests" :key="item.selection.testId"><td><span class="order-index">{{ index + 1 }}</span></td><td>{{ item.name }}</td><td>{{ item.type }}</td><td>{{ item.group }}</td><td>{{ item.startedAt ? new Date(item.startedAt).toLocaleString() : '缺失' }}</td>
+ <header class="panel-heading"><div><strong>拼接测试</strong></div><small>{{ selectedTests.length }} 个来源段</small></header>
+ <div class="test-table-wrap"><table class="test-table"><thead><tr><th>顺序</th><th>测试名称</th><th>测试类型</th><th>电池组</th><th>电池号</th><th v-if="!editingId">操作</th></tr></thead><tbody>
+ <tr v-for="(item, index) in selectedTests" :key="spliceSourceKey(item)"><td><span class="order-index">{{ index + 1 }}</span></td><td>{{ item.name }}</td><td>{{ item.type }}</td><td>{{ item.group }}</td><td>{{ item.batteryNo }}</td>
<td v-if="!editingId"><el-tooltip content="上移"><el-button circle aria-label="上移测试" :disabled="index === 0" @click="moveTest(index, -1)"><el-icon><ArrowUp /></el-icon></el-button></el-tooltip><el-tooltip content="下移"><el-button circle aria-label="下移测试" :disabled="index === selectedTests.length - 1" @click="moveTest(index, 1)"><el-icon><ArrowDown /></el-icon></el-button></el-tooltip><button class="delete-link" @click="removeSpliceTest(index)">删除</button></td></tr>
<tr v-if="!selectedTests.length"><td colspan="6">暂无已选测试</td></tr>
</tbody></table></div>
<div v-if="!editingId" class="panel-actions"><button class="primary-button compact-button" type="button" @click="openAddTest"><el-icon><Plus /></el-icon>添加拼接</button>
- <el-checkbox-group v-model="samples" aria-label="共同参与电池"><el-checkbox v-for="battery in commonBatteries" :key="battery" :value="battery">{{ battery }}</el-checkbox></el-checkbox-group>
- <span v-if="selectedTests.length && !commonBatteries.length">无共同参与电池</span>
</div>
</section>
<section class="detail-panel">
<header class="panel-heading"><div><strong>测试详情</strong></div></header>
<div class="detail-meta"><label><span>报表说明</span><el-input v-model="experimentName" /></label><div><span>电池组</span><strong>{{ batteryGroup }}</strong></div></div>
- <div class="detail-table-wrap"><table class="detail-table" :style="{ minWidth: `${Math.max(760, samples.length * 190 + 170)}px` }"><thead><tr><th>字段</th><th v-for="sample in samples" :key="sample">{{ sample }}</th></tr></thead><tbody>
+ <div v-if="segmentMode" class="detail-table-wrap"><table class="detail-table" :style="{ minWidth: `${Math.max(760, detailColumns.length * 190 + 170)}px` }"><thead><tr><th>字段</th><th v-for="segment in detailColumns" :key="spliceSourceKey(segment.source)">{{ segment.source.name }}<br>{{ segment.source.group }} · {{ segment.source.batteryNo }}</th></tr></thead><tbody>
+ <tr><td>实验</td><td v-for="segment in detailColumns" :key="spliceSourceKey(segment.source)"><el-input :model-value="spliceExperiment(segment, statisticSettings)" placeholder="缺失" @update:model-value="sourceExperimentNames[spliceSourceKey(segment.source)] = $event" /></td></tr>
+ <tr v-for="field in fields" :key="field.name"><td>{{ field.displayName }}{{ field.unit ? ` (${field.unit})` : '' }}</td><td v-for="segment in detailColumns" :key="spliceSourceKey(segment.source)"><el-input :model-value="spliceSourceValue(segment, statisticSettings, field)" size="small" placeholder="缺失" @update:model-value="setFieldValue(field, spliceSourceKey(segment.source), $event)" /></td></tr>
+ </tbody></table></div>
+ <div v-else class="detail-table-wrap"><table class="detail-table" :style="{ minWidth: `${Math.max(760, samples.length * 190 + 170)}px` }"><thead><tr><th>字段</th><th v-for="sample in samples" :key="sample">{{ sample }}</th></tr></thead><tbody>
<tr v-for="field in fields" :key="field.name"><td>{{ field.displayName }}{{ field.unit ? ` (${field.unit})` : '' }}</td><td v-for="sample in samples" :key="sample"><el-input :model-value="fieldValue(field, sample)" size="small" placeholder="缺失" @update:model-value="setFieldValue(field, sample, $event)" /></td></tr>
</tbody></table></div>
<div class="detail-actions"><button class="primary-button" type="button" @click="addSpecialField"><el-icon><Plus /></el-icon>添加项</button></div>
@@ -243,8 +257,9 @@
<section v-if="statisticPreview" class="chart-panel">
<div class="metric-switches"><label v-for="metric in statisticMetrics" :key="metric.key"><span>{{ metric.title }}</span><el-switch v-model="statisticVisible[metric.key]" /></label></div>
- <div class="chart-legend-bar"><div v-for="(sample, index) in samples" :key="sample" class="chart-legend-item"><el-color-picker :model-value="seriesColors[index]" :aria-label="`修改${sample}颜色`" @change="updateSeriesColor(index, $event)" /><span>{{ sample }}</span></div></div>
- <SpliceSavedReport :report="statisticPreview" />
+ <div v-if="segmentMode" class="chart-legend-bar"><div class="chart-legend-item"><el-color-picker :model-value="seriesColors[0]" aria-label="修改接续曲线颜色" @change="updateSeriesColor(0, $event)" /><button type="button" :aria-pressed="!hiddenSeries" @click="hiddenSeries = !hiddenSeries">接续曲线{{ hiddenSeries ? '(已隐藏)' : '' }}</button></div></div>
+ <div v-else class="chart-legend-bar"><div v-for="(sample, index) in samples" :key="sample" class="chart-legend-item"><el-color-picker :model-value="seriesColors[index]" :aria-label="`修改${sample}颜色`" @change="updateSeriesColor(index, $event)" /><span>{{ sample }}</span></div></div>
+ <SpliceSavedReport :report="statisticPreview" :charts-only="segmentMode" />
</section>
<el-alert v-if="previewError" :title="previewError" type="error" :closable="false" />
<p v-if="previewing">正在加载循环统计</p>
@@ -272,7 +287,7 @@
<el-dialog v-model="addDialogVisible" class="add-dialog" title="添加拼接" width="min(560px, 94vw)" align-center destroy-on-close>
<div class="dialog-form">
- <label><span>电池组</span><el-select v-model="candidateGroupId" clearable :disabled="selectedTests.length > 0" placeholder="全部"><el-option v-for="[id, name] in candidateGroups" :key="id" :label="name" :value="id" /></el-select></label>
+ <label><span>电池组</span><el-select v-model="candidateGroupId" clearable placeholder="全部"><el-option v-for="[id, name] in candidateGroups" :key="id" :label="name" :value="id" /></el-select></label>
<label><span>测试名称</span><el-input v-model="candidateQuery" placeholder="输入测试名称或ID搜索" /></label>
<label><span>选择测试电池</span><el-select v-model="selectedCandidateId" filterable clearable placeholder="选择检测完成测试电池"><el-option v-for="item in candidateTests" :key="`${item.selection.testId}:${item.selection.testBatteryId}`" :label="`${item.name}(${item.group},${item.batteryNo})`" :value="`${item.selection.testId}:${item.selection.testBatteryId}`" :disabled="selectedTests.some(test => test.selection.testId === item.selection.testId && test.selection.testBatteryId === item.selection.testBatteryId)" /></el-select></label>
</div>
diff --git a/tests/e2e/data-analysis-integrated-acceptance.spec.ts b/tests/e2e/data-analysis-integrated-acceptance.spec.ts
index cf2e81f..48f9df6 100644
--- a/tests/e2e/data-analysis-integrated-acceptance.spec.ts
+++ b/tests/e2e/data-analysis-integrated-acceptance.spec.ts
@@ -196,7 +196,7 @@
expect(errors).toEqual([])
})
-test('拼接前置行为:共同多电池、跳号和末工步缺失', async ({ page }) => {
+test('拼接逐电池来源、跳号和末工步缺失', async ({ page }) => {
await login(page)
// 后段从循环5开始,必须平移到前段末循环5之后;不额外假设首段的归零规则。
const single = [selection(9303), selection(9302)]
@@ -205,12 +205,13 @@
expect.soft(nextSegment.originalCycleNos.map((no: number) => no + nextSegment.offset), '后段应紧接前段且保留段内跳号').toEqual([6, 8])
expect.soft(preview.segments[0].statistics['3'].finalVoltage1 ?? null, '最后实际工步缺失,不能回退到 4.2 V').toBeNull()
const multiple = await request(page, 'splice-report/preview', 'POST', [selection(9302), selection(9302, 9402), selection(9303), selection(9303, 9403)])
- expect.soft(multiple.code, `共同多电池:${multiple.msg}`).toBe(200)
+ expect.soft(multiple.code, `逐电池段:${multiple.msg}`).toBe(200)
const mismatch = await request(page, 'splice-report/preview', 'POST', [selection(9302), selection(9304)])
- expect(mismatch.code).not.toBe(200)
+ expect(mismatch.code).toBe(200)
+ for (const valid of [[selection(9302), selection(9302, 9402)], [selection(9302), selection(9303), selection(9303, 9403)]])
+ expect((await request(page, 'splice-report/preview', 'POST', valid)).code).toBe(200)
for (const invalid of [
- [selection(9302), selection(9302, 9402)],
- [selection(9302), selection(9303), selection(9303, 9403)],
+ [selection(9302), selection(9302)],
[selection(9302), selection(9305)],
[selection(9302, 9403), selection(9303)],
]) expect((await request(page, 'splice-report/preview', 'POST', invalid)).code).not.toBe(200)
@@ -256,7 +257,8 @@
test('拼接模板仅保存所属类型字段结构,业务字段和人工指标随版本独立保存', async ({ page }) => {
await login(page)
- const fields = [{ name: 'F', category: 'battery', displayName: '业务F', unit: '' }, { name: '说明项', category: 'special', displayName: '人工说明', unit: '' }]
+ const fieldDefinitionId = Number(stack.queryDatabase("SELECT field_id FROM t_battery_group_field WHERE field_name='F'"))
+ const fields = [{ name: 'F', category: 'battery', displayName: '业务F', unit: '', fieldDefinitionId }, { name: '说明项', category: 'special', displayName: '人工说明', unit: '' }]
const template = (await api(page, 'splice-report/templates', 'POST', { name: '拼接专用字段', fields })).data
const templates = (await api(page, 'splice-report/templates')).data
const stored = templates.find((item: any) => item.id === template)
@@ -269,8 +271,8 @@
const draft = { ...payload('splice', '模板与人工统计'), selections, settings: { ...payload('splice', '').settings, fieldStructure: stored.fields, statisticValues: {} } }
const first = (await api(page, 'splice-report', 'POST', draft)).data
expect(first.segments[0].businessFields.map((field: any) => field.fieldName)).toEqual(['F'])
- expect(first.cycleNos).toEqual([1, 3, 5, 6, 8])
- expect(first.segments.map((segment: any) => segment.offset)).toEqual([0, 0, 1, 1])
+ expect(first.cycleNos).toEqual([1, 3, 5, 6, 8, 10, 11, 13, 14, 16])
+ expect(first.segments.map((segment: any) => segment.offset)).toEqual([0, 5, 6, 9])
const original = (await api(page, 'test/9303/data/steps')).data
const second = (await api(page, `splice-report/${first.id}/versions`, 'POST', { name: first.name, description: '', baseVersionNo: 1, changeSummary: '人工调整', settings: { ...first.settings, statisticValues: { '9303|9303|3|finalVoltage1': '-1.25' } } })).data
expect(second.segments).toEqual(first.segments)
@@ -287,22 +289,21 @@
page.on('pageerror', error => errors.push(error.message))
await page.goto('/report-splice/new')
await page.getByPlaceholder('请输入拼接报表名称').fill('浏览器多电池拼接')
- for (const name of ['连续来源3', '连续来源2']) {
+ for (const [name, battery] of [['连续来源3', '同名电池'], ['连续来源3', '第二电池'], ['连续来源2', '同名电池'], ['连续来源2', '第二电池']]) {
await page.locator('.panel-actions').getByRole('button', { name: '添加拼接', exact: true }).click()
const dialog = page.getByRole('dialog', { name: '添加拼接', exact: true })
await dialog.getByPlaceholder('输入测试名称或ID搜索').fill(name)
await dialog.getByRole('combobox').last().click()
- await page.getByRole('option', { name: new RegExp(name) }).click()
+ await page.getByRole('option', { name: new RegExp(`${name}.*${battery}`) }).click()
await dialog.getByRole('button', { name: '添加拼接', exact: true }).click()
await expect(dialog).not.toBeVisible()
}
await expect(page.locator('.test-table tbody tr').first()).toContainText('连续来源2')
- await page.getByRole('button', { name: '下移测试', exact: true }).first().click()
+ await page.getByRole('button', { name: '上移测试', exact: true }).nth(2).click()
+ await page.getByRole('button', { name: '上移测试', exact: true }).nth(1).click()
+ await page.getByRole('button', { name: '上移测试', exact: true }).nth(3).click()
+ await page.getByRole('button', { name: '上移测试', exact: true }).nth(2).click()
await expect(page.locator('.test-table tbody tr').first()).toContainText('连续来源3')
- await expect(page.locator('.statistics-editor tbody tr')).toHaveCount(10)
- await page.locator('.panel-actions .el-checkbox').filter({ hasText: '第二电池' }).click()
- await expect(page.locator('.statistics-editor tbody tr')).toHaveCount(5)
- await page.locator('.panel-actions .el-checkbox').filter({ hasText: '第二电池' }).click()
await expect(page.locator('.statistics-editor tbody tr')).toHaveCount(10)
await expect(page.locator('.splice-snapshot')).not.toContainText('SP2')
await expect(page.locator('.splice-chart')).toHaveCount(2)
@@ -312,7 +313,7 @@
await fRow.locator('.el-checkbox').click()
await fRow.getByRole('textbox', { name: '展示名称', exact: true }).fill('冻结业务F')
await fieldDialog.getByRole('button', { name: '确认', exact: true }).click()
- await expect(page.locator('.detail-table tbody tr').first().locator('input').first()).toHaveValue(/^(12.5|25)$/)
+ await expect(page.locator('.detail-table tbody tr').filter({ hasText: '冻结业务F' }).locator('input').first()).toHaveValue(/^(12.5|25)$/)
await page.getByRole('button', { name: '选择字段模板', exact: true }).click()
await page.getByRole('button', { name: '保存为模板', exact: true }).click()
const saveTemplate = page.getByRole('dialog', { name: '保存拼接字段模板', exact: true })
@@ -322,10 +323,7 @@
await page.getByRole('dialog', { name: '选择拼接字段模板', exact: true }).getByRole('button', { name: '应用模板', exact: true }).click()
await page.getByRole('textbox', { name: '连续来源3 同名电池 循环3 最终电压1', exact: true }).fill('-1.25')
await page.getByRole('textbox', { name: '连续来源3 第二电池 循环3 最终电压1', exact: true }).fill('9')
- await page.locator('.panel-actions .el-checkbox').filter({ hasText: '第二电池' }).click()
- await expect(page.locator('.statistics-editor tbody tr')).toHaveCount(5)
- await page.locator('.panel-actions .el-checkbox').filter({ hasText: '第二电池' }).click()
- await expect(page.getByRole('textbox', { name: '连续来源3 第二电池 循环3 最终电压1', exact: true })).toHaveValue('')
+ await expect(page.getByRole('textbox', { name: '连续来源3 第二电池 循环3 最终电压1', exact: true })).toHaveValue('9')
await expect(page.getByRole('textbox', { name: '连续来源3 同名电池 循环3 最终电压1', exact: true })).toHaveValue('-1.25')
for (const width of [1440, 390]) {
await page.setViewportSize({ width, height: 900 })
diff --git a/tests/e2e/splice-battery-segments.spec.ts b/tests/e2e/splice-battery-segments.spec.ts
new file mode 100644
index 0000000..219be35
--- /dev/null
+++ b/tests/e2e/splice-battery-segments.spec.ts
@@ -0,0 +1,134 @@
+import { expect, test, type Page } from '@playwright/test'
+import { Ticket04Stack } from './support/ticket04-stack'
+
+const stack = new Ticket04Stack()
+const selection = (testBatteryId: number) => ({ testId: testBatteryId < 9703 ? 9701 : 9702, testBatteryId })
+const fields = [{ name: '备注', category: 'special', displayName: '备注', unit: '' }]
+test.use({ actionTimeout: 20_000 })
+test.beforeAll(async () => {
+ await stack.start()
+ stack.executeIsolatedSql(`
+ INSERT INTO t_battery_group(group_id,group_name,model) VALUES(9701,'甲组','型号'),(9702,'乙组','型号');
+ INSERT INTO t_test_type(test_type_id,test_type_name) VALUES(9701,'容量');
+ INSERT INTO t_test(test_id,test_name,creator_id,principal_id,test_type_id,test_purpose,test_status,reception_status,started_at_utc)
+ VALUES(9701,'测试甲',1,1,9701,'隔离验收','COMPLETED','ENDED','2026-09-01 00:00:00'),(9702,'测试乙',1,1,9701,'隔离验收','COMPLETED','ENDED','2026-09-02 00:00:00');
+ INSERT INTO t_test_battery(test_battery_id,test_id,group_id,battery_no,collector_id)
+ VALUES(9701,9701,9701,'1号',9701),(9702,9701,9701,'2号',9702),(9703,9702,9702,'1号',9703),(9704,9702,9702,'空段',9704),(9705,9702,9702,'长段',9705);
+ `)
+ for (const [id, cycles] of [[9701, [1,3]], [9702,[2,4]], [9703,[1]], [9705, Array.from({length:45},(_,index) => index+1)]] as const) {
+ for (const no of cycles) stack.executeIsolatedSql(`
+ INSERT INTO t_test_cycle_record(test_id,test_battery_id,binding_id,adapter_code,source_identity,source_record_id,dedupe_key,adapter_timezone_id,device_time_utc,device_time_raw,server_received_at_utc,cycle_no,charge_energy_wh,discharge_energy_wh,content_sha256)
+ VALUES(${selection(id).testId},${id},${id},'splice','splice:${id}','${no}','${no}','Asia/Hong_Kong','2026-09-01 00:00:00','2026-09-01 08:00:00','2026-09-01 00:00:01',${no},${no === 3 ? 0 : 1},1.2,UNHEX(SHA2('${id}:${no}',256)));
+ INSERT INTO t_test_step_record(test_id,test_battery_id,binding_id,adapter_code,source_identity,source_record_id,dedupe_key,adapter_timezone_id,device_time_utc,device_time_raw,server_received_at_utc,cycle_no,step_no,voltage_v,content_sha256)
+ VALUES(${selection(id).testId},${id},${id},'splice','splice:${id}','${no}','${no}','Asia/Hong_Kong','2026-09-01 00:00:00','2026-09-01 08:00:00','2026-09-01 00:00:01',${no},1,${no === 3 ? 'NULL' : no === 1 ? 0 : -0.2},UNHEX(SHA2('${id}:${no}',256)));
+ `)
+ }
+})
+test.afterAll(async () => stack.stop())
+async function login(page: Page) {
+ await page.goto('/login')
+ await page.locator('input[autocomplete="username"]').fill('admin')
+ await page.locator('input[autocomplete="current-password"]').fill('admin123')
+ await page.getByRole('button',{name:'登录',exact:true}).click()
+ await expect(page).toHaveURL(/dashboard$/)
+}
+async function api(page: Page, path: string, method = 'GET', data?: unknown, code = 200) {
+ const token = await page.evaluate(() => localStorage.getItem('battery_detection_auth_token'))
+ const result = await (await page.request.fetch(`${stack.backendUrl}/system/${path}`,{method,data,headers:{Authorization:`Bearer ${token}`}})).json()
+ expect(result.code, `${path}: ${result.msg}`).toBe(code)
+ return result.data
+}
+test('逐电池身份、接续、来源详情、界面重排及保存打印闭环',async ({page}) => {
+ await login(page)
+ await api(page,'test/9701/console-detail','PUT',{experimentName:'实验甲',batteryFields:['人工字段无共享身份'],specialFields:['备注'],fieldValues:{备注:{'1号':'甲一','2号':''},人工字段无共享身份:{'1号':'原样单位未知','2号':'另一值'}}})
+ await api(page,'test/9702/console-detail','PUT',{experimentName:'实验乙',batteryFields:[],specialFields:['备注'],fieldValues:{备注:{'1号':'乙一','空段':'空来源','长段':'长来源'}}})
+ const selections = [9701,9702,9703].map(selection)
+ const preview = await api(page,'splice-report/preview','POST',selections)
+ expect(preview.segments.map((s:any) => s.originalCycleNos.map((no:number) => no+s.offset))).toEqual([[1,3],[4,6],[7]])
+ expect(preview.segments.map((s:any) => s.consoleDetail.fieldValues.备注)).toEqual(['甲一','','乙一'])
+ expect(preview.segments[0].statistics['1']).toMatchObject({finalVoltage1:0,energyEfficiency:120})
+ expect(preview.segments[0].statistics['3'].finalVoltage1 ?? null).toBeNull()
+ expect(preview.segments[0].statistics['3'].energyEfficiency ?? null).toBeNull()
+ for (const charge of ['NULL','0','-1']) {
+ stack.executeIsolatedSql(`UPDATE t_test_cycle_record SET charge_energy_wh=${charge} WHERE test_battery_id=9703`)
+ expect((await api(page,'splice-report/preview','POST',selections)).segments[2].statistics['1'].energyEfficiency ?? null).toBeNull()
+ }
+ stack.executeIsolatedSql('UPDATE t_test_cycle_record SET charge_energy_wh=1 WHERE test_battery_id=9703')
+ stack.executeIsolatedSql("UPDATE t_test SET test_status='WAITING' WHERE test_id=9702")
+ await api(page,'splice-report/preview','POST',selections,500)
+ stack.executeIsolatedSql("UPDATE t_test SET test_status='COMPLETED' WHERE test_id=9702")
+ await api(page,'splice-report/preview','POST',[selection(9701),selection(9701)],500)
+ await api(page,'splice-report/preview','POST',[selection(9701),{testId:9701,testBatteryId:9703}],500)
+ const sameTest = await api(page,'splice-report','POST',{name:'同测试两段',selections:selections.slice(0,2),settings:{fieldStructure:fields}})
+ expect(sameTest.segments).toHaveLength(2)
+ const reordered = await api(page,'splice-report/preview','POST',[selection(9703),selection(9704),selection(9701)])
+ expect(reordered.segments.map((s:any) => s.originalCycleNos.map((no:number) => no+s.offset))).toEqual([[1],[],[2,4]])
+ await page.goto('/report-splice/new')
+ await page.getByPlaceholder('请输入拼接报表名称').fill('逐电池拼接界面')
+ for (const label of ['测试甲(甲组,1号)','测试甲(甲组,2号)','测试乙(乙组,1号)']) {
+ await page.locator('.panel-actions').getByRole('button',{name:'添加拼接',exact:true}).click()
+ await page.getByRole('dialog').locator('.el-select').last().click()
+ await page.getByRole('option',{name:label,exact:true}).click()
+ await page.getByRole('dialog').getByRole('button',{name:'添加拼接',exact:true}).click()
+ }
+ await expect(page.locator('.splice-chart')).toHaveCount(2)
+ await expect(page.locator('.splice-snapshot h1')).toHaveCount(0)
+ const detailRow = page.locator('.detail-table tr').filter({has:page.locator('td').filter({hasText:/^备注$/})})
+ await expect(detailRow.locator('input').nth(0)).toHaveValue('甲一')
+ await expect(detailRow.locator('input').nth(1)).toHaveValue('')
+ await detailRow.locator('input').nth(2).fill('乙一报表')
+ await page.getByLabel('测试乙 1号 循环1 最终电压1').fill('8')
+ await page.getByRole('button',{name:'上移测试',exact:true}).nth(2).click()
+ await expect(detailRow.locator('input').nth(1)).toHaveValue('乙一报表')
+ await expect(page.getByLabel('测试乙 1号 循环1 最终电压1')).toHaveValue('8')
+ await page.locator('.test-table .delete-link').last().click()
+ await expect(detailRow.locator('input')).toHaveCount(2)
+ await expect(detailRow.locator('input').nth(1)).toHaveValue('乙一报表')
+ await page.locator('.panel-actions').getByRole('button',{name:'添加拼接',exact:true}).click()
+ await page.getByRole('dialog').locator('.el-select').last().click()
+ await page.getByRole('option',{name:'测试甲(甲组,2号)',exact:true}).click()
+ await page.getByRole('dialog').getByRole('button',{name:'添加拼接',exact:true}).click()
+ await expect(page.getByLabel('测试乙 1号 循环1 最终电压1')).toHaveValue('8')
+ await page.getByRole('button',{name:'接续曲线',exact:true}).click()
+ await page.getByRole('button',{name:'保存报表',exact:true}).click()
+ await expect(page).toHaveURL(/report-splice$/)
+ const token = await page.evaluate(() => localStorage.getItem('battery_detection_auth_token'))
+ const list = await (await page.request.get(`${stack.backendUrl}/system/splice-report/list`,{headers:{Authorization:`Bearer ${token}`}})).json()
+ const saved = await api(page,`splice-report/${list.rows.find((r:any) => r.name === '逐电池拼接界面').id}`)
+ expect(saved.selections).toEqual([selection(9701),selection(9703),selection(9702)])
+ expect(saved.settings.hiddenSeries).toBe(true)
+ expect(saved.settings.detailFieldValues.备注['9702|9703']).toBe('乙一报表')
+ expect(saved.settings.statisticValues['9702|9703|1|finalVoltage1']).toBe('8')
+ expect(saved.segments[1].statistics['1'].finalVoltage1).toBe(0)
+ expect((await api(page,'test/9702')).consoleDetail.fieldValues.备注['1号']).toBe('乙一')
+ await page.goto(`/report-splice/${saved.id}/edit?mode=view`)
+ await page.reload()
+ await expect(page.locator('.splice-snapshot')).toContainText('乙一报表')
+ await page.context().addInitScript(() => {window.print=()=>{document.body.dataset.printRequested='true'}})
+ const popupPromise = page.waitForEvent('popup')
+ await page.getByRole('button',{name:'打印 / 另存 PDF',exact:true}).click()
+ const popup = await popupPromise
+ await expect(popup.locator('body')).toHaveAttribute('data-print-requested','true')
+ expect(await popup.locator('.splice-snapshot').innerText()).toEqual(await page.locator('.splice-snapshot').innerText())
+ await popup.close()
+ const long = await api(page,'splice-report','POST',{name:'完整长线',selections:[9701,9702,9703,9704,9705].map(selection),settings:{fieldStructure:fields,seriesColors:['#123456']}})
+ await page.goto(`/report-splice/${long.id}/edit?mode=view`)
+ await expect(page.locator('.splice-chart')).toHaveCount(2)
+ await expect(page.locator('.splice-chart svg path[stroke="#123456"]').first()).toBeAttached()
+ await expect(page.locator('.splice-statistics')).toHaveCount(5)
+ for (const width of [1440,390]) {
+ await page.setViewportSize({width,height:900})
+ await expect.poll(() => page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true)
+ await page.screenshot({path:`tests/e2e/.artifacts/splice-segments-${width}.png`,fullPage:true})
+ }
+ const longPopup = page.waitForEvent('popup')
+ await page.getByRole('button',{name:'打印 / 另存 PDF',exact:true}).click()
+ const printed = await longPopup
+ await expect(printed.locator('.splice-statistics')).toHaveCount(5)
+ await printed.pdf({path:'tests/e2e/.artifacts/splice-segments-long.pdf',preferCSSPageSize:true,printBackground:true})
+ await printed.close()
+ await api(page,'test/9701','DELETE')
+ await api(page,'test/9702','DELETE')
+ expect(await api(page,`splice-report/${long.id}/versions/1/print`)).toEqual(long)
+ await api(page,`splice-report/${long.id}/refresh-source`,'POST',{baseVersionNo:1,changeSummary:'来源已删'},500)
+})
diff --git a/tests/e2e/splice-saved-version-print.spec.ts b/tests/e2e/splice-saved-version-print.spec.ts
index e61918e..66070ee 100644
--- a/tests/e2e/splice-saved-version-print.spec.ts
+++ b/tests/e2e/splice-saved-version-print.spec.ts
@@ -63,14 +63,14 @@
await page.getByRole('button', { name: '打印 / 另存 PDF', exact: true }).click()
await expect(page.getByText('有未保存修改,请先保存新版本后再打印', { exact: true })).toBeVisible()
await page.getByPlaceholder('请输入拼接报表名称').fill('拼接打印V3')
- await page.locator('.detail-table input').fill('人工新版')
+ await page.locator('.detail-table tr').filter({ hasText: '人工备注' }).locator('input').first().fill('人工新版')
await page.locator('.metric-switches label').filter({ hasText: '能量效率' }).click()
await page.getByRole('button', { name: '保存修改', exact: true }).click()
await expect(page).toHaveURL(/\/report-splice$/)
const v3 = (await api(page, `splice-report/${v1.id}`)).data
expect(v3.versionNo).toBe(3)
expect(v3.segments).toEqual(v1.segments)
- expect(v3.settings.detailFieldValues['人工备注']['电池01']).toBe('人工新版')
+ expect(v3.settings.detailFieldValues['人工备注']['9201|9201']).toBe('人工新版')
expect(v3.settings.metrics).toEqual(['finalVoltage1'])
await page.goto(`/report-splice/${v1.id}/edit?mode=view&version=999`)
await expect(page.locator('.el-alert')).toContainText('版本不存在')
@@ -130,7 +130,7 @@
expect(saved.segments[0].statistics['1'].energyEfficiency).toBeCloseTo(124.46730345)
expect(saved.segments[0].statistics['3'].finalVoltage1 ?? null).toBeNull()
await page.goto(`/report-splice/${saved.id}/edit?mode=view`)
- await expect(page.locator('.splice-chart')).toHaveCount(6)
+ await expect(page.locator('.splice-chart')).toHaveCount(2)
await expect(page.locator('.splice-statistics').first()).toContainText('124.4673')
await expect(page.locator('.splice-statistics').first()).toContainText('缺失')
for (const width of [1440, 390]) {
@@ -145,7 +145,7 @@
await page.getByRole('button', { name: '打印 / 另存 PDF', exact: true }).click()
const popup = await popupPromise
await expect(popup.locator('body')).toHaveAttribute('data-print-requested', 'true')
- await expect(popup.locator('.splice-chart')).toHaveCount(6)
+ await expect(popup.locator('.splice-chart')).toHaveCount(2)
await expect(popup.locator('.splice-chart svg path[stroke="#123456"]').first()).toBeAttached()
expect(await popup.locator('.splice-snapshot').innerText()).toEqual(await page.locator('.splice-snapshot').innerText())
await popup.emulateMedia({ media: 'print' })
--
Gitblit v1.7.1