From d2aee81f665ffdd628dc88c8fca4c7c80aaad5ea Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期四, 03 九月 2026 15:03:37 +0800
Subject: [PATCH] 项目图表读取当前有效数据

---
 src/components/ConsoleProjectChartPanel.vue |  145 +++++++++++++-----------------------------------
 1 files changed, 39 insertions(+), 106 deletions(-)

diff --git a/src/components/ConsoleProjectChartPanel.vue b/src/components/ConsoleProjectChartPanel.vue
index 6ef6cf1..febf352 100644
--- a/src/components/ConsoleProjectChartPanel.vue
+++ b/src/components/ConsoleProjectChartPanel.vue
@@ -5,129 +5,62 @@
 import { CanvasRenderer } from 'echarts/renderers'
 import { GridComponent, LegendComponent, TooltipComponent } from 'echarts/components'
 import VChart from 'vue-echarts'
+import type { PersistedTestCycle, PersistedTestStep } from '@/api/tests'
 
-interface ConsoleCell {
-  no: string
-  voltage: string
-  temperature: string
-  positive: string
-  negative: string
-  capacity: string
-  current: string
-}
-
-interface ProjectMetric {
-  key: 'finalVoltage1' | 'finalVoltage2' | 'dischargeCapacityF'
-  title: string
-  unit: string
-}
-
-const props = defineProps<{
-  cells: ConsoleCell[]
-}>()
-
+interface ConsoleCell { no: string }
+type MetricKey = 'finalVoltage1' | 'finalVoltage2' | 'dischargeCapacityF'
+interface ProjectMetric { key: MetricKey; title: string; unit: string }
+const props = defineProps<{ cells: ConsoleCell[]; steps: PersistedTestStep[]; cycles: PersistedTestCycle[]; capacityFactor: string }>()
 use([CanvasRenderer, LineChart, GridComponent, LegendComponent, TooltipComponent])
-
 const metrics: ProjectMetric[] = [
   { key: 'finalVoltage1', title: '最终电压1', unit: 'V' },
   { key: 'finalVoltage2', title: '最终电压2', unit: 'V' },
-  { key: 'dischargeCapacityF', title: '每循环放电末容量 / F', unit: 'F' },
+  { key: 'dischargeCapacityF', title: '每循环放电末容量 / F', unit: '' },
 ]
 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 cycleLabels = Array.from({ length: 61 }, (_, index) => String(index + 1))
 const chartUpdateOptions = { notMerge: true, lazyUpdate: false }
