From b2f8041203c8eaf9bbba0a0cb439cae4519d29c4 Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期四, 03 九月 2026 11:46:00 +0800
Subject: [PATCH] 实现工步图表当前有效数据与测试详情保存

---
 src/components/ConsoleChartPanel.vue |  108 +++++++++++++++++++++++++++++-------------------------
 1 files changed, 58 insertions(+), 50 deletions(-)

diff --git a/src/components/ConsoleChartPanel.vue b/src/components/ConsoleChartPanel.vue
index db48b69..430c402 100644
--- a/src/components/ConsoleChartPanel.vue
+++ b/src/components/ConsoleChartPanel.vue
@@ -12,7 +12,7 @@
 } from '@/utils/compareReportPrint'
 import type { CompareMetricKey } from '@/stores/compareReports'
 import { useBatteryGroupFields } from '@/stores/batteryGroupFields'
-import type { PersistedTestRecord, TestLogicStepMapping } from '@/api/tests'
+import { saveTestConsoleDetail, type PersistedTestRecord, type TestConsoleDetail, type TestLogicStepMapping } from '@/api/tests'
 
 interface ConsoleCell {
   no: string
@@ -25,18 +25,21 @@
 }
 
 const props = withDefaults(defineProps<{
+  testId: number
   cells: ConsoleCell[]
   durationMinutes?: number
   testName?: string
   batteryGroup?: string
   records: PersistedTestRecord[]
   logicStepMappings?: TestLogicStepMapping[]
+  consoleDetail?: TestConsoleDetail | null
 }>(), {
   durationMinutes: 5,
   testName: '',
   batteryGroup: '',
   records: () => [],
   logicStepMappings: () => [],
+  consoleDetail: null,
 })
 
 use([CanvasRenderer, LineChart, GridComponent, LegendComponent, TooltipComponent])
@@ -50,6 +53,7 @@
 const batteryFields = ref(['额定容量', '电流', '内阻'])
 const specialFields = ref(['温升'])
 const detailFieldValues = reactive<Record<string, Record<string, string>>>({})
+const detailSaving = ref(false)
 const templateDialogVisible = ref(false)
 const templateMode = ref<'battery' | 'special'>('battery')
 const selectedBatteryFields = ref<string[]>([])
