|
|
<template> <div id="running-container"> <div class="circular-loader"> <!-- 中心元素 --> <div class="center-square" :style="getCenterSquareStyle()"> <template v-if="selectedItem"> <span>{{ selectedItem.sampleBarcode || '无条码' }}</span> <span>{{ selectedItem.userid || '无患者ID' }}</span> <span>{{ selectedItem.projInfo.projName || '无项目' }}</span> <span>{{ selectedItem.projInfo.lotId ?? '无批次号' }}</span> <span>{{ getBloodTypeLabel(selectedItem.bloodType) || '无类型' }}</span> <span>{{ getRemainingTime(selectedItem) }}</span> </template> <template v-else> </template> </div>
<!-- 外圈的矩形元素 --> <div v-for="(item, index) in incubationPlates" :key="item.sampleId" class="rectangular-item" :style="[getRotationStyle(item, index), getItemStyle(item)]" @click="!item.isPlaceholder && toggleSelectItem(item,index)"> <template v-if="item.isPlaceholder"> <!-- 占位符 --> <span class="placeholder-text">空位</span> </template> <template v-else> <!-- 正常数据 --> <span class="project-name">{{ item.projInfo.projShortName || '无项目' }}</span> <span class="barcode">{{ item.sampleBarcode || '无条码' }}</span> <div class="emergency-icon" v-if="item.pos === 'EMERGENCY'"> <img class="icon" src="@/assets/emergency.svg" /> <span class="text">急</span> </div> <span class="time">{{ getRemainingTime(item) }}</span> </template> <div v-show="currentIndex == index" :style="`margin-top:${item.pos=='EMERGENCY'?'20px':'75px'}`" class="quan">{{ index +1 }}</div> </div> </div> <div class="consumables-container"> <!-- 第一行 --> <div class="row-first"> <!-- 急诊按钮 --> <div class="emergency-button" @click="showEmergencyAlert = !showEmergencyAlert"> <span>急诊</span> </div> <!-- 试管架区域 --> <div class="test-tube-rack-area"> <div class="tube-project-tab"> <tube-item :tube="sampleTube" :index="0" :projects="sampleProjects" :bloodTypes="sampleBloodTypes" /> </div> <div class="tube-items"> <!-- <SampleDisplay :samples="tubeHolderState.tubes" :selectedSamples="selectedSamples" @updateSelectedSamples="updateSelectedSamples" /> --> <div class="tube-container"> <template v-for="(tube, index) in tubes" :key="index"> <tube-item :tube="tube" :index="index" :projects="projects" :bloodTypes="bloodTypes" /> </template> </div> </div> </div> </div> <!-- 第二行 --> <div class="row-second"> <!-- 反应板区域 --> <div class="plates-area"> <PlateDisplay :projects="consumablesStore.plates" /> </div> <!-- 小缓冲液区域 --> <div class="little-buffer-liquid"> <LittleBufferDisplay :bufferData="consumablesStore.bufferLittles" /> </div> <!-- tips 大缓冲液区域 --> <div class="tips-and-big-buffer"> <div class="tips-item"> <div class="tip-fill" :style="getFillStyle(consumablesStore.moveLiquids[0])"></div> <div class="tip-text"> {{ consumablesStore.moveLiquids[0].tipNum }}/120 </div> </div> <BallGrid :total="6" :customColors="true" width="160px" height="110px" :data="consumablesStore.bufferBig" :columns="3" class="buffer-grid" /> </div> <!-- 废料区域 --> <div class="waste-area" :style="getWasteStyle()"> <div class="waste-text">废料箱</div> </div> </div> </div> </div> <!-- 弹窗提示 --> <teleport to="body"> <div v-if="showWasteAlert" class="alert-overlay"> <div class="alert-container"> <div class="alert-title">废料箱已满</div> <div class="alert-message">请尽快清理废料箱</div> <el-button type="danger" @click="closeAlert" class="alert-button">确认</el-button> </div> </div> </teleport> <!-- 急诊弹窗提示 --> <teleport to="body"> <div v-if="showEmergencyAlert" class="alert-overlay"> <div class="alert-container"> <div class="alert-icon"> <img class="icon" src="@/assets/emergency.svg" /> <span>急</span> </div> <div class="alert-message">确认要添加急诊吗?</div> <div class="action-buttons"> <el-button type="info" @click="cancelEmergency" class="confirm-button">取消</el-button> <el-button type="primary" @click="confirmEmergency" class="cancel-button">确认</el-button> </div> </div> </div> </teleport> <EmergencyResultDialog :result="emergencyResult!" :visible="isDialogVisible" @update:visible="confirmEmergencyWarn" /> </template>
<script setup lang="ts"> import { ref, onMounted, onUnmounted, watch, onActivated, onDeactivated } from 'vue' import { useRouter, useRoute } from 'vue-router' import { useTestTubeStore, useConsumablesStore } from '../../../store' import { getBloodTypeLabel, processTubeSettings } from '../Utils' import { SampleDisplay, PlateDisplay, LittleBufferDisplay, EmergencyResultDialog, } from '../Components' import tubeItem from '../components/TestTube/Tube.vue' import BallGrid from '../Components/Consumables/BallGrid.vue' import { wasteArea, getTubeRackState } from '../../../services/index' import type { Subtank, TubeRackInfo } from '../../../types/Index' import { getRunningList } from '../../../services/Index/running/running' import { createWebSocket } from '../../../websocket/socket' import type { TubeHolderStateMessage, IncubationPlateStateMessage, EmergencyPosStateMessage, ProjectInfo } from '../../../websocket/socket' import { getServerInfo } from '../../../utils/getServerInfo' import { useEmergencyStore } from '../../../store/modules/emergency' const { wsUrl } = getServerInfo('/api/v1/app/ws/state') const ws = createWebSocket(wsUrl) const testTubeStore = useTestTubeStore() const emergencyStore = useEmergencyStore() const consumablesStore = useConsumablesStore() const router = useRouter() const route = useRoute()
// 定义试管架状态
const tubeRack = ref<TubeRackInfo>({ tubes: [], // 默认空试管数据
state: 'IDLE', hasTubeToBeProcessed: false, }) // 假设这是10条真实数据
// 模拟10条真实数据
const tubes = [ { userid: 'user1', projId: [1], bloodType: 'A' }, { userid: 'user2', projId: [2], bloodType: 'B' }, { userid: 'user3', projId: [1, 2], bloodType: 'AB' }, { userid: 'user4', projId: [], bloodType: 'O' }, { userid: 'user5', projId: [3], bloodType: 'A' }, { userid: 'user6', projId: [4], bloodType: 'B' }, { userid: 'user7', projId: [3, 4], bloodType: 'AB' }, { userid: 'user8', projId: [], bloodType: 'O' }, { userid: 'user9', projId: [5], bloodType: 'A' }, { userid: 'user10', projId: [6], bloodType: 'B' } ]
// 项目数据
const projects = [ { projId: 1, projName: 'Project Alpha', color: 'red' }, { projId: 2, projName: 'Project Beta', color: 'blue' }, { projId: 3, projName: 'Project Gamma', color: 'green' }, { projId: 4, projName: 'Project Delta', color: 'yellow' }, { projId: 5, projName: 'Project Epsilon', color: 'purple' }, { projId: 6, projName: 'Project Zeta', color: 'orange' } ]
// 血型数据
const bloodTypes = [ { key: 'A', name: 'Type A' }, { key: 'B', name: 'Type B' }, { key: 'AB', name: 'Type AB' }, { key: 'O', name: 'Type O' } ]
// 示例数据
const sampleTube = { userid: 'user123', projId: [1, 2], bloodType: 'A' }
const sampleProjects = [ { projId: 1, projName: 'Project One', color: 'red' }, { projId: 2, projName: 'Project Two', color: 'blue' } ]
const sampleBloodTypes = [ { key: 'A', name: 'Type A' }, { key: 'B', name: 'Type B' } ]
// 修改试管架状态的类型定义
interface TubeHolderState { tubeHolderType: string tubes: Array<{ sampleId: string | null pos: number isHighTube: boolean isEmergency: boolean bloodType: string sampleBarcode: string userid: string projInfo: any[] projIds: number[] state: string errors: string[] }> state: string }
// 试管架状态
const tubeHolderStateData = ref<TubeHolderState>({ tubeHolderType: '', tubes: [], state: 'IDLE' }) //处理试管架状态
const handleTubeHolderStateMessage = (data: TubeHolderStateMessage['data']) => { // console.log('试管架状态:', data)
tubeHolderStateData.value = data } //处理孵育盘状态
const handleIncubationPlateStateMessage = (data: IncubationPlateStateMessage['data']) => { console.log('孵育盘状态:', data) // incubationPlates.value = data.subtanks
incubationPlates.value =[ { pos: "A1", state: "REACTED", bloodType: "WHOLE_BLOOD", sampleBarcode: "BC001", userid: "U001", projInfo: { projId: 1, projName: "血液疾病检测项目", projShortName: "项目1", color: "red" }, sampleId: "S001", projId: 1, startIncubatedTime: 1612345678, incubatedTimeSec: 1800, errors: [] }, { pos: "A2", state: "REACTED", bloodType: "PLASMA", sampleBarcode: "BC002", userid: "U002", projInfo: { projId: 1, projName: "血液疾病检测项目", projShortName: "项目1", color: "red" }, sampleId: "S002", projId: 1, startIncubatedTime: 1612345778, incubatedTimeSec: 2100, errors: [] }, { pos: "A3", state: "PARTIAL_REACTION", bloodType: "SERUM", sampleBarcode: "BC003", userid: "U003", projInfo: { projId: 1, projName: "血液疾病检测项目", projShortName: "项目1", color: "red" }, sampleId: "S003", projId: 1, startIncubatedTime: 1612345878, incubatedTimeSec: 1900, errors: [] }, { pos: "B1", state: "REACTED", bloodType: "WHOLE_BLOOD", sampleBarcode: "BC004", userid: "U004", projInfo: { projId: 2, projName: "血液成分分析项目", projShortName: "项目2", color: "blue" }, sampleId: "S004", projId: 2, startIncubatedTime: 1612345978, incubatedTimeSec: 2200, errors: [] }, { pos: "B2", state: "NO_REACTION", bloodType: "PLASMA", sampleBarcode: "BC005", userid: "U005", projInfo: { projId: 2, projName: "血液成分分析项目", projShortName: "项目2", color: "blue" }, sampleId: "S005", projId: 2, startIncubatedTime: 1612346078, incubatedTimeSec: 2000, errors: [] }, { pos: "B3", state: "REACTED", bloodType: "SERUM", sampleBarcode: "BC006", userid: "U006", projInfo: { projId: 2, projName: "血液成分分析项目", projShortName: "项目2", color: "blue" }, sampleId: "S006", projId: 2, startIncubatedTime: 1612346178, incubatedTimeSec: 2300, errors: [] }, { pos: "C1", state: "PARTIAL_REACTION", bloodType: "WHOLE_BLOOD", sampleBarcode: "BC007", userid: "U007", projInfo: { projId: 3, projName: "特殊血液指标检测项目", projShortName: "项目3", color: "green" }, sampleId: "S007", projId: 3, startIncubatedTime: 1612346278, incubatedTimeSec: 1950, errors: [] }, { pos: "C2", state: "REACTED", bloodType: "PLASMA", sampleBarcode: "BC008", userid: "U008", projInfo: { projId: 3, projName: "特殊血液指标检测项目", projShortName: "项目3", color: "green" }, sampleId: "S008", projId: 3, startIncubatedTime: 1612346378, incubatedTimeSec: 2150, errors: [] }, { pos: "C3", state: "NO_REACTION", bloodType: "SERUM", sampleBarcode: "BC009", userid: "U009", projInfo: { projId: 3, projName: "特殊血液指标检测项目", projShortName: "项目3", color: "green" }, sampleId: "S009", projId: 3, startIncubatedTime: 1612346478, incubatedTimeSec: 2050, errors: [] }, { pos: "D1", state: "REACTED", bloodType: "WHOLE_BLOOD", sampleBarcode: "BC010", userid: "U010", projInfo: { projId: 1, projName: "血液疾病检测项目", projShortName: "项目1", color: "red" }, sampleId: "S010", projId: 1, startIncubatedTime: 1612346578, incubatedTimeSec: 2250, errors: [] }, { pos: "D2", state: "PARTIAL_REACTION", bloodType: "PLASMA", sampleBarcode: "BC011", userid: "U011", projInfo: { projId: 1, projName: "血液疾病检测项目", projShortName: "项目1", color: "red" }, sampleId: "S011", projId: 1, startIncubatedTime: 1612346678, incubatedTimeSec: 1850, errors: [] }, { pos: "D3", state: "NO_REACTION", bloodType: "SERUM", sampleBarcode: "BC012", userid: "U012", projInfo: { projId: 1, projName: "血液疾病检测项目", projShortName: "项目1", color: "red" }, sampleId: "S012", projId: 1, startIncubatedTime: 1612346778, incubatedTimeSec: 2000, errors: [] }, { pos: "E1", state: "REACTED", bloodType: "WHOLE_BLOOD", sampleBarcode: "BC013", userid: "U013", projInfo: { projId: 2, projName: "血液成分分析项目", projShortName: "项目2", color: "blue" }, sampleId: "S013", projId: 2, startIncubatedTime: 1612346878, incubatedTimeSec: 2350, errors: [] }, { pos: "E2", state: "PARTIAL_REACTION", bloodType: "PLASMA", sampleBarcode: "BC014", userid: "U014", projInfo: { projId: 2, projName: "血液成分分析项目", projShortName: "项目2", color: "blue" }, sampleId: "S014", projId: 2, startIncubatedTime: 1612346978, incubatedTimeSec: 1900, errors: [] }, { pos: "E3", state: "NO_REACTION", bloodType: "SERUM", sampleBarcode: "BC015", userid: "U015", projInfo: { projId: 2, projName: "血液成分分析项目", projShortName: "项目2", color: "blue" }, sampleId: "S015", projId: 2, startIncubatedTime: 1612347078, incubatedTimeSec: 2100, errors: [] }, { pos: "F1", state: "REACTED", bloodType: "WHOLE_BLOOD", sampleBarcode: "BC016", userid: "U016", projInfo: { projId: 3, projName: "特殊血液指标检测项目", projShortName: "项目3", color: "green" }, sampleId: "S016", projId: 3, startIncubatedTime: 1612347178, incubatedTimeSec: 2200, errors: [] }, { pos: "F2", state: "PARTIAL_REACTION", bloodType: "PLASMA", sampleBarcode: "BC017", userid: "U017", projInfo: { projId: 3, projName: "特殊血液指标检测项目", projShortName: "项目3", color: "green" }, sampleId: "S017", projId: 3, startIncubatedTime: 1612347278, incubatedTimeSec: 1950, errors: [] }, { pos: "F3", state: "NO_REACTION", bloodType: "SERUM", sampleBarcode: "BC018", userid: "U018", projInfo: { projId: 3, projName: "特殊血液指标检测项目", projShortName: "项目3", color: "green" }, sampleId: "S018", projId: 3, startIncubatedTime: 1612347378, incubatedTimeSec: 2050, errors: [] } ] }
// 定义方法以更新试管架状态
const fetchTubeRackState = async () => { try { const response = await getTubeRackState() if (response && response.success) { tubeRack.value = response.data } else { console.error('获取试管架状态失败', response) } } catch (err) { console.error('请求试管架状态接口出错:', err) } }
// 页面加载时获取试管架状态
onMounted(() => { fetchTubeRackState() setInterval(fetchTubeRackState, 10000) // 每隔指定时间更新状态
})
onMounted(() => { ws.connect() ws.subscribe<TubeHolderStateMessage>('TubeHolderState', handleTubeHolderStateMessage) ws.subscribe<IncubationPlateStateMessage>('IncubationPlateState', handleIncubationPlateStateMessage) }) onActivated(() => { ws.connect() ws.subscribe<TubeHolderStateMessage>('TubeHolderState', handleTubeHolderStateMessage) ws.subscribe<IncubationPlateStateMessage>('IncubationPlateState', handleIncubationPlateStateMessage) })
onDeactivated(() => { ws.disconnect() ws.unsubscribe<TubeHolderStateMessage>('TubeHolderState', handleTubeHolderStateMessage) ws.unsubscribe<IncubationPlateStateMessage>('IncubationPlateState', handleIncubationPlateStateMessage) })
onUnmounted(() => { ws.disconnect() ws.unsubscribe<TubeHolderStateMessage>('TubeHolderState', handleTubeHolderStateMessage) ws.unsubscribe<IncubationPlateStateMessage>('IncubationPlateState', handleIncubationPlateStateMessage) })
onMounted(() => { // 在页面加载时检查是否有急诊数据传递
// 初次获取路由参数
fetchIncubationData() // 初次加载,处理可能已经存在的数据
updateProcessedTubeSettings()
// 监听 testTubeStore 数据的变化
watch( () => testTubeStore.tubeInfo?.tubeSettings, // 监听目标
(newTubeSettings) => { if (newTubeSettings) { // 当 tubeSettings 发生变化时重新处理
updateProcessedTubeSettings() } }, { deep: true, immediate: true }, // 深度监听 + 初始调用
) })
// 监听急诊数据变化
watch( () => emergencyStore.emergencyInfo, (newData) => { if (newData) { const emergencyInfo = newData as EmergencyPosStateMessage['data']['tube'] if (emergencyInfo.projInfo && Array.isArray(emergencyInfo.projInfo)) { emergencyInfo.projInfo.forEach((project: ProjectInfo, index: number) => { const subtank: Subtank = { pos: `EMERGENCY-${index + 1}`, state: 'OCCUPIED', bloodType: emergencyInfo.bloodType, sampleBarcode: emergencyInfo.sampleBarcode, userid: emergencyInfo.userid, projInfo: project, sampleId: `${emergencyInfo.sampleId}-${index}`, projId: project.projId, startIncubatedTime: Date.now(), incubatedTimeSec: 300, errors: [], isEmergency: true } // 添加到孵育盘数据中
// incubationPlates.value.push(subtank)
}) } fetchEmergencyData() } }, )
// 页面激活时触发
onActivated(() => { fetchEmergencyData() })
// 提取逻辑到独立函数
const updateProcessedTubeSettings = () => { const tubeSettings = testTubeStore.tubeInfo?.tubeSettings || [] // 获取当前试管数据
const plateData = plates.value || [] // 获取当前板数据
processedTubeSettings.value = processTubeSettings( tubeSettings, plateData, getBloodTypeLabel, ) }
//急诊状态
const showEmergencyAlert = ref(false)
//确认添加急诊
const confirmEmergency = () => { showEmergencyAlert.value = false router.push('/index/emergency') }
//确认结果
const confirmEmergencyWarn = (val: any) => { console.log('急诊结果:', val) isDialogVisible.value = false }
//获取路由参数
const fetchEmergencyData = () => { const emergencyQuery = route.query.emergencyData as string if (emergencyQuery) { emergencyData.value = JSON.parse(emergencyQuery) console.log('急诊数据:', emergencyData.value) } }
//取消
const cancelEmergency = () => { showEmergencyAlert.value = false }
// 获取圆心样式:选中时显示蓝色边框
const getCenterSquareStyle = () => ({ borderColor: selectedItem.value ? 'blue' : '#ffffff', borderStyle: selectedItem.value ? 'solid' : 'none', })
//被选中的样本id列表
const selectedSamples = ref<number[]>([])
// 外圈矩形元素的数量
// 孵育盘列表数据
const incubationPlates = ref<Subtank[]>([]) const selectedItem = ref<Subtank | null>(null) // 当前选中的样本
const selectedItemId = ref<string | null>(null) //
const TOTAL_SLOTS = 20 // 总矩形数
const emergencyData = ref<Subtank | null>(null)
// 修改获取旋转样式的方法
const getRotationStyle = (item: Subtank, index: number) => { const totalItems = TOTAL_SLOTS const angleStep = 360 / totalItems const angle = index * angleStep
return { transform: `
translate(-50%, -50%) /* 将矩形中心点移到圆心 */ rotate(${angle}deg) /* 旋转到对应角度 */ translateY(-400px) /* 向上偏移到圆环位置 */ `,
backgroundColor: item.projInfo?.color || '#ffffff', borderColor: item.isPlaceholder ? '#f0f0f0' : selectedItemId.value === item.sampleId ? '#1890ff' : getRemainingTime(item) === '已完成' ? '#ff4d4f' : 'transparent', borderWidth: '3px', borderStyle: 'solid', } }
// 获取当前元素样式:若为选中状态则添加蓝色边框,若为完成状态则添加红色边框
const getItemStyle = (item: Subtank) => { if (item.isPlaceholder) { return { borderColor: '#ffffff', // 占位符边框为白色
borderStyle: 'solid', backgroundColor: '#f8f8f8', // 占位符背景色
} }
const remainingTime = getRemainingTime(item) return { backgroundColor: item.pos === 'EMERGENCY' ? '#ffeded' : item.projInfo.color, // 急诊位特殊背景
borderColor: selectedItemId.value === item.sampleId ? 'blue' : remainingTime === '已完成' ? 'red' : 'transparent', borderStyle: selectedItemId.value === item.sampleId || remainingTime === '已完成' ? 'solid' : 'none', } }
// 切换选中状态
let currentIndex = ref(0) const toggleSelectItem = (item: Subtank,index: number) => { // 如果点击的是已选中的样本,则取消选中
if (selectedItemId.value === item.sampleId) { selectedItem.value = null selectedItemId.value = null } else { // 否则选中新的样本
selectedItem.value = item selectedItemId.value = item.sampleId currentIndex.value = index } }
//反应板数据
const plates = ref(consumablesStore.plates)
//样本数据
const processedTubeSettings = ref()
// 更新选中的样本列表
const updateSelectedSamples = (sampleIds: number[]) => { selectedSamples.value = sampleIds }
// 计算填充样式
const getFillStyle = (item: any) => { const percentage = (item.tipNum / 120) * 100 return { background: `linear-gradient(to top, #bbd3fb ${percentage}%,#c9c9c9 ${percentage}%)`, } }
//控制废料区的状态
const isFull = ref(consumablesStore.wasteStatus) const showWasteAlert = ref(false) // 获取废料区样式
const getWasteStyle = () => ({ backgroundColor: isFull.value ? '#d9534f' : '#5cb85c', // 红色表示满,绿色表示未满
transition: 'background-color 0.3s ease', // 增加平滑过渡效果
})
const closeAlert = () => { showWasteAlert.value = false if (wasteAlertTimeout) clearTimeout(wasteAlertTimeout) // 确保清除防抖计时器
}
let wasteAlertTimeout: ReturnType<typeof setTimeout> | null = null watch(isFull, (newValue) => { if (newValue) { console.log('废料箱已满,更新样式并展示弹窗') } else { console.log('废料箱未满,恢复样式') } })
const pollWasteAreaStatus = async () => { try { const response = await wasteArea() if (response.success) { const isWasteBinFull = response.data.wasteBinFullFlag
// 更新废料箱状态
if (isFull.value !== isWasteBinFull) { isFull.value = isWasteBinFull
// 如果废料箱满,且未展示弹窗,则展示弹窗
if (isFull.value && !showWasteAlert.value) { if (wasteAlertTimeout) clearTimeout(wasteAlertTimeout) // 清除可能存在的防抖计时器
wasteAlertTimeout = setTimeout(() => { showWasteAlert.value = true }, 300) // 防抖时间,避免连续触发
} } } } catch (error) { console.error('废料区接口轮询失败:', error) } }
//轮询孵育盘列表
// 轮询间隔
const POLL_INTERVAL = 1000 // 每 秒获取一次最新据
let pollIntervalId: ReturnType<typeof setInterval>
// 获取当前孵育盘列表数据,并初始化倒计时
const fetchIncubationData = async () => { try { const response = await getRunningList() let data: Subtank[] = []
if (response && response.success) { // console.log('获取孵育盘列表成功:', response.data)
// 处理服务器返回的数据
data = response.data.subtanks.map((plate: Subtank) => ({ ...plate, isSelected: plate.sampleId === selectedItemId.value, })) }
// 添加急诊数据到末位(如果存在急诊数据)
const filledData = emergencyData.value ? [...data, emergencyData.value] : data
// 填充占位符,确保总数为 20 个
while (filledData.length < TOTAL_SLOTS) { filledData.push({ pos: `PLACEHOLDER-${filledData.length + 1}`, state: 'EMPTY', bloodType: '', sampleBarcode: '', userid: '', projInfo: { projId: 0, projName: '', projShortName: '', color: '', }, startIncubatedTime: 0, incubatedTimeSec: 0, sampleId: `PLACEHOLDER-${filledData.length + 1}`, projId: 0, errors: [], isPlaceholder: true, // 标记为占位符
}) }
// 更新 `incubationPlates` 数据
// incubationPlates.value = filledData
// 更新开始时间戳
updateStartTimes() } catch (error) { console.error('获取孵育盘列表失败:', error) // 即使失败,也需要填充占位符
const filledData = emergencyData.value ? [emergencyData.value] : [] while (filledData.length < TOTAL_SLOTS) { filledData.push({ pos: `PLACEHOLDER-${filledData.length + 1}`, state: 'EMPTY', bloodType: '', sampleBarcode: '', userid: '', projInfo: { projId: 0, projName: '', projShortName: '', color: '', }, startIncubatedTime: 0, incubatedTimeSec: 0, sampleId: `PLACEHOLDER-${filledData.length + 1}`, projId: 0, errors: [], isPlaceholder: true, }) } // incubationPlates.value = filledData
} }
// 更新开始时间戳(表示计时起点)
const startTimes = ref<Record<string, number>>({}) const updateStartTimes = () => { const currentTime = Date.now() // 获取当前时间戳
incubationPlates.value?.forEach((plate) => { if (!startTimes.value[plate.pos]) { startTimes.value[plate.pos] = currentTime // 记录初始时戳
} }) }
// 计算剩余时间
const getRemainingTime = (plate: Subtank) => { const startTime = startTimes.value[plate.pos] || Date.now() const elapsed = (Date.now() - startTime) / 1000 // 已经过的秒数
const remaining = Math.max(0, plate.incubatedTimeSec - elapsed) // 剩余时间,最小为0
if (remaining === 0) return '已完成' const minutes = Math.floor(remaining / 60) const seconds = Math.floor(remaining % 60) return `${minutes}:${seconds < 10 ? '0' : ''}${seconds}` }
// 开始轮询
const startPolling = () => { fetchIncubationData() // 初始化获取数据
pollIntervalId = setInterval(fetchIncubationData, POLL_INTERVAL) // 设置轮询
}
// 结束轮询
const stopPolling = () => { if (pollIntervalId) clearInterval(pollIntervalId) }
//设置轮询
let pollInterval: ReturnType<typeof setInterval> onMounted(() => { pollInterval = setInterval(pollWasteAreaStatus, 1000) //每一秒轮询一次
startPolling() })
onUnmounted(() => { clearInterval(pollInterval) stopPolling() })
const emergencyResult = ref() // 存储急诊结果
const isDialogVisible = ref(false) // 控制弹窗的显示
let emergencyCompleted = ref(false) // 标志位,确保急诊结果只显示一次
// 监听急诊位的反应时间并触���弹窗
const watchEmergencyCompletion = () => { watch( () => incubationPlates.value, (newPlates) => { const emergencyPlate = newPlates.find( (plate) => plate.pos === 'EMERGENCY', ) if (emergencyPlate && getRemainingTime(emergencyPlate) === '已完成') { if (!emergencyCompleted.value) { // 如果急诊结果还没显示过,显示弹窗并更新结果
emergencyResult.value = { date: new Date().toLocaleString(), sampleBarcode: emergencyPlate.sampleBarcode, projInfo: emergencyPlate.projInfo, bloodType: emergencyPlate.bloodType, operator: 'admin', // 示例操作人
expiryDate: '2025-12-31', // 示例数据
serialNumber: 'SN12345678', // 示例数据
reference: '参考值', // 示例数据
value: '5.24 mg/L', // 示例数据
} isDialogVisible.value = true emergencyCompleted.value = true // 设置为已显示
} } }, { deep: true }, ) }
onMounted(() => { watchEmergencyCompletion() })
// 停止监听的逻辑
onUnmounted(() => { isDialogVisible.value = false emergencyResult.value = null })
// 修改试管架状态的类型定义
interface TubeHolderState { tubeHolderType: string tubes: Array<{ sampleId: string | null pos: number isHighTube: boolean isEmergency: boolean bloodType: string sampleBarcode: string userid: string projInfo: any[] projIds: number[] state: string errors: string[] }> state: string }
// 试管架状态
const tubeHolderState = ref<TubeHolderState>({ tubeHolderType: '', tubes: [], state: 'IDLE' })
// 处理 WebSocket 消息
const handleTubeHolderState = (data: TubeHolderStateMessage['data']) => { tubeHolderState.value = data }
onMounted(() => { // 订阅 WebSocket 消息
ws.subscribe<TubeHolderStateMessage>('TubeHolderState', handleTubeHolderState) })
onUnmounted(() => { // 取消订阅
ws.unsubscribe<TubeHolderStateMessage>('TubeHolderState', handleTubeHolderState) }) </script> <style lang="less" scoped> #running-container { display: flex; flex-direction: column; justify-content: space-around; align-items: center; background-color: #f9fafb; height: 92vh;
/* 更柔和的背景色 */ // 孵育盘
.circular-loader { position: relative; width: 800px; height: 800px; border-radius: 50%; background-color: #f0f2f5; display: flex; align-items: center; justify-content: center; margin: 0 auto; border: 8px solid #ffffff; box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.1);
/* 轻微阴影增加立体感 */ .center-square { width: 380px; // 调整中心矩形的宽度
height: 380px; // 调整中心矩形的高度
background-color: #ffffff; border-radius: 50%; position: absolute; border: 8px solid #ffffff; box-shadow: inset 0px 4px 10px rgba(0, 0, 0, 0.1); display: flex; flex-direction: column; justify-content: space-around; gap: 20px; z-index: 2;
span { text-align: center; }
span:nth-child(3), span:nth-child(6) { font-size: 32px; font-weight: bold; }
span:nth-child(1), span:nth-child(2), span:nth-child(4), span:nth-child(5) { font-size: 24px; font-weight: 300; } }
.rectangular-item { position: absolute; left: 50%; // 将所有矩形的起始位置设置在圆心
top: 74.5%; // 将所有矩形的起始位置设置在圆心
width: 75px; height: 170px; transform-origin: center -110px; // 调整旋转中心点
border-radius: 12px; background-color: #721616; box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08); display: flex; flex-direction: column; align-items: center; padding: 15px 8px; box-sizing: border-box; cursor: pointer;
.icon { width: 60px; height: 60px; margin: 0 auto; }
.text { position: absolute; top: 60%; left: 50%; transform: translate(-50%, -50%); font-size: 24px; color: #fff; font-weight: bold; }
&.placeholder { background-color: #f8f8f8; box-shadow: none; opacity: 0.6; }
.barcode { font-size: 16px; font-weight: bold; color: #333; margin-bottom: 10px; word-break: break-all; // 自动换行
text-align: center; }
.blood-type { font-size: 18px; color: #666; margin-bottom: auto; }
.time { font-size: 20px; font-weight: bold; color: #333; position: absolute; bottom: 15px;
&.completed { color: #ff4d4f; } } } }
// 耗材区
.consumables-container { width: 100%; box-sizing: border-box; padding: 0 30px; display: flex; flex-direction: column; background-color: #ffffff; box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.05); border-radius: 20px 20px 0 0;
// 第一行
.row-first { display: flex; align-items: center; justify-content: flex-start;
// 急诊按钮
.emergency-button { background: linear-gradient(135deg, #ff6b6b, #ff4757); border-radius: 20px; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: all 0.3s ease; box-shadow: 0 4px 15px rgba(255, 107, 107, 0.3);
&:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(255, 107, 107, 0.4); }
span { font-size: 32px; color: #ffffff; font-weight: bold; } }
// 试管架区域
.test-tube-rack-area { display: flex; align-items: center; gap: 20px;
.tube-project-tab { display: flex; align-items: center; justify-content: center; font-weight: bold; color: #1976d2; position: relative; box-shadow: 0 2px 8px rgba(25, 118, 210, 0.1); &::after { content: ''; width: 3px; height: 120px; background: linear-gradient(to bottom, #e0341d, #fa4f0b); position: absolute; right: -30px; } } } }
//第二行
.row-second { display: grid; grid-template-columns: 3fr 4fr 2fr 1fr; padding: 10px 0;
.tips-and-big-buffer { display: flex; flex-direction: column; gap: 20px;
.tips-item { width: 200px; height: 137px; border-radius: 16px; display: flex; align-items: center; justify-content: center; position: relative; background: #f8f9fa; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); overflow: hidden;
.tip-fill { position: absolute; width: 100%; height: 100%; border-radius: 16px; transition: all 0.3s ease; background: linear-gradient(to top, rgba(92, 184, 92, 0.1), transparent); }
.tip-text { font-size: 36px; color: #2c3e50; font-weight: bold; z-index: 1; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); } } }
.buffer-grid { margin-top: -10px; }
.waste-area { border-radius: 25px; display: flex; flex-direction: column; justify-content: center; align-items: center; transition: all 0.3s ease; background: #f8f9fa; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
.waste-text { font-size: 28px; font-weight: 700; color: #ffffff; writing-mode: vertical-rl; text-orientation: upright; letter-spacing: 4px; text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); }
&:hover { transform: translateY(-2px); } } }
// 添加响应式设计
@media screen and (max-width: 800px) { padding: 15px 20px; // 修改padding值以适应小屏幕
.row-first { margin-bottom: 20px; gap: 20px;
.emergency-button { width: 200px; height: 100px;
span { font-size: 28px; } } }
.row-second { gap: 15px;
.tips-item { width: 180px; height: 120px;
.tip-text { font-size: 32px; } }
.waste-area { width: 90px; height: 285px;
.waste-text { font-size: 24px; } } } } } }
.alert-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.7); backdrop-filter: blur(5px); display: flex; justify-content: center; align-items: center; z-index: 1000;
.alert-container { background-color: #ffffff; padding: 40px; border-radius: 20px; text-align: center; max-width: 600px; animation: slideIn 0.3s ease; position: relative;
.alert-icon { .icon { width: 80px; height: 80px; margin: 0 auto; }
span { font-size: 32px; position: absolute; top: 30%; left: 50%; transform: translate(-50%, -50%); color: #fff; } }
.alert-title { font-size: 36px; font-weight: bold; color: #d9534f; margin-bottom: 20px; }
.alert-message { font-size: 28px; color: #495057; margin-bottom: 30px; }
.action-buttons { display: flex; justify-content: center; gap: 20px;
.el-button { min-width: 160px; height: 50px; font-size: 24px; border-radius: 25px; transition: all 0.3s ease;
&:hover { transform: translateY(-2px); } } } } }
@keyframes slideIn { from { opacity: 0; transform: translateY(-20px); }
to { opacity: 1; transform: translateY(0); } }
// 添加动画
@keyframes pulse { 0% { transform: scale(1); }
50% { transform: scale(1.05); }
100% { transform: scale(1); } } .quan { width: 18px; height: 18px; line-height: 18px; text-align: center; border-radius: 50%; border: 2px solid red; font-size: 13px; } .tube-container { display: flex; flex-direction: row; flex-wrap: nowrap; justify-content: space-around; align-items: center; height: auto; padding: 20px; width: 100%; } </style>
|