本文档记录了 Worklist 和 History 等页面表格的列配置功能实现,支持通过配置信息动态控制表格显示的列,而不是显示所有35个列。
在 Worklist 和 History 页面,表格默认显示所有35个列,导致:
主要功能:
技术要求:
采用端口适配器模式实现灵活的配置源切换:
┌─────────────────────────────────────────────┐
│ 配置源(可切换) │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ 本地硬编码 │ │ 远程 API │ │
│ └──────────────┘ └──────────────┘ │
└─────────────┬──────────────┬────────────────┘
│ │
↓ ↓
┌──────────────────────────────────┐
│ IColumnConfigProvider (端口) │
│ - getColumnConfig() │
│ - getAllColumnConfigs() │
│ - isAvailable() │
└──────────────────────────────────┘
↓
┌──────────────────────────────────┐
│ ColumnConfigService (领域服务) │
│ - 策略模式 │
│ - 主提供者 + 回退提供者 │
│ - 自动切换 │
└──────────────────────────────────┘
↓
┌──────────────────────────────────┐
│ 父组件 (worklist.tsx) │
│ - 获取配置 │
│ - 通过 props 传递 │
└──────────────────────────────────┘
↓
┌──────────────────────────────────┐
│ WorklistTable 组件 │
│ - 根据配置过滤列 │
│ - 根据配置排序列 │
│ - 应用列宽 │
└──────────────────────────────────┘
应用启动
↓
父组件 useEffect
↓
columnConfigService.getColumnConfig('worklist')
↓
├─→ 尝试主提供者 (RemoteColumnConfigAdapter)
│ ├─→ 成功 → 返回远程配置
│ └─→ 失败 ↓
│
└─→ 回退提供者 (LocalColumnConfigAdapter)
└─→ 返回本地配置
↓
父组件接收配置 → setState
↓
通过 props 传递给 WorklistTable
↓
WorklistTable.visibleColumns (useMemo)
├─→ 配置为空 → 显示所有35列
└─→ 配置不为空 → 过滤 + 排序 + 应用宽度
↓
渲染表格
文件:src/config/tableColumns/types/columnConfig.ts
/**
* 单个列的配置
*/
export interface ColumnConfig {
key: string; // 列标识(对应 dataIndex)
visible: boolean; // 是否显示
order: number; // 显示顺序
width?: number; // 列宽(可选)
fixed?: 'left' | 'right'; // 固定列(可选)
}
/**
* 表格名称类型
*/
export type TableName = 'worklist' | 'history' | 'archive' | 'output';
/**
* 表格列配置
*/
export interface TableColumnConfig {
tableName: TableName;
columns: ColumnConfig[];
version?: string; // 配置版本
updatedAt?: string; // 更新时间
}
/**
* 完整的配置响应
*/
export interface ColumnConfigResponse {
data: TableColumnConfig[];
success: boolean;
message?: string;
}
文件:src/config/tableColumns/ports/IColumnConfigProvider.ts
import { TableColumnConfig, TableName } from '../types/columnConfig';
export interface IColumnConfigProvider {
/**
* 获取指定表格的列配置
*/
getColumnConfig(tableName: TableName): Promise<TableColumnConfig>;
/**
* 获取所有表格的配置
*/
getAllColumnConfigs(): Promise<TableColumnConfig[]>;
/**
* 检查提供者是否可用
*/
isAvailable(): Promise<boolean>;
}
文件:src/config/tableColumns/adapters/LocalColumnConfigAdapter.ts
export class LocalColumnConfigAdapter implements IColumnConfigProvider {
private readonly defaultConfigs: Map<TableName, TableColumnConfig>;
constructor() {
this.defaultConfigs = new Map([
// worklist 的默认列配置
['worklist', {
tableName: 'worklist',
columns: [
{ key: 'PatientID', visible: true, order: 1, width: 120 },
{ key: 'PatientName', visible: true, order: 2, width: 150 },
{ key: 'StudyID', visible: true, order: 3, width: 120 },
{ key: 'AccessionNumber', visible: true, order: 4, width: 150 },
{ key: 'StudyStatus', visible: true, order: 5, width: 100 },
{ key: 'Modality', visible: true, order: 6, width: 100 },
{ key: 'StudyStartDatetime', visible: true, order: 7, width: 180 },
{ key: 'PatientAge', visible: true, order: 8, width: 80 },
{ key: 'PatientSex', visible: true, order: 9, width: 80 },
// 其他列默认隐藏 (visible: false)
],
version: '1.0.0',
}],
// history 的默认列配置
['history', {
tableName: 'history',
columns: [
{ key: 'StudyID', visible: true, order: 1, width: 120 },
{ key: 'PatientName', visible: true, order: 2, width: 150 },
{ key: 'StudyDescription', visible: true, order: 3, width: 200 },
{ key: 'StudyStartDatetime', visible: true, order: 4, width: 180 },
{ key: 'IsExported', visible: true, order: 5, width: 100 },
{ key: 'StudyStatus', visible: true, order: 6, width: 100 },
],
version: '1.0.0',
}],
]);
}
async getColumnConfig(tableName: TableName): Promise<TableColumnConfig> {
const config = this.defaultConfigs.get(tableName);
if (!config) {
throw new Error(`No default config found for table: ${tableName}`);
}
return Promise.resolve(config);
}
async getAllColumnConfigs(): Promise<TableColumnConfig[]> {
return Promise.resolve(Array.from(this.defaultConfigs.values()));
}
async isAvailable(): Promise<boolean> {
return Promise.resolve(true); // 本地配置总是可用
}
}
设计特点:
文件:src/config/tableColumns/adapters/RemoteColumnConfigAdapter.ts
export class RemoteColumnConfigAdapter implements IColumnConfigProvider {
private configCache: Map<TableName, TableColumnConfig> = new Map();
private cacheExpiry: number = 5 * 60 * 1000; // 5分钟缓存
private lastFetchTime: number = 0;
async getColumnConfig(tableName: TableName): Promise<TableColumnConfig> {
// 如果缓存有效,直接返回
if (this.isCacheValid() && this.configCache.has(tableName)) {
return this.configCache.get(tableName)!;
}
// 否则从API获取
await this.fetchAndCacheConfigs();
const config = this.configCache.get(tableName);
if (!config) {
throw new Error(`No config found for table: ${tableName}`);
}
return config;
}
async getAllColumnConfigs(): Promise<TableColumnConfig[]> {
if (!this.isCacheValid()) {
await this.fetchAndCacheConfigs();
}
return Array.from(this.configCache.values());
}
async isAvailable(): Promise<boolean> {
try {
await this.fetchAndCacheConfigs();
return true;
} catch (error) {
console.error('Remote config provider unavailable:', error);
return false;
}
}
private async fetchAndCacheConfigs(): Promise<void> {
try {
const response = await fetchTableColumnConfig();
if (response.success && response.data) {
this.configCache.clear();
response.data.forEach(config => {
this.configCache.set(config.tableName, config);
});
this.lastFetchTime = Date.now();
}
} catch (error) {
throw new Error(`Failed to fetch remote config: ${error}`);
}
}
private isCacheValid(): boolean {
return Date.now() - this.lastFetchTime < this.cacheExpiry;
}
}
设计特点:
文件:src/API/tableColumnConfig.ts
import axios from './interceptor';
import { ColumnConfigResponse } from '../config/tableColumns/types/columnConfig';
/**
* 获取表格列配置
*/
export async function fetchTableColumnConfig(): Promise<ColumnConfigResponse> {
const response = await axios.get<ColumnConfigResponse>(
'/api/config/table-columns'
);
return response.data;
}
API 端点:GET /api/config/table-columns
响应格式:
{
"success": true,
"data": [
{
"tableName": "worklist",
"columns": [
{ "key": "PatientID", "visible": true, "order": 1, "width": 120 },
{ "key": "PatientName", "visible": true, "order": 2, "width": 150 }
],
"version": "1.0.0",
"updatedAt": "2025-10-07T10:00:00Z"
}
]
}
文件:src/config/tableColumns/domain/ColumnConfigService.ts
export class ColumnConfigService {
private primaryProvider: IColumnConfigProvider;
private fallbackProvider: IColumnConfigProvider;
constructor(
primaryProvider?: IColumnConfigProvider,
fallbackProvider?: IColumnConfigProvider
) {
// 默认:优先使用远程,回退到本地
this.primaryProvider = primaryProvider || new RemoteColumnConfigAdapter();
this.fallbackProvider = fallbackProvider || new LocalColumnConfigAdapter();
}
/**
* 获取表格列配置
* 优先使用主提供者,失败则回退到备用提供者
*/
async getColumnConfig(tableName: TableName): Promise<TableColumnConfig> {
try {
if (await this.primaryProvider.isAvailable()) {
return await this.primaryProvider.getColumnConfig(tableName);
}
} catch (error) {
console.warn('Primary provider failed, using fallback:', error);
}
// 回退到备用提供者
return await this.fallbackProvider.getColumnConfig(tableName);
}
/**
* 切换提供者
*/
switchProvider(provider: IColumnConfigProvider): void {
this.primaryProvider = provider;
}
}
// 导出单例
export const columnConfigService = new ColumnConfigService();
设计特点:
文件:src/config/tableColumns/index.ts
// 类型定义
export * from './types/columnConfig';
// 端口接口
export * from './ports/IColumnConfigProvider';
// 适配器
export * from './adapters/LocalColumnConfigAdapter';
export * from './adapters/RemoteColumnConfigAdapter';
// 领域服务
export * from './domain/ColumnConfigService';
export { columnConfigService } from './domain/ColumnConfigService';
文件:src/pages/patient/components/WorklistTable.tsx
interface WorklistTableProps {
columnConfig?: ColumnConfig[]; // ⭐ 新增:列配置(可选)
worklistData: Task[];
filters?: WorkFilter;
page?: number;
pageSize?: number;
selectedIds: string[];
handleRowClick: (record: Task) => void;
handleRowDoubleClick: (record: Task) => void;
}
const WorklistTable: React.FC<WorklistTableProps> = ({
columnConfig = [], // 接收配置,默认为空数组
worklistData,
selectedIds,
handleRowClick,
handleRowDoubleClick,
}) => {
// 根据传入的配置过滤和排序列
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]);
// 列可调整大小的逻辑
const [columns, setColumns] = useState<TableColumnsType<DataType>>(visibleColumns);
useEffect(() => {
setColumns(visibleColumns);
}, [visibleColumns]);
// ... 其他代码
};
关键点:
useMemo
优化性能,只在配置变化时重新计算文件:src/pages/patient/worklist.tsx
import { columnConfigService } from '@/config/tableColumns';
import { ColumnConfig } from '@/config/tableColumns/types/columnConfig';
const WorklistPage: React.FC = () => {
const [columnConfig, setColumnConfig] = useState<ColumnConfig[]>([]); // 列配置状态
// 获取列配置
useEffect(() => {
columnConfigService
.getColumnConfig('worklist')
.then(config => {
setColumnConfig(config.columns);
})
.catch(error => {
console.error('Failed to load worklist column config:', error);
// 失败时使用空配置,表格会显示所有列
setColumnConfig([]);
});
}, []);
return (
<WorklistTable
columnConfig={columnConfig} // ⭐ 传递配置
worklistData={worklistData}
selectedIds={selectedIds}
handleRowClick={handleRowClick}
handleRowDoubleClick={handleRowDoubleClick}
/>
);
};
const columnsDef = [
{ title: 'StudyInstanceUID', dataIndex: 'StudyInstanceUID' },
{ title: 'StudyID', dataIndex: 'StudyID' },
{ title: 'SpecificCharacterSet', dataIndex: 'SpecificCharacterSet' },
{ title: 'AccessionNumber', dataIndex: 'AccessionNumber' },
{ title: 'PatientID', dataIndex: 'PatientID' },
{ title: 'PatientName', dataIndex: 'PatientName' },
{ title: 'DisplayPatientName', dataIndex: 'DisplayPatientName' },
{ title: 'PatientSize', dataIndex: 'PatientSize' },
{ title: 'PatientAge', dataIndex: 'PatientAge' },
{ title: 'PatientSex', dataIndex: 'PatientSex' },
{ title: 'AdmittingTime', dataIndex: 'AdmittingTime' },
{ title: 'RegSource', dataIndex: 'RegSource' },
{ title: 'StudyStatus', dataIndex: 'StudyStatus' },
{ title: 'RequestedProcedureID', dataIndex: 'RequestedProcedureID' },
{ title: 'PerformedProtocolCodeValue', dataIndex: 'PerformedProtocolCodeValue' },
{ title: 'PerformedProtocolCodeMeaning', dataIndex: 'PerformedProtocolCodeMeaning' },
{ title: 'PerformedProcedureStepID', dataIndex: 'PerformedProcedureStepID' },
{ title: 'StudyDescription', dataIndex: 'StudyDescription' },
{ title: 'StudyStartDatetime', dataIndex: 'StudyStartDatetime' },
{ title: 'ScheduledProcedureStepStartDate', dataIndex: 'ScheduledProcedureStepStartDate' },
{ title: 'StudyLock', dataIndex: 'StudyLock' },
{ title: 'OperatorID', dataIndex: 'OperatorID' },
{ title: 'Modality', dataIndex: 'Modality' },
{ title: 'Views', dataIndex: 'Views' },
{ title: 'Thickness', dataIndex: 'Thickness' },
{ title: 'PatientType', dataIndex: 'PatientType' },
{ title: 'StudyType', dataIndex: 'StudyType' },
{ title: 'QRCode', dataIndex: 'QRCode' },
{ title: 'IsExported', dataIndex: 'IsExported' },
{ title: 'IsEdited', dataIndex: 'IsEdited' },
{ title: 'WorkRef', dataIndex: 'WorkRef' },
{ title: 'IsAppended', dataIndex: 'IsAppended' },
{ title: 'CreationTime', dataIndex: 'CreationTime' },
{ title: 'MappedStatus', dataIndex: 'MappedStatus' },
{ title: 'IsDelete', dataIndex: 'IsDelete' },
];
详见:docs/测试/表格列配置功能测试方案.md
cypress/e2e/patient/worklist/column-config.cy.ts
cypress/support/mock/handlers/columnConfig.ts
cypress/support/pageObjects/WorklistPage.ts
(扩展)文件路径 | 作用 |
---|---|
src/config/tableColumns/types/columnConfig.ts |
类型定义 |
src/config/tableColumns/ports/IColumnConfigProvider.ts |
端口接口 |
src/config/tableColumns/adapters/LocalColumnConfigAdapter.ts |
本地适配器 |
src/config/tableColumns/adapters/RemoteColumnConfigAdapter.ts |
远程适配器 |
src/config/tableColumns/domain/ColumnConfigService.ts |
领域服务 |
src/config/tableColumns/index.ts |
导出模块 |
src/API/tableColumnConfig.ts |
API调用 |
cypress/support/mock/handlers/columnConfig.ts |
Mock handlers |
cypress/e2e/patient/worklist/column-config.cy.ts |
E2E测试 |
docs/测试/表格列配置功能测试方案.md |
测试文档 |
文件路径 | 修改内容 |
---|---|
src/pages/patient/components/WorklistTable.tsx |
添加 columnConfig prop,实现列过滤和排序逻辑 |
src/pages/patient/worklist.tsx |
获取列配置并传递给 WorklistTable |
cypress/support/pageObjects/WorklistPage.ts |
添加列相关测试方法 |
要为 History 页面添加相同功能,只需:
import { columnConfigService } from '@/config/tableColumns';
import { ColumnConfig } from '@/config/tableColumns/types/columnConfig';
const HistoryList: React.FC = () => {
const [columnConfig, setColumnConfig] = useState<ColumnConfig[]>([]);
useEffect(() => {
columnConfigService
.getColumnConfig('history') // ⭐ 使用 'history'
.then(config => {
setColumnConfig(config.columns);
})
.catch(error => {
console.error('Failed to load history column config:', error);
setColumnConfig([]);
});
}, []);
return (
<WorklistTable // 或者 HistoryTable
columnConfig={columnConfig}
// ... 其他 props
/>
);
};
在 LocalColumnConfigAdapter.ts
中已经包含 history 的默认配置:
['history', {
tableName: 'history',
columns: [
{ key: 'StudyID', visible: true, order: 1, width: 120 },
{ key: 'PatientName', visible: true, order: 2, width: 150 },
{ key: 'StudyDescription', visible: true, order: 3, width: 200 },
{ key: 'StudyStartDatetime', visible: true, order: 4, width: 180 },
{ key: 'IsExported', visible: true, order: 5, width: 100 },
{ key: 'StudyStatus', visible: true, order: 6, width: 100 },
],
version: '1.0.0',
}]
key
必须与 columnsDef
中的 dataIndex
完全匹配order
应该从 1 开始连续编号visible: false
的列不会显示,无论 order 值如何success
和 data
字段data
是数组,每个元素是一个表格的配置columnConfig
为空数组时,显示所有35个列cacheExpiry
值允许用户在 UI 中自定义列配置:
提供独立的配置管理页面:
扩展配置支持:
A: 端口适配器模式提供了以下优势:
A: 考虑因素:
A: 两者各有用途:
A: 只需两步:
LocalColumnConfigAdapter.ts
的 Map 中添加新表格的默认配置columnConfigService.getColumnConfig('新表格名')
A: 不会。系统有三层保障:
docs/测试/表格列配置功能测试方案.md
docs/实现/
日期 | 修改人 | 修改内容 |
---|---|---|
2025/10/10 | - | 创建文档,完整记录表格列配置功能实现 |
// 在任何需要表格配置的页面中
import { useState, useEffect } from 'react';
import { columnConfigService } from '@/config/tableColumns';
import { ColumnConfig } from '@/config/tableColumns/types/columnConfig';
const MyTablePage: React.FC = () => {
const [columnConfig, setColumnConfig] = useState<ColumnConfig[]>([]);
useEffect(() => {
columnConfigService
.getColumnConfig('worklist') // 或 'history'
.then(config => setColumnConfig(config.columns))
.catch(error => {
console.error('Failed to load column config:', error);
setColumnConfig([]); // 使用空配置作为回退
});
}, []);
return (
<MyTable
columnConfig={columnConfig}
// ... 其他 props
/>
);
};
import { IColumnConfigProvider } from '@/config/tableColumns';
// 创建数据库提供者
class DatabaseColumnConfigAdapter implements IColumnConfigProvider {
async getColumnConfig(tableName: TableName): Promise<TableColumnConfig> {
// 从数据库获取配置
const config = await database.getTableConfig(tableName);
return config;
}
async getAllColumnConfigs(): Promise<TableColumnConfig[]> {
return await database.getAllTableConfigs();
}
async isAvailable(): Promise<boolean> {
return await database.isConnected();
}
}
// 使用自定义提供者
const customService = new ColumnConfigService(
new DatabaseColumnConfigAdapter(), // 主提供者
new LocalColumnConfigAdapter() // 回退提供者
);
{
tableName: 'worklist',
columns: [
{ key: 'PatientID', visible: true, order: 1 },
{ key: 'PatientName', visible: true, order: 2 }
]
}
{
tableName: 'worklist',
columns: [
{
key: 'PatientID',
visible: true,
order: 1,
width: 120,
fixed: 'left'
},
{
key: 'PatientName',
visible: true,
order: 2,
width: 150
},
{
key: 'StudyStatus',
visible: true,
order: 3,
width: 100,
fixed: 'right'
}
],
version: '1.0.0',
updatedAt: '2025-10-10T12:00:00Z'
}
文档结束