@@ -86,41 +90,27 @@
 }
 
 const cycleOptions = computed(() => [...new Set(props.records
+  .filter(record => record.sourceType === 'SNAPSHOT')
   .map(record => Number(record.cycleNo))
   .filter(value => Number.isFinite(value) && value > 0))].sort((a, b) => a - b))
 
 const stepOptions = computed<StepOption[]>(() => {
+  const cycleNo = Number(selectedCycle.value)
+  if (!Number.isFinite(cycleNo) || cycleNo <= 0) return []
   const actual = new Map<string, StepOption>()
-  const actualAcrossCycles = new Map<number, StepOption>()
-  for (const record of props.records) {
-    const cycleNo = Number(record.cycleNo)
+  for (const record of props.records.filter(item => item.sourceType === 'SNAPSHOT')) {
     const stepNo = Number(record.stepNo)
-    if (!Number.isFinite(cycleNo) || !Number.isFinite(stepNo) || cycleNo <= 0 || stepNo <= 0) continue
+    if (Number(record.cycleNo) !== cycleNo || !Number.isFinite(stepNo) || stepNo <= 0) continue
     const value = `actual:${cycleNo}:${stepNo}`
     if (!actual.has(value)) actual.set(value, { value, label: `${cycleNo}-${stepNo} ${record.stepName || '-'}`, cycleNo, stepNo })
-    if (!actualAcrossCycles.has(stepNo)) actualAcrossCycles.set(stepNo, {
-      value: `actual:all:${stepNo}`,
-      label: `全部循环-${stepNo} ${record.stepName || '-'}`,
-      cycleNo: 0,
-      stepNo,
-    })
   }
   const logical: StepOption[] = []
-  const logicalAcrossCycles: StepOption[] = []
-  for (const cycleNo of cycleOptions.value) {
-    for (const mapping of props.logicStepMappings || []) {
-      logical.push({ value: `logical:${cycleNo}:${mapping.logicalNo}`, label: `${cycleNo}-逻辑${mapping.logicalNo} ${mapping.logicalName}`, cycleNo, logical: mapping })
-    }
-  }
   for (const mapping of props.logicStepMappings || []) {
-    logicalAcrossCycles.push({
-      value: `logical:all:${mapping.logicalNo}`,
-      label: `全部循环-逻辑${mapping.logicalNo} ${mapping.logicalName}`,
-      cycleNo: 0,
-      logical: mapping,
-    })
+    const hit = props.records.some(record => record.sourceType === 'SNAPSHOT' && Number(record.cycleNo) === cycleNo
+      && Number(record.stepNo) >= mapping.startStepNo && Number(record.stepNo) <= mapping.endStepNo)
+    if (hit) logical.push({ value: `logical:${cycleNo}:${mapping.logicalNo}`, label: `${cycleNo}-逻辑${mapping.logicalNo} ${mapping.logicalName}`, cycleNo, logical: mapping })
   }
-  return [...actualAcrossCycles.values(), ...actual.values(), ...logicalAcrossCycles, ...logical]
+  return [...actual.values(), ...logical]
 })
 
 function ensureDetailFieldValues() {
@@ -137,6 +127,8 @@
 watch(() => props.testName, value => {
   if (value) experimentName.value = value
 })
+
+watch(() => props.consoleDetail, value => applyConsoleDetail(value), { immediate: true, deep: true })
 
 watch(() => props.cells, cells => {
   const validCellNos = new Set(cells.map(cell => cell.no))
@@ -158,8 +150,9 @@
 
 watch(cycleOptions, options => {
   const current = Number(selectedCycle.value)
-  if (selectedCycle.value && !options.includes(current)) selectedCycle.value = ''
-})
+  if (!options.length) selectedCycle.value = ''
+  else if (!options.includes(current)) selectedCycle.value = String(options[0])
+}, { immediate: true })
 
 async function addCustomField() {
   try {
@@ -186,6 +179,23 @@
   delete detailFieldValues[field]
 }
 
+async function saveConsoleDetail() {
+  detailSaving.value = true
+  try {
+    const detail: TestConsoleDetail = {
+      experimentName: experimentName.value.trim() || null,
+      batteryFields: [...batteryFields.value],
+      specialFields: [...specialFields.value],
+      fieldValues: JSON.parse(JSON.stringify(detailFieldValues)) as Record<string, Record<string, string>>,
+    }
+    const saved = await saveTestConsoleDetail(props.testId, detail)
+    applyConsoleDetail(saved.consoleDetail)
+    ElMessage.success('测试详情已保存')
+  } catch (error) {
+    ElMessage.error(error instanceof Error ? error.message : '测试详情保存失败')
+  } finally { detailSaving.value = false }
+}
+
 function openFieldTemplate(mode: 'battery' | 'special') {
   templateMode.value = mode
   selectedBatteryFields.value = [...batteryFields.value]
@@ -204,26 +214,12 @@
   ElMessage.success('字段模板已应用')
 }
 
-async function saveSpecialTemplate() {
-  if (!specialFields.value.length) {
-    ElMessage.warning('请先添加特殊字段')
-    return
-  }
-  try {
-    const result = await ElMessageBox.prompt('请输入特殊字段模板名称', '另存特殊字段模板', {
-      confirmButtonText: '保存',
-      cancelButtonText: '取消',
-      inputPlaceholder: '输入模板名称',
-      inputValidator: value => value.trim() ? true : '请输入模板名称',
-    })
-    ElMessage.success(`已另存为“${result.value.trim()}”`)
-  } catch {
-    // 用户取消另存。
-  }
-}
-
 function updateSeriesColor(index: number, color: string | null) {
   if (color) seriesColors.value[index] = color
+}
+
+function ensureSelectedCell() {
+  if (!selectedCellNos.value.length && props.cells.length) selectedCellNos.value = [props.cells[0].no]
 }
 
 function metricMatches(metric: CompareMetricKey, record: PersistedTestRecord) {
@@ -239,6 +235,16 @@
   return false
 }
 
+function applyConsoleDetail(detail: TestConsoleDetail | null | undefined) {
+  if (!detail) return
+  experimentName.value = detail.experimentName || ''
+  if (Array.isArray(detail.batteryFields)) batteryFields.value = [...detail.batteryFields]
+  if (Array.isArray(detail.specialFields)) specialFields.value = [...detail.specialFields]
+  Object.keys(detailFieldValues).forEach(key => delete detailFieldValues[key])
+  Object.entries(detail.fieldValues || {}).forEach(([field, values]) => { detailFieldValues[field] = { ...values } })
+  ensureDetailFieldValues()
+}
+
 function metricValue(metric: CompareMetricKey, record: PersistedTestRecord) {
   if (metric === 'voltage') return Number(record.voltage)
   if (metric === 'current') return Number(record.current)
@@ -249,9 +255,11 @@
 function selectedRecords() {
   const option = stepOptions.value.find(item => item.value === selectedStep.value)
   if (!option) return []
-  const cycle = selectedCycle.value ? Number(selectedCycle.value) : option.cycleNo > 0 ? option.cycleNo : null
+  const cycle = Number(selectedCycle.value)
+  if (!Number.isFinite(cycle) || cycle <= 0) return []
   return props.records.filter(record => {
-    if (cycle !== null && Number(record.cycleNo) !== cycle) return false
+    if (record.sourceType !== 'SNAPSHOT') return false
+    if (Number(record.cycleNo) !== cycle) return false
     const stepNo = Number(record.stepNo)
     if (option.logical) return stepNo >= option.logical.startStepNo && stepNo <= option.logical.endStepNo
     return stepNo === option.stepNo
@@ -357,15 +365,15 @@
           </tbody>
         </table>
       </div>
-      <div class="detail-actions"><button class="primary-button" type="button" @click="addCustomField"><el-icon><Plus /></el-icon>添加项</button><button class="secondary-button" type="button" @click="openFieldTemplate('battery')">电池组字段模板</button><button class="secondary-button" type="button" @click="openFieldTemplate('special')">选择特殊字段模板</button><button class="secondary-button" type="button" @click="saveSpecialTemplate">另存特殊字段模板</button></div>
+      <div class="detail-actions"><button class="primary-button" type="button" :disabled="detailSaving" @click="saveConsoleDetail"><el-icon><Check /></el-icon>保存详情</button><button class="secondary-button" type="button" @click="addCustomField"><el-icon><Plus /></el-icon>添加项</button><button class="secondary-button" type="button" @click="openFieldTemplate('battery')">电池组字段模板</button><button class="secondary-button" type="button" @click="openFieldTemplate('special')">选择特殊字段模板</button></div>
     </section>
 
     <section class="data-select-panel">
       <header class="panel-heading"><div><strong>数据选择</strong><el-icon><ArrowUp /></el-icon></div></header>
       <div class="data-select-content">
-        <label class="step-select"><span>循环</span><el-select v-model="selectedCycle" clearable placeholder="跟随工步"><el-option v-for="item in cycleOptions" :key="item" :label="`循环 ${item}`" :value="String(item)" /></el-select></label>
-        <label class="step-select"><span>循环工步</span><el-select v-model="selectedStep" :disabled="!stepOptions.length" placeholder="暂无实际工步"><el-option v-for="item in stepOptions" :key="item.value" :label="item.label" :value="item.value" /></el-select></label>
-        <div class="battery-select"><span>电池号</span><el-checkbox-group v-model="selectedCellNos"><el-checkbox v-for="cell in cells" :key="cell.no" :value="cell.no">{{ cell.no }}</el-checkbox></el-checkbox-group></div>
+        <label class="step-select"><span>循环</span><el-select v-model="selectedCycle" :disabled="!cycleOptions.length" placeholder="暂无实时循环"><el-option v-for="item in cycleOptions" :key="item" :label="`循环 ${item}`" :value="String(item)" /></el-select></label>
+        <label class="step-select"><span>循环工步</span><el-select v-model="selectedStep" :disabled="!stepOptions.length" placeholder="暂无该循环工步"><el-option v-for="item in stepOptions" :key="item.value" :label="item.label" :value="item.value" /></el-select></label>
+        <div class="battery-select"><span>电池号</span><el-checkbox-group v-model="selectedCellNos" @change="ensureSelectedCell"><el-checkbox v-for="cell in cells" :key="cell.no" :value="cell.no">{{ cell.no }}</el-checkbox></el-checkbox-group></div>
       </div>
     </section>
 

--
Gitblit v1.7.1