diff --git a/web/src/components/common/ui/CardTable.js b/web/src/components/common/ui/CardTable.js new file mode 100644 index 00000000..3418b51b --- /dev/null +++ b/web/src/components/common/ui/CardTable.js @@ -0,0 +1,164 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { Table, Card, Skeleton, Pagination } 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))} +
+ ); + } + + // 渲染移动端卡片 + 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 && ( +
+ +
+ )} +
+ ); +}; + +CardTable.propTypes = { + columns: PropTypes.array.isRequired, + dataSource: PropTypes.array, + loading: PropTypes.bool, + rowKey: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), +}; + +export default CardTable; \ No newline at end of file diff --git a/web/src/components/table/channels/ChannelsTable.jsx b/web/src/components/table/channels/ChannelsTable.jsx index c95d0b17..618039d2 100644 --- a/web/src/components/table/channels/ChannelsTable.jsx +++ b/web/src/components/table/channels/ChannelsTable.jsx @@ -1,5 +1,6 @@ import React, { useMemo } from 'react'; -import { Table, Empty } from '@douyinfe/semi-ui'; +import { Empty } from '@douyinfe/semi-ui'; +import CardTable from '../../common/ui/CardTable.js'; import { IllustrationNoResult, IllustrationNoResultDark @@ -96,7 +97,7 @@ const ChannelsTable = (channelsData) => { }, [compactMode, visibleColumnsList]); return ( -
{ }, [compactMode, visibleColumnsList]); return ( -
{ return ( <> -
{ }, [compactMode, visibleColumnsList]); return ( -
{ }, [compactMode, columns]); return ( -
{ }; return ( -
{ return ( <> -