yeg
2026-09-03 9dbd6bbcd9a3078540e624c8470434aaae6c821b
完善工步图表电池范围选择

支持多电池公共循环工步对比,并为无公共数据的电池提供单独循环和工步选择。按每颗电池循环内工步位置匹配逻辑工步,保留缺失数据为空白并在图例显示实际范围。
2个文件已修改
187 ■■■■■ 已修改文件
src/components/ConsoleChartPanel.vue 185 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/TestConsole.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/ConsoleChartPanel.vue
@@ -12,7 +12,7 @@
} from '@/utils/compareReportPrint'
import type { CompareMetricKey } from '@/stores/compareReports'
import { useBatteryGroupFields } from '@/stores/batteryGroupFields'
import { saveTestConsoleDetail, type PersistedTestRecord, type TestConsoleDetail, type TestLogicStepMapping } from '@/api/tests'
import { saveTestConsoleDetail, type BtsTestPlanRow, type PersistedTestRecord, type TestConsoleDetail, type TestLogicStepMapping } from '@/api/tests'
interface ConsoleCell {
  no: string
@@ -31,6 +31,7 @@
  testName?: string
  batteryGroup?: string
  records: PersistedTestRecord[]
  stepPlanRows?: BtsTestPlanRow[]
  logicStepMappings?: TestLogicStepMapping[]
  consoleDetail?: TestConsoleDetail | null
}>(), {
@@ -38,6 +39,7 @@
  testName: '',
  batteryGroup: '',
  records: () => [],
  stepPlanRows: () => [],
  logicStepMappings: () => [],
  consoleDetail: null,
})
@@ -49,6 +51,8 @@
const selectedStep = ref('')
const selectedCycle = ref('')
const selectedCellNos = ref<string[]>(props.cells.map(cell => cell.no))
const batteryOverrides = reactive<Record<string, { cycle: string; step: string }>>({})
const overrideEditing = reactive<Record<string, boolean>>({})
const experimentName = ref(props.testName || 'XXX实验')
const batteryFields = ref(['额定容量', '电流', '内阻'])
const specialFields = ref(['温升'])
@@ -94,11 +98,55 @@
  .map(record => Number(record.cycleNo))
  .filter(value => Number.isFinite(value) && value > 0))].sort((a, b) => a - b))
