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 (
-