yeg
2026-09-02 78f234d1d36b56d1bae1d96e7579b18a67f4a79d
移除前端接收区间与补采展示

控制台数据列表直接展示后端持久化的 record、step、cycle 事实,移除接收区间及补采字段。
4个文件已修改
128 ■■■■■ 已修改文件
src/api/tests.ts 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/ConsoleDataListPanel.vue 87 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/TestConsole.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tests/test-ticket23-console-step-regression.test.mjs 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/tests.ts
@@ -79,21 +79,6 @@
    fieldValue: string | null
  }>
  runtimeAttach?: boolean
  backfillStatus?: 'NOT_STARTED' | 'RUNNING' | 'WAITING' | 'COMPLETED' | 'FAILED'
  backfillStartedAtUtc?: string | null
  backfillCompletedAtUtc?: string | null
  receptionIntervals?: Array<{
    intervalId: number
    bindingId: number | null
    sourceType: 'CABINET_CHANNEL' | 'COLLECTOR_CHANNEL' | string
    sourceId: number
    sourceChannelCode: string | null
    actionAtUtc: string
    startBoundaryAtUtc: string
    endBoundaryAtUtc: string | null
    boundaryNote: string | null
    intervalStatus: 'OPEN' | 'CLOSED' | string
  }>
  logicStepMappings: TestLogicStepMapping[]
}
src/components/ConsoleDataListPanel.vue
@@ -4,7 +4,7 @@
import { useRoute, useRouter } from 'vue-router'
import * as XLSX from 'xlsx'
import { Download, EditPen, Refresh, Search, UploadFilled, Clock, Check } from '@element-plus/icons-vue'
import { getPersistedTestCycles, getPersistedTestRecords, getPersistedTestSteps, type PersistedTestCycle, type PersistedTestRecord, type PersistedTestStep, type TestLogicStepMapping, type TestPlanStepPayload } from '@/api/tests'
import { getPersistedTestCycles, getPersistedTestRecords, getPersistedTestSteps, type PersistedTestCycle, type PersistedTestRecord, type PersistedTestStep } from '@/api/tests'
import { getConsoleDataOverrides, submitConsoleDataAction, type ConsoleDataType } from '@/api/consoleData'
type DataTab = 'realtime' | 'step' | 'cycle'
@@ -19,8 +19,6 @@
}
interface DataRow {
  id: string
  sourceIds?: string[]
  cyclePosition?: number
  recordNo: string
  batteryNo: string
  cycleNo: number
@@ -45,8 +43,7 @@
  chargeEnergy: string
  dischargeEnergy: string
}
type ReceptionInterval = { bindingId: number | null; sourceType: string; sourceId: number; startBoundaryAtUtc: string; endBoundaryAtUtc?: string | null }
const props = defineProps<{ testId: number; testName: string; cells: ConsoleCell[]; canImport?: boolean; receptionStatus?: string; bindings?: Array<{ batteryNo: string; bindingId?: number | null; channelId?: number | null; devid?: number | null; chlid?: number | null }>; receptionIntervals?: ReceptionInterval[]; logicStepMappings?: TestLogicStepMapping[]; planSteps?: Array<Pick<TestPlanStepPayload, 'cycleNo' | 'stepNo' | 'sortOrder' | 'stepType'>> }>()
const props = defineProps<{ testId: number; testName: string; cells: ConsoleCell[]; canImport?: boolean; receptionStatus?: string; bindings?: Array<{ batteryNo: string; bindingId?: number | null; channelId?: number | null; devid?: number | null; chlid?: number | null }>}>()
const route = useRoute()
const router = useRouter()
const requestedDataType = String(route.query.dataType || 'REALTIME')
@@ -168,77 +165,8 @@
    loading.value = false
  }
}
function mappingForPosition(position: number) {
  return (props.logicStepMappings || []).find(item => position >= item.startStepNo && position <= item.endStepNo)
}
const plannedStepPositions = computed(() => {
  const byCycleAndStep = new Map<string, number>()
  const byStep = new Map<number, number>()
  const counters = new Map<number, number>()
  const ordered = [...(props.planSteps || [])].filter(item => item.stepType !== 'CYCLE' && item.stepType !== 'END').sort((a, b) => a.sortOrder - b.sortOrder)
  ordered.forEach(item => {
    const position = (counters.get(item.cycleNo) || 0) + 1
    counters.set(item.cycleNo, position)
    byCycleAndStep.set(`${item.cycleNo}:${item.stepNo}`, position)
    if (!byStep.has(item.stepNo)) byStep.set(item.stepNo, position)
  })
  return { byCycleAndStep, byStep }
})
const observedStepPositions = computed(() => {
  const byCycleAndStep = new Map<string, number>()
  const stepsByBatteryAndCycle = new Map<string, number[]>()
  stepRows.value.forEach(row => {
    const cycleKey = `${row.batteryNo}:${row.cycleNo}`
    const steps = stepsByBatteryAndCycle.get(cycleKey) || []
    if (!steps.includes(row.stepNo)) steps.push(row.stepNo)
    stepsByBatteryAndCycle.set(cycleKey, steps)
  })
  stepsByBatteryAndCycle.forEach((steps, cycleKey) => {
    steps.sort((a, b) => a - b).forEach((stepNo, index) => byCycleAndStep.set(`${cycleKey}:${stepNo}`, index + 1))
  })
  return byCycleAndStep
})
function cyclePositionFor(row: DataRow) {
  if (row.cyclePosition) return row.cyclePosition
  const planned = plannedStepPositions.value.byCycleAndStep.get(`${row.cycleNo}:${row.stepNo}`)
  if (planned !== undefined) return planned
  const observed = observedStepPositions.value.get(`${row.batteryNo}:${row.cycleNo}:${row.stepNo}`)
  if (observed !== undefined) return observed
  return plannedStepPositions.value.byStep.get(row.stepNo) || row.stepNo
}
const mappedStepRows = computed(() => {
  const groups = new Map<string, DataRow[]>()
  // 位置来自设备 step 汇总事实,不能根据接收窗口内实际返回的行重新压缩,否则缺失工步会使合并范围漂移。
  stepRows.value.forEach(row => {
    const position = cyclePositionFor(row)
    const mapping = mappingForPosition(position)
    const logicalName = mapping?.logicalName || row.stepName
    const key = `${row.batteryNo}:${row.cycleNo}:${mapping ? `mapping:${mapping.startStepNo}-${mapping.endStepNo}` : `position:${position}`}`
    const group = groups.get(key) || []
    group.push({ ...row, stepName: logicalName })
    groups.set(key, group)
  })
  const aggregated = [...groups.values()]
    .map(group => {
      group.sort((a, b) => a.sequenceId - b.sequenceId)
      const first = group[0]
      const last = group[group.length - 1]
      const numberValues = (field: keyof DataRow) => group.map(item => Number(item[field])).filter(value => Number.isFinite(value))
      const sumValue = (field: keyof DataRow) => numberValues(field).reduce((sum, value) => sum + value, 0)
      const peaks = numberValues('chargePeakVoltage')
      const valleys = numberValues('dischargeValleyVoltage')
      return { ...last, id: `logical-step:${first.batteryNo}:${first.cycleNo}:${first.stepNo}`, sourceIds: group.map(item => item.id), stepName: first.stepName, actualStepLabel: group.length === 1 ? first.actualStepLabel : `${group.map(item => item.actualStepLabel.split(' · ')[0]).join('-')}`, stepDuration: String(group.reduce((sum, item) => sum + Number(item.stepDuration || 0), 0)), capacity: sumValue('capacity').toFixed(4), energy: sumValue('energy').toFixed(4), startVoltage: first.startVoltage, endVoltage: last.endVoltage, chargePeakVoltage: peaks.length ? Math.max(...peaks).toFixed(4) : '-', dischargeValleyVoltage: valleys.length ? Math.min(...valleys).toFixed(4) : '-' }
    })
    .sort((a, b) => a.batteryNo.localeCompare(b.batteryNo) || a.cycleNo - b.cycleNo || (a.cyclePosition || a.stepNo) - (b.cyclePosition || b.stepNo))
  const logicalNos = new Map<string, number>()
  return aggregated.map(row => {
    const key = `${row.batteryNo}:${row.cycleNo}`
    const logicalNo = (logicalNos.get(key) || 0) + 1
    logicalNos.set(key, logicalNo)
    return { ...row, stepNo: logicalNo, recordNo: `STEP-${row.batteryNo}-${row.cycleNo}-${logicalNo}` }
  })
})
const sourceRows = computed(() => (activeTab.value === 'step' ? mappedStepRows.value : activeTab.value === 'cycle' ? cycleRows.value : rows.value))
// 数据列表只展示后端持久化的 record、step、cycle 事实;逻辑工步合并留给统计图表等派生视图。
const sourceRows = computed(() => (activeTab.value === 'step' ? stepRows.value : activeTab.value === 'cycle' ? cycleRows.value : rows.value))
const singleRow = computed(() => Object.values(draftRows.value)[0] || null)
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))
@@ -343,9 +271,9 @@
  }
  try {
    const editItems = changed.flatMap(row => {
      const ids = dataType.value === 'STEP' && row.sourceIds?.length ? row.sourceIds : [row.id]
      const ids = [row.id]
      return ids.map(id => {
        const before = dataType.value === 'STEP' ? stepRows.value.find(item => item.id === id) || row : row
        const before = row
        const after = { ...before, ...Object.fromEntries(fieldOptions.value.map(item => [item.value, row[item.value as keyof DataRow]])) }
        return { identity: id, before, after, override: { stableIdentity: id, fieldValues: JSON.stringify(Object.fromEntries(fieldOptions.value.map(item => [item.value, row[item.value as keyof DataRow]]))), deleted: false } }
      })
@@ -371,8 +299,7 @@
    ElMessage.warning('请先勾选需要删除的数据')
    return
  }
  const displayRows = sourceRows.value.filter(row => ids.includes(row.id))
  const physicalIds = dataType.value === 'STEP' ? displayRows.flatMap(row => (row.sourceIds?.length ? row.sourceIds : [row.id])) : ids
  const physicalIds = ids
  let result: { value: string }
  try {
    result = await ElMessageBox.prompt(`确认物理删除 ${ids.length} 条${activeTab.value}数据?删除后不可恢复。`, '删除确认', { inputPlaceholder: '请输入删除原因', inputValidator: value => (value.trim() ? true : '删除原因不能为空') })
src/views/TestConsole.vue
@@ -339,7 +339,7 @@
    <ConsoleChartPanel v-if="activeTab === 'step-chart'" :cells="cells" :test-name="profile.name" :battery-group="profile.batteryGroup" :duration-minutes="5" />
    <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 }))" :reception-intervals="backendDetail.receptionIntervals" :logic-step-mappings="backendDetail.logicStepMappings" :plan-steps="backendDetail.planSteps" />
