yeg
2026-09-02 78f234d1d36b56d1bae1d96e7579b18a67f4a79d
src/views/TestConsole.vue
@@ -49,9 +49,10 @@
const initialStatus = currentTest.value?.status
const state = ref<ConsoleState>(initialStatus === '检测完成' || initialStatus === '完成确认中' || initialStatus === '已终止' || route.query.state === 'ended' ? '已结束' : initialStatus === '暂停' ? '暂停' : initialStatus === '待测试' ? '未开始接收' : '检测中')
const elapsedSeconds = ref(42 * 60 + 25)
const stepDialogVisible = ref(false)
const stepSettings = ref<TestLogicStepMapping[]>([])
const stepDraft = ref<TestLogicStepMapping[]>([])
const selectedPlanSteps = ref<number[]>([])
const stepEditing = ref(false)
const activeTab = ref<ConsoleTab>(route.query.tab === 'data-list' ? 'data-list' : 'realtime')
const consoleTabs: Array<{ key: ConsoleTab; label: string }> = [
  { key: 'realtime', label: '实时数据' },
@@ -182,13 +183,16 @@
const currentExceptions = computed(() => (liveState.value?.exceptions || [])
  .filter(item => item.sourceType !== 'COLLECTOR' || !item.status || item.status === 'OPEN'))
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 }) }
function openStepSettings() { stepDraft.value = stepSettings.value.map(item => ({ ...item })); selectedPlanSteps.value = []; stepEditing.value = true }
function togglePlanStep(position: number, checked: string | number | boolean) { const next = new Set(selectedPlanSteps.value); checked ? next.add(position) : next.delete(position); selectedPlanSteps.value = [...next].sort((a, b) => a - b) }
async function addStepMapping() { const positions = [...selectedPlanSteps.value].sort((a, b) => a - b); if (positions.length < 2) { ElMessage.warning('请先勾选至少两个连续工步'); return }; const contiguous = positions.every((value, index) => index === 0 || value === positions[index - 1] + 1); if (!contiguous) { ElMessage.warning('只能合并连续工步'); return }; if (stepDraft.value.some(item => positions[0] <= item.endStepNo && positions[positions.length - 1] >= item.startStepNo)) { ElMessage.warning('选中的工步已在其他合并组中'); return }; try { const result = await ElMessageBox.prompt('请输入逻辑工步名称', '合并工步', { inputPlaceholder: '例如:充电阶段', inputValidator: value => value.trim() ? true : '逻辑工步名称不能为空' }); const next = stepDraft.value.length + 1; stepDraft.value.push({ logicalNo: next, logicalName: result.value.trim(), startStepNo: positions[0], endStepNo: positions[positions.length - 1] }); selectedPlanSteps.value = [] } catch { /* 用户取消命名 */ } }
function normalizeStepDraft() { stepDraft.value.sort((a, b) => a.startStepNo - b.startStepNo); stepDraft.value.forEach((item, i) => { item.logicalNo = i + 1 }) }
function removeStepMapping(index: number) { stepDraft.value.splice(index, 1); normalizeStepDraft() }
async function saveSteps() {
  try { const detail = await saveLogicStepMappings(testId.value, stepDraft.value); stepSettings.value = detail.logicStepMappings || []; backendDetail.value = detail; stepDialogVisible.value = false; ElMessage.success('工步合并规则已保存') }
  try { normalizeStepDraft(); const detail = await saveLogicStepMappings(testId.value, stepDraft.value); stepSettings.value = detail.logicStepMappings || []; backendDetail.value = detail; stepEditing.value = false; selectedPlanSteps.value = []; ElMessage.success('工步合并规则已保存') }
  catch (error) { ElMessage.error(error instanceof Error ? error.message : '工步设置保存失败') }
}
function cancelStepSettings() { stepEditing.value = false; selectedPlanSteps.value = [] }
interface PlanDisplayStep { no: number; name: string; time: string; voltage: string; state: '进行中' | '已完成' | '待执行' }
function formatDuration(seconds: number) {
@@ -206,16 +210,29 @@
const currentStepNo = computed(() => liveState.value?.testPlan?.currentStepNo
  || snapshotNumber('stepNo') || snapshotNumber('step') || 0)
const plannedCycleCount = computed(() => liveState.value?.testPlan?.plannedCycleCount || 0)
const detectorPlanRows = computed(() => liveState.value?.testPlan?.rows || [])
const actualPlanRows = computed(() => detectorPlanRows.value.filter(row => row.rowType === 'STEP').map((row, index) => ({ ...row, cyclePosition: row.cyclePosition || index + 1 })))
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 physicalSteps = actualPlanRows.value
  const consumed = new Set<string>()
  const displayRows: PlanDisplayStep[] = []
  detectorPlanRows.value.forEach(row => {
    if (row.rowType !== 'STEP') {
      displayRows.push({ no: row.stepNo, name: row.stepName, time: row.rowType === 'CYCLE' ? `起始工步 ${row.stepNo || '-'}` : '-', voltage: row.rowType === 'CYCLE' ? `当前循环 ${row.cycleCount || '-'}` : '-', state: (row.executionStatus || '待执行') as PlanDisplayStep['state'] })
      return
    }
    const physical = physicalSteps.find(item => item.stepNo === row.stepNo && item.stepName === row.stepName) || physicalSteps.find(item => item.stepNo === row.stepNo)
    const position = physical?.cyclePosition || row.stepNo
    const mapping = stepSettings.value.find(item => position >= item.startStepNo && position <= item.endStepNo)
    const key = mapping ? `mapping:${mapping.startStepNo}-${mapping.endStepNo}` : `position:${position}`
    if (consumed.has(key)) return
    const grouped = mapping ? physicalSteps.filter(item => { const itemMapping = stepSettings.value.find(candidate => item.cyclePosition >= candidate.startStepNo && item.cyclePosition <= candidate.endStepNo); return itemMapping?.startStepNo === mapping.startStepNo && itemMapping.endStepNo === mapping.endStepNo }) : [physical || row]
    grouped.forEach(item => consumed.add(String(item.cyclePosition)))
    consumed.add(key)
    const state = grouped.some(item => item.executionStatus === '进行中') ? '进行中' : grouped.every(item => item.executionStatus === '已完成') ? '已完成' : '待执行'
    displayRows.push({ no: mapping?.logicalNo || row.stepNo, name: mapping?.logicalName || row.stepName, time: formatDuration(grouped.reduce((sum, item) => sum + Number(item.durationSeconds || 0), 0)), voltage: grouped[grouped.length - 1].voltageV == null ? '-' : `${Number(grouped[grouped.length - 1].voltageV).toFixed(4)} V`, state })
  })
  return displayRows
})
const parameters = [
@@ -311,7 +328,7 @@
    <div class="console-layout">
      <main>
        <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 step-panel"><header class="panel-heading"><div><i></i><strong>项目参数</strong><span>STEP PARAMETERS</span></div><div class="step-actions"><template v-if="stepEditing"><button class="primary-button compact-button" type="button" @click="addStepMapping"><el-icon><Plus /></el-icon>合并选中</button><button class="primary-button compact-button" type="button" @click="saveSteps"><el-icon><Check /></el-icon>保存</button><button class="secondary-button compact-button" type="button" @click="cancelStepSettings">取消</button></template><button v-else class="primary-button compact-button" type="button" @click="openStepSettings"><el-icon><Setting /></el-icon>工步设置</button></div></header><div class="project-meta"><span>测试项目 <strong>{{ profile.purpose }}</strong></span><span>当前循环 <strong>{{ currentCycleNo || '-' }} / 计划 {{ plannedCycleCount || '-' }}</strong></span><span>当前工步 <strong>{{ currentStepNo || '-' }}</strong></span><span>执行状态 <strong>{{ testStatusLabel }}</strong></span></div><div class="step-table-wrap"><table><thead><tr><th v-if="stepEditing">选择</th><th>工步号</th><th>工步名称</th><th>时间(h:min:s.ms)</th><th>电压(V)</th><th>执行状态</th></tr></thead><tbody><template v-if="stepEditing"><tr v-for="step in actualPlanRows" :key="`edit-${step.cyclePosition}`" :class="{ active: step.executionStatus === '进行中' }"><td><el-checkbox :model-value="selectedPlanSteps.includes(step.cyclePosition)" @change="(checked: string | number | boolean) => togglePlanStep(step.cyclePosition, checked)" /></td><td>{{ step.stepNo }}</td><td><strong>{{ step.stepName }}</strong></td><td>{{ formatDuration(step.durationSeconds) }}</td><td>{{ step.voltageV == null ? '-' : `${Number(step.voltageV).toFixed(4)} V` }}</td><td><span class="step-state" :class="step.executionStatus">{{ step.executionStatus }}</span></td></tr></template><template v-else><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></template><tr v-if="!planRows.length && !actualPlanRows.length"><td :colspan="stepEditing ? 6 : 5" class="empty-cell">暂无后端检测柜工步方案</td></tr></tbody></table></div><div v-if="stepEditing && stepDraft.length" class="mapping-list"><div v-for="(item, index) in stepDraft" :key="index" class="mapping-row"><strong>逻辑工步 {{ item.logicalNo }}</strong><span>第 {{ item.startStepNo }} 至 {{ item.endStepNo }} 个工步</span><button class="secondary-button compact-button" type="button" aria-label="解除合并" @click="removeStepMapping(index)"><el-icon><Delete /></el-icon>解除合并</button></div></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>
@@ -322,9 +339,8 @@
    <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 }))" :logic-step-mappings="backendDetail.logicStepMappings" />
