From 7be6615a8906f0db4b4fefa1995dcaa6e3c6cdcb Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期三, 02 九月 2026 20:39:07 +0800
Subject: [PATCH] 让工步图表读取实时有效数据

---
 src/components/ConsoleChartPanel.vue |  142 +++++++++++++++++++++++++++++++++--------------
 1 files changed, 100 insertions(+), 42 deletions(-)

diff --git a/src/components/ConsoleChartPanel.vue b/src/components/ConsoleChartPanel.vue
index 2ef3740..b432da0 100644
--- a/src/components/ConsoleChartPanel.vue
+++ b/src/components/ConsoleChartPanel.vue
@@ -12,6 +12,7 @@
 } from '@/utils/compareReportPrint'
 import type { CompareMetricKey } from '@/stores/compareReports'
 import { useBatteryGroupFields } from '@/stores/batteryGroupFields'
+import type { PersistedTestRecord, TestLogicStepMapping } from '@/api/tests'
 
 interface ConsoleCell {
   no: string
@@ -28,17 +29,22 @@
   durationMinutes?: number
   testName?: string
   batteryGroup?: string
+  records: PersistedTestRecord[]
+  logicStepMappings?: TestLogicStepMapping[]
 }>(), {
   durationMinutes: 5,
   testName: '',
   batteryGroup: '',
+  records: () => [],
+  logicStepMappings: () => [],
 })
 
 use([CanvasRenderer, LineChart, GridComponent, LegendComponent, TooltipComponent])
 
 const palette = ['#3978f6', '#7d42d8', '#f08a3e', '#23a983', '#e15268', '#1b9ac8', '#b5892f', '#6957c8', '#4f8d44', '#d264a3', '#46748e', '#a95d37']
 const seriesColors = ref<string[]>(props.cells.map((_, index) => palette[index % palette.length]))
-const selectedStep = ref('5-21 恒流充电')
+const selectedStep = ref('')
+const selectedCycle = ref('')
 const selectedCellNos = ref<string[]>(props.cells.map(cell => cell.no))
 const experimentName = ref(props.testName || 'XXX实验')
 const batteryFields = ref(['额定容量', '电流', '内阻'])
@@ -69,9 +75,37 @@
   循环寿命模板: ['容量保持率', '能量效率', '循环次数', '内阻变化'],
   热特性模板: ['最高温度', '温差', '温升速率'],
 }
-const stepOptions = ['5-21 恒流充电', '5-22 搁置', '5-23 恒流放电', '5-24 搁置']
-const pointCount = 31
 const chartUpdateOptions = { notMerge: true, lazyUpdate: false }