-
-watch(() => props.cells, cells => {
-  seriesColors.value = cells.map((_, index) => seriesColors.value[index] || palette[index % palette.length])
-}, { deep: true })
-
-const chartStateKey = computed(() => props.cells.map(cell => cell.no).join('|'))
-
-function metricBase(cell: ConsoleCell, metric: ProjectMetric) {
-  if (metric.key === 'finalVoltage1') return Number(cell.voltage)
-  if (metric.key === 'finalVoltage2') return Number(cell.positive)
-  return Number(cell.capacity) / 1000
+watch(() => props.cells, cells => { seriesColors.value = cells.map((_, index) => seriesColors.value[index] || palette[index % palette.length]) }, { deep: true })
+const cycleLabels = computed(() => [...new Set([...props.steps, ...props.cycles].map(row => Number(row.cycleNo)).filter(value => Number.isFinite(value) && value > 0))].sort((a, b) => a - b).map(String))
+const capacityFactor = computed(() => { const value = Number(props.capacityFactor); return Number.isFinite(value) && value !== 0 ? value : null })
+const capacityFactorUnavailable = computed(() => capacityFactor.value === null)
+const chartStateKey = computed(() => `${props.cells.map(cell => cell.no).join('|')}:${cycleLabels.value.join('|')}:${props.steps.length}:${props.cycles.length}:${props.capacityFactor}`)
+function numericValue(value: unknown): number | null {
+  if (value === null || value === undefined || (typeof value === 'string' && !value.trim())) return null
+  const number = Number(value)
+  return Number.isFinite(number) ? number : null
 }
-
-function seriesValues(cell: ConsoleCell, metric: ProjectMetric, cellIndex: number) {
-  const base = metricBase(cell, metric)
-  return cycleLabels.map((_, pointIndex) => {
-    const progress = pointIndex / (cycleLabels.length - 1)
-    const cycleWave = Math.sin(progress * Math.PI * 4 + cellIndex * 0.38)
-    const localWave = Math.cos(progress * Math.PI * 9 + cellIndex * 0.2)
-    if (metric.key === 'dischargeCapacityF') {
-      return Number((base * (1 - progress * 0.08) + cycleWave * 0.018 + localWave * 0.006).toFixed(4))
-    }
-    const direction = metric.key === 'finalVoltage1' ? 1 : -1
-    return Number((base + cycleWave * 0.075 + localWave * 0.018 + direction * progress * 0.04).toFixed(4))
-  })
+// 只读取当前有效工步;缺少该循环或最后工步终止电压时保留空点,不补值。
+function lastStepVoltage(batteryNo: string, cycleNo: number): number | null {
+  const rows = props.steps.filter(row => String(row.batteryNo) === batteryNo && Number(row.cycleNo) === cycleNo).sort((a, b) => Number(a.stepNo) - Number(b.stepNo))
+  return rows.length ? numericValue(rows[rows.length - 1].endVoltage) : null
 }
-
+// F 是当前电池组的非零数值除数,缺失循环或无效 F 均不生成曲线点。
+function dischargeCapacityPerFactor(batteryNo: string, cycleNo: number): number | null {
+  if (capacityFactor.value === null) return null
+  const row = props.cycles.find(item => String(item.batteryNo) === batteryNo && Number(item.cycleNo) === cycleNo)
+  const capacity = row ? numericValue(row.dischargeCapacity) : null
+  return capacity === null ? null : capacity / capacityFactor.value
+}
+function seriesValues(cell: ConsoleCell, metric: ProjectMetric): Array<number | null> {
+  return cycleLabels.value.map(label => metric.key === 'finalVoltage1' ? lastStepVoltage(cell.no, Number(label)) : dischargeCapacityPerFactor(cell.no, Number(label)))
+}
 function chartOption(metric: ProjectMetric) {
-  const seriesData = props.cells.map((cell, index) => seriesValues(cell, metric, index))
-  const values = seriesData.flat()
-  const dataMin = values.length ? Math.min(...values) : 0
-  const dataMax = values.length ? Math.max(...values) : 1
-  const margin = Math.max((dataMax - dataMin) * 0.15, metric.key === 'dischargeCapacityF' ? 0.03 : 0.08)
-  const createYAxis = (position: 'left' | 'right') => ({
-    type: 'value',
-    name: metric.unit,
-    position,
-    nameLocation: 'middle',
-    nameGap: 42,
-    nameRotate: position === 'left' ? 90 : -90,
-    min: Number((dataMin - margin).toFixed(3)),
-    max: Number((dataMax + margin).toFixed(3)),
-    nameTextStyle: { color: '#8795a3', fontSize: 10 },
-    axisLine: { show: true, lineStyle: { color: '#cbd8e4' } },
-    axisTick: { show: true, lineStyle: { color: '#cbd8e4' } },
-    axisLabel: { color: '#8795a3', fontSize: 10, margin: 8 },
-    splitLine: { show: position === 'left', lineStyle: { color: '#e8eef4' } },
-  })
-
-  return {
-    animation: false,
-    color: seriesColors.value,
-    grid: { top: 62, right: 62, bottom: 48, left: 62 },
-    tooltip: { trigger: 'axis' },
-    legend: {
-      top: 8,
-      left: 12,
-      right: 12,
-      type: 'scroll',
-      icon: 'roundRect',
-      itemWidth: 14,
-      itemHeight: 8,
-      itemGap: 16,
-      textStyle: { fontSize: 10, color: '#52606d' },
-    },
-    xAxis: {
-      type: 'category',
-      boundaryGap: false,
-      name: '循环次数',
-      nameLocation: 'middle',
-      nameGap: 30,
-      data: cycleLabels,
-      axisLine: { lineStyle: { color: '#cbd8e4' } },
-      axisLabel: { color: '#8795a3', fontSize: 10, interval: 9 },
-      nameTextStyle: { color: '#52606d', fontSize: 10 },
-    },
-    yAxis: [createYAxis('left'), createYAxis('right')],
-    series: props.cells.map((cell, index) => ({ name: cell.no, type: 'line', smooth: true, showSymbol: false, lineStyle: { width: 2 }, data: seriesData[index] })),
-  }
+  const hasDataSeries = metric.key !== 'finalVoltage2'
+  const seriesData = hasDataSeries ? props.cells.map(cell => seriesValues(cell, metric)) : []
+  const values = seriesData.flat().filter((value): value is number => value !== null)
+  const dataMin = values.length ? Math.min(...values) : undefined
+  const dataMax = values.length ? Math.max(...values) : undefined
+  const margin = dataMin === undefined || dataMax === undefined ? undefined : Math.max((dataMax - dataMin) * 0.15, 0.01)
+  return { animation: false, color: seriesColors.value, grid: { top: 62, right: 34, bottom: 48, left: 62 }, tooltip: { trigger: 'axis' }, legend: { top: 8, left: 12, right: 12, type: 'scroll', icon: 'roundRect', itemWidth: 14, itemHeight: 8, itemGap: 16, textStyle: { fontSize: 10, color: '#52606d' } }, xAxis: { type: 'category', boundaryGap: false, name: '循环次数', nameLocation: 'middle', nameGap: 30, data: cycleLabels.value, axisLine: { lineStyle: { color: '#cbd8e4' } }, axisLabel: { color: '#8795a3', fontSize: 10 }, nameTextStyle: { color: '#52606d', fontSize: 10 } }, yAxis: { type: 'value', name: metric.unit, nameLocation: 'middle', nameGap: metric.unit ? 42 : 0, min: margin === undefined ? undefined : dataMin! - margin, max: margin === undefined ? undefined : dataMax! + margin, axisLabel: { color: '#8795a3', fontSize: 10 }, splitLine: { lineStyle: { color: '#e8eef4' } } }, series: hasDataSeries ? props.cells.map((cell, index) => ({ name: cell.no, type: 'line', showSymbol: false, connectNulls: false, lineStyle: { width: 2 }, data: seriesData[index] })) : [] }
 }
 </script>
-
 <template>
   <section class="project-chart-panel">
-    <article v-for="metric in metrics" :key="metric.key" class="project-chart-card">
-      <header><strong>{{ metric.title }}</strong></header>
-      <VChart :key="`${metric.key}-${chartStateKey}`" class="project-chart" :option="chartOption(metric)" :update-options="chartUpdateOptions" autoresize />
-    </article>
+    <p v-if="capacityFactorUnavailable" class="factor-warning" role="status">电池组 F 不可用,未计算每循环放电末容量 / F。</p>
+    <article v-for="metric in metrics" :key="metric.key" class="project-chart-card"><header><strong>{{ metric.title }}</strong></header><VChart :key="`${metric.key}-${chartStateKey}`" class="project-chart" :option="chartOption(metric)" :update-options="chartUpdateOptions" autoresize /></article>
   </section>
 </template>
-
 <style scoped>
-.project-chart-panel{display:grid;gap:16px;min-width:0}.project-chart-card{min-width:0;border:1px solid #d8e1e9;background:#fff}.project-chart-card>header{height:44px;display:flex;align-items:center;padding:0 18px;border-bottom:1px solid #edf1f5}.project-chart-card>header strong{color:#26333f;font-size:13px}.project-chart{width:100%;height:270px}@media(max-width:680px){.project-chart{height:250px}.project-chart-card>header{padding:0 12px}}
+.project-chart-panel{display:grid;gap:16px;min-width:0}.project-chart-card{min-width:0;border:1px solid #d8e1e9;background:#fff}.project-chart-card>header{height:44px;display:flex;align-items:center;padding:0 18px;border-bottom:1px solid #edf1f5}.project-chart-card>header strong{color:#26333f;font-size:13px}.project-chart{width:100%;height:270px}.factor-warning{margin:0;padding:10px 14px;border:1px solid #efd59e;color:#8b5a14;background:#fff8e9;font-size:12px}@media(max-width:680px){.project-chart{height:250px}.project-chart-card>header{padding:0 12px}}
 </style>

--
Gitblit v1.7.1