From 8d364c1d7d26580d4b4e5cc9cd334922275f1e63 Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期一, 31 八月 2026 22:01:00 +0800
Subject: [PATCH] 统一测试完成状态展示

---
 src/views/TestConsole.vue                 |   14 +++++---------
 src/views/TestList.vue                    |    2 +-
 src/api/tests.ts                          |   30 +++---------------------------
 tests/test-console-live.contract.test.mjs |    4 ++++
 4 files changed, 13 insertions(+), 37 deletions(-)

diff --git a/src/api/tests.ts b/src/api/tests.ts
index 1a143e3..d129f6b 100644
--- a/src/api/tests.ts
+++ b/src/api/tests.ts
@@ -128,7 +128,8 @@
 
 const statusMap: Record<TestStatusCode, TestStatus> = {
   WAITING: '待测试', RUNNING: '检测中', PAUSED: '暂停', RUNNING_ABNORMAL: '检测中(异常)',
-  COMPLETION_PENDING: '完成确认中', COMPLETED: '检测完成', TERMINATED: '已终止',
+  // 历史兼容状态统一按当前“停止即完成”语义展示。
+  COMPLETION_PENDING: '检测完成', COMPLETED: '检测完成', TERMINATED: '检测完成',
 }
 const receptionStatusMap: Record<ReceptionStatusCode, ReceptionStatus> = {
   NOT_STARTED: '未开始接收', RECEIVING: '接收中', PAUSED: '暂停接收', ENDED: '已结束接收',
@@ -227,37 +228,12 @@
   updatedAt: string
   sources: LiveSource[]
   exceptions: Array<Record<string, unknown>>
-  integrityIssues?: TestIntegrityIssue[]
 }
 
 interface TestLiveStateResponse extends ApiResponse { data: TestLiveState }
 
 export async function getTestLiveState(testId: number): Promise<TestLiveState> {
   return (await request<TestLiveStateResponse>(`/system/test/${testId}/live-state`)).data
-}
-
-export interface TestIntegrityIssue {
-  issueId: number
-  intervalId: number
-  bindingId: number
-  sourceType: string
-  sourceId: number
-  sourceChannelCode?: string | null
-  issueType: string
-  issueStatus: 'OPEN' | 'RESOLVED' | string
-  missingFromSequence?: number | null
-  missingToSequence?: number | null
-  retryCount: number
-  detail?: string | null
-  firstDetectedAtUtc: string
-  lastDetectedAtUtc: string
-  resolvedAtUtc?: string | null
-}
-
-interface TestIntegrityResponse extends ApiResponse { data: TestIntegrityIssue[] }
-
-export async function getTestIntegrityIssues(testId: number): Promise<TestIntegrityIssue[]> {
-  return (await request<TestIntegrityResponse>(`/system/test/${testId}/integrity`)).data
 }
 
 export interface BtsRecord {
@@ -316,7 +292,7 @@
   return getSimulatorChannelData<BtsCycleRecord>(deviceId, channelId, 'cycles')
 }
 
-export type TestLiveEvent = 'snapshot' | 'device-status' | 'reception-status' | 'test-status' | 'exception' | 'integrity' | 'heartbeat'
+export type TestLiveEvent = 'snapshot' | 'device-status' | 'reception-status' | 'test-status' | 'exception' | 'heartbeat'
 
 /** 使用 fetch 流读取鉴权 SSE;调用返回函数关闭连接,断线由页面决定是否重连。 */
 export function openTestLiveStream(testId: number, onEvent: (event: TestLiveEvent, data: unknown) => void,
diff --git a/src/views/TestConsole.vue b/src/views/TestConsole.vue
index 7b2ad97..8da5e69 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, getTestDetail, getTestLiveState, openTestLiveStream, pauseTestReception, resumeTestReception, startTestReception, type TestConfigurationDetail, type TestLiveState, type TestIntegrityIssue } from '@/api/tests'
+import { endTestReception, getTestDetail, getTestLiveState, openTestLiveStream, pauseTestReception, resumeTestReception, startTestReception, type TestConfigurationDetail, type TestLiveState } from '@/api/tests'
 
 type ConsoleState = '未开始接收' | '检测中' | '暂停' | '已结束'
 type ReceptionState = '未开始接收' | '接收中' | '暂停接收' | '已结束接收'
