yeg
2026-09-09 b226aa0db7fbecb8fff92476537adc0ca3e16a4f
src/components/ConsoleDataListPanel.vue
@@ -8,6 +8,8 @@
import { consoleImportColumns, submitConsoleDataAction, type ConsoleDataType } from '@/api/consoleData'
import { formatBusinessTime as formatTime } from '@/utils/businessTime'
import { formatTestDisplayValue, toTestApiValue } from '@/utils/testDataUnits'
import { createConsoleImportWorkbook } from '@/utils/consoleDataImport'
import { parseTestDuration } from '@/utils/testDataDuration'
type DataTab = 'realtime' | 'step' | 'cycle'
interface ConsoleCell {
@@ -35,6 +37,7 @@
  actualStepLabel: string
  sequenceId: number
  recordedAt: string
  relativeTimeSeconds: string
  absoluteTime: string
  serverReceivedAt: string
  stepDuration: string
@@ -97,7 +100,7 @@
        ]
    : activeTab.value === 'step'
      ? [
          { label: '工步时间', value: 'stepDuration' },
          { label: '工步时间(h:min:s.ms)', value: 'stepDuration' },
          { label: '容量(mAh)', value: 'capacity' },
          { label: '能量(mWh)', value: 'energy' },
          { label: '起始电压(V)', value: 'startVoltage' },
@@ -114,7 +117,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, recordedAt: '', absoluteTime: '', serverReceivedAt: '', stepDuration: '-', startVoltage: '-', endVoltage: '-', chargeMedianVoltage: '-', dischargeMedianVoltage: '-', voltage: '-', current: '-', capacity: '-', energy: '-', temperature: '-', positivePotential: '-', negativePotential: '-', extensionMeasurements: '-', chargeCapacity: '-', dischargeCapacity: '-', efficiency: '-', chargeEnergy: '-', dischargeEnergy: '-', ...base }
  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 }
}
function mapRecord(record: PersistedTestRecord): DataRow {
  const deviceTime = formatTime(record.deviceTime ?? record.recordedAt)
@@ -131,17 +134,17 @@
      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: record.recordNo, 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, 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 || '-', 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)
  const identity = record.stableIdentity || `step:${[record.adapterId, record.devtype, record.deviceId, record.subdevid, record.channelId, record.cycleNo, record.stepNo].join(':')}`
  return emptyRow({ id: identity, recordNo: record.recordNo, batteryNo: record.batteryNo, cycleNo: Number(record.cycleNo), stepNo: Number(record.stepNo), stepName: record.stepName || '-', actualStepLabel: record.actualStepLabel || `${record.stepNo} · ${record.stepName || '-'}`, sequenceId: Number(record.sequenceId || 0), recordedAt: startedAt, absoluteTime: formatTime(record.absoluteTime || record.recordedAt), serverReceivedAt: formatTime(record.serverReceivedAt), stepDuration: record.stepDuration == null ? '-' : String(record.stepDuration), capacity: formatTestDisplayValue('capacity', record.capacity), energy: formatTestDisplayValue('energy', record.energy), startVoltage: record.startVoltage == null ? '-' : Number(record.startVoltage).toFixed(4), endVoltage: record.endVoltage == null ? '-' : Number(record.endVoltage).toFixed(4), chargeMedianVoltage: record.chargeMedianVoltage == null ? '-' : Number(record.chargeMedianVoltage).toFixed(4), dischargeMedianVoltage: record.dischargeMedianVoltage == null ? '-' : Number(record.dischargeMedianVoltage).toFixed(4) })
  return emptyRow({ id: identity, recordNo: identity, batteryNo: record.batteryNo, cycleNo: Number(record.cycleNo), stepNo: Number(record.stepNo), stepName: record.stepName || '-', actualStepLabel: record.actualStepLabel || `${record.stepNo} · ${record.stepName || '-'}`, sequenceId: Number(record.sequenceId || 0), recordedAt: startedAt, absoluteTime: formatTime(record.absoluteTime || record.recordedAt), serverReceivedAt: formatTime(record.serverReceivedAt), stepDuration: formatTestDisplayValue('stepDuration', record.stepDuration), capacity: formatTestDisplayValue('capacity', record.capacity), energy: formatTestDisplayValue('energy', record.energy), startVoltage: record.startVoltage == null ? '-' : Number(record.startVoltage).toFixed(4), endVoltage: record.endVoltage == null ? '-' : Number(record.endVoltage).toFixed(4), chargeMedianVoltage: record.chargeMedianVoltage == null ? '-' : Number(record.chargeMedianVoltage).toFixed(4), dischargeMedianVoltage: record.dischargeMedianVoltage == null ? '-' : Number(record.dischargeMedianVoltage).toFixed(4) })
}
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: record.recordNo, 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) })
}
async function loadRecords() {
  loading.value = true
@@ -215,9 +218,7 @@
  router.push({ path: '/data-revision', query: { testId: String(props.testId), dataType: dataType.value } })
}
function downloadTemplate() {
  const worksheet = XLSX.utils.aoa_to_sheet([Object.values(consoleImportColumns[dataType.value])])
  const workbook = XLSX.utils.book_new()
  XLSX.utils.book_append_sheet(workbook, worksheet, `${activeTab.value}模板`)
  const workbook = createConsoleImportWorkbook(dataType.value)
  XLSX.writeFile(workbook, `测试${activeTab.value}数据导入模板.xlsx`)
  ElMessage.success('导入模板已下载')
}
@@ -246,9 +247,10 @@
    /* 用户取消原因填写 */
  }
}
function isEditableNumericValue(value: unknown) {
function isEditableNumericValue(value: unknown, field: string) {
  // “-”表示设备未提供该指标;允许保留缺失值,以便只修订本次实际填写的字段。
  const normalized = String(value ?? '').trim()
  if (field === 'stepDuration') return normalized === '-' || (normalized !== '' && Number.isFinite(parseTestDuration(normalized)))
  return normalized === '-' || (normalized !== '' && Number.isFinite(Number(normalized)))
}
async function submitBatchEdit() {
@@ -257,7 +259,7 @@
    singleRow.value &&
    editorFieldOptions.value.some(field => {
      const value = singleRow.value?.[field.value as keyof DataRow]
      return !isEditableNumericValue(value)
      return !isEditableNumericValue(value, field.value)
    })
  ) {
    ElMessage.warning('请填写完整、有效的数据')
@@ -283,7 +285,7 @@
          .filter(item => row[item.value as keyof DataRow] !== before[item.value as keyof DataRow])
          .map(item => [item.value, row[item.value as keyof DataRow]]))
        // 编辑框使用毫单位;提交字段和摘要都回到 API 单位,正式修订前值仍由后端读取。
        const apiValues = Object.fromEntries(Object.entries(values).map(([field, value]) => [field, toTestApiValue(field, String(value))]))
        const apiValues = Object.fromEntries(Object.entries(values).map(([field, value]) => [field, String(value).trim() === '-' ? null : toTestApiValue(field, String(value))]))
        const apiBefore = { ...before, ...Object.fromEntries(fields.map(item => [item.value, toTestApiValue(item.value, String(before[item.value as keyof DataRow]))])) }
        return { identity: id, before: apiBefore, after: { ...apiBefore, ...apiValues }, override: { stableIdentity: id, fieldValues: JSON.stringify(apiValues), deleted: false } }
      })
