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