From 92bcbc62cd6a870907c867e024e13ac22f6eabe3 Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期日, 30 八月 2026 16:07:24 +0800
Subject: [PATCH] 调整测试暂停继续和停止控制台

---
 src/views/TestConsole.vue                |   52 ++++++++-----------------
 src/api/tests.ts                         |   25 ++----------
 AGENTS.md                                |    4 +-
 tests/e2e/pause-resume-reception.spec.ts |   37 +++++++++++-------
 4 files changed, 46 insertions(+), 72 deletions(-)

diff --git a/AGENTS.md b/AGENTS.md
index 21bd533..477fdc3 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -13,7 +13,7 @@
 
 - 正式测试状态只展示:`待测试`、`检测中`、`暂停`、`检测中(异常)`、`检测完成`、`已终止`。“完成确认中”是后端内部阶段,前端可用进度文案表达但不新增正式测试状态。
 - 设备状态和接收状态单独并列展示。接收状态使用:`未开始接收`、`接收中`、`暂停接收`、`已结束接收`;不要把设备、接收和测试三套状态合并到一个字段或筛选项。
-- 开始、暂停、继续和结束测试的页面动作只改变数据接收区间,不表示控制 BTS 或采集器。确认文案必须明确不控制设备,暂停期间实时快照继续更新并标记“不纳入结果”。
+- 开始、暂停、继续和停止测试的页面动作只改变系统数据请求,不表示控制 BTS 或采集器。确认文案必须明确不控制设备;暂停期间停止实时请求、保留最后快照并标记“不纳入结果”,停止后进入“已终止”。
 - 实时快照与历史结果在类型、接口和页面文案上分开;实时点不能加入最终原始数据列表。
 - 第一版每个测试电池号必须选择一个已登记采集器,以及主温度、正电位和负电位通道;可选次要通道和动态测量项保持现有交互。`S-01...S-N` 仅属于演示合同。
 
@@ -21,7 +21,7 @@
 
 - HTTP 请求复用 `src/api/request.ts` 的鉴权和错误处理;按业务域在 `src/api` 中维护带类型的接口,不在视图内散落 URL。
 - SSE 使用 `fetch` 流式读取 `text/event-stream` 并携带 `Authorization` 请求头。JWT 不放入 URL,不使用原生 `EventSource` 绕开现有鉴权。
-- SSE 重连后用服务端当前快照替换本地快照;历史区间通过 REST 查询。不要把 SSE 当作可靠消息队列或在浏览器累积所有采样点。
+- SSE 重连后用服务端当前快照替换本地快照;历史数据按测试动作时间由后端查询。不要把 SSE 当作可靠消息队列或在浏览器累积所有采样点。
 - API 时间按 ISO 8601 解析并以用户本地时区展示;单位换算只发生在展示或导入边界,并保留接口返回的原始单位。
 
 ## 交互与数据安全
diff --git a/src/api/tests.ts b/src/api/tests.ts
index 3d652e4..53d6e91 100644
--- a/src/api/tests.ts
+++ b/src/api/tests.ts
@@ -169,34 +169,19 @@
   return (await request<TestDetailResponse>(`/system/test/${testId}/reception/start`, { method: 'POST' })).data
 }
 
-/** 暂停系统数据接收;设备实时观察继续进行。 */
+/** 暂停系统数据请求;设备不接收控制命令。 */
 export async function pauseTestReception(testId: number): Promise<TestConfigurationDetail> {
   return (await request<TestDetailResponse>(`/system/test/${testId}/reception/pause`, { method: 'POST' })).data
 }
 
-/** 继续系统数据接收;后端从新的成功观测边界打开区间。 */
+/** 继续系统数据请求;恢复暂停后的实时请求。 */
 export async function resumeTestReception(testId: number): Promise<TestConfigurationDetail> {
   return (await request<TestDetailResponse>(`/system/test/${testId}/reception/resume`, { method: 'POST' })).data
 }
 