<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 }))" />
    <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>
</template>
@@ -336,5 +352,5 @@
/* 双栏收缩边界:宽表只能在所属面板内横向滚动,不能撑开主网格。 */
.console-tabs{min-height:52px;display:flex;align-items:center;gap:10px;margin-bottom:14px;padding:0 4px;border-bottom:1px solid #d5e0e9}.console-tabs button{height:30px;min-width:88px;padding:0 16px;border:1px solid transparent;border-radius:16px;color:#52606d;background:#f5f7fa;font-size:12px;cursor:pointer;transition:.16s ease}.console-tabs button:hover{color:var(--blue);background:#edf5fd}.console-tabs button.active{color:#1677dd;background:#dff0ff;border-color:#c4e3ff;font-weight:600}.tab-content{min-width:0}.data-list-panel{min-width:0;border:1px solid #d8e1e9;background:#fff}.data-list-panel .panel-heading>small{color:#8795a3;font-size:10px}.data-table-wrap{width:100%;overflow-x:auto;padding:16px 18px 20px}.data-table-wrap table{min-width:1260px}.data-table-wrap td strong{color:#1268dc}.data-voltage{color:#7f35c5;font-weight:600}.data-temperature{color:#bf6100;font-weight:600}.console-layout{grid-template-columns:minmax(0,2.35fr) minmax(300px,1fr);gap:24px;min-width:0}.console-layout>main,.console-layout>.parameter-panel,.console-layout main>.panel{min-width:0;max-width:100%}.step-table-wrap,.channel-table-wrap{width:100%;max-width:100%;min-width:0;overflow-x:auto;overflow-y:hidden}.step-table-wrap table{min-width:720px}.channel-table-wrap table{width:1120px;min-width:1120px;max-width:none}.parameter-panel{position:relative;z-index:1;overflow:hidden}@media(max-width:1280px){.console-layout{grid-template-columns:minmax(0,1fr) 300px;gap:20px}}@media(max-width:1050px){.console-layout{grid-template-columns:minmax(0,1fr);gap:16px}.parameter-panel{grid-row:2}}@media(max-width:600px){.console-tabs{overflow-x:auto}.console-tabs button{flex:0 0 auto}.data-table-wrap{padding:12px}}
.device-state.离线{color:#b34f4f;background:#fff1f1;border-color:#e9bcbc}
.device-state.未知{color:#7b8794;background:#f2f4f6;border-color:#d8dde2}.mapping-list{display:grid;gap:10px;max-height:360px;overflow:auto}.mapping-row{display:grid;grid-template-columns:100px minmax(180px,1fr) 110px 20px 110px 40px;align-items:center;gap:8px}.mapping-row strong{color:#52606d;font-size:12px}.mapping-row :deep(.el-input-number){width:100%}
.device-state.未知{color:#7b8794;background:#f2f4f6;border-color:#d8dde2}.mapping-list{display:grid;gap:10px;max-height:360px;overflow:auto;margin:0 16px 16px;padding-top:12px;border-top:1px solid #edf1f5}.mapping-row{display:grid;grid-template-columns:minmax(0,1fr) auto auto;align-items:center;gap:8px;padding:8px 10px;border:1px solid #d8e1e9;background:#f8fafc}.mapping-row strong{color:#52606d;font-size:12px}.mapping-row span{color:#71808d;font-size:11px}.mapping-row .compact-button{height:28px}.mapping-row :deep(.el-input-number){width:100%}.step-actions{display:flex;align-items:center;gap:8px}
</style>