From 35789d2f15c426bafa876806487034f4c5ddc3eb Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期一, 31 八月 2026 20:54:11 +0800
Subject: [PATCH] 实现Ticket22控制台数据三子页

---
 src/components/ConsoleDataListPanel.vue |  356 +++++------------------------------------------------------
 1 files changed, 32 insertions(+), 324 deletions(-)

diff --git a/src/components/ConsoleDataListPanel.vue b/src/components/ConsoleDataListPanel.vue
index 1a44de3..aaabf7c 100644
--- a/src/components/ConsoleDataListPanel.vue
+++ b/src/components/ConsoleDataListPanel.vue
@@ -1,334 +1,42 @@
 <script setup lang="ts">
-import { computed, reactive, ref } from 'vue'
-import { ElMessage, ElMessageBox } from 'element-plus'
-import { useRouter } from 'vue-router'
-import * as XLSX from 'xlsx'
-import { dataRevisionRecords, type RevisionOperation } from '@/stores/dataRevisions'
-import { consoleDataRowsByTest, type ConsoleDataRow as DataRow } from '@/stores/consoleTestData'
+import { computed, onMounted, reactive, ref, watch } from 'vue'
+import { ElMessage } from 'element-plus'
+import { Download, Refresh, Search } from '@element-plus/icons-vue'
+import { getBtsCycles, getBtsRecords, getBtsSteps, type BtsCycleRecord, type BtsRecord, type BtsStepRecord } from '@/api/tests'
 
