BodyPositionList.tsx 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. import React, { useEffect, useState, useRef } from 'react';
  2. import { useSelector, useDispatch } from 'react-redux';
  3. import { ExtendedBodyPosition } from '../../../states/exam/bodyPositionListSlice';
  4. import { RootState, AppDispatch } from '../../../states/store';
  5. import { message, Badge } from 'antd';
  6. import { CheckOutlined, CloseOutlined } from '@ant-design/icons';
  7. import AppendViewIcon from '@/components/AppendViewIcon';
  8. import ImageViewer from './ImageViewer';
  9. import { getExposedImageUrl, getViewIconUrl } from '../../../API/bodyPosition';
  10. import {
  11. manualSelectBodyPositionWithFlowSwitch,
  12. autoSelectFirstBodyPosition,
  13. } from '@/domain/exam/bodyPositionSelection';
  14. import AppendViewModal from './AppendViewModal';
  15. interface BodyPositionListProps {
  16. layout: 'horizontal' | 'vertical';
  17. showAddButton?: boolean;
  18. }
  19. const BodyPositionList: React.FC<BodyPositionListProps> = ({
  20. layout,
  21. showAddButton = true,
  22. }) => {
  23. const dispatch = useDispatch<AppDispatch>();
  24. const currentKey = useSelector(
  25. (state: RootState) => state.BusinessFlow.currentKey
  26. );
  27. // 🔧 修改:处理单击事件
  28. const handleImageClick = async (
  29. bodyPosition: ExtendedBodyPosition
  30. ): Promise<void> => {
  31. console.log(`[BodyPositionList] Single-click on: ${bodyPosition.view_name}`);
  32. // 单击时,禁止自动流程切换
  33. await manualSelectBodyPositionWithFlowSwitch(
  34. bodyPosition,
  35. dispatch,
  36. currentKey,
  37. false // allowFlowSwitch = false(禁止流程切换)
  38. );
  39. };
  40. // 🆕 新增:处理双击事件
  41. const handleImageDoubleClick = async (
  42. bodyPosition: ExtendedBodyPosition
  43. ): Promise<void> => {
  44. console.log(`[BodyPositionList] Double-click on: ${bodyPosition.view_name}`);
  45. // 双击时,允许自动流程切换
  46. await manualSelectBodyPositionWithFlowSwitch(
  47. bodyPosition,
  48. dispatch,
  49. currentKey,
  50. true // allowFlowSwitch = true(允许流程切换)
  51. );
  52. };
  53. const bodyPositions = useSelector(
  54. (state: RootState) => state.bodyPositionList.bodyPositions
  55. );
  56. const selectedBodyPosition = useSelector(
  57. (state: RootState) => state.bodyPositionList.selectedBodyPosition
  58. );
  59. // 🔧 修复:使用 ref 跟踪是否已经执行过初始化
  60. // 避免在 judgeImage 等操作导致 bodyPositions 引用改变时重复执行
  61. const hasInitializedRef = useRef(false);
  62. useEffect(() => {
  63. // 只在第一次体位列表有值时执行自动选择
  64. if (bodyPositions.length > 0 && !hasInitializedRef.current) {
  65. hasInitializedRef.current = true;
  66. console.log(
  67. '[BodyPositionList] Auto-selecting first body position on component mount'
  68. );
  69. autoSelectFirstBodyPosition(bodyPositions, dispatch, currentKey).catch(
  70. (error) => {
  71. message.error(
  72. `Failed to auto-select the first body position ${error}`
  73. );
  74. }
  75. );
  76. }
  77. }, [bodyPositions, dispatch, currentKey]);
  78. const [isAppendModalOpen, setIsAppendModalOpen] = useState(false);
  79. const addBodyPositionClick = (): void => {
  80. console.log('[BodyPositionList] Add button clicked');
  81. console.log(
  82. '[BodyPositionList] selectedBodyPosition:',
  83. selectedBodyPosition
  84. );
  85. if (!selectedBodyPosition) {
  86. message.warning('请先选择一个体位');
  87. return;
  88. }
  89. console.log('[BodyPositionList] Opening append modal');
  90. setIsAppendModalOpen(true);
  91. };
  92. const handleModalClose = (): void => {
  93. setIsAppendModalOpen(false);
  94. };
  95. const handleDragStart = (sop_isntance_id, e) => {
  96. // 存储序列唯一标识(关键:用于后续加载图像)
  97. console.log(`要拖拽的sop isntance uid 是 ${sop_isntance_id}`);
  98. e.dataTransfer.setData('seriesInstanceUID', sop_isntance_id);
  99. e.dataTransfer.effectAllowed = 'copy'; // 拖拽效果
  100. };
  101. return (
  102. // 父级是flex,这里是grid,grid的高度需要设置为0,并且flex-grow,这样才能不撑开grid
  103. <div className={`${layout} h-full flex flex-col`}>
  104. <div className="flex-grow overflow-y-auto flex flex-col ">
  105. {bodyPositions.map((bodyPosition, index) => (
  106. <div key={index} className="relative w-[50%] mx-auto"
  107. draggable
  108. onDragStart={(e) => handleDragStart(bodyPosition.sop_instance_uid, e)}
  109. >
  110. <ImageViewer
  111. src={
  112. bodyPosition.dview.expose_status === 'Exposed'
  113. ? getExposedImageUrl(bodyPosition.sop_instance_uid)
  114. : getViewIconUrl(bodyPosition.view_icon_name)
  115. }
  116. className={`image-viewer-item hover:border-[var(--color-primary)] hover:border-4
  117. ${bodyPosition.sop_instance_uid ===
  118. selectedBodyPosition?.sop_instance_uid
  119. ? 'border-4 border-[var(--color-primary)] '
  120. : ''
  121. }`}
  122. onClick={() => handleImageClick(bodyPosition)}
  123. onDoubleClick={() => handleImageDoubleClick(bodyPosition)}
  124. />
  125. {/* 左上角:判断状态badge */}
  126. {bodyPosition.dview.expose_status === 'Exposed' && bodyPosition.dview.judged_status && (
  127. <div className="absolute top-1 left-1 z-10">
  128. <Badge
  129. count={
  130. bodyPosition.dview.judged_status === 'Accept' ? (
  131. <CheckOutlined style={{ color: '#fff', fontSize: '16px' }} />
  132. ) : bodyPosition.dview.judged_status === 'Reject' ? (
  133. <CloseOutlined style={{ color: '#fff', fontSize: '16px' }} />
  134. ) : 'Un'
  135. }
  136. style={{
  137. backgroundColor: bodyPosition.dview.judged_status === 'Accept' ? '#52c41a' : '#ff4d4f',
  138. borderRadius: '50%',
  139. display: 'flex',
  140. alignItems: 'center',
  141. justifyContent: 'center',
  142. }}
  143. />
  144. </div>
  145. )}
  146. </div>
  147. ))}
  148. </div>
  149. {showAddButton && (
  150. <div
  151. className="mx-auto cursor-pointer"
  152. style={{ width: '50%' }}
  153. onClick={addBodyPositionClick}
  154. >
  155. <div
  156. style={{
  157. stroke: 'var(--color-primary)',
  158. strokeWidth: 0.5,
  159. color: 'var(--color-text)',
  160. }}
  161. >
  162. <AppendViewIcon className="w-full h-full hover:opacity-100 " />
  163. </div>
  164. </div>
  165. )}
  166. <AppendViewModal open={isAppendModalOpen} onCancel={handleModalClose} />
  167. </div>
  168. );
  169. };
  170. export default BodyPositionList;