通用表格组件GenericDataTable.md 10 KB

通用表格组件 GenericDataTable 使用文档

概述

GenericDataTable 是一个基于 Ant Design Table 的通用数据表格组件,支持泛型数据类型、列配置、列宽调整、行交互等功能。

📁 文件位置

  • 组件:src/components/GenericDataTable.tsx
  • 使用示例:
    • src/pages/patient/OutputList.tsx - 传输队列表格
    • src/pages/patient/worklist.tsx - 工作列表表格(待改造)

🎯 核心特性

1. 泛型支持

  • 支持任意数据类型
  • TypeScript 类型安全
  • 自动推断列的数据类型

2. 列配置系统

  • 显示/隐藏列
  • 列宽调整
  • 列排序
  • columnConfigService 集成

3. 列宽调整

  • 支持鼠标拖拽调整列宽
  • 最小宽度 50px
  • 使用 react-resizable

4. 行交互

  • 单击选择
  • 双击操作
  • 触摸屏双击支持
  • 行高亮显示

5. 其他特性

  • 加载状态
  • 自定义渲染
  • 响应式布局

💡 使用示例

基础用法

import { GenericDataTable, ColumnDefinition } from '@/components/GenericDataTable';

interface MyData {
  id: string;
  name: string;
  status: string;
}

const columns: ColumnDefinition<MyData>[] = [
  {
    title: '名称',
    dataIndex: 'name',
    width: 150,
  },
  {
    title: '状态',
    dataIndex: 'status',
    width: 100,
  },
];

function MyTable() {
  const [selectedIds, setSelectedIds] = useState<string[]>([]);
  const data: MyData[] = [
    { id: '1', name: '张三', status: '正常' },
    { id: '2', name: '李四', status: '异常' },
  ];

  return (
    <GenericDataTable<MyData>
      dataSource={data}
      rowKey="id"
      columnDefinitions={columns}
      selectedIds={selectedIds}
      onRowClick={(record) => setSelectedIds([record.id])}
    />
  );
}

带国际化的列定义

import { FormattedMessage } from 'react-intl';

const columns: ColumnDefinition<SendJob>[] = [
  {
    title: (
      <FormattedMessage
        id="outputTable.name"
        defaultMessage="Patient Name"
      />
    ),
    dataIndex: 'patient_name',
    width: 150,
  },
  {
    title: (
      <FormattedMessage
        id="outputTable.status"
        defaultMessage="Status"
      />
    ),
    dataIndex: 'status',
    width: 120,
  },
];

自定义渲染

const columns: ColumnDefinition<SendJob>[] = [
  {
    title: '重试次数',
    dataIndex: 'retry_count',
    width: 100,
    render: (value) => value ?? 0, // 显示 0 而不是 undefined
  },
  {
    title: '状态',
    dataIndex: 'status',
    width: 100,
    render: (value) => {
      const statusMap = {
        'ARRIVED': '已到达',
        'SENDING': '发送中',
        'FAILED': '失败',
        'SUCCESS': '成功',
      };
      return statusMap[value] || value;
    },
  },
];

集成列配置系统

import { columnConfigService } from '@/config/tableColumns';
import { ColumnConfig } from '@/config/tableColumns/types/columnConfig';

function MyTable() {
  const [columnConfig, setColumnConfig] = useState<ColumnConfig[]>([]);

  // 加载列配置
  useEffect(() => {
    columnConfigService
      .getColumnConfig('output')
      .then((config) => setColumnConfig(config.columns))
      .catch((error) => {
        console.error('Failed to load column config:', error);
        setColumnConfig([]);
      });
  }, []);

  return (
    <GenericDataTable<SendJob>
      dataSource={data}
      rowKey="task_id"
      columnDefinitions={columns}
      columnConfig={columnConfig} // 传入列配置
      selectedIds={selectedIds}
      onRowClick={handleRowClick}
    />
  );
}

完整示例:传输队列表格

import React, { useState, useEffect } from 'react';
import { GenericDataTable, ColumnDefinition } from '@/components/GenericDataTable';
import GenericPagination from '@/components/GenericPagination';
import { useAppDispatch, useAppSelector } from '@/states/store';
import {
  fetchSendQueueThunk,
  sendJobSelectionSlice,
  sendJobPaginationSlice,
} from '@/states/output/sendJob/slices/sendJobSlice';
import { SendJob } from '@/domain/output/sendJob';
import { FormattedMessage } from 'react-intl';

// 定义列
const sendJobColumns: ColumnDefinition<SendJob>[] = [
  {
    title: <FormattedMessage id="outputTable.name" defaultMessage="Patient Name" />,
    dataIndex: 'patient_name',
    width: 150,
  },
  {
    title: <FormattedMessage id="outputTable.status" defaultMessage="Status" />,
    dataIndex: 'status',
    width: 120,
  },
];