-interface ConsoleCell {
-  no: string
-  voltage: string
-  temperature: string
-  positive: string
-  negative: string
-  capacity: string
-  current: string
-}
-
-interface FilterState {
-  batteryNo: string
-  dateRange: [string, string] | null
-  cycleNo: string
-  stepNo: string
-}
-
-type EditableField = 'voltage' | 'current' | 'capacity' | 'energy' | 'temperature' | 'positivePotential' | 'negativePotential'
-
-const props = defineProps<{
-  testId: number
-  testName: string
-  startedAt: string
-  cells: ConsoleCell[]
-  canImport: boolean
-}>()
-
-const router = useRouter()
-const stepNames = ['恒流充电', '搁置', '恒流放电', '搁置', '恒流充电']
-const fieldOptions: Array<{ value: EditableField; label: string; unit: string }> = [
-  { value: 'voltage', label: '电压', unit: 'V' },
-  { value: 'current', label: '电流', unit: 'mA' },
-  { value: 'capacity', label: '容量', unit: 'mAh' },
-  { value: 'energy', label: '能量', unit: 'mWh' },
-  { value: 'temperature', label: '温度', unit: '℃' },
-  { value: 'positivePotential', label: '正电位', unit: 'V' },
-  { value: 'negativePotential', label: '负电位', unit: 'V' },
-]
-
-function formatDate(date: Date) {
-  const pad = (value: number) => String(value).padStart(2, '0')
-  return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
-}
-
-function buildRows(): DataRow[] {
-  const start = new Date(props.startedAt.replace(' ', 'T')).getTime()
-  return Array.from({ length: 658 }, (_, index) => {
-    const cell = props.cells[index % props.cells.length]
-    const stepIndex = index % stepNames.length
-    const point = Math.floor(index / Math.max(props.cells.length, 1))
-    const voltage = Number(cell?.voltage || 0) + Math.sin(index / 17) * 0.018
-    const capacity = Number(cell?.capacity || 0) + point * 0.82
-    return {
-      id: index + 1,
-      recordNo: `RD-${String(props.testId).padStart(4, '0')}-${String(index + 1).padStart(6, '0')}`,
-      batteryNo: cell?.no || '-',
-      recordedAt: formatDate(new Date(start + index * 60_000)),
-      stepDuration: `00:${String(point % 60).padStart(2, '0')}:${String(index % 60).padStart(2, '0')}.000`,
-      cycleNo: Math.floor(index / (stepNames.length * Math.max(props.cells.length, 1))) + 1,
-      stepNo: stepIndex + 1,
-      stepName: stepNames[stepIndex],
-      voltage: voltage.toFixed(4),
-      current: (Number(cell?.current || 0) + Math.cos(index / 13) * 8).toFixed(1),
-      capacity: capacity.toFixed(1),
-      energy: (capacity * voltage).toFixed(1),
-      temperature: (Number(cell?.temperature || 0) + Math.sin(index / 23) * 0.5).toFixed(1),
-      positivePotential: (Number(cell?.positive || 0) + Math.sin(index / 19) * 0.006).toFixed(4),
-      negativePotential: (Number(cell?.negative || 0) + Math.cos(index / 21) * 0.006).toFixed(4),
-    }
-  })
-}
-
-if (!consoleDataRowsByTest[props.testId]) consoleDataRowsByTest[props.testId] = buildRows()
-const rows = computed<DataRow[]>({
-  get: () => consoleDataRowsByTest[props.testId] || [],
-  set: value => { consoleDataRowsByTest[props.testId] = value },
-})
-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<number>>(new Set())
-const editorVisible = ref(false)
-const editForm = reactive<{ field: EditableField; value: string; remark: string }>({ field: 'voltage', value: '', remark: '' })
-
-const batteryOptions = computed(() => [...new Set(props.cells.map(cell => cell.no))])
-const cycleOptions = computed(() => [...new Set(rows.value.map(row => row.cycleNo))].sort((a, b) => a - b))
-const filteredRows = computed(() => rows.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 (query.stepNo && row.stepNo !== Number(query.stepNo)) return false
-  if (query.dateRange) {
-    const date = row.recordedAt.slice(0, 10)
-    if (date < query.dateRange[0] || date > query.dateRange[1]) return false
-  }
-  return true
-}))
-const pagedRows = computed(() => {
-  const start = (currentPage.value - 1) * pageSize.value
-  return filteredRows.value.slice(start, start + pageSize.value)
-})
-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 applySearch() {
-  appliedFilters.value = {
-    batteryNo: filters.batteryNo,
-    dateRange: filters.dateRange ? [...filters.dateRange] as [string, string] : null,
-    cycleNo: filters.cycleNo,
-    stepNo: filters.stepNo,
-  }
-  currentPage.value = 1
-  selectedIds.value = new Set()
-  ElMessage.success(`已找到 ${filteredRows.value.length} 条数据`)
-}
-
-function resetSearch() {
-  filters.batteryNo = ''
-  filters.dateRange = null
-  filters.cycleNo = ''
-  filters.stepNo = ''
-  appliedFilters.value = { batteryNo: '', dateRange: null, cycleNo: '', stepNo: '' }
-  currentPage.value = 1
-  selectedIds.value = new Set()
-}
-
-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
-}
-
-function toggleRowSelection(id: number, checked: boolean) {
-  const next = new Set(selectedIds.value)
-  checked ? next.add(id) : next.delete(id)
-  selectedIds.value = next
-}
-
-function openSingleEditor(row: DataRow) {
-  router.push({
-    path: `/test-list/${props.testId}/console/data/${row.id}/edit`,
-    query: { testName: props.testName, completed: props.canImport ? '1' : '0' },
-  })
-}
-
-function openBatchEditor() {
-  if (!selectedIds.value.size) {
-    ElMessage.warning('请先勾选需要批量修改的数据')
-    return
-  }
-  editForm.field = 'voltage'
-  editForm.value = ''
-  editForm.remark = ''
-  editorVisible.value = true
-}
-
-function revisionValues(row: DataRow) {
-  return {
-    voltage: row.voltage,
-    current: row.current,
-    capacity: row.capacity,
-    energy: row.energy,
-    temperature: row.temperature,
-    positivePotential: row.positivePotential,
-    negativePotential: row.negativePotential,
-  }
-}
-
-function recordRevision(row: DataRow, operation: RevisionOperation, remark: string, revisedRow: DataRow | null) {
-  const now = formatDate(new Date())
-  dataRevisionRecords.unshift({
-    id: `DR-${Date.now()}-${row.id}`,
-    testId: props.testId,
-    testName: props.testName,
-    testedAt: row.recordedAt,
-    generatedAt: row.recordedAt,
-    modifiedAt: now,
-    modifier: '管理员',
-    remark,
-    operation,
-    stepName: row.stepName,
-    cycleNo: row.cycleNo,
-    stepNo: row.stepNo,
-    stepDuration: row.stepDuration,
-    absoluteTime: row.recordedAt,
-    originalData: revisionValues(row),
-    revisedData: revisedRow ? revisionValues(revisedRow) : null,
-  })
-}
-
-function saveEdit() {
-  if (!editForm.value.trim()) {
-    ElMessage.warning('请输入修改后的数值')
-    return
-  }
-  if (!Number.isFinite(Number(editForm.value))) {
-    ElMessage.warning('修改值必须是有效数字')
-    return
-  }
-  if (!editForm.remark.trim()) {
-    ElMessage.warning('请填写修改原因')
-    return
-  }
-  const targetIds = selectedIds.value
-  rows.value = rows.value.map(row => {
-    if (!targetIds.has(row.id)) return row
-    const revised = { ...row, [editForm.field]: editForm.value.trim() }
-    recordRevision(row, '修改', editForm.remark.trim(), revised)
-    return revised
-  })
-  editorVisible.value = false
-  ElMessage.success(`已修改 ${targetIds.size} 条数据并生成修订记录`)
-}
-
-async function removeRow(row: DataRow) {
-  try {
-    const { value } = await ElMessageBox.prompt('删除后数据不可恢复,请填写删除原因。原始数据和原因会保留在修订记录中。', '删除数据', {
-      confirmButtonText: '确认删除', cancelButtonText: '取消', type: 'warning', confirmButtonClass: 'danger-confirm',
-      inputPlaceholder: '请输入删除原因', inputValidator: input => Boolean(input.trim()) || '删除原因不能为空',
-    })
-    recordRevision(row, '删除', value.trim(), null)
-    rows.value = rows.value.filter(item => item.id !== row.id)
-    const next = new Set(selectedIds.value)
-    next.delete(row.id)
-    selectedIds.value = next
-    if (pagedRows.value.length === 0 && currentPage.value > 1) currentPage.value -= 1
-    ElMessage.success('数据已删除,修订记录已保留')
-  } catch { /* 用户取消 */ }
-}
-
-function escapeCsv(value: string | number) {
-  return `"${String(value).replaceAll('"', '""')}"`
-}
-
-function downloadCsv(source: DataRow[], filename: string) {
-  if (!source.length) {
-    ElMessage.warning('没有可下载的数据')
-    return
-  }
-  const headers = ['记录编号', '电池号', '记录时间', '工步时间', '循环序号', '工步序号', '工步名称', '电压(V)', '电流(mA)', '容量(mAh)', '能量(mWh)', '温度(℃)', '正电位(V)', '负电位(V)']
-  const records = source.map(row => [row.recordNo, row.batteryNo, row.recordedAt, row.stepDuration, row.cycleNo, row.stepNo, row.stepName, row.voltage, row.current, row.capacity, row.energy, row.temperature, row.positivePotential, row.negativePotential])
-  const content = `\uFEFF${[headers, ...records].map(record => record.map(escapeCsv).join(',')).join('\n')}`
-  const url = URL.createObjectURL(new Blob([content], { type: 'text/csv;charset=utf-8' }))
-  const anchor = document.createElement('a')
-  anchor.href = url
-  anchor.download = filename
-  anchor.click()
-  window.setTimeout(() => URL.revokeObjectURL(url), 1000)
-  ElMessage.success(`已下载 ${source.length} 条数据`)
-}
-
-function downloadSelected() {
-  downloadCsv(rows.value.filter(row => selectedIds.value.has(row.id)), `${props.testName}_选中数据.csv`)
-}
-
-function downloadTemplate() {
-  const headers = [['记录编号', '电池号', '记录时间', '工步时间', '循环序号', '工步序号', '工步名称', '电压(V)', '电流(mA)', '容量(mAh)', '能量(mWh)', '温度(℃)', '正电位(V)', '负电位(V)']]
-  const worksheet = XLSX.utils.aoa_to_sheet(headers)
-  worksheet['!cols'] = headers[0].map(header => ({ wch: Math.max(header.length + 4, 12) }))
-  const workbook = XLSX.utils.book_new()
-  XLSX.utils.book_append_sheet(workbook, worksheet, '测试数据')
-  XLSX.writeFile(workbook, '测试数据导入模板.xlsx')
-  ElMessage.success('导入模板已下载')
-}
-
-function goToImport() {
-  router.push({
-    path: `/test-list/${props.testId}/console/data/import`,
-    query: { testName: props.testName, completed: props.canImport ? '1' : '0' },
-  })
-}
-
-function goToRevisions() {
-  router.push({ path: '/data-revision', query: { testId: String(props.testId), testName: props.testName } })
-}
+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 }
+const props = defineProps<{ testId: number; testName: string; cells: ConsoleCell[]; bindings?: Array<{ batteryNo: string; devid?: number | null; chlid?: number | null }> }>()
+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 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)
+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) } }
+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) } }
+function mapCycle(record: BtsCycleRecord, batteryNo: string, deviceId: number, channelId: number): DataRow { const startedAt = formatTime(record.startedAt); return { id: `cycle:${deviceId}:${channelId}:${record.cycleNo}`, recordNo: `CYCLE-${deviceId}-${channelId}-${record.cycleNo}`, batteryNo, cycleNo: record.cycleNo, stepNo: 0, stepName: '循环统计', sequenceId: record.recordCount, recordedAt: startedAt, absoluteTime: startedAt, voltage: '-', current: '-', capacity: Number(record.chargeCapacityAh || record.dischargeCapacityAh).toFixed(4), energy: Number(record.chargeEnergyWh || 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 } }
+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 (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 }))
+const pagedRows = computed(() => filteredRows.value.slice((currentPage.value - 1) * pageSize.value, currentPage.value * pageSize.value))
+function applySearch() { appliedFilters.value = { batteryNo: filters.batteryNo, dateRange: filters.dateRange ? [...filters.dateRange] as [string, string] : null, cycleNo: filters.cycleNo, stepNo: filters.stepNo }; currentPage.value = 1 }
+function resetSearch() { Object.assign(filters, { batteryNo: '', dateRange: null, cycleNo: '', stepNo: '' }); Object.assign(appliedFilters.value, { batteryNo: '', dateRange: null, cycleNo: '', stepNo: '' }); currentPage.value = 1 }
+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 = ['记录编号', '电池', '循环号', '实际工步号', '实际工步名称', '记录序号', '记录时间', '绝对时间', '电压(V)', '电流(A)', '容量(Ah)', '能量(Wh)']; const records = 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) }
+onMounted(loadRecords); watch(() => props.bindings, loadRecords, { deep: true })
 </script>
