Browse Source

Merge branch 'dev' into feature/debugger_250106

feature/user-0111
LiLongLong 7 months ago
parent
commit
066d879f8d
  1. 3
      src/assets/icon_cross.svg
  2. 3
      src/assets/icon_cross_gray.svg
  3. 2
      src/pages/Index/Index.vue
  4. 212
      src/pages/Index/Regular/TestTube.vue
  5. 435
      src/pages/Index/TestTube/ChangeUser.vue
  6. 374
      src/pages/Index/TestTube/TubeUserId.vue
  7. 4
      src/pages/Index/components/Running/EmergencyResultDialog.vue
  8. 93
      src/pages/Index/components/TestTube/TestTubeRack.vue
  9. 80
      src/pages/Index/components/TestTube/Tube.vue
  10. 2
      src/router/router.ts
  11. 2
      src/services/Index/index.ts
  12. 11
      src/services/Index/testTube.ts
  13. 39
      src/store/modules/testTube.ts
  14. 24
      src/types/Index/TestTube.ts

3
src/assets/icon_cross.svg

@ -0,0 +1,3 @@
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.3571 11L22 19.6429L19.6429 22L11 13.3571L2.35714 22L0 19.6429L8.64286 11L1.40497e-06 2.35714L2.35714 0L11 8.64286L19.6429 0L22 2.35714L13.3571 11Z" fill="#FA5151"/>
</svg>

3
src/assets/icon_cross_gray.svg

@ -0,0 +1,3 @@
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.3571 11L22 19.6429L19.6429 22L11 13.3571L2.35714 22L0 19.6429L8.64286 11L1.40497e-06 2.35714L2.35714 0L11 8.64286L19.6429 0L22 2.35714L13.3571 11Z" fill="#999999"/>
</svg>

2
src/pages/Index/Index.vue

@ -33,7 +33,7 @@
<!-- 主内容区域 -->
<main class="main-content">
<router-view v-slot="{ Component }">
<keep-alive>
<keep-alive :exclude="['TubeUserId']">
<component :is="Component" />
</keep-alive>
</router-view>

212
src/pages/Index/Regular/TestTube.vue

