yeg
2026-08-30 8283a06e31c82d3052ef4fdba761cbee87fb77b1
接入 Ticket12 人工结束与顺序接入

控制台调用独立人工结束接口,补充区间类型、Ticket12 浏览器接缝和测试脚本。
1个文件已添加
4个文件已修改
78 ■■■■■ 已修改文件
package.json 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/tests.ts 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/TestConsole.vue 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tests/e2e/manual-end-sequential-join.spec.ts 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tests/e2e/pause-resume-reception.spec.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
package.json
@@ -14,6 +14,7 @@
    "test:e2e:ticket08": "playwright test tests/e2e/running-start-and-reception-boundaries.spec.ts",
    "test:e2e:ticket10": "playwright test tests/e2e/pause-resume-reception.spec.ts",
    "test:e2e:ticket11": "playwright test tests/e2e/collector-disconnect-recovery.spec.ts",
    "test:e2e:ticket12": "playwright test tests/e2e/manual-end-sequential-join.spec.ts",
    "test:contract:ticket09": "node tests/test-console-live.contract.test.mjs"
  },
  "dependencies": {
src/api/tests.ts
@@ -79,6 +79,18 @@
    fieldValue: string | null
  }>
  runtimeAttach?: boolean
  receptionIntervals?: Array<{
    intervalId: number
    bindingId: number | null
    sourceType: 'CABINET_CHANNEL' | 'COLLECTOR_CHANNEL' | string
    sourceId: number
    sourceChannelCode: string | null
    actionAtUtc: string
    startBoundaryAtUtc: string
    endBoundaryAtUtc: string | null
    boundaryNote: string | null
    intervalStatus: 'OPEN' | 'CLOSED' | string
  }>
}
export interface TestConfigurationOptions {
@@ -184,6 +196,11 @@
  return (await request<TestDetailResponse>(`/system/test/${testId}/stop`, { method: 'POST' })).data
}
/** 人工结束接收的独立动作接口;保留 stopTest 兼容旧页面调用。 */
export async function endTestReception(testId: number): Promise<TestConfigurationDetail> {
  return (await request<TestDetailResponse>(`/system/test/${testId}/reception/end`, { method: 'POST' })).data
}
export interface LiveSource {
  sourceType: 'CABINET_CHANNEL' | 'COLLECTOR' | string
  sourceId: number
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, openTestLiveStream, pauseTestReception, resumeTestReception, startTestReception, stopTest, type TestConfigurationDetail, type TestLiveState } from '@/api/tests'
import { endTestReception, getTestDetail, getTestLiveState, openTestLiveStream, pauseTestReception, resumeTestReception, startTestReception, type TestConfigurationDetail, type TestLiveState } from '@/api/tests'
type ConsoleState = '未开始接收' | '检测中' | '暂停' | '已结束'
type ReceptionState = '未开始接收' | '接收中' | '暂停接收' | '已结束接收'
@@ -246,7 +246,7 @@
  if (state.value === '已结束') return
  try {
    await ElMessageBox.confirm('停止测试后将不可恢复,系统会停止请求本测试的数据,但不会向设备发送停止命令,是否继续?', '确认停止测试', { confirmButtonText: '停止测试', cancelButtonText: '取消', type: 'warning', confirmButtonClass: 'danger-confirm' })
    backendDetail.value = await stopTest(testId.value)
    backendDetail.value = await endTestReception(testId.value)
    state.value = '已结束'
    receptionState.value = '已结束接收'
    testStatusLabel.value = '检测完成'
tests/e2e/manual-end-sequential-join.spec.ts
New file
@@ -0,0 +1,54 @@
import { expect, test } from '@playwright/test'
test('人工结束立即释放来源,第二项测试可从后续边界运行中接入', async ({ page }) => {
  const actions: string[] = []
  const details: Record<number, any> = {
    42: { testId: 42, testName: '测试 A', testStatus: 'RUNNING', receptionStatus: 'RECEIVING', runtimeAttach: false,
      batteries: [], planSteps: [], thresholds: [], fieldSnapshots: [], receptionIntervals: [
        { intervalId: 1, sourceType: 'CABINET_CHANNEL', sourceId: 100, actionAtUtc: '2026-08-30T06:00:00Z', startBoundaryAtUtc: '2026-08-30T06:00:00Z', endBoundaryAtUtc: null, intervalStatus: 'OPEN' },
      ] },
    43: { testId: 43, testName: '测试 B', testStatus: 'WAITING', receptionStatus: 'NOT_STARTED', runtimeAttach: false,
      batteries: [], planSteps: [], thresholds: [], fieldSnapshots: [], receptionIntervals: [] },
  }
  await page.addInitScript(() => localStorage.setItem('battery_detection_auth_token', 'ticket12-test-token'))
  await page.route('**/system/test/**', async route => {
    const match = route.request().url().match(/\/system\/test\/(\d+)/)
    const testId = match ? Number(match[1]) : 42
    const detail = details[testId]
    if (!detail) return route.continue()
    if (route.request().method() === 'POST' && route.request().url().endsWith('/reception/end')) {
      actions.push(`stop-${testId}`)
      detail.testStatus = 'COMPLETED'
      detail.receptionStatus = 'ENDED'
      detail.receptionIntervals = detail.receptionIntervals.map((interval: any) => ({ ...interval, endBoundaryAtUtc: '2026-08-30T06:05:00Z', intervalStatus: 'CLOSED', boundaryNote: 'MANUAL_END' }))
    } else if (route.request().method() === 'POST' && route.request().url().endsWith('/reception/start')) {
      actions.push(`start-${testId}`)
      detail.testStatus = 'RUNNING'
      detail.receptionStatus = 'RECEIVING'
      detail.runtimeAttach = true
      detail.receptionIntervals = [{ intervalId: 2, sourceType: 'CABINET_CHANNEL', sourceId: 100, actionAtUtc: '2026-08-30T06:05:01Z', startBoundaryAtUtc: '2026-08-30T06:05:01Z', endBoundaryAtUtc: null, intervalStatus: 'OPEN' }]
    }
    if (route.request().url().endsWith('/live-state')) {
      await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ code: 200, data: { testId, testStatus: detail.testStatus, receptionStatus: detail.receptionStatus, updatedAt: '2026-08-30T06:05:01Z', sources: [], exceptions: [] } }) })
      return
    }
    if (route.request().url().endsWith('/live')) {
      await route.fulfill({ status: 200, contentType: 'text/event-stream', body: 'event: heartbeat\ndata: {"at":"2026-08-30T06:05:01Z"}\n\n' })
      return
    }
    await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ code: 200, data: detail }) })
  })
  await page.goto('/test-list/42/console')
  await page.getByRole('button', { name: '停止测试', exact: true }).click()
  await page.getByRole('button', { name: '停止测试', exact: true }).last().click()
  await expect(page.locator('.test-state')).toHaveText('测试状态:检测完成')
  await expect(page.locator('.reception-state')).toHaveText('已结束接收')
  await page.goto('/test-list/43/console')
  await page.getByRole('button', { name: '开始接收', exact: true }).click()
  await expect(page.getByText('运行中接入', { exact: true })).toBeVisible()
  await expect(page.locator('.reception-state')).toHaveText('接收中')
  expect(actions).toEqual(['stop-42', 'start-43'])
  expect(details[42].receptionIntervals[0].endBoundaryAtUtc <= details[43].receptionIntervals[0].startBoundaryAtUtc).toBeTruthy()
})
tests/e2e/pause-resume-reception.spec.ts
@@ -13,7 +13,7 @@
    const request = route.request()
    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 = 'COMPLETED'; actionRequests.push('stop') }
    if (request.method() === 'POST' && (request.url().endsWith('/stop') || request.url().endsWith('/reception/end'))) { status = 'ENDED'; testStatus = 'COMPLETED'; 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, receptionStatus: status, updatedAt: '2026-08-30T06:00:00Z', sources: [], exceptions: [] } }) })
      return