|
|
<template> <div id="regular-container"> <!-- 加载蒙层 --> <div class="loading-overlay" v-if="isLoading"> <div class="loading-content"> <div class="loading-spinner"></div> <span class="loading-text">正在扫描耗材...</span> </div> </div> <!-- 扫描结果弹窗 --> <div v-if="showScanResults" class="scan-results-overlay"> <div class="scan-results-container"> <div class="scan-results-header"> <span class="title">扫描结果</span> </div> <div class="scan-results-content"> <table class="scan-results-table"> <thead> <tr> <th>通道</th> <th>项目名称</th> <th>批次号</th> <th>状态</th> </tr> </thead> <tbody> <tr v-for="report in formattedReports" :key="report.channel" :class="{ 'error-row': report.isError }" > <td>通道{{ report.channel + 1 }}</td> <td>{{ report.projName || '--' }}</td> <td>{{ report.lotId || '--' }}</td> <td :class="report.isError ? 'error-text' : 'success-text'"> {{ report.message }} </td> </tr> </tbody> </table> </div> <div class="scan-results-footer"> <button class="confirm-btn" @click="handleConfirmScan">确认</button> </div> </div> </div>
<!--耗材页面 --> <div class="main-top"> <div class="plate-area"> <SpttingPlates :plates="plates" :temperature="currentTemperature" />
<!-- <SliderAreaEx v-model="sliderValue" v-model:totalValue="totalVal"/> <DragAreaEx v-model:hVal="hVal" v-model:hTotal="hTotal" v-model:vVal="vVal" v-model:vTotal="vTotal"/> --> </div> <div class="move-liquid-area"> <MoveLiquidArea :isLoad="isLoad" :isLoading="isLoading" :moveLiquids="moveLiquids" :tempTipNum="tempTipNum" :bufferBig="bufferBig" :emergencyInfo="emergencyInfo" :wasteStatus="wasteStatus" @loadConsumables="handleIsLoad" @unloadConsumables="handleIsUnload" @updateTipNum="updateTipNum" /> </div> </div> <div class="main-bottom"> <!--缓冲液区域小--> <div class="buffer-little-title"> <div class="line"></div> <div class="content">缓冲液(小)</div> </div> <div class="ball-area"> <MainComponent v-for="item in bufferLittles" :key="item" class="ball-grid" :projectName="item.projShortName" :currentCount="item.num" :totalBalls="25" :activatedBalls="item.num" :columns="5" gridWidth="240px" gridHeight="235px" :activeColor="item.color" /> </div> </div> </div> </template>
<script setup lang="ts"> import { MoveLiquidArea, SpttingPlates, MainComponent } from '../components' import SliderAreaEx from '../components/Consumables/SliderAreaEx.vue'; import DragAreaEx from '../components/Consumables/DragAreaEx.vue'; import { ref, onMounted, onActivated, onBeforeUnmount, watch } from 'vue' import { getInitState, scanConsumables, updateTipsNum, } from '../../../services/Index/index' import { useConsumablesStore, useEmergencyStore } from '../../../store' import { useDeviceStore } from '../../../store/index' import { eventBus } from '../../../eventBus' import { ReactionPlate, BottleGroup, LiquidState } from '../../../types/Index' import { createWebSocket } from '../../../websocket/socket' import type { ConsumablesStateMessage, SensorStateMessage, EmergencyPosStateMessage, } from '../../../websocket/socket' import { getServerInfo } from '../../../utils/getServerInfo' import { formatScanReports } from '../../../utils/errorHandler' import { ElMessage } from 'element-plus' const { wsUrl } = getServerInfo('/api/v1/app/ws/state') const socket = createWebSocket(wsUrl)
const sliderValue = ref(25); // 初始值
const totalVal = ref(50)
const hVal = ref(10) const hTotal = ref(60) const vVal = ref(10) const vTotal = ref(60)
const consumableStore = useConsumablesStore() const emergencyStore = useEmergencyStore() const deviceStore = useDeviceStore() // 温度状态
const currentTemperature = ref(40) // 废料区状态
const wasteStatus = ref(false) // 父组件状态
const isLoad = ref(false) const isLoading = ref(false) // 反应板夹的状态
const plates = ref<ReactionPlate[]>([]) // 移液盘的状态
const moveLiquids = ref<LiquidState[]>([ { id: 1, tipNum: 0, }, { id: 2, tipNum: 0, }, { id: 3, tipNum: 0, }, ]) // 新增状态
const showScanResults = ref(false) interface ScanReport { channel: number code: string message: string isError: boolean projName: string | null lotId: string | null } const formattedReports = ref<ScanReport[]>([]) //是否加载
const isAlreadyLoad = ref(false) // 临时状态管理小球激活数量
const tempTipNum = ref<number[]>([ ...moveLiquids.value.map((liquid) => liquid.tipNum), ]) //管理缓冲液小的状态
interface BufferLittle { id: number num: number projShortName: string color: string lotId?: string type?: string projId?: number projName?: string } const bufferLittles = ref<BufferLittle[]>([ { id: 1, num: 0, projShortName: '', color: '#4caf50', }, { id: 2, num: 0, projShortName: '', color: '#4caf50', }, { id: 3, num: 0, projShortName: '', color: '#4caf50', }, { id: 4, num: 0, projShortName: '', color: '#4caf50', }, { id: 5, num: 0, projShortName: '', color: '#4caf50', }, { id: 6, num: 0, projShortName: '', color: '#4caf50', }, ]) //管理大缓冲液的状态
const bufferBig = ref<BottleGroup[]>([]) //急诊区状态
const emergencyInfo = ref(emergencyStore.$state.emergencyInfo || {}) //是否处理扫描结果
const isHandleScan = ref(false) // 确认扫描结果
const handleConfirmScan = () => { showScanResults.value = false isHandleScan.value = true if (formattedReports.value.some((report) => report.isError)) { isLoad.value = false isAlreadyLoad.value = false ElMessage.warning('存在错误,请检查耗材') } } //使用websocket保证数据的实时性
const startWebSocket = () => { socket.connect() } // 处理传感器状态消息
const handleSensorState = (data: SensorStateMessage['data']) => { // 更新温度值(这里使用孵育盒温度)
currentTemperature.value = data.incubateBoxTemperature wasteStatus.value = data.wasteBinFullFlag consumableStore.updateWasteStatus(wasteStatus.value) // 可以添加温度异常处理逻辑
if (currentTemperature.value > 40) { console.warn('温度过高警告') // 可以在这里添加其他警告逻辑
} }
//处理耗材状态
const handleConsumablesState = (data: ConsumablesStateMessage['data']) => { if (isAlreadyLoad.value && isHandleScan.value) { consumableStore.setConsumablesData(data) moveLiquids.value = data.tips plates.value = data.reactionPlateGroup as ReactionPlate[] bufferLittles.value = data.littBottleGroup as BufferLittle[] bufferBig.value = data.larBottleGroup as BottleGroup[] } else { return } } // 使用事件总线更新状态
const updatePlatesAndBuffers = ({ value, index, }: { value: number index: number }) => { if (plates.value && plates.value[index]) { plates.value[index].num = value } if (bufferLittles.value && bufferLittles.value[index]) { bufferLittles.value[index].num = value } if (bufferBig.value && bufferBig.value[index]) { bufferBig.value[index].isUse = value > 0 } } onMounted(() => { eventBus.on('confirm', updatePlatesAndBuffers) startWebSocket() socket.subscribe<SensorStateMessage>('SensorState', handleSensorState) socket.subscribe<ConsumablesStateMessage>( 'ConsumablesStateService', handleConsumablesState, ) // getEmergencyInfo()
}) onBeforeUnmount(() => { // 清除事件总线的监听
eventBus.off('confirm', updatePlatesAndBuffers) if (socket !== null) { socket.disconnect() // 断开连接
} socket.unsubscribe<SensorStateMessage>('SensorState', handleSensorState) socket.unsubscribe<ConsumablesStateMessage>( 'ConsumablesStateService', handleConsumablesState, ) }) // 在组件激活时恢复状态
onActivated(() => { emergencyInfo.value = emergencyStore.$state.emergencyInfo || {} if (!isLoad.value) { console.log('组件被激活了') } }) // 子组件传值
//存储耗材数据
const consumablesData = ref<ConsumablesStateMessage['data'] | null>(null) // 修改加载耗材的处理函数
const handleIsLoad = async () => { const res = await getInitState() if (res.ecode === 'SUC' && !res.data.deviceInited) { eventBus.emit('initDevice') return } try { isLoading.value = true const res = await scanConsumables() isLoading.value = false
// 格式化扫描结果
formattedReports.value = formatScanReports(res.data.scanReports) console.log('🚀 ~ handleIsLoad ~ formattedReports:', formattedReports.value) // 显示扫描结果弹窗
showScanResults.value = true consumablesData.value = res.data.consumableState } catch (error) { console.error('加载耗材失败:', error) isLoading.value = false isAlreadyLoad.value = false ElMessage.error('加载耗材失败') } } watch(isHandleScan, (newVal) => { if (newVal) { // 确认后更新状态
if (consumablesData.value) { moveLiquids.value = consumablesData.value.tips plates.value = consumablesData.value.reactionPlateGroup as ReactionPlate[] bufferLittles.value = consumablesData.value .littBottleGroup as BufferLittle[] bufferBig.value = consumablesData.value.larBottleGroup as BottleGroup[] tempTipNum.value = [...moveLiquids.value.map((liquid) => liquid.tipNum)] isLoad.value = true isAlreadyLoad.value = true consumableStore.setConsumablesData(consumablesData.value) } } else { // 取消时清空临时数据
consumablesData.value = null } }) const handleIsUnload = () => { isLoad.value = !isLoad.value isLoading.value = false isAlreadyLoad.value = false socket.unsubscribe<ConsumablesStateMessage>( 'ConsumablesStateService', handleConsumablesState, ) // 重置 moveLiquids 和 tempTipNum
moveLiquids.value = [ { id: 1, tipNum: 0 }, { id: 2, tipNum: 0 }, { id: 3, tipNum: 0 }, ] tempTipNum.value = [...moveLiquids.value.map((liquid) => liquid.tipNum)] plates.value = [] emergencyStore.unloadInfo() emergencyInfo.value = {} as EmergencyPosStateMessage['data']['tube'] bufferLittles.value = [ { id: 1, num: 0, projShortName: '', color: '#4caf50', }, { id: 2, num: 0, projShortName: '', color: '#4caf50', }, { id: 3, num: 0, projShortName: '', color: '#4caf50', }, { id: 4, num: 0, projShortName: '', color: '#4caf50', }, { id: 5, num: 0, projShortName: '', color: '#4caf50', }, { id: 6, num: 0, projShortName: '', color: '#4caf50', }, ] bufferBig.value = [] } const updateTipNum = async ({ index, tipNum, }: { index: number tipNum: number }) => { // 新临时状态
tempTipNum.value[index] = tipNum console.log('🚀 ~ updateTipNum ~ tempTipNum:', tempTipNum.value) //调用接口
try { if (deviceStore.status === 'IDLE') { await updateTipsNum({ group: `TipG${index+1}`, num: tipNum }) } else { ElMessage.error('设备正在工作,无法修改数值') } } catch (error) { console.error('修改耗材数量失败:', error) } } </script>
<style scoped lang="less"> #regular-container { width: 100%; height: 100%; display: flex; flex-direction: column;
.loading-overlay { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0, 0, 0, 0.7); backdrop-filter: blur(4px); display: flex; justify-content: center; align-items: center; z-index: 9998; }
.loading-content { display: flex; flex-direction: column; align-items: center; gap: 20px; }
.loading-spinner { width: 60px; height: 60px; border: 4px solid #f3f3f3; border-top: 4px solid #4caf50; border-radius: 50%; animation: spin 1s linear infinite; }
.loading-text { color: white; font-size: 24px; font-weight: 500; }
.loading-progress { width: 300px; height: 6px; background: rgba(255, 255, 255, 0.2); border-radius: 3px; overflow: hidden; }
.progress-bar { height: 100%; background: #4caf50; border-radius: 3px; transition: width 0.1s linear; }
@keyframes spin { 0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); } }
.main-top { display: flex; justify-content: space-between; margin-top: 10px; height: 100%;
.plate-area { width: 355px; }
.move-liquid-area { width: calc(100% - 355px); } }
.main-bottom { width: 100%; flex: 1; .buffer-little-title { display: flex; height: 40px; line-height: 40px; align-items: center;
.line { width: 4px; height: 20px; background-color: #4caf50; margin: 0 10px 0; }
.content { font-size: 26px; } }
.ball-area { display: flex; justify-self: space-between; flex-wrap: wrap;
.ball-grid { margin: 0 15px 5px 15px; } } }
.scan-results-overlay { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0, 0, 0, 0.7); backdrop-filter: blur(4px); display: flex; justify-content: center; align-items: center; z-index: 10000; pointer-events: auto; }
.scan-results-container { background: white; border: 2px solid #ff0000; border-radius: 8px; width: 800px; padding: 20px; position: relative; z-index: 10001; pointer-events: auto;
.scan-results-header { text-align: center; margin-bottom: 20px;
.title { color: #ff0000; font-size: 32px; font-weight: bold; } }
.scan-results-content { .scan-results-table { width: 100%; border-collapse: collapse; margin: 20px 0;
th, td { padding: 15px; text-align: left; font-size: 24px; border-bottom: 1px solid #eee; }
th { color: #ff0000; font-weight: bold; background-color: #fff; }
.error-row { background-color: rgba(255, 0, 0, 0.05); }
.error-text { color: #ff0000; }
.success-text { color: #4caf50; } } }
.scan-results-footer { margin-top: 20px; text-align: center;
.confirm-btn { background: #ff0000; color: white; border: none; border-radius: 25px; padding: 10px 40px; font-size: 24px; cursor: pointer;
&:hover { background: darken(#ff0000, 10%); } } } } } </style>
|