@@ -344,8 +346,9 @@
    ElMessage.warning('没有可下载的数据')
    return
  }
  const headers = activeTab.value === 'cycle' ? ['记录编号', '电池', '循环序号', '充电容量(mAh)', '放电容量(mAh)', '充放电效率(%)', '充电能量(mWh)', '放电能量(mWh)'] : activeTab.value === 'step' ? ['记录编号', '电池', '循环序号', '工步序号', '工步名称', '工步时间(h:min:s.ms)', '容量(mAh)', '能量(mWh)', '起始电压(V)', '终止电压(V)', '充电中值电压(V)', '放电中值电压(V)'] : ['记录编号', '电池', '循环序号', '工步序号', '工步名称', '记录序号', '记录时间(h:min:s.ms)', '电压(V)', '电流(mA)', '容量(mAh)', '能量(mWh)', '绝对时间', '温度(℃)', '正电位(V)', '负电位(V)']
  const records = activeTab.value === 'cycle' ? filteredRows.value.map(row => [row.recordNo, row.batteryNo, row.cycleNo, row.chargeCapacity, row.dischargeCapacity, row.efficiency, row.chargeEnergy, row.dischargeEnergy]) : activeTab.value === 'step' ? filteredRows.value.map(row => [row.recordNo, row.batteryNo, row.cycleNo, row.stepNo, row.stepName, row.stepDuration, row.capacity, row.energy, row.startVoltage, row.endVoltage, row.chargeMedianVoltage, row.dischargeMedianVoltage]) : filteredRows.value.map(row => [row.recordNo, row.batteryNo, row.cycleNo, row.stepNo, row.stepName, row.sequenceId, row.recordedAt, row.voltage, row.current, row.capacity, row.energy, row.absoluteTime, row.temperature, row.positivePotential, row.negativePotential])
  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 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')
