import React, { useState, useEffect, useMemo } from 'react'; import { Table, TableColumnsType } from 'antd'; import { FormattedMessage } from 'react-intl'; import { Task, Task as DataType } from '@/domain/work'; import type { ResizeCallbackData } from 'react-resizable'; import { Resizable } from 'react-resizable'; import { WorkFilter } from '@/states/patient/worklist/types/workfilter'; import { useTouchDoubleClick } from '@/hooks/useTouchDoubleClick'; import { useMultiSelection } from '@/hooks/useMultiSelection'; import { ColumnConfig } from '@/config/tableColumns/types/columnConfig'; import { ExposureProgress } from '@/domain/patient/getExposureProgress'; interface TitlePropsType { width: number; onResize: ( e: React.SyntheticEvent, data: ResizeCallbackData ) => void; } const ResizableTitle: React.FC< Readonly< React.HTMLAttributes & TitlePropsType > > = (props) => { const { onResize, width, ...restProps } = props; if (!width) { return ; } return ( e.stopPropagation()} /> } onResize={onResize} draggableOpts={{ enableUserSelectHack: false }} > ); }; interface WorklistTableProps { productName?: string; // 新增:产品名称(用于区分人医/宠物医) columnConfig?: ColumnConfig[]; // 列配置(可选) exposureProgressMap?: Record; // 新增:曝光进度映射 worklistData: Task[]; filters?: WorkFilter; page?: number; pageSize?: number; selectedIds: string[]; // 必填(保持向后兼容) selectedSecondaryIds?: string[]; // 可选(新增) handleRowClick: (record: Task, event?: React.MouseEvent) => void; handleRowDoubleClick: (record: Task) => void; className?: string; } const WorklistTable: React.FC = ({ productName, // 新增 columnConfig = [], // 接收配置,默认为空数组 exposureProgressMap = null, // 新增:曝光进度映射 worklistData, // filters, // page, // pageSize, selectedIds, selectedSecondaryIds, // 可能为 undefined handleRowClick, handleRowDoubleClick, className, }) => { // 生成列定义的辅助函数 const generateColumnsDef = (productName?: string): any[] => [ { title: ( ), dataIndex: 'StudyInstanceUID', }, { title: ( ), dataIndex: 'StudyID', }, { title: ( ), dataIndex: 'SpecificCharacterSet', }, { title: ( ), dataIndex: 'AccessionNumber', }, { title: ( ), dataIndex: 'PatientID', }, { title: ( ), dataIndex: 'PatientName', }, { title: ( ), dataIndex: 'DisplayPatientName', }, { title: ( ), dataIndex: 'PatientSize', }, { title: ( ), dataIndex: 'PatientAge', }, { title: ( ), dataIndex: 'PatientSex', }, { title: ( ), dataIndex: 'AdmittingTime', }, { title: ( ), dataIndex: 'RegSource', }, { title: ( ), dataIndex: 'StudyStatus', }, { title: ( ), dataIndex: 'RequestedProcedureID', }, { title: ( ), dataIndex: 'PerformedProtocolCodeValue', }, { title: ( ), dataIndex: 'PerformedProtocolCodeMeaning', }, { title: ( ), dataIndex: 'PerformedProcedureStepID', }, { title: ( ), dataIndex: 'StudyDescription', }, { title: ( ), dataIndex: 'StudyStartDatetime', render: (_: any, record: Task) => { if (!record.StudyStartDatetime || typeof record.StudyStartDatetime !== 'object') { return ''; } const { seconds, nanos } = record.StudyStartDatetime; const date = new Date(seconds * 1000 + Math.floor(nanos / 1000000)); // 格式化为本地时间字符串 return date.toLocaleString(); } }, { title: ( ), dataIndex: 'ScheduledProcedureStepStartDate', }, { title: ( ), dataIndex: 'StudyLock', }, { title: ( ), dataIndex: 'OperatorID', }, { title: ( ), dataIndex: 'Modality', }, { title: ( ), dataIndex: 'Views', }, { title: ( ), dataIndex: 'Thickness', }, { title: ( ), dataIndex: 'PatientType', }, { title: ( ), dataIndex: 'StudyType', }, { title: ( ), dataIndex: 'QRCode', }, { title: ( ), dataIndex: 'IsExported', }, { title: ( ), dataIndex: 'IsEdited', }, { title: ( ), dataIndex: 'WorkRef', }, { title: ( ), dataIndex: 'IsAppended', }, { title: ( ), dataIndex: 'CreationTime', }, { title: ( ), dataIndex: 'MappedStatus', }, { title: ( ), dataIndex: 'IsDelete', }, { title: ( ), dataIndex: 'ExposureProgress', render: (_: any, record: Task) => { // 如果没有 StudyID,说明是 RIS 数据,不需要显示曝光进度 if (!record.StudyID || record.StudyID.trim() === '') { return '-'; // RIS 数据显示 "-" } const progress = exposureProgressMap?.[record.StudyID]; if (!progress) { return '加载中...'; // 数据还未加载完成 } return `${progress.exposedCount}/${progress.totalCount}`; } }, ]; // 根据 productName 和 exposureProgressMap 生成列定义 const columnsDef = useMemo(() => { const cols = generateColumnsDef(productName); return cols; }, [productName, exposureProgressMap]); // 依赖 productName 和 exposureProgressMap 的变化 // 判断是否使用双ID模式 const useDualIdMode = selectedSecondaryIds !== undefined; // 生成 rowKey const getRowKey = (record: Task): string => { if (useDualIdMode) { // 新模式:使用 StudyID + entry_id 组合 return `${record.StudyID || 'no-study'}_${record.entry_id || 'no-entry'}`; } // 兼容模式:仅使用 StudyID return record.StudyID; }; // 判断记录是否被选中 const isRecordSelected = (record: Task): boolean => { if (useDualIdMode) { // 新模式:检查双ID if (record.StudyID) { const indexByStudyId = selectedIds.indexOf(record.StudyID); // 关键修复:检查索引是否有效,避免 undefined === undefined 的误判 if (indexByStudyId === -1) return false; // 修复:保持与 useMultiSelection 一致,使用 ?? '' 处理 undefined return (selectedSecondaryIds[indexByStudyId] ?? '') === (record.entry_id ?? ''); } else if (record.entry_id) { const indexByEntryId = selectedSecondaryIds.indexOf(record.entry_id); // 关键修复:检查索引是否有效,避免 undefined === undefined 的误判 if (indexByEntryId === -1) return false; // 修复:保持与 useMultiSelection 一致,使用 ?? '' 处理 undefined return (selectedIds[indexByEntryId] ?? '') === (record.StudyID ?? ''); } throw new Error('Record must have at least StudyID or entry_id defined in dual ID mode.'); } else { // 兼容模式:仅检查 StudyID return selectedIds.includes(record.StudyID); } }; // 根据传入的配置过滤和排序列 const visibleColumns = useMemo(() => { // 如果没有配置,显示所有列(保持当前行为) if (columnConfig.length === 0) { return columnsDef.map((col) => ({ ...col, width: 150, })); } // 根据配置过滤出可见的列 return columnsDef .filter((col) => { const config = columnConfig.find((c) => c.key === col.dataIndex); return config?.visible ?? false; }) .map((col) => { const config = columnConfig.find((c) => c.key === col.dataIndex); return { ...col, width: config?.width ?? 150, }; }) .sort((a, b) => { const orderA = columnConfig.find((c) => c.key === a.dataIndex)?.order ?? 999; const orderB = columnConfig.find((c) => c.key === b.dataIndex)?.order ?? 999; return orderA - orderB; }); }, [columnConfig, columnsDef]); // 列可调整大小的逻辑 const [columns, setColumns] = useState>(visibleColumns); useEffect(() => { setColumns(visibleColumns); }, [visibleColumns, exposureProgressMap]); const handleResize = (index: number) => (_: React.SyntheticEvent, { size }: ResizeCallbackData) => { console.log('Resizing column:', index, size); const newColumns = [...columns]; newColumns[index] = { ...newColumns[index], width: size.width, }; setColumns(newColumns); }; const mergedColumns = columns.map[number]>( (col, index) => ({ ...col, onHeaderCell: (column: TableColumnsType[number]) => ({ width: column.width, onResize: handleResize(index) as React.ReactEventHandler, style: { whiteSpace: 'nowrap' }, }), onCell: () => ({ style: { whiteSpace: 'nowrap' } }), }) ); return ( bordered className={className} columns={mergedColumns} scroll={{ x: 'max-content' }} components={{ header: { cell: ResizableTitle } }} dataSource={worklistData} rowKey={getRowKey} pagination={false} onRow={(record, index) => { const isSelected = isRecordSelected(record); const { handleTouchStart } = useTouchDoubleClick({ onDoubleClick: () => handleRowDoubleClick(record), delay: 300, }); return { onClick: (event) => handleRowClick(record, event), onDoubleClick: () => handleRowDoubleClick(record), onTouchStart: (e) => { handleTouchStart(e); }, 'data-testid': `row-${index}`, style: { cursor: 'pointer', userSelect: 'none', // 防止文本选择 backgroundColor: isSelected ? 'rgba(255, 255, 0, 0.3)' : 'transparent', }, }; }} rowHoverable={false} rowClassName={(record) => isRecordSelected(record) ? 'bg-yellow-500 hover:bg-yellow-800' : '' } /> ); }; export default WorklistTable;