forked from gzt/A8000
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
733 lines
17 KiB
733 lines
17 KiB
// src/websocket/socket.ts
|
|
|
|
import { eventBus } from "@/eventBus"
|
|
|
|
// 基础消息接口
|
|
interface BaseMessage {
|
|
messageType: 'Report' // 消息类型
|
|
dataType: string // 数据类型
|
|
timestamp: number // 时间戳
|
|
}
|
|
|
|
export type OptScanModuleState = 'EMPTY' | 'PLATE_IS_READY' | 'SCANNING' | 'ERROR'
|
|
// 耗材状态消息
|
|
interface OptScanModuleStateMessage extends BaseMessage {
|
|
type: 'OptScanModuleState'
|
|
messageType: 'Report'
|
|
dataType: 'OptScanModuleState'
|
|
data: {
|
|
state: OptScanModuleState // 状态
|
|
isErrorPlate: boolean // 是否为错误板(标识当前光学模组中的反应板存在异常)
|
|
bloodType: 'WHOLE_BLOOD' | 'SERUM_OR_PLASMA' // 血液类型
|
|
sampleBarcode: string // 样本条码
|
|
userid: string // 用户ID
|
|
projInfo: ProjectInfo // 项目信息
|
|
sampleId: string // 样本ID
|
|
projId: number // 项目ID
|
|
isEmergency: boolean // 是否急诊
|
|
}
|
|
timestamp: number
|
|
}
|
|
|
|
export interface EventReport extends PromptAvailable {
|
|
typeName: 'AppPromptEvent'
|
|
timestamp: number
|
|
eventId: string
|
|
prompt: TextPrompt | FormsPrompt | TablePrompt
|
|
}
|
|
|
|
export interface IdCardReport {
|
|
typeName: 'AppIDCardMountEvent' | 'AppIDCardUnmountEvent'
|
|
projectInfo: IdCardPrompt
|
|
}
|
|
|
|
export interface PromptAvailable {
|
|
eventId?: string
|
|
prompt: EventReport['prompt']
|
|
}
|
|
|
|
export type IdCardPrompt = {
|
|
color: string,
|
|
expiryDate: number,
|
|
id: number,
|
|
lotId: string,
|
|
palteCode: number,//palteCode 字段,接口命名错了。应该是plateCode
|
|
projId: number,
|
|
projName: string,
|
|
updateChipVersion: string,
|
|
}
|
|
|
|
export type TextPrompt = {
|
|
messageLevel: MsgLevel
|
|
title: string
|
|
info: string
|
|
detailInfoType: 'Text'
|
|
detailInfo: string
|
|
}
|
|
export type FormsPrompt = {
|
|
messageLevel: MsgLevel
|
|
title: string
|
|
info: string
|
|
detailInfoType: 'Forms'
|
|
detailInfo: { name: string; description: string }[]
|
|
}
|
|
export type TablePrompt = {
|
|
messageLevel: MsgLevel
|
|
title: string
|
|
info: string
|
|
detailInfoType: 'Table'
|
|
detailInfo: {
|
|
header: string[]
|
|
types: string[]
|
|
vars: TablePromptRecord[]
|
|
}
|
|
}
|
|
|
|
export interface AlarmTextPrompt extends TextPrompt {
|
|
triggeredStateName: string
|
|
notTriggeredDisName: string
|
|
flagType: string
|
|
}
|
|
|
|
export interface AlarmTablePrompt extends TablePrompt {
|
|
triggeredStateName: string
|
|
notTriggeredDisName: string
|
|
flagType: string
|
|
}
|
|
|
|
export interface AlarmFormsPrompt extends FormsPrompt {
|
|
triggeredStateName: string
|
|
notTriggeredDisName: string
|
|
flagType: string
|
|
}
|
|
export type TablePromptRecord = {
|
|
messageLevel: MsgLevel
|
|
val: string
|
|
}[]
|
|
// 应用事件消息接口
|
|
interface AppEventMessage extends BaseMessage {
|
|
type: 'AppEvent'
|
|
messageType: 'Report'
|
|
dataType: 'AppEvent'
|
|
data: EventReport | IdCardReport | ReactionRecordReport
|
|
timestamp: number
|
|
}
|
|
|
|
export type MsgLevel = 'Info' | 'Warn' | 'Error'
|
|
export type MsgItem = {
|
|
time: number
|
|
messageLevel: MsgLevel
|
|
message: string
|
|
}
|
|
export interface FooterMessageState extends BaseMessage {
|
|
type: 'MessageBoxState'
|
|
messageType: 'Report'
|
|
dataType: 'MessageBoxState'
|
|
data: {
|
|
topMessage: MsgItem
|
|
messageBoxList: MsgItem[]
|
|
}
|
|
}
|
|
|
|
export type BloodType = 'WHOLE_BLOOD' | 'SERUM_OR_PLASMA' | 'FECES'
|
|
|
|
type WorkState = 'IDLE' | 'WORKING' | 'PAUSE'
|
|
// 设备工作状态消息
|
|
interface DeviceWorkStateMessage extends BaseMessage {
|
|
type: 'DeviceWorkState'
|
|
messageType: 'Report'
|
|
dataType: 'DeviceWorkState'
|
|
data: {
|
|
workState: WorkState // 设备工作状态
|
|
lastWorkState: WorkState
|
|
fatalErrorFlag: boolean // 致命错误标志
|
|
ecodeList: string[] // 错误代码列表
|
|
pending: boolean // 待处理状态
|
|
consumeNotEnoughErrorFlag: boolean
|
|
}
|
|
timestamp: number // 时间戳
|
|
}
|
|
|
|
export type LoginUser = {
|
|
id: number
|
|
account: string
|
|
password: string
|
|
usrRole: string
|
|
isBuiltInUser: boolean
|
|
}
|
|
export interface DeviceContextStateMessage extends BaseMessage {
|
|
type: 'DeviceContext'
|
|
messageType: 'Report'
|
|
dataType: 'DeviceContext'
|
|
data: {
|
|
loginFlag: boolean
|
|
loginUser: LoginUser
|
|
deviceInitedFlag: boolean
|
|
}
|
|
}
|
|
|
|
export type EmergencyTubeState =
|
|
| 'EMPTY'
|
|
| 'TO_BE_PROCESSED'
|
|
| 'PENDING'
|
|
| 'RESOURCE_IS_READY'
|
|
| 'PROCESSING'
|
|
| 'PROCESS_COMPLETE'
|
|
| 'ERROR'
|
|
|
|
export const emergencyStateDesc = {
|
|
EMPTY: '空',
|
|
TO_BE_PROCESSED: '待处理',
|
|
PENDING: '已挂起',
|
|
RESOURCE_IS_READY: '已就绪',
|
|
PROCESSING: '处理中',
|
|
PROCESS_COMPLETE: '完成',
|
|
ERROR: '出错',
|
|
}
|
|
|
|
export const emergencyStateColor = {
|
|
EMPTY: '',
|
|
TO_BE_PROCESSED: '#56ccab',
|
|
PENDING: '#a793ec',
|
|
RESOURCE_IS_READY: '#17b60d',
|
|
PROCESSING: '#e35958',
|
|
PROCESS_COMPLETE: '#773f16',
|
|
ERROR: '#e93527',
|
|
}
|
|
|
|
// 急诊位状态消息
|
|
interface EmergencyPosStateMessage extends BaseMessage {
|
|
type: 'EmergencyPosState'
|
|
messageType: 'Report'
|
|
dataType: 'EmergencyPosState'
|
|
data: {
|
|
tube: {
|
|
sampleId: string | null // 样本ID
|
|
pos: number // 位置
|
|
isHighTube: boolean // 是否为高试管
|
|
isEmergency: boolean // 是否为急诊
|
|
bloodType: BloodType // 血液类型
|
|
sampleBarcode: string // 样本条码
|
|
userid: string // 用户ID
|
|
projInfo: ProjectInfo[] // 项目信息列表
|
|
projIds: number[] // 项目ID列表
|
|
state: EmergencyTubeState // 状态
|
|
errors: string[] // 错误信息列表
|
|
}
|
|
}
|
|
timestamp: number
|
|
}
|
|
|
|
export type RunningTubeState =
|
|
| 'EMPTY'
|
|
| 'TO_BE_PROCESSED'
|
|
| 'PENDING'
|
|
| 'RESOURCE_IS_READY'
|
|
| 'PROCESSING'
|
|
| 'PROCESSED'
|
|
| 'PROCESS_COMPLETE'
|
|
| 'ERROR'
|
|
|
|
export type TubeHolderType =
|
|
| 'BloodTube'
|
|
| 'MiniTube'
|
|
| 'MiniBlood'
|
|
| 'BulletTube1P5'
|
|
| 'BulletTube0P5'
|
|
| 'StoolTestTube'
|
|
|
|
// 试管架状态消息
|
|
interface TubeHolderStateMessage extends BaseMessage {
|
|
type: 'TubeHolderState'
|
|
messageType: 'Report'
|
|
dataType: 'TubeHolderState'
|
|
data: {
|
|
tubeHolderType: TubeHolderType
|
|
tubes: Array<{
|
|
sampleId: string | null
|
|
pos: number
|
|
isHighTube: boolean
|
|
isEmergency: boolean
|
|
bloodType: BloodType
|
|
sampleBarcode: string
|
|
userid: string
|
|
projInfo: ProjectInfo[]
|
|
projIds: number[]
|
|
state: RunningTubeState
|
|
errors: string[]
|
|
erroInfo: string
|
|
}>
|
|
state: string
|
|
}
|
|
timestamp: number
|
|
}
|
|
|
|
export interface TubeSetting {
|
|
tubeIndex: number
|
|
userid: string
|
|
sampleBarcode: string
|
|
projId: number[]
|
|
bloodType: BloodType
|
|
}
|
|
export interface TestTubeRack {
|
|
uuid: string
|
|
state: 'INACTIVE' | 'ACTIVE' | 'LOCKED'
|
|
tubeSettings: TubeSetting[]
|
|
}
|
|
|
|
export interface TubeHolderSettingMessage extends BaseMessage {
|
|
type: 'TubeHolderSetting'
|
|
messageType: 'Report'
|
|
dataType: 'TubeHolderSetting'
|
|
data: TestTubeRack[]
|
|
}
|
|
|
|
// 传感器状态消息
|
|
interface SensorStateMessage extends BaseMessage {
|
|
type: 'SensorState'
|
|
messageType: 'Report'
|
|
dataType: 'SensorState'
|
|
data: {
|
|
pboxTemperature: number //板夹仓温度
|
|
incubateBoxTemperature: number // 孵育盘温度
|
|
wasteBinFullFlag: boolean // 废废料仓状态
|
|
}
|
|
timestamp: number
|
|
}
|
|
|
|
// 项目信息接口
|
|
interface ProjectInfo {
|
|
projId: number
|
|
projName: string
|
|
projShortName: string
|
|
color: string
|
|
supportBloodTypes: string[]
|
|
}
|
|
|
|
export type SubTankState =
|
|
| 'EMPTY'
|
|
| 'RESERVED'
|
|
| 'WAITING_FOR_DROP'
|
|
| 'INCUBATING'
|
|
| 'INCUBATION_COMPLETE'
|
|
| 'ERROR'
|
|
// 子槽位信息接口
|
|
export interface SubTank {
|
|
pos: string // 位置编号 SPACE01-SPACE20
|
|
state: SubTankState // 槽位状态
|
|
bloodType: BloodType // 血液类型
|
|
sampleBarcode: string // 样本条码
|
|
lotId: string
|
|
userid: string // 用户ID
|
|
projInfo: ProjectInfo // 项目信息
|
|
sampleId: string // 样本ID
|
|
projId: number // 项目ID
|
|
startIncubatedTime: number // 开始孵育时间
|
|
incubatedTimeSec: number // 孵育时间(秒)
|
|
remainTimeSec: number //剩余孵育时间(显示)
|
|
errors: string[] // 错误信息列表
|
|
isEmergency: boolean //是否急诊位
|
|
errorInfo: SubTankError
|
|
// isPlaceholder?: boolean // 是否为占位符
|
|
}
|
|
|
|
interface DetailInfo {
|
|
description: string
|
|
name: string
|
|
}
|
|
|
|
interface SubTankError {
|
|
detailInfo: DetailInfo[]
|
|
detailInfoType: string
|
|
info: string
|
|
messageLevel: string
|
|
title: string
|
|
}
|
|
|
|
// 孵育板状态消息
|
|
interface IncubationPlateStateMessage extends BaseMessage {
|
|
type: 'IncubationPlateState'
|
|
messageType: 'Report'
|
|
dataType: 'IncubationPlateState'
|
|
data: {
|
|
subtanks: SubTank[] // 20个子槽位信息
|
|
}
|
|
timestamp: number
|
|
}
|
|
|
|
// ��材组信息基础接口
|
|
export interface ConsumableGroupBase {
|
|
projId: number
|
|
projName: string
|
|
projShortName: string
|
|
lotId: string
|
|
color: string
|
|
num: number
|
|
isInstall: boolean
|
|
reserveNum: number
|
|
group: string
|
|
}
|
|
|
|
export interface ReactionPlateGroup extends Partial<ConsumableGroupBase> {}
|
|
|
|
// 小缓冲液接口
|
|
interface LittleBottleGroup extends Partial<ConsumableGroupBase> {}
|
|
|
|
// 大缓冲液接口
|
|
interface LargeBottleGroup extends Partial<ConsumableGroupBase> {}
|
|
|
|
// Tips信息接口
|
|
interface TipInfo {
|
|
tipNum: number
|
|
totalNum?: number
|
|
}
|
|
|
|
// 耗材状态消息接口 consumableStore
|
|
interface ConsumablesStateMessage extends BaseMessage {
|
|
type: 'ConsumablesState'
|
|
messageType: 'Report'
|
|
dataType: 'ConsumablesState'
|
|
data: {
|
|
tips: TipInfo[]
|
|
reactionPlateGroup: ReactionPlateGroup[]
|
|
littBottleGroup: LittleBottleGroup[]
|
|
larBottleGroup: LargeBottleGroup[]
|
|
}
|
|
timestamp: number
|
|
}
|
|
|
|
// 消息类型联合
|
|
type WebSocketMessage =
|
|
| OptScanModuleStateMessage
|
|
| DeviceWorkStateMessage
|
|
| DeviceContextStateMessage
|
|
| EmergencyPosStateMessage
|
|
| TubeHolderStateMessage
|
|
| TubeHolderSettingMessage
|
|
| SensorStateMessage
|
|
| IncubationPlateStateMessage
|
|
| AppEventMessage
|
|
| FooterMessageState
|
|
| ConsumablesStateMessage
|
|
| AppFlagStateListMessage
|
|
| PreReactionPosGroupStateMessage
|
|
|
|
// 消息处理器类型
|
|
type MessageHandler<T extends WebSocketMessage> = (data: T['data']) => void
|
|
|
|
class WebSocketClient {
|
|
private ws: WebSocket | null = null
|
|
private url: string
|
|
private reconnectAttempts: number = -1
|
|
private maxReconnectAttempts: number = 5
|
|
private reconnectInterval: number = 3000
|
|
|
|
// 使用类型安全的消息处理器映射
|
|
private messageHandlers: Map<
|
|
WebSocketMessage['dataType'],
|
|
Set<MessageHandler<any>>
|
|
> = new Map()
|
|
|
|
constructor(url: string) {
|
|
this.url = url
|
|
}
|
|
|
|
// 类型安全的订阅方法
|
|
subscribe<T extends WebSocketMessage>(
|
|
messageType: T['dataType'],
|
|
handler: MessageHandler<T>,
|
|
): void {
|
|
if (!this.messageHandlers.has(messageType)) {
|
|
console.log(
|
|
'🚀 ~ WebSocketClient ~ subscribe ~ messageType:',
|
|
messageType,
|
|
)
|
|
this.messageHandlers.set(messageType, new Set())
|
|
}
|
|
console.log('messageType---', messageType)
|
|
this.messageHandlers.get(messageType)?.add(handler)
|
|
}
|
|
|
|
// 类型安全的取消订阅方法
|
|
unsubscribe<T extends WebSocketMessage>(
|
|
messageType: T['dataType'],
|
|
handler: MessageHandler<T>,
|
|
): void {
|
|
this.messageHandlers.get(messageType)?.delete(handler)
|
|
}
|
|
|
|
private handleMessage(message: WebSocketMessage): void {
|
|
const handlers = this.messageHandlers.get(message.dataType)
|
|
if (!handlers) {
|
|
// if (message.dataType === 'MessageBoxState') {
|
|
// console.log(
|
|
// '🚀 ~ WebSocketClient ~ handleMessage ~ handlers is undefined for message type:',
|
|
// message.dataType,
|
|
// )
|
|
// }
|
|
return
|
|
}
|
|
handlers.forEach((handler) => {
|
|
try {
|
|
handler(message.data)
|
|
} catch (error) {
|
|
console.error(`处理 ${message.dataType} 消息时出错:`, error)
|
|
}
|
|
})
|
|
}
|
|
|
|
// 连接 WebSocket
|
|
connect(): void {
|
|
try {
|
|
// WebSocket.CONNECTING (0) WebSocket.OPEN (1)
|
|
if (this.ws && this.ws.readyState <= 1) {
|
|
// 已连接
|
|
console.log(`${this.url} 正在连接或已连接,无需重复连接`)
|
|
} else {
|
|
this.ws = new WebSocket(this.url)
|
|
this.bindEvents()
|
|
}
|
|
} catch (error) {
|
|
console.error('WebSocket 连接失败:', error)
|
|
this.reconnect()
|
|
}
|
|
}
|
|
|
|
// 绑定事件
|
|
private bindEvents(): void {
|
|
if (!this.ws) return
|
|
|
|
// 连接建立时的处理
|
|
this.ws.onopen = () => {
|
|
if (this.reconnectAttempts !== -1) {
|
|
// 从断开连接 进入 恢复连接
|
|
this.reconnectAttempts = -1
|
|
eventBus.emit('socketClosed', -1)
|
|
}
|
|
console.log('WebSocket 连接已建立')
|
|
this.reconnectAttempts = -1 // 重置重连次数
|
|
}
|
|
|
|
// 接收消息的处理
|
|
this.ws.onmessage = (event: MessageEvent) => {
|
|
try {
|
|
const data = JSON.parse(event.data)
|
|
// console.log('🚀 ~ WebSocketClient ~ bindEvents ~ data:', data)
|
|
this.handleMessage(data)
|
|
} catch (error) {
|
|
console.error('消息解析错误:', error)
|
|
}
|
|
}
|
|
|
|
// 连接关闭时的处理
|
|
this.ws.onclose = () => {
|
|
console.log('WebSocket 连接已关闭')
|
|
eventBus.emit('socketClosed', 0)
|
|
this.reconnect()
|
|
}
|
|
|
|
// 错误处理
|
|
this.ws.onerror = (error) => {
|
|
console.error('WebSocket 错误:', error)
|
|
}
|
|
}
|
|
|
|
// 重连机制
|
|
private reconnect(): void {
|
|
if (this.reconnectAttempts === -1) {
|
|
this.reconnectAttempts = 0
|
|
}
|
|
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
eventBus.emit('socketClosed', this.maxReconnectAttempts)
|
|
console.log('达到最大重连次数,停止重连')
|
|
this.reconnectAttempts = -1
|
|
return
|
|
}
|
|
|
|
setTimeout(() => {
|
|
console.log(`尝试第 ${this.reconnectAttempts + 1} 次重连...`)
|
|
this.reconnectAttempts++
|
|
this.connect()
|
|
}, this.reconnectInterval)
|
|
}
|
|
|
|
// 关闭连接
|
|
disconnect(): void {
|
|
if (this.ws) {
|
|
this.ws.close()
|
|
this.ws = null
|
|
}
|
|
}
|
|
}
|
|
|
|
const urlSocketMap = new Map()
|
|
|
|
// 导出消息类型
|
|
// 导出消息类型
|
|
export type {
|
|
WebSocketMessage,
|
|
OptScanModuleStateMessage,
|
|
DeviceWorkStateMessage,
|
|
EmergencyPosStateMessage,
|
|
TubeHolderStateMessage,
|
|
SensorStateMessage,
|
|
IncubationPlateStateMessage,
|
|
ProjectInfo,
|
|
AppEventMessage,
|
|
ConsumablesStateMessage,
|
|
LittleBottleGroup,
|
|
LargeBottleGroup,
|
|
TipInfo,
|
|
AppFlagStateListMessage,
|
|
PreReactionPosGroupStateMessage,
|
|
}
|
|
|
|
// 导出 WebSocket 客户端
|
|
export const createWebSocket = (url: string): WebSocketClient => {
|
|
if (urlSocketMap.has(url)) {
|
|
return urlSocketMap.get(url)
|
|
} else {
|
|
const client = new WebSocketClient(url)
|
|
urlSocketMap.set(url, client)
|
|
return client
|
|
}
|
|
}
|
|
|
|
// 使用示例:
|
|
/*
|
|
import { createWebSocket } from './websocket/socket';
|
|
import { ProjectInfo } from '../types/Index/Emergency';
|
|
|
|
// 创建 WebSocket 连接
|
|
const ws = createWebSocket('ws://your-websocket-server-url');
|
|
ws.connect();
|
|
|
|
// 在组件销毁时断开连接
|
|
onUnmounted(() => {
|
|
ws.disconnect();
|
|
});
|
|
*/
|
|
|
|
export type WarningState = {
|
|
state: boolean
|
|
keyName: string
|
|
cnName: string
|
|
errorDetailInfo?: TextPrompt | FormsPrompt | TablePrompt
|
|
errorPromptInfo: TextPrompt | FormsPrompt | TablePrompt
|
|
triggeredStateName: string
|
|
notTriggeredDisName: string
|
|
flagType: string
|
|
}
|
|
|
|
export type AppFlagStateList = {
|
|
hasErrorTriggerFlag: boolean
|
|
hasWarningTriggerFlag: boolean
|
|
states: WarningState[]
|
|
}
|
|
|
|
export type TankInfo = {
|
|
state: string
|
|
projId: string
|
|
bloodType: string
|
|
errorInfo?: ErrorPromptInfo
|
|
}
|
|
|
|
export type Table = {
|
|
messageLevel: string,
|
|
title: string,
|
|
info: string,
|
|
detailInfoType: string,
|
|
detailInfo: TableDetailInfo
|
|
}
|
|
|
|
export type TableDetailInfo = {
|
|
header: string[],
|
|
types: string[],
|
|
vars: vars[][]
|
|
}
|
|
|
|
type vars = {
|
|
messageLevel: string
|
|
val: string
|
|
}
|
|
|
|
type Forms = {
|
|
name: string
|
|
description: string
|
|
}
|
|
|
|
export type ErrorPromptInfo = {
|
|
detailInfo: TableDetailInfo | Forms | string
|
|
detailInfoType: string
|
|
info: string
|
|
messageLevel: string
|
|
title: string
|
|
state: string
|
|
}
|
|
|
|
interface AppFlagStateListMessage extends BaseMessage {
|
|
type: 'AppFlagStateList'
|
|
messageType: 'Report'
|
|
dataType: 'AppFlagStateList'
|
|
data: AppFlagStateList
|
|
}
|
|
interface SampleInfo {
|
|
sampleId: string
|
|
isHighTube: boolean
|
|
isEmergency: boolean
|
|
bloodType: string
|
|
sampleBarcode: string
|
|
userid: string
|
|
}
|
|
interface Grids {
|
|
state: string
|
|
consumableType?: string
|
|
group: string
|
|
posIndex: number
|
|
startReactionTime: number
|
|
reactionRemainingTime: number
|
|
totalReactionTime: number
|
|
sampleInfo: SampleInfo
|
|
bindIncubatorPos?: string | null
|
|
}
|
|
|
|
interface ProjBriefInfo {
|
|
projId: number
|
|
projName: string
|
|
projShortName: string
|
|
color: string
|
|
}
|
|
|
|
export interface PreReactionPosGroupState {
|
|
grids: Grids[]
|
|
consumableType?: string
|
|
group: string
|
|
hasSomeGridInReacting: boolean
|
|
hasSomeGridReactedCompleted: boolean
|
|
isInstalled: boolean
|
|
version: string
|
|
projBriefInfo: ProjBriefInfo
|
|
inReacting: boolean
|
|
}
|
|
|
|
export interface PreReactionPosGroupStateMap {
|
|
[key: string]: PreReactionPosGroupState
|
|
}
|
|
|
|
interface PreReactionPosGroupStateMessage extends BaseMessage {
|
|
type: 'PreReactionPosGroupState'
|
|
messageType: 'Report'
|
|
dataType: 'PreReactionPosGroupState'
|
|
data: PreReactionPosGroupState
|
|
}
|
|
|
|
type reactionRecordItem = {
|
|
id: string
|
|
}
|
|
|
|
export type ReactionRecordReport = {
|
|
typeName: 'AppNewReactionRecordEvent',
|
|
reactionRecord: reactionRecordItem
|
|
}
|
|
|
|
export type StatusDetailReport = TextPrompt | FormsPrompt | TablePrompt
|