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.
 
 
 
 

1505 lines
40 KiB

<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>