forked from gzt/A8000
13 changed files with 6 additions and 548 deletions
-
2src/pages/Index/Regular/Running.vue
-
2src/pages/Index/Regular/TestTube.vue
-
245src/pages/Index/components/Consumables/ChangeNum.vue
-
1src/pages/Index/components/Consumables/index.ts
-
187src/pages/Index/components/Running/SampleDisplay.vue
-
2src/pages/Index/components/Running/index.ts
-
7src/pages/Index/components/TestTube/Tube.vue
-
35src/pages/Index/utils/generateSampleBackground.ts
-
15src/pages/Index/utils/getBloodTypeLabel.ts
-
3src/pages/Index/utils/index.ts
-
45src/pages/Index/utils/processTubeSettings.ts
-
8src/types/Index/TestTube.ts
-
2src/websocket/socket.ts
@ -1,245 +0,0 @@ |
|||
<template> |
|||
<div> |
|||
<!-- 使用 teleport 渲染到 body --> |
|||
<teleport to="body"> |
|||
<div v-if="isOpen" class="modal-overlay"> |
|||
<div class="modal"> |
|||
<div class="modal-header"> |
|||
<span class="modal-title">{{ dialogTitle }}</span> |
|||
<button class="close-btn" @click="handleCancel">X</button> |
|||
</div> |
|||
|
|||
<!-- 主体内容 --> |
|||
<div class="modal-body"> |
|||
<div class="input-box"> |
|||
<input id="slider" type="range" v-model="value" @change="handleSliderChange" min="0" max="25" step="1" |
|||
class="slider" /> |
|||
<div class="change-num"> |
|||
<button class="minus" @click="handleMinus">-</button> |
|||
<input type="text" v-model="value" class="input" /> |
|||
<button class="plus" @click="handlePlus">+</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- 底部按钮 --> |
|||
<div class="modal-footer"> |
|||
<button class="footer-btn" @click="handleCancel">取消</button> |
|||
<button class="footer-btn primary" @click="handleConfirm"> |
|||
确认 |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</teleport> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup> |
|||
import { ref, watch } from 'vue' |
|||
import { updateConsumables } from '../../../../services' |
|||
import { eventBus } from '../../../../eventBus' |
|||
|
|||
const isOpen = ref(false) |
|||
const value = ref(0) |
|||
const title = ref('') |
|||
const dialogTitle = ref(`请选择${title.value || ''}数值`) |
|||
const plateIndex = ref(0) |
|||
|
|||
// 使用计算属性来保持 query 与 value 的同步 |
|||
const query = ref({ |
|||
group: '', |
|||
num: 0 |
|||
}) |
|||
|
|||
// 监听 value 的变化,同步更新 query |
|||
watch(value, (newVal) => { |
|||
query.value = { |
|||
group: `CG${ plateIndex.value + 1 }`, |
|||
num: Number(newVal) |
|||
} |
|||
}) |
|||
|
|||
const handleSliderChange = (e) => { |
|||
value.value = Number(e.target.value) |
|||
} |
|||
|
|||
// 打开对话框 |
|||
const openDialog = (plate, index) => { |
|||
isOpen.value = true |
|||
value.value = Number(plate.num) |
|||
plateIndex.value = index |
|||
title.value = plate.projShortName || '' |
|||
dialogTitle.value = `请选择${ title.value } 数值` |
|||
|
|||
// 初始化 query |
|||
query.value = { |
|||
group: `CG${index+1}`, |
|||
num: Number(plate.num) |
|||
} |
|||
} |
|||
|
|||
defineExpose({ |
|||
openDialog, |
|||
}) |
|||
|
|||
const handleCancel = () => { |
|||
isOpen.value = false |
|||
} |
|||
|
|||
const handlePlus = () => { |
|||
if (value.value < 25) { |
|||
value.value = Number(value.value) + 1 |
|||
} |
|||
} |
|||
|
|||
const handleMinus = () => { |
|||
if (value.value > 0) { |
|||
value.value = Number(value.value) - 1 |
|||
} |
|||
} |
|||
|
|||
// 处理确认按钮 |
|||
const handleConfirm = async () => { |
|||
|
|||
try { |
|||
const res = await updateConsumables(query.value) |
|||
if (res.success) { |
|||
eventBus.emit('confirm', { |
|||
type: 'Plate', |
|||
value: Number(value.value), |
|||
index: plateIndex.value, |
|||
}) |
|||
isOpen.value = false |
|||
} |
|||
} catch (error) { |
|||
console.error('更新失败:', error) |
|||
} |
|||
|
|||
} |
|||
</script> |
|||
|
|||
<style scoped lang="less"> |
|||
/* 背景遮罩 */ |
|||
.modal-overlay { |
|||
position: fixed; |
|||
top: 0; |
|||
left: 0; |
|||
width: 100%; |
|||
height: 100%; |
|||
background-color: rgba(0, 0, 0, 0.5); |
|||
display: flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
z-index: 1000; |
|||
} |
|||
|
|||
/* 对话框容器 */ |
|||
.modal { |
|||
background-color: white; |
|||
border-radius: 8px; |
|||
width: 700px; |
|||
padding: 20px; |
|||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2); |
|||
font-size: 32px; |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: flex-start; |
|||
} |
|||
|
|||
/* 对话框标题 */ |
|||
.modal-header { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
width: 100%; |
|||
font-size: 32px; |
|||
font-weight: bold; |
|||
margin-bottom: 10px; |
|||
} |
|||
|
|||
/* 关闭按钮 */ |
|||
.close-btn { |
|||
background: none; |
|||
border: none; |
|||
font-size: 20px; |
|||
cursor: pointer; |
|||
} |
|||
|
|||
/* 对话框主体内容 */ |
|||
.modal-body { |
|||
width: 100%; |
|||
margin-bottom: 20px; |
|||
|
|||
.input-box { |
|||
display: flex; |
|||
align-items: center; |
|||
|
|||
/* Slider 样式 */ |
|||
.slider { |
|||
width: 70%; |
|||
margin: 10px 0; |
|||
} |
|||
|
|||
.change-num { |
|||
width: 30%; |
|||
border: 1px solid #ccc; |
|||
display: flex; |
|||
justify-content: space-around; |
|||
margin-left: 20px; |
|||
|
|||
button { |
|||
width: 52px; |
|||
height: 46px; |
|||
background-color: #f5f7fa; |
|||
font-size: 26px; |
|||
} |
|||
|
|||
.minus { |
|||
border-right: 1px solid #ccc; |
|||
} |
|||
|
|||
.plus { |
|||
border-left: 1px solid #ccc; |
|||
} |
|||
|
|||
.input { |
|||
width: 100px; |
|||
outline-color: #66b1ff; |
|||
border: none; |
|||
font-size: 32px; |
|||
text-align: center; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
/* 底部按钮 */ |
|||
.modal-footer { |
|||
display: flex; |
|||
justify-content: flex-end; |
|||
width: 100%; |
|||
|
|||
.footer-btn { |
|||
width: 100px; |
|||
height: 60px; |
|||
margin-left: 10px; |
|||
font-size: 26px; |
|||
cursor: pointer; |
|||
border: none; |
|||
border-radius: 5px; |
|||
} |
|||
|
|||
.primary { |
|||
background-color: #409eff; |
|||
color: white; |
|||
} |
|||
|
|||
.primary:hover { |
|||
background-color: #66b1ff; |
|||
} |
|||
|
|||
.footer-btn:hover { |
|||
background-color: #f1f1f1; |
|||
} |
|||
} |
|||
</style> |
@ -1,187 +0,0 @@ |
|||
<template> |
|||
<div class="samples"> |
|||
<el-popover v-for="(sample, index) in samples" :key="index" trigger="click" placement="bottom-start" |
|||
:ref="'popover-' + index" popper-class="custom-popover" :popper-style="getPopoverStyle()"> |
|||
<template #reference> |
|||
<div class="sample-item" :style="[ |
|||
generateSampleBackground(sample.projInfo), |
|||
getActiveStyle(sample.pos), |
|||
]" @click="toggleSampleSelection(sample.pos)" @dblclick="showPopover(index)"> |
|||
<div class="sample-number">{{ index + 1 }}</div> |
|||
<div class="sample-blood-type"> |
|||
{{ getBloodTypeLabel(sample.bloodType) || '未知类型' }} |
|||
</div> |
|||
</div> |
|||
</template> |
|||
<template #default> |
|||
<div class="item-detail"> |
|||
<div v-if="sample.projInfo && sample.projInfo.length > 0"> |
|||
<div v-for="(project, projIndex) in sample.projInfo" :key="projIndex"> |
|||
<button :style="{ |
|||
backgroundColor: project.color || '#ccc', |
|||
color: 'white', |
|||
border: 'none', |
|||
padding: '10px 20px', |
|||
borderRadius: '5px', |
|||
margin: '2px', |
|||
cursor: 'pointer', |
|||
}"> |
|||
{{ project.projName || '未知项目' }} |
|||
</button> |
|||
</div> |
|||
</div> |
|||
<div v-else> |
|||
<span>没有项目信息</span> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
</el-popover> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
import { ref } from 'vue' |
|||
import { generateSampleBackground, getBloodTypeLabel } from '../../utils' |
|||
import type { TubeHolderStateMessage } from '../../../../websocket/socket' |
|||
defineProps<{ |
|||
samples: TubeHolderStateMessage['data']['tubes'] // 接收的样本数据 |
|||
selectedSamples: number[] // 当前选中的样本 |
|||
}>() |
|||
|
|||
const emits = defineEmits<{ |
|||
(e: 'updateSelectedSamples', sampleIds: number[]): void |
|||
}>() |
|||
|
|||
// 当前选中样本ID列表 |
|||
const selectedSampleIds = ref<number[]>([]) |
|||
|
|||
// 样本选中样式 |
|||
const selectedStyle = { |
|||
outline: '3px solid #4A90E2', // 高亮选中样式 |
|||
boxShadow: '0 0 10px rgba(74, 144, 226, 0.6)', |
|||
} |
|||
|
|||
// 切换样本选择状态 |
|||
const toggleSampleSelection = (sampleId: number) => { |
|||
const index = selectedSampleIds.value.indexOf(sampleId) |
|||
if (index === -1) { |
|||
selectedSampleIds.value.push(sampleId) // 选中样本 |
|||
} else { |
|||
selectedSampleIds.value.splice(index, 1) // 取消选中 |
|||
} |
|||
emits('updateSelectedSamples', selectedSampleIds.value) |
|||
} |
|||
|
|||
// 获取选中样本的样式 |
|||
const getActiveStyle = (tubeIndex: number) => { |
|||
return selectedSampleIds.value.includes(tubeIndex) ? selectedStyle : {} |
|||
} |
|||
|
|||
// 格式化状态信息 |
|||
// const formatState = (state: TubeState): string => { |
|||
// const stateMap: Record<TubeState, string> = { |
|||
// EMPTY: '空', |
|||
// TO_BE_PROCESSED: '待处理', |
|||
// PENDING: '挂起', |
|||
// RESOURCE_IS_READY: '资源准备好', |
|||
// PRE_PROCESSING: '预处理', |
|||
// PRE_PROCESSED: '预处理完成', |
|||
// PROCESSING: '处理中', |
|||
// PROCESSED: '已处理', |
|||
// POST_PROCESSING: '后处理', |
|||
// POST_PROCESSED: '后处理完成', |
|||
// PROCESS_COMPLETE: '处理完成', |
|||
// ERROR: '错误', |
|||
// } |
|||
// return stateMap[state] || '未知状态' |
|||
// } |
|||
|
|||
// 获取弹出框样式 |
|||
const getPopoverStyle = () => ({ |
|||
width: '150px', |
|||
display: 'flex', |
|||
flexDirection: 'column', |
|||
justifyContent: 'space-between', |
|||
alignItems: 'center', |
|||
}) |
|||
|
|||
// 显示弹出框 |
|||
const showPopover = (index: number) => { |
|||
const popover = 'popover-' + index |
|||
const popoverElement = document.querySelector( |
|||
`[aria-describedby="${popover}"]`, |
|||
) |
|||
if (popoverElement instanceof HTMLElement) { |
|||
popoverElement.click() |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style scoped lang="less"> |
|||
.samples { |
|||
display: flex; |
|||
align-items: center; |
|||
flex-wrap: nowrap; |
|||
background-color: #fafafa; |
|||
padding: 10px; |
|||
|
|||
&::-webkit-scrollbar { |
|||
height: 6px; |
|||
} |
|||
|
|||
&::-webkit-scrollbar-track { |
|||
background: #f1f1f1; |
|||
border-radius: 3px; |
|||
} |
|||
|
|||
&::-webkit-scrollbar-thumb { |
|||
background: #c1c1c1; |
|||
border-radius: 3px; |
|||
|
|||
&:hover { |
|||
background: #a8a8a8; |
|||
} |
|||
} |
|||
|
|||
.item-detail { |
|||
width: 100%; |
|||
height: auto; |
|||
display: flex; |
|||
flex-direction: column; |
|||
gap: 5px; |
|||
margin: 10px 0; |
|||
} |
|||
|
|||
.sample-item { |
|||
width: 70px; |
|||
height: 70px; |
|||
border-radius: 50%; |
|||
border: 2px solid black; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
position: relative; |
|||
margin: 1.5px; |
|||
flex-direction: column; |
|||
|
|||
.sample-number { |
|||
position: absolute; |
|||
top: -28px; |
|||
font-size: 14px; |
|||
font-weight: bold; |
|||
color: #333; |
|||
} |
|||
|
|||
.sample-blood-type { |
|||
color: black; |
|||
font-size: 26px; |
|||
font-weight: bold; |
|||
} |
|||
|
|||
.sample-state { |
|||
font-size: 16px; |
|||
color: gray; |
|||
} |
|||
} |
|||
} |
|||
</style> |
@ -1,4 +1,4 @@ |
|||
export { default as SampleDisplay } from './SampleDisplay.vue' |
|||
|
|||
export { default as PlateDisplay } from './PlateDisplay.vue' |
|||
export { default as LittleBufferDisplay } from './LittleBufferDisplay.vue' |
|||
export { default as EmergencyResultDialog } from './EmergencyResultDialog.vue' |
@ -1,35 +0,0 @@ |
|||
import type { ReactionPlate, ProjectInfo } from '../../../types/Index' |
|||
|
|||
/** |
|||
* 根据传入的项目数组(ReactionPlate 或 ProjectInfo)生成背景样式 |
|||
* @param projects - ReactionPlate[] 或 ProjectInfo[] 类型的数组,用于提供颜色信息 |
|||
* @param defaultColor - 当数组为空时的默认背景颜色 |
|||
* @returns 包含背景样式的对象 |
|||
*/ |
|||
export const generateSampleBackground = ( |
|||
projects: ReactionPlate[] | ProjectInfo[], |
|||
defaultColor = '#e9e9e9', |
|||
): { backgroundColor?: string; background?: string } => { |
|||
// 提取颜色信息,兼容两种数据结构
|
|||
const colors = projects.map((item) => item.color) |
|||
|
|||
if (!colors.length) return { backgroundColor: defaultColor } |
|||
|
|||
if (colors.length === 1) { |
|||
// 只有一种颜色时,直接设置背景颜色
|
|||
return { backgroundColor: colors[0] } |
|||
} else { |
|||
// 多种颜色时,生成等分颜色显示
|
|||
const colorStops = colors |
|||
.map((color, index) => { |
|||
const start = (index / colors.length) * 100 |
|||
const end = ((index + 1) / colors.length) * 100 |
|||
return `${color} ${start}% ${end}%` |
|||
}) |
|||
.join(', ') |
|||
|
|||
return { |
|||
background: `conic-gradient(${colorStops})`, |
|||
} |
|||
} |
|||
} |
@ -1,15 +0,0 @@ |
|||
/** |
|||
* 获取血液类型标签 |
|||
* @param bloodType - 要转换的血液类型字符串 |
|||
* @param bloodTypeMap - 自定义的血液类型映射对象 |
|||
* @returns 转换后的血液类型标签 |
|||
*/ |
|||
export const getBloodTypeLabel = ( |
|||
bloodType: string, |
|||
bloodTypeMap: { [key: string]: string } = { |
|||
WHOLE_BLOOD: '全血', |
|||
SERUM_OR_PLASMA: '血清/血浆', |
|||
}, |
|||
): string => { |
|||
return bloodTypeMap[bloodType] || '空' |
|||
} |
@ -1,45 +0,0 @@ |
|||
import { TubeRack, TubeSetting } from '../../../types/Index' |
|||
import { ConsumableGroupBase } from '../../../websocket/socket' |
|||
|
|||
/** |
|||
* 处理试管设置数据,将每个 `projId` 映射到 `ReactionPlate` 类型 |
|||
* @param tubeSettings - 试管架的 tubeSettings 列表 |
|||
* @param plates - ReactionPlate 类型的数组,用于匹配 projId |
|||
* @param getBloodTypeLabel - 用于转换 bloodType 的函数 |
|||
* @returns 处理后的试管设置列表 |
|||
*/ |
|||
|
|||
export function processTubeSettings( |
|||
tubeSettings: TubeSetting[], |
|||
plates: ConsumableGroupBase[], |
|||
getBloodTypeLabel: (bloodType: string) => string, |
|||
): TubeRack[] { |
|||
//@ts-ignore
|
|||
return tubeSettings.map((setting) => { |
|||
const newProjId = setting.projId.map((id) => { |
|||
// 如果 id 是对象,直接返回;否则查找匹配的 ReactionPlate
|
|||
if (typeof id === 'object') { |
|||
return id |
|||
} |
|||
|
|||
const project = plates.find((plate) => plate.projId === id) |
|||
return ( |
|||
project || { |
|||
projId: id, |
|||
projName: '', |
|||
projShortName: '', |
|||
lotId: '', |
|||
color: '#e9e9e9', |
|||
enable: false, |
|||
num: 0, |
|||
} |
|||
) |
|||
}) |
|||
|
|||
return { |
|||
...setting, |
|||
projId: newProjId, |
|||
bloodType: getBloodTypeLabel(setting.bloodType), |
|||
} |
|||
}) |
|||
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue