| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
|
|
| 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';
|
|
|
| |
| |
| |
| |
| |
|
|
| 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;
|
|
|