appendViewSlice.ts 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit';
  2. import { View, fetchViews } from '@/API/patient/viewActions';
  3. import {
  4. appendBodyPositionToStudy,
  5. AppendViewRequest,
  6. } from '@/API/exam/appendBodyPosition';
  7. import { ExtendedBodyPosition, addBodyPosition } from './bodyPositionListSlice';
  8. import { fetchViewDetail } from '@/API/patient/viewActions';
  9. import { dview } from '@/domain/dview';
  10. import { Work } from './examWorksCacheSlice';
  11. import {
  12. addSelectedView,
  13. removeSelectedView,
  14. clearSelectedViews as clearViewSelectionViews,
  15. type ExtendedView,
  16. } from '@/states/patient/viewSelection';
  17. import { v4 as uuidv4 } from 'uuid';
  18. interface AppendViewState {
  19. availableViews: View[];
  20. selectedViews: ExtendedView[];
  21. isModalOpen: boolean;
  22. loading: boolean;
  23. error: string | null;
  24. currentSelectionType: 'protocol' | 'view';
  25. currentBodyPartId: string | null;
  26. }
  27. const initialState: AppendViewState = {
  28. availableViews: [],
  29. selectedViews: [],
  30. isModalOpen: false,
  31. loading: false,
  32. error: null,
  33. currentSelectionType: 'view',
  34. currentBodyPartId: null,
  35. };
  36. /**
  37. * 获取可用体位列表
  38. */
  39. export const fetchAvailableViewsThunk = createAsyncThunk(
  40. 'appendView/fetchAvailableViews',
  41. async (
  42. params: {
  43. patient_type: string;
  44. body_part_id: string;
  45. },
  46. { rejectWithValue }
  47. ) => {
  48. try {
  49. const response = await fetchViews(
  50. params.patient_type,
  51. params.body_part_id,
  52. true,
  53. null
  54. );
  55. return response.data.views;
  56. } catch (error) {
  57. return rejectWithValue(
  58. error.message || 'Failed to fetch available views'
  59. );
  60. }
  61. }
  62. );
  63. /**
  64. * 追加体位到Study并更新bodyPositionList
  65. */
  66. export const appendViewsThunk = createAsyncThunk(
  67. 'appendView/appendViews',
  68. async (
  69. params: {
  70. study_id: string;
  71. views: View[];
  72. currentWork: Work;
  73. },
  74. { dispatch, rejectWithValue }
  75. ) => {
  76. try {
  77. // 1. 构建请求数据
  78. const request: AppendViewRequest = {
  79. study_id: params.study_id,
  80. views: params.views.map((view) => ({
  81. view_id: view.view_id,
  82. procedure_id: view.procedure_id || '',
  83. })),
  84. };
  85. // 2. 调用API追加体位
  86. const response = await appendBodyPositionToStudy(request);
  87. // 3. 将返回的 Series.images 转换为 ExtendedBodyPosition
  88. const newBodyPositions: ExtendedBodyPosition[] = [];
  89. for (const series of response.data.series) {
  90. for (const image of series.images) {
  91. // 获取完整的 view 详情
  92. const viewDetail = await fetchViewDetail(image.view_id);
  93. const dviewData: dview = {
  94. view_id: image.view_id,
  95. series_instance_uid: image.series_instance_uid,
  96. study_instance_uid: image.study_instance_uid,
  97. study_id: image.study_id,
  98. procedure_id: series.procedure_id,
  99. view_description: image.view_description,
  100. view_type: image.view_id,
  101. PrimarySopUID: image.sop_instance_uid,
  102. thumbnail_file: image.image_file_path,
  103. image_file: image.image_file_path,
  104. image_file_path: image.image_file_path,
  105. expose_status: 'Unexposed',
  106. judged_status:image.judged_status,
  107. };
  108. const extendedBodyPosition: ExtendedBodyPosition = {
  109. ...viewDetail,
  110. collimator_length:
  111. viewDetail.config_object.DX?.CollimatorSizeLength ||
  112. viewDetail.config_object?.Common?.CollimatorSizeLength ||
  113. 0,
  114. collimator_width:
  115. viewDetail.config_object.DX?.CollimatorSizeWidth ||
  116. viewDetail.config_object?.Common?.CollimatorSizeWidth ||
  117. 0,
  118. sid: '',
  119. patient_name: params.currentWork.PatientName,
  120. patient_id: params.currentWork.PatientID,
  121. registration_number: params.currentWork.AccessionNumber,
  122. study_description: params.currentWork.StudyDescription,
  123. body_position_image: viewDetail.view_icon_name,
  124. work: params.currentWork,
  125. study_instance_uid: params.currentWork.StudyInstanceUID,
  126. sop_instance_uid: image.sop_instance_uid,
  127. series_instance_uid: image.series_instance_uid,
  128. secondary_sop_uid: image.secondary_sop_uid,
  129. study_id: image.study_id,
  130. dview: dviewData,
  131. };
  132. newBodyPositions.push(extendedBodyPosition);
  133. }
  134. }
  135. // 4. 将新体位添加到 bodyPositionList
  136. for (const bodyPosition of newBodyPositions) {
  137. dispatch(addBodyPosition(bodyPosition));
  138. }
  139. return newBodyPositions;
  140. } catch (error) {
  141. return rejectWithValue(
  142. error.message || 'Failed to append body positions'
  143. );
  144. }
  145. }
  146. );
  147. const appendViewSlice = createSlice({
  148. name: 'appendView',
  149. initialState,
  150. reducers: {
  151. setModalOpen: (state, action: PayloadAction<boolean>) => {
  152. state.isModalOpen = action.payload;
  153. if (!action.payload) {
  154. // 关闭模态框时清空选中
  155. state.selectedViews = [];
  156. }
  157. },
  158. toggleViewSelection: (state, action: PayloadAction<View>) => {
  159. const index = state.selectedViews.findIndex(
  160. (v) => v.view_id === action.payload.view_id
  161. );
  162. if (index >= 0) {
  163. // 已选中,取消选中
  164. state.selectedViews.splice(index, 1);
  165. } else {
  166. // 未选中,添加到选中列表,需要添加 guid
  167. const extendedView: ExtendedView = {
  168. ...action.payload,
  169. guid: uuidv4(),
  170. };
  171. state.selectedViews.push(extendedView);
  172. }
  173. },
  174. clearSelectedViews: (state) => {
  175. state.selectedViews = [];
  176. },
  177. clearError: (state) => {
  178. state.error = null;
  179. },
  180. setSelectionType: (state, action: PayloadAction<'protocol' | 'view'>) => {
  181. state.currentSelectionType = action.payload;
  182. },
  183. setCurrentBodyPart: (state, action: PayloadAction<string | null>) => {
  184. state.currentBodyPartId = action.payload;
  185. },
  186. },
  187. extraReducers: (builder) => {
  188. builder
  189. // fetchAvailableViewsThunk
  190. .addCase(fetchAvailableViewsThunk.pending, (state) => {
  191. state.loading = true;
  192. state.error = null;
  193. })
  194. .addCase(fetchAvailableViewsThunk.fulfilled, (state, action) => {
  195. state.loading = false;
  196. state.availableViews = action.payload;
  197. })
  198. .addCase(fetchAvailableViewsThunk.rejected, (state, action) => {
  199. state.loading = false;
  200. state.error = action.payload as string;
  201. })
  202. // appendViewsThunk
  203. .addCase(appendViewsThunk.pending, (state) => {
  204. state.loading = true;
  205. state.error = null;
  206. })
  207. .addCase(appendViewsThunk.fulfilled, (state) => {
  208. state.loading = false;
  209. state.isModalOpen = false;
  210. state.selectedViews = [];
  211. })
  212. .addCase(appendViewsThunk.rejected, (state, action) => {
  213. state.loading = false;
  214. state.error = action.payload as string;
  215. })
  216. // 监听 viewSelection 的 actions,实现状态同步
  217. .addCase(addSelectedView, (state, action: PayloadAction<View>) => {
  218. // addSelectedView 接收的是 View 类型,viewSelection slice 会内部添加 guid
  219. // 这里也需要添加 guid
  220. const view = action.payload;
  221. // 检查是否已存在(通过 view_id 判断)
  222. const exists = state.selectedViews.some(
  223. (v) => v.view_id === view.view_id
  224. );
  225. if (!exists) {
  226. const extendedView: ExtendedView = {
  227. ...view,
  228. guid: uuidv4(),
  229. };
  230. state.selectedViews.push(extendedView);
  231. }
  232. })
  233. .addCase(removeSelectedView, (state, action: PayloadAction<string>) => {
  234. // action.payload 是 guid,通过 guid 删除
  235. state.selectedViews = state.selectedViews.filter(
  236. (view) => view.guid !== action.payload
  237. );
  238. })
  239. .addCase(clearViewSelectionViews, (state) => {
  240. // 清空已选择的体位
  241. state.selectedViews = [];
  242. });
  243. },
  244. });
  245. export const {
  246. setModalOpen,
  247. toggleViewSelection,
  248. clearSelectedViews,
  249. clearError,
  250. setSelectionType,
  251. setCurrentBodyPart,
  252. } = appendViewSlice.actions;
  253. export default appendViewSlice.reducer;