From 9dfc7d64b00a704b7e77a6000c1dcadd66b0d345 Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期五, 09 十月 2026 15:52:09 +0800
Subject: [PATCH] 工步名称的说明同时覆盖实时页

---
 src/components/ConsoleDataListPanel.vue |   54 ++++++++++++++++++++++++++++++++++++++++++++++++------
 1 files changed, 48 insertions(+), 6 deletions(-)

diff --git a/src/components/ConsoleDataListPanel.vue b/src/components/ConsoleDataListPanel.vue
index d7fa6fa..f5b488e 100644
--- a/src/components/ConsoleDataListPanel.vue
+++ b/src/components/ConsoleDataListPanel.vue
@@ -54,12 +54,14 @@
   negativePotential: string
   extensionMeasurements: string
   sourceIdentity: string
+  sourceRecordId: string
   collectorDeviceTime: string
   chargeCapacity: string
   dischargeCapacity: string
   efficiency: string
   chargeEnergy: string
   dischargeEnergy: string
+  sourceKind: string
 }
 const props = defineProps<{ testId: number; testName: string; cells: ConsoleCell[]; canImport?: boolean; receptionStatus?: string; bindings?: Array<{ batteryNo: string; bindingId?: number | null; channelId?: number | null; devid?: number | null; chlid?: number | null }>}>()
 const route = useRoute()
@@ -76,6 +78,14 @@
   { key: 'step', label: '工步' },
   { key: 'cycle', label: '循环' },
 ] as const
+// 循环统计的来源类别:设备与文件给的值原样展示,推导值必须说明它是聚合出来的。
+const cycleSourceLabels: Record<string, string> = { DEVICE: '设备提供', AGGREGATED: '工步层聚合', IMPORTED: '文件导入' }
+const CYCLE_SOURCE_UNKNOWN = '未标注'
+const cycleSourceTitle = '「工步层聚合」表示设备没有提供循环汇总行,该循环的容量、能量与效率由本系统按工步层同方向工步求和得出;推导结果可能带有设备工步行里的偏差。'
+const hasAggregatedCycles = computed(() => cycleRows.value.some(row => row.sourceKind === cycleSourceLabels.AGGREGATED))
+// 本来源的工步方案里只有「步_N」标号,没有描述性名称:那里的工步名称是本系统按电流符号判定的方向(充电/放电/搁置)。
+const derivedStepNames = ['充电', '放电', '搁置']
+const hasDerivedStepNames = computed(() => [...rows.value, ...stepRows.value].some(row => derivedStepNames.includes(String(row.stepName || '').trim())))
 const filters = reactive<FilterState>({ batteryNo: '', dateRange: null, cycleNo: '', stepNo: '' })
 const appliedFilters = ref<FilterState>({ batteryNo: '', dateRange: null, cycleNo: '', stepNo: '' })
 const currentPage = ref(1)
@@ -117,7 +127,7 @@
         ],
 )
 function emptyRow(base: Partial<DataRow>): DataRow {
-  return { sourceIdentity: '-', collectorDeviceTime: '-', id: '', sourceType: 'CABINET', metricCode: '-', metricValue: '-', unit: '-', collectorCode: '-', channelCode: '-', recordNo: '', batteryNo: '', cycleNo: 0, stepNo: 0, stepName: '', actualStepLabel: '-', sequenceId: 0, relativeTimeSeconds: '-', recordedAt: '', absoluteTime: '', serverReceivedAt: '', stepDuration: '-', startVoltage: '-', endVoltage: '-', chargeMedianVoltage: '-', dischargeMedianVoltage: '-', voltage: '-', current: '-', capacity: '-', energy: '-', temperature: '-', positivePotential: '-', negativePotential: '-', extensionMeasurements: '-', chargeCapacity: '-', dischargeCapacity: '-', efficiency: '-', chargeEnergy: '-', dischargeEnergy: '-', ...base }
+  return { sourceIdentity: '-', sourceRecordId: '-', collectorDeviceTime: '-', id: '', sourceType: 'CABINET', metricCode: '-', metricValue: '-', unit: '-', collectorCode: '-', channelCode: '-', recordNo: '', batteryNo: '', cycleNo: 0, stepNo: 0, stepName: '', actualStepLabel: '-', sequenceId: 0, relativeTimeSeconds: '-', recordedAt: '', absoluteTime: '', serverReceivedAt: '', stepDuration: '-', startVoltage: '-', endVoltage: '-', chargeMedianVoltage: '-', dischargeMedianVoltage: '-', voltage: '-', current: '-', capacity: '-', energy: '-', temperature: '-', positivePotential: '-', negativePotential: '-', extensionMeasurements: '-', chargeCapacity: '-', dischargeCapacity: '-', efficiency: '-', chargeEnergy: '-', dischargeEnergy: '-', sourceKind: '-', ...base }
 }
 function mapRecord(record: PersistedTestRecord): DataRow {
   const deviceTime = formatTime(record.deviceTime ?? record.recordedAt)
@@ -134,7 +144,7 @@
       extensionMeasurements = Object.entries(parsed as Record<string, { value?: unknown; metricValue?: unknown; unit?: string; channelCode?: string }>).map(([code, value]) => `${code}${value?.channelCode ? ` (${value.channelCode})` : ''}=${value?.value ?? value?.metricValue ?? '-'}${value?.unit ? ` ${value.unit}` : ''}`).join('; ') || '-'
     } catch { extensionMeasurements = String(record.extensionMeasurementsJson) }
   }