function OutputList() {
  const dispatch = useAppDispatch();
  const { data: sendJobs } = useAppSelector((state) => state.sendJobEntities);
  const { loading } = useAppSelector((state) => state.sendJobUI);
  const { page, pageSize } = useAppSelector((state) => state.sendJobPagination);
  const selectedIds = useAppSelector((state) => state.sendJobSelection.selectedIds);

  // 加载数据
  useEffect(() => {
    dispatch(fetchSendQueueThunk({ page, pageSize, filters: {} }));
  }, [dispatch, page, pageSize]);

  // 行点击
  const handleRowClick = (record: SendJob) => {
    dispatch(sendJobSelectionSlice.actions.setSelectedIds([record.task_id]));
  };

  return (
    <div className="h-full flex flex-col">
      <div className="flex-1 overflow-auto">
        <GenericDataTable<SendJob>
          dataSource={sendJobs}
          rowKey="task_id"
          columnDefinitions={sendJobColumns}
          selectedIds={selectedIds}
          onRowClick={handleRowClick}
          loading={loading}
          className="px-4"
        />
      </div>
      <GenericPagination
        paginationSelector={(state) => state.sendJobPagination}
        entitiesSelector={(state) => state.sendJobEntities}
        paginationActions={sendJobPaginationSlice.actions}
        className="border-t"
      />
    </div>
  );
}

📝 API 参考

GenericDataTableProps

ColumnDefinition

属性 类型 必填 默认值 描述
dataSource T[] - 数据源
rowKey keyof T | ((record: T) => string) - 行键,用于唯一标识每一行
columnDefinitions ColumnDefinition[] - 列定义数组
columnConfig ColumnConfig[] [] 列配置(显示/隐藏、排序等)
selectedIds string[] - 选中的行 ID 列表
onRowClick (record: T) => void - 行点击事件
onRowDoubleClick (record: T) => void - 行双击事件
className string '' 自定义类名
loading boolean false 加载状态
属性 类型 必填 默认值 描述
title React.ReactNode - 列标题
dataIndex keyof T - 数据索引
width number 150 默认宽度(px)
render (value: any, record: T, index: number) => React.ReactNode - 自定义渲染函数

🔧 高级用法

1. 动态行键

<GenericDataTable<MyData>
  dataSource={data}
  rowKey={(record) => `${record.type}_${record.id}`} // 组合键
  // ...
/>

2. 条件渲染

const columns: ColumnDefinition<SendJob>[] = [
  {
    title: '状态',
    dataIndex: 'status',
    render: (value, record) => {
      if (value === 'FAILED') {
        return <span className="text-red-500">失败</span>;
      }
      if (value === 'SUCCESS') {
        return <span className="text-green-500">成功</span>;
      }
      return value;
    },
  },
];

3. 多选支持

const [selectedIds, setSelectedIds] = useState<string[]>([]);

const handleRowClick = (record: MyData) => {
  setSelectedIds((prev) => {
    if (prev.includes(record.id)) {
      // 取消选择
      return prev.filter((id) => id !== record.id);
    } else {
      // 添加选择
      return [...prev, record.id];
    }
  });
};

<GenericDataTable<MyData>
  // ...
  selectedIds={selectedIds}
  onRowClick={handleRowClick}
/>

⚠️ 注意事项

1. 泛型类型约束

数据类型 T 必须是对象类型:

T extends Record<string, any>

2. rowKey 必须返回唯一值

// ✅ 正确
rowKey="id"  // 确保 id 唯一

// ✅ 正确
rowKey={(record) => record.task_id}

// ❌ 错误 - 可能重复
rowKey={(record) => record.status}

3. 列配置优先级

如果提供了 columnConfig,则:

  1. 只显示 visible: true 的列
  2. order 排序
  3. 使用 width 覆盖默认宽度

如果没有 columnConfig,显示所有列。

4. 性能优化

对于大数据集,考虑:

  • 使用虚拟滚动
  • 后端分页
  • 懒加载

🎨 样式定制

1. 自定义类名

<GenericDataTable
  className="my-custom-table"
  // ...
/>

2. 选中行样式

当前使用 bg-yellow-500hover:bg-yellow-800,可以通过修改组件源码自定义:

// 在 GenericDataTable.tsx 中
rowClassName={(record) => {
  const rowId = getRowKey(record);
  return selectedIds.includes(rowId)
    ? 'bg-blue-500 hover:bg-blue-700' // 自定义颜色
    : '';
}}

🔄 与现有组件对比

WorklistTable vs GenericDataTable

特性 WorklistTable GenericDataTable
数据类型 固定 Task 泛型 T
列定义 硬编码 传入参数
复用性
类型安全 Task 类型 完全类型安全
列配置 支持 支持
列宽调整 支持 支持
行交互 支持 支持

📚 相关文档

🚀 未来改进

  1. 虚拟滚动 - 支持大数据集
  2. 列拖拽排序 - 用户自定义列顺序
  3. 列固定 - 支持左右固定列
  4. 行展开 - 支持嵌套数据
  5. 导出功能 - CSV/Excel 导出
  6. 搜索过滤 - 列内搜索

🐛 已知问题

暂无

📝 更新日志

v1.0.0 (2025-10-11)

  • ✨ 初始版本
  • ✅ 泛型支持
  • ✅ 列配置系统
  • ✅ 列宽调整
  • ✅ 行交互
  • ✅ 在 OutputList 中应用