实现控制台数据编辑导入删除
增加实时、工步、循环数据模板、修订原因提交、后端覆盖恢复和完成状态导入校验。
| New file |
| | |
| | | import { request, type ApiResponse } from './request' |
| | | |
| | | export type ConsoleDataType = 'REALTIME' | 'STEP' | 'CYCLE' |
| | | export type ConsoleDataOperation = 'EDIT' | 'IMPORT' | 'DELETE' |
| | | |
| | | export interface ConsoleDataOverride { |
| | | overrideId: number |
| | | testId: number |
| | | dataType: ConsoleDataType |
| | | stableIdentity: string |
| | | fieldValues: string | null |
| | | deleted: boolean |
| | | actionId: number |
| | | createdAtUtc: string |
| | | } |
| | | |
| | | export interface ConsoleDataActionPayload { |
| | | dataType: ConsoleDataType |
| | | operationType: ConsoleDataOperation |
| | | scopeSummary: string |
| | | identitySummary: string |
| | | changesSummary: string | null |
| | | reason: string |
| | | overrides: Array<{ |
| | | stableIdentity: string |
| | | fieldValues: string | null |
| | | deleted: boolean |
| | | }> |
| | | btsSampleIds?: number[] |
| | | collectorSampleIds?: number[] |
| | | } |
| | | |
| | | interface OverrideResponse extends ApiResponse { data: ConsoleDataOverride[] } |
| | | interface ActionResponse extends ApiResponse { actionId: number } |
| | | |
| | | export async function getConsoleDataOverrides(testId: number, dataType: ConsoleDataType) { |
| | | return (await request<OverrideResponse>(`/system/test/${testId}/console-data/${dataType}/overrides`)).data || [] |
| | | } |
| | | |
| | | export async function submitConsoleDataAction(testId: number, payload: ConsoleDataActionPayload) { |
| | | return request<ActionResponse>(`/system/test/${testId}/console-data/actions`, { |
| | | method: 'POST', |
| | | body: JSON.stringify(payload), |
| | | }) |
| | | } |
| | |
| | | <script setup lang="ts"> |
| | | import { computed, onMounted, reactive, ref, watch } from 'vue' |
| | | import { ElMessage } from 'element-plus' |
| | | import { ElMessage, ElMessageBox } from 'element-plus' |
| | | import { useRouter } from 'vue-router' |
| | | import * as XLSX from 'xlsx' |
| | | import { dataRevisionRecords } from '@/stores/dataRevisions' |
| | | import { Download, EditPen, Refresh, Search, UploadFilled, Clock, Check } from '@element-plus/icons-vue' |
| | | import { getBtsCycles, getBtsRecords, getBtsSteps, type BtsCycleRecord, type BtsRecord, type BtsStepRecord } from '@/api/tests' |
| | | import { getConsoleDataOverrides, submitConsoleDataAction, type ConsoleDataType } from '@/api/consoleData' |
| | | |
| | | type DataTab = 'realtime' | 'step' | 'cycle' |
| | | interface ConsoleCell { no: string } |
| | | interface FilterState { batteryNo: string; dateRange: [string, string] | null; cycleNo: string; stepNo: string } |
| | | interface DataRow { id: string; recordNo: string; batteryNo: string; cycleNo: number; stepNo: number; stepName: string; sequenceId: number; recordedAt: string; absoluteTime: string; voltage: string; current: string; capacity: string; energy: string; chargeCapacity: string; dischargeCapacity: string; efficiency: string; chargeEnergy: string; dischargeEnergy: string } |
| | | interface DataRow { id: string; recordNo: string; batteryNo: string; cycleNo: number; stepNo: number; stepName: string; sequenceId: number; recordedAt: string; absoluteTime: string; stepDuration: string; startVoltage: string; endVoltage: string; chargePeakVoltage: string; dischargeValleyVoltage: string; voltage: string; current: string; capacity: string; energy: string; chargeCapacity: string; dischargeCapacity: string; efficiency: string; chargeEnergy: string; dischargeEnergy: string } |
| | | const props = defineProps<{ testId: number; testName: string; cells: ConsoleCell[]; canImport?: boolean; bindings?: Array<{ batteryNo: string; devid?: number | null; chlid?: number | null }> }>() |
| | | const router = useRouter() |
| | | const activeTab = ref<DataTab>('realtime'); const loading = ref(false); const loadError = ref(''); const rows = ref<DataRow[]>([]); const stepRows = ref<DataRow[]>([]); const cycleRows = ref<DataRow[]>([]) |
| | | const dataTabs = [{ key: 'realtime', label: '实时' }, { key: 'step', label: '工步' }, { key: 'cycle', label: '循环' }] as const |
| | | const filters = reactive<FilterState>({ batteryNo: '', dateRange: null, cycleNo: '', stepNo: '' }); const appliedFilters = ref<FilterState>({ batteryNo: '', dateRange: null, cycleNo: '', stepNo: '' }); const currentPage = ref(1); const pageSize = ref(20) |
| | | const selectedIds = ref<Set<string>>(new Set()); const editorVisible = ref(false); const editForm = reactive({ field: 'voltage' as 'voltage' | 'current' | 'capacity' | 'energy', value: '', remark: '' }) |
| | | const selectedIds = ref<Set<string>>(new Set()); const editorVisible = ref(false); const editing = ref(false); const draftRows = ref<Record<string, DataRow>>({}); const editForm = reactive({ field: 'voltage' as keyof DataRow, value: '', remark: '' }) |
| | | const dataType = computed<ConsoleDataType>(() => activeTab.value === 'step' ? 'STEP' : activeTab.value === 'cycle' ? 'CYCLE' : 'REALTIME') |
| | | const fieldOptions = computed(() => activeTab.value === 'realtime' |
| | | ? [{ label: '电压(V)', value: 'voltage' }, { label: '电流(A)', value: 'current' }, { label: '容量(Ah)', value: 'capacity' }, { label: '能量(Wh)', value: 'energy' }] |
| | | : activeTab.value === 'step' |
| | | ? [{ label: '工步时间', value: 'stepDuration' }, { label: '容量(Ah)', value: 'capacity' }, { label: '能量(Wh)', value: 'energy' }, { label: '起始电压(V)', value: 'startVoltage' }, { label: '终止电压(V)', value: 'endVoltage' }, { label: '充电峰值电压(V)', value: 'chargePeakVoltage' }, { label: '放电谷值电压(V)', value: 'dischargeValleyVoltage' }] |
| | | : [{ label: '充电容量(Ah)', value: 'chargeCapacity' }, { label: '放电容量(Ah)', value: 'dischargeCapacity' }, { label: '效率(%)', value: 'efficiency' }, { label: '充电能量(Wh)', value: 'chargeEnergy' }, { label: '放电能量(Wh)', value: 'dischargeEnergy' }]) |
| | | function formatTime(value: string) { return value ? value.replace('T', ' ').replace('Z', '').replace(/\.\d{3,6}$/, '') : '-' } |
| | | function mapRecord(record: BtsRecord, batteryNo: string, deviceId: number, channelId: number): DataRow { const timestamp = formatTime(record.deviceTime); return { id: `${deviceId}:${channelId}:${record.sequenceId}`, recordNo: `RD-${deviceId}-${channelId}-${String(record.sequenceId).padStart(6, '0')}`, batteryNo: batteryNo || '-', cycleNo: record.cycleNo, stepNo: record.stepNo, stepName: record.stepName, sequenceId: record.sequenceId, recordedAt: timestamp, absoluteTime: timestamp, voltage: Number(record.voltageV).toFixed(4), current: Number(record.currentA).toFixed(4), capacity: Number(record.capacityAh).toFixed(4), energy: Number(record.energyWh).toFixed(4), chargeCapacity: '-', dischargeCapacity: '-', efficiency: '-', chargeEnergy: '-', dischargeEnergy: '-' } } |
| | | function mapStep(record: BtsStepRecord, batteryNo: string, deviceId: number, channelId: number): DataRow { const startedAt = formatTime(record.startedAt); return { id: `step:${deviceId}:${channelId}:${record.cycleNo}:${record.stepNo}`, recordNo: `STEP-${deviceId}-${channelId}-${record.cycleNo}-${record.stepNo}`, batteryNo, cycleNo: record.cycleNo, stepNo: record.stepNo, stepName: record.stepName, sequenceId: record.recordCount, recordedAt: startedAt, absoluteTime: startedAt, voltage: '-', current: '-', capacity: Number(record.capacityAh).toFixed(4), energy: Number(record.energyWh).toFixed(4), chargeCapacity: '-', dischargeCapacity: '-', efficiency: '-', chargeEnergy: '-', dischargeEnergy: '-' } } |
| | | function mapCycle(record: BtsCycleRecord, batteryNo: string, deviceId: number, channelId: number): DataRow { const startedAt = formatTime(record.startedAt); return { id: `cycle:${deviceId}:${channelId}:${record.cycleNo}`, recordNo: record.recordNo, batteryNo, cycleNo: record.cycleNo, stepNo: 0, stepName: '循环统计', sequenceId: record.recordCount, recordedAt: startedAt, absoluteTime: startedAt, voltage: '-', current: '-', capacity: '-', energy: '-', chargeCapacity: Number(record.chargeCapacityAh).toFixed(4), dischargeCapacity: Number(record.dischargeCapacityAh).toFixed(4), efficiency: `${Number(record.efficiency).toFixed(2)}%`, chargeEnergy: Number(record.chargeEnergyWh).toFixed(4), dischargeEnergy: Number(record.dischargeEnergyWh).toFixed(4) } } |
| | | async function loadRecords() { loading.value = true; loadError.value = ''; try { const bindings = (props.bindings || []).filter(item => Number.isFinite(item.devid) && Number.isFinite(item.chlid)); const batches = await Promise.all(bindings.map(async binding => { const deviceId = Number(binding.devid); const channelId = Number(binding.chlid); const [records, steps, cycles] = await Promise.all([getBtsRecords(deviceId, channelId), getBtsSteps(deviceId, channelId), getBtsCycles(deviceId, channelId)]); return { records: records.map(item => mapRecord(item, binding.batteryNo, deviceId, channelId)), steps: steps.map(item => mapStep(item, binding.batteryNo, deviceId, channelId)), cycles: cycles.map(item => mapCycle(item, binding.batteryNo, deviceId, channelId)) } })); rows.value = batches.flatMap(item => item.records).sort((a, b) => b.absoluteTime.localeCompare(a.absoluteTime) || b.sequenceId - a.sequenceId); stepRows.value = batches.flatMap(item => item.steps); cycleRows.value = batches.flatMap(item => item.cycles) } catch (error) { rows.value = []; stepRows.value = []; cycleRows.value = []; loadError.value = error instanceof Error ? error.message : '检测柜记录加载失败' } finally { loading.value = false } } |
| | | function emptyRow(base: Partial<DataRow>): DataRow { return { id: '', recordNo: '', batteryNo: '', cycleNo: 0, stepNo: 0, stepName: '', sequenceId: 0, recordedAt: '', absoluteTime: '', stepDuration: '-', startVoltage: '-', endVoltage: '-', chargePeakVoltage: '-', dischargeValleyVoltage: '-', voltage: '-', current: '-', capacity: '-', energy: '-', chargeCapacity: '-', dischargeCapacity: '-', efficiency: '-', chargeEnergy: '-', dischargeEnergy: '-', ...base } } |
| | | function mapRecord(record: BtsRecord, batteryNo: string, deviceId: number, channelId: number): DataRow { const timestamp = formatTime(record.deviceTime); return emptyRow({ id: `${deviceId}:${channelId}:${record.sequenceId}`, recordNo: `RD-${deviceId}-${channelId}-${String(record.sequenceId).padStart(6, '0')}`, batteryNo: batteryNo || '-', cycleNo: record.cycleNo, stepNo: record.stepNo, stepName: record.stepName, sequenceId: record.sequenceId, recordedAt: timestamp, absoluteTime: timestamp, voltage: Number(record.voltageV).toFixed(4), current: Number(record.currentA).toFixed(4), capacity: Number(record.capacityAh).toFixed(4), energy: Number(record.energyWh).toFixed(4) }) } |
| | | function mapStep(record: BtsStepRecord, batteryNo: string, deviceId: number, channelId: number): DataRow { const startedAt = formatTime(record.startedAt); return emptyRow({ id: `step:${deviceId}:${channelId}:${record.cycleNo}:${record.stepNo}`, recordNo: `STEP-${deviceId}-${channelId}-${record.cycleNo}-${record.stepNo}`, batteryNo, cycleNo: record.cycleNo, stepNo: record.stepNo, stepName: record.stepName, sequenceId: record.recordCount, recordedAt: startedAt, absoluteTime: startedAt, stepDuration: String(record.durationSeconds), capacity: Number(record.capacityAh).toFixed(4), energy: Number(record.energyWh).toFixed(4), startVoltage: Number(record.startVoltageV).toFixed(4), endVoltage: Number(record.endVoltageV).toFixed(4), chargePeakVoltage: record.chargePeakVoltageV == null ? '-' : Number(record.chargePeakVoltageV).toFixed(4), dischargeValleyVoltage: record.dischargeValleyVoltageV == null ? '-' : Number(record.dischargeValleyVoltageV).toFixed(4) }) } |
| | | function mapCycle(record: BtsCycleRecord, batteryNo: string, deviceId: number, channelId: number): DataRow { const startedAt = formatTime(record.startedAt); return emptyRow({ id: `cycle:${deviceId}:${channelId}:${record.cycleNo}`, recordNo: record.recordNo, batteryNo, cycleNo: record.cycleNo, sequenceId: record.recordCount, recordedAt: startedAt, absoluteTime: startedAt, chargeCapacity: Number(record.chargeCapacityAh).toFixed(4), dischargeCapacity: Number(record.dischargeCapacityAh).toFixed(4), efficiency: Number(record.efficiency).toFixed(2), chargeEnergy: Number(record.chargeEnergyWh).toFixed(4), dischargeEnergy: Number(record.dischargeEnergyWh).toFixed(4) }) } |
| | | function applyOverrides(list: DataRow[], overrides: Awaited<ReturnType<typeof getConsoleDataOverrides>>) { const byIdentity = new Map(overrides.map(item => [item.stableIdentity, item])); const known = new Set(list.map(row => row.id)); const result = list.flatMap(row => { const override = byIdentity.get(row.id); if (!override) return [row]; if (override.deleted) return []; try { return [{ ...row, ...(override.fieldValues ? JSON.parse(override.fieldValues) : {}) }] } catch { return [row] } }); overrides.filter(item => !known.has(item.stableIdentity) && !item.deleted && item.fieldValues).forEach(item => { try { result.push(emptyRow({ ...(JSON.parse(item.fieldValues || '{}')), id: item.stableIdentity, recordNo: item.stableIdentity })) } catch { /* 忽略无法解析的历史覆盖 */ } }); return result } |
| | | async function loadRecords() { loading.value = true; loadError.value = ''; try { const bindings = (props.bindings || []).filter(item => Number.isFinite(item.devid) && Number.isFinite(item.chlid)); const batches = await Promise.all(bindings.map(async binding => { const deviceId = Number(binding.devid); const channelId = Number(binding.chlid); const [records, steps, cycles] = await Promise.all([getBtsRecords(deviceId, channelId), getBtsSteps(deviceId, channelId), getBtsCycles(deviceId, channelId)]); return { records: records.map(item => mapRecord(item, binding.batteryNo, deviceId, channelId)), steps: steps.map(item => mapStep(item, binding.batteryNo, deviceId, channelId)), cycles: cycles.map(item => mapCycle(item, binding.batteryNo, deviceId, channelId)) } })); const overrides = await Promise.all((['REALTIME', 'STEP', 'CYCLE'] as ConsoleDataType[]).map(type => getConsoleDataOverrides(props.testId, type))); rows.value = applyOverrides(batches.flatMap(item => item.records), overrides[0]).sort((a, b) => b.absoluteTime.localeCompare(a.absoluteTime) || b.sequenceId - a.sequenceId); stepRows.value = applyOverrides(batches.flatMap(item => item.steps), overrides[1]); cycleRows.value = applyOverrides(batches.flatMap(item => item.cycles), overrides[2]) } catch (error) { rows.value = []; stepRows.value = []; cycleRows.value = []; loadError.value = error instanceof Error ? error.message : '检测柜记录加载失败' } finally { loading.value = false } } |
| | | const sourceRows = computed(() => activeTab.value === 'step' ? stepRows.value : activeTab.value === 'cycle' ? cycleRows.value : rows.value) |
| | | const batteryOptions = computed(() => [...new Set(sourceRows.value.map(row => row.batteryNo).filter(Boolean))]); 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(() => sourceRows.value.filter(row => { const query = appliedFilters.value; if (query.batteryNo && row.batteryNo !== query.batteryNo) return false; if (query.cycleNo && row.cycleNo !== Number(query.cycleNo)) return false; if (activeTab.value !== 'cycle' && query.stepNo && row.stepNo !== Number(query.stepNo)) return false; if (query.dateRange) { const date = row.absoluteTime.slice(0, 10); if (date < query.dateRange[0] || date > query.dateRange[1]) return false } return true })) |
| | |
| | | function togglePageSelection(checked: boolean) { const next = new Set(selectedIds.value); pagedRows.value.forEach(row => checked ? next.add(row.id) : next.delete(row.id)); selectedIds.value = next } |
| | | const allPageSelected = computed(() => pagedRows.value.length > 0 && pagedRows.value.every(row => selectedIds.value.has(row.id))) |
| | | const somePageSelected = computed(() => pagedRows.value.some(row => selectedIds.value.has(row.id)) && !allPageSelected.value) |
| | | function goToImport() { router.push({ path: `/test-list/${props.testId}/console/data/import`, query: { testName: props.testName, completed: props.canImport ? '1' : '0' } }) } |
| | | function goToImport() { router.push({ path: `/test-list/${props.testId}/console/data/import`, query: { testName: props.testName, completed: props.canImport ? '1' : '0', dataType: dataType.value } }) } |
| | | function goToRevisions() { router.push({ path: '/data-revision', query: { testId: String(props.testId), testName: props.testName } }) } |
| | | function downloadTemplate() { const headers = [['记录编号', '电池号', '记录时间', '循环序号', '工步序号', '工步名称', '电压(V)', '电流(A)', '容量(Ah)', '能量(Wh)']]; const worksheet = XLSX.utils.aoa_to_sheet(headers); const workbook = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(workbook, worksheet, '测试数据'); XLSX.writeFile(workbook, '测试数据导入模板.xlsx'); ElMessage.success('导入模板已下载') } |
| | | function openBatchEditor() { if (!selectedIds.value.size) { ElMessage.warning('请先勾选需要修改的数据'); return }; editForm.field = 'voltage'; editForm.value = ''; editForm.remark = ''; editorVisible.value = true } |
| | | function saveBatchEdit() { if (!editForm.value.trim() || !Number.isFinite(Number(editForm.value))) { ElMessage.warning('修改值必须是有效数字'); return }; if (!editForm.remark.trim()) { ElMessage.warning('请填写修改原因'); return }; const ids = selectedIds.value; rows.value = rows.value.map(row => { if (!ids.has(row.id)) return row; const revised = { ...row, [editForm.field]: editForm.value.trim() }; dataRevisionRecords.unshift({ id: `DR-${Date.now()}-${row.id}`, testId: props.testId, testName: props.testName, testedAt: row.recordedAt, generatedAt: row.recordedAt, modifiedAt: new Date().toLocaleString('zh-CN', { hour12: false }), modifier: '管理员', remark: editForm.remark.trim(), operation: '修改', stepName: row.stepName, cycleNo: row.cycleNo, stepNo: row.stepNo, stepDuration: '-', absoluteTime: row.absoluteTime, originalData: { voltage: row.voltage, current: row.current, capacity: row.capacity, energy: row.energy, temperature: '-', positivePotential: '-', negativePotential: '-' }, revisedData: { voltage: revised.voltage, current: revised.current, capacity: revised.capacity, energy: revised.energy, temperature: '-', positivePotential: '-', negativePotential: '-' } }); return revised }); editorVisible.value = false; selectedIds.value = new Set(); ElMessage.success(`已修改 ${ids.size} 条数据并生成修订记录`) } |
| | | function templateHeaders() { return activeTab.value === 'cycle' ? ['记录编号', '电池号', '循环序号', '充放电容量(Ah)', '效率(%)', '能量(Wh)'] : activeTab.value === 'step' ? ['工步记录编号', '电池号', '循环序号', '工步序号', '工步名称', '工步时间', '容量(Ah)', '能量(Wh)', '起止电压(V)', '充电峰值电压(V)', '放电谷值电压(V)'] : ['记录编号', '电池号', '记录时间', '循环序号', '工步序号', '工步名称', '记录序号', '电压(V)', '电流(A)', '容量(Ah)', '能量(Wh)'] } |
| | | function downloadTemplate() { const worksheet = XLSX.utils.aoa_to_sheet([templateHeaders()]); const workbook = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(workbook, worksheet, `${activeTab.value}模板`); XLSX.writeFile(workbook, `测试${activeTab.value}数据导入模板.xlsx`); ElMessage.success('导入模板已下载') } |
| | | function openBatchEditor() { if (!selectedIds.value.size) { ElMessage.warning('请先勾选需要修改的数据'); return }; editing.value = true; draftRows.value = Object.fromEntries(sourceRows.value.filter(row => selectedIds.value.has(row.id)).map(row => [row.id, { ...row }])) } |
| | | function saveBatchEdit() { if (!Object.keys(draftRows.value).length) { ElMessage.warning('请先勾选需要修改的数据'); return }; editForm.field = String(fieldOptions.value[0]?.value || 'voltage') as keyof DataRow; editForm.value = ''; editForm.remark = ''; editorVisible.value = true } |
| | | async function submitBatchEdit() { if (!editForm.value.trim() || !Number.isFinite(Number(editForm.value))) { ElMessage.warning('修改值必须是有效数字'); return }; if (!editForm.remark.trim()) { ElMessage.warning('请填写修改原因'); return }; Object.values(draftRows.value).forEach(row => { row[editForm.field] = editForm.value.trim() as never }); const originals = new Map(sourceRows.value.map(row => [row.id, row])); const changed = Object.values(draftRows.value).filter(row => JSON.stringify(originals.get(row.id)) !== JSON.stringify(row)); if (!changed.length) { ElMessage.warning('没有检测到修改'); return }; try { await submitConsoleDataAction(props.testId, { dataType: dataType.value, operationType: 'EDIT', scopeSummary: `按格修改${changed.length}条${activeTab.value}数据`, identitySummary: JSON.stringify(changed.map(row => row.id)), changesSummary: JSON.stringify(changed.map(row => ({ identity: row.id, before: originals.get(row.id), after: row }))), reason: editForm.remark.trim(), overrides: changed.map(row => ({ stableIdentity: row.id, fieldValues: JSON.stringify({ [editForm.field]: row[editForm.field] }), deleted: false })) }); editorVisible.value = false; editing.value = false; selectedIds.value = new Set(); await loadRecords(); ElMessage.success(`已提交 ${changed.length} 条数据修改`) } catch (error) { ElMessage.error(error instanceof Error ? error.message : '数据修改提交失败') } } |
| | | function cancelBatchEdit() { editing.value = false; draftRows.value = {}; selectedIds.value = new Set() } |
| | | async function deleteSelected() { if (!selectedIds.value.size) { ElMessage.warning('请先勾选需要删除的数据'); return }; let result: { value: string }; try { result = await ElMessageBox.prompt(`确认物理删除 ${selectedIds.value.size} 条${activeTab.value}数据?删除后不可恢复。`, '删除确认', { inputPlaceholder: '请输入删除原因', inputValidator: value => value.trim() ? true : '删除原因不能为空' }) } catch { return }; const ids = [...selectedIds.value]; try { await submitConsoleDataAction(props.testId, { dataType: dataType.value, operationType: 'DELETE', scopeSummary: `物理删除${ids.length}条${activeTab.value}数据`, identitySummary: JSON.stringify(ids), changesSummary: null, reason: result.value.trim(), overrides: ids.map(stableIdentity => ({ stableIdentity, fieldValues: null, deleted: true })) }); const target = sourceRows.value; const kept = target.filter(row => !selectedIds.value.has(row.id)); if (activeTab.value === 'realtime') rows.value = kept; else if (activeTab.value === 'step') stepRows.value = kept; else cycleRows.value = kept; selectedIds.value = new Set(); ElMessage.success(`已物理删除 ${ids.length} 条数据`) } catch (error) { ElMessage.error(error instanceof Error ? error.message : '数据删除失败') } } |
| | | function changeTab(tab: DataTab) { activeTab.value = tab; currentPage.value = 1 } |
| | | function escapeCsv(value: unknown) { return `"${String(value ?? '').replaceAll('"', '""')}"` } |
| | | function downloadCsv() { if (!filteredRows.value.length) { ElMessage.warning('没有可下载的数据'); return } const headers = activeTab.value === 'cycle' ? ['记录编号', '电池', '循环号', '充电容量(Ah)', '放电容量(Ah)', '充放电效率(%)', '充电能量(Wh)', '放电能量(Wh)'] : ['记录编号', '电池', '循环号', '实际工步号', '实际工步名称', '记录序号', '记录时间', '绝对时间', '电压(V)', '电流(A)', '容量(Ah)', '能量(Wh)']; 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]) : filteredRows.value.map(row => [row.recordNo, row.batteryNo, row.cycleNo, row.stepNo, row.stepName, row.sequenceId, row.recordedAt, row.absoluteTime, row.voltage, row.current, row.capacity, row.energy]); 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'); anchor.href = url; anchor.download = `${props.testName}-${activeTab.value}.csv`; anchor.click(); URL.revokeObjectURL(url) } |
| | |
| | | <section class="console-data-panel"> |
| | | <header class="section-heading"><div><i></i><strong>数据列表</strong><span>TEST DATA</span></div><nav class="data-tabs" aria-label="数据列表子页"><button v-for="tab in dataTabs" :key="tab.key" type="button" :class="{ active: activeTab === tab.key }" :aria-current="activeTab === tab.key ? 'page' : undefined" @click="changeTab(tab.key)">{{ tab.label }}</button></nav><small>{{ loading ? '加载中...' : `共 ${filteredRows.length} 条` }}</small></header> |
| | | <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><label><span>设备时间</span><el-date-picker v-model="filters.dateRange" type="daterange" value-format="YYYY-MM-DD" start-placeholder="开始日期" end-placeholder="结束日期" range-separator="至" /></label><label><span>循环</span><el-select v-model="filters.cycleNo" clearable placeholder="全部"><el-option label="全部" value="" /><el-option v-for="item in cycleOptions" :key="item" :label="`循环 ${item}`" :value="String(item)" /></el-select></label><label><span>实际工步</span><el-select v-model="filters.stepNo" clearable placeholder="全部"><el-option label="全部" value="" /><el-option v-for="item in stepOptions" :key="item[0]" :label="`${item[0]}-${item[1]}`" :value="String(item[0])" /></el-select></label><div class="filter-actions"><button class="primary-button" type="button" @click="applySearch"><el-icon><Search /></el-icon>查询</button><button class="secondary-button" type="button" @click="resetSearch"><el-icon><Refresh /></el-icon>重置</button></div></div> |
| | | <div class="data-toolbar"><div class="toolbar-group"><button class="primary-button" type="button" @click="goToImport"><el-icon><UploadFilled /></el-icon>导入</button><button class="primary-button" type="button" @click="downloadTemplate"><el-icon><Download /></el-icon>下载模板</button><button class="primary-button" type="button" @click="goToRevisions"><el-icon><Clock /></el-icon>修订记录</button><button class="primary-button" type="button" @click="openBatchEditor"><el-icon><EditPen /></el-icon>批量修改</button></div><span v-if="loadError" class="load-error">{{ loadError }}</span><span v-else>仅展示检测柜实际返回的 {{ activeTab === 'realtime' ? 'record' : activeTab === 'step' ? 'step' : 'cycle' }} 数据</span><div class="toolbar-group"><button class="secondary-button" type="button" :disabled="loading" @click="loadRecords"><el-icon><Refresh /></el-icon>刷新</button><button class="secondary-button" type="button" @click="downloadCsv"><el-icon><Download /></el-icon>下载</button></div></div> |
| | | <div class="data-toolbar"><div class="toolbar-group"><button class="primary-button" type="button" @click="goToImport"><el-icon><UploadFilled /></el-icon>导入</button><button class="primary-button" type="button" @click="downloadTemplate"><el-icon><Download /></el-icon>下载模板</button><button class="primary-button" type="button" @click="goToRevisions"><el-icon><Clock /></el-icon>修订记录</button><button v-if="!editing" class="primary-button" type="button" @click="openBatchEditor"><el-icon><EditPen /></el-icon>批量修改</button><template v-else><button class="primary-button" type="button" @click="saveBatchEdit"><el-icon><Check /></el-icon>保存修改</button><button class="secondary-button" type="button" @click="cancelBatchEdit">取消</button></template><button class="danger-button" type="button" :disabled="editing" @click="deleteSelected">删除</button></div><span v-if="loadError" class="load-error">{{ loadError }}</span><span v-else>{{ editing ? '编辑模式:仅允许修改当前页规定字段,保存后填写原因并一次提交' : `仅展示检测柜实际返回的 ${activeTab === 'realtime' ? 'record' : activeTab === 'step' ? 'step' : 'cycle'} 数据` }}</span><div class="toolbar-group"><button class="secondary-button" type="button" :disabled="loading || editing" @click="loadRecords"><el-icon><Refresh /></el-icon>刷新</button><button class="secondary-button" type="button" :disabled="editing" @click="downloadCsv"><el-icon><Download /></el-icon>下载</button></div></div> |
| | | <div class="table-wrap"><table><thead><tr v-if="activeTab === 'realtime'"><th class="selection-column"><el-checkbox :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>电压(V)</th><th>电流(A)</th><th>容量(Ah)</th><th>能量(Wh)</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>结束时间</th><th>容量(Ah)</th><th>能量(Wh)</th></tr><tr v-else><th class="selection-column"></th><th>循环记录</th><th>电池</th><th>循环号</th><th>充电容量(Ah)</th><th>放电容量(Ah)</th><th>充放电效率(%)</th><th>充电能量(Wh)</th><th>放电能量(Wh)</th></tr></thead><tbody><template v-for="row in pagedRows" :key="row.id"><tr v-if="activeTab === 'realtime'"><td class="selection-column"><el-checkbox :model-value="selectedIds.has(row.id)" :aria-label="`选择 ${row.recordNo}`" @change="(checked: string | number | boolean) => toggleRowSelection(row.id, Boolean(checked))" /></td><td class="record-code">{{ row.recordNo }}</td><td><strong>{{ row.batteryNo }}</strong></td><td>{{ row.cycleNo }}</td><td>{{ row.stepNo }}</td><td>{{ row.stepName }}</td><td>{{ row.sequenceId }}</td><td>{{ row.recordedAt }}</td><td>{{ row.absoluteTime }}</td><td>{{ row.voltage }}</td><td>{{ row.current }}</td><td>{{ row.capacity }}</td><td>{{ row.energy }}</td></tr><tr v-else-if="activeTab === 'step'"><td class="selection-column"><el-checkbox :model-value="selectedIds.has(row.id)" :aria-label="`选择 ${row.recordNo}`" @change="(checked: string | number | boolean) => toggleRowSelection(row.id, Boolean(checked))" /></td><td class="record-code">{{ row.recordNo }}</td><td><strong>{{ row.batteryNo }}</strong></td><td>{{ row.cycleNo }}</td><td>{{ row.stepNo }}</td><td>{{ row.stepName }}</td><td>{{ row.sequenceId }}</td><td>{{ row.recordedAt }}</td><td>{{ row.absoluteTime }}</td><td>{{ row.capacity }}</td><td>{{ row.energy }}</td></tr><tr v-else><td class="selection-column"><el-checkbox :model-value="selectedIds.has(row.id)" :aria-label="`选择 ${row.recordNo}`" @change="(checked: string | number | boolean) => toggleRowSelection(row.id, Boolean(checked))" /></td><td class="record-code">{{ row.recordNo }}</td><td><strong>{{ row.batteryNo }}</strong></td><td>{{ row.cycleNo }}</td><td>{{ row.chargeCapacity }}</td><td>{{ row.dischargeCapacity }}</td><td>{{ row.efficiency }}</td><td>{{ row.chargeEnergy }}</td><td>{{ row.dischargeEnergy }}</td></tr></template><tr v-if="!loading && !pagedRows.length"><td :colspan="activeTab === 'realtime' ? 13 : activeTab === 'step' ? 11 : 9" class="empty-cell">{{ loadError || '暂无检测柜记录' }}</td></tr></tbody></table></div> |
| | | <footer class="pagination-bar"><span>显示 {{ pagedRows.length }} 条,共 {{ filteredRows.length }} 条</span><el-pagination v-model:current-page="currentPage" v-model:page-size="pageSize" :page-sizes="[20, 50, 100]" :total="filteredRows.length" layout="sizes, prev, pager, next" background /></footer> |
| | | <el-dialog v-model="editorVisible" :title="`批量修改(${selectedIds.size} 条)`" width="520px" align-center><div class="edit-form"><label><span>修改字段</span><el-select v-model="editForm.field"><el-option label="电压(V)" value="voltage" /><el-option label="电流(A)" value="current" /><el-option label="容量(Ah)" value="capacity" /><el-option label="能量(Wh)" value="energy" /></el-select></label><label><span>修改值</span><el-input v-model="editForm.value" placeholder="请输入修改后的数值" /></label><label><span>修改原因</span><el-input v-model="editForm.remark" type="textarea" :rows="3" maxlength="120" show-word-limit placeholder="必填,将写入数据修订记录" /></label></div><template #footer><button class="secondary-button" type="button" @click="editorVisible=false">取消</button><button class="primary-button" type="button" @click="saveBatchEdit"><el-icon><Check /></el-icon>确认修改</button></template></el-dialog> |
| | | <el-dialog v-model="editorVisible" title="提交数据修改" width="520px" align-center><div class="edit-form"><p>修改已暂存,请填写本次行为原因后一次提交。</p><label><span>修改字段</span><el-select v-model="editForm.field"><el-option v-for="item in fieldOptions" :key="String(item.value)" :label="item.label" :value="item.value" /></el-select></label><label><span>修改值</span><el-input v-model="editForm.value" inputmode="decimal" placeholder="请输入修改后的数值" /></label><label><span>修改原因</span><el-input v-model="editForm.remark" type="textarea" :rows="3" maxlength="120" show-word-limit placeholder="必填,将写入数据修订记录" /></label></div><template #footer><button class="secondary-button" type="button" @click="editorVisible=false">取消</button><button class="primary-button" type="button" @click="submitBatchEdit"><el-icon><Check /></el-icon>提交修改</button></template></el-dialog> |
| | | </section> |
| | | </template> |
| | | <style scoped> |
| | | .danger-button{height:34px;padding:0 13px;border:1px solid #ee6666;border-radius:3px;color:#fff;background:#ee6666;cursor:pointer}.danger-button:disabled{opacity:.5;cursor:not-allowed} |
| | | .console-data-panel{min-width:0;border:1px solid #d8e1e9;background:#fff}.section-heading{position:relative;height:60px;display:flex;align-items:center;justify-content:space-between;padding:0 16px;border-bottom:1px solid var(--line);background:var(--surface-soft)}.section-heading>div{display:flex;align-items:center;gap:8px}.section-heading i{width:3px;height:16px;background:#2b78a3}.section-heading strong{color:var(--ink);font-size:14px}.section-heading span{color:#81909d;font-size:10px}.section-heading small{color:#647380;font-size:11px}.data-tabs{position:absolute;top:50%;left:50%;display:flex;justify-content:center;gap:8px;padding:0;border:0;background:transparent;transform:translate(-50%,-50%)}.data-tabs button{height:32px;min-width:78px;padding:0 18px;border:1px solid #cbd8e5;border-radius:4px;color:#52606d;background:#fff;box-shadow:0 1px 4px rgba(40,75,105,.08);font-size:13px;font-weight:600;cursor:pointer;transition:color .16s ease,background .16s ease,border-color .16s ease,box-shadow .16s ease}.data-tabs button:hover{color:#1677dd;border-color:#8ec4f4;box-shadow:0 3px 8px rgba(36,128,220,.14)}.data-tabs button.active{color:#fff;background:#1687ed;border-color:#1677dd;box-shadow:0 3px 8px rgba(26,112,220,.24)}.filter-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px 18px;padding:16px}.filter-grid label{min-width:0;display:grid;grid-template-columns:68px minmax(0,1fr);align-items:center;gap:8px}.filter-grid label>span{color:#52606d;font-size:12px}.filter-grid :deep(.el-select),.filter-grid :deep(.el-date-editor){width:100%}.filter-actions{grid-column:-2/-1;display:flex;justify-content:flex-end;gap:8px}.primary-button,.secondary-button{height:34px;display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:0 13px;border:1px solid transparent;border-radius:3px;font-size:13px;cursor:pointer;white-space:nowrap}.primary-button{color:#fff;background:var(--blue)}.secondary-button{color:#52606d;background:#fff;border-color:var(--line-strong)}.secondary-button:disabled{opacity:.5;cursor:not-allowed}.data-toolbar{min-height:52px;display:flex;align-items:center;justify-content:space-between;gap:12px;margin:0 16px;border-top:1px solid #d8e1e9;color:#7b8794;font-size:11px}.data-toolbar>div{display:flex;gap:8px}.load-error{color:#b34f4f}.table-wrap{width:100%;overflow-x:auto}table{width:100%;min-width:1260px;table-layout:fixed;border-collapse:collapse;color:#52606d;font-size:12px}th,td{height:44px;padding:0 10px;border-right:1px solid #dde7f0;border-bottom:1px solid #dde7f0;text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}th{height:40px;color:#52606d;background:#eaf4ff;border-color:#c7e0ff;font-weight:600}.record-code{font-family:'Cascadia Mono',Consolas,monospace;font-size:11px}.empty-cell{height:150px;color:#8795a3}.pagination-bar{min-height:58px;display:flex;align-items:center;justify-content:space-between;gap:16px;padding:8px 14px 8px 16px}.pagination-bar>span{color:#7b8794;font-size:11px}.pagination-bar :deep(.el-pagination){--el-pagination-button-bg-color:#edf4fa;--el-pagination-hover-color:var(--blue)}@media(max-width:900px){.filter-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.data-toolbar{align-items:flex-start;flex-direction:column;padding:10px 0}}@media(max-width:600px){.filter-grid{grid-template-columns:1fr;padding:14px 12px}.filter-actions{grid-column:auto}.filter-actions button{flex:1}.data-tabs{position:static;transform:none;gap:6px;padding:0 8px}.data-tabs button{min-width:0;flex:1;padding:0 12px}.data-toolbar{margin:0 12px}.pagination-bar{align-items:flex-start;flex-direction:column;overflow-x:auto}} |
| | | .toolbar-group{display:flex;align-items:center;gap:8px}.selection-column{width:46px;padding:0}.edit-form{display:grid;gap:14px}.edit-form label{display:grid;grid-template-columns:82px minmax(0,1fr);align-items:center;gap:10px}.edit-form label>span{color:#52606d;font-size:12px}.edit-form :deep(.el-select){width:100%} |
| | | /* 三个数据子页使用连续分段控件,保持当前页在标题栏中央突出显示。 */ |
| | |
| | | <script setup lang="ts"> |
| | | import { computed, ref } from 'vue' |
| | | import { ElMessage } from 'element-plus' |
| | | import { computed, onMounted, ref } from 'vue' |
| | | import { ElMessage, ElMessageBox } from 'element-plus' |
| | | import { useRoute, useRouter } from 'vue-router' |
| | | import * as XLSX from 'xlsx' |
| | | import { consoleDataRowsByTest, type ConsoleDataRow } from '@/stores/consoleTestData' |
| | | import { submitConsoleDataAction, type ConsoleDataType } from '@/api/consoleData' |
| | | import { getTestDetail } from '@/api/tests' |
| | | |
| | | type ImportStage = 'upload' | 'preview' | 'result' |
| | | type RawExcelRow = Record<string, unknown> |
| | |
| | | const router = useRouter() |
| | | const testId = Number(route.params.id) |
| | | const testName = String(route.query.testName || `测试任务 #${testId}`) |
| | | const canImport = route.query.completed === '1' |
| | | const canImport = ref(false) |
| | | const dataType = String(route.query.dataType || 'REALTIME') as ConsoleDataType |
| | | const fileInput = ref<HTMLInputElement | null>(null) |
| | | const selectedFile = ref<File | null>(null) |
| | | const validatedRows = ref<ValidatedRow[]>([]) |
| | | const stage = ref<ImportStage>('upload') |
| | | const dragging = ref(false) |
| | | |
| | | const requiredHeaders = ['电池号', '记录时间', '工步时间', '循环序号', '工步序号', '工步名称', '电压(V)', '电流(mA)', '容量(mAh)', '能量(mWh)', '温度(℃)', '正电位(V)', '负电位(V)'] |
| | | const numericHeaders = ['电压(V)', '电流(mA)', '容量(mAh)', '能量(mWh)', '温度(℃)', '正电位(V)', '负电位(V)'] |
| | | const requiredHeaders = dataType === 'CYCLE' ? ['记录编号', '电池号', '循环序号', '充电容量(Ah)', '放电容量(Ah)', '充放电效率(%)', '充电能量(Wh)', '放电能量(Wh)'] : dataType === 'STEP' ? ['工步记录编号', '电池号', '循环序号', '工步序号', '工步名称', '工步时间', '容量(Ah)', '能量(Wh)', '起始电压(V)', '终止电压(V)', '充电峰值电压(V)', '放电谷值电压(V)'] : ['记录编号', '电池号', '记录时间', '循环序号', '工步序号', '工步名称', '记录序号', '电压(V)', '电流(A)', '容量(Ah)', '能量(Wh)'] |
| | | const numericHeaders = requiredHeaders.filter(header => !['记录编号', '工步记录编号', '电池号', '记录时间', '工步名称', '工步时间'].includes(header)) |
| | | const validRows = computed(() => validatedRows.value.filter(row => row.errors.length === 0)) |
| | | const failedRows = computed(() => validatedRows.value.filter(row => row.errors.length > 0)) |
| | | const knownBatteries = computed(() => new Set((consoleDataRowsByTest[testId] || []).map(row => row.batteryNo))) |
| | | |
| | | function backToDataList() { |
| | | router.push({ path: `/test-list/${testId}/console`, query: { tab: 'data-list', state: canImport ? 'ended' : 'running' } }) |
| | | router.push({ path: `/test-list/${testId}/console`, query: { tab: 'data-list', state: canImport.value ? 'ended' : 'running' } }) |
| | | } |
| | | |
| | | onMounted(async () => { |
| | | try { |
| | | const detail = await getTestDetail(testId) |
| | | canImport.value = detail.receptionStatus === 'ENDED' && detail.testStatus === 'COMPLETED' |
| | | } catch { |
| | | canImport.value = false |
| | | } |
| | | }) |
| | | |
| | | function textValue(row: RawExcelRow, key: string) { |
| | | return String(row[key] ?? '').trim() |
| | | } |
| | | |
| | | function normalizeImportRow(row: RawExcelRow): RawExcelRow { |
| | | if (dataType === 'REALTIME') return { ...row, '工步时间': row['工步时间'] || '-', '电流(A)': row['电流(A)'], '容量(Ah)': row['容量(Ah)'], '能量(Wh)': row['能量(Wh)'], '温度(℃)': row['温度(℃)'] || '0', '正电位(V)': row['正电位(V)'] || '0', '负电位(V)': row['负电位(V)'] || '0' } |
| | | if (dataType === 'STEP') return { ...row, '记录编号': row['工步记录编号'], '记录时间': row['工步时间'], '电流(A)': '0', '容量(Ah)': row['容量(Ah)'], '能量(Wh)': row['能量(Wh)'], '温度(℃)': '0', '正电位(V)': '0', '负电位(V)': '0' } |
| | | return { ...row, '记录时间': '-', '工步时间': '-', '工步名称': '循环统计', '工步序号': '1', '电压(V)': '0', '电流(A)': '0', '容量(Ah)': row['充电容量(Ah)'], '能量(Wh)': row['充电能量(Wh)'], '温度(℃)': '0', '正电位(V)': '0', '负电位(V)': '0' } |
| | | } |
| | | |
| | | function validateRow(raw: RawExcelRow, sourceRow: number): ValidatedRow { |
| | |
| | | stepNo: stepNo || 1, |
| | | stepName: textValue(raw, '工步名称'), |
| | | voltage: textValue(raw, '电压(V)'), |
| | | current: textValue(raw, '电流(mA)'), |
| | | capacity: textValue(raw, '容量(mAh)'), |
| | | energy: textValue(raw, '能量(mWh)'), |
| | | current: textValue(raw, '电流(A)'), |
| | | capacity: textValue(raw, '容量(Ah)'), |
| | | energy: textValue(raw, '能量(Wh)'), |
| | | temperature: textValue(raw, '温度(℃)'), |
| | | positivePotential: textValue(raw, '正电位(V)'), |
| | | negativePotential: textValue(raw, '负电位(V)'), |
| | |
| | | return |
| | | } |
| | | selectedFile.value = file |
| | | validatedRows.value = rawRows.map((row, index) => validateRow(row, index + 2)) |
| | | const seen = new Set<string>() |
| | | validatedRows.value = rawRows.map((row, index) => { |
| | | const normalized = normalizeImportRow(row) |
| | | const validated = validateRow(normalized, index + 2) |
| | | const identity = validated.data.recordNo |
| | | if (identity && seen.has(identity)) validated.errors.push('本批次存在重复稳定身份') |
| | | if (identity) seen.add(identity) |
| | | return validated |
| | | }) |
| | | stage.value = 'preview' |
| | | } catch (error) { |
| | | ElMessage.error(error instanceof Error ? error.message : 'Excel 文件解析失败') |
| | |
| | | stage.value = 'upload' |
| | | } |
| | | |
| | | function confirmImport() { |
| | | if (!canImport) { |
| | | async function confirmImport() { |
| | | if (!canImport.value) { |
| | | ElMessage.warning('测试完成后才可以导入数据') |
| | | return |
| | | } |
| | | const currentRows = consoleDataRowsByTest[testId] || [] |
| | | let nextId = Math.max(0, ...currentRows.map(row => row.id)) + 1 |
| | | const imported = validRows.value.map(item => { |
| | | const id = nextId++ |
| | | return { |
| | | ...item.data, |
| | | id, |
| | | recordNo: item.data.recordNo || `RD-${String(testId).padStart(4, '0')}-IMP-${String(id).padStart(5, '0')}`, |
| | | } |
| | | }) |
| | | consoleDataRowsByTest[testId] = [...imported, ...currentRows] |
| | | let reason = '' |
| | | try { const result = await ElMessageBox.prompt('请输入本次导入原因', '提交导入', { inputPlaceholder: '必填,将写入数据修订记录', inputValidator: value => value.trim() ? true : '导入原因不能为空' }); reason = result.value.trim() } catch { return } |
| | | const imported = validRows.value.map(item => ({ ...item.data })) |
| | | try { |
| | | await submitConsoleDataAction(testId, { dataType, operationType: 'IMPORT', scopeSummary: `导入${validRows.value.length}条${dataType}数据`, identitySummary: JSON.stringify(imported.map(row => row.recordNo)), changesSummary: JSON.stringify(imported), reason, overrides: imported.map(row => ({ stableIdentity: row.recordNo, fieldValues: JSON.stringify(row), deleted: false })) }) |
| | | } catch (error) { ElMessage.error(error instanceof Error ? error.message : '导入提交失败'); return } |
| | | stage.value = 'result' |
| | | } |
| | | |
| | |
| | | <div v-else-if="stage === 'preview'" class="preview-stage"> |
| | | <div class="file-summary"><div><el-icon><Document /></el-icon><span><strong>{{ selectedFile?.name }}</strong><small>{{ ((selectedFile?.size || 0) / 1024).toFixed(1) }} KB · 已完成字段校验</small></span></div><button type="button" @click="resetUpload">重新选择</button></div> |
| | | <div class="validation-summary"><div><span>总数据</span><strong>{{ validatedRows.length }}</strong></div><div class="success"><span>校验通过</span><strong>{{ validRows.length }}</strong></div><div class="failed"><span>校验失败</span><strong>{{ failedRows.length }}</strong></div></div> |
| | | <div class="preview-table-wrap"><table><thead><tr><th>Excel行号</th><th>电池号</th><th>记录时间</th><th>循环</th><th>工步</th><th>工步名称</th><th>电压(V)</th><th>容量(mAh)</th><th>校验结果</th></tr></thead><tbody><tr v-for="item in validatedRows.slice(0, 12)" :key="item.sourceRow"><td>{{ item.sourceRow }}</td><td>{{ item.data.batteryNo }}</td><td>{{ item.data.recordedAt }}</td><td>{{ item.data.cycleNo }}</td><td>{{ item.data.stepNo }}</td><td>{{ item.data.stepName }}</td><td>{{ item.data.voltage }}</td><td>{{ item.data.capacity }}</td><td><span class="validation-state" :class="item.errors.length ? 'failed' : 'success'">{{ item.errors.length ? item.errors.join(';') : '通过' }}</span></td></tr></tbody></table></div> |
| | | <div class="preview-table-wrap"><table><thead><tr><th>Excel行号</th><th>电池号</th><th>记录时间</th><th>循环</th><th>工步</th><th>工步名称</th><th>电压(V)</th><th>容量(Ah)</th><th>校验结果</th></tr></thead><tbody><tr v-for="item in validatedRows.slice(0, 12)" :key="item.sourceRow"><td>{{ item.sourceRow }}</td><td>{{ item.data.batteryNo }}</td><td>{{ item.data.recordedAt }}</td><td>{{ item.data.cycleNo }}</td><td>{{ item.data.stepNo }}</td><td>{{ item.data.stepName }}</td><td>{{ item.data.voltage }}</td><td>{{ item.data.capacity }}</td><td><span class="validation-state" :class="item.errors.length ? 'failed' : 'success'">{{ item.errors.length ? item.errors.join(';') : '通过' }}</span></td></tr></tbody></table></div> |
| | | <p v-if="validatedRows.length > 12" class="preview-tip">当前预览前 12 条,共 {{ validatedRows.length }} 条数据</p> |
| | | <div class="stage-actions"><button class="primary-button" type="button" :disabled="!canImport" @click="confirmImport"><el-icon><Check /></el-icon>确认导入</button><button class="secondary-button" type="button" @click="resetUpload">返回上传</button></div> |
| | | </div> |