// record.sourceType !== 'SNAPSHOT' 的数据不参与工步图表。
const snapshotRecords = computed(() => props.records.filter(record => record.sourceType === 'SNAPSHOT'))
const cycleStepPositions = computed(() => {
  // 工步号在设备侧跨循环递增;以测试内每个循环最早观测到的工步号为基准,保留缺失工步形成的空位。
  const stepsByCycle = new Map<number, number[]>()
  snapshotRecords.value.forEach(record => {
    const cycleNo = Number(record.cycleNo)
    const stepNo = Number(record.stepNo)
    if (!Number.isFinite(cycleNo) || !Number.isFinite(stepNo) || cycleNo <= 0 || stepNo <= 0) return
    const key = cycleNo
    const steps = stepsByCycle.get(key) || []
    if (!steps.includes(stepNo)) steps.push(stepNo)
    stepsByCycle.set(key, steps)
  })
  const positions = new Map<string, number>()
  let planCycleNo = 1
  ;(props.stepPlanRows || []).forEach(row => {
    if (row.rowType === 'CYCLE') {
      planCycleNo = Number(row.cycleCount) || planCycleNo + 1
      return
    }
    if (row.rowType === 'STEP' && row.cyclePosition != null && Number(row.stepNo) > 0) {
      positions.set(`${planCycleNo}::${Number(row.stepNo)}`, Number(row.cyclePosition))
    }
  })
  stepsByCycle.forEach((steps, key) => {
    const firstStepNo = Math.min(...steps)
    steps.forEach(stepNo => {
      const positionKey = `${key}::${stepNo}`
      if (!positions.has(positionKey)) positions.set(positionKey, stepNo - firstStepNo + 1)
    })
  })
  return positions
})
function cycleStepPosition(record: PersistedTestRecord) {
  return cycleStepPositions.value.get(`${Number(record.cycleNo)}::${Number(record.stepNo)}`) || null
}
function logicalStepMatches(record: PersistedTestRecord, mapping: TestLogicStepMapping) {
  const position = cycleStepPosition(record)
  return position !== null && position >= mapping.startStepNo && position <= mapping.endStepNo
}
const stepOptions = computed<StepOption[]>(() => {
  const cycleNo = Number(selectedCycle.value)
  if (!Number.isFinite(cycleNo) || cycleNo <= 0) return []
  const actual = new Map<string, StepOption>()
  for (const record of props.records.filter(item => item.sourceType === 'SNAPSHOT')) {
  for (const record of snapshotRecords.value) {
    const stepNo = Number(record.stepNo)
    if (Number(record.cycleNo) !== cycleNo || !Number.isFinite(stepNo) || stepNo <= 0) continue
    const value = `actual:${cycleNo}:${stepNo}`
@@ -106,8 +154,7 @@
  }
  const logical: StepOption[] = []
  for (const mapping of props.logicStepMappings || []) {
    const hit = props.records.some(record => record.sourceType === 'SNAPSHOT' && Number(record.cycleNo) === cycleNo
      && Number(record.stepNo) >= mapping.startStepNo && Number(record.stepNo) <= mapping.endStepNo)
    const hit = snapshotRecords.value.some(record => Number(record.cycleNo) === cycleNo && logicalStepMatches(record, mapping))
    if (hit) logical.push({ value: `logical:${cycleNo}:${mapping.logicalNo}`, label: `${cycleNo}-逻辑${mapping.logicalNo} ${mapping.logicalName}`, cycleNo, logical: mapping })
  }
  return [...actual.values(), ...logical]
@@ -153,6 +200,13 @@
  if (!options.length) selectedCycle.value = ''
  else if (!options.includes(current)) selectedCycle.value = String(options[0])
}, { immediate: true })
watch(() => props.logicStepMappings, mappings => {
  if (!mappings?.length || !selectedCycle.value || selectedStep.value.startsWith('logical:')) return
  // 保存新的合并规则后立即切到逻辑工步,让合并结果在图表中可见;用户仍可手动切回实际工步。
  const logical = stepOptions.value.find(option => option.logical)
  if (logical) selectedStep.value = logical.value
}, { deep: true, immediate: true })
async function addCustomField() {
  try {
@@ -220,6 +274,86 @@
function ensureSelectedCell() {
  if (!selectedCellNos.value.length && props.cells.length) selectedCellNos.value = [props.cells[0].no]
  const selected = new Set(selectedCellNos.value)
  Object.keys(overrideEditing).forEach(cellNo => {
    if (!selected.has(cellNo)) overrideEditing[cellNo] = false
  })
}
function cellCycleOptions(cellNo: string) {
  return [...new Set(snapshotRecords.value.filter(record => String(record.batteryNo) === cellNo)
    .map(record => Number(record.cycleNo)).filter(value => Number.isFinite(value) && value > 0))].sort((a, b) => a - b)
}
function cellStepOptions(cellNo: string, cycleValue: string | number) {
  const cycleNo = Number(cycleValue)
  if (!Number.isFinite(cycleNo) || cycleNo <= 0) return [] as StepOption[]
  const actual = new Map<string, StepOption>()
  snapshotRecords.value.filter(record => String(record.batteryNo) === cellNo && Number(record.cycleNo) === cycleNo)
    .forEach(record => {
      const stepNo = Number(record.stepNo)
      if (!Number.isFinite(stepNo) || stepNo <= 0) return
      const value = `actual:${cycleNo}:${stepNo}`
      if (!actual.has(value)) actual.set(value, { value, label: `${cycleNo}-${stepNo} ${record.stepName || '-'}`, cycleNo, stepNo })
    })
  const logical = (props.logicStepMappings || []).filter(mapping => snapshotRecords.value.some(record => String(record.batteryNo) === cellNo
    && Number(record.cycleNo) === cycleNo && logicalStepMatches(record, mapping)))
    .map(mapping => ({ value: `logical:${cycleNo}:${mapping.logicalNo}`, label: `${cycleNo}-逻辑${mapping.logicalNo} ${mapping.logicalName}`, cycleNo, logical: mapping }))
  return [...actual.values(), ...logical]
}
function cellUsesOverride(cellNo: string) {
  return Boolean(batteryOverrides[cellNo] && overrideEditing[cellNo])
}
function cellSelection(cellNo: string) {
  const override = batteryOverrides[cellNo]
  if (cellUsesOverride(cellNo) && override) return override
  return { cycle: selectedCycle.value, step: selectedStep.value }
}
function cellHasPublicData(cellNo: string) {
  const selection = { cycle: selectedCycle.value, step: selectedStep.value }
  return recordsForCellSelection(cellNo, selection).length > 0
}
function recordsForCellSelection(cellNo: string, selection: { cycle: string; step: string }) {
  const cycle = Number(selection.cycle)
  const option: StepOption | undefined = selection.step ? (cellStepOptions(cellNo, selection.cycle).find(item => item.value === selection.step)
    || stepOptions.value.find(item => item.value === selection.step && item.cycleNo === cycle)) : undefined
  if (!Number.isFinite(cycle) || cycle <= 0 || !option) return []
  return snapshotRecords.value.filter(record => {
    if (String(record.batteryNo) !== cellNo || Number(record.cycleNo) !== cycle) return false
    if (option.logical) return logicalStepMatches(record, option.logical)
    return Number(record.stepNo) === option.stepNo
  })
}
function toggleBatteryOverride(cellNo: string) {
  if (cellUsesOverride(cellNo)) {
    overrideEditing[cellNo] = false
    return
  }
  const cycles = cellCycleOptions(cellNo)
  const cycle = cycles.includes(Number(selectedCycle.value)) ? selectedCycle.value : String(cycles[0] || '')
  const steps = cellStepOptions(cellNo, cycle)
  batteryOverrides[cellNo] = { cycle, step: steps[0]?.value || '' }
  overrideEditing[cellNo] = true
}
function onOverrideCycleChange(cellNo: string) {
  const override = batteryOverrides[cellNo]
  if (!override) return
  const options = cellStepOptions(cellNo, override.cycle)
  if (!options.some(option => option.value === override.step)) override.step = options[0]?.value || ''
}
function selectedRangeLabel(cellNo: string) {
  const selection = cellSelection(cellNo)
  const option = cellStepOptions(cellNo, selection.cycle).find(item => item.value === selection.step)
    || stepOptions.value.find(item => item.value === selection.step && item.cycleNo === Number(selection.cycle))
  if (!selection.cycle || !option) return '未选择范围'
  return `循环 ${selection.cycle} / ${option.label}`
}
function metricMatches(metric: CompareMetricKey, record: PersistedTestRecord) {
@@ -253,17 +387,7 @@
}
function selectedRecords() {
  const option = stepOptions.value.find(item => item.value === selectedStep.value)
  if (!option) return []
  const cycle = Number(selectedCycle.value)
  if (!Number.isFinite(cycle) || cycle <= 0) return []
  return props.records.filter(record => {
    if (record.sourceType !== 'SNAPSHOT') return false
    if (Number(record.cycleNo) !== cycle) return false
    const stepNo = Number(record.stepNo)
    if (option.logical) return stepNo >= option.logical.startStepNo && stepNo <= option.logical.endStepNo
    return stepNo === option.stepNo
  })
  return selectedCells.value.flatMap(cell => recordsForCellSelection(cell.no, cellSelection(cell.no)))
}
const hasSelectedRecords = computed(() => selectedRecords().length > 0)
@@ -374,6 +498,23 @@
        <label class="step-select"><span>循环</span><el-select v-model="selectedCycle" :disabled="!cycleOptions.length" placeholder="暂无实时循环"><el-option v-for="item in cycleOptions" :key="item" :label="`循环 ${item}`" :value="String(item)" /></el-select></label>
        <label class="step-select"><span>循环工步</span><el-select v-model="selectedStep" :disabled="!stepOptions.length" placeholder="暂无该循环工步"><el-option v-for="item in stepOptions" :key="item.value" :label="item.label" :value="item.value" /></el-select></label>
        <div class="battery-select"><span>电池号</span><el-checkbox-group v-model="selectedCellNos" @change="ensureSelectedCell"><el-checkbox v-for="cell in cells" :key="cell.no" :value="cell.no">{{ cell.no }}</el-checkbox></el-checkbox-group></div>
        <div v-if="selectedCells.length" class="battery-ranges">
          <div v-for="cell in selectedCells" :key="`range-${cell.no}`" class="battery-range-row">
            <strong>{{ cell.no }}</strong>
            <span v-if="cellUsesOverride(cell.no)" class="range-status override">已单独选择</span>
            <span v-else :class="['range-status', { missing: !cellHasPublicData(cell.no) }]">{{ cellHasPublicData(cell.no) ? '公共范围' : '暂无公共范围数据' }}</span>
            <span class="range-label">{{ selectedRangeLabel(cell.no) }}</span>
            <button class="secondary-button compact-button" type="button" @click="toggleBatteryOverride(cell.no)">{{ cellUsesOverride(cell.no) ? '使用公共范围' : '单独选择' }}</button>
            <template v-if="cellUsesOverride(cell.no)">
              <el-select v-model="batteryOverrides[cell.no].cycle" class="range-select" placeholder="循环" @change="onOverrideCycleChange(cell.no)">
                <el-option v-for="item in cellCycleOptions(cell.no)" :key="item" :label="`循环 ${item}`" :value="String(item)" />
              </el-select>
              <el-select v-model="batteryOverrides[cell.no].step" class="range-select" placeholder="循环工步">
                <el-option v-for="item in cellStepOptions(cell.no, batteryOverrides[cell.no].cycle)" :key="item.value" :label="item.label" :value="item.value" />
              </el-select>
            </template>
          </div>
        </div>
      </div>
    </section>
@@ -386,7 +527,7 @@
        <strong>电池图例</strong>
        <div v-for="cell in selectedCells" :key="cell.no" class="chart-legend-item">
          <el-color-picker class="legend-color-picker" :model-value="seriesColors[cells.findIndex(item => item.no === cell.no)]" :show-alpha="false" popper-class="console-color-popper" :aria-label="`修改${cell.no}颜色`" @change="updateSeriesColor(cells.findIndex(item => item.no === cell.no), $event)" />
          <span>{{ cell.no }}</span>
          <span>{{ cell.no }}</span><small>{{ selectedRangeLabel(cell.no) }}</small>
        </div>
      </div>
@@ -412,3 +553,15 @@
.report-chart-page{min-width:0}.compare-info-panel,.detail-panel,.metrics-panel{min-width:0;border:1px solid #d8e1e9;background:#fff}.compare-info-panel,.detail-panel{margin-bottom:16px;padding-bottom:18px}.panel-heading{min-height:48px;display:flex;align-items:center;justify-content:space-between;padding:0 24px;border-bottom:1px solid var(--line)}.panel-heading>div{display:flex;align-items:center;gap:9px;color:#26333f;font-size:13px}.panel-heading>div::before{content:'';width:3px;height:16px;background:#2b78a3}.panel-heading strong{color:#26333f;font-size:13px}.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;border-color:#176fcb}.secondary-button{color:#52606d;background:#fff;border-color:var(--line-strong)}.secondary-button:hover{color:var(--blue);border-color:#9dc8f5}.compare-table-wrap,.field-table-wrap{width:100%;max-width:100%;overflow-x:auto;padding:18px 24px 0}.compare-table,.field-table{width:100%;min-width:820px;table-layout:fixed;border-collapse:collapse;color:#52606d;font-size:13px}.compare-table th,.compare-table td,.field-table th,.field-table td{height:42px;padding:0 12px;border:1px solid #cbd8e4;text-align:center;white-space:nowrap}.compare-table th,.field-table th{color:#fff;background:#3c6497;font-size:12px;font-weight:600}.compare-table th:nth-child(1){width:22%}.compare-table th:nth-child(2){width:17%}.compare-table th:nth-child(3){width:18%}.compare-table th:nth-child(4){width:17%}.compare-table th:nth-child(5){width:14%}.compare-table th:last-child{width:12%}.compare-table tbody tr:hover,.field-table tbody tr:hover{background:#f7fbff}.compact-button{margin:16px 24px 0}.fixed-item{color:#8795a3;font-size:12px}.detail-meta{display:grid;grid-template-columns:minmax(280px,520px);gap:22px;padding:18px 24px 0}.detail-meta label{display:grid;grid-template-columns:48px minmax(0,1fr);align-items:center;gap:10px}.detail-meta label>span{color:#52606d;font-size:12px;font-weight:500}.detail-meta :deep(.el-input__wrapper),.field-table :deep(.el-input__wrapper),.compare-select-form :deep(.el-input__wrapper),.compare-select-form :deep(.el-select__wrapper){min-height:34px;border-radius:3px;box-shadow:0 0 0 1px #d8e1e9 inset;background:#f8fafc}.field-table{min-width:900px}.field-table th:first-child,.field-table td:first-child{width:140px;text-align:left}.field-table th:last-child,.field-table td:last-child{width:74px}.field-table .sample-row td,.field-table .detail-meta-row td{background:#fbfdff}.field-table .detail-meta-row td{color:#26333f}.field-table .detail-meta-row td:not(:first-child){font-weight:500}.field-table :deep(.el-input){width:100%}.delete-link{border:0;color:#d15b5b;background:transparent;font-size:12px;cursor:pointer}.delete-link:hover{text-decoration:underline}.field-lock{color:#8795a3}.empty-cell{height:72px!important;color:#8795a3}.detail-actions{display:flex;flex-wrap:wrap;gap:10px;padding:16px 24px 0}.metrics-panel{padding:0 24px 24px}.metric-switches{display:flex;flex-wrap:wrap;align-items:center;gap:20px 28px;min-height:58px;border-bottom:1px solid var(--line)}.metric-switches label{display:inline-flex;align-items:center;gap:7px;color:#52606d;font-size:12px}.metric-switches :deep(.el-switch){--el-switch-on-color:#3978f6}.chart-legend-bar{min-height:52px;display:flex;flex-wrap:wrap;align-items:center;gap:12px 24px;border-bottom:1px solid var(--line)}.chart-legend-bar>strong{color:#26333f;font-size:12px;font-weight:600}.chart-legend-item{display:inline-flex;align-items:center;gap:7px;min-width:0;color:#52606d;font-size:12px}.chart-legend-item span{max-width:260px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.legend-color-picker{width:18px;height:18px;display:inline-flex;align-items:center;justify-content:center}.legend-color-picker :deep(.el-color-picker__trigger){width:14px!important;height:14px!important;padding:0!important;border:0!important;border-radius:2px;background:transparent!important;box-shadow:none!important;overflow:hidden}.legend-color-picker :deep(.el-color-picker__color){width:14px;height:14px;border:0;border-radius:2px;box-shadow:0 0 0 1px rgba(36,54,71,.18)}.legend-color-picker :deep(.el-color-picker__color-inner){border-radius:2px}.legend-color-picker :deep(.el-color-picker__icon),.legend-color-picker :deep(.el-color-picker__empty),.legend-color-picker :deep(.el-color-picker__trigger svg),.legend-color-picker :deep(.el-color-picker__trigger .el-icon){display:none!important}.legend-color-picker :deep(.el-color-picker__trigger:hover){box-shadow:0 0 0 2px rgba(43,120,163,.24)!important}:global(.console-color-popper.el-popper){padding:0!important;border:1px solid #b9c9d8!important;border-radius:4px!important;box-shadow:0 10px 24px rgba(25,48,69,.18)!important;overflow:hidden}:global(.console-color-popper .el-color-picker-panel){border:0!important;box-shadow:none!important}.charts-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px;padding-top:18px}.chart-card{min-width:0;border:1px solid #edf1f5;background:#fff}.chart-card>header{height:38px;display:flex;align-items:center;gap:18px;padding:0 14px;color:#52606d;font-size:11px}.chart-card>header strong{margin-right:auto;color:#26333f;font-size:13px}.metric-chart{width:100%;height:230px}.chart-empty{height:180px;display:flex;align-items:center;justify-content:center;color:#8795a3;font-size:12px}.compare-select-form{display:grid;gap:15px}.compare-select-form label{display:grid;grid-template-columns:84px minmax(0,1fr);align-items:center;gap:12px;color:#52606d;font-size:12px}.template-field-options{display:flex;flex-direction:column;gap:10px;max-height:280px;overflow:auto;padding:14px;border:1px solid #d8e1e9;background:#f8fafc}.template-field-options :deep(.el-checkbox),.template-field-options :deep(.el-radio){height:auto;min-height:34px;margin-right:0}.report-chart-page :deep(.el-dialog){max-width:calc(100vw - 28px);border-radius:4px}.report-chart-page :deep(.el-dialog__header){margin:0;padding:17px 20px;border-bottom:1px solid var(--line)}.report-chart-page :deep(.el-dialog__body){padding:20px}.report-chart-page :deep(.el-dialog__footer){display:flex;justify-content:flex-end;gap:8px;padding:14px 20px;border-top:1px solid var(--line)}@media(max-width:900px){.charts-grid{grid-template-columns:minmax(0,1fr)}}@media(max-width:680px){.panel-heading,.metric-switches,.chart-legend-bar{padding-left:12px;padding-right:12px}.compare-table-wrap,.detail-meta,.field-table-wrap,.detail-actions{padding-left:12px;padding-right:12px}.detail-meta{grid-template-columns:1fr}.metrics-panel{padding-left:12px;padding-right:12px}.chart-legend-bar{align-items:flex-start;flex-direction:column;gap:10px;padding-top:12px;padding-bottom:12px}.metric-switches{gap:14px}.charts-grid{grid-template-columns:1fr}.metric-chart{height:250px}.compare-select-form label{grid-template-columns:1fr}}
.data-select-panel{min-width:0;margin-bottom:16px;padding-bottom:18px;border:1px solid #d8e1e9;background:#fff}.data-select-content{display:grid;grid-template-columns:minmax(260px,360px) minmax(0,1fr);align-items:start;gap:28px;padding:18px 24px 0}.step-select,.battery-select{display:grid;gap:10px;color:#52606d;font-size:12px}.step-select{grid-template-columns:76px minmax(0,1fr);align-items:center}.battery-select{grid-template-columns:52px minmax(0,1fr);align-items:start}.battery-select>span{padding-top:7px}.step-select :deep(.el-select__wrapper){min-height:34px;border-radius:3px;box-shadow:0 0 0 1px #d8e1e9 inset;background:#f8fafc}.battery-select :deep(.el-checkbox-group){display:flex;flex-wrap:wrap;align-items:center;gap:8px 22px;min-height:34px}.battery-select :deep(.el-checkbox){margin-right:0}@media(max-width:900px){.data-select-content{grid-template-columns:1fr}}@media(max-width:680px){.data-select-content{gap:14px;padding:14px 12px 0}.step-select,.battery-select{grid-template-columns:1fr}.battery-select>span{padding-top:0}}
</style>
<style scoped>
.chart-legend-item small{max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#8795a3;font-size:11px}
.battery-ranges{grid-column:1 / -1;display:grid;gap:8px;padding-top:2px;border-top:1px solid var(--line)}
.battery-range-row{display:flex;flex-wrap:wrap;align-items:center;gap:8px 12px;min-height:38px;color:#52606d;font-size:12px}
.battery-range-row strong{min-width:72px;color:#26333f}
.range-status{color:#2b78a3}.range-status.missing{color:#c27b2c}.range-status.override{color:#3978f6}
.range-label{color:#8795a3;min-width:180px;max-width:360px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.battery-range-row .compact-button{margin:0;height:30px;padding:0 10px}
.range-select{width:150px}.range-select :deep(.el-select__wrapper){min-height:30px}
@media(max-width:680px){.battery-ranges{padding-top:10px}.battery-range-row{align-items:flex-start}.range-label{width:100%;max-width:none}.range-select{width:100%}}
</style>
src/views/TestConsole.vue
@@ -404,7 +404,7 @@
    </div>
    </div>
    <ConsoleChartPanel v-if="activeTab === 'step-chart'" :test-id="testId" :cells="cells" :records="realtimeRecords" :logic-step-mappings="stepSettings" :console-detail="backendDetail?.consoleDetail" :test-name="profile.name" :battery-group="profile.batteryGroup" />
    <ConsoleChartPanel v-if="activeTab === 'step-chart'" :test-id="testId" :cells="cells" :records="realtimeRecords" :step-plan-rows="detectorPlanRows" :logic-step-mappings="stepSettings" :console-detail="backendDetail?.consoleDetail" :test-name="profile.name" :battery-group="profile.batteryGroup" />
    <ConsoleProjectChartPanel v-if="activeTab === 'project-chart'" :cells="cells" />
<ConsoleDataListPanel v-if="activeTab === 'data-list' && backendDetail" :test-id="testId" :test-name="profile.name" :cells="cells" :can-import="state === '检测完成'" :reception-status="backendDetail.receptionStatus" :bindings="backendDetail.batteries.map(item => ({ batteryNo: item.batteryNo, bindingId: item.binding?.bindingId, channelId: item.binding?.channelId, devid: item.binding?.devid, chlid: item.binding?.chlid }))" />