CardTable.js 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. import React, { useState, useEffect, useRef } from 'react';
  2. import { Table, Card, Skeleton, Pagination, Empty } from '@douyinfe/semi-ui';
  3. import PropTypes from 'prop-types';
  4. import { useIsMobile } from '../../../hooks/common/useIsMobile';
  5. /**
  6. * CardTable 响应式表格组件
  7. *
  8. * 在桌面端渲染 Semi-UI 的 Table 组件,在移动端则将每一行数据渲染成 Card 形式。
  9. * 该组件与 Table 组件的大部分 API 保持一致,只需将原 Table 换成 CardTable 即可。
  10. */
  11. const CardTable = ({ columns = [], dataSource = [], loading = false, rowKey = 'key', ...tableProps }) => {
  12. const isMobile = useIsMobile();
  13. // Skeleton 显示控制,确保至少展示 500ms 动效
  14. const [showSkeleton, setShowSkeleton] = useState(loading);
  15. const loadingStartRef = useRef(Date.now());
  16. useEffect(() => {
  17. if (loading) {
  18. loadingStartRef.current = Date.now();
  19. setShowSkeleton(true);
  20. } else {
  21. const elapsed = Date.now() - loadingStartRef.current;
  22. const remaining = Math.max(0, 500 - elapsed);
  23. if (remaining === 0) {
  24. setShowSkeleton(false);
  25. } else {
  26. const timer = setTimeout(() => setShowSkeleton(false), remaining);
  27. return () => clearTimeout(timer);
  28. }
  29. }
  30. }, [loading]);
  31. // 解析行主键
  32. const getRowKey = (record, index) => {
  33. if (typeof rowKey === 'function') return rowKey(record);
  34. return record[rowKey] !== undefined ? record[rowKey] : index;
  35. };
  36. // 如果不是移动端,直接渲染原 Table
  37. if (!isMobile) {
  38. return (
  39. <Table
  40. columns={columns}
  41. dataSource={dataSource}
  42. loading={loading}
  43. rowKey={rowKey}
  44. {...tableProps}
  45. />
  46. );
  47. }
  48. // 加载中占位:根据列信息动态模拟真实布局
  49. if (showSkeleton) {
  50. const visibleCols = columns.filter((col) => {
  51. if (tableProps?.visibleColumns && col.key) {
  52. return tableProps.visibleColumns[col.key];
  53. }
  54. return true;
  55. });
  56. const renderSkeletonCard = (key) => {
  57. const placeholder = (
  58. <div className="p-2">
  59. {visibleCols.map((col, idx) => {
  60. if (!col.title) {
  61. return (
  62. <div key={idx} className="mt-2 flex justify-end">
  63. <Skeleton.Title active style={{ width: 100, height: 24 }} />
  64. </div>
  65. );
  66. }
  67. return (
  68. <div key={idx} className="flex justify-between items-center py-1 border-b last:border-b-0 border-dashed border-gray-200">
  69. <Skeleton.Title active style={{ width: 80, height: 14 }} />
  70. <Skeleton.Title active style={{ width: `${50 + (idx % 3) * 10}%`, maxWidth: 180, height: 14 }} />
  71. </div>
  72. );
  73. })}
  74. </div>
  75. );
  76. return (
  77. <Card key={key} className="!rounded-2xl shadow-sm">
  78. <Skeleton loading={true} active placeholder={placeholder}></Skeleton>
  79. </Card>
  80. );
  81. };
  82. return (
  83. <div className="flex flex-col gap-2">
  84. {[1, 2, 3].map((i) => renderSkeletonCard(i))}
  85. </div>
  86. );
  87. }
  88. // 渲染移动端卡片
  89. const isEmpty = !showSkeleton && (!dataSource || dataSource.length === 0);
  90. if (isEmpty) {
  91. // 若传入 empty 属性则使用之,否则使用默认 Empty
  92. if (tableProps.empty) return tableProps.empty;
  93. return (
  94. <div className="flex justify-center p-4">
  95. <Empty description="No Data" />
  96. </div>
  97. );
  98. }
  99. return (
  100. <div className="flex flex-col gap-2">
  101. {dataSource.map((record, index) => {
  102. const rowKeyVal = getRowKey(record, index);
  103. return (
  104. <Card key={rowKeyVal} className="!rounded-2xl shadow-sm">
  105. {columns.map((col, colIdx) => {
  106. // 忽略隐藏列
  107. if (tableProps?.visibleColumns && !tableProps.visibleColumns[col.key]) {
  108. return null;
  109. }
  110. const title = col.title;
  111. // 计算单元格内容
  112. const cellContent = col.render
  113. ? col.render(record[col.dataIndex], record, index)
  114. : record[col.dataIndex];
  115. // 空标题列(通常为操作按钮)单独渲染
  116. if (!title) {
  117. return (
  118. <div
  119. key={col.key || colIdx}
  120. className="mt-2 flex justify-end"
  121. >
  122. {cellContent}
  123. </div>
  124. );
  125. }
  126. return (
  127. <div
  128. key={col.key || colIdx}
  129. className="flex justify-between items-start py-1 border-b last:border-b-0 border-dashed border-gray-200"
  130. >
  131. <span className="font-medium text-gray-600 mr-2 whitespace-nowrap select-none">
  132. {title}
  133. </span>
  134. <div className="flex-1 break-all flex justify-end items-center gap-1">
  135. {cellContent !== undefined && cellContent !== null ? cellContent : '-'}
  136. </div>
  137. </div>
  138. );
  139. })}
  140. </Card>
  141. );
  142. })}
  143. {/* 分页组件 */}
  144. {tableProps.pagination && dataSource.length > 0 && (
  145. <div className="mt-2 flex justify-center">
  146. <Pagination {...tableProps.pagination} />
  147. </div>
  148. )}
  149. </div>
  150. );
  151. };
  152. CardTable.propTypes = {
  153. columns: PropTypes.array.isRequired,
  154. dataSource: PropTypes.array,
  155. loading: PropTypes.bool,
  156. rowKey: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
  157. };
  158. export default CardTable;