Browse Source

update

master
gzt 8 months ago
parent
commit
ad633b10d9
  1. 244
      src/pages/Index/Regular/Consumables.vue
  2. 155
      src/pages/Index/Regular/Emergency.vue
  3. 14
      src/pages/Index/components/Consumables/ChangeNum.vue
  4. 2
      src/services/Index/running/running.ts
  5. 4
      src/types/Index/Consumables.ts
  6. 60
      src/utils/errorHandler.ts

244
src/pages/Index/Regular/Consumables.vue

@ -1,15 +1,46 @@
<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 class="loading-progress">
<div class="progress-bar" :style="{ width: `${loadingProgress}%` }"></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">
@ -25,7 +56,7 @@
<!--缓冲液区域小-->
<div class="buffer-little-title">
<div class="line"></div>
<div class="content">缓冲液()</div>
<div class="content">缓冲液()</div>
</div>
<div class="ball-area">
<MainComponent v-for="item in bufferLittles" :key="item" class="ball-grid" :projectName="item.projShortName"
@ -38,7 +69,7 @@
<script setup lang="ts">
import { MoveLiquidArea, SpttingPlates, MainComponent } from '../components'
import { ref, onMounted, onActivated, onBeforeUnmount } from 'vue'
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'
@ -51,6 +82,7 @@ import {
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)
@ -62,8 +94,6 @@ const deviceStore = useDeviceStore()
const currentTemperature = ref(40);
//
const wasteStatus = ref(false)
//
const loadingProgress = ref(0)
//
const isLoad = ref(false)
const isLoading = ref(false)
@ -84,6 +114,17 @@ const moveLiquids = ref<LiquidState[]>([
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)
//
@ -143,32 +184,18 @@ const bufferLittles = ref<BufferLittle[]>([
const bufferBig = ref<BottleGroup[]>([])
//
const emergencyInfo = ref(emergencyStore.$state.emergencyInfo || {})
//
const GetLoadConsumables = async () => {
isLoading.value = true
try {
const res = await scanConsumables()
moveLiquids.value = res.data.consumableState.tips
plates.value = res.data.consumableState.reactionPlateGroup
bufferLittles.value = res.data.consumableState.littBottleGroup
bufferBig.value = res.data.consumableState.larBottleGroup
tempTipNum.value = [...moveLiquids.value.map((liquid) => liquid.tipNum)]
isLoad.value = true
isLoading.value = false
isAlreadyLoad.value = true
consumableStore.setConsumablesData(res.data.consumableState)
} catch (error) {
//
isLoading.value = false
//
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('存在错误,请检查耗材')
}
}
// const getEmergencyInfo = async () => {
// const res = await isTubeExist()
// emergencyInfo.value = res.data.tube
// emergencyStore.setInfo(res.data.tube)
// }
//使websocket
const startWebSocket = () => {
socket.connect()
@ -188,7 +215,7 @@ const handleSensorState = (data: SensorStateMessage['data']) => {
//
const handleConsumablesState = (data: ConsumablesStateMessage['data']) => {
if (isAlreadyLoad.value) {
if (isAlreadyLoad.value && isHandleScan.value) {
consumableStore.setConsumablesData(data)
moveLiquids.value = data.tips
plates.value = data.reactionPlateGroup as ReactionPlate[]
@ -236,40 +263,53 @@ onActivated(() => {
}
})
//
//
const consumablesData = ref<ConsumablesStateMessage['data'] | null>(null)
//
const handleIsLoad = async () => {
isLoading.value = true
loadingProgress.value = 0
//
const startTime = Date.now()
const duration = 3000 // 30
try {
const res = await scanConsumables()
isLoading.value = false
const updateProgress = () => {
const elapsed = Date.now() - startTime
const progress = Math.min((elapsed / duration) * 100, 100)
loadingProgress.value = progress
//
formattedReports.value = formatScanReports(res.data.scanReports)
console.log('🚀 ~ handleIsLoad ~ formattedReports:', formattedReports.value)
//
showScanResults.value = true
consumablesData.value = res.data.consumableState
//
if (res.data.consumableState) {
if (progress < 100) {
requestAnimationFrame(updateProgress)
}
}
requestAnimationFrame(updateProgress)
// 3
await new Promise(resolve => setTimeout(resolve, duration))
//
try {
await GetLoadConsumables()
} catch (error) {
console.error('加载耗材失败:', error)
} finally {
isLoading.value = false
loadingProgress.value = 0
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
@ -326,13 +366,13 @@ const handleIsUnload = () => {
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: `GROUP${index}`, num: tipNum })
await updateTipsNum({ group: `CG`${ index }`, num: tipNum })
} else {
ElMessage.error('设备正在工作,无法修改数值')
}
@ -360,7 +400,7 @@ const updateTipNum = async ({ index, tipNum }: { index: number; tipNum: number }
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
z-index: 9998;
}
.loading-content {
@ -458,5 +498,95 @@ const updateTipNum = async ({ index, tipNum }: { index: number; tipNum: number }
}
}
}
.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>

155
src/pages/Index/Regular/Emergency.vue

@ -77,7 +77,7 @@
<!-- 急诊控制 -->
<div class="emergency-controller" :class="{ isOpacity: !isEmergencyEnabled }">
<button class="cancel-button" :disabled="!isEmergencyEnabled" @click="cancelHandle">取消</button>
<button class="ok-button" :disabled="!isEmergencyEnabled" @click="confirmHandle"></button>
<button class="ok-button" :disabled="!isEmergencyEnabled" @click="confirmHandle"></button>
</div>
<!-- 键盘 -->
@ -86,20 +86,42 @@
<SimpleKeyboard :input="currentInputValue" @onChange="handleKeyboardInput" @onKeyPress="handleKeyPress" />
</div>
</transition>
<!-- 试管选择弹窗 -->
<div v-if="showTubeSelector" class="tube-selector-overlay">
<div class="tube-selector-container">
<div class="tube-selector-header">
<span class="title">选择试管位置</span>
</div>
<div class="tube-selector-content">
<div class="tube-grid">
<button v-for="i in 10" :key="i - 1" :class="['tube-button', { 'occupied': isTubeOccupied(i - 1) }]"
:disabled="isTubeOccupied(i - 1)" @click="selectTube(i - 1)">
{{ i }}
</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
import { ref, onMounted, onUnmounted, onActivated, onDeactivated, watch } from 'vue';
import { useRouter } from 'vue-router';
import { nanoid } from 'nanoid';
import { insertEmergency } from '../../../services/Index/index';
import { useEmergencyStore, useConsumablesStore } from '../../../store';
import { useEmergencyStore, useConsumablesStore, useDeviceStore } from '../../../store';
import type { ReactionPlate, AddEmergencyInfo } from '../../../types/Index';
import type { EmergencyPosStateMessage } from '../../../websocket/socket';
import type { EmergencyPosStateMessage, TubeHolderStateMessage } from '../../../websocket/socket';
import { ElMessage } from 'element-plus';
import { createWebSocket } from '../../../websocket/socket';
import { getServerInfo } from '../../../utils/getServerInfo';
const ws = createWebSocket(getServerInfo().wsUrl);
const consumableStore = useConsumablesStore();
const emergencyStore = useEmergencyStore();
const deviceStore = useDeviceStore();
// /
const isEmergencyEnabled = ref(false);
@ -145,8 +167,33 @@ const emergencyPosition = ref<AddEmergencyInfo>({
bloodType: '', //
});
//
const tubeRackState = ref<TubeHolderStateMessage['data']>({
tubeHolderType: '',
tubes: [],
state: 'IDLE'
});
//
const handleTubeHolderStateMessage = (data: TubeHolderStateMessage['data']) => {
tubeRackState.value = data
}
onMounted(() => {
ws.connect()
ws.subscribe<TubeHolderStateMessage>('TubeHolderState', handleTubeHolderStateMessage)
})
onActivated(() => {
ws.connect()
ws.subscribe<TubeHolderStateMessage>('TubeHolderState', handleTubeHolderStateMessage)
})
onDeactivated(() => {
ws.disconnect()
ws.unsubscribe<TubeHolderStateMessage>('TubeHolderState', handleTubeHolderStateMessage);
})
onUnmounted(() => {
ws.disconnect()
ws.unsubscribe<TubeHolderStateMessage>('TubeHolderState', handleTubeHolderStateMessage);
})
//
const router = useRouter();
const goBack = () => {
@ -163,10 +210,18 @@ const getProjectInfo = (projIds: number[]) => {
}
//
const confirmHandle = async () => {
//
if (deviceStore.status !== 'PAUSE') {
ElMessage.error('设备未暂停,无法添加急诊');
return
}
const res = await insertEmergency(emergencyPosition.value);
if (res.success) {
const emergencyInfo = emergencyPosition.value;
if (emergencyInfo.projIds.length === 0) { }
if (emergencyInfo.projIds.length === 0) {
ElMessage.error('请选择项目');
return
}
//
const emergencyData: EmergencyPosStateMessage['data']['tube'] = {
pos: 1,
@ -181,7 +236,8 @@ const confirmHandle = async () => {
isHighTube: false,
isEmergency: true
};
emergencyStore.setInfo(emergencyData)
emergencyStore.setInfo(emergencyData);
//
router.push({
path: "/index/regular/running",
@ -242,6 +298,13 @@ const toggleEmergency = () => {
bloodType.value = '';
}
};
watch(isEmergencyEnabled, (newVal) => {
if (newVal) {
showTubeSelector.value = true
} else {
selectedTubePos.value = null
}
})
//
onMounted(() => {
@ -267,7 +330,7 @@ const currentInputField = ref<'sampleBarcode' | 'userid' | ''>('')
//
const showKeyboard = (field: 'sampleBarcode' | 'userid') => {
//
//
currentInputValue.value = ''
currentInputField.value = field
keyboardVisible.value = true
@ -314,6 +377,23 @@ onUnmounted(() => {
hideKeyboard()
})
const showTubeSelector = ref(false)
const selectedTubePos = ref<number | null>(null)
//
const isTubeOccupied = (pos: number) => {
return tubeRackState.value.tubes.some(tube =>
tube.pos === pos && tube.state === 'OCCUPIED'
)
}
//
const selectTube = (pos: number) => {
selectedTubePos.value = pos
showTubeSelector.value = false
isEmergencyEnabled.value = true
}
</script>
@ -813,5 +893,64 @@ onUnmounted(() => {
.slide-up-leave-to {
transform: translateY(100%);
}
.tube-selector-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;
}
.tube-selector-container {
background: white;
border-radius: 8px;
padding: 20px;
width: 600px;
.tube-selector-header {
text-align: center;
margin-bottom: 20px;
.title {
font-size: 32px;
font-weight: bold;
}
}
.tube-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 15px;
padding: 20px;
.tube-button {
padding: 20px;
font-size: 24px;
border: 2px solid #409eff;
border-radius: 8px;
background: white;
cursor: pointer;
transition: all 0.3s;
&:hover:not(:disabled) {
background: #409eff;
color: white;
}
&.occupied {
border-color: #909399;
background: #f4f4f5;
cursor: not-allowed;
}
}
}
}
}
</style>

14
src/pages/Index/components/Consumables/ChangeNum.vue

@ -57,7 +57,7 @@ const query = ref({
// value query
watch(value, (newVal) => {
query.value = {
group: `GROUP${plateIndex.value}`,
group: `CG`${ plateIndex.value } `,
num: Number(newVal)
}
})
@ -72,18 +72,18 @@ const openDialog = (plate, index) => {
value.value = Number(plate.num)
plateIndex.value = index
title.value = plate.projShortName || ''
dialogTitle.value = `请选择${title.value}数值`
dialogTitle.value = `请选择${ title.value } 数值`
// query
query.value = {
group: `GROUP${index}`,
group: `CG`${index}`,
num: Number(plate.num)
}
}
}
defineExpose({
openDialog,
})
defineExpose({
openDialog,
})
const handleCancel = () => {
isOpen.value = false

2
src/services/Index/running/running.ts

@ -12,7 +12,7 @@ export const getRunningList = async () => {
}
}
//获取试管架的状态 轮询请求
//获取试管架的状态
export const getTubeRackState = async () => {
try {
const res = await apiClient.post(

4
src/types/Index/Consumables.ts

@ -2,9 +2,11 @@
// scanReports 接口
export interface ScanReport {
chNum: number
report: string // 如:PASS 或者其他状态
state: string // 如:PASS 或者其他状态
projId: number
lotId: string
projName: string
projShortName: string
}
// scanRawResults 接口

60
src/utils/errorHandler.ts

@ -0,0 +1,60 @@
// 错误码映射表
const ERROR_MAP = {
PASS: '通过',
EMPTY: '空',
EXPIRED: '转材过期',
MISS_REACTION_PLATE: '没有反应板架',
MISS_LITTSB: '缺少小缓冲液',
MISS_LARBS: '缺少大缓冲液',
MISS_IDCARD: '未找到匹配的项目ID卡',
LITTSB_LOTID_MISMATCH: '小缓冲液批号不匹配',
LARBS_LOTID_MISMATCH: '大缓冲液批号不匹配',
CODE_ERROR_PROJINFO_IS_ERROR: '代码错误,项目信息异常',
UN_SUPPORT_PROJ: '不支持的项目',
REACTION_PLATE_2D_CODE_FORMATE_ERROR: '反应板二维码格式错误',
} as const
type ErrorCode = keyof typeof ERROR_MAP
/**
*
* @param code
* @returns
*/
export const getErrorMessage = (code: ErrorCode): string => {
return ERROR_MAP[code as ErrorCode] || '未知错误'
}
/**
*
* @param code
* @returns
*/
export const isError = (code: ErrorCode): boolean => {
return code !== 'PASS'
}
/**
*
* @param reports
* @returns
*/
export const formatScanReports = (
reports: Array<{
chNum: number
state: string
projId: number
lotId: string
projName: string
projShortName: string
}>,
) => {
return reports.map((item) => ({
channel: item.chNum,
code: item.state,
message: getErrorMessage(item.state as ErrorCode),
isError: isError(item.state as ErrorCode),
projName: item.projName,
lotId: item.lotId,
}))
}
Loading…
Cancel
Save