<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 }))" />
  </div>
</template>
tests/test-ticket23-console-step-regression.test.mjs
@@ -13,8 +13,7 @@
assert.match(view, /计划 \{\{ plannedCycleCount \|\| '-' \}\}/)
assert.doesNotMatch(view, /<strong>工步数据<\/strong><span>STEP SUMMARY<\/span>/)
assert.doesNotMatch(view, /visibleStepRows/)
assert.match(view, /:logic-step-mappings="backendDetail\.logicStepMappings"/)
assert.match(view, /:plan-steps="backendDetail\.planSteps"/)
assert.match(view, /logicStepMappings/)
assert.match(view, /selectedPlanSteps/)
assert.match(view, /合并选中/)
assert.match(view, /解除合并/)
@@ -23,26 +22,21 @@
assert.match(view, /const grouped = mapping \? physicalSteps\.filter/)
assert.match(view, /const physical = physicalSteps\.find\(item => item\.stepNo === row\.stepNo && item\.stepName === row\.stepName\) \|\| physicalSteps\.find\(item => item\.stepNo === row\.stepNo\)/)
assert.match(view, /displayRows\.push\(\{ no: mapping\?\.logicalNo \|\| row\.stepNo/)
assert.match(dataList, /mappingForPosition\(position: number\)/)
assert.match(dataList, /起始电压\(V\)/)
assert.match(dataList, /放电谷值\(V\)/)
assert.match(dataList, /sourceIds: group\.map\(item => item\.id\)/)
assert.match(dataList, /<th>工步<\/th>/)
assert.doesNotMatch(dataList, /<th>逻辑工步<\/th>/)
assert.doesNotMatch(dataList, /<th>实际工步<\/th>/)
assert.match(dataList, /capacity: sumValue\('capacity'\)\.toFixed\(4\)/)
assert.match(dataList, /energy: sumValue\('energy'\)\.toFixed\(4\)/)
assert.match(dataList, /planSteps\?:/)
assert.match(dataList, /plannedStepPositions/)
assert.match(dataList, /const observedStepPositions = computed\(\(\) => \{/)
assert.match(dataList, /observedStepPositions\.value\.get\(`\$\{row\.batteryNo\}:\$\{row\.cycleNo\}:\$\{row\.stepNo\}`\)/)
assert.match(dataList, /const observed = observedStepPositions\.value\.get/)
assert.match(dataList, /if \(observed !== undefined\) return observed/)
assert.match(dataList, /cyclePositionFor\(row\)/)
assert.match(dataList, /activeTab\.value === 'step' \? mappedStepRows\.value/)
assert.match(dataList, /activeTab\.value === 'step' \? stepRows\.value/)
assert.doesNotMatch(view, /:logic-step-mappings=/)
assert.doesNotMatch(view, /:plan-steps=/)
assert.doesNotMatch(dataList, /mappedStepRows/)
assert.doesNotMatch(dataList, /observedStepPositions/)
assert.doesNotMatch(dataList, /sumValue\('capacity'\)/)
assert.doesNotMatch(dataList, /logical-step:/)
assert.match(dataList, /const filteredRows = computed\(\(\) =>\n  sourceRows\.value\.filter/)
assert.match(dataList, /:total="filteredRows\.length"/)
assert.match(dataList, /receptionIntervals/)
assert.doesNotMatch(dataList, /receptionIntervals/)
assert.match(dataList, /physicalIds/)
assert.match(dataList, /props\.receptionStatus === 'NOT_STARTED'/)
assert.match(liveService, /simulator\.btsTest\(battery\.getBinding\(\)\.getDevid\(\), battery\.getBinding\(\)\.getChlid\(\), 1\)/)