| | |
| | | import type { ReceptionStatus, TestRecord, TestStatus } from '@/stores/tests' |
| | | import { getAuthToken, request, type ApiResponse } from './request' |
| | | import { formatBusinessTime } from '@/utils/businessTime' |
| | | |
| | | export type TestStatusCode = 'WAITING' | 'RUNNING' | 'PAUSED' | 'RUNNING_ABNORMAL' | 'COMPLETION_PENDING' | 'COMPLETED' | 'TERMINATED' |
| | | export type TestStatusCode = 'WAITING' | 'RUNNING' | 'PAUSED' | 'COMPLETED' |
| | | export type ReceptionStatusCode = 'NOT_STARTED' | 'RECEIVING' | 'PAUSED' | 'ENDED' |
| | | |
| | | export interface TestListParams { |
| | |
| | | |
| | | export interface TestBatteryPayload { |
| | | relationId?: number |
| | | testBatteryId?: number |
| | | groupId: number |
| | | groupName?: string | null |
| | | batteryNo: string |
| | | collectorId: number |
| | | collectorName?: string | null |
| | | binding: { bindingId?: number; channelId: number; expectedBarcode: string | null; claimStatus?: string; cabinetCode?: string | null; cabinetName?: string | null; displayNo?: string | null } |
| | | binding: { bindingId?: number; channelId: number; expectedBarcode: string | null; claimStatus?: string; cabinetCode?: string | null; cabinetName?: string | null; displayNo?: string | null; devtype?: number | null; devid?: number | null; subdevid?: number | null; chlid?: number | null } |
| | | collectorChannels: Array<{ |
| | | mappingId?: number |
| | | measurementCode: string |
| | | measurementName: string |
| | | channelCode: string |
| | | }> |
| | | fields: Array<{ fieldId?: number; fieldName: string; fieldValue: string }> |
| | | fields: Array<{ fieldId?: number; fieldDefinitionId?: number; fieldName: string; fieldType?: string; fieldValue: string }> |
| | | } |
| | | |
| | | export interface TestThresholdPayload { |
| | |
| | | export interface TestConfigurationDetail extends TestConfigurationPayload { |
| | | testId: number |
| | | creatorId: number |
| | | createTime?: number | null |
| | | updateTime?: number | null |
| | | creatorName: string | null |
| | | principalName: string | null |
| | | testTypeName: string | null |
| | | testStatus: TestStatusCode |
| | | receptionStatus: ReceptionStatusCode |
| | | fieldSnapshots: Array<{ |
| | | startedAtUtc?: number | null |
| | | endedAtUtc?: number | null |
| | | /** 历史兼容字段;新接口不返回测试字段快照,字段从 batteries[].fields 读取。 */ |
| | | fieldSnapshots?: Array<{ |
| | | snapshotId: number |
| | | fieldDefinitionId: number |
| | | fieldName: string |
| | |
| | | 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 |
| | | }> |
| | | logicStepMappings: TestLogicStepMapping[] |
| | | consoleDetail?: TestConsoleDetail | null |
| | | } |
| | | |
| | | export interface TestConsoleDetail { |
| | | experimentName: string | null |
| | | batteryFields: string[] |
| | | specialFields: string[] |
| | | fieldValues: Record<string, Record<string, string>> |
| | | } |
| | | |
| | | export interface TestLogicStepMapping { |
| | | logicalNo: number |
| | | logicalName: string |
| | | startStepNo: number |
| | | endStepNo: number |
| | | } |
| | | |
| | | export interface TestConfigurationOptions { |
| | |
| | | testStatus: TestStatusCode | null |
| | | receptionStatus: ReceptionStatusCode | null |
| | | batteryNo: string | null |
| | | createTime: string | null |
| | | startedAtUtc: string | null |
| | | endedAtUtc: string | null |
| | | createTime: number | null |
| | | startedAtUtc: number | null |
| | | endedAtUtc: number | null |
| | | runtimeAttach?: boolean | null |
| | | remark: string | null |
| | | batteryCount: number | null |
| | |
| | | interface TestOptionsResponse extends ApiResponse { data: TestConfigurationOptions } |
| | | export interface TestListResult { total: number; rows: TestRecord[] } |
| | | |
| | | const statusMap: Record<TestStatusCode, TestStatus> = { |
| | | WAITING: '待测试', RUNNING: '检测中', PAUSED: '暂停', RUNNING_ABNORMAL: '检测中(异常)', |
| | | COMPLETION_PENDING: '完成确认中', COMPLETED: '检测完成', TERMINATED: '检测完成', |
| | | export const testStatusLabels: Record<string, TestStatus> = { |
| | | WAITING: '待测试', RUNNING: '检测中', PAUSED: '暂停', COMPLETED: '检测完成', |
| | | // 历史数据只作兼容展示,不会由新流程写入。 |
| | | RUNNING_ABNORMAL: '检测中', COMPLETION_PENDING: '检测完成', TERMINATED: '检测完成', |
| | | } |
| | | const receptionStatusMap: Record<ReceptionStatusCode, ReceptionStatus> = { |
| | | NOT_STARTED: '未开始接收', RECEIVING: '接收中', PAUSED: '暂停接收', ENDED: '已结束接收', |
| | | } |
| | | const legacyStatusMap: Record<string, TestStatusCode> = { |
| | | '0': 'WAITING', '1': 'RUNNING', '2': 'RUNNING_ABNORMAL', '3': 'COMPLETED', |
| | | } |
| | | |
| | | function mapTest(item: TestDto): TestRecord { |
| | | const status = item.testStatus ? statusMap[item.testStatus] : '未知' |
| | | const status = item.testStatus ? testStatusLabels[item.testStatus] : '未知' |
| | | const receptionStatus = item.receptionStatus ? receptionStatusMap[item.receptionStatus] : '未知' |
| | | return { |
| | | id: item.testId, name: item.testName || '-', type: item.testTypeName || '-', batteryNo: item.batteryNo || '-', batteryGroup: '-', |
| | | batteryCount: Number(item.batteryCount) || 0, createdAt: item.createTime || '-', startedAt: item.startedAtUtc || '-', |
| | | endedAt: item.endedAtUtc || '-', purpose: item.testPurpose || '-', owner: item.principalName || '-', |
| | | batteryCount: Number(item.batteryCount) || 0, createdAt: formatBusinessTime(item.createTime), startedAt: formatBusinessTime(item.startedAtUtc), |
| | | endedAt: formatBusinessTime(item.endedAtUtc), purpose: item.testPurpose || '-', owner: item.principalName || '-', |
| | | remark: item.remark || '-', status, receptionStatus, editable: item.receptionStatus === 'NOT_STARTED', |
| | | } |
| | | } |
| | |
| | | const searchParams = new URLSearchParams() |
| | | Object.entries(params).forEach(([key, value]) => { |
| | | if (value === undefined || value === '') return |
| | | if (key === 'status') searchParams.set('testStatus', legacyStatusMap[String(value)] || String(value)) |
| | | if (key === 'status') searchParams.set('testStatus', String(value)) |
| | | else searchParams.set(key, String(value)) |
| | | }) |
| | | const result = await request<TestListResponse>(`/system/test/list?${searchParams.toString()}`) |
| | |
| | | await request<ApiResponse>(`/system/test/${testIds.join(',')}`, { method: 'DELETE' }) |
| | | } |
| | | |
| | | /** 原子开始接收;后端确认全部来源后才会改变正式状态。 */ |
| | | /** 建立测试接收边界;来源离线不阻断状态,只有成功观测数据才进入测试事实。 */ |
| | | export async function startTestReception(testId: number): Promise<TestConfigurationDetail> { |
| | | return (await request<TestDetailResponse>(`/system/test/${testId}/reception/start`, { method: 'POST' })).data |
| | | } |
| | |
| | | latestSnapshot: { |
| | | source?: string |
| | | unit?: string |
| | | observedAt?: string |
| | | observedAt?: number |
| | | sampleIntervalMs?: number |
| | | /** 通道工作状态;读库来源下为 UNOBSERVABLE,界面按“无法观测”展示。 */ |
| | | workStatus?: string |
| | | /** 来源里最近一次采样行的设备时间(毫秒);没有采样行时为空。 */ |
| | | lastDataAt?: number | null |
| | | measurements?: Array<Record<string, unknown>> |
| | | units?: string[] |
| | | [key: string]: unknown |
| | |
| | | testId: number |
| | | testStatus: TestStatusCode |
| | | receptionStatus: ReceptionStatusCode |
| | | updatedAt: string |
| | | updatedAt: number |
| | | sources: LiveSource[] |
| | | exceptions: Array<Record<string, unknown>> |
| | | testPlan?: BtsTestSnapshot | null |
| | | } |
| | | |
| | | export interface BtsTestPlanRow { |
| | | rowType: 'STEP' | 'CYCLE' | 'END' | string |
| | | stepNo: number |
| | | cyclePosition: number | null |
| | | stepName: string |
| | | durationSeconds: number |
| | | voltageV: number | null |
| | | executionStatus: string |
| | | cycleCount: number | null |
| | | recordCount: number | null |
| | | } |
| | | |
| | | export interface BtsTestSnapshot { |
| | | currentCycleNo: number |
| | | currentStepNo: number |
| | | plannedCycleCount: number |
| | | rows: BtsTestPlanRow[] |
| | | } |
| | | |
| | | export async function saveLogicStepMappings(testId: number, mappings: TestLogicStepMapping[]): Promise<TestConfigurationDetail> { |
| | | return (await request<TestDetailResponse>(`/system/test/${testId}/logic-step-mappings`, { method: 'PUT', body: JSON.stringify(mappings) })).data |
| | | } |
| | | |
| | | 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 async function saveTestConsoleDetail(testId: number, detail: TestConsoleDetail): Promise<TestConfigurationDetail> { |
| | | return (await request<TestDetailResponse>(`/system/test/${testId}/console-detail`, { method: 'PUT', body: JSON.stringify(detail) })).data |
| | | } |
| | | |
| | | export interface PersistedTestRecord { |
| | | id: number | string |
| | | stableIdentity?: string |
| | | sampleId?: number |
| | | sourceType?: 'CABINET' | 'COLLECTOR' | 'SNAPSHOT' | string |
| | | metricCode?: string |
| | | metricValue?: number | string | null |
| | | unit?: string | null |
| | | collectorId?: number |
| | | collectorCode?: string |
| | | channelCode?: string |
| | | recordNo: string |
| | | batteryNo: string |
| | | cycleNo: number |
| | | stepNo: number |
| | | stepName: string |
| | | stepType?: string |
| | | sequenceId: number |
| | | cabinetSequenceId?: number | null |
| | | relativeTimeSeconds?: number | string | null |
| | | recordedAt?: number | null |
| | | absoluteTime?: number | null |
| | | deviceTime?: number | null |
| | | collectorDeviceTime?: number | null |
| | | captureSlot?: number | null |
| | | serverReceivedAt?: number | null |
| | | voltage: number | string | null |
| | | current: number | string | null |
| | | capacity: number | string | null |
| | | energy: number | string | null |
| | | temperature?: number | string | null |
| | | positivePotential?: number | string | null |
| | | negativePotential?: number | string | null |
| | | extensionMeasurementsJson?: string | Record<string, unknown> | null |
| | | sourceIdentity?: string |
| | | sourceRecordId?: string |
| | | deviceId: number |
| | | channelId: number |
| | | adapterId?: number |
| | | adapterCode?: string |
| | | devtype?: number |
| | | subdevid?: number |
| | | } |
| | | |
| | | export interface PersistedTestStep extends PersistedTestRecord { |
| | | actualStepLabel: string |
| | | stepDuration: number | string | null |
| | | startVoltage: number | string | null |
| | | endVoltage: number | string | null |
| | | chargeMedianVoltage: number | string | null |
| | | dischargeMedianVoltage: number | string | null |
| | | } |
| | | |
| | | export interface PersistedTestCycle extends PersistedTestRecord { |
| | | chargeCapacity: number | string | null |
| | | dischargeCapacity: number | string | null |
| | | efficiency: number | string | null |
| | | chargeEnergy: number | string | null |
| | | dischargeEnergy: number | string | null |
| | | /** 来源类别:DEVICE 设备提供、AGGREGATED 本系统按工步层聚合、IMPORTED 授权文件导入。 */ |
| | | sourceKind?: string | null |
| | | } |
| | | |
| | | // 三子页和图表查询持久化当前有效数据,不回放实时监控或触发预警判定。 |
| | | async function getPersistedTestData<T>(testId: number, kind: 'records' | 'steps' | 'cycles'): Promise<T[]> { |
| | | const result = await request<ApiResponse & { data?: T[] }>(`/system/test/${testId}/data/${kind}`) |
| | | return Array.isArray(result.data) ? result.data : [] |
| | | } |
| | | |
| | | export function getPersistedTestRecords(testId: number): Promise<PersistedTestRecord[]> { |
| | | return getPersistedTestData<PersistedTestRecord>(testId, 'records') |
| | | } |
| | | |
| | | export function getPersistedTestSteps(testId: number): Promise<PersistedTestStep[]> { |
| | | return getPersistedTestData<PersistedTestStep>(testId, 'steps') |
| | | } |
| | | |
| | | export function getPersistedTestCycles(testId: number): Promise<PersistedTestCycle[]> { |
| | | return getPersistedTestData<PersistedTestCycle>(testId, 'cycles') |
| | | } |
| | | |
| | | export interface WarningExceptionRecord { |
| | | exceptionId: number; testId: number; testName?: string; sourceType: string; sourceIdentity: string; |
| | | exceptionType: 'THRESHOLD' | 'DEVICE_OFFLINE'; |
| | | metricCode: string; abnormalValue: number | null; unit: string; ruleSnapshot: string; severity: string; |
| | | exceptionStatus: 'OPEN' | 'CLOSED'; firstOccurredAtUtc: number; lastOccurredAtUtc: number; |
| | | occurrenceCount: number; handledByName?: string; handledAtUtc?: number | null; handlingNote?: string; |
| | | } |
| | | export async function getWarningExceptions(params: { testId?: number; status?: string } = {}): Promise<WarningExceptionRecord[]> { |
| | | const query = new URLSearchParams(); if (params.testId) query.set('testId', String(params.testId)); if (params.status) query.set('status', params.status); |
| | | const result = await request<ApiResponse & { rows?: WarningExceptionRecord[] }>(`/system/test-warning-exceptions/list?${query}`) |
| | | return result.rows || [] |
| | | } |
| | | export async function closeWarningException(exceptionId: number, handlingNote: string): Promise<WarningExceptionRecord> { |
| | | const result = await request<ApiResponse & { data: WarningExceptionRecord }>(`/system/test-warning-exceptions/${exceptionId}/close`, { method: 'PUT', body: JSON.stringify({ handlingNote }) }) |
| | | return result.data |
| | | } |
| | | |
| | | export type TestLiveEvent = 'snapshot' | 'device-status' | 'reception-status' | 'test-status' | 'exception' | 'heartbeat' |
| | | |
| | | /** 使用 fetch 流读取鉴权 SSE;调用返回函数关闭连接,断线由页面决定是否重连。 */ |
| | | /** 订阅实时监控观测与提醒(接收中后端约 5 秒观测);使用鉴权 SSE,调用返回函数关闭连接。 */ |
| | | export function openTestLiveStream(testId: number, onEvent: (event: TestLiveEvent, data: unknown) => void, |
| | | onError: (error: Error) => void): () => void { |
| | | const controller = new AbortController() |
| | |
| | | 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})`) |
| | | if (!response.ok || !response.body) throw new Error([401, 403].includes(response.status) ? '实时流权限不足' : `实时流连接失败(${response.status})`) |
| | | if (!response.headers.get('content-type')?.includes('text/event-stream')) throw new Error('实时流响应格式错误') |
| | | const reader = response.body.getReader() |
| | | const decoder = new TextDecoder() |
| | | let buffer = '' |