| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221 |
- /*
- 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 <https://www.gnu.org/licenses/>.
- For commercial licensing, please contact support@quantumnous.com
- */
- import React, { useState, useEffect, useRef } from 'react';
- import { useTranslation } from 'react-i18next';
- import { Table, Card, Skeleton, Pagination, Empty, Button, Collapsible } from '@douyinfe/semi-ui';
- import { IconChevronDown, IconChevronUp } from '@douyinfe/semi-icons';
- import PropTypes from 'prop-types';
- import { useIsMobile } from '../../../hooks/common/useIsMobile';
- import { useMinimumLoadingTime } from '../../../hooks/common/useMinimumLoadingTime';
- /**
- * CardTable 响应式表格组件
- *
- * 在桌面端渲染 Semi-UI 的 Table 组件,在移动端则将每一行数据渲染成 Card 形式。
- * 该组件与 Table 组件的大部分 API 保持一致,只需将原 Table 换成 CardTable 即可。
- */
- const CardTable = ({
- columns = [],
- dataSource = [],
- loading = false,
- rowKey = 'key',
- hidePagination = false,
- ...tableProps
- }) => {
- const isMobile = useIsMobile();
- const { t } = useTranslation();
- const showSkeleton = useMinimumLoadingTime(loading);
- const getRowKey = (record, index) => {
- if (typeof rowKey === 'function') return rowKey(record);
- return record[rowKey] !== undefined ? record[rowKey] : index;
- };
- if (!isMobile) {
- const finalTableProps = hidePagination
- ? { ...tableProps, pagination: false }
- : tableProps;
- return (
- <Table
- columns={columns}
- dataSource={dataSource}
- loading={loading}
- rowKey={rowKey}
- {...finalTableProps}
- />
- );
- }
- if (showSkeleton) {
- const visibleCols = columns.filter((col) => {
- if (tableProps?.visibleColumns && col.key) {
- return tableProps.visibleColumns[col.key];
- }
- return true;
- });
- const renderSkeletonCard = (key) => {
- const placeholder = (
- <div className="p-2">
- {visibleCols.map((col, idx) => {
- if (!col.title) {
- return (
- <div key={idx} className="mt-2 flex justify-end">
- <Skeleton.Title active style={{ width: 100, height: 24 }} />
- </div>
- );
- }
- return (
- <div key={idx} className="flex justify-between items-center py-1 border-b last:border-b-0 border-dashed" style={{ borderColor: 'var(--semi-color-border)' }}>
- <Skeleton.Title active style={{ width: 80, height: 14 }} />
- <Skeleton.Title
- active
- style={{
- width: `${50 + (idx % 3) * 10}%`,
- maxWidth: 180,
- height: 14,
- }}
- />
- </div>
- );
- })}
- </div>
- );
- return (
- <Card key={key} className="!rounded-2xl shadow-sm">
- <Skeleton loading={true} active placeholder={placeholder}></Skeleton>
- </Card>
- );
- };
- return (
- <div className="flex flex-col gap-2">
- {[1, 2, 3].map((i) => renderSkeletonCard(i))}
- </div>
- );
- }
- const isEmpty = !showSkeleton && (!dataSource || dataSource.length === 0);
- const MobileRowCard = ({ record, index }) => {
- const [showDetails, setShowDetails] = useState(false);
- const rowKeyVal = getRowKey(record, index);
- const hasDetails =
- tableProps.expandedRowRender &&
- (!tableProps.rowExpandable || tableProps.rowExpandable(record));
- return (
- <Card key={rowKeyVal} className="!rounded-2xl shadow-sm">
- {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 (
- <div key={col.key || colIdx} className="mt-2 flex justify-end">
- {cellContent}
- </div>
- );
- }
- return (
- <div
- key={col.key || colIdx}
- className="flex justify-between items-start py-1 border-b last:border-b-0 border-dashed"
- style={{ borderColor: 'var(--semi-color-border)' }}
- >
- <span className="font-medium text-gray-600 mr-2 whitespace-nowrap select-none">
- {title}
- </span>
- <div className="flex-1 break-all flex justify-end items-center gap-1">
- {cellContent !== undefined && cellContent !== null ? cellContent : '-'}
- </div>
- </div>
- );
- })}
- {hasDetails && (
- <>
- <Button
- theme='borderless'
- size='small'
- className='w-full flex justify-center mt-2'
- icon={showDetails ? <IconChevronUp /> : <IconChevronDown />}
- onClick={(e) => {
- e.stopPropagation();
- setShowDetails(!showDetails);
- }}
- >
- {showDetails ? t('收起') : t('详情')}
- </Button>
- <Collapsible isOpen={showDetails} keepDOM>
- <div className="pt-2">
- {tableProps.expandedRowRender(record, index)}
- </div>
- </Collapsible>
- </>
- )}
- </Card>
- );
- };
- if (isEmpty) {
- if (tableProps.empty) return tableProps.empty;
- return (
- <div className="flex justify-center p-4">
- <Empty description="No Data" />
- </div>
- );
- }
- return (
- <div className="flex flex-col gap-2">
- {dataSource.map((record, index) => (
- <MobileRowCard key={getRowKey(record, index)} record={record} index={index} />
- ))}
- {!hidePagination && tableProps.pagination && dataSource.length > 0 && (
- <div className="mt-2 flex justify-center">
- <Pagination {...tableProps.pagination} />
- </div>
- )}
- </div>
- );
- };
- CardTable.propTypes = {
- columns: PropTypes.array.isRequired,
- dataSource: PropTypes.array,
- loading: PropTypes.bool,
- rowKey: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
- hidePagination: PropTypes.bool,
- };
- export default CardTable;
|