+
+interface StepOption {
+  value: string
+  label: string
+  cycleNo: number
+  stepNo?: number
+  logical?: TestLogicStepMapping
+}
+
+const cycleOptions = computed(() => [...new Set(props.records
+  .map(record => Number(record.cycleNo))
+  .filter(value => Number.isFinite(value) && value > 0))].sort((a, b) => a - b))
+
+const stepOptions = computed<StepOption[]>(() => {
+  const actual = new Map<string, StepOption>()
+  for (const record of props.records) {
+    const cycleNo = Number(record.cycleNo)
+    const stepNo = Number(record.stepNo)
+    if (!Number.isFinite(cycleNo) || !Number.isFinite(stepNo) || cycleNo <= 0 || stepNo <= 0) continue
+    const value = `actual:${cycleNo}:${stepNo}`
+    if (!actual.has(value)) actual.set(value, { value, label: `${cycleNo}-${stepNo} ${record.stepName || '-'}`, cycleNo, stepNo })
+  }
+  const logical: 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 })
+    }
+  }
+  return [...actual.values(), ...logical]
+})
 
 function ensureDetailFieldValues() {
   detailFields.value.forEach(field => {
@@ -97,6 +131,19 @@
 }, { deep: true })
 
 watch(detailFields, ensureDetailFieldValues)
+
+watch(stepOptions, options => {
+  if (!options.length) {
+    selectedStep.value = ''
+    return
+  }
+  if (!options.some(option => option.value === selectedStep.value)) selectedStep.value = options[0].value
+}, { immediate: true })
+
+watch(cycleOptions, options => {
+  const current = Number(selectedCycle.value)
+  if (selectedCycle.value && !options.includes(current)) selectedCycle.value = ''
+})
 
 async function addCustomField() {
   try {
@@ -163,48 +210,55 @@
   if (color) seriesColors.value[index] = color
 }
 
-function elapsedLabel(index: number) {
-  const totalSeconds = Math.round(props.durationMinutes * 60 * index / (pointCount - 1))
-  const hours = Math.floor(totalSeconds / 3600)
-  const minutes = Math.floor((totalSeconds % 3600) / 60)
-  return `${hours}:${String(minutes).padStart(2, '0')}`
+function metricMatches(metric: CompareMetricKey, record: PersistedTestRecord) {
+  if (metric === 'voltage') return record.sourceType !== 'COLLECTOR' && record.voltage != null
+  if (metric === 'current') return record.sourceType !== 'COLLECTOR' && record.current != null
+  if (record.sourceType !== 'COLLECTOR') return false
+  const code = String(record.metricCode || '').toUpperCase()
+  if (metric === 'temperature') return code === 'TEMPERATURE' || code.includes('TEMPERATURE')
+  if (metric === 'positiveVoltage') return code === 'POSITIVE_POTENTIAL' || code.includes('POSITIVE_POTENTIAL')
+  if (metric === 'negativeVoltage') return code === 'NEGATIVE_POTENTIAL' || code.includes('NEGATIVE_POTENTIAL')
+  return false
 }
 
-function baseValue(cell: ConsoleCell, metric: CompareMetricDefinition) {
-  if (metric.key === 'voltage') return Number(cell.voltage)
-  if (metric.key === 'current') return Number(cell.current) / 1000
-  if (metric.key === 'positiveVoltage') return Number(cell.positive)
-  if (metric.key === 'negativeVoltage') return Number(cell.negative)
-  return Number(cell.temperature)
+function metricValue(metric: CompareMetricKey, record: PersistedTestRecord) {
+  if (metric === 'voltage') return Number(record.voltage)
+  if (metric === 'current') return Number(record.current)
+  return Number(record.metricValue)
 }
 
-function seriesValues(cell: ConsoleCell, metric: CompareMetricDefinition, cellIndex: number) {
-  const base = baseValue(cell, metric)
-  const isRest = selectedStep.value.includes('搁置')
-  const isDischarge = selectedStep.value.includes('放电')
-  return Array.from({ length: pointCount }, (_, pointIndex) => {
-    const progress = pointIndex / (pointCount - 1)
-    const wave = Math.sin(progress * Math.PI * 3 + cellIndex * 0.45)
-    const ripple = Math.cos(progress * Math.PI * 7 + cellIndex * 0.18)
-    const amplitude = metric.key === 'temperature' ? 1.8 : metric.key === 'current' ? 0.55 : 0.12
-    if (isRest) {
-      const restBase = metric.key === 'current' ? 0 : base
-      const restAmplitude = metric.key === 'temperature' ? 0.35 : metric.key === 'current' ? 0.04 : 0.025
-      return Number((restBase + wave * restAmplitude + ripple * restAmplitude * 0.18).toFixed(3))
-    }
-    const trend = metric.key === 'temperature'
-      ? progress * (isDischarge ? 1.2 : 2.6)
-      : metric.key === 'current'
-        ? progress * (isDischarge ? -0.42 : 0.3)
-        : progress * (isDischarge ? -0.3 : 0.22)
-    return Number((base + wave * amplitude + ripple * amplitude * 0.22 + trend).toFixed(3))
+function selectedRecords() {
+  const option = stepOptions.value.find(item => item.value === selectedStep.value)
+  if (!option) return []
+  const cycle = selectedCycle.value ? Number(selectedCycle.value) : option.cycleNo
+  return props.records.filter(record => {
+    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
   })
 }
 
+const hasSelectedRecords = computed(() => selectedRecords().length > 0)
+
+function recordTime(record: PersistedTestRecord) {
+  const value = record.deviceTime || record.recordedAt
+  const parsed = Date.parse(value || '')
+  return Number.isFinite(parsed) ? parsed : null
+}
+
 function chartOption(metric: CompareMetricDefinition) {
+  const source = selectedRecords()
   const cellIndexes = selectedCells.value.map(cell => props.cells.findIndex(item => item.no === cell.no))
-  const seriesData = selectedCells.value.map((cell, index) => seriesValues(cell, metric, Math.max(0, cellIndexes[index])))
-  const values = seriesData.flat()
+  const seriesData = selectedCells.value.map(cell => {
+    return source.filter(record => record.batteryNo === cell.no && metricMatches(metric.key, record))
+      .map(record => ({ time: recordTime(record), value: metricValue(metric.key, record) }))
+      .filter(point => point.time !== null && Number.isFinite(point.value))
+      .sort((a, b) => (a.time as number) - (b.time as number))
+  })
+  const timestamps = [...new Set(seriesData.flat().map(point => point.time as number))].sort((a, b) => a - b)
+  const start = timestamps[0]
+  const values = seriesData.flat().map(point => point.value)
   const dataMin = values.length ? Math.min(...values) : 0
   const dataMax = values.length ? Math.max(...values) : 1
   const margin = Math.max((dataMax - dataMin) * 0.16, metric.key === 'temperature' ? 1 : 0.08)
@@ -243,10 +297,10 @@
     xAxis: {
       type: 'category',
       boundaryGap: false,
-      name: '时间/h:mm',
+      name: '设备时间',
       nameLocation: 'middle',
       nameGap: 29,
-      data: Array.from({ length: pointCount }, (_, index) => elapsedLabel(index)),
+      data: timestamps.map(value => start == null ? new Date(value).toISOString() : `${((value - start) / 3600000).toFixed(3)} h`),
       axisLine: { lineStyle: { color: '#cbd8e4' } },
       axisLabel: { color: '#8795a3', fontSize: 10, interval: 4 },
       nameTextStyle: { color: '#52606d', fontSize: 10 },
@@ -258,7 +312,10 @@
       smooth: true,
       showSymbol: false,
       lineStyle: { width: 2 },
-      data: seriesData[index],
+      data: timestamps.map(timestamp => {
+        const point = seriesData[index].find(item => item.time === timestamp)
+        return point ? point.value : null
+      }),
     })),
   }
 }
@@ -287,7 +344,8 @@
     <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="selectedStep"><el-option v-for="item in stepOptions" :key="item" :label="item" :value="item" /></el-select></label>
+        <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>
       </div>
     </section>
@@ -305,13 +363,13 @@
         </div>
       </div>
 
-      <div v-if="selectedCells.length && visibleMetrics.length" class="charts-grid">
+      <div v-if="selectedCells.length && visibleMetrics.length && hasSelectedRecords" class="charts-grid">
         <article v-for="metric in visibleMetrics" :key="metric.key" class="chart-card">
           <header><strong>{{ metric.title }}</strong></header>
           <VChart :key="`${metric.key}-${selectedStep}-${selectedCellNos.join('|')}`" class="metric-chart" :option="chartOption(metric)" :update-options="chartUpdateOptions" autoresize />
         </article>
       </div>
-      <div v-else class="chart-empty">暂无可展示数据</div>
+      <div v-else class="chart-empty">暂无当前测试实时数据</div>
     </section>
 
     <el-dialog v-model="templateDialogVisible" :title="templateMode === 'battery' ? '选择电池组字段模板' : '选择特殊字段模板'" width="620px" align-center>

--
Gitblit v1.7.1