From bae879a08fad30d45604bc4f72a9eea77080e9c3 Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期二, 01 九月 2026 13:19:17 +0800
Subject: [PATCH] 恢复实时页检测柜工步方案
---
src/views/TestConsole.vue | 109 ++++++++++++++++--------------------------------------
1 files changed, 32 insertions(+), 77 deletions(-)
diff --git a/src/views/TestConsole.vue b/src/views/TestConsole.vue
index 8ac9ef7..b981f81 100644
--- a/src/views/TestConsole.vue
+++ b/src/views/TestConsole.vue
@@ -6,7 +6,7 @@
import ConsoleProjectChartPanel from '@/components/ConsoleProjectChartPanel.vue'
import ConsoleDataListPanel from '@/components/ConsoleDataListPanel.vue'
import { useTests } from '@/stores/tests'
-import { endTestReception, getBtsRecords, getTestDetail, getTestLiveState, openTestLiveStream, pauseTestReception, resumeTestReception, saveLogicStepMappings, startTestReception, type BtsRecord, type TestConfigurationDetail, type TestLiveState, type TestLogicStepMapping } from '@/api/tests'
+import { endTestReception, getTestDetail, getTestLiveState, openTestLiveStream, pauseTestReception, resumeTestReception, saveLogicStepMappings, startTestReception, type TestConfigurationDetail, type TestLiveState, type TestLogicStepMapping } from '@/api/tests'
type ConsoleState = '未开始接收' | '检测中' | '暂停' | '已结束'
type ReceptionState = '未开始接收' | '接收中' | '暂停接收' | '已结束接收'
@@ -23,7 +23,6 @@
const testStatusLabel = ref('待测试')
let closeLiveStream: (() => void) | null = null
let reconnectTimer: number | null = null
-let stepRefreshTimer: number | null = null
const runtimeAttach = ref(false)
const receptionState = ref<ReceptionState>('未开始接收')
const testProfiles: Record<number, { name: string; owner: string; purpose: string; startedAt: string; batteryGroup: string; batteryCount: number; abnormal: boolean }> = {
@@ -53,10 +52,6 @@
const stepDialogVisible = ref(false)
const stepSettings = ref<TestLogicStepMapping[]>([])
const stepDraft = ref<TestLogicStepMapping[]>([])
-interface ScopedBtsRecord extends BtsRecord { receptionIntervalId: number }
-interface TestStepRow { batteryNo: string; deviceId: number; channelId: number; cycleNo: number; stepNo: number; stepName: string; durationSeconds: number; capacityAh: number; energyWh: number; startVoltageV: number; endVoltageV: number; chargePeakVoltageV: number | null; dischargeValleyVoltageV: number | null }
-const stepRows = ref<TestStepRow[]>([])
-const stepLoadError = ref('')
const activeTab = ref<ConsoleTab>(route.query.tab === 'data-list' ? 'data-list' : 'realtime')
const consoleTabs: Array<{ key: ConsoleTab; label: string }> = [
{ key: 'realtime', label: '实时数据' },
@@ -119,8 +114,6 @@
const detail = await getTestDetail(testId.value)
backendDetail.value = detail
stepSettings.value = detail.logicStepMappings || []
- await loadStepRows(detail)
- stepRefreshTimer = window.setInterval(() => { if (backendDetail.value) void loadStepRows(backendDetail.value) }, 5000)
runtimeAttach.value = detail.runtimeAttach === true
if (detail.receptionStatus === 'NOT_STARTED') {
state.value = '未开始接收'
@@ -144,7 +137,7 @@
ElMessage.error(error instanceof Error ? error.message : '测试状态加载失败')
}
})
-onBeforeUnmount(() => { closeLiveStream?.(); if (reconnectTimer !== null) window.clearTimeout(reconnectTimer); if (stepRefreshTimer !== null) window.clearInterval(stepRefreshTimer) })
+onBeforeUnmount(() => { closeLiveStream?.(); if (reconnectTimer !== null) window.clearTimeout(reconnectTimer) })
const elapsedText = computed(() => {
const hours = Math.floor(elapsedSeconds.value / 3600)
@@ -189,72 +182,6 @@
const currentExceptions = computed(() => (liveState.value?.exceptions || [])
.filter(item => item.sourceType !== 'COLLECTOR' || !item.status || item.status === 'OPEN'))
-async function loadStepRows(detail: TestConfigurationDetail) {
- try {
- const batches = await Promise.all((detail.batteries || []).map(async battery => {
- const binding = battery.binding
- if (binding?.bindingId == null || !Number.isFinite(binding.devid) || !Number.isFinite(binding.chlid)) return []
- const deviceId = Number(binding.devid); const channelId = Number(binding.chlid)
- const intervals = (detail.receptionIntervals || []).filter(item => item.bindingId === binding.bindingId && item.sourceType === 'CABINET_CHANNEL')
- // 接收时段是测试归属边界;暂停空档和同通道其他运行记录不进入当前测试工步。
- const records = (await getBtsRecords(deviceId, channelId)).flatMap(record => {
- const time = Date.parse(record.deviceTime)
- const interval = intervals.find(item => time >= Date.parse(item.startBoundaryAtUtc) && (!item.endBoundaryAtUtc || time <= Date.parse(item.endBoundaryAtUtc)))
- return interval ? [{ ...record, receptionIntervalId: interval.intervalId }] : []
- })
- return summarizeSteps(records, battery.batteryNo, deviceId, channelId)
- }))
- stepRows.value = batches.flat()
- stepLoadError.value = ''
- } catch (error) {
- stepLoadError.value = error instanceof Error ? error.message : '工步数据加载失败'
- }
-}
-
-function summarizeSteps(records: ScopedBtsRecord[], batteryNo: string, deviceId: number, channelId: number): TestStepRow[] {
- const groups = new Map<string, ScopedBtsRecord[]>()
- records.forEach(record => { const key = `${record.cycleNo}:${record.stepNo}`; const group = groups.get(key) || []; group.push(record); groups.set(key, group) })
- return [...groups.values()].map(group => {
- group.sort((a, b) => Date.parse(a.deviceTime) - Date.parse(b.deviceTime) || a.sequenceId - b.sequenceId)
- const first = group[0]; const last = group[group.length - 1]
- const charge = group.filter(item => item.currentA >= 0).map(item => item.voltageV)
- const discharge = group.filter(item => item.currentA < 0).map(item => item.voltageV)
- const durationSeconds = [...new Set(group.map(item => item.receptionIntervalId))].reduce((total, intervalId) => {
- const scoped = group.filter(item => item.receptionIntervalId === intervalId)
- return total + Math.max(0, (Date.parse(scoped[scoped.length - 1].deviceTime) - Date.parse(scoped[0].deviceTime)) / 1000)
- }, 0)
- return { batteryNo, deviceId, channelId, cycleNo: first.cycleNo, stepNo: first.stepNo, stepName: first.stepName,
- durationSeconds,
- capacityAh: last.capacityAh, energyWh: last.energyWh, startVoltageV: first.voltageV, endVoltageV: last.voltageV,
- chargePeakVoltageV: charge.length ? Math.max(...charge) : null,
- dischargeValleyVoltageV: discharge.length ? Math.min(...discharge) : null }
- })
-}
-
-function formatDuration(seconds: number) {
- const millis = Math.round(Number(seconds || 0) * 1000)
- const h = Math.floor(millis / 3600000); const m = Math.floor((millis % 3600000) / 60000); const s = Math.floor((millis % 60000) / 1000); const ms = millis % 1000
- return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}.${String(ms).padStart(3, '0')}`
-}
-function formatVoltage(value: number | null) { return value === null ? '-' : Number(value).toFixed(4) }
-function mappingFor(stepNo: number) { return stepSettings.value.find(item => stepNo >= item.startStepNo && stepNo <= item.endStepNo) }
-const mappedStepRows = computed(() => {
- const groups = new Map<string, (typeof stepRows.value[number] & { displayNo: number; displayName: string })[]>()
- stepRows.value.forEach(row => { const mapping = mappingFor(row.stepNo); const displayNo = mapping?.logicalNo || row.stepNo; const displayName = mapping?.logicalName || row.stepName; const key = `${row.batteryNo}:${row.cycleNo}:${displayNo}`; const group = groups.get(key) || []; group.push({ ...row, displayNo, displayName }); groups.set(key, group) })
- return [...groups.values()].map(group => {
- group.sort((a, b) => a.stepNo - b.stepNo)
- const first = group[0]; const last = group[group.length - 1]
- const chargePeaks = group.map(item => item.chargePeakVoltageV).filter((value): value is number => value !== null)
- const dischargeValleys = group.map(item => item.dischargeValleyVoltageV).filter((value): value is number => value !== null)
- return { ...last, displayNo: first.displayNo, displayName: first.displayName,
- actualStepLabel: first.stepNo === last.stepNo ? `${first.stepNo} · ${first.stepName}` : `${first.stepNo}-${last.stepNo}`,
- durationSeconds: group.reduce((sum, item) => sum + Number(item.durationSeconds || 0), 0),
- startVoltageV: first.startVoltageV, endVoltageV: last.endVoltageV,
- chargePeakVoltageV: chargePeaks.length ? Math.max(...chargePeaks) : null,
- dischargeValleyVoltageV: dischargeValleys.length ? Math.min(...dischargeValleys) : null }
- })
-})
-const visibleStepRows = computed(() => [...mappedStepRows.value].sort((a, b) => a.batteryNo.localeCompare(b.batteryNo) || a.cycleNo - b.cycleNo || a.displayNo - b.displayNo))
function openStepSettings() { stepDraft.value = stepSettings.value.map(item => ({ ...item })) ; stepDialogVisible.value = true }
function addStepMapping() { const next = stepDraft.value.length + 1; const start = stepDraft.value.length ? Math.max(...stepDraft.value.map(item => item.endStepNo)) + 1 : 1; stepDraft.value.push({ logicalNo: next, logicalName: `逻辑工步 ${next}`, startStepNo: start, endStepNo: start }) }
function removeStepMapping(index: number) { stepDraft.value.splice(index, 1); stepDraft.value.forEach((item, i) => { item.logicalNo = i + 1 }) }
@@ -262,6 +189,34 @@
try { const detail = await saveLogicStepMappings(testId.value, stepDraft.value); stepSettings.value = detail.logicStepMappings || []; backendDetail.value = detail; stepDialogVisible.value = false; ElMessage.success('工步合并规则已保存') }
catch (error) { ElMessage.error(error instanceof Error ? error.message : '工步设置保存失败') }
}
+
+interface PlanDisplayStep { no: number; name: string; time: string; voltage: string; state: '进行中' | '已完成' | '待执行' }
+function formatDuration(seconds: number) {
+ const millis = Math.round(Number(seconds || 0) * 1000)
+ const h = Math.floor(millis / 3600000); const m = Math.floor((millis % 3600000) / 60000); const s = Math.floor((millis % 60000) / 1000); const ms = millis % 1000
+ return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}.${String(ms).padStart(3, '0')}`
+}
+function snapshotNumber(key: string) {
+ const snapshots = liveState.value?.sources.map(source => source.latestSnapshot as Record<string, unknown>) || []
+ const values = snapshots.map(snapshot => snapshot[key]).filter(value => Number.isFinite(Number(value))).map(Number)
+ return values.length ? Math.max(...values) : undefined
+}
+const currentCycleNo = computed(() => liveState.value?.testPlan?.currentCycleNo
+ || snapshotNumber('cycleNo') || snapshotNumber('cycle') || 0)
+const currentStepNo = computed(() => liveState.value?.testPlan?.currentStepNo
+ || snapshotNumber('stepNo') || snapshotNumber('step') || 0)
+const plannedCycleCount = computed(() => liveState.value?.testPlan?.plannedCycleCount || 0)
+const planRows = computed<PlanDisplayStep[]>(() => {
+ const detectorRows = liveState.value?.testPlan?.rows
+ if (detectorRows?.length) return detectorRows.map(row => ({
+ no: row.stepNo,
+ name: row.stepName,
+ time: row.rowType === 'CYCLE' ? `起始工步 ${row.stepNo || '-'}` : row.rowType === 'END' ? '-' : formatDuration(row.durationSeconds),
+ voltage: row.rowType === 'CYCLE' ? `循环次数 ${row.cycleCount || '-'}` : row.voltageV == null ? '-' : `${Number(row.voltageV).toFixed(4)} V`,
+ state: (row.executionStatus || '待执行') as PlanDisplayStep['state'],
+ }))
+ return []
+})
const parameters = [
['测试名称', profile.value.name], ['创建者', '陈师傅'], ['负责人', profile.value.owner], ['测试项目', profile.value.purpose],
@@ -356,7 +311,7 @@
<div class="console-layout">
<main>
- <section class="panel step-panel"><header class="panel-heading"><div><i></i><strong>工步数据</strong><span>STEP SUMMARY</span></div><button class="primary-button compact-button" type="button" @click="openStepSettings"><el-icon><Setting /></el-icon>工步设置</button></header><div class="project-meta"><span>测试项目 <strong>{{ profile.purpose }}</strong></span><span>按当前测试规则映射,覆盖全部循环</span></div><div class="step-table-wrap"><table><thead><tr><th>电池号</th><th>循环</th><th>逻辑工步</th><th>实际工步</th><th>工步时间</th><th>容量(Ah)</th><th>能量(Wh)</th><th>起始电压(V)</th><th>终止电压(V)</th><th>充电峰值(V)</th><th>放电谷值(V)</th></tr></thead><tbody><tr v-for="row in visibleStepRows" :key="`${row.deviceId}:${row.channelId}:${row.cycleNo}:${row.stepNo}`"><td><strong>{{ row.batteryNo }}</strong></td><td>{{ row.cycleNo }}</td><td>{{ row.displayNo }} · {{ row.displayName }}</td><td>{{ row.actualStepLabel }}</td><td>{{ formatDuration(row.durationSeconds) }}</td><td>{{ Number(row.capacityAh).toFixed(4) }}</td><td>{{ Number(row.energyWh).toFixed(4) }}</td><td>{{ Number(row.startVoltageV).toFixed(4) }}</td><td>{{ Number(row.endVoltageV).toFixed(4) }}</td><td>{{ formatVoltage(row.chargePeakVoltageV) }}</td><td>{{ formatVoltage(row.dischargeValleyVoltageV) }}</td></tr><tr v-if="!visibleStepRows.length"><td colspan="11" class="empty-cell">{{ stepLoadError || '暂无当前测试接收时段内的检测柜实际工步' }}</td></tr></tbody></table></div></section>
+ <section class="panel step-panel"><header class="panel-heading"><div><i></i><strong>项目参数</strong><span>STEP PARAMETERS</span></div><button class="primary-button compact-button" type="button" @click="openStepSettings"><el-icon><Setting /></el-icon>工步设置</button></header><div class="project-meta"><span>测试项目 <strong>{{ profile.purpose }}</strong></span><span>当前循环 <strong>{{ currentCycleNo || '-' }} / 计划 {{ plannedCycleCount || '-' }}</strong></span><span>执行状态 <strong>{{ testStatusLabel }}</strong></span></div><div class="step-table-wrap"><table><thead><tr><th>工步号</th><th>工步名称</th><th>时间(h:min:s.ms)</th><th>电压(V)</th><th>执行状态</th></tr></thead><tbody><tr v-for="(step, index) in planRows" :key="`${index}-${step.no}-${step.name}`" :class="{ active: step.state === '进行中' }"><td>{{ step.no }}</td><td><strong>{{ step.name }}</strong></td><td>{{ step.time }}</td><td>{{ step.voltage }}</td><td><span class="step-state" :class="step.state">{{ step.state }}</span></td></tr><tr v-if="!planRows.length"><td colspan="5" class="empty-cell">暂无后端检测柜工步方案</td></tr></tbody></table></div></section>
<section class="panel alarm-panel"><header class="panel-heading"><div><i></i><strong>异常警告</strong><span>ALARM</span></div></header><div v-if="currentExceptions.length" class="alarm-list"><div v-for="(item, index) in currentExceptions" :key="`${item.sourceId || item.sourceType}-${item.channelCode || index}-${index}`"><el-icon><WarningFilled /></el-icon><span><strong>{{ item.sourceType === 'COLLECTOR' ? `采集器${item.collectorName || item.collectorCode || `#${item.sourceId}`}${item.status === 'OPEN' ? '离线' : '断线已恢复'} · ${item.channelCode || '来源'}` : (item.message || '数据来源当前不可观测') }}</strong><small>{{ item.measurementName ? `${item.measurementName}受影响 · ` : '' }}{{ item.status === 'OPEN' ? '缺失区间开放' : (item.message || '当前异常') }} · {{ item.missingFrom || item.openedAt || '-' }}<template v-if="item.missingTo"> 至 {{ item.missingTo }}</template></small></span></div></div><div v-else class="alarm-empty"><el-icon><CircleCheckFilled /></el-icon>暂无异常</div></section>
<section class="panel channel-panel"><header class="panel-heading"><div><i></i><strong>通道</strong><span>CHANNEL STATUS</span></div></header><div class="channel-table-wrap"><table><thead><tr><th>电池号</th><th>测试柜</th><th>柜状态</th><th>检测通道</th><th>采集器</th><th>采集器状态</th><th>温度</th><th>正电位</th><th>负电位</th></tr></thead><tbody><tr v-for="item in channels" :key="item.no"><td><strong>{{ item.no }}</strong></td><td>{{ item.cabinet }}</td><td><span class="device-state" :class="item.cabinetState">{{ item.cabinetState }}</span></td><td>{{ item.channel }}</td><td>{{ item.collector }}</td><td><span class="device-state" :class="{ 在线: item.collectorState === '在线', 异常: item.collectorState !== '在线' }">{{ item.collectorState }}</span></td><td>{{ item.temperature }}</td><td>{{ item.positive }}</td><td>{{ item.negative }}</td></tr></tbody></table></div></section>
</main>
@@ -367,7 +322,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 === '已结束'" :bindings="backendDetail.batteries.map(item => ({ batteryNo: item.batteryNo, devid: item.binding?.devid, chlid: item.binding?.chlid }))" />
+<ConsoleDataListPanel v-if="activeTab === 'data-list' && backendDetail" :test-id="testId" :test-name="profile.name" :cells="cells" :can-import="state === '已结束'" :bindings="backendDetail.batteries.map(item => ({ batteryNo: item.batteryNo, devid: item.binding?.devid, chlid: item.binding?.chlid }))" :logic-step-mappings="backendDetail.logicStepMappings" />
<el-dialog v-model="stepDialogVisible" title="工步设置" width="760px" align-center><div class="step-dialog-content"><p>将连续实际工步合并为逻辑工步;循环和结束统计行不参与,原始检测柜数据保持不变。</p><div class="mapping-list"><div v-for="(item, index) in stepDraft" :key="index" class="mapping-row"><strong>逻辑工步 {{ item.logicalNo }}</strong><el-input v-model="item.logicalName" placeholder="逻辑工步名称" /><el-input-number v-model="item.startStepNo" :min="1" controls-position="right" /><span>至</span><el-input-number v-model="item.endStepNo" :min="1" controls-position="right" /><button class="secondary-button" type="button" aria-label="取消合并" @click="removeStepMapping(index)"><el-icon><Delete /></el-icon></button></div></div><button class="secondary-button" type="button" @click="addStepMapping"><el-icon><Plus /></el-icon>新增合并组</button></div><template #footer><button class="secondary-button" type="button" @click="stepDialogVisible=false">取消</button><button class="primary-button" type="button" @click="saveSteps"><el-icon><Check /></el-icon>保存规则</button></template></el-dialog>
</div>
--
Gitblit v1.7.1