| | |
| | | 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 { |
| | |
| | | actualStepLabel: string |
| | | sequenceId: number |
| | | recordedAt: string |
| | | relativeTimeSeconds: string |
| | | absoluteTime: string |
| | | serverReceivedAt: string |
| | | stepDuration: string |
| | |
| | | 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() |
| | |
| | | { 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)) |
| | | const filters = reactive<FilterState>({ batteryNo: '', dateRange: null, cycleNo: '', stepNo: '' }) |
| | | const appliedFilters = ref<FilterState>({ batteryNo: '', dateRange: null, cycleNo: '', stepNo: '' }) |
| | | const currentPage = ref(1) |
| | |
| | | ] |
| | | : 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' }, |
| | |
| | | ], |
| | | ) |
| | | 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: '-', 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) |
| | |
| | | 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 || '-', 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) |
| | | 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), sourceKind: cycleSourceLabels[String(record.sourceKind || 'DEVICE')] || CYCLE_SOURCE_UNKNOWN }) |
| | | } |
| | | async function loadRecords() { |
| | | loading.value = true |
| | |
| | | ? [{ 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(() => |
| | |
| | | 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('导入模板已下载') |
| | | } |
| | |
| | | /* 用户取消原因填写 */ |
| | | } |
| | | } |
| | | 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() { |
| | |
| | | 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('请填写完整、有效的数据') |
| | |
| | | .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 } } |
| | | }) |
| | |
| | | ElMessage.warning('没有可下载的数据') |
| | | return |
| | | } |
| | | const headers = activeTab.value === 'cycle' ? ['记录编号', '电池', '循环号', '充电容量(mAh)', '放电容量(mAh)', '充放电效率(%)', '充电能量(mWh)', '放电能量(mWh)'] : activeTab.value === 'step' ? ['记录编号', '电池', '循环号', '工步序号', '工步', '工步时间', '容量(mAh)', '能量(mWh)', '起始电压(V)', '终止电压(V)', '充电中值电压(V)', '放电中值电压(V)'] : ['记录编号', '电池', '循环号', '实际工步号', '实际工步名称', '记录序号', '设备时间', '服务器接收时间', '电压(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.serverReceivedAt, row.voltage, row.current, row.capacity, row.energy, row.temperature, row.positivePotential, row.negativePotential]) |
| | | const columns = consoleImportColumns[dataType.value] |
| | | // 导出与界面一致地带上来源列:循环的推导值不能在被导出后失去"聚合来源"这个事实; |
| | | // 实时的"来源定位"是设备侧自己的对照串(如厂商库名与记录号),导出后仍能拿它跟厂商软件核对。 |
| | | 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') |
| | |
| | | </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> |
| | | <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 |
| | |
| | | <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 class="source-column">来源</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>服务器接收时间</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>设备时间</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 v-if="activeTab === 'cycle'" :title="cycleSourceTitle">数据来源</th> |
| | | <th v-if="activeTab === 'realtime'" title="设备侧的对照串(厂商库名与记录号等),用于跟厂商软件核对">来源定位</th> |
| | | <th class="action-column">操作</th> |
| | | </tr> |
| | | </thead> |
| | |
| | | <td>{{ row.stepNo }}</td> |
| | | <td>{{ row.stepName }}</td> |
| | | <td>{{ row.sequenceId }}</td> |
| | | <td>{{ row.recordedAt }}</td> |
| | | <td>{{ row.serverReceivedAt }}</td> |
| | | <td class="source-column" :title="row.sourceIdentity">{{ row.sourceType === 'COLLECTOR' ? `采集器 ${row.collectorCode}` : row.sourceType === 'SNAPSHOT' ? '检测柜 + 采集器' : '检测柜' }}<small>{{ row.sourceIdentity }}</small><small v-if="row.sourceType === 'SNAPSHOT'">采集器 {{ row.collectorCode }} · {{ row.collectorDeviceTime }}</small><small v-if="row.sourceType === 'COLLECTOR'">{{ row.channelCode }}</small><small v-if="row.extensionMeasurements !== '-'">{{ row.extensionMeasurements }}</small></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> |
| | |
| | | <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="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> |
| | |
| | | <strong>{{ row.batteryNo }}</strong> |
| | | </td> |
| | | <td>{{ row.cycleNo }}</td> |
| | | <td>{{ row.stepNo }} · {{ row.stepName }}</td> |
| | | <td>{{ row.recordedAt }}</td> |
| | | <td>{{ row.serverReceivedAt }}</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> |
| | |
| | | <strong>{{ row.batteryNo }}</strong> |
| | | </td> |
| | | <td>{{ row.cycleNo }}</td> |
| | | <td>{{ row.recordedAt }}</td> |
| | | <td>{{ row.serverReceivedAt }}</td> |
| | | <td> |
| | | <el-input v-if="editing && draftRows[row.id]" v-model="draftRows[row.id].chargeCapacity" size="small" /> |
| | | <span v-else>{{ row.chargeCapacity }}</span> |
| | |
| | | <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' ? 19 : activeTab === 'step' ? 15 : 12" class="empty-cell"> |
| | | <td :colspan="activeTab === 'realtime' ? 18 : activeTab === 'step' ? 14 : 11" class="empty-cell"> |
| | | {{ loadError || '暂无检测柜记录' }} |
| | | </td> |
| | | </tr> |
| | |
| | | <dd>{{ singleRow.stepName }}</dd> |
| | | </div> |
| | | <div> |
| | | <dt>记录时间</dt> |
| | | <dt>绝对时间</dt> |
| | | <dd>{{ singleRow.recordedAt }}</dd> |
| | | </div> |
| | | <div> |
| | |
| | | .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; |
| | |
| | | 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; |
| | | } |
| | | .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 { |