yeg
2026-09-03 4f9b7140c10ce7c18e541de012ffadd80cf535a8
修正控制台设备状态与预警展示
1个文件已添加
1个文件已修改
53 ■■■■■ 已修改文件
src/views/TestConsole.vue 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tests/test-console-alarm-and-status.contract.test.mjs 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/TestConsole.vue
@@ -31,6 +31,30 @@
function formatDateTime(value?: string | null) {
  return value ? value.replace('T', ' ').replace(/Z$/, '').replace(/\.\d{3,6}$/, '') : '-'
}
// 控制台的柜状态和采集器状态只表达连接事实,设备工作状态由设备事实另行保留。
function connectionStatusLabel(status?: string) {
  return status === 'ONLINE' ? '在线' : '离线'
}
function exceptionTitle(item: Record<string, unknown>) {
  if (item.exceptionId !== undefined && item.exceptionId !== null) {
    const source = item.sourceType === 'COLLECTOR' ? '采集器' : '检测柜'
    const field = String(item.metricCode || item.measurementName || '字段')
    return `${source} · ${field}超限`
  }
  if (item.sourceType === 'COLLECTOR') {
    const name = item.collectorName || item.collectorCode || `#${item.sourceId || '-'}`
    return `采集器${name}${item.status === 'OPEN' ? '离线' : '异常已处理'} · ${item.channelCode || '来源'}`
  }
  return String(item.message || '数据来源当前不可观测')
}
function exceptionDetail(item: Record<string, unknown>) {
  if (item.exceptionId !== undefined && item.exceptionId !== null)
    return `异常值:${item.value ?? '-'} ${item.unit || ''} · 测试预警异常`
  return String(item.message || (item.status === 'OPEN' ? '当前异常' : '异常已处理'))
}
const profile = computed(() => {
  const detail = backendDetail.value
  const batteries = detail?.batteries || []
@@ -227,8 +251,8 @@
    observedAt: source?.latestSnapshot?.observedAt,
    collectorObservedAt: collectorSource?.latestSnapshot?.observedAt,
    collectorUnit: collectorSource?.latestSnapshot?.units?.join('/') || '-',
    collectorStatus: ({ ONLINE: '在线', OFFLINE: '离线', UNKNOWN: '未知', WORKING: '运行', PAUSED: '暂停', STOPPED: '结束', FINISHED: '结束' } as Record<string, string>)[collectorSource?.deviceStatus || 'UNKNOWN'] || '异常',
    deviceStatus: ({ ONLINE: '在线', OFFLINE: '离线', UNKNOWN: '未知', WORKING: '运行', PAUSED: '暂停', STOPPED: '结束', FINISHED: '结束' } as Record<string, string>)[source?.deviceStatus || 'UNKNOWN'] || '异常',
    collectorStatus: connectionStatusLabel(collectorSource?.deviceStatus),
    deviceStatus: connectionStatusLabel(source?.deviceStatus),
  }
}))
@@ -378,7 +402,6 @@
  const cabinet = sources.find(item => item.sourceType === 'CABINET_CHANNEL')
  const collector = sources.find(item => item.sourceType === 'COLLECTOR')
  const battery = backendDetail.value?.batteries?.find(item => item.batteryNo === cell.no)
  const label = (status?: string) => ({ ONLINE: '在线', OFFLINE: '离线', UNKNOWN: '未知', WORKING: '运行', PAUSED: '暂停', STOPPED: '结束', FINISHED: '结束' } as Record<string, string>)[status || 'UNKNOWN'] || '异常'
  const measurements: Record<string, string> = {}
  if (receptionState.value === '接收中') {
    const sourceMeasurements = collector?.latestSnapshot?.measurements || []
@@ -390,8 +413,8 @@
      if (measurement?.value !== undefined && measurement?.value !== null) measurements[measurementCode] = String(measurement.value)
    }
  }
  return { no: cell.no, cabinet: cell.cabinet, cabinetState: label(cabinet?.deviceStatus), channel: cell.channel,
    collector: battery?.collectorName || '-', collectorState: label(collector?.deviceStatus), measurements }
  return { no: cell.no, cabinet: cell.cabinet, cabinetState: connectionStatusLabel(cabinet?.deviceStatus), channel: cell.channel,
    collector: battery?.collectorName || '-', collectorState: connectionStatusLabel(collector?.deviceStatus), measurements }
}))
async function togglePause() {
@@ -473,8 +496,8 @@
    <div class="console-layout">
      <main>
    <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="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.message || (item.status === 'OPEN' ? '当前异常' : '异常已处理') }}</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 :style="{ minWidth: `${1120 + collectorColumns.length * 120}px` }"><thead><tr><th>电池号</th><th>测试柜</th><th>柜状态</th><th>检测通道</th><th>采集器</th><th>采集器状态</th><th v-for="column in collectorColumns" :key="column.key">{{ column.label }}</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 v-for="column in collectorColumns" :key="column.key">{{ item.measurements[column.measurementCode] || '-' }}</td></tr><tr v-if="!channels.length"><td :colspan="6 + collectorColumns.length" 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.exceptionId || item.sourceId || item.sourceType}-${item.channelCode || index}-${index}`"><el-icon><WarningFilled /></el-icon><span><strong>{{ exceptionTitle(item) }}</strong><small>{{ item.measurementName ? `${item.measurementName}受影响 · ` : '' }}{{ exceptionDetail(item) }}</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 :style="{ minWidth: `${1120 + collectorColumns.length * 120}px` }"><thead><tr><th>电池号</th><th>测试柜</th><th>柜状态</th><th>检测通道</th><th>采集器</th><th>采集器状态</th><th v-for="column in collectorColumns" :key="column.key">{{ column.label }}</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 }}</span></td><td v-for="column in collectorColumns" :key="column.key">{{ item.measurements[column.measurementCode] || '-' }}</td></tr><tr v-if="!channels.length"><td :colspan="6 + collectorColumns.length" class="empty-cell">暂无通道配置</td></tr></tbody></table></div></section>
      </main>
      <aside class="parameter-panel"><header class="panel-heading"><div><i></i><strong>测试参数</strong><span>TEST SETTINGS</span></div></header><dl><div v-for="item in parameters" :key="item[0]"><dt>{{ item[0] }}</dt><dd>{{ item[1] }}</dd></div></dl></aside>
    </div>
tests/test-console-alarm-and-status.contract.test.mjs
New file
@@ -0,0 +1,16 @@
import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
const view = await readFile(new URL('../src/views/TestConsole.vue', import.meta.url), 'utf8')
assert.match(view, /function connectionStatusLabel\(status\?: string\)/)
assert.match(view, /status === 'ONLINE' \? '在线' : '离线'/)
assert.doesNotMatch(view, /STOPPED: '结束'/)
assert.doesNotMatch(view, /FINISHED: '结束'/)
assert.match(view, /function exceptionTitle\(item: Record<string, unknown>\)/)
assert.match(view, /item\.exceptionId !== undefined && item\.exceptionId !== null/)
assert.match(view, /item\.sourceType === 'COLLECTOR' \? '采集器' : '检测柜'/)
assert.doesNotMatch(view, /`#\$\{item\.sourceId\}`/)
assert.doesNotMatch(view, /item\.sourceType === 'COLLECTOR' \? `采集器\$\{item\.collectorName/)
console.log('控制台在线状态与测试预警异常契约通过')