From d80b338eb822455f230cb0888513147d50965904 Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期日, 30 八月 2026 01:26:09 +0800
Subject: [PATCH] 接入双状态实时控制台

---
 src/api/tests.ts |   75 +++++++++++++++++++++++++++++++++++++
 1 files changed, 74 insertions(+), 1 deletions(-)

diff --git a/src/api/tests.ts b/src/api/tests.ts
index 08d5f8b..8d4ccfc 100644
--- a/src/api/tests.ts
+++ b/src/api/tests.ts
@@ -1,5 +1,5 @@
 import type { ReceptionStatus, TestRecord, TestStatus } from '@/stores/tests'
-import { request, type ApiResponse } from './request'
+import { getAuthToken, request, type ApiResponse } from './request'
 
 export type TestStatusCode = 'WAITING' | 'RUNNING' | 'PAUSED' | 'RUNNING_ABNORMAL' | 'COMPLETION_PENDING' | 'COMPLETED' | 'TERMINATED'
 export type ReceptionStatusCode = 'NOT_STARTED' | 'RECEIVING' | 'PAUSED' | 'ENDED'
@@ -168,3 +168,76 @@
 export async function startTestReception(testId: number): Promise<TestConfigurationDetail> {
   return (await request<TestDetailResponse>(`/system/test/${testId}/reception/start`, { method: 'POST' })).data
 }
+
+export interface LiveSource {
+  sourceType: 'CABINET_CHANNEL' | 'COLLECTOR' | string
+  sourceId: number
+  batteryNo: string
+  deviceStatus: string
+  latestSnapshot: {
+    source?: string
+    unit?: string
+    observedAt?: string
+    sampleIntervalMs?: number
+    measurements?: Array<Record<string, unknown>>
+    units?: string[]
+    [key: string]: unknown
+  }
+}
+
+export interface TestLiveState {
+  testId: number
+  testStatus: TestStatusCode
+  receptionStatus: ReceptionStatusCode
+  updatedAt: string
+  sources: LiveSource[]
+  exceptions: Array<Record<string, unknown>>
+}
+
+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 type TestLiveEvent = 'snapshot' | 'device-status' | 'reception-status' | 'test-status' | 'exception' | 'heartbeat'
+
+/** 使用 fetch 流读取鉴权 SSE;调用返回函数关闭连接,断线由页面决定是否重连。 */
+export function openTestLiveStream(testId: number, onEvent: (event: TestLiveEvent, data: unknown) => void,
+  onError: (error: Error) => void): () => void {
+  const controller = new AbortController()
+  const token = getAuthToken()
+  void (async () => {
+    try {
+      const response = await fetch(`${import.meta.env.VITE_API_BASE_URL || '/dev-api'}/system/test/${testId}/live`, {
+        headers: token ? { Authorization: `Bearer ${token}`, Accept: 'text/event-stream' } : { Accept: 'text/event-stream' },
+        signal: controller.signal,
+      })
+      if (!response.ok || !response.body) throw new Error(response.status === 401 ? '实时流权限不足' : `实时流连接失败(${response.status})`)
+      const reader = response.body.getReader()
+      const decoder = new TextDecoder()
+      let buffer = ''
+      let eventName: TestLiveEvent = 'snapshot'
+      let data = ''
+      while (true) {
+        const chunk = await reader.read()
+        if (chunk.done) throw new Error('实时流已断开')
+        buffer += decoder.decode(chunk.value, { stream: true })
+        const lines = buffer.split(/\r?\n/)
+        buffer = lines.pop() || ''
+        for (const line of lines) {
+          if (line.startsWith('event:')) eventName = line.slice(6).trim() as TestLiveEvent
+          else if (line.startsWith('data:')) data += line.slice(5).trim()
+          else if (line === '' && data) {
+            try { onEvent(eventName, JSON.parse(data)) } catch { onError(new Error('实时流数据格式错误')) }
+            eventName = 'snapshot'
+            data = ''
+          }
+        }
+      }
+    } catch (error) {
+      if (!controller.signal.aborted) onError(error instanceof Error ? error : new Error('实时流连接失败'))
+    }
+  })()
+  return () => controller.abort()
+}

--
Gitblit v1.7.1