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.

435 lines
11 KiB

8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
  1. <template>
  2. <div id="changeUser-container" :key="refreshKey">
  3. <!-- 顶部导航 -->
  4. <div class="header">
  5. <div class="header-left">
  6. <img src="@/assets/Index/left.svg" alt="返回" @click.stop="goBack" />
  7. <span class="title">患者信息</span>
  8. <div class="divider"></div>
  9. <span class="tube-type">试管类型({{ tubeType }})</span>
  10. </div>
  11. </div>
  12. <!-- 主要内容区域 -->
  13. <div class="content">
  14. <!-- 上半部分样本 -->
  15. <div class="sample-section" v-for="box in boxes" :key="box.id">
  16. <!-- 左侧标题栏 -->
  17. <div class="section-labels">
  18. <div class="label-column">
  19. <div class="label-item">
  20. <span class="label">序号</span>
  21. </div>
  22. <div class="label-item">
  23. <span class="label">试管信息</span>
  24. </div>
  25. <div class="label-item">
  26. <span class="label">样本条形码</span>
  27. </div>
  28. <div class="label-item">
  29. <span class="label">用户ID</span>
  30. </div>
  31. </div>
  32. </div>
  33. <!-- 右侧样本展示 -->
  34. <div class="samples-grid">
  35. <div v-for="item in box.samples" :key="item.tubeIndex" class="sample-item"
  36. :class="{ 'selected': selectedSampleId === item.tubeIndex }" @click="selectSample(item.tubeIndex)">
  37. <div class="sample-content">
  38. <!-- 序号 -->
  39. <div class="item-index">{{ item.tubeIndex + 1 }}</div>
  40. <!-- 试管圆圈 -->
  41. <div class="sample-circle"
  42. :style="generateSampleBackground(item.projId!.filter(proj => proj !== null) as ReactionPlate[])">
  43. <span class="blood-type">{{ item.bloodType }}</span>
  44. </div>
  45. <!-- 输入框组 -->
  46. <div class="inputs">
  47. <input class="input-field" v-model="item.sampleBarcode" placeholder="条形码"
  48. @focus="showKeyboard('barcode', item)" readonly />
  49. <input class="input-field" v-model="item.userid" placeholder="用户ID"
  50. @focus="showKeyboard('userid', item)" readonly />
  51. </div>
  52. </div>
  53. </div>
  54. </div>
  55. </div>
  56. </div>
  57. <!-- 底部按钮 -->
  58. <div class="footer">
  59. <button class="btn cancel" @click="goBack">取消</button>
  60. <button class="btn confirm" @click="confirmChange">确定</button>
  61. </div>
  62. <!-- 键盘组件 -->
  63. <transition name="slide-up">
  64. <div class="keyboard-container">
  65. <SimpleKeyboard v-if="keyboardVisible" :input="currentInputValue" @onChange="handleKeyboardInput"
  66. @onKeyPress="handleKeyPress" />
  67. </div>
  68. </transition>
  69. </div>
  70. </template>
  71. <script setup lang="ts">
  72. import { onMounted, ref, watchEffect, onUnmounted, onActivated } from 'vue'
  73. import { useRouter } from 'vue-router'
  74. import { useTestTubeStore, useConsumablesStore, useSettingTestTubeStore } from '../../../store'
  75. import SimpleKeyboard from '../../../components/SimpleKeyboard.vue'
  76. import type {
  77. DataItem,
  78. ReactionPlate,
  79. TubeRack,
  80. handleTube,
  81. } from '../../../types/Index'
  82. import {
  83. getBloodTypeLabel,
  84. processTubeSettings,
  85. generateSampleBackground,
  86. } from '../Utils'
  87. import { updateTubeConfig } from '../../../services'
  88. import { ElMessage } from 'element-plus'
  89. import { ConsumableGroupBase } from '../../../websocket/socket'
  90. const testTubeStore = useTestTubeStore()
  91. const consumableStore = useConsumablesStore()
  92. const router = useRouter()
  93. const tubeInfo = ref<DataItem>({} as DataItem) //试管架信息
  94. const processedTubeInfo = ref<handleTube>({} as handleTube) //经过清洗的试管架信息
  95. const tubeSettings = ref<TubeRack[]>([]) //获取到的试管信息
  96. const tubeType = ref<string>(testTubeStore.type || '自动') //导航栏的试管类型
  97. const plates = ref<ConsumableGroupBase[]>(consumableStore.plates) //反应板信息
  98. const selectedSampleId = ref<number | null>(null) //选中的试管
  99. const keyboardVisible = ref(false)
  100. const currentInputValue = ref('')
  101. const currentInput = ref<{
  102. type: 'barcode' | 'userid',
  103. item: any
  104. }>({
  105. type: 'barcode',
  106. item: null
  107. })
  108. const settingTestTubeStore = useSettingTestTubeStore()
  109. const refreshKey = ref(0)
  110. const modifiedTubes = ref(new Map<number, any>())
  111. onMounted(() => {
  112. tubeInfo.value = testTubeStore.tubeInfo
  113. console.log("🚀 ~ onMounted ~ tubeInfo.value:", tubeInfo.value)
  114. tubeSettings.value = processTubeSettings(
  115. tubeInfo.value.tubeSettings,
  116. plates.value,
  117. getBloodTypeLabel,
  118. )
  119. processedTubeInfo.value = {
  120. ...tubeInfo.value,
  121. tubeSettings: tubeSettings.value,
  122. }
  123. })
  124. onActivated(() => {
  125. tubeInfo.value = testTubeStore.tubeInfo
  126. console.log("🚀 ~ onActivated ~ tubeInfo.value:", tubeInfo.value)
  127. tubeSettings.value = processTubeSettings(
  128. tubeInfo.value.tubeSettings,
  129. plates.value,
  130. getBloodTypeLabel,
  131. )
  132. processedTubeInfo.value = {
  133. ...tubeInfo.value,
  134. tubeSettings: tubeSettings.value,
  135. }
  136. })
  137. //返回试管页面
  138. const goBack = () => {
  139. router.push('/index/regular/test-tube')
  140. }
  141. // 选择样本的函数
  142. const selectSample = (id: number) => {
  143. if (selectedSampleId.value === id) {
  144. selectedSampleId.value = null
  145. } else {
  146. selectedSampleId.value = id
  147. }
  148. }
  149. const boxes = ref([
  150. {
  151. id: 1,
  152. samples: [] as TubeRack[],
  153. },
  154. {
  155. id: 2,
  156. samples: [] as TubeRack[],
  157. },
  158. ])
  159. watchEffect(() => {
  160. boxes.value.forEach((item) => {
  161. if (item.id === 1) {
  162. item.samples = tubeSettings.value.slice(0, 5)
  163. } else {
  164. item.samples = tubeSettings.value.slice(5, 10)
  165. }
  166. })
  167. })
  168. //确认事件
  169. const confirmChange = async () => {
  170. const modifiedSettings = Array.from(modifiedTubes.value.values())
  171. if (modifiedSettings.length > 0) {
  172. try {
  173. const updatePromises = modifiedSettings.map(setting =>
  174. updateTubeConfig({
  175. uuid: processedTubeInfo.value.uuid,
  176. setting
  177. })
  178. )
  179. const results = await Promise.all(updatePromises)
  180. const allSuccess = results.every(res => res && res.success)
  181. if (allSuccess) {
  182. ElMessage.success('所有修改已保存')
  183. goBack()
  184. } else {
  185. ElMessage.error('部分修改保存失败')
  186. }
  187. } catch (error) {
  188. console.error('保存修改失败:', error)
  189. ElMessage.error('保存失败')
  190. }
  191. } else {
  192. ElMessage.warning('没有需要保存的修改')
  193. }
  194. }
  195. // 显示键盘
  196. const showKeyboard = (type: 'barcode' | 'userid', item: any) => {
  197. keyboardVisible.value = true
  198. currentInput.value = { type, item }
  199. currentInputValue.value = type === 'barcode' ? item.sampleBarcode : item.userid
  200. }
  201. // 处理键盘输入
  202. const handleKeyboardInput = (input: string) => {
  203. if (!currentInput.value.item) return
  204. if (currentInput.value.type === 'barcode') {
  205. currentInput.value.item.sampleBarcode = input
  206. } else {
  207. currentInput.value.item.userid = input
  208. }
  209. currentInputValue.value = input
  210. modifiedTubes.value.set(currentInput.value.item.tubeIndex, {
  211. tubeIndex: currentInput.value.item.tubeIndex,
  212. userid: currentInput.value.item.userid,
  213. sampleBarcode: currentInput.value.item.sampleBarcode,
  214. projId: currentInput.value.item.projId?.map((p: ReactionPlate) => p.projId) || [],
  215. bloodType: currentInput.value.item.bloodType,
  216. })
  217. settingTestTubeStore.updateTubeSetting(tubeInfo.value.uuid, {
  218. tubeIndex: currentInput.value.item.tubeIndex,
  219. [currentInput.value.type === 'userid' ? 'userid' : 'sampleBarcode']: input
  220. })
  221. }
  222. // 处理键盘按键
  223. const handleKeyPress = (button: string) => {
  224. if (button === '{enter}') {
  225. keyboardVisible.value = false
  226. currentInputValue.value = ''
  227. }
  228. }
  229. // 在组件卸载时清理
  230. onUnmounted(() => {
  231. modifiedTubes.value.clear()
  232. })
  233. </script>
  234. <style lang="less" scoped>
  235. #changeUser-container {
  236. width: 100%;
  237. height: 95vh;
  238. display: flex;
  239. flex-direction: column;
  240. position: relative;
  241. background-color: #f5f7fa;
  242. overflow: hidden;
  243. .header {
  244. height: 80px;
  245. background-color: #fff;
  246. padding: 0 20px;
  247. display: flex;
  248. align-items: center;
  249. .header-left {
  250. display: flex;
  251. align-items: center;
  252. gap: 12px;
  253. img {
  254. width: 24px;
  255. height: 24px;
  256. cursor: pointer;
  257. }
  258. .title {
  259. font-size: 28px;
  260. color: #303133;
  261. }
  262. .tube-type {
  263. font-size: 24px;
  264. color: #606266;
  265. }
  266. }
  267. }
  268. .content {
  269. padding: 16px;
  270. display: flex;
  271. flex-direction: column;
  272. gap: 20px;
  273. overflow: hidden;
  274. .sample-section {
  275. background-color: #fff;
  276. border-radius: 8px;
  277. padding: 20px;
  278. display: flex;
  279. height: 380px;
  280. .section-labels {
  281. width: 120px;
  282. display: flex;
  283. flex-direction: column;
  284. padding-top: 20px;
  285. .label-column {
  286. .label-item {
  287. margin-bottom: 16px;
  288. .label {
  289. font-size: 24px;
  290. color: #606266;
  291. }
  292. }
  293. }
  294. }
  295. .samples-grid {
  296. flex: 1;
  297. display: grid;
  298. grid-template-columns: repeat(5, 1fr);
  299. gap: 12px;
  300. .sample-item {
  301. background-color: #f5f7fa;
  302. border-radius: 4px;
  303. padding: 12px;
  304. .sample-content {
  305. display: flex;
  306. flex-direction: column;
  307. align-items: center;
  308. .item-index {
  309. font-size: 22px;
  310. color: #606266;
  311. margin-bottom: 8px;
  312. }
  313. .sample-circle {
  314. width: 60px;
  315. height: 60px;
  316. border-radius: 50%;
  317. display: flex;
  318. align-items: center;
  319. justify-content: center;
  320. margin: 8px 0;
  321. .blood-type {
  322. font-size: 20px;
  323. color: #fff;
  324. }
  325. }
  326. .inputs {
  327. width: 100%;
  328. margin-top: 12px;
  329. .input-field {
  330. width: 100%;
  331. height: 36px;
  332. border: 1px solid #dcdfe6;
  333. border-radius: 4px;
  334. margin-bottom: 8px;
  335. font-size: 20px;
  336. text-align: center;
  337. background-color: #fff;
  338. &::placeholder {
  339. color: #c0c4cc;
  340. font-size: 18px;
  341. }
  342. }
  343. }
  344. }
  345. }
  346. }
  347. }
  348. }
  349. .footer {
  350. height: 80px;
  351. padding: 10px 20px;
  352. background-color: #fff;
  353. display: flex;
  354. justify-content: center;
  355. gap: 16px;
  356. .btn {
  357. width: 320px;
  358. height: 60px;
  359. border-radius: 30px;
  360. font-size: 24px;
  361. font-weight: normal;
  362. &.cancel {
  363. background-color: #f5f7fa;
  364. color: #606266;
  365. border: 1px solid #dcdfe6;
  366. }
  367. &.confirm {
  368. background-color: #409eff;
  369. color: #fff;
  370. }
  371. }
  372. }
  373. .keyboard-container {
  374. position: absolute;
  375. bottom: 0;
  376. left: 0;
  377. width: 100%;
  378. height: 20vh;
  379. background-color: #fff;
  380. }
  381. // 键盘动画
  382. .slide-up-enter-active,
  383. .slide-up-leave-active {
  384. transition: transform 0.3s ease;
  385. }
  386. .slide-up-enter-from,
  387. .slide-up-leave-to {
  388. transform: translateY(100%);
  389. }
  390. }
  391. </style>