@@ -422,52 +425,11 @@
    <div class="table-wrap">
      <table>
        <thead>
          <tr v-if="activeTab === 'realtime'">
          <tr>
            <th class="selection-column">
              <el-checkbox :model-value="allPageSelected" :indeterminate="somePageSelected" aria-label="选择本页" @change="(checked: string | number | boolean) => togglePageSelection(Boolean(checked))" />
              <el-checkbox v-if="activeTab === 'realtime'" :model-value="allPageSelected" :indeterminate="somePageSelected" aria-label="选择本页" @change="(checked: string | number | boolean) => togglePageSelection(Boolean(checked))" />
            </th>
            <th>记录编号</th>
            <th>电池</th>
            <th>循环序号</th>
            <th>工步序号</th>
            <th>工步名称</th>
            <th>记录序号</th>
            <th>设备时间</th>
            <th>绝对时间</th>
            <th>温度(℃)</th>
            <th>正电位(V)</th>
            <th>负电位(V)</th>
            <th>电压(V)</th>
            <th>电流(mA)</th>
            <th>容量(mAh)</th>
            <th>能量(mWh)</th>
            <th class="action-column">操作</th>
          </tr>
          <tr v-else-if="activeTab === 'step'">
            <th class="selection-column"></th>
            <th>记录编号</th>
            <th>电池</th>
            <th>循环序号</th>
            <th>工步名称</th>
            <th>工步时间</th>
            <th>容量(mAh)</th>
            <th>能量(mWh)</th>
            <th>起始电压(V)</th>
            <th>终止电压(V)</th>
            <th>充电中值电压(V)</th>
            <th>放电中值电压(V)</th>
            <th class="action-column">操作</th>
          </tr>
          <tr v-else>
            <th class="selection-column"></th>
            <th>记录编号</th>
            <th>电池</th>
            <th>循环序号</th>
            <th>充电容量(mAh)</th>
            <th>放电容量(mAh)</th>
            <th>充放电效率(%)</th>
            <th>充电能量(mWh)</th>
            <th>放电能量(mWh)</th>
            <th v-for="(label, field) in consoleImportColumns[dataType]" :key="field">{{ label }}</th>
            <th class="action-column">操作</th>
          </tr>
        </thead>
@@ -485,11 +447,7 @@
              <td>{{ row.stepNo }}</td>
              <td>{{ row.stepName }}</td>
              <td>{{ row.sequenceId }}</td>
              <td>{{ row.recordedAt }}</td>
              <td>{{ row.absoluteTime }}</td>
              <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>{{ row.relativeTimeSeconds }}</td>
              <td>
                <el-input v-if="editing && draftRows[row.id]" v-model="draftRows[row.id].voltage" size="small" />
                <span v-else>{{ row.voltage }}</span>
@@ -506,6 +464,10 @@
                <el-input v-if="editing && draftRows[row.id]" v-model="draftRows[row.id].energy" size="small" />
                <span v-else>{{ row.energy }}</span>
              </td>
              <td>{{ row.absoluteTime }}</td>
              <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="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>
@@ -519,7 +481,8 @@
                <strong>{{ row.batteryNo }}</strong>
              </td>
              <td>{{ row.cycleNo }}</td>
              <td>{{ row.stepNo }} · {{ row.stepName }}</td>
              <td>{{ row.stepNo }}</td>
              <td>{{ row.stepName }}</td>
              <td>
                <el-input v-if="editing && draftRows[row.id]" v-model="draftRows[row.id].stepDuration" size="small" />
                <span v-else>{{ row.stepDuration }}</span>
@@ -621,7 +584,7 @@
            <dd>{{ singleRow.stepName }}</dd>
          </div>
          <div>
            <dt>记录时间</dt>
            <dt>绝对时间</dt>
            <dd>{{ singleRow.recordedAt }}</dd>
          </div>
          <div>
@@ -857,7 +820,7 @@
  min-width: 2100px;
}
.console-data-panel.realtime th:nth-child(8),
.console-data-panel.realtime th:nth-child(9) {
.console-data-panel.realtime th:nth-child(13) {
  width: 185px;
}
.source-column small {