@@ -47,7 +47,7 @@
   }
 })
 const initialStatus = currentTest.value?.status
-const state = ref<ConsoleState>(initialStatus === '检测完成' || route.query.state === 'ended' ? '已结束' : initialStatus === '暂停' ? '暂停' : initialStatus === '待测试' ? '未开始接收' : '检测中')
+const state = ref<ConsoleState>(initialStatus === '检测完成' || initialStatus === '完成确认中' || initialStatus === '已终止' || route.query.state === 'ended' ? '已结束' : initialStatus === '暂停' ? '暂停' : initialStatus === '待测试' ? '未开始接收' : '检测中')
 const elapsedSeconds = ref(42 * 60 + 25)
 const stepDialogVisible = ref(false)
 const activeTab = ref<ConsoleTab>(route.query.tab === 'data-list' ? 'data-list' : 'realtime')
@@ -66,7 +66,7 @@
   liveConnection.value = next.sources.some(source => Boolean(source.latestSnapshot?.observedAt)
     || Boolean(source.latestSnapshot?.measurements?.length)) ? '已连接' : '空数据'
   liveError.value = ''
-  testStatusLabel.value = ({ WAITING: '待测试', RUNNING: '检测中', PAUSED: '暂停', RUNNING_ABNORMAL: '检测中(异常)', COMPLETION_PENDING: '完成确认中', COMPLETED: '检测完成', TERMINATED: '已终止' } as Record<string, string>)[next.testStatus] || '未知'
+  testStatusLabel.value = ({ WAITING: '待测试', RUNNING: '检测中', PAUSED: '暂停', RUNNING_ABNORMAL: '检测中(异常)', COMPLETION_PENDING: '检测完成', COMPLETED: '检测完成', TERMINATED: '检测完成' } as Record<string, string>)[next.testStatus] || '未知'
   const mapped = next.receptionStatus
   if (mapped === 'NOT_STARTED') receptionState.value = '未开始接收'
   else if (mapped === 'PAUSED') receptionState.value = '暂停接收'
@@ -100,8 +100,6 @@
       liveState.value = { ...liveState.value, exceptions: current }
       liveError.value = current.length ? '检测来源存在异常' : ''
     }
