| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359 |
- import React, { useState, useEffect } from 'react';
- import { Row, Col, Button, Drawer } from 'antd';
- import { SettingOutlined } from '@ant-design/icons';
- import { FormattedMessage } from 'react-intl';
- import { useSelector, useDispatch } from 'react-redux';
- import { useRisAutoSync } from '@/hooks/useRisAutoSync';
- import {
- fetchWorkThunk,
- workSelectionSlice,
- workPaginationSlice,
- workFiltersSlice,
- } from '../../states/patient/worklist/slices/workSlice';
- import { updateThumbnailsFromWorkSelection } from '../../states/patient/worklist/slices/thumbnailListSlice';
- import WorklistTable from './components/WorklistTable';
- import OperationPanel from './components/OperationPanel';
- import ThumbnailList from './components/ThumbnailList';
- import GenericPagination from '../../components/GenericPagination';
- import PatientPortraitFloat from './components/PatientPortraitFloat';
- import { RootState, AppDispatch } from '../../states/store';
- import { Task } from '@/domain/work';
- import worklistToExam from '../../domain/patient/worklistToExam';
- import { saveRisData } from '../../domain/patient/risSaveLogic';
- import { columnConfigService } from '@/config/tableColumns';
- import { ColumnConfig } from '@/config/tableColumns/types/columnConfig';
- import { useMultiSelection } from '@/hooks/useMultiSelection';
- import {
- getExposureProgress,
- ExposureProgress,
- } from '@/domain/patient/getExposureProgress';
- import AppendViewModal from '../exam/components/AppendViewModal';
- import useEffectiveBreakpoint from '../../hooks/useEffectiveBreakpoint';
- const WorklistPage: React.FC = () => {
- const screens = useEffectiveBreakpoint();
- const [drawerVisible, setDrawerVisible] = useState(false);
- const [columnConfig, setColumnConfig] = useState<ColumnConfig[]>([]); // 新增:列配置状态
- const [exposureProgressMap, setExposureProgressMap] = useState<
- Record<string, ExposureProgress>
- >({}); // 曝光进度映射
- const [selectedPatientForPortrait, setSelectedPatientForPortrait] =
- useState<Task | null>(null); // 照片显示用的选中患者
- const [isAppendModalOpen, setIsAppendModalOpen] = useState(false); // 追加体位模态框状态
- const [currentStudy, setCurrentStudy] = useState<Task | null>(null); // 当前保存的study信息
- // 启用RIS自动同步(在后台静默运行)
- useRisAutoSync();
- const dispatch: AppDispatch = useDispatch();
- const filters = useSelector((state: RootState) => state.workFilters);
- const page = useSelector((state: RootState) => state.workPagination.page);
- const pageSize = useSelector(
- (state: RootState) => state.workPagination.pageSize
- );
- const selectedIds = useSelector(
- (state: RootState) => state.workSelection.selectedIds
- );
- const selectedSecondaryIds = useSelector(
- // 获取次要选中ID列表
- (state: RootState) => state.workSelection.selectedSecondaryIds
- );
- const worklistData = useSelector(
- (state: RootState) => state.workEntities.data
- );
- const productName = useSelector(
- (state: RootState) => state.product.productName
- );
- // 新增:获取列配置
- useEffect(() => {
- columnConfigService
- .getColumnConfig('worklist')
- .then((config) => {
- setColumnConfig(config.columns);
- })
- .catch((error) => {
- console.error('Failed to load worklist column config:', error);
- // 失败时使用空配置,表格会显示所有列
- setColumnConfig([]);
- });
- }, []);
- // 批量获取曝光进度
- useEffect(() => {
- const fetchExposureProgress = async () => {
- if (worklistData.length === 0) return;
- const progressMap: Record<string, ExposureProgress> = {};
- // 并发获取曝光进度,但限制并发数量避免过多请求
- const batchSize = 5;
- for (let i = 0; i < worklistData.length; i += batchSize) {
- const batch = worklistData.slice(i, i + batchSize);
- await Promise.all(
- batch.map(async (task) => {
- // 跳过没有 StudyID 的记录(主要是 RIS 数据)
- if (!task.StudyID || task.StudyID.trim() === '') {
- console.warn(
- `[getExposureProgress] Skipping task without StudyID:`,
- task
- );
- return;
- }
- try {
- console.log(
- `[getExposureProgress] Fetching progress for StudyID: ${task.StudyID}`
- );
- const progress = await getExposureProgress(task.StudyID);
- progressMap[task.StudyID] = progress;
- console.log(
- `[getExposureProgress] Got progress for ${task.StudyID}:`,
- progress
- );
- } catch (error) {
- console.error(
- `[getExposureProgress] Failed to get progress for ${task.StudyID}:`,
- error
- );
- progressMap[task.StudyID] = { exposedCount: 0, totalCount: 0 };
- }
- })
- );
- }
- setExposureProgressMap({ ...progressMap }); // 使用展开运算符创建新对象
- };
- fetchExposureProgress();
- }, [worklistData]);
- // 同步分页状态到过滤器 - 只在分页变化时更新
- useEffect(() => {
- const currentFilters = filters;
- const needsUpdate =
- currentFilters.page !== page || currentFilters.page_size !== pageSize;
- if (needsUpdate) {
- console.log('[worklist] Syncing pagination to filters:', {
- page,
- pageSize,
- });
- dispatch(
- workFiltersSlice.actions.setFilters({
- page,
- page_size: pageSize,
- })
- );
- }
- }, [dispatch, page, pageSize, filters]);
- // 获取工作清单数据 - 使用稳定的依赖
- useEffect(() => {
- console.log(
- '[worklist] Fetching worklist data with filters:',
- filters,
- 'page:',
- page,
- 'pageSize:',
- pageSize
- );
- dispatch(fetchWorkThunk({ page, pageSize, filters }));
- }, [dispatch, page, pageSize]); // 移除 filters 依赖,使用稳定的分页参数
- // 使用多选 Hook
- const { handleRowClick } = useMultiSelection({
- selectedIds,
- selectedSecondaryIds,
- onSelectionChange: (newIds, secondNewIds) => {
- console.log('Selected IDs changed:', newIds, secondNewIds);
- // 如果只有一个选中项,设置选中患者用于显示照片
- if (newIds.length === 1) {
- const selectedRecord = worklistData.find(
- (item) =>
- !item.entry_id && // 只处理本地数据(不是RIS数据)
- item.StudyID === newIds[0] && // StudyID匹配
- newIds[0] // 确保StudyID不为空
- );
- if (selectedRecord) {
- setSelectedPatientForPortrait(selectedRecord);
- }
- } else {
- // 多选时清空患者照片
- setSelectedPatientForPortrait(null);
- }
- // 更新 Redux 状态(用于其他功能)
- dispatch(workSelectionSlice.actions.setSelectedIds(newIds));
- dispatch(
- workSelectionSlice.actions.setSelectedSecondaryIds(secondNewIds ?? [])
- );
- dispatch(updateThumbnailsFromWorkSelection(newIds));
- },
- enableMultiSelect: true,
- });
- // 处理行点击事件(兼容现有功能)
- const handleRowClickInternal = (record: Task, event?: React.MouseEvent) => {
- handleRowClick(record, event || ({} as React.MouseEvent));
- };
- const handleRowDoubleClick = (record: Task) => {
- console.log(
- '[WorklistTable] Row double-clicked:',
- JSON.stringify(record, null, 2)
- );
- // 判断是否为RIS数据
- if (record.entry_id) {
- // RIS数据:触发保存到本地,保存成功后打开追加体位对话框
- saveRisData(record.entry_id, (task) => {
- console.log(
- '[WorklistPage] RIS数据保存成功,Study ID:',
- task.StudyID,
- '打开追加体位对话框'
- );
- setCurrentStudy(task);
- setIsAppendModalOpen(true);
- });
- } else {
- // 本地study数据:进入检查
- worklistToExam(record);
- }
- };
- return (
- <div className="h-full">
- {/* 患者照片浮动组件 */}
- <PatientPortraitFloat
- patient={selectedPatientForPortrait}
- onClose={() => setSelectedPatientForPortrait(null)}
- />
- {screens.xs ? (
- <>
- <div className="flex-1 overflow-auto">
- <WorklistTable
- productName={productName}
- columnConfig={columnConfig}
- exposureProgressMap={exposureProgressMap}
- worklistData={worklistData}
- filters={filters}
- page={page}
- pageSize={pageSize}
- selectedIds={selectedIds}
- selectedSecondaryIds={selectedSecondaryIds} // 新增
- handleRowClick={handleRowClickInternal}
- handleRowDoubleClick={handleRowDoubleClick}
- />
- </div>
- <GenericPagination
- paginationSelector={(state) => state.workPagination}
- entitiesSelector={(state) => state.workEntities}
- paginationActions={workPaginationSlice.actions}
- className="border-t"
- />
- <Button
- type="primary"
- shape="circle"
- icon={<SettingOutlined />}
- className="fixed bottom-6 right-6 z-50"
- onClick={() => setDrawerVisible(true)}
- />
- <Drawer
- title={
- <FormattedMessage
- id="worklist.operationPanel"
- defaultMessage="worklist.operationPanel"
- />
- }
- placement="left"
- onClose={() => setDrawerVisible(false)}
- open={drawerVisible}
- width={300}
- >
- <OperationPanel />
- </Drawer>
- </>
- ) : (
- <Row className="h-full">
- <Col
- span={screens.lg ? 18 : screens.md ? 20 : 24}
- className="h-full flex flex-col"
- >
- <div className="flex flex-col h-[80%]">
- <div className="overflow-auto flex flex-col">
- <WorklistTable
- productName={productName}
- columnConfig={columnConfig}
- exposureProgressMap={exposureProgressMap}
- worklistData={worklistData}
- filters={filters}
- page={page}
- pageSize={pageSize}
- selectedIds={selectedIds}
- selectedSecondaryIds={selectedSecondaryIds} // 新增
- handleRowClick={handleRowClickInternal}
- handleRowDoubleClick={handleRowDoubleClick}
- className="flex-1 overflow-auto"
- />
- </div>
- <GenericPagination
- paginationSelector={(state) => state.workPagination}
- entitiesSelector={(state) => state.workEntities}
- paginationActions={workPaginationSlice.actions}
- className="border-t"
- />
- </div>
- <div className=" overflow-auto">
- <ThumbnailList />
- </div>
- </Col>
- <Col
- span={screens.lg ? 6 : screens.md ? 4 : 0}
- className="h-full overflow-auto"
- >
- <OperationPanel />
- </Col>
- </Row>
- )}
- {/* 追加体位模态框 */}
- <AppendViewModal
- open={isAppendModalOpen}
- onCancel={() => {
- setIsAppendModalOpen(false);
- setCurrentStudy(null); // 关闭时清空study信息
- }}
- studyId={currentStudy?.StudyID}
- currentWork={currentStudy}
- onSuccess={() => {
- setIsAppendModalOpen(false);
- // 追加成功后选中该study,然后进入检查
- if (currentStudy) {
- const studyId = currentStudy.StudyID;
- const entryId = currentStudy.entry_id;
- // 更新选中状态
- dispatch(workSelectionSlice.actions.setSelectedIds([studyId]));
- dispatch(
- workSelectionSlice.actions.setSelectedSecondaryIds(
- entryId ? [entryId] : []
- )
- );
- console.log(
- '[WorklistPage] 追加成功,选中study:',
- studyId,
- '进入检查'
- );
- worklistToExam(currentStudy);
- }
- }}
- />
- </div>
- );
- };
- export default WorklistPage;
|