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.
 
 
 
 

555 lines
13 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="consumableStore.consumableData?.reactionPlateGroup"
:temperature="deviceStore.sensorState?.pboxTemperature || 20"
@updateReactionPlateNum="updateReactionPlateNum"
@unloadConsumable="handleUnloadConsumable"
/>
<!-- <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
:moveLiquids="consumableStore.consumableData?.tips"
:tempTipNum="
consumableStore.consumableData?.tips.map((t) => t.tipNum)
"
:bufferBig="consumableStore.consumableData.larBottleGroup"
:emergencyInfo="emergencyInfo"
:wasteStatus="deviceStore.sensorState?.wasteBinFullFlag || false"
@loadConsumables="handleIsLoad"
@unloadConsumables="handleUnloadAll"
@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, idx) in consumableStore.consumableData?.littBottleGroup"
:key="idx"
class="ball-grid"
:projectName="item.projShortName || ''"
:currentCount="item.num"
:totalBalls="25"
:activatedBalls="item.num"
:columns="5"
gridWidth="240px"
gridHeight="240px"
:activeColor="item.color"
:inUse="item.isInstall"
/>
</div>
</div>
<InitWarn
v-if="showUnloadConsumableWarnModal"
:visible="showUnloadConsumableWarnModal"
title="注意"
message="请先卸载部分耗材后再扫描耗材"
cancelText="返回"
icon="/src/assets/update-pin-icon.svg"
confirmText="确认"
@close="confirmWarn"
@confirm="confirmWarn"
/>
</div>
</template>
<script setup lang="ts">
import { MoveLiquidArea, SpttingPlates, MainComponent } from '../Components'
import { ref, onMounted, onActivated, onBeforeUnmount } from 'vue'
// import * as R from 'ramda'
import {
getInitState,
scanConsumables,
updateTipsNum,
updateConsumables,
unloadAllConsumable,
unloadConsumable,
ConsumableGroupNo,
} from '../../../services/Index/index'
import { useConsumablesStore, useEmergencyStore } from '../../../store'
import { useDeviceStore } from '../../../store/index'
import { eventBus } from '../../../eventBus'
import { createWebSocket } from '../../../websocket/socket'
import type {
ConsumablesStateMessage,
SensorStateMessage,
} from '../../../websocket/socket'
import InitWarn from '../Components/Consumables/Warn/InitWarn.vue'
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 isLoading = ref(false)
// 是否显示扫描报告(列表弹窗)
const showScanResults = ref(false)
const showUnloadConsumableWarnModal = ref(false)
interface ScanReport {
channel: number
code: string
message: string
isError: boolean
projName: string | null
lotId: string | null
}
const formattedReports = ref<ScanReport[]>([])
//急诊区状态
const emergencyInfo = ref(emergencyStore.$state.emergencyInfo || {})
// 确认扫描结果
const handleConfirmScan = () => {
showScanResults.value = false
if (formattedReports.value.some((report) => report.isError)) {
ElMessage.warning('存在错误,请检查耗材')
}
}
const confirmWarn = () => {
showUnloadConsumableWarnModal.value = false
}
//使用websocket保证数据的实时性
const startWebSocket = () => {
socket.connect()
}
// 处理传感器状态消息
const handleSensorState = (data: SensorStateMessage['data']) => {
deviceStore.setSensorState(data)
}
//处理耗材状态
const handleConsumablesState = (data: ConsumablesStateMessage['data']) => {
if (!isDragging.value) {
consumableStore.setConsumablesData(data)
} else {
console.log('正在拖动,不更新耗材')
}
}
onMounted(() => {
startWebSocket()
socket.subscribe<SensorStateMessage>('SensorState', handleSensorState)
socket.subscribe<ConsumablesStateMessage>(
'ConsumablesState',
handleConsumablesState,
)
})
onBeforeUnmount(() => {
if (socket !== null) {
socket.disconnect() // 断开连接
}
socket.unsubscribe<SensorStateMessage>('SensorState', handleSensorState)
socket.unsubscribe<ConsumablesStateMessage>(
'ConsumablesState',
handleConsumablesState,
)
})
// 在组件激活时恢复状态
onActivated(() => {
emergencyInfo.value = emergencyStore.$state.emergencyInfo || {}
// if (!isLoadedConsumables.value) {
// console.log('组件被激活了')
// }
})
const handleIsLoad = async () => {
if (deviceStore.deviceState.workState === 'WORKING') {
ElMessage.error('设备正在运行中,不可操作耗材')
return
}
const res = await getInitState()
if (res.ecode === 'SUC' && !res.data.deviceInited) {
eventBus.emit('initDevice')
return
}
try {
if (
consumableStore.consumableData.reactionPlateGroup.every(
(p) => !!p.isInstall,
)
) {
showUnloadConsumableWarnModal.value = true
return
}
isLoading.value = true
const res = await scanConsumables()
isLoading.value = false
if (res.success) {
// 格式化扫描结果
formattedReports.value = formatScanReports(res.data.scanReports)
console.log(
'🚀 ~ handleIsLoad ~ formattedReports:',
formattedReports.value,
)
// 显示扫描结果弹窗
showScanResults.value = true
} else {
ElMessage.error(res.data.info)
}
} catch (error) {
isLoading.value = false
ElMessage.error('加载耗材失败')
}
}
const handleUnloadAll = async () => {
if (deviceStore.deviceState.workState === 'WORKING') {
ElMessage.error('设备正在运行中,不可操作耗材')
return
}
const res = await unloadAllConsumable()
if (res.success) {
// 接口未返回数据
}
}
const handleUnloadConsumable = async (index: number) => {
if (deviceStore.deviceState.workState === 'WORKING') {
ElMessage.error('设备正在运行中,不可操作耗材')
return
}
const res = await unloadConsumable({
group: `CG${index + 1}` as ConsumableGroupNo,
})
if (!res.success && res.data.info) {
ElMessage.error(res.data.info)
}
}
const isDragging = ref(false)
const updateReactionPlateNum = async ({
index,
plateNum,
sync,
}: {
index: number
plateNum: number
sync: boolean
}) => {
if (plateNum < 0) {
return
}
if (deviceStore.deviceState.workState !== 'WORKING') {
consumableStore.updateReactionPlateNum(index, plateNum)
if (sync) {
console.log(`🚀 ~ updatePlateNum ~ order ${index + 1}, num ${plateNum}`)
try {
await updateConsumables({ group: `CG${index + 1}`, num: plateNum })
} catch (error) {
console.error('修改耗材数量失败:', error)
}
isDragging.value = false
} else {
isDragging.value = true
}
} else {
ElMessage.error('设备正在工作,无法修改数值')
}
}
const updateTipNum = async ({
index,
tipNum,
sync,
}: {
index: number
tipNum: number
sync: boolean
}) => {
if (tipNum < 0) {
return
}
if (deviceStore.deviceState.workState !== 'WORKING') {
consumableStore.updateTipNum(index, tipNum)
if (sync) {
console.log(`🚀 ~ updateTipNum ~ order ${index + 1}, num ${tipNum}`)
try {
await updateTipsNum({ group: `TipG${index + 1}`, num: tipNum })
} catch (error) {
console.error('修改耗材数量失败:', error)
}
isDragging.value = false
} else {
isDragging.value = true
}
} else {
ElMessage.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;
flex-wrap: wrap;
column-gap: 20px;
row-gap: 12px;
padding: 0 24px;
.ball-grid {
overflow: hidden;
}
}
}
.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>