@ -12,8 +12,10 @@
:index="index"
:projects="projectsAvailable"
:bloodTypes="bloodTypes"
@delete:rack="deleteHandle"
@clickTubeItem="updateTubeSettingsHandler"
@delete:rack="deleteTubeRack"
@active:rack="handleActivateChange"
@patient:edit="handleChangeUser"
@clickTubeItem="updateTubeSettings"
/>
</div>
<!-- 添加试管架按钮 -->
@ -31,12 +33,14 @@
<div v-for="proj in projectsAvailable" :key="proj.projName">
<div
class="project-item"
:class="{ active: isElemActive(proj) }"
:style="elemStyle(proj)"
:class="{ active: isProjElemActive(proj) }"
:style="styleOfProjElem(proj)"
@click="clickProjectItem(proj)"
>
<span class="proj-name">{{ proj.projName }}</span>
<span class="proj-num">{{ proj.num }}</span>
<span class="proj-num">{{
`${projIdCntMap[proj.projId] || 0}/${proj.num}`
}}</span>
</div>
</div>
</div>
@ -60,7 +64,7 @@
</template>
<script setup>
import { ref, onMounted, nextTick, computed } from 'vue'
import { ref, onMounted, nextTick, computed, onActivated } from 'vue'
import * as R from 'ramda'
import { useRouter } from 'vue-router'
import TestTubeRack from '../Components/TestTube/TestTubeRack.vue'
@ -70,7 +74,8 @@ import {
deleteTube,
updateTubeConfig,
getBloodTypes,
} from '../../../services/Index/Test-tube/test-tube'
updateTubeActivationStatus,
} from '../../../services/Index/testTube'
// import type { DataItem, TubeSetting } from '../../../types/Index/index'
// import { ConsumableGroupBase } from '../../../websocket/socket'
import {
@ -83,15 +88,13 @@ import { ElMessage } from 'element-plus'
const router = useRouter()
const testTubeStore = useTestTubeStore()
const settingTestTubeStore = useSettingTestTubeStore()
// const settingTestTubeStore = useSettingTestTubeStore()
const consumables = useConsumablesStore()
const tubeRacks = ref([]) //<DataItem[]>
const bloodTypes = ref([])
const loading = ref(false) //
const selectedUUID = ref('') //<string>
const selectedProjIds = ref([]) // number[]
const selectedBloodTypeKey = ref('WHOLE_BLOOD')
@ -101,15 +104,19 @@ onMounted(() => {
getBloodTypeData()
})
onActivated(() => {
// console.log('TestTube activated')
})
const onClickTubeItem = (rackIdx, tubeIdx) => {
console.log(rackIdx, tubeIdx)
}
const isElemActive = (proj) => {
const isProjElemActive = (proj) => {
return selectedProjIds.value.includes(proj.projId)
}
const elemStyle = (proj) => {
const active = isElemActive(proj)
const styleOfProjElem = (proj) => {
const active = isProjElemActive(proj)
if (active) {
return {
border: 'solid 1px #FFF',
@ -159,6 +166,31 @@ const projectsAvailable = computed(() => {
return projArr
})
const projIdCntMap = computed(() => {
return R.reduce(
(acc, curr) => {
const projIdCntRack = R.reduce(
(ac, cur) => {
cur.projId.forEach((pId) => {
ac[pId] = (ac[pId] || 0) + 1
})
return ac
},
{},
curr.tubeSettings,
)
Object.keys(projIdCntRack).forEach((k) => {
acc[k] = (acc[k] || 0) + projIdCntRack[k]
})
return acc
},
{},
tubeRacks.value,
)
})
const getBloodTypeData = async () => {
loading.value = true
const res = await getBloodTypes()
@ -193,26 +225,35 @@ const getTubeData = async () => {
}
loading.value = false
}
const selectedProject = ref(null)
//
const handleChangeUser = async (uuid) => {
selectedUUID.value = uuid
const handleChangeUser = async (index) => {
const rack = tubeRacks.value[index]
if (rack.state !== 'INACTIVE') {
ElMessage({
message: '试管架处理激活状态,不可修改',
type: 'error',
})
return
}
testTubeStore.setTubeRack(rack)
testTubeStore.setBloodTypes(bloodTypes.value)
const selectedTube = tubeRacks.value.find(
(t) => t.uuid === selectedUUID.value,
)
if (!selectedTube) return
testTubeStore.setTubeInfo(selectedTube)
await updateTubeSettingsHandler()
router.push({
path: '/index/change-user',
})
}
//
const deleteHandle = async (idx) => {
const deleteTubeRack = async (idx) => {
const rack = tubeRacks.value[idx]
if (rack.state !== 'INACTIVE') {
ElMessage({
message: '试管架处理激活状态,不可删除',
type: 'error',
})
return
}
const res = await deleteTube(rack.uuid)
if (res.success) {
tubeRacks.value = tubeRacks.value.filter((v, i) => i !== idx)
@ -245,106 +286,48 @@ const addTubeRack = async () => {
}
//
const handleActivateChange = (update) => {
//: { uuid: string; active: boolean }
const rack = tubeRacks.value.find((t) => t.uuid === update.uuid)
if (rack) {
rack.active = update.active
const handleActivateChange = async (index) => {
const rack = tubeRacks.value[index]
if (rack.state === 'LOCKED') {
ElMessage({
message: '试管已锁定,不能修改',
type: 'error',
})
return
}
}
const handleSampleUpdate = async ({
projectIds, // number[]
bloodType, // string
}) => {
// Promise<void>
if (selectedSampleIdsInParent.value.length > 0) {
// tubeRacks
const updatedTubeRacks = JSON.parse(JSON.stringify(tubeRacks.value))
// tubeRack
const targetTubeRack = updatedTubeRacks.find(
// t: DataItem
(t) => t.uuid === UUID.value,
)
if (targetTubeRack) {
targetTubeRack.tubeSettings = targetTubeRack.tubeSettings.map(
//tube; TubeSetting
(tube) => {
if (selectedSampleIdsInParent.value.includes(tube.tubeIndex)) {
return {
...tube,
projId: [...projectIds],
bloodType: bloodType,
}
}
return tube
},
)
//
tubeRacks.value = []
await nextTick()
tubeRacks.value = updatedTubeRacks //
console.log(
'🚀 ~ handleSampleUpdate ~ updatedTubeRacks:',
updatedTubeRacks,
)
const selectedTube = updatedTubeRacks.find((t) => t.uuid === UUID.value)
console.log('🚀 ~ handleSampleUpdate ~ selectedTube:', selectedTube)
testTubeStore.setTubeInfo(selectedTube)
console.log(
'🚀 ~ handleSampleUpdate ~ testTubeStore.$state.tubeInfo:',
testTubeStore.$state.tubeInfo,
)
selectedProject.value = null //
await nextTick()
ElMessage({
message: '样本信息已更新',
type: 'success',
duration: 2000,
})
}
let destState = rack.state === 'INACTIVE' ? 'ACTIVE' : 'INACTIVE'
const res = await updateTubeActivationStatus({
uuid: rack.uuid,
active: destState === 'ACTIVE',
})
if (res.success) {
tubeRacks.value = tubeRacks.value.map((rack, idx) => {
if (idx === index) {
rack.state = destState
}
return rack
})
} else {
ElMessage({
message: '请先选择样本后再选择项目',
type: 'warning',
duration: 2000,
message: '更改激活状态失败',
type: 'error',
})
}
}
// ID
const selectedSampleIdsInParent = ref([]) //<number[]>
const showProjectSelector = ref(false)
//
const handleUpdateSelectedSamples = ({
sampleIds, //number[]
uuid,
}) => {
//
const tubeRack = tubeRacks.value.find((tube) => tube.uuid === uuid)
if (tubeRack) {
// sampleIds
if (sampleIds.length === 0) {
showProjectSelector.value = false
selectedSampleIdsInParent.value = []
} else {
selectedSampleIdsInParent.value = sampleIds
showProjectSelector.value = true
}
}
}
//
const updateTubeSettingsHandler = async (rackIdx, tubeIdx) => {
const updateTubeSettings = async (rackIdx, tubeIdx) => {
if (!selectedBloodTypeKey.value) {
ElMessage.error('请选择血液类型')
return
}
const rack = tubeRacks.value[rackIdx]
if (rack.state !== 'INACTIVE') {
ElMessage({
message: '试管架已激活,不能修改',
type: 'error',
})
return
}
if (tubeIdx < rack.tubeSettings.length) {
const setting = rack.tubeSettings[tubeIdx]
const updSetting = {
@ -355,7 +338,7 @@ const updateTubeSettingsHandler = async (rackIdx, tubeIdx) => {
try {
const response = await updateTubeConfig({
uuid: rack.uuid,
setting: updSetting,
setting: [updSetting],
})
if (response.success) {
tubeRacks.value = tubeRacks.value.map((rack, idx) => {
@ -443,7 +426,7 @@ const updateTubeSettingsHandler = async (rackIdx, tubeIdx) => {
display: flex;
.project-item {
border-radius: 8px;
padding: 6px 10px;
padding: 4px 10px;
margin-right: 8px;
display: flex;
flex-direction: column;
@ -451,6 +434,7 @@ const updateTubeSettingsHandler = async (rackIdx, tubeIdx) => {
min-width: 50px;
.proj-name {
font-weight: 600;
font-size: 1.2rem;
}
}
}
@ -466,6 +450,8 @@ const updateTubeSettingsHandler = async (rackIdx, tubeIdx) => {
padding: 10px;
margin-right: 8px;
color: @active-color;
font-weight: 600;
font-size: 1.2rem;
&.active {
color: #fff;
background-color: @active-color;

435
src/pages/Index/TestTube/ChangeUser.vue

@ -1,435 +0,0 @@
<template>
<div id="changeUser-container" :key="refreshKey">
<!-- 顶部导航 -->
<div class="header">
<div class="header-left">
<img src="@/assets/Index/left.svg" alt="返回" @click.stop="goBack" />
<span class="title">患者信息</span>
<div class="divider"></div>
<span class="tube-type">试管类型({{ tubeType }})</span>
</div>
</div>
<!-- 主要内容区域 -->
<div class="content">
<!-- 上半部分样本 -->
<div class="sample-section" v-for="box in boxes" :key="box.id">
<!-- 左侧标题栏 -->
<div class="section-labels">
<div class="label-column">
<div class="label-item">
<span class="label">序号</span>
</div>
<div class="label-item">
<span class="label">试管信息</span>
</div>
<div class="label-item">
<span class="label">样本条形码</span>
</div>
<div class="label-item">
<span class="label">用户ID</span>
</div>
</div>
</div>
<!-- 右侧样本展示 -->
<div class="samples-grid">
<div v-for="item in box.samples" :key="item.tubeIndex" class="sample-item"
:class="{ 'selected': selectedSampleId === item.tubeIndex }" @click="selectSample(item.tubeIndex)">
<div class="sample-content">
<!-- 序号 -->
<div class="item-index">{{ item.tubeIndex + 1 }}</div>
<!-- 试管圆圈 -->
<div class="sample-circle"
:style="generateSampleBackground(item.projId!.filter(proj => proj !== null) as ReactionPlate[])">
<span class="blood-type">{{ item.bloodType }}</span>
</div>
<!-- 输入框组 -->
<div class="inputs">
<input class="input-field" v-model="item.sampleBarcode" placeholder="条形码"
@focus="showKeyboard('barcode', item)" readonly />
<input class="input-field" v-model="item.userid" placeholder="用户ID"
@focus="showKeyboard('userid', item)" readonly />
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 底部按钮 -->
<div class="footer">
<button class="btn cancel" @click="goBack">取消</button>
<button class="btn confirm" @click="confirmChange">确定</button>
</div>
<!-- 键盘组件 -->
<transition name="slide-up">
<div class="keyboard-container">
<SimpleKeyboard v-if="keyboardVisible" :input="currentInputValue" @onChange="handleKeyboardInput"
@onKeyPress="handleKeyPress" />
</div>
</transition>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref, watchEffect, onUnmounted, onActivated } from 'vue'
import { useRouter } from 'vue-router'
import { useTestTubeStore, useConsumablesStore, useSettingTestTubeStore } from '../../../store'
import SimpleKeyboard from '../../../components/SimpleKeyboard.vue'
import type {
DataItem,
ReactionPlate,
TubeRack,
handleTube,
} from '../../../types/Index'
import {
getBloodTypeLabel,
processTubeSettings,
generateSampleBackground,
} from '../Utils'
import { updateTubeConfig } from '../../../services'
import { ElMessage } from 'element-plus'
import { ConsumableGroupBase } from '../../../websocket/socket'
const testTubeStore = useTestTubeStore()
const consumableStore = useConsumablesStore()
const router = useRouter()
const tubeInfo = ref<DataItem>({} as DataItem) //
const processedTubeInfo = ref<handleTube>({} as handleTube) //
const tubeSettings = ref<TubeRack[]>([]) //
const tubeType = ref<string>(testTubeStore.type || '自动') //
const plates = ref<ConsumableGroupBase[]>(consumableStore.plates) //
const selectedSampleId = ref<number | null>(null) //
const keyboardVisible = ref(false)
const currentInputValue = ref('')
const currentInput = ref<{
type: 'barcode' | 'userid',
item: any
}>({
type: 'barcode',
item: null
})
const settingTestTubeStore = useSettingTestTubeStore()
const refreshKey = ref(0)
const modifiedTubes = ref(new Map<number, any>())
onMounted(() => {
tubeInfo.value = testTubeStore.tubeInfo
console.log("🚀 ~ onMounted ~ tubeInfo.value:", tubeInfo.value)
tubeSettings.value = processTubeSettings(
tubeInfo.value.tubeSettings,
plates.value,
getBloodTypeLabel,
)
processedTubeInfo.value = {
...tubeInfo.value,
tubeSettings: tubeSettings.value,
}
})
onActivated(() => {
tubeInfo.value = testTubeStore.tubeInfo
console.log("🚀 ~ onActivated ~ tubeInfo.value:", tubeInfo.value)
tubeSettings.value = processTubeSettings(
tubeInfo.value.tubeSettings,
plates.value,
getBloodTypeLabel,
)
processedTubeInfo.value = {
...tubeInfo.value,
tubeSettings: tubeSettings.value,
}
})
//
const goBack = () => {
router.push('/index/regular/test-tube')
}
//
const selectSample = (id: number) => {
if (selectedSampleId.value === id) {
selectedSampleId.value = null
} else {
selectedSampleId.value = id
}
}
const boxes = ref([
{
id: 1,
samples: [] as TubeRack[],
},
{
id: 2,
samples: [] as TubeRack[],
},
])
watchEffect(() => {
boxes.value.forEach((item) => {
if (item.id === 1) {
item.samples = tubeSettings.value.slice(0, 5)
} else {
item.samples = tubeSettings.value.slice(5, 10)
}
})
})
//
const confirmChange = async () => {
const modifiedSettings = Array.from(modifiedTubes.value.values())
if (modifiedSettings.length > 0) {
try {
const updatePromises = modifiedSettings.map(setting =>
updateTubeConfig({
uuid: processedTubeInfo.value.uuid,
setting
})
)
const results = await Promise.all(updatePromises)
const allSuccess = results.every(res => res && res.success)
if (allSuccess) {
ElMessage.success('所有修改已保存')
goBack()
} else {
ElMessage.error('部分修改保存失败')
}
} catch (error) {
console.error('保存修改失败:', error)
ElMessage.error('保存失败')
}
} else {
ElMessage.warning('没有需要保存的修改')
}
}
//
const showKeyboard = (type: 'barcode' | 'userid', item: any) => {
keyboardVisible.value = true
currentInput.value = { type, item }
currentInputValue.value = type === 'barcode' ? item.sampleBarcode : item.userid
}
//
const handleKeyboardInput = (input: string) => {
if (!currentInput.value.item) return
if (currentInput.value.type === 'barcode') {
currentInput.value.item.sampleBarcode = input
} else {
currentInput.value.item.userid = input
}
currentInputValue.value = input
modifiedTubes.value.set(currentInput.value.item.tubeIndex, {
tubeIndex: currentInput.value.item.tubeIndex,
userid: currentInput.value.item.userid,
sampleBarcode: currentInput.value.item.sampleBarcode,
projId: currentInput.value.item.projId?.map((p: ReactionPlate) => p.projId) || [],
bloodType: currentInput.value.item.bloodType,
})
settingTestTubeStore.updateTubeSetting(tubeInfo.value.uuid, {
tubeIndex: currentInput.value.item.tubeIndex,
[currentInput.value.type === 'userid' ? 'userid' : 'sampleBarcode']: input
})
}
//
const handleKeyPress = (button: string) => {
if (button === '{enter}') {
keyboardVisible.value = false
currentInputValue.value = ''
}
}
//
onUnmounted(() => {
modifiedTubes.value.clear()
})
</script>
<style lang="less" scoped>
#changeUser-container {
width: 100%;
height: 95vh;
display: flex;
flex-direction: column;
position: relative;
background-color: #f5f7fa;
overflow: hidden;
.header {
height: 80px;
background-color: #fff;
padding: 0 20px;
display: flex;
align-items: center;
.header-left {
display: flex;
align-items: center;
gap: 12px;
img {
width: 24px;
height: 24px;
cursor: pointer;
}
.title {
font-size: 28px;
color: #303133;
}
.tube-type {
font-size: 24px;
color: #606266;
}
}
}
.content {
padding: 16px;
display: flex;
flex-direction: column;
gap: 20px;
overflow: hidden;
.sample-section {
background-color: #fff;
border-radius: 8px;
padding: 20px;
display: flex;
height: 380px;
.section-labels {
width: 120px;
display: flex;
flex-direction: column;
padding-top: 20px;
.label-column {
.label-item {
margin-bottom: 16px;
.label {
font-size: 24px;
color: #606266;
}
}
}
}
.samples-grid {
flex: 1;
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 12px;
.sample-item {
background-color: #f5f7fa;
border-radius: 4px;
padding: 12px;
.sample-content {
display: flex;
flex-direction: column;
align-items: center;
.item-index {
font-size: 22px;
color: #606266;
margin-bottom: 8px;
}
.sample-circle {
width: 60px;
height: 60px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 8px 0;
.blood-type {
font-size: 20px;
color: #fff;
}
}
.inputs {
width: 100%;
margin-top: 12px;
.input-field {
width: 100%;
height: 36px;
border: 1px solid #dcdfe6;
border-radius: 4px;
margin-bottom: 8px;
font-size: 20px;
text-align: center;
background-color: #fff;
&::placeholder {
color: #c0c4cc;
font-size: 18px;
}
}
}
}
}
}
}
}
.footer {
height: 80px;
padding: 10px 20px;
background-color: #fff;
display: flex;
justify-content: center;
gap: 16px;
.btn {
width: 320px;
height: 60px;
border-radius: 30px;
font-size: 24px;
font-weight: normal;
&.cancel {
background-color: #f5f7fa;
color: #606266;
border: 1px solid #dcdfe6;
}
&.confirm {
background-color: #409eff;
color: #fff;
}
}
}
.keyboard-container {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 20vh;
background-color: #fff;
}
//
.slide-up-enter-active,
.slide-up-leave-active {
transition: transform 0.3s ease;
}
.slide-up-enter-from,
.slide-up-leave-to {
transform: translateY(100%);
}
}
</style>

374
src/pages/Index/TestTube/TubeUserId.vue

@ -0,0 +1,374 @@
<template>
<div id="changeUser-container">
<!-- 顶部导航 -->
<div class="header">
<div class="header-left" @click.stop="goBack">
<img src="@/assets/Index/left.svg" alt="返回" />
<span class="title">患者信息</span>
</div>
</div>
<!-- 主要内容区域 -->
<div class="content">
<div class="sample-section" v-for="(group, idx) in tubeGroups" :key="idx">
<!-- 左侧标题栏 -->
<div class="section-labels">
<div class="label-column">
<div class="label-item">
<span class="label">条形码</span>
</div>
<div class="label-item">
<span class="label">用户ID</span>
</div>
</div>
</div>
<!-- 右侧样本展示 -->
<div class="samples-grid">
<div class="sample-item" v-for="item in group" :key="item.tubeIndex">
<div class="sample-content">
<!-- 序号 -->
<div class="item-index">{{ item.tubeIndex + 1 }}</div>
<!-- 试管圆圈 -->
<div class="tube-circle">
<Tube
:tube="item"
:index="item.tubeIndex"
:projects="projectsAvailable"
:bloodTypes="tubeStore.bloodTypes"
:showNum="false"
@clickTubeItem=""
/>
</div>
<!-- 输入框组 -->
<div class="inputs">
<input
class="input-field"
:class="{ onFocus: isFocused('barcode', item) }"
v-model="item.sampleBarcode"
placeholder="条形码"
@focus="showKeyboard('barcode', item)"
readonly
/>
<input
class="input-field"
:class="{ onFocus: isFocused('userid', item) }"
v-model="item.userid"
placeholder="用户ID"
@focus="showKeyboard('userid', item)"
readonly
/>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 底部按钮 -->
<div class="footer">
<button class="btn cancel" @click="goBack">取消</button>
<button class="btn confirm" @click="confirmChange">确定</button>
</div>
<!-- 键盘组件 -->
<transition name="slide-up">
<div class="keyboard-container">
<SimpleKeyboard
v-if="keyboardVisible"
:input="currentInputValue"
@onChange="handleKeyboardInput"
@onKeyPress="handleKeyPress"
/>
</div>
</transition>
</div>
</template>
<script setup>
import { computed, onActivated, ref, watch } from 'vue'
import { useRouter } from 'vue-router'
import * as R from 'ramda'
import { useTestTubeStore, useConsumablesStore } from '../../../store'
import SimpleKeyboard from '../../../components/SimpleKeyboard.vue'
import Tube from '../Components/TestTube/Tube.vue'
import { updateTubeConfig } from '../../../services'
import { ElMessage } from 'element-plus'
const router = useRouter()
const consumables = useConsumablesStore()
const tubeStore = useTestTubeStore()
defineOptions({
name: 'TubeUserId',
})
const tubeGroups = ref([
R.clone(tubeStore.tubeRack.tubeSettings.slice(0, 5)),
R.clone(tubeStore.tubeRack.tubeSettings.slice(5, 10)),
])
const projectsAvailable = computed(() => {
const group = R.groupBy((p) => p.projName, consumables.plates)
const pNames = R.keys(group)
const projArr = pNames.map((n) =>
R.reduce(
(acc, curr) => {
return { ...curr, num: acc.num + curr.num }
},
{ num: 0 },
group[n],
),
)
return projArr
})
const keyboardVisible = ref(false)
const currentInputValue = ref('')
const currentInput = ref({
type: 'barcode',
item: null,
})
const isFocused = (type, item) => {
if (
currentInput.value.item &&
item.tubeIndex === currentInput.value.item.tubeIndex &&
type === currentInput.value.type
) {
return true
}
return false
}
//
const goBack = () => {
//router.push('/index/regular/test-tube')
router.back()
}
//
const confirmChange = async () => {
const settings = R.flatten(tubeGroups.value)
const res = await updateTubeConfig({
uuid: tubeStore.tubeRack.uuid,
setting: settings,
})
if (res.success) {
tubeStore.tubeRack.tubeSettings = settings
goBack()
} else {
ElMessage.error('更新失败')
}
}
//
const showKeyboard = (type, item) => {
keyboardVisible.value = true
currentInput.value = { type, item }
currentInputValue.value =
type === 'barcode' ? item.sampleBarcode : item.userid
}
//
const handleKeyboardInput = (input) => {
if (!currentInput.value.item) return
const groupIdx = currentInput.value.item.tubeIndex < 5 ? 0 : 1
const idxOffset = groupIdx === 1 ? 5 : 0
const tubeIdx = currentInput.value.item.tubeIndex - idxOffset
if (currentInput.value.type === 'barcode') {
currentInput.value.item.sampleBarcode = input
tubeGroups.value[groupIdx][tubeIdx].sampleBarcode = input
} else {
currentInput.value.item.userid = input
tubeGroups.value[groupIdx][tubeIdx].userid = input
}
currentInputValue.value = input
// console.log(tubeGroups.value[groupIdx])
}
//
const handleKeyPress = (button) => {
if (button === '{enter}') {
keyboardVisible.value = false
currentInputValue.value = ''
}
}
</script>
<style lang="less" scoped>
#changeUser-container {
width: 100%;
height: 95vh;
display: flex;
flex-direction: column;
position: relative;
overflow: hidden;
.header {
height: 80px;
background-color: #fff;
padding: 0 20px;
display: flex;
align-items: center;
.header-left {
display: flex;
align-items: center;
gap: 12px;
img {
width: 24px;
height: 24px;
cursor: pointer;
}
.title {
font-size: 28px;
color: #303133;
}
}
}
.content {
padding: 16px;
display: flex;
flex-direction: column;
gap: 20px;
overflow: hidden;
.sample-section {
border-radius: 8px;
padding: 20px;
display: flex;
// height: 380px;
.section-labels {
width: 100px;
display: flex;
flex-direction: column-reverse;
padding-bottom: 16px;
.label-column {
> * {
box-sizing: border-box;
}
.label-item {
margin-bottom: 16px;
.label {
font-size: 24px;
font-weight: 600;
color: #606266;
}
}
}
}
.samples-grid {
flex: 1;
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 12px;
.sample-item {
// background-color: #f5f7fa;
border-radius: 12px;
padding: 12px;
.sample-content {
display: flex;
flex-direction: column;
align-items: center;
.item-index {
font-size: 22px;
color: #333;
margin-bottom: 8px;
}
.tube-circle {
height: 80px;
}
.inputs {
width: 100%;
margin-top: 24px;
.input-field {
&.onFocus {
border: solid 2px #333;
}
width: 100%;
height: 36px;
border: 1px solid #aaa;
border-radius: 4px;
margin-bottom: 14px;
font-size: 20px;
font-weight: 600;
text-align: center;
background-color: #fff;
&::placeholder {
color: #c0c4cc;
font-size: 18px;
}
}
}
}
}
}
}
}
.footer {
height: 80px;
padding: 10px 20px;
background-color: #fff;
display: flex;
justify-content: center;
gap: 16px;
.btn {
width: 320px;
height: 60px;
border-radius: 30px;
font-size: 24px;
font-weight: normal;
&.cancel {
background-color: #f5f7fa;
color: #606266;
border: 1px solid #dcdfe6;
}
&.confirm {
background-color: #409eff;
color: #fff;
}
}
}
.keyboard-container {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 20vh;
background-color: #fff;
}
//
.slide-up-enter-active,
.slide-up-leave-active {
transition: transform 0.3s ease;
}
.slide-up-enter-from,
.slide-up-leave-to {
transform: translateY(100%);
}
}
</style>

4
src/pages/Index/components/Running/EmergencyResultDialog.vue

@ -42,11 +42,11 @@
defineProps({
result: {
type: Object,
required: true,
// required: true,
},
visible: {
type: Boolean,
required: true,
// required: true,
},
})

93
src/pages/Index/components/TestTube/TestTubeRack.vue

@ -1,9 +1,13 @@
<template>
<div class="test-tube-rack">
<div class="test-tube-rack" :class="{ locked: tubeRack.state === 'LOCKED' }">
<!--状态-->
<div class="tube-rack-state">
<div class="tube-rack-state" @click="$emit('active:rack', index)">
<div class="status-icon">
<img src="@/assets/Vector.svg" alt="Status Icon" v-if="tubeRack.state==='INACTIVE'" />
<img
src="@/assets/Vector.svg"
alt="Status Icon"
v-if="tubeRack.state === 'INACTIVE'"
/>
<img src="@/assets/Active-Vector.svg" alt="Status Icon" v-else />
</div>
</div>
@ -11,24 +15,41 @@
<!--试管区-->
<section class="tube-list">
<div v-for="(tube, idx) in tubeRack.tubeSettings">
<Tube :tube="tube" :index="idx" :projects="projects" :bloodTypes="bloodTypes" @clickTubeItem="onClickTubeItem" />
<Tube
:tube="tube"
:index="idx"
:projects="projects"
:bloodTypes="bloodTypes"
@clickTubeItem="onClickTubeItem"
/>
</div>
</section>
</div>
<div class="test-tube-rack-op">
<div class="rack-op">修改试管架</div>
<div class="rack-op">编辑患者信息</div>
<div class="rack-op" @click="$emit('delete:rack', index)">删除试管架</div>
<div
class="test-tube-rack-op"
:class="{ active: tubeRack.state !== 'INACTIVE' }"
>
<div class="rack-op rack-edit">修改试管架</div>
<div class="rack-op" @click="$emit('patient:edit', index)">编辑患者信息</div>
<div class="rack-op rack-del" @click="$emit('delete:rack', index)">
<img
v-if="tubeRack.state === 'INACTIVE'"
src="@/assets/icon_cross.svg"
alt="del"
/>
<img v-else src="@/assets/icon_cross_gray.svg" alt="del" />
<span>删除试管架</span>
</div>
</div>
</template>
<script setup>
import Tube from './Tube.vue'
const props = defineProps(['tubeRack', 'index', 'projects', 'bloodTypes'])
const emit = defineEmits(['delete:rack', 'clickTubeItem'])
const emit = defineEmits(['delete:rack', 'active:rack','patient:edit', 'clickTubeItem'])
const onClickTubeItem = (idx) => {
emit('clickTubeItem', props.index, idx)
emit('clickTubeItem', props.index, idx)
}
</script>
@ -38,18 +59,24 @@ const onClickTubeItem = (idx) => {
height: 150px;
display: flex;
align-items: center;
&.locked {
background-color: #C8C8C8;
}
.tube-rack-state {
align-self: stretch;
width: 100px;
display: flex;
align-items: center;
justify-content: center;
.status-icon img {
width: 56px;
width: 56px;
}
}
.split {
width: 1px;
height: 80px;
background-color: rgb(192,192,192);
background-color: rgb(192, 192, 192);
margin-right: 10px;
}
@ -61,18 +88,38 @@ const onClickTubeItem = (idx) => {
}
@op-bar-height: 68px;
.test-tube-rack-op {
height: @op-bar-height;
background-color: rgb(235,235,235);
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 40px;
height: @op-bar-height;
background-color: rgb(235, 235, 235);
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 40px;
font-size: 22px;
font-weight: 600;
font-size: 22px;
font-weight: 600;
&.active {
color: #999;
.rack-op {
align-self: stretch;
line-height: @op-bar-height;
&.rack-del {
color: #999;
}
}
}
.rack-op {
align-self: stretch;
line-height: @op-bar-height;
&.rack-edit {
color: #999;
}
&.rack-del {
display: flex;
align-items: center;
img {
width: 20px;
margin-right: 8px;
}
color: red;
}
}
}
</style>

80
src/pages/Index/components/TestTube/Tube.vue

@ -1,6 +1,6 @@
<template>
<div class="tube-item" @click="$emit('clickTubeItem', index)">
<span class="order">{{ index + 1 }}</span>
<span v-if="!!showNum" class="order">{{ index + 1 }}</span>
<div class="tube-circle" v-if="tube.projId.length === 0">
<span class="add-symbol">+</span>
</div>
@ -21,7 +21,7 @@
</div>
</div>
<span class="user-id">{{ tube.userid || '-' }}</span>
<span v-if="showNum" class="user-id">{{ tube.userid || '-' }}</span>
</div>
</template>
@ -34,7 +34,21 @@ tube: {userid,projId:number[],bloodType}
projects: [{projId,projName,color}]
bloodTypes: [{key,name}]
*/
const props = defineProps(['tube', 'index', 'projects', 'bloodTypes'])
const props = defineProps({
tube: {
type: Object,
},
index: {
type: Number,
},
projects: {
type: Array,
},
bloodTypes: {
type: Array,
},
showNum: { type: Boolean, default: true },
})
const emit = defineEmits(['clickTubeItem'])
const projIdMap = computed(() => {
@ -108,37 +122,43 @@ function drawPieChart(ctx, data) {
</script>
<style scoped lang="less">
.tube-circle {
width: 72px;
height: 72px;
border-radius: 999px;
border: solid 1px gray;
.tube-item {
display: flex;
flex-direction: column;
.tube-circle {
width: 72px;
height: 72px;
border-radius: 999px;
border: solid 1px gray;
background-color: #fff;
background-color: #fff;
display: flex;
justify-content: center;
align-items: center;
.add-symbol {
color: black;
font-size: 20px;
}
}
.tube-project {
position: relative;
.text-layer {
position: absolute;
left: 50%;
top: 50%;
transform: translateX(-50%) translateY(-55%);
display: flex;
flex-direction: column;
> * {
height: 22px;
text-wrap: nowrap;
justify-content: center;
align-items: center;
.add-symbol {
color: black;
font-size: 20px;
}
.blood-text {
color: #fff;
margin-bottom: 6px;
}
.tube-project {
position: relative;
.text-layer {
position: absolute;
left: 50%;
top: 50%;
transform: translateX(-50%) translateY(-55%);
display: flex;
flex-direction: column;
color: red;
> * {
height: 22px;
text-wrap: nowrap;
}
.blood-text {
color: #fff;
}
}
}
}

2
src/router/router.ts

@ -74,7 +74,7 @@ const routes = [
},
{
path: 'change-user', //编辑患者页面
component: () => import('@/pages/Index/TestTube/ChangeUser.vue'),
component: () => import('@/pages/Index/TestTube/TubeUserId.vue'),
},
],
},

2
src/services/Index/index.ts

@ -1,7 +1,7 @@
export * from './regular'
export * from './emergency'
export * from './history'
export * from './Test-tube/test-tube'
export * from './testTube'
export * from './user-manage'
export * from './idCard'
export * from './init'

11
src/services/Index/Test-tube/test-tube.ts → src/services/Index/testTube.ts

@ -1,5 +1,5 @@
import apiClient from '../../../utils/axios'
import type { TubeActivationStatus, TubeSetting } from '../../../types/Index'
import apiClient from '../../utils/axios'
import type { TubeActivationStatus, TubeSetting } from '../../types/Index'
/*
"data": [
{
@ -8,7 +8,7 @@ import type { TubeActivationStatus, TubeSetting } from '../../../types/Index'
},
{
"key": "SERUM_OR_PLASMA",
"name": "血清或者血浆"
"name": "血清/血浆"
}
]
*/
@ -88,7 +88,8 @@ export const updateTubeActivationStatus = async (
try {
const res = await apiClient.post(
'/api/v1/app/appTubeSettingMgr/updateActiveState',
data,
null,
{ params: data },
)
return res.data
} catch (error) {
@ -98,7 +99,7 @@ export const updateTubeActivationStatus = async (
// 更新试管配置
export const updateTubeConfig = async (config: {
uuid: string
setting: TubeSetting
setting: TubeSetting[]
}) => {
console.log('🚀 ~ updateTubeConfig ~ config:', config)
try {

39
src/store/modules/testTube.ts

@ -1,36 +1,33 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import type { DataItem } from '../../types/Index'
import type { TestTubeRack } from '../../types/Index'
export const useTestTubeStore = defineStore(
'testTube',
() => {
const tubeInfo = ref<DataItem>({} as DataItem)
const type = ref<string>('')
const projectSettings = ref<Record<string, string>>({})
const setTubeInfo = (info: DataItem) => {
tubeInfo.value = info
const tubeRack = ref<TestTubeRack>({} as TestTubeRack)
const bloodTypes = ref<{ key: string; name: string }[]>([])
// const projectSettings = ref<Record<string, string>>({})
const setTubeRack = (rack: TestTubeRack) => {
tubeRack.value = rack
}
const setTypeInfo = (typeInfo: string) => {
type.value = typeInfo
const setBloodTypes = (types: { key: string; name: string }[]) => {
bloodTypes.value = types
}
// 设置指定试管架的项目设置
const setProjectSetting = (uuid: string, setting: string) => {
projectSettings.value[uuid] = setting
}
// const setProjectSetting = (uuid: string, setting: string) => {
// projectSettings.value[uuid] = setting
// }
// 获取指定试管架的项目设置,默认返回 '自动'
const getProjectSetting = (uuid: string): string => {
return projectSettings.value[uuid] || '自动'
}
// const getProjectSetting = (uuid: string): string => {
// return projectSettings.value[uuid] || '自动'
// }
// const bloodTypes = ref([])
return {
type,
tubeInfo,
setTubeInfo,
setTypeInfo,
projectSettings,
setProjectSetting,
getProjectSetting,
tubeRack,
setTubeRack,
bloodTypes,
setBloodTypes,
}
},
{

24
src/types/Index/TestTube.ts

@ -5,7 +5,7 @@ export interface TubeSetting {
userid: string
sampleBarcode: string
projId: number[]
bloodType: string
bloodType: 'WHOLE_BLOOD' | 'SERUM_OR_PLASMA'
}
// 更新后的类型定义
export interface Project {
@ -17,23 +17,15 @@ export interface Project {
reactionTemperature: number
color: string
}
export interface DataItem {
export interface TestTubeRack {
uuid: string
active: boolean
state: 'INACTIVE' | 'ACTIVE' | 'LOCKED'
tubeSettings: TubeSetting[]
lock: boolean
// selectedSampleIds?: Array<number>
}
export interface handleTube {
uuid: string
active: boolean
tubeSettings: TubeRack[]
lock: boolean
}
export interface ApiResponse {
dataType: string
data: DataItem[]
data: TestTubeRack[]
timestamp: number
success: boolean
}
@ -50,14 +42,6 @@ export interface TubeActivationStatus {
active: boolean
}
export interface Setting {
tubeIndex: number
userid: string
sampleBarcode: string
projId: number[]
bloodType: string
}
export interface UpdateTubeSettingsResponse {
success: boolean
message?: string

Loading…
Cancel
Save