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.
592 lines
15 KiB
592 lines
15 KiB
<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" />
|
|
</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 { ref, onMounted, onActivated, onBeforeUnmount, watch } from 'vue'
|
|
import { 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 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 () => {
|
|
isLoading.value = true
|
|
|
|
try {
|
|
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
|
|
// 更新状态
|
|
if (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)
|
|
// 清空临时数据
|
|
consumablesData.value = null
|
|
}
|
|
} 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: `CG${index}`, 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;
|
|
margin-top: -90px;
|
|
|
|
.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>
|