BodyPositionList.tsx 7.6 KB

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