-export interface TestReceptionInterval {
-  intervalId: number
-  testId: number
-  bindingId: number | null
-  sourceType: string
-  sourceId: number
-  sourceChannelCode: string | null
-  actionAtUtc: string
-  startBoundaryAtUtc: string
-  endBoundaryAtUtc: string | null
-  boundaryNote: string | null
-  intervalStatus: 'OPEN' | 'CLOSED' | string
-}
-
-interface TestReceptionIntervalsResponse extends ApiResponse { data: TestReceptionInterval[] }
-
-export async function getTestReceptionIntervals(testId: number): Promise<TestReceptionInterval[]> {
-  return (await request<TestReceptionIntervalsResponse>(`/system/test/${testId}/reception/intervals`)).data || []
+/** 停止测试并进入不可恢复的已终止状态。 */
+export async function stopTest(testId: number): Promise<TestConfigurationDetail> {
+  return (await request<TestDetailResponse>(`/system/test/${testId}/stop`, { method: 'POST' })).data
 }
 
 export interface LiveSource {
diff --git a/src/views/TestConsole.vue b/src/views/TestConsole.vue
index 49d6c72..5007914 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 { getTestDetail, getTestLiveState, getTestReceptionIntervals, openTestLiveStream, pauseTestReception, resumeTestReception, startTestReception, type TestConfigurationDetail, type TestLiveState, type TestReceptionInterval } from '@/api/tests'
+import { getTestDetail, getTestLiveState, openTestLiveStream, pauseTestReception, resumeTestReception, startTestReception, stopTest, type TestConfigurationDetail, type TestLiveState } from '@/api/tests'
 
 type ConsoleState = '未开始接收' | '检测中' | '暂停' | '已结束'
 type ReceptionState = '未开始接收' | '接收中' | '暂停接收' | '已结束接收'
@@ -25,8 +25,6 @@
 let reconnectTimer: number | null = null
 const runtimeAttach = ref(false)
 const receptionState = ref<ReceptionState>('未开始接收')
-const receptionIntervals = ref<TestReceptionInterval[]>([])
-const intervalError = ref('')
 const testProfiles: Record<number, { name: string; owner: string; purpose: string; startedAt: string; batteryGroup: string; batteryCount: number; abnormal: boolean }> = {
   1024: { name: '高温循环性能测试', owner: '张工', purpose: '验证高温环境循环寿命', startedAt: '2026-07-10 14:40:00', batteryGroup: '高温循环 A 组', batteryCount: 8, abnormal: false },
   1023: { name: '电芯容量一致性测试', owner: '李工', purpose: '筛查批次容量离散度', startedAt: '2026-07-10 11:30:00', batteryGroup: '容量一致性 A 组', batteryCount: 6, abnormal: true },
@@ -59,16 +57,6 @@
   { key: 'project-chart', label: '项目图表' },
   { key: 'data-list', label: '数据列表' },
 ]
-
-function intervalSourceLabel(interval: TestReceptionInterval) {
-  return interval.sourceType === 'CABINET_CHANNEL'
-    ? `检测柜通道 ${interval.sourceId}`
-    : `采集器 ${interval.sourceId}${interval.sourceChannelCode ? ` · ${interval.sourceChannelCode}` : ''}`
-}
-
-function formatIntervalTime(value: string | null) {
-  return value ? new Date(value).toLocaleString('zh-CN', { hour12: false }) : '开放中'
-}
 
 const timer = window.setInterval(() => { if (state.value === '检测中') elapsedSeconds.value += 1 }, 1000)
 onBeforeUnmount(() => window.clearInterval(timer))
@@ -103,8 +91,6 @@
     else if (event === 'reception-status' && data && typeof data === 'object' && 'receptionStatus' in data) {
       const next = { ...(liveState.value || {}), ...(data as object) } as TestLiveState
       if (liveState.value) applyLiveState({ ...liveState.value, receptionStatus: (data as { receptionStatus: TestLiveState['receptionStatus'] }).receptionStatus })
-      void getTestReceptionIntervals(testId.value).then(value => { receptionIntervals.value = value; intervalError.value = '' })
-        .catch(() => { intervalError.value = '接收区间刷新失败,请稍后重试' })
       void next
     }
     else if (event === 'test-status' && data && typeof data === 'object' && 'testStatus' in data && liveState.value)
@@ -121,8 +107,6 @@
   try {
     const detail = await getTestDetail(testId.value)
     backendDetail.value = detail
-    try { receptionIntervals.value = await getTestReceptionIntervals(testId.value) }
-    catch { receptionIntervals.value = []; intervalError.value = '接收区间加载失败,请刷新重试' }
     runtimeAttach.value = detail.runtimeAttach === true
     if (detail.receptionStatus === 'NOT_STARTED') {
       state.value = '未开始接收'
@@ -220,8 +204,8 @@
   try {
     await ElMessageBox.confirm(
       pausing
-        ? '暂停接收将关闭当前数据接收区间,但设备仍继续运行和更新实时快照,不会向设备发送暂停命令,是否继续?'
-        : '继续接收将从新的成功观测边界打开数据接收区间,不追认暂停期间数据,也不会向设备发送继续命令,是否继续?',
+        ? '暂停接收将停止系统向检测柜和采集器请求新数据,但设备仍继续运行,不会向设备发送暂停命令,是否继续?'
+        : '继续接收将恢复系统数据请求,不会追认暂停期间数据,也不会向设备发送继续命令,是否继续?',
       pausing ? '确认暂停接收' : '确认继续接收',
       { confirmButtonText: pausing ? '暂停接收' : '继续接收', cancelButtonText: '取消', type: 'warning' },
     )
@@ -230,9 +214,7 @@
       : await resumeTestReception(testId.value)
     state.value = pausing ? '暂停' : '检测中'
     receptionState.value = pausing ? '暂停接收' : '接收中'
-    try { receptionIntervals.value = await getTestReceptionIntervals(testId.value); intervalError.value = '' }
-    catch { intervalError.value = '接收区间刷新失败,请稍后重试'; ElMessage.warning('动作已生效,但接收区间暂时无法刷新') }
-    ElMessage.success(pausing ? '已暂停接收,实时快照继续更新且不纳入结果' : '已继续接收,已从新的观测边界打开区间')
+    ElMessage.success(pausing ? '已暂停接收,系统已停止请求新数据' : '已继续接收,系统已恢复数据请求')
   } catch (error) {
     if (error === 'cancel' || error === 'close') return
     ElMessage.error(error instanceof Error ? error.message : (pausing ? '暂停接收失败' : '继续接收失败'))
@@ -255,9 +237,16 @@
 async function endTest() {
   if (state.value === '已结束') return
   try {
-    await ElMessageBox.confirm('结束接收将关闭本测试的数据接收区间,不会停止设备,是否继续?', '确认结束接收', { confirmButtonText: '结束接收', cancelButtonText: '取消', type: 'warning', confirmButtonClass: 'danger-confirm' })
-    ElMessage.info('结束接收动作由后端接收区间接口处理,当前版本仅展示实时事实')
-  } catch { /* 用户取消 */ }
+    await ElMessageBox.confirm('停止测试后将不可恢复,系统会停止请求本测试的数据,但不会向设备发送停止命令,是否继续?', '确认停止测试', { confirmButtonText: '停止测试', cancelButtonText: '取消', type: 'warning', confirmButtonClass: 'danger-confirm' })
+    backendDetail.value = await stopTest(testId.value)
+    state.value = '已结束'
+    receptionState.value = '已结束接收'
+    testStatusLabel.value = '已终止'
+    ElMessage.success('测试已停止,系统不再请求数据')
+  } catch (error) {
+    if (error === 'cancel' || error === 'close') return
+    ElMessage.error(error instanceof Error ? error.message : '停止测试失败')
+  }
 }
 </script>
 
@@ -270,23 +259,18 @@
     <div v-show="activeTab === 'realtime'" class="tab-content">
     <section class="realtime-panel">
       <header class="realtime-header">
-        <div><strong>实时数据</strong><span class="cell-count">{{ cells.length }} 个电池</span><span class="running-state" :class="state"><i></i>{{ state }}</span><span class="test-state">测试状态:{{ testStatusLabel }}</span><span class="reception-state"><i></i>{{ receptionState }}</span><span class="live-connection" :class="liveConnection"><i></i>{{ liveConnection }}</span><span v-if="runtimeAttach" class="attach-state">运行中接入</span><span class="interval-count">接收区间 {{ receptionIntervals.length }} 段</span></div>
+        <div><strong>实时数据</strong><span class="cell-count">{{ cells.length }} 个电池</span><span class="running-state" :class="state"><i></i>{{ state }}</span><span class="test-state">测试状态:{{ testStatusLabel }}</span><span class="reception-state"><i></i>{{ receptionState }}</span><span class="live-connection" :class="liveConnection"><i></i>{{ liveConnection }}</span><span v-if="runtimeAttach" class="attach-state">运行中接入</span></div>
         <dl><div><dt>测试开始时间</dt><dd>{{ profile.startedAt }}</dd></div><div><dt>测试已进行时间</dt><dd>{{ elapsedText }}</dd></div></dl>
-        <div class="test-actions"><button v-if="state === '未开始接收'" class="primary-button" type="button" @click="startReception"><el-icon><VideoPlay /></el-icon>开始接收</button><button v-else class="primary-button" type="button" :disabled="state === '已结束'" @click="togglePause"><el-icon><VideoPause v-if="state !== '暂停'" /><VideoPlay v-else /></el-icon>{{ state === '暂停' ? '继续接收' : '暂停接收' }}</button><button class="danger-button" type="button" :disabled="state === '已结束' || state === '未开始接收'" @click="endTest"><el-icon><CircleCloseFilled /></el-icon>结束接收</button></div>
+        <div class="test-actions"><button v-if="state === '未开始接收'" class="primary-button" type="button" @click="startReception"><el-icon><VideoPlay /></el-icon>开始接收</button><button v-else class="primary-button" type="button" :disabled="state === '已结束'" @click="togglePause"><el-icon><VideoPause v-if="state !== '暂停'" /><VideoPlay v-else /></el-icon>{{ state === '暂停' ? '继续接收' : '暂停接收' }}</button><button class="danger-button" type="button" :disabled="state === '已结束' || state === '未开始接收'" @click="endTest"><el-icon><CircleCloseFilled /></el-icon>停止测试</button></div>
       </header>
       <div class="cell-grid">
         <article v-for="cell in cells" :key="cell.no" class="cell-card">
-          <div class="cell-summary"><strong>{{ cell.no }}</strong><span>{{ profile.batteryGroup }}</span><p>{{ cell.cabinet }} · {{ cell.channel }}</p><small>检测柜来源 · 单位:{{ cell.unit }} · 采样时间:{{ cell.observedAt || '-' }}</small><small>采集器来源 · 状态:{{ cell.collectorStatus }} · 单位:{{ cell.collectorUnit }} · 采样时间:{{ cell.collectorObservedAt || '-' }}</small><small v-if="receptionState === '暂停接收'">当前快照不纳入结果</small><small>工步进行时间:{{ elapsedText }}</small></div>
+          <div class="cell-summary"><strong>{{ cell.no }}</strong><span>{{ profile.batteryGroup }}</span><p>{{ cell.cabinet }} · {{ cell.channel }}</p><small>检测柜来源 · 单位:{{ cell.unit }} · 采样时间:{{ cell.observedAt || '-' }}</small><small>采集器来源 · 状态:{{ cell.collectorStatus }} · 单位:{{ cell.collectorUnit }} · 采样时间:{{ cell.collectorObservedAt || '-' }}</small><small v-if="receptionState === '暂停接收'">暂停期间停止请求,当前快照不纳入结果</small><small>工步进行时间:{{ elapsedText }}</small></div>
           <dl class="metric-list"><div><dt>电压(V)</dt><dd class="voltage">{{ cell.voltage }}</dd></div><div><dt>温度(℃)</dt><dd class="temperature">{{ cell.temperature }}</dd></div><div><dt>正电位</dt><dd class="positive">{{ cell.positive }}</dd></div><div><dt>负电位</dt><dd class="negative">{{ cell.negative }}</dd></div><div><dt>容量(Ah)</dt><dd class="capacity">{{ cell.capacity }}</dd></div><div><dt>电流(A)</dt><dd class="current">{{ cell.current }}</dd></div></dl>
         </article>
       </div>
-      <section v-if="receptionIntervals.length" class="interval-panel" aria-label="接收区间边界">
-        <div class="interval-heading"><strong>接收区间边界</strong><span>实际观测边界 · 暂停期间不纳入结果</span></div>
-        <div class="interval-list"><div v-for="interval in receptionIntervals" :key="interval.intervalId"><span>{{ intervalSourceLabel(interval) }}</span><time>{{ formatIntervalTime(interval.startBoundaryAtUtc) }} - {{ formatIntervalTime(interval.endBoundaryAtUtc) }}</time><em>{{ interval.intervalStatus === 'OPEN' ? '开放中' : '已关闭' }}</em></div></div>
-      </section>
     </section>
     <p v-if="liveError" class="live-error" role="status">{{ liveError }},正在尝试恢复实时状态</p>
-    <p v-if="intervalError" class="live-error" role="status">{{ intervalError }}</p>
 
     <div class="console-layout">
       <main>
@@ -311,8 +295,6 @@
 .console-page{width:100%;padding-bottom:24px}.page-tools{height:34px;display:flex;align-items:center;justify-content:space-between}.page-tools>span{color:#7f8d99;font-size:10px;font-weight:600}.back-button{display:inline-flex;align-items:center;gap:6px;border:0;color:#52606d;background:transparent;font-size:13px;cursor:pointer}.back-button:hover{color:var(--blue)}.primary-button,.secondary-button,.danger-button{height:34px;display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:0 13px;border:1px solid transparent;border-radius:3px;font-size:13px;font-weight:500;cursor:pointer;white-space:nowrap}.primary-button{color:#fff;background:var(--blue);border-color:var(--blue)}.secondary-button{color:#52606d;background:#fff;border-color:var(--line-strong)}.danger-button{color:#fff;background:#ee6666;border-color:#ee6666}.primary-button:disabled,.danger-button:disabled{opacity:.5;cursor:not-allowed}.realtime-panel,.panel,.parameter-panel{border:1px solid #d8e1e9;background:#fff}.realtime-panel{margin-bottom:16px}.realtime-header{min-height:96px;display:grid;grid-template-columns:minmax(190px,1fr) minmax(360px,2fr) auto;align-items:center;gap:24px;padding:16px 20px;border-bottom:1px solid var(--line)}.realtime-header>div:first-child{display:flex;align-items:center;gap:12px}.realtime-header strong{font-size:15px}.cell-count{color:#71808d;font-size:11px}.running-state{display:inline-flex;align-items:center;gap:6px;padding:4px 7px;border:1px solid;font-size:11px}.running-state i{width:5px;height:5px;border-radius:50%;background:currentColor}.running-state.检测中{color:#23865e;background:#edf9f3;border-color:#b8dfcd}.running-state.暂停{color:#b06a18;background:#fff8e9;border-color:#efd59e}.running-state.已结束{color:#7b8794;background:#f2f4f6;border-color:#d8dde2}.running-state.未开始接收{color:#7b8794;background:#f2f4f6;border-color:#d8dde2}.realtime-header dl{display:flex;gap:44px}.realtime-header dl>div{display:flex;align-items:center;gap:16px}.realtime-header dt{color:#52606d;font-size:12px}.realtime-header dd{color:#7b8794;font-family:'Cascadia Mono',Consolas,monospace;font-size:12px;font-weight:600}.test-actions{display:flex;gap:10px}.cell-grid{display:grid;grid-template-columns:repeat(4,minmax(190px,1fr));gap:24px;padding:22px 36px}.cell-card{min-width:0;border:1px solid #bec9d3;border-radius:6px;background:#f5f7fa;overflow:hidden}.cell-summary{display:flex;flex-direction:column;gap:7px;padding:16px 18px 12px;border-bottom:1px solid #c8d1d9}.cell-summary strong{color:#1268dc;font-size:13px}.cell-summary span{color:#37688e;font-size:12px;font-weight:600}.cell-summary p,.cell-summary small{color:#71808d;font-size:10px}.metric-list{display:grid;gap:12px;padding:14px 18px 16px}.metric-list>div{display:flex;align-items:center;justify-content:space-between}.metric-list dt{color:#52606d;font-size:10px}.metric-list dd{font-size:17px;font-weight:700}.metric-list .voltage{color:#7f35c5}.metric-list .temperature{color:#bf6100}.metric-list .positive{color:#e99f9f}.metric-list .negative{color:#176ee8}.metric-list .capacity{color:#f59a2a}.metric-list .current{color:#21628c}.console-layout{display:grid;grid-template-columns:minmax(0,2.35fr) minmax(270px,1fr);gap:16px;align-items:start}.console-layout main{display:grid;gap:16px}.panel-heading{height:48px;display:flex;align-items:center;justify-content:space-between;padding:0 16px;border-bottom:1px solid var(--line);background:#fbfdff}.panel-heading>div{display:flex;align-items:center;gap:8px}.panel-heading>div>i{width:3px;height:16px;background:#2b78a3}.panel-heading strong{color:#26333f;font-size:14px}.panel-heading span{color:#81909d;font-size:10px}.compact-button{height:30px;font-size:12px}.project-meta{display:flex;gap:32px;padding:14px 16px 0;color:#71808d;font-size:11px}.project-meta strong{margin-left:8px;color:#31404d}.step-table-wrap,.channel-table-wrap{overflow-x:auto;padding:14px 16px 18px}table{width:100%;border-collapse:collapse;color:#52606d;font-size:12px}th,td{height:38px;padding:0 10px;border:1px solid #d5e0e9;text-align:center;white-space:nowrap}th{color:#52606d;background:#eaf4ff;font-weight:600}.step-table tr.active td{background:#eef7ff}.step-table tr.active td:first-child{border-left:3px solid #2188f5}.step-state{font-size:11px}.step-state.进行中{color:#1677dd}.step-state.已完成{color:#23865e}.step-state.待执行{color:#8795a3}.parameter-panel dl{display:grid;gap:0;padding:12px 18px 18px}.parameter-panel dl>div{display:grid;grid-template-columns:88px minmax(0,1fr);gap:10px;padding:9px 0;border-bottom:1px solid #edf1f5}.parameter-panel dl>div:last-child{border-bottom:0}.parameter-panel dt{color:#71808d;font-size:11px}.parameter-panel dd{min-width:0;color:#52606d;font-size:11px;overflow-wrap:anywhere}.alarm-empty{height:68px;display:flex;align-items:center;gap:8px;margin:14px 16px;padding:0 16px;color:#71808d;background:#f4f7fa;font-size:12px}.alarm-empty .el-icon{color:#23865e;font-size:16px}.alarm-list{display:grid;gap:8px;padding:14px 16px}.alarm-list>div{display:flex;align-items:center;gap:10px;padding:10px 12px;border:1px solid #f0c8a3;background:#fff7ef;color:#b35a25}.alarm-list span,.alarm-list strong,.alarm-list small{display:block}.alarm-list strong{font-size:12px}.alarm-list small{margin-top:3px;color:#8c6849;font-size:10px}.channel-table-wrap table{min-width:1120px}.device-state{display:inline-flex;padding:3px 7px;border:1px solid;font-size:10px}.device-state.在线{color:#23865e;background:#edf9f3;border-color:#b8dfcd}.device-state.异常{color:#b34f4f;background:#fff1f1;border-color:#e9bcbc}.step-dialog-content{display:grid;gap:12px}.step-dialog-content>p{color:#71808d;font-size:12px}.step-dialog-content>strong{color:#26333f}.step-dialog-content dl{display:grid;grid-template-columns:repeat(3,1fr);gap:10px}.step-dialog-content dl>div{padding:12px;border:1px solid #d8e1e9;background:#f8fafc}.step-dialog-content dt{color:#8795a3;font-size:10px}.step-dialog-content dd{margin-top:5px;color:#405261;font-size:12px}@media(max-width:1280px){.cell-grid{gap:16px;padding:20px}.console-layout{grid-template-columns:minmax(0,1fr) 280px}}@media(max-width:1050px){.cell-grid{grid-template-columns:repeat(2,minmax(210px,1fr))}.realtime-header{grid-template-columns:1fr auto}.realtime-header dl{grid-column:1/-1;grid-row:2}.console-layout{grid-template-columns:1fr}.parameter-panel{grid-row:2}}@media(max-width:680px){.realtime-header{grid-template-columns:1fr}.realtime-header dl{grid-column:auto;grid-row:auto;align-items:flex-start;flex-direction:column;gap:10px}.test-actions{justify-content:stretch}.test-actions button{flex:1}.cell-grid{grid-template-columns:1fr;padding:14px}.project-meta{flex-direction:column;gap:8px}.step-dialog-content dl{grid-template-columns:1fr}}
 
 .reception-state{padding:4px 7px;border:1px solid #b9decf;color:#23865e;background:#edf9f3;font-size:11px}.test-state{padding:4px 7px;border:1px solid #d8dde2;color:#52606d;background:#f5f7fa;font-size:11px}.attach-state{padding:4px 7px;border:1px solid #bdd8f4;color:#1677dd;background:#edf6ff;font-size:11px}
-.interval-count{color:#71808d;font-size:10px;white-space:nowrap}
-.interval-panel{margin:0 22px 18px;border-top:1px solid #d8e1e9}.interval-heading{display:flex;align-items:center;gap:10px;padding:12px 2px 8px;color:#26333f;font-size:12px}.interval-heading span{color:#8795a3;font-size:10px}.interval-list{display:grid;gap:6px}.interval-list>div{display:grid;grid-template-columns:minmax(170px,1fr) minmax(280px,2fr) auto;gap:12px;align-items:center;padding:8px 10px;background:#f8fafc;color:#52606d;font-size:11px}.interval-list time{font-family:'Cascadia Mono',Consolas,monospace;color:#71808d}.interval-list em{font-style:normal;color:#23865e}@media(max-width:680px){.interval-panel{margin:0 14px 14px}.interval-list>div{grid-template-columns:1fr;gap:4px}}
 .live-connection{display:inline-flex;align-items:center;gap:5px;padding:4px 7px;border:1px solid #cbd5df;color:#52606d;background:#f5f7fa;font-size:11px}.live-connection i{width:5px;height:5px;border-radius:50%;background:currentColor}.live-connection.已连接{color:#23865e;background:#edf9f3;border-color:#b8dfcd}.live-connection.权限不足{color:#b34f4f;background:#fff1f1;border-color:#e9bcbc}.live-error{margin:8px 20px 14px;padding:9px 12px;border:1px solid #e9bcbc;color:#a94444;background:#fff5f5;font-size:12px}
 /* 双栏收缩边界:宽表只能在所属面板内横向滚动,不能撑开主网格。 */
 .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}}
diff --git a/tests/e2e/pause-resume-reception.spec.ts b/tests/e2e/pause-resume-reception.spec.ts
index ffd531d..6693649 100644
--- a/tests/e2e/pause-resume-reception.spec.ts
+++ b/tests/e2e/pause-resume-reception.spec.ts
@@ -1,23 +1,21 @@
 import { expect, test } from '@playwright/test'
 
-test('暂停和继续接收形成多区间且不控制设备', async ({ page }) => {
+test('暂停停止系统请求、继续恢复请求并可停止测试', async ({ page }) => {
   const base = {
-    testId: 42, testName: 'Ticket10 多区间测试', testStatus: 'RUNNING', receptionStatus: 'RECEIVING',
+    testId: 42, testName: 'Ticket10 生命周期测试', testStatus: 'RUNNING', receptionStatus: 'RECEIVING',
     batteries: [], planSteps: [], thresholds: [], fieldSnapshots: [], runtimeAttach: false,
   }
   let status = 'RECEIVING'
-  let intervalCount = 1
+  let testStatus = 'RUNNING'
+  const actionRequests: string[] = []
   await page.addInitScript(() => localStorage.setItem('battery_detection_auth_token', 'ticket10-test-token'))
   await page.route('**/system/test/42**', async route => {
     const request = route.request()
-    if (request.method() === 'POST' && request.url().endsWith('/reception/pause')) status = 'PAUSED'
-    if (request.method() === 'POST' && request.url().endsWith('/reception/resume')) { status = 'RECEIVING'; intervalCount += 1 }
-    if (request.url().endsWith('/reception/intervals')) {
-      await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ code: 200, data: Array.from({ length: intervalCount }, (_, i) => ({ intervalId: i + 1, testId: 42, sourceType: 'CABINET_CHANNEL', sourceId: 100, actionAtUtc: '2026-08-30T06:00:00Z', startBoundaryAtUtc: '2026-08-30T06:00:00Z', endBoundaryAtUtc: i === 0 ? '2026-08-30T06:05:00Z' : null, intervalStatus: i === 0 ? 'CLOSED' : 'OPEN' })) }) })
-      return
-    }
+    if (request.method() === 'POST' && request.url().endsWith('/reception/pause')) { status = 'PAUSED'; actionRequests.push('pause') }
+    if (request.method() === 'POST' && request.url().endsWith('/reception/resume')) { status = 'RECEIVING'; actionRequests.push('resume') }
+    if (request.method() === 'POST' && request.url().endsWith('/stop')) { status = 'ENDED'; testStatus = 'TERMINATED'; actionRequests.push('stop') }
     if (request.url().endsWith('/live-state')) {
-      await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ code: 200, data: { testId: 42, testStatus: status === 'PAUSED' ? 'PAUSED' : 'RUNNING', receptionStatus: status, updatedAt: '2026-08-30T06:00:00Z', sources: [], exceptions: [] } }) })
+      await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ code: 200, data: { testId: 42, testStatus, receptionStatus: status, updatedAt: '2026-08-30T06:00:00Z', sources: [], exceptions: [] } }) })
       return
     }
     if (request.url().endsWith('/live')) {
@@ -25,19 +23,28 @@
       return
     }
     if (request.method() === 'POST') {
-      await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ code: 200, data: { ...base, receptionStatus: status, testStatus: status === 'PAUSED' ? 'PAUSED' : 'RUNNING' } }) })
+      await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ code: 200, data: { ...base, receptionStatus: status, testStatus } }) })
       return
     }