-    else if (event === 'integrity' && Array.isArray(data) && liveState.value)
-      liveState.value = { ...liveState.value, integrityIssues: data as TestIntegrityIssue[] }
   }, error => {
     liveConnection.value = error.message.includes('权限') ? '权限不足' : '已断开'
     liveError.value = error.message
@@ -251,7 +249,7 @@
     backendDetail.value = await endTestReception(testId.value)
     state.value = '已结束'
     receptionState.value = '已结束接收'
-    testStatusLabel.value = ({ WAITING: '待测试', RUNNING: '检测中', PAUSED: '暂停', RUNNING_ABNORMAL: '检测中(异常)', COMPLETION_PENDING: '完成确认中', COMPLETED: '检测完成', TERMINATED: '已终止' } as Record<string, string>)[backendDetail.value.testStatus || 'COMPLETION_PENDING'] || '完成确认中'
+    testStatusLabel.value = ({ WAITING: '待测试', RUNNING: '检测中', PAUSED: '暂停', RUNNING_ABNORMAL: '检测中(异常)', COMPLETION_PENDING: '检测完成', COMPLETED: '检测完成', TERMINATED: '检测完成' } as Record<string, string>)[backendDetail.value.testStatus || 'COMPLETED'] || '检测完成'
     ElMessage.success('测试已停止,系统不再请求数据')
   } catch (error) {
     if (error === 'cancel' || error === 'close') return
@@ -286,8 +284,7 @@
       <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="stepDialogVisible=true"><el-icon><Setting /></el-icon>工步设置</button></header><div class="project-meta"><span>测试项目 <strong>{{ profile.purpose }}</strong></span><span>当前循环 <strong>循环 36 / 计划 60</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 in steps" :key="step.no" :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></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 integrity-panel"><header class="panel-heading"><div><i></i><strong>完整性检查</strong><span>INTERVAL INTEGRITY</span></div><span :class="(liveState?.integrityIssues || []).some(item => item.issueStatus === 'OPEN') ? 'integrity-open' : 'integrity-ok'">{{ (liveState?.integrityIssues || []).some(item => item.issueStatus === 'OPEN') ? '完成确认中' : '已通过' }}</span></header><div v-if="(liveState?.integrityIssues || []).length" class="integrity-list"><div v-for="item in (liveState?.integrityIssues || [])" :key="item.issueId" :class="item.issueStatus === 'OPEN' ? 'open' : 'resolved'"><strong>{{ item.sourceType === 'COLLECTOR_CHANNEL' ? '采集器' : '检测柜' }}<template v-if="item.intervalId"> · 区间 #{{ item.intervalId }}</template></strong><span>{{ item.issueType === 'SEQUENCE_GAP' ? `序号缺口 ${item.missingFromSequence} - ${item.missingToSequence}` : item.issueType }}</span><small>{{ item.issueStatus === 'OPEN' ? '待补采或处理' : '已恢复' }} · 重试 {{ item.retryCount }} 次</small></div></div><div v-else class="integrity-empty">所有接收区间序号连续,时间间隔仅作辅助指标</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>
+         <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>
       <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>
@@ -311,5 +308,4 @@
 .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}
-.integrity-panel .panel-heading>span{font-size:11px}.integrity-open{color:#b34f4f}.integrity-ok{color:#23865e}.integrity-list{display:grid;gap:8px;padding:14px 16px}.integrity-list>div{display:grid;grid-template-columns:1.2fr 1fr auto;align-items:center;gap:10px;padding:9px 12px;border:1px solid #e5c3a4;background:#fff8f1;font-size:11px}.integrity-list>div.resolved{border-color:#b8dfcd;background:#f2fbf6}.integrity-list span{color:#52606d}.integrity-list small{color:#8795a3}.integrity-empty{padding:16px;color:#71808d;font-size:11px}
 </style>
diff --git a/src/views/TestList.vue b/src/views/TestList.vue
index 46fdc6d..5bcab51 100644
--- a/src/views/TestList.vue
+++ b/src/views/TestList.vue
@@ -101,7 +101,7 @@
     '检测完成': 'completed',
     '暂停': 'paused',
     '检测中(异常)': 'abnormal',
-    '完成确认中': 'pending',
+    '完成确认中': 'completed',
     '已终止': 'completed',
     '未知': 'paused',
   }[status]
diff --git a/tests/test-console-live.contract.test.mjs b/tests/test-console-live.contract.test.mjs
index 4793fca..a72544f 100644
--- a/tests/test-console-live.contract.test.mjs
+++ b/tests/test-console-live.contract.test.mjs
@@ -14,4 +14,8 @@
 assert.match(view, /不纳入结果/)
 assert.doesNotMatch(view, /localStorage\.(setItem|getItem)\([^)]*test/i)
 assert.match(view, /item\.sourceType !== 'COLLECTOR' \|\| !item\.status \|\| item\.status === 'OPEN'/)
+assert.doesNotMatch(view, /完整性检查/)
+assert.doesNotMatch(view, /integrity-panel/)
+assert.match(api, /COMPLETION_PENDING: '检测完成'/)
+assert.match(api, /TERMINATED: '检测完成'/)
 console.log('Ticket09 前端实时控制台契约通过')

--
Gitblit v1.7.1