From b226aa0db7fbecb8fff92476537adc0ca3e16a4f Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期三, 09 九月 2026 13:25:57 +0800
Subject: [PATCH] 统一测试三类数据字段并修正记录编号与时长
---
src/utils/consoleDataImport.ts | 57 ++++++++++++++++++++++++++++++++++++++++-----------------
1 files changed, 40 insertions(+), 17 deletions(-)
diff --git a/src/utils/consoleDataImport.ts b/src/utils/consoleDataImport.ts
index 42f2e21..95eae19 100644
--- a/src/utils/consoleDataImport.ts
+++ b/src/utils/consoleDataImport.ts
@@ -1,10 +1,11 @@
import * as XLSX from 'xlsx'
import type { ConsoleDataType } from '../api/consoleData'
+import { parseTestDuration } from './testDataDuration.ts'
export const consoleImportColumns = {
- REALTIME: { recordNo: '记录编号', batteryNo: '电池号', recordedAt: '记录时间(h:min:s.ms)', cycleNo: '循环序号', stepNo: '工步序号', stepName: '工步名称', sequenceId: '记录序号', voltage: '电压(V)', current: '电流(mA)', capacity: '容量(mAh)', energy: '能量(mWh)', temperature: '温度(℃)', positivePotential: '正电位(V)', negativePotential: '负电位(V)' },
- STEP: { recordNo: '记录编号', batteryNo: '电池号', recordedAt: '设备时间', cycleNo: '循环序号', stepNo: '工步序号', stepName: '工步', stepDuration: '工步时间', capacity: '容量(mAh)', energy: '能量(mWh)', startVoltage: '起始电压(V)', endVoltage: '终止电压(V)', chargeMedianVoltage: '充电中值电压(V)', dischargeMedianVoltage: '放电中值电压(V)' },
- CYCLE: { recordNo: '记录编号', batteryNo: '电池号', recordedAt: '设备时间', cycleNo: '循环序号', chargeCapacity: '充电容量(mAh)', dischargeCapacity: '放电容量(mAh)', efficiency: '充放电效率(%)', chargeEnergy: '充电能量(mWh)', dischargeEnergy: '放电能量(mWh)' },
+ REALTIME: { recordNo: '记录编号', batteryNo: '电池', cycleNo: '循环序号', stepNo: '工步序号', stepName: '工步名称', sequenceId: '记录序号', relativeTimeSeconds: '记录时间(h:min:s.ms)', voltage: '电压(V)', current: '电流(mA)', capacity: '容量(mAh)', energy: '能量(mWh)', recordedAt: '绝对时间', temperature: '温度(℃)', positivePotential: '正电位(V)', negativePotential: '负电位(V)' },
+ STEP: { recordNo: '记录编号', batteryNo: '电池', cycleNo: '循环序号', stepNo: '工步序号', stepName: '工步名称', stepDuration: '工步时间(h:min:s.ms)', capacity: '容量(mAh)', energy: '能量(mWh)', startVoltage: '起始电压(V)', endVoltage: '终止电压(V)', chargeMedianVoltage: '充电中值电压(V)', dischargeMedianVoltage: '放电中值电压(V)' },
+ CYCLE: { recordNo: '记录编号', batteryNo: '电池', cycleNo: '循环序号', chargeCapacity: '充电容量(mAh)', dischargeCapacity: '放电容量(mAh)', efficiency: '充放电效率(%)', chargeEnergy: '充电能量(mWh)', dischargeEnergy: '放电能量(mWh)' },
} satisfies Record<ConsoleDataType, Record<string, string>>
export interface ValidatedImportRow {
@@ -23,11 +24,13 @@
}
/** 创建包含三个标准列头的空工作簿,不生成测量数据。 */
-export function createConsoleImportWorkbook(): XLSX.WorkBook {
+export function createConsoleImportWorkbook(selectedType?: ConsoleDataType): XLSX.WorkBook {
const workbook = XLSX.utils.book_new()
- for (const type of Object.keys(consoleSheetLabels) as ConsoleDataType[]) {
+ for (const type of selectedType ? [selectedType] : Object.keys(consoleSheetLabels) as ConsoleDataType[]) {
XLSX.utils.book_append_sheet(workbook, XLSX.utils.aoa_to_sheet([Object.values(consoleImportColumns[type])]), consoleSheetLabels[type])
}
+ // 单页汇总没有绝对时间,辅助页只供匹配,不隐式导入实时数据。
+ if (selectedType && selectedType !== 'REALTIME') XLSX.utils.book_append_sheet(workbook, XLSX.utils.aoa_to_sheet([Object.values(consoleImportColumns.REALTIME)]), 'record')
return workbook
}
@@ -81,29 +84,45 @@
const table = XLSX.utils.sheet_to_json<unknown[]>(sheet, { header: 1, raw: true, defval: '', blankrows: true })
const headers = (table[0] || []).map(value => String(value).trim())
const party = name === sheetNames[type][2]
+ const legacyRealtime = type === 'REALTIME' && !headers.includes('绝对时间') && (headers.includes('设备时间') || headers.includes('记录时间(h:min:s.ms)'))
+ const legacySourceNumber = headers.includes('电池号') && (headers.includes('设备时间') || legacyRealtime)
const date1904 = !!workbook.Workbook?.WBProps?.date1904
const endTimes = new Map<string, unknown>()
- if (party && type !== 'REALTIME' && workbook.Sheets.record) {
+ const invalidEndTimes = new Set<string>()
+ const batteryFor = (row: Record<string, unknown>) => String(row['电池'] || row['电池号'] || selectedBattery || (batteries.length === 1 ? batteries[0] : '')).trim()
+ const timeKey = (row: Record<string, unknown>) => JSON.stringify([batteryFor(row), String(row['循环序号']), ...(type === 'STEP' ? [String(row['工步序号'])] : [])])
+ const recordNames = workbook.SheetNames.filter(name => sheetNames.REALTIME.includes(name) && workbook.Sheets[name])
+ if (type !== 'REALTIME' && recordNames.length > 1) throw new Error('存在多个实时辅助工作表,无法确定时间来源')
+ if (type !== 'REALTIME' && recordNames.length === 1) {
// 汇总表没有绝对时间,只借用匹配实时记录的末次时间,不导入其他 Sheet 的行。
- const records = XLSX.utils.sheet_to_json<Record<string, unknown>>(workbook.Sheets.record, { raw: true })
+ const records = XLSX.utils.sheet_to_json<Record<string, unknown>>(workbook.Sheets[recordNames[0]!]!, { raw: true })
for (const record of records) {
- const key = type === 'STEP' ? `${record['循环序号']}:${record['工步序号']}` : String(record['循环序号'])
+ const key = timeKey(record)
const time = deviceTime(record['绝对时间'], date1904)
+ if (!Number.isFinite(time)) invalidEndTimes.add(key)
if (Number.isFinite(time) && (!endTimes.has(key) || time > deviceTime(endTimes.get(key), date1904))) endTimes.set(key, record['绝对时间'])
}
+ // 组内有未知时间就无法可靠确定末次记录,不能静默退到更早的有效行。
+ for (const key of invalidEndTimes) endTimes.delete(key)
}
// 旧标准模板按原列头单位读取,避免对 A/Ah/Wh 再次缩放。
const headerFor = (field: string, standardHeader: string) => {
+ if (field === 'recordedAt' && legacyRealtime) return headers.includes('设备时间') ? '设备时间' : '记录时间(h:min:s.ms)'
if (party) return partyHeaders[field] || standardHeader
+ if (field === 'batteryNo' && headers.includes('电池号')) return '电池号'
+ if (field === 'stepName' && headers.includes('工步')) return '工步'
+ if (field === 'stepDuration' && headers.includes('工步时间')) return '工步时间'
if (field === 'recordedAt' && headers.includes('设备时间')) return '设备时间'
const legacyHeader = standardHeader.replace(/\(m(A|Ah|Wh)\)/, '($1)')
return headers.includes(standardHeader) ? standardHeader : headers.includes(legacyHeader) ? legacyHeader : standardHeader
}
- const columns: Record<string, string> = consoleImportColumns[type]
+ const columns: Record<string, string> = { ...consoleImportColumns[type], ...(type !== 'REALTIME' ? { recordedAt: '绝对时间' } : {}) }
const missing = Object.entries(columns).filter(([field, header]) => {
// 中值只读取对应列;缺列可为空,不借用旧峰值或谷值。
- if (party && ['recordNo', 'batteryNo', 'temperature', 'positivePotential', 'negativePotential', 'chargeMedianVoltage', 'dischargeMedianVoltage'].includes(field)) return false
- if (party && type !== 'REALTIME' && field === 'recordedAt') return false
+ if (['recordNo', 'batteryNo'].includes(field)) return false
+ if (field === 'relativeTimeSeconds' && legacyRealtime) return false
+ if (party && ['temperature', 'positivePotential', 'negativePotential', 'chargeMedianVoltage', 'dischargeMedianVoltage'].includes(field)) return false
+ if (type !== 'REALTIME' && field === 'recordedAt') return false
return !headers.includes(headerFor(field, header))
}).map(([field, header]) => headerFor(field, header))
if (missing.length) throw new Error(`第1行,列「${missing.join('、')}」:模板字段缺失`)
@@ -116,12 +135,16 @@
for (const [field, standardHeader] of Object.entries(columns)) {
const header = headerFor(field, standardHeader)
let value: unknown = raw[header]
- if (party && field === 'batteryNo') value = selectedBattery || (batteries.length === 1 ? batteries[0] : '')
- if (party && field === 'recordNo') value = raw['记录编号'] || (type === 'REALTIME' ? raw['记录序号'] : type === 'STEP' ? `step:${raw['循环序号']}:${raw['工步序号']}` : `cycle:${raw['循环序号']}`)
- if (party && type !== 'REALTIME' && field === 'recordedAt' && !value) value = endTimes.get(type === 'STEP' ? `${raw['循环序号']}:${raw['工步序号']}` : String(raw['循环序号']))
- if (field === 'stepDuration' && typeof value === 'string' && value.includes(':')) {
- const duration = /^(\d+):([0-5]\d):([0-5]\d)(?:\.(\d{1,3}))?$/.exec(value.trim())
- value = duration ? Number(duration[1]) * 3600 + Number(duration[2]) * 60 + Number(duration[3]) + Number((duration[4] || '').padEnd(3, '0')) / 1000 : NaN
+ if (field === 'batteryNo') value = batteryFor(raw)
+ // 系统记录编号只供定位修订,导入不能拿它代替设备/文件的来源编号。
+ if (field === 'recordNo') value = type === 'REALTIME' ? raw['记录序号'] : type === 'STEP' ? `step:${raw['循环序号']}:${raw['工步序号']}` : `cycle:${raw['循环序号']}`
+ // 旧标准模板的编号曾表示来源,保留其重复导入身份;新模板编号不参与去重。
+ if (field === 'recordNo' && legacySourceNumber && raw['记录编号']) value = raw['记录编号']
+ if (type !== 'REALTIME' && field === 'recordedAt' && !value) value = endTimes.get(timeKey(raw))
+ if (field === 'stepDuration' || field === 'relativeTimeSeconds') {
+ const cell = sheet[XLSX.utils.encode_cell({ r: sourceRow - 1, c: headers.indexOf(header) })]
+ if (typeof value === 'number' && cell?.z && /[hs]/i.test(cell.z)) value = XLSX.utils.format_cell(cell)
+ value = field === 'relativeTimeSeconds' && legacyRealtime ? null : parseTestDuration(value)
}
const text = String(value ?? '').trim()
const fail = (reason: string) => errors.push(`第${sourceRow}行,列「${header}」:${reason}`)
--
Gitblit v1.7.1