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.
 
 
 
 

1024 lines
25 KiB

<template>
<div id="add-emergency-container">
<!-- 添加急诊 -->
<div class="page-header">
<img class="page-header-icon" src="@/assets/Index/left.svg" @click="goBack"></img>
<div class="page-header-title">添加急诊</div>
</div>
<!-- 急诊信息 -->
<!-- <div class="emergency-info" :class="{ 'disable-section': !isEmergencyEnabled }">
<div class="tab-content">
<div class="tab-form" :class="{ isOpacity: !isEmergencyEnabled }">
<div class="line-code">
<label>样本条形码</label>
<input type="text" placeholder="请输入样本条形码信息" @focus="showKeyboard('sampleBarcode')"
:value="emergencyPosition.sampleBarcode"/>
</div>
<div class="id-info">
<label>用户ID</label>
<input type="text" placeholder="请输入用户ID信息" @focus="showKeyboard('userid')" :value="emergencyPosition.userid"
:disabled="!isEmergencyEnabled" readonly />
</div>
</div>
</div>
</div> -->
<div class="emergency-main">
<el-row>
<el-col :span="5">
<label>样本条形码</label>
</el-col>
<el-col :span="12" style="padding-left:20px">
<input type="text" placeholder="请输入样本条形码信息" @focus="showKeyboard('sampleBarcode')" style="width:400px"
:value="emergencyPosition.sampleBarcode"/>
</el-col>
</el-row>
<el-row>
<el-col :span="5">
<label>用户ID</label>
</el-col>
<el-col :span="12" style="padding-left:20px">
<input type="text" placeholder="请输入用户ID信息" @focus="showKeyboard('userid')" :value="emergencyPosition.userid" style="width:400px"
:disabled="!isEmergencyEnabled" readonly />
</el-col>
</el-row>
</div>
<hr style="height: 1px; border: none;" />
<!-- 急诊项目 -->
<div class="emergency-project" :class="{ isOpacity: !isEmergencyEnabled }">
<!-- 项目选择内容根据需要添加 -->
<div class="project-title">
<span>项目选择</span>
</div>
<div class="project-list">
<div v-for="(item, index) in projects" :key="index" @click="selectProject(item)" :class="[
isEmergencyEnabled ? 'project-item' : 'disabled-project-item',
{ 'active-project-item': emergencyPosition.projIds.includes(item.projId) && isEmergencyEnabled },
]">
<span :disabled="!isEmergencyEnabled">{{ item.projName }}</span>
<span :disabled="!isEmergencyEnabled"> {{ item.num }}/25</span>
</div>
</div>
<hr style="width: 98%; margin: 0 auto; background-color: gray; margin: 10px 0;" />
<div class="project-controller">
<!-- 血液类型 -->
<div class="blood-type">
<div class="project-title">
<span>血液类型</span>
</div>
<div class="type-list">
<button v-for="(item, index) in bloodTypes" :key="index" @click="selectBloodType(item)"
:disabled="!isEmergencyEnabled" :class="getTypeClass(item)">
{{ item.projectName }}
</button>
</div>
</div>
</div>
</div>
<hr style="height: 1px; border: none;" />
<!-- 急诊控制 -->
<div class="emergency-controller" :class="{ isOpacity: !isEmergencyEnabled }" style="padding:50px 0">
<el-button class="cancel-button" @click="cancelHandle">取消</el-button>
<el-button v-if="showSaveBtn" class="ok-button" :disabled="!isEmergencyEnabled" @click="confirmHandle">确定</el-button>
</div>
<!-- 键盘 -->
<transition name="slide-up">
<div class="keyboard" v-if="keyboardVisible">
<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, onActivated, onDeactivated, watch } from 'vue';
import { useRouter } from 'vue-router';
import { nanoid } from 'nanoid';
import { insertEmergency } from '@/services/Index/index';
import { useEmergencyStore, useConsumablesStore, useDeviceStore } from '@/store';
import type { ReactionPlate, AddEmergencyInfo } from '@/types/Index';
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(true);//设置表单是否可输入
// 模拟项目列表数据
const projects = ref<ReactionPlate[]>([]);
onMounted(() => {
// 延迟加载 projects,确保数据从持久化存储中加载完成
if (consumableStore.plates.length > 0) {
projects.value = consumableStore.plates as ReactionPlate[];
}
//过滤projId为null的数据
projects.value = projects.value.filter(item => item.projId)
})
const bloodTypes = ref([
{
id: nanoid(),
projectName: '血清/血浆',
projectType: "SERUM_OR_PLASMA"
},
{
id: nanoid(),
projectName: '全血',
projectType: "WHOLE_BLOOD"
},
]);
// 获取类型class
const getTypeClass = (project: { projectType: string }) => {
return [
isEmergencyEnabled.value ? 'blood-button' : 'disabled-blood',
{ 'active-item': bloodType.value === project.projectType && isEmergencyEnabled.value },
];
};
// 公共状态管理
const projectName = ref('');
const bloodType = ref('');
// 急诊位数据
const emergencyPosition = ref<AddEmergencyInfo>({
sampleBarcode: '', // 样本条形码
userid: '', // 用户ID
projIds: [], // 项目选择
bloodType: '', // 血液类型
});
//获取试管架状态
const tubeRackState = ref<TubeHolderStateMessage['data']>({
tubeHolderType: '',
tubes: [],
state: 'IDLE'
});
//处理试管架状态
const handleTubeHolderStateMessage = (data: TubeHolderStateMessage['data']) => {
tubeRackState.value = data
}
//急诊位可操作状态
const emergencyStateList = ["EMPTY", "TO_BE_PROCESSED", "PROCESS_COMPLETE", "ERROR"]
let showSaveBtn = ref<Boolean>(false)
onMounted(() => {
ws.connect()
ws.subscribe<TubeHolderStateMessage>('TubeHolderState', handleTubeHolderStateMessage)
})
onActivated(() => {
ws.connect()
ws.subscribe<TubeHolderStateMessage>('TubeHolderState', handleTubeHolderStateMessage)
const { emergencyInfo } = emergencyStore.$state
console.log('emergencyInfo---', emergencyInfo)
if(emergencyInfo && emergencyInfo.state){
const {state, sampleBarcode} = emergencyInfo
currentInputValue.value = sampleBarcode
if(emergencyStateList.includes(state)){
showSaveBtn.value = true
}
}
})
onDeactivated(() => {
ws.disconnect()
ws.unsubscribe<TubeHolderStateMessage>('TubeHolderState', handleTubeHolderStateMessage);
})
onUnmounted(() => {
ws.disconnect()
ws.unsubscribe<TubeHolderStateMessage>('TubeHolderState', handleTubeHolderStateMessage);
})
// 返回事件处理
const router = useRouter();
const goBack = () => {
router.go(-1);
};
// 取消和确认事件
const cancelHandle = () => {
router.push('/index/regular/consumables');
};
const getProjectInfo = (projIds: number[]) => {
const projectInfo = projects.value.filter(item => projIds.includes(item.projId));
return projectInfo;
}
// 确认请求
const confirmHandle = async () => {
//只有设备暂停状态才能添加急诊
// if (deviceStore.status !== 'PAUSE') {
// ElMessage.error('设备未暂停,无法添加急诊');
// return
// }
const emergencyInfo = emergencyPosition.value;
if(!emergencyInfo.sampleBarcode){
ElMessage.error('请输入样本条形码');
return
}
if(!emergencyInfo.userid){
ElMessage.error('请输入用户ID');
return
}
if (emergencyInfo.projIds.length === 0) {
ElMessage.error('请选择项目');
return
}
if(!emergencyInfo.bloodType){
ElMessage.error('请选择血液类型');
return;
}
const res = await insertEmergency(emergencyPosition.value);
if (res.success) {
// 构造急诊位数据
const emergencyData: EmergencyPosStateMessage['data']['tube'] = {
pos: 1,
state: "OCCUPIED",
bloodType: emergencyInfo.bloodType as "SERUM_OR_PLASMA" | "WHOLE_BLOOD",
sampleBarcode: emergencyInfo.sampleBarcode || "",
userid: emergencyInfo.userid || "",
projInfo: getProjectInfo(emergencyInfo.projIds),
sampleId: `EMERGENCY-${Date.now()}`,
projIds: emergencyInfo.projIds,
errors: [],
isHighTube: false,
isEmergency: true
};
emergencyStore.setInfo(emergencyData);
// 跳转到运行中页面并传递数据
router.push({
path: "/index/regular/running",
});
} else {
console.log("🚀 ~ confirmHandle ~ res:", res)
}
};
// 项目选择事件
const selectProject = (item: any) => {
if (!isEmergencyEnabled.value) return;
if (item === '自动') {
// 自动选择清空项目
projectName.value = '自动';
emergencyPosition.value.projIds = [];
} else {
projectName.value = ""
const projectIndex = emergencyPosition.value.projIds.findIndex((proj) => proj === item.projId);
if (projectIndex > -1) {
// 如果已经选中,则移除该项目
emergencyPosition.value.projIds.splice(projectIndex, 1);
} else {
// 如果未选中,则添加该项目
emergencyPosition.value.projIds.push(item.projId);
}
}
};
// 血液类型选择事件
const selectBloodType = (item: any) => {
if (!isEmergencyEnabled.value) return;
if (item === "自动") {
// 自动选择清空血液类型
console.log("血液类型为自动");
bloodType.value = '自动';
emergencyPosition.value.bloodType = '';
return
}
emergencyPosition.value.bloodType = item.projectType;
bloodType.value = item.projectType;
};
// 切换急诊位开启/关闭状态
const toggleEmergency = () => {
isEmergencyEnabled.value = !isEmergencyEnabled.value;
if (!isEmergencyEnabled.value) {
// 清空数据
emergencyPosition.value = {
sampleBarcode: '',
userid: '',
projIds: [],
bloodType: '',
};
projectName.value = '';
bloodType.value = '';
}
};
watch(isEmergencyEnabled, (newVal) => {
if (newVal) {
showTubeSelector.value = true
} else {
selectedTubePos.value = null
}
})
// 处理回显数据
onMounted(() => {
const { emergencyInfo } = emergencyStore.$state
if (Object.keys(emergencyInfo).length > 0) {
console.log("🚀 ~ onMounted ~ emergencyInfo:", emergencyInfo)
isEmergencyEnabled.value = true
emergencyPosition.value.bloodType = emergencyInfo.bloodType;
emergencyInfo.projInfo.forEach(item => {
emergencyPosition.value.projIds.push(item.projId);
})
emergencyPosition.value.sampleBarcode = emergencyInfo.sampleBarcode;
emergencyPosition.value.userid = emergencyInfo.userid;
bloodType.value = emergencyInfo.bloodType;
projectName.value = '';
}
});
// 键盘相关状态
const keyboardVisible = ref(false)
const currentInputValue = ref('')
const currentInputField = ref<'sampleBarcode' | 'userid' | ''>('')
// 显示键盘
const showKeyboard = (field: 'sampleBarcode' | 'userid') => {
// 清空当前输入值,避免累加
// currentInputValue.value = ''
if(field == 'sampleBarcode'){
currentInputValue.value = emergencyPosition.value.sampleBarcode
}
if(field == 'userid'){
currentInputValue.value = emergencyPosition.value.userid
}
currentInputField.value = field
keyboardVisible.value = true
}
// 处理键盘输入
const handleKeyboardInput = (value: string) => {
if (!currentInputField.value) return
console.log('value----', value)
// 更新当前输入值
currentInputValue.value = value
// 更新对应字段的值
if (currentInputField.value === 'sampleBarcode') {
emergencyPosition.value.sampleBarcode = value
} else {
emergencyPosition.value.userid = value
}
}
// 处理键盘按键
const handleKeyPress = (button: string) => {
if (button === '{enter}') {
hideKeyboard()
} else if (button === '{bksp}') {
// 处理退格键
const value = currentInputValue.value
if (value.length > 0) {
const newValue = value.slice(0, -1)
handleKeyboardInput(newValue)
}
}
}
// 隐藏键盘
const hideKeyboard = () => {
keyboardVisible.value = false
currentInputField.value = ''
currentInputValue.value = ''
}
// 在组件卸载时清理状态
onUnmounted(() => {
hideKeyboard()
})
const showTubeSelector = ref(false)
const selectedTubePos = ref<number | null>(1)
// 检查试管是否被占用
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>
<style lang="less" scoped>
input {
margin-bottom: 20px;
padding: 8px 5px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 32px;
transition: box-shadow 0.2s ease;
border-radius: 10px;
&::placeholder {
font-size: 32px;
font-weight: 100;
color: #d8d8d8;
}
}
label {
margin-bottom: 8px;
font-size: 32px;
font-weight: 700;
text-align: left;
}
#add-emergency-container {
margin: 0;
padding: 0;
position: relative;
height: 100%;
width: 100%;
background-color: #f4f6f9;
box-sizing: border-box;
/* 设置背景色 */
.active-item {
background-color: #528dfe;
color: white !important;
}
.isOpacity {
opacity: 0.4;
}
hr {
background-color: #e0e0e0;
/* 细化分割线的颜色 */
}
.page-header {
width: 100%;
height: 100px;
display: flex;
align-items: center;
background-color: #ffffff;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
border-radius: 8px;
.page-header-icon {
width: 16.14px;
height: 26.76px;
margin: 0 20px;
cursor: pointer;
transition: transform 0.2s ease;
&:hover {
transform: scale(1.1);
/* 放大效果 */
}
}
.page-header-title {
font-size: 36px;
font-weight: 900;
line-height: 1.2;
color: #333;
}
}
.emergency-tabs {
width: 1200px;
height: 118px;
display: flex;
background-color: #ffffff;
display: flex;
transition: all 0.3s ease;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
border-radius: 8px;
overflow: hidden;
margin-top: 20px;
.tab-button {
flex: 1;
padding: 10px;
background-color: #f6f6f6;
border: none;
cursor: pointer;
font-size: 40px;
font-weight: 700;
line-height: 24px;
transition: background-color 0.3s ease, color 0.3s ease;
&:hover {
background-color: #eef4ff;
/* 悬停时背景色 */
}
}
.tab-button.active {
background-color: #528dfe;
color: white;
}
}
.emergency-info {
width: 100%;
height: auto;
margin-top: 20px;
border-radius: 8px;
padding: 20px 0;
.tab-content {
display: flex;
font-size: 32px;
font-weight: 700;
padding: 0 40px;
.tab-form {
flex: 0.7;
padding: 20px;
.line-code,
.id-info {
display: flex;
flex-direction: column;
margin-bottom: 20px;
label {
margin-bottom: 8px;
font-size: 32px;
font-weight: 700;
text-align: left;
}
input {
margin-bottom: 20px;
padding: 8px 5px;
border: 1px solid #ccc;
border-radius: 4px;
width: 80%;
font-size: 32px;
transition: box-shadow 0.2s ease;
border-radius: 10px;
&::placeholder {
font-size: 32px;
font-weight: 100;
color: #d8d8d8;
}
//自定义获取焦点后的样式
&:focus {
outline: none; // 去掉默认的聚焦边框
border-color: #528dfe;
box-shadow: 0 0 5px rgba(181, 189, 181, 0.5);
/* 添加阴影效果 */
}
}
}
}
.active-button {
flex: 0.3;
display: flex;
align-items: center;
justify-content: center;
.open-button {
width: 80%;
height: 70%;
background-color: #4caf50;
/* 开启状态颜色:绿色 */
color: white;
font-size: 40px;
font-weight: 600;
border: none;
border-radius: 6px;
cursor: pointer;
transition: background-color 0.3s ease, transform 0.2s ease;
}
.close-button {
width: 80%;
height: 70%;
background-color: #f44336;
/* 关闭状态颜色:红色 */
color: white;
font-size: 40px;
font-weight: 600;
border: none;
border-radius: 6px;
cursor: pointer;
transition: background-color 0.3s ease, transform 0.2s ease;
}
button {
width: 100%;
height: 70%;
background-color: #528dfe;
color: white;
border: none;
border-radius: 7px;
cursor: pointer;
margin-top: 50px;
font-size: 40px;
transition: background-color 0.3s ease, transform 0.2s ease;
}
}
}
}
.emergency-project {
width: 100%;
padding: 20px 40px;
border-radius: 10px;
margin-top: 20px;
box-sizing: border-box;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
.disabled-item {
color: gray;
border: 1px solid gray;
height: 70px;
width: 150px;
background-color: white;
border-radius: 8px;
margin-right: 20px;
font-size: 32px;
}
.project-auto {
height: 70px;
width: 150px;
border: 1px solid #4a90e2;
color: #4a90e2;
border-radius: 8px;
margin-right: 20px;
cursor: pointer;
font-size: 32px;
transition: all 0.3s ease
}
//禁用状态下的样式
.project-title {
width: 100%;
text-align: left;
margin-bottom: 10px;
gap: 10px;
span {
font-size: 32px;
font-weight: 700;
}
}
.project-list {
display: flex;
align-items: center;
padding: 10px 0;
border-radius: 8px;
justify-content: flex-start;
.active-project-item {
background-color: #528dfe !important;
color: white !important;
}
.disabled-item {
color: gray;
border: 1px solid gray;
height: 70px;
width: 150px;
background-color: white;
border-radius: 8px;
margin-right: 20px;
font-size: 32px;
}
.project-item {
width: 150px;
display: flex;
flex-direction: column;
align-items: center;
padding: 0 10px;
color: #4a90e2;
background-color: #f2f2f2;
font-size: 24px;
text-align: center;
position: relative;
border-radius: 8px;
&::before {
content: "";
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 1px;
height: 60%;
background-color: #ccc;
}
}
.disabled-project-item {
width: 150px;
display: flex;
flex-direction: column;
align-items: center;
padding: 0 10px;
color: gray;
font-size: 24px;
text-align: center;
position: relative;
background-color: #f2f2f2;
border-radius: 8px;
pointer-events: none;
&::before {
content: "";
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 1px;
height: 60%;
background-color: #ccc;
}
}
}
.project-controller {
display: flex;
width: 100%;
margin: 20px 0;
justify-content: space-between;
gap: 20px;
.blood-type {
width: 60%;
display: flex;
flex-direction: column;
.type-list {
display: flex;
gap: 15px;
justify-content: space-between;
padding: 10px 0 10px;
.blood-button,
.disabled-blood {
flex: 1;
min-width: 150px;
padding: 15px;
font-size: 32px;
border-radius: 8px;
text-align: center;
transition: all 0.3s ease
}
.blood-button {
border: 1px solid #5c94fe;
border-radius: 8px;
padding: 0;
color: #4a90e2;
border-radius: 8px;
margin-right: 20px;
cursor: pointer;
font-size: 32px;
}
.disabled-blood {
border: 1px solid gray;
border-radius: 8px;
padding: 0;
color: gray;
border-radius: 8px;
margin-right: 20px;
cursor: none;
font-size: 32px;
}
}
}
.sample-attenuation {
flex: 1;
display: flex;
flex-direction: column;
position: relative;
.active-item {
background-color: #528dfe;
color: white !important;
}
.project-title {
margin-left: 20px;
}
&::before {
content: "";
position: absolute;
left: 0;
top: 70%;
transform: translateY(-50%);
width: 2px;
height: 40%;
background-color: #ccc;
}
.sample-button {
width: 80%;
margin: 0 20px;
height: 74px;
border-radius: 8px;
border: 1px solid #5c94fe;
color: #4a90e2;
border-radius: 8px;
margin-right: 20px;
cursor: pointer;
font-size: 32px;
}
.disabled-sample-button {
width: 80%;
margin: 0 20px;
height: 74px;
border-radius: 8px;
border: 1px solid gray;
color: gray;
border-radius: 8px;
margin-right: 20px;
cursor: pointer;
font-size: 32px;
}
}
}
}
.emergency-controller {
width: 100%;
height: 120px;
display: flex;
margin-top: 20px;
display: flex;
justify-content: center;
align-items: center;
.cancel-button,
.ok-button {
width: 365px;
height: 100px;
border-radius: 30px;
font-size: 40px;
font-weight: 400;
margin: 0 10px;
border: none;
cursor: pointer;
transition: background-color 0.3s ease, transform 0.2s ease;
}
.ok-button {
background-color: #528dfe;
color: white;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.cancel-button {
background-color: #f2f2f2;
color: black;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
}
.keyboard {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
height: 300px;
background-color: #f5f7fa;
border-top-left-radius: 16px;
border-top-right-radius: 16px;
box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.1);
z-index: 1000;
}
// 键盘动画
.slide-up-enter-active,
.slide-up-leave-active {
transition: transform 0.3s ease;
}
.slide-up-enter-from,
.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;
}
}
}
}
}
.emergency-main{
margin-top:40px;
margin-left:80px
}
</style>