|
@@ -0,0 +1,967 @@
|
|
|
+# 表格列配置功能 - 实现文档
|
|
|
+
|
|
|
+## 1. 概述
|
|
|
+
|
|
|
+本文档记录了 Worklist 和 History 等页面表格的列配置功能实现,支持通过配置信息动态控制表格显示的列,而不是显示所有35个列。
|
|
|
+
|
|
|
+## 2. 需求背景
|
|
|
+
|
|
|
+### 2.1 业务场景
|
|
|
+
|
|
|
+在 Worklist 和 History 页面,表格默认显示所有35个列,导致:
|
|
|
+- 表格过宽,需要大量横向滚动
|
|
|
+- 用户难以快速找到关键信息
|
|
|
+- 不同用户对列的需求不同
|
|
|
+- 无法根据业务场景灵活调整
|
|
|
+
|
|
|
+### 2.2 核心需求
|
|
|
+
|
|
|
+**主要功能**:
|
|
|
+- ✅ 支持配置哪些列显示、哪些列隐藏
|
|
|
+- ✅ 支持配置列的显示顺序
|
|
|
+- ✅ 支持配置列的宽度
|
|
|
+- ✅ 支持从远程 API 获取配置
|
|
|
+- ✅ API 失败时自动回退到本地默认配置
|
|
|
+- ✅ 不同表格(worklist/history)使用独立配置
|
|
|
+- ✅ 向后兼容:无配置时显示所有列
|
|
|
+
|
|
|
+**技术要求**:
|
|
|
+- 配置可来自本地硬编码或远程 API
|
|
|
+- 远程配置失败时有回退机制
|
|
|
+- 配置通过 props 传递给表格组件
|
|
|
+- 支持多表格独立配置
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 3. 架构设计
|
|
|
+
|
|
|
+### 3.1 端口适配器模式(Port-Adapter Pattern)
|
|
|
+
|
|
|
+采用端口适配器模式实现灵活的配置源切换:
|
|
|
+
|
|
|
+```
|
|
|
+┌─────────────────────────────────────────────┐
|
|
|
+│ 配置源(可切换) │
|
|
|
+│ ┌──────────────┐ ┌──────────────┐ │
|
|
|
+│ │ 本地硬编码 │ │ 远程 API │ │
|
|
|
+│ └──────────────┘ └──────────────┘ │
|
|
|
+└─────────────┬──────────────┬────────────────┘
|
|
|
+ │ │
|
|
|
+ ↓ ↓
|
|
|
+ ┌──────────────────────────────────┐
|
|
|
+ │ IColumnConfigProvider (端口) │
|
|
|
+ │ - getColumnConfig() │
|
|
|
+ │ - getAllColumnConfigs() │
|
|
|
+ │ - isAvailable() │
|
|
|
+ └──────────────────────────────────┘
|
|
|
+ ↓
|
|
|
+ ┌──────────────────────────────────┐
|
|
|
+ │ ColumnConfigService (领域服务) │
|
|
|
+ │ - 策略模式 │
|
|
|
+ │ - 主提供者 + 回退提供者 │
|
|
|
+ │ - 自动切换 │
|
|
|
+ └──────────────────────────────────┘
|
|
|
+ ↓
|
|
|
+ ┌──────────────────────────────────┐
|
|
|
+ │ 父组件 (worklist.tsx) │
|
|
|
+ │ - 获取配置 │
|
|
|
+ │ - 通过 props 传递 │
|
|
|
+ └──────────────────────────────────┘
|
|
|
+ ↓
|
|
|
+ ┌──────────────────────────────────┐
|
|
|
+ │ WorklistTable 组件 │
|
|
|
+ │ - 根据配置过滤列 │
|
|
|
+ │ - 根据配置排序列 │
|
|
|
+ │ - 应用列宽 │
|
|
|
+ └──────────────────────────────────┘
|
|
|
+```
|
|
|
+
|
|
|
+### 3.2 数据流
|
|
|
+
|
|
|
+```
|
|
|
+应用启动
|
|
|
+ ↓
|
|
|
+父组件 useEffect
|
|
|
+ ↓
|
|
|
+columnConfigService.getColumnConfig('worklist')
|
|
|
+ ↓
|
|
|
+ ├─→ 尝试主提供者 (RemoteColumnConfigAdapter)
|
|
|
+ │ ├─→ 成功 → 返回远程配置
|
|
|
+ │ └─→ 失败 ↓
|
|
|
+ │
|
|
|
+ └─→ 回退提供者 (LocalColumnConfigAdapter)
|
|
|
+ └─→ 返回本地配置
|
|
|
+ ↓
|
|
|
+父组件接收配置 → setState
|
|
|
+ ↓
|
|
|
+通过 props 传递给 WorklistTable
|
|
|
+ ↓
|
|
|
+WorklistTable.visibleColumns (useMemo)
|
|
|
+ ├─→ 配置为空 → 显示所有35列
|
|
|
+ └─→ 配置不为空 → 过滤 + 排序 + 应用宽度
|
|
|
+ ↓
|
|
|
+渲染表格
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 4. 技术实现
|
|
|
+
|
|
|
+### 4.1 类型定义
|
|
|
+
|
|
|
+**文件**:`src/config/tableColumns/types/columnConfig.ts`
|
|
|
+
|
|
|
+```typescript
|
|
|
+/**
|
|
|
+ * 单个列的配置
|
|
|
+ */
|
|
|
+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;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 4.2 端口接口
|
|
|
+
|
|
|
+**文件**:`src/config/tableColumns/ports/IColumnConfigProvider.ts`
|
|
|
+
|
|
|
+```typescript
|
|
|
+import { TableColumnConfig, TableName } from '../types/columnConfig';
|
|
|
+
|
|
|
+export interface IColumnConfigProvider {
|
|
|
+ /**
|
|
|
+ * 获取指定表格的列配置
|
|
|
+ */
|
|
|
+ getColumnConfig(tableName: TableName): Promise<TableColumnConfig>;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取所有表格的配置
|
|
|
+ */
|
|
|
+ getAllColumnConfigs(): Promise<TableColumnConfig[]>;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 检查提供者是否可用
|
|
|
+ */
|
|
|
+ isAvailable(): Promise<boolean>;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 4.3 本地适配器实现
|
|
|
+
|
|
|
+**文件**:`src/config/tableColumns/adapters/LocalColumnConfigAdapter.ts`
|
|
|
+
|
|
|
+```typescript
|
|
|
+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); // 本地配置总是可用
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**设计特点**:
|
|
|
+- 使用 Map 存储多表格配置
|
|
|
+- 默认显示9个核心列
|
|
|
+- 本地配置总是可用(isAvailable 返回 true)
|
|
|
+
|
|
|
+### 4.4 远程适配器实现
|
|
|
+
|
|
|
+**文件**:`src/config/tableColumns/adapters/RemoteColumnConfigAdapter.ts`
|
|
|
+
|
|
|
+```typescript
|
|
|
+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;
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**设计特点**:
|
|
|
+- 实现5分钟缓存机制,减少 API 调用
|
|
|
+- isAvailable 通过尝试获取配置来判断可用性
|
|
|
+- 失败时抛出错误,触发回退机制
|
|
|
+
|
|
|
+### 4.5 API 调用
|
|
|
+
|
|
|
+**文件**:`src/API/tableColumnConfig.ts`
|
|
|
+
|
|
|
+```typescript
|
|
|
+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`
|
|
|
+
|
|
|
+**响应格式**:
|
|
|
+```json
|
|
|
+{
|
|
|
+ "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"
|
|
|
+ }
|
|
|
+ ]
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 4.6 领域服务(策略模式 + 回退机制)
|
|
|
+
|
|
|
+**文件**:`src/config/tableColumns/domain/ColumnConfigService.ts`
|
|
|
+
|
|
|
+```typescript
|
|
|
+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();
|
|
|
+```
|
|
|
+
|
|
|
+**设计特点**:
|
|
|
+- 策略模式:可运行时切换提供者
|
|
|
+- 回退机制:主提供者失败自动使用备用
|
|
|
+- 单例导出:全局共享服务实例
|
|
|
+
|
|
|
+### 4.7 导出模块
|
|
|
+
|
|
|
+**文件**:`src/config/tableColumns/index.ts`
|
|
|
+
|
|
|
+```typescript
|
|
|
+// 类型定义
|
|
|
+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';
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 5. 组件改造
|
|
|
+
|
|
|
+### 5.1 WorklistTable 组件
|
|
|
+
|
|
|
+**文件**:`src/pages/patient/components/WorklistTable.tsx`
|
|
|
+
|
|
|
+#### 新增接口定义
|
|
|
+
|
|
|
+```typescript
|
|
|
+interface WorklistTableProps {
|
|
|
+ columnConfig?: ColumnConfig[]; // ⭐ 新增:列配置(可选)
|
|
|
+ worklistData: Task[];
|
|
|
+ filters?: WorkFilter;
|
|
|
+ page?: number;
|
|
|
+ pageSize?: number;
|
|
|
+ selectedIds: string[];
|
|
|
+ handleRowClick: (record: Task) => void;
|
|
|
+ handleRowDoubleClick: (record: Task) => void;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### 核心逻辑:根据配置过滤和排序列
|
|
|
+
|
|
|
+```typescript
|
|
|
+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` 优化性能,只在配置变化时重新计算
|
|
|
+- 支持向后兼容:配置为空时显示所有列
|
|
|
+- 三步处理:过滤 → 应用宽度 → 排序
|
|
|
+
|
|
|
+### 5.2 父组件集成
|
|
|
+
|
|
|
+**文件**:`src/pages/patient/worklist.tsx`
|
|
|
+
|
|
|
+#### 导入依赖
|
|
|
+
|
|
|
+```typescript
|
|
|
+import { columnConfigService } from '@/config/tableColumns';
|
|
|
+import { ColumnConfig } from '@/config/tableColumns/types/columnConfig';
|
|
|
+```
|
|
|
+
|
|
|
+#### 状态管理
|
|
|
+
|
|
|
+```typescript
|
|
|
+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}
|
|
|
+ />
|
|
|
+ );
|
|
|
+};
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 6. 完整的 35 个列定义
|
|
|
+
|
|
|
+```typescript
|
|
|
+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' },
|
|
|
+];
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 7. 测试方案
|
|
|
+
|
|
|
+详见:`docs/测试/表格列配置功能测试方案.md`
|
|
|
+
|
|
|
+### 7.1 核心测试场景
|
|
|
+
|
|
|
+1. **TC-WL-COL-01**: 使用本地配置显示列
|
|
|
+2. **TC-WL-COL-02**: 使用远程API配置显示列
|
|
|
+3. **TC-WL-COL-03**: API失败时回退到本地配置
|
|
|
+4. **TC-WL-COL-04**: 验证特定列的显示/隐藏
|
|
|
+5. **TC-WL-COL-05**: 验证列的显示顺序
|
|
|
+6. **TC-WL-COL-06**: 验证列宽配置
|
|
|
+7. **TC-WL-COL-07**: 无配置时显示所有列(向后兼容)
|
|
|
+
|
|
|
+### 7.2 E2E 测试文件
|
|
|
+
|
|
|
+- `cypress/e2e/patient/worklist/column-config.cy.ts`
|
|
|
+- `cypress/support/mock/handlers/columnConfig.ts`
|
|
|
+- `cypress/support/pageObjects/WorklistPage.ts`(扩展)
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 8. 相关文件清单
|
|
|
+
|
|
|
+### 8.1 新增文件(10个)
|
|
|
+
|
|
|
+| 文件路径 | 作用 |
|
|
|
+|---------|------|
|
|
|
+| `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` | 测试文档 |
|
|
|
+
|
|
|
+### 8.2 修改文件(3个)
|
|
|
+
|
|
|
+| 文件路径 | 修改内容 |
|
|
|
+|---------|---------|
|
|
|
+| `src/pages/patient/components/WorklistTable.tsx` | 添加 columnConfig prop,实现列过滤和排序逻辑 |
|
|
|
+| `src/pages/patient/worklist.tsx` | 获取列配置并传递给 WorklistTable |
|
|
|
+| `cypress/support/pageObjects/WorklistPage.ts` | 添加列相关测试方法 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 9. History 页面扩展指南
|
|
|
+
|
|
|
+要为 History 页面添加相同功能,只需:
|
|
|
+
|
|
|
+### 步骤 1:修改 HistoryList.tsx
|
|
|
+
|
|
|
+```typescript
|
|
|
+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
|
|
|
+ />
|
|
|
+ );
|
|
|
+};
|
|
|
+```
|
|
|
+
|
|
|
+### 步骤 2:配置默认列
|
|
|
+
|
|
|
+在 `LocalColumnConfigAdapter.ts` 中已经包含 history 的默认配置:
|
|
|
+
|
|
|
+```typescript
|
|
|
+['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',
|
|
|
+}]
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 10. 优势与特点
|
|
|
+
|
|
|
+### 10.1 架构优势
|
|
|
+
|
|
|
+1. **灵活切换**:端口适配器模式支持运行时切换配置源
|
|
|
+2. **容错机制**:远程配置失败自动回退到本地
|
|
|
+3. **独立配置**:不同表格使用各自的配置,互不影响
|
|
|
+4. **向后兼容**:无配置时显示所有列,不影响现有功能
|
|
|
+5. **易于扩展**:新增表格只需添加配置,无需修改核心代码
|
|
|
+
|
|
|
+### 10.2 性能优化
|
|
|
+
|
|
|
+1. **缓存机制**:远程配置缓存5分钟,减少 API 调用
|
|
|
+2. **useMemo**:列计算使用 useMemo,避免不必要的重渲染
|
|
|
+3. **按需加载**:只在组件初始化时获取配置
|
|
|
+
|
|
|
+### 10.3 用户体验
|
|
|
+
|
|
|
+1. **精简显示**:只显示关键列,提升可读性
|
|
|
+2. **自定义顺序**:重要列靠前显示
|
|
|
+3. **合适宽度**:每列设置合适宽度,减少调整需求
|
|
|
+4. **回退保障**:配置失败不影响使用
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 11. 注意事项
|
|
|
+
|
|
|
+### 11.1 配置格式要求
|
|
|
+
|
|
|
+- `key` 必须与 `columnsDef` 中的 `dataIndex` 完全匹配
|
|
|
+- `order` 应该从 1 开始连续编号
|
|
|
+- `visible: false` 的列不会显示,无论 order 值如何
|
|
|
+
|
|
|
+### 11.2 API 要求
|
|
|
+
|
|
|
+- 响应必须包含 `success` 和 `data` 字段
|
|
|
+- `data` 是数组,每个元素是一个表格的配置
|
|
|
+- 支持一次返回多个表格的配置
|
|
|
+
|
|
|
+### 11.3 向后兼容
|
|
|
+
|
|
|
+- 当 `columnConfig` 为空数组时,显示所有35个列
|
|
|
+- 这确保了在配置缺失或加载失败时,系统仍然可用
|
|
|
+
|
|
|
+### 11.4 缓存策略
|
|
|
+
|
|
|
+- 远程配置缓存5分钟
|
|
|
+- 如需立即更新配置,需要刷新页面或重启应用
|
|
|
+- 可根据业务需求调整 `cacheExpiry` 值
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 12. 未来改进建议
|
|
|
+
|
|
|
+### 12.1 用户自定义
|
|
|
+
|
|
|
+允许用户在 UI 中自定义列配置:
|
|
|
+- 拖拽调整列顺序
|
|
|
+- 点击切换列显示/隐藏
|
|
|
+- 调整列宽后保存到配置
|
|
|
+- 重置为默认配置
|
|
|
+
|
|
|
+### 12.2 配置管理界面
|
|
|
+
|
|
|
+提供独立的配置管理页面:
|
|
|
+- 可视化编辑列配置
|
|
|
+- 预览配置效果
|
|
|
+- 导入/导出配置文件
|
|
|
+- 版本控制
|
|
|
+
|
|
|
+### 12.3 更多配置项
|
|
|
+
|
|
|
+扩展配置支持:
|
|
|
+- 列的对齐方式(左对齐/居中/右对齐)
|
|
|
+- 列的数据格式化
|
|
|
+- 列的排序功能开关
|
|
|
+- 列的过滤功能开关
|
|
|
+- 固定列(fixed: 'left' | 'right')
|
|
|
+
|
|
|
+### 12.4 性能优化
|
|
|
+
|
|
|
+- 虚拟滚动:大量数据时只渲染可见行
|
|
|
+- 懒加载:分批加载数据
|
|
|
+- 请求合并:批量获取多个表格配置
|
|
|
+
|
|
|
+### 12.5 多租户支持
|
|
|
+
|
|
|
+- 不同用户/角色使用不同配置
|
|
|
+- 用户级别配置覆盖系统级别配置
|
|
|
+- 配置权限管理
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 13. 常见问题(FAQ)
|
|
|
+
|
|
|
+### Q1: 为什么选择端口适配器模式?
|
|
|
+
|
|
|
+**A**: 端口适配器模式提供了以下优势:
|
|
|
+1. **灵活性**:可以轻松切换配置源(本地/远程/数据库等)
|
|
|
+2. **可测试性**:可以注入 Mock 提供者进行单元测试
|
|
|
+3. **解耦**:业务逻辑不依赖具体的配置来源
|
|
|
+4. **扩展性**:新增配置源只需实现接口,无需修改现有代码
|
|
|
+
|
|
|
+### Q2: 为什么使用 props 传递配置而不是 Redux?
|
|
|
+
|
|
|
+**A**: 考虑因素:
|
|
|
+1. **职责分离**:配置获取是父组件的职责,表格组件只负责渲染
|
|
|
+2. **灵活性**:不同父组件可以传递不同的配置
|
|
|
+3. **测试简单**:测试时直接传入 Mock 配置即可
|
|
|
+4. **避免全局状态污染**:配置是组件级别的,不需要全局共享
|
|
|
+
|
|
|
+### Q3: 为什么有本地配置还需要远程配置?
|
|
|
+
|
|
|
+**A**: 两者各有用途:
|
|
|
+- **本地配置**:作为默认值和回退方案,确保系统在任何情况下都可用
|
|
|
+- **远程配置**:支持动态调整、多环境部署、用户自定义等高级功能
|
|
|
+
|
|
|
+### Q4: 如何添加新的表格配置?
|
|
|
+
|
|
|
+**A**: 只需两步:
|
|
|
+1. 在 `LocalColumnConfigAdapter.ts` 的 Map 中添加新表格的默认配置
|
|
|
+2. 在父组件中调用 `columnConfigService.getColumnConfig('新表格名')`
|
|
|
+
|
|
|
+### Q5: 配置失败会影响用户使用吗?
|
|
|
+
|
|
|
+**A**: 不会。系统有三层保障:
|
|
|
+1. 远程配置失败自动回退到本地配置
|
|
|
+2. 本地配置失败(理论上不会发生)返回空数组
|
|
|
+3. 空数组时表格显示所有列(向后兼容)
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 14. 参考资料
|
|
|
+
|
|
|
+### 14.1 设计模式
|
|
|
+
|
|
|
+- **端口适配器模式**(Ports and Adapters / Hexagonal Architecture)
|
|
|
+- **策略模式**(Strategy Pattern)
|
|
|
+- **单例模式**(Singleton Pattern)
|
|
|
+
|
|
|
+### 14.2 相关技术
|
|
|
+
|
|
|
+- [React useMemo](https://react.dev/reference/react/useMemo)
|
|
|
+- [TypeScript Generics](https://www.typescriptlang.org/docs/handbook/2/generics.html)
|
|
|
+- [Ant Design Table](https://ant.design/components/table-cn)
|
|
|
+
|
|
|
+### 14.3 项目内部文档
|
|
|
+
|
|
|
+- 测试方案:`docs/测试/表格列配置功能测试方案.md`
|
|
|
+- 其他实现文档:`docs/实现/`
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 15. 更新记录
|
|
|
+
|
|
|
+| 日期 | 修改人 | 修改内容 |
|
|
|
+|------|--------|----------|
|
|
|
+| 2025/10/10 | - | 创建文档,完整记录表格列配置功能实现 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 附录 A:完整代码示例
|
|
|
+
|
|
|
+### A.1 使用示例
|
|
|
+
|
|
|
+```typescript
|
|
|
+// 在任何需要表格配置的页面中
|
|
|
+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
|
|
|
+ />
|
|
|
+ );
|
|
|
+};
|
|
|
+```
|
|
|
+
|
|
|
+### A.2 自定义提供者
|
|
|
+
|
|
|
+```typescript
|
|
|
+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() // 回退提供者
|
|
|
+);
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 附录 B:配置示例
|
|
|
+
|
|
|
+### B.1 最小配置
|
|
|
+
|
|
|
+```typescript
|
|
|
+{
|
|
|
+ tableName: 'worklist',
|
|
|
+ columns: [
|
|
|
+ { key: 'PatientID', visible: true, order: 1 },
|
|
|
+ { key: 'PatientName', visible: true, order: 2 }
|
|
|
+ ]
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### B.2 完整配置
|
|
|
+
|
|
|
+```typescript
|
|
|
+{
|
|
|
+ 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'
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+**文档结束**
|