-
 <template>
   <section class="console-data-panel">
-    <header class="section-heading"><div><i></i><strong>查询表格</strong><span>TEST DATA QUERY</span></div><small>共 {{ 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="battery in batteryOptions" :key="battery" :label="battery" :value="battery" /></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="至" unlink-panels /></label>
-      <label><span>循环</span><el-select v-model="filters.cycleNo" clearable placeholder="全部"><el-option label="全部" value="" /><el-option v-for="cycle in cycleOptions" :key="cycle" :label="`循环 ${cycle}`" :value="String(cycle)" /></el-select></label>
-      <label><span>工步</span><el-select v-model="filters.stepNo" clearable placeholder="全部"><el-option label="全部" value="" /><el-option v-for="(name, index) in stepNames" :key="index" :label="`${index + 1}-${name}`" :value="String(index + 1)" /></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><RefreshLeft /></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>
-      <div class="toolbar-group">
-        <button class="secondary-button" type="button" @click="downloadCsv(filteredRows, `${testName}_全部数据.csv`)"><el-icon><Download /></el-icon>下载全部</button>
-        <button class="secondary-button" type="button" @click="downloadSelected"><el-icon><Download /></el-icon>下载选中</button>
-      </div>
-    </div>
-
-    <div class="table-wrap">
-      <table>
-        <thead><tr><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>工步时间(h:min:s.ms)</th><th>循环序号</th><th>工步序号</th><th>工步名称</th><th>电压(V)</th><th>电流(mA)</th><th>容量(mAh)</th><th>能量(mWh)</th><th>温度(℃)</th><th>正电位(V)</th><th>负电位(V)</th><th class="action-column">操作</th></tr></thead>
-        <tbody>
-          <tr v-for="row in pagedRows" :key="row.id">
-            <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.recordedAt }}</td><td>{{ row.stepDuration }}</td><td>{{ row.cycleNo }}</td><td>{{ row.stepNo }}</td><td>{{ row.stepName }}</td><td>{{ row.voltage }}</td><td>{{ row.current }}</td><td>{{ row.capacity }}</td><td>{{ row.energy }}</td><td>{{ row.temperature }}</td><td>{{ row.positivePotential }}</td><td>{{ row.negativePotential }}</td>
-            <td class="action-column"><div class="row-actions"><button type="button" @click="openSingleEditor(row)">编辑</button><button class="delete-action" type="button" @click="removeRow(row)">删除</button></div></td>
-          </tr>
-          <tr v-if="!pagedRows.length"><td colspan="16" class="empty-cell">暂无符合条件的测试数据</td></tr>
-        </tbody>
-      </table>
-    </div>
+    <header class="section-heading"><div><i></i><strong>数据列表</strong><span>TEST DATA</span></div><small>{{ loading ? '加载中...' : `共 ${filteredRows.length} 条` }}</small></header>
+    <nav class="data-tabs" aria-label="数据列表子页"><button v-for="tab in [{ key: 'realtime', label: '实时' }, { key: 'step', label: '工步' }, { key: 'cycle', label: '循环' }]" :key="tab.key" type="button" :class="{ active: activeTab === tab.key }" @click="changeTab(tab.key as DataTab)">{{ tab.label }}</button></nav>
+    <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"><span v-if="loadError" class="load-error">{{ loadError }}</span><span v-else>仅展示检测柜实际返回的 {{ activeTab === 'realtime' ? 'record' : activeTab === 'step' ? 'step' : 'cycle' }} 数据</span><div><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="table-wrap"><table><thead><tr v-if="activeTab === 'realtime'"><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>工步记录</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>循环记录</th><th>电池</th><th>循环号</th><th>记录数量</th><th>开始时间</th><th>结束时间</th><th>充电容量(Ah)</th><th>放电容量(Ah)</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="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="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="record-code">{{ row.recordNo }}</td><td><strong>{{ row.batteryNo }}</strong></td><td>{{ row.cycleNo }}</td><td>{{ row.sequenceId }}</td><td>{{ row.recordedAt }}</td><td>{{ row.absoluteTime }}</td><td>{{ row.capacity }}</td><td>-</td><td>{{ row.energy }}</td><td>-</td></tr></template><tr v-if="!loading && !pagedRows.length"><td :colspan="activeTab === 'realtime' ? 12 : 10" 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 v-for="field in fieldOptions" :key="field.value" :label="`${field.label}(${field.unit})`" :value="field.value" /></el-select></label><label><span>修改值</span><el-input v-model="editForm.value" placeholder="请输入修改后的数值" /></label><label class="remark-field"><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="saveEdit"><el-icon><Check /></el-icon>确认修改</button></template>
-    </el-dialog>
   </section>
 </template>
-
 <style scoped>
-.console-data-panel{min-width:0;border:1px solid #d8e1e9;background:#fff}.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;font-weight:500;cursor:pointer;white-space:nowrap}.primary-button{color:#fff;background:var(--blue);border-color:var(--blue)}.primary-button:hover{background:#176fcb}.secondary-button{color:#52606d;background:#fff;border-color:var(--line-strong)}.secondary-button:hover{color:var(--blue);border-color:#9dc8f5}.section-heading{height:46px;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}.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;font-weight:500}.filter-grid :deep(.el-select),.filter-grid :deep(.el-date-editor){width:100%}.filter-grid :deep(.el-input__wrapper),.filter-grid :deep(.el-select__wrapper){min-height:34px;border-radius:3px;background:#f8fafc;box-shadow:0 0 0 1px #d8e1e9 inset}.filter-actions{grid-column:-2/-1;display:flex;justify-content:flex-end;gap:8px}.data-toolbar{min-height:58px;display:flex;align-items:center;justify-content:space-between;gap:14px;margin:0 16px;padding:10px 0;border-top:1px solid #d8e1e9}.toolbar-group{display:flex;align-items:center;gap:8px}.file-input{display:none}.table-wrap{width:100%;overflow-x:auto}table{width:100%;min-width:1820px;table-layout:fixed;border-collapse:collapse;color:#52606d;font-size:12px}th,td{height:46px;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}th:nth-child(2){width:158px}th:nth-child(3){width:110px}th:nth-child(4){width:158px}th:nth-child(5){width:150px}th:nth-child(6),th:nth-child(7){width:78px}th:nth-child(8){width:100px}th:nth-child(n+9):nth-child(-n+15){width:96px}.selection-column{width:46px;padding:0}.record-code{color:#647380;font-family:'Cascadia Mono',Consolas,monospace;font-size:11px}td strong{color:#26333f}.action-column{position:sticky;right:0;z-index:2;width:112px;background:#fff;overflow:visible;box-shadow:inset 1px 0 0 #b4c7d9,-8px 0 14px rgba(23,49,74,.1)}th.action-column{z-index:3;background:#eaf4ff}tbody tr:hover td{background:#f7fbff}tbody tr:hover td.action-column{background:#f7fbff}.row-actions{display:flex;align-items:center;justify-content:center;gap:14px}.row-actions button{border:0;color:var(--blue);background:transparent;font-size:12px;cursor:pointer}.row-actions .delete-action{color:#ad4a4a}.empty-cell{height:150px;color:#8795a3;text-align:center}.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)}.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 .remark-field{align-items:start}.edit-form :deep(.el-select){width:100%}@media(max-width:1250px){.filter-grid{grid-template-columns:repeat(3,minmax(0,1fr))}.filter-actions{grid-column:-2/-1}}@media(max-width:900px){.filter-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.data-toolbar{align-items:flex-start;flex-direction:column}.toolbar-group{flex-wrap:wrap}}@media(max-width:600px){.filter-grid{grid-template-columns:1fr;padding:14px 12px}.filter-actions{grid-column:auto}.filter-actions button{flex:1}.data-toolbar{margin:0 12px}.toolbar-group{width:100%}.toolbar-group button{flex:1;padding:0 8px}.pagination-bar{align-items:flex-start;flex-direction:column;overflow-x:auto}.section-heading span{display:none}}
+.console-data-panel{min-width:0;border:1px solid #d8e1e9;background:#fff}.section-heading{height:46px;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{display:flex;gap:8px;padding:12px 16px 0;border-bottom:1px solid #d8e1e9}.data-tabs button{height:32px;padding:0 18px;border:1px solid transparent;border-bottom:2px solid transparent;color:#52606d;background:#fff;font-size:12px;cursor:pointer}.data-tabs button.active{color:#1677dd;border-bottom-color:#1677dd;font-weight:600}.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{overflow-x:auto}.data-toolbar{margin:0 12px}.pagination-bar{align-items:flex-start;flex-direction:column;overflow-x:auto}}
 </style>

--
Gitblit v1.7.1