/*
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see .
For commercial licensing, please contact support@quantumnous.com
*/
import React, { useState, useEffect, useRef } from 'react';
import { Table, Card, Skeleton, Pagination, Empty } from '@douyinfe/semi-ui';
import PropTypes from 'prop-types';
import { useIsMobile } from '../../../hooks/common/useIsMobile';
/**
* CardTable 响应式表格组件
*
* 在桌面端渲染 Semi-UI 的 Table 组件,在移动端则将每一行数据渲染成 Card 形式。
* 该组件与 Table 组件的大部分 API 保持一致,只需将原 Table 换成 CardTable 即可。
*/
const CardTable = ({ columns = [], dataSource = [], loading = false, rowKey = 'key', ...tableProps }) => {
const isMobile = useIsMobile();
// Skeleton 显示控制,确保至少展示 500ms 动效
const [showSkeleton, setShowSkeleton] = useState(loading);
const loadingStartRef = useRef(Date.now());
useEffect(() => {
if (loading) {
loadingStartRef.current = Date.now();
setShowSkeleton(true);
} else {
const elapsed = Date.now() - loadingStartRef.current;
const remaining = Math.max(0, 500 - elapsed);
if (remaining === 0) {
setShowSkeleton(false);
} else {
const timer = setTimeout(() => setShowSkeleton(false), remaining);
return () => clearTimeout(timer);
}
}
}, [loading]);
// 解析行主键
const getRowKey = (record, index) => {
if (typeof rowKey === 'function') return rowKey(record);
return record[rowKey] !== undefined ? record[rowKey] : index;
};
// 如果不是移动端,直接渲染原 Table
if (!isMobile) {
return (
);
}
// 加载中占位:根据列信息动态模拟真实布局
if (showSkeleton) {
const visibleCols = columns.filter((col) => {
if (tableProps?.visibleColumns && col.key) {
return tableProps.visibleColumns[col.key];
}
return true;
});
const renderSkeletonCard = (key) => {
const placeholder = (
{visibleCols.map((col, idx) => {
if (!col.title) {
return (
);
}
return (
);
})}
);
return (
);
};
return (
{[1, 2, 3].map((i) => renderSkeletonCard(i))}
);
}
// 渲染移动端卡片
const isEmpty = !showSkeleton && (!dataSource || dataSource.length === 0);
if (isEmpty) {
// 若传入 empty 属性则使用之,否则使用默认 Empty
if (tableProps.empty) return tableProps.empty;
return (
);
}
return (
{dataSource.map((record, index) => {
const rowKeyVal = getRowKey(record, index);
return (
{columns.map((col, colIdx) => {
// 忽略隐藏列
if (tableProps?.visibleColumns && !tableProps.visibleColumns[col.key]) {
return null;
}
const title = col.title;
// 计算单元格内容
const cellContent = col.render
? col.render(record[col.dataIndex], record, index)
: record[col.dataIndex];
// 空标题列(通常为操作按钮)单独渲染
if (!title) {
return (
{cellContent}
);
}
return (
{title}
{cellContent !== undefined && cellContent !== null ? cellContent : '-'}
);
})}
);
})}
{/* 分页组件 */}
{tableProps.pagination && dataSource.length > 0 && (
)}
);
};
CardTable.propTypes = {
columns: PropTypes.array.isRequired,
dataSource: PropTypes.array,
loading: PropTypes.bool,
rowKey: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
};
export default CardTable;