-    await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ code: 200, data: { ...base, receptionStatus: status } }) })
+    await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ code: 200, data: { ...base, receptionStatus: status, testStatus } }) })
   })
   await page.goto('/test-list/42/console')
   await expect(page.getByRole('button', { name: '暂停接收', exact: true })).toBeVisible()
   await page.getByRole('button', { name: '暂停接收', exact: true }).click()
   await page.getByRole('button', { name: '暂停接收', exact: true }).last().click()
   await expect(page.locator('.reception-state')).toHaveText('暂停接收')
-  await expect(page.getByText('已暂停接收,实时快照继续更新且不纳入结果', { exact: true })).toBeVisible()
+  await expect(page.getByText('暂停期间停止请求,当前快照不纳入结果', { exact: true }).first()).toBeVisible()
+  await expect(page.getByText('已暂停接收,系统已停止请求新数据', { exact: true })).toBeVisible()
   await page.getByRole('button', { name: '继续接收', exact: true }).click()
   await page.getByRole('button', { name: '继续接收', exact: true }).last().click()
-  await expect(page.getByText('已继续接收,已从新的观测边界打开区间', { exact: true })).toBeVisible()
-  await expect(page.locator('.interval-count')).toContainText('2')
+  await expect(page.locator('.reception-state')).toHaveText('接收中')
+  await expect(page.getByText('已继续接收,系统已恢复数据请求', { exact: true })).toBeVisible()
+  await page.getByRole('button', { name: '停止测试', exact: true }).click()
+  await page.getByRole('button', { name: '停止测试', exact: true }).last().click()
+  await expect(page.locator('.running-state')).toHaveText('已结束')
+  await expect(page.locator('.test-state')).toHaveText('测试状态:已终止')
+  await expect(page.locator('.reception-state')).toHaveText('已结束接收')
+  await expect(page.locator('button.danger-button')).toBeDisabled()
+  await expect(page.locator('.interval-count')).toHaveCount(0)
+  expect(actionRequests).toEqual(['pause', 'resume', 'stop'])
 })

--
Gitblit v1.7.1