-  return emptyRow({ sourceIdentity: record.sourceIdentity || '-', collectorDeviceTime: formatTime(record.collectorDeviceTime), id: identity, sourceType: record.sourceType || 'CABINET', metricCode: record.metricCode || '-', metricValue: extensionValue, unit: record.unit || '-', collectorCode: record.collectorCode || String(record.collectorId ?? '-'), channelCode: record.channelCode || '-', recordNo: identity, batteryNo: record.batteryNo || '-', cycleNo: Number(record.cycleNo || 0), stepNo: Number(record.stepNo || 0), stepName: collector ? '-' : (record.stepName || '-'), sequenceId: Number(record.sequenceId || record.cabinetSequenceId || 0), recordedAt: deviceTime, relativeTimeSeconds: formatTestDisplayValue('relativeTimeSeconds', record.relativeTimeSeconds), absoluteTime: deviceTime, serverReceivedAt: formatTime(record.serverReceivedAt), voltage: collector ? '-' : (record.voltage == null ? '-' : Number(record.voltage).toFixed(4)), current: collector ? '-' : formatTestDisplayValue('current', record.current), capacity: collector ? '-' : formatTestDisplayValue('capacity', record.capacity), energy: collector ? '-' : formatTestDisplayValue('energy', record.energy), temperature: snapshot ? (record.temperature == null ? '-' : Number(record.temperature).toFixed(4)) : extension('TEMPERATURE'), positivePotential: snapshot ? (record.positivePotential == null ? '-' : Number(record.positivePotential).toFixed(4)) : extension('POSITIVE_POTENTIAL'), negativePotential: snapshot ? (record.negativePotential == null ? '-' : Number(record.negativePotential).toFixed(4)) : extension('NEGATIVE_POTENTIAL'), extensionMeasurements })
+  return emptyRow({ sourceIdentity: record.sourceIdentity || '-', sourceRecordId: record.sourceRecordId || '-', collectorDeviceTime: formatTime(record.collectorDeviceTime), id: identity, sourceType: record.sourceType || 'CABINET', metricCode: record.metricCode || '-', metricValue: extensionValue, unit: record.unit || '-', collectorCode: record.collectorCode || String(record.collectorId ?? '-'), channelCode: record.channelCode || '-', recordNo: identity, batteryNo: record.batteryNo || '-', cycleNo: Number(record.cycleNo || 0), stepNo: Number(record.stepNo || 0), stepName: collector ? '-' : (record.stepName || '-'), sequenceId: Number(record.sequenceId || record.cabinetSequenceId || 0), recordedAt: deviceTime, relativeTimeSeconds: formatTestDisplayValue('relativeTimeSeconds', record.relativeTimeSeconds), absoluteTime: deviceTime, serverReceivedAt: formatTime(record.serverReceivedAt), voltage: collector ? '-' : (record.voltage == null ? '-' : Number(record.voltage).toFixed(4)), current: collector ? '-' : formatTestDisplayValue('current', record.current), capacity: collector ? '-' : formatTestDisplayValue('capacity', record.capacity), energy: collector ? '-' : formatTestDisplayValue('energy', record.energy), temperature: snapshot ? (record.temperature == null ? '-' : Number(record.temperature).toFixed(4)) : extension('TEMPERATURE'), positivePotential: snapshot ? (record.positivePotential == null ? '-' : Number(record.positivePotential).toFixed(4)) : extension('POSITIVE_POTENTIAL'), negativePotential: snapshot ? (record.negativePotential == null ? '-' : Number(record.negativePotential).toFixed(4)) : extension('NEGATIVE_POTENTIAL'), extensionMeasurements })
 }
 function mapStep(record: PersistedTestStep): DataRow {
   const startedAt = formatTime(record.recordedAt)
@@ -144,7 +154,7 @@
 function mapCycle(record: PersistedTestCycle): DataRow {
   const startedAt = formatTime(record.recordedAt)
   const identity = record.stableIdentity || `cycle:${[record.adapterId, record.devtype, record.deviceId, record.subdevid, record.channelId, record.cycleNo].join(':')}`
-  return emptyRow({ id: identity, recordNo: identity, batteryNo: record.batteryNo, cycleNo: Number(record.cycleNo), sequenceId: Number(record.sequenceId || 0), recordedAt: startedAt, absoluteTime: formatTime(record.absoluteTime || record.recordedAt), serverReceivedAt: formatTime(record.serverReceivedAt), chargeCapacity: formatTestDisplayValue('chargeCapacity', record.chargeCapacity), dischargeCapacity: formatTestDisplayValue('dischargeCapacity', record.dischargeCapacity), efficiency: record.efficiency == null ? '-' : Number(record.efficiency).toFixed(2), chargeEnergy: formatTestDisplayValue('chargeEnergy', record.chargeEnergy), dischargeEnergy: formatTestDisplayValue('dischargeEnergy', record.dischargeEnergy) })
+  return emptyRow({ id: identity, recordNo: identity, batteryNo: record.batteryNo, cycleNo: Number(record.cycleNo), sequenceId: Number(record.sequenceId || 0), recordedAt: startedAt, absoluteTime: formatTime(record.absoluteTime || record.recordedAt), serverReceivedAt: formatTime(record.serverReceivedAt), chargeCapacity: formatTestDisplayValue('chargeCapacity', record.chargeCapacity), dischargeCapacity: formatTestDisplayValue('dischargeCapacity', record.dischargeCapacity), efficiency: record.efficiency == null ? '-' : Number(record.efficiency).toFixed(2), chargeEnergy: formatTestDisplayValue('chargeEnergy', record.chargeEnergy), dischargeEnergy: formatTestDisplayValue('dischargeEnergy', record.dischargeEnergy), sourceKind: cycleSourceLabels[String(record.sourceKind || 'DEVICE')] || CYCLE_SOURCE_UNKNOWN })
 }
 async function loadRecords() {
   loading.value = true
@@ -174,6 +184,11 @@
   ? [{ label: '扩展值', value: 'metricValue' as keyof DataRow }]
   : fieldOptions.value)
 const batteryOptions = computed(() => [...new Set(sourceRows.value.map(row => row.batteryNo).filter(Boolean))])
+// 最近数据时间取本测试已接收实时记录里最新的设备绝对时间,与展示刷新节奏无关。
+const latestRealtimeDataAt = computed(() => {
+  const times = rows.value.map(row => row.absoluteTime).filter(value => value && value !== '-')
+  return times.length ? times.reduce((latest, current) => (current > latest ? current : latest)) : '-'
+})
 const cycleOptions = computed(() => [...new Set(sourceRows.value.map(row => row.cycleNo))].sort((a, b) => a - b))
 const stepOptions = computed(() => [...new Map(sourceRows.value.map(row => [row.stepNo, row.stepName])).entries()].filter(item => item[0] > 0).sort((a, b) => a[0] - b[0]))
 const filteredRows = computed(() =>
@@ -347,8 +362,12 @@
     return
   }
   const columns = consoleImportColumns[dataType.value]
-  const headers = Object.values(columns)
-  const records = filteredRows.value.map(row => Object.keys(columns).map(field => row[field as keyof DataRow]))
+  // 导出与界面一致地带上来源列:循环的推导值不能在被导出后失去"聚合来源"这个事实;
+  // 实时的"来源定位"是设备侧自己的对照串(如厂商库名与记录号),导出后仍能拿它跟厂商软件核对。
+  const extraColumn = activeTab.value === 'cycle' ? { field: 'sourceKind' as const, label: '数据来源' }
+    : activeTab.value === 'realtime' ? { field: 'sourceRecordId' as const, label: '来源定位' } : null
+  const headers = [...Object.values(columns), ...(extraColumn ? [extraColumn.label] : [])]
+  const records = filteredRows.value.map(row => [...Object.keys(columns).map(field => row[field as keyof DataRow]), ...(extraColumn ? [row[extraColumn.field]] : [])])
   const blob = new Blob([`\uFEFF${[headers, ...records].map(item => item.map(escapeCsv).join(',')).join('\n')}`], { type: 'text/csv;charset=utf-8' })
   const url = URL.createObjectURL(blob)
   const anchor = document.createElement('a')
@@ -377,6 +396,9 @@
       </nav>
       <small>{{ loading ? '加载中...' : `共 ${filteredRows.length} 条` }}</small>
     </header>
+    <p v-if="activeTab === 'realtime'" class="realtime-hint">最近数据时间:{{ latestRealtimeDataAt }} · 设备约 60 秒产生一行新数据,展示刷新比它快是正常的</p>
+    <p v-if="activeTab === 'cycle' && hasAggregatedCycles" class="realtime-hint">来源标注「工步层聚合」表示设备没有提供循环汇总行,该循环由本系统按工步层同方向工步求和得出,不是设备事实;界面与导出都带这一列。</p>
+    <p v-if="(activeTab === 'realtime' || activeTab === 'step') && hasDerivedStepNames" class="realtime-hint">工步名称「充电 / 放电 / 搁置」是本系统按电流符号判定的方向,不是设备给的名称:该来源的工步方案里只有「步_N」标号与操作类型,没有描述性名称。</p>
     <div class="filter-grid">
       <label
         ><span>电池</span><el-select v-model="filters.batteryNo" clearable placeholder="全部"><el-option label="全部" value="" /><el-option v-for="item in batteryOptions" :key="item" :label="item" :value="item" /></el-select></label
@@ -430,6 +452,8 @@
               <el-checkbox v-if="activeTab === 'realtime'" :model-value="allPageSelected" :indeterminate="somePageSelected" aria-label="选择本页" @change="(checked: string | number | boolean) => togglePageSelection(Boolean(checked))" />
             </th>
             <th v-for="(label, field) in consoleImportColumns[dataType]" :key="field">{{ label }}</th>
+            <th v-if="activeTab === 'cycle'" :title="cycleSourceTitle">数据来源</th>
+            <th v-if="activeTab === 'realtime'" title="设备侧的对照串(厂商库名与记录号等),用于跟厂商软件核对">来源定位</th>
             <th class="action-column">操作</th>
           </tr>
         </thead>
@@ -468,6 +492,7 @@
               <td><el-input v-if="editing && draftRows[row.id]" v-model="draftRows[row.id].temperature" size="small" /><span v-else>{{ row.temperature }}</span></td>
               <td><el-input v-if="editing && draftRows[row.id]" v-model="draftRows[row.id].positivePotential" size="small" /><span v-else>{{ row.positivePotential }}</span></td>
               <td><el-input v-if="editing && draftRows[row.id]" v-model="draftRows[row.id].negativePotential" size="small" /><span v-else>{{ row.negativePotential }}</span></td>
+              <td class="source-cell" :title="row.sourceRecordId">{{ row.sourceRecordId }}</td>
               <td class="action-column">
                 <div class="row-actions"><button type="button" @click="openRowEditor(row)">编辑</button><button class="delete-action" type="button" @click="deleteSelected([row.id])">删除</button></div>
               </td>
@@ -544,13 +569,14 @@
                 <el-input v-if="editing && draftRows[row.id]" v-model="draftRows[row.id].dischargeEnergy" size="small" />
                 <span v-else>{{ row.dischargeEnergy }}</span>
               </td>
+              <td :title="cycleSourceTitle">{{ row.sourceKind }}</td>
               <td class="action-column">
                 <div class="row-actions"><button type="button" @click="openRowEditor(row)">编辑</button><button class="delete-action" type="button" @click="deleteSelected([row.id])">删除</button></div>
               </td>
             </tr></template
           >
           <tr v-if="!loading && !pagedRows.length">
-            <td :colspan="activeTab === 'realtime' ? 17 : activeTab === 'step' ? 14 : 10" class="empty-cell">
+            <td :colspan="activeTab === 'realtime' ? 18 : activeTab === 'step' ? 14 : 11" class="empty-cell">
               {{ loadError || '暂无检测柜记录' }}
             </td>
           </tr>
@@ -650,6 +676,14 @@
 .section-heading strong {
   color: var(--ink);
   font-size: 14px;
+}
+.realtime-hint {
+  margin: 0;
+  padding: 9px 16px;
+  border-bottom: 1px solid var(--line);
+  color: #71808d;
+  background: #f8fbfe;
+  font-size: 11px;
 }
 .section-heading span {
   color: #81909d;
@@ -816,6 +850,14 @@
   white-space: normal;
   overflow-wrap: anywhere;
 }
+/* 来源定位是设备侧的对照串(可能较长),窄列里允许换行显示 */
+.source-cell {
+  width: 220px;
+  font-family: 'Cascadia Mono', Consolas, monospace;
+  font-size: 11px;
+  white-space: normal;
+  overflow-wrap: anywhere;
+}
 .console-data-panel.realtime table {
   min-width: 2100px;
 }

--
Gitblit v1.7.1