CardTable.js 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. import React, { useState, useEffect, useRef } from 'react';
  2. import { Table, Card, Skeleton, Pagination } 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. return (
  90. <div className="flex flex-col gap-2">
  91. {dataSource.map((record, index) => {
  92. const rowKeyVal = getRowKey(record, index);
  93. return (
  94. <Card key={rowKeyVal} className="!rounded-2xl shadow-sm">
  95. {columns.map((col, colIdx) => {
  96. // 忽略隐藏列
  97. if (tableProps?.visibleColumns && !tableProps.visibleColumns[col.key]) {
  98. return null;
  99. }
  100. const title = col.title;
  101. // 计算单元格内容
  102. const cellContent = col.render
  103. ? col.render(record[col.dataIndex], record, index)
  104. : record[col.dataIndex];
  105. // 空标题列(通常为操作按钮)单独渲染
  106. if (!title) {
  107. return (
  108. <div
  109. key={col.key || colIdx}
  110. className="mt-2 flex justify-end"
  111. >
  112. {cellContent}
  113. </div>
  114. );
  115. }
  116. return (
  117. <div
  118. key={col.key || colIdx}
  119. className="flex justify-between items-start py-1 border-b last:border-b-0 border-dashed border-gray-200"
  120. >
  121. <span className="font-medium text-gray-600 mr-2 whitespace-nowrap select-none">
  122. {title}
  123. </span>
  124. <div className="flex-1 break-all flex justify-end items-center gap-1">
  125. {cellContent !== undefined && cellContent !== null ? cellContent : '-'}
  126. </div>
  127. </div>
  128. );
  129. })}
  130. </Card>
  131. );
  132. })}
  133. {/* 分页组件 */}
  134. {tableProps.pagination && (
  135. <div className="mt-2 flex justify-center">
  136. <Pagination {...tableProps.pagination} />
  137. </div>
  138. )}
  139. </div>
  140. );
  141. };
  142. CardTable.propTypes = {
  143. columns: PropTypes.array.isRequired,
  144. dataSource: PropTypes.array,
  145. loading: PropTypes.bool,
  146. rowKey: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
  147. };
  148. export default CardTable;