import * as React from "react" import { useVirtualizer } from "@tanstack/react-virtual" import { Badge } from "@/components/ui/badge" import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" import { typeConfig, statusConfig } from "@/components/common/general/table-filter" import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" import { TableHeader, TableRow, TableHead, TableBody, TableCell } from "@/components/ui/table" import { ErrorInline } from "@/components/layout/error" import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area" import { EmptyStateWithBorder } from "@/components/layout/empty" import { LoadingStateWithBorder } from "@/components/layout/loading" import { Lightbulb, ListRestart, Layers, LucideIcon } from "lucide-react" import { formatDateTime } from "@/lib/utils" import type { Order } from "@/lib/services" import { useUser } from "@/contexts/user-context" import { OrderDetailDialog, CreateDisputeDialog, ViewDisputeHistoryDialog, RefundReviewDialog, } from "./dispute-dialog" import {usePublicConfig} from "@/hooks/use-public-config"; const FALLBACK_TYPE_CONFIG = { label: '历史活动', color: 'bg-muted/50 text-gray-800 dark:bg-gray-900 dark:text-gray-300', } const ROW_HEIGHT = 36 /** * 虚拟化交易数据表格组件 * 使用 @tanstack/react-virtual 实现大数据量高性能渲染 */ export const TransactionDataTable = React.memo(function TransactionDataTable({ transactions }: { transactions: Order[] }) { const scrollAreaRef = React.useRef(null) const rowVirtualizer = useVirtualizer({ count: transactions.length, getScrollElement: () => { if (!scrollAreaRef.current) return null const viewport = scrollAreaRef.current.querySelector('[data-slot="scroll-area-viewport"]') return viewport as Element }, estimateSize: () => ROW_HEIGHT, overscan: 5, }) const virtualItems = rowVirtualizer.getVirtualItems() const totalSize = rowVirtualizer.getTotalSize() const paddingTop = virtualItems.length > 0 ? virtualItems[0].start : 0 const paddingBottom = virtualItems.length > 0 ? totalSize - virtualItems[virtualItems.length - 1].end : 0 return (
名称 积分 类型 状态 积分动向 应用名 编号 业务单号 创建时间 交易时间 订单过期时间 备注 操作 {paddingTop > 0 && ( )} {virtualItems.map((virtualRow) => { const order = transactions[virtualRow.index] return ( ) })} {paddingBottom > 0 && ( )}
) }) /** * 交易表格行组件 */ const TransactionTableRow = React.memo(React.forwardRef>(function TransactionTableRow({ order, style, ...props }, ref) { const { user } = useUser() const getAmountDisplay = (amount: string) => ( {parseFloat(amount).toFixed(2)} ) const { config: publicConfig } = usePublicConfig() const isDisputeSupported = order.type === 'payment' || order.type === 'online' || order.type === 'receive' const isCurrentUserPayer = user?.id === order.payer_user_id const isCurrentUserPayee = user?.id === order.payee_user_id const isDisputing = order.status === 'disputing' && isCurrentUserPayee const hasPendingTransfer = order.payee_transfer_status === 'pending' && isCurrentUserPayee const currentTypeConfig = typeConfig[order.type as keyof typeof typeConfig] ?? FALLBACK_TYPE_CONFIG const pendingTransferHint = React.useMemo(() => { const settleAt = order.payee_transfer_at ? formatDateTime(order.payee_transfer_at) : `${publicConfig?.settlement_delay_days_min ?? 7}-${publicConfig?.settlement_delay_days_max ?? 14} 天` switch (order.status) { case 'success': return `延迟结算机制:积分会在 ${settleAt} 后转入可用积分` case 'refused': return `延迟结算机制:已拒绝退款,积分会在 ${settleAt} 后转入可用积分` case 'disputing': return `延迟结算机制:争议处理中,积分会在 ${settleAt} 后转入可用积分` case 'refund': return `延迟结算机制:已退款成功,积分会在 ${settleAt} 后转入可用积分` default: return `延迟结算机制:积分会在 ${settleAt} 后转入可用积分` } }, [order.status, order.payee_transfer_at, publicConfig?.settlement_delay_days_max, publicConfig?.settlement_delay_days_min]) return ( {order.order_name} {getAmountDisplay(order.amount)} {currentTypeConfig.label}
{statusConfig[order.status].label} {hasPendingTransfer && ( {pendingTransferHint} )}
{order.status === 'pending' || order.status === 'expired' || order.type === 'community' || order.type === 'red_envelope_send' || order.type === 'red_envelope_refund' ? (
-
) : (
{order.payer_username.substring(0, 1).toUpperCase()}
⭢
{order.payee_username.substring(0, 1).toUpperCase()}

消费方

ID: {order.payer_user_id}

账户: {order.payer_username}

服务方

ID: {order.payee_user_id}

账户: {order.payee_username}

)}
{order.app_name || '-'} {order.order_no} {order.merchant_order_no || '-'} {formatDateTime(order.created_at)} {(order.status === 'success' || order.status === 'refund') ? formatDateTime(order.trade_time) : '-'} {formatDateTime(order.expires_at)} {order.remark || '-'} {/* 场景1:消费方对成功的订单发起争议 */} {isDisputeSupported && isCurrentUserPayer && order.status === 'success' && ( )} {/* 场景2:消费方查看正在进行的争议 */} {isDisputeSupported && isCurrentUserPayer && order.status === 'disputing' && ( )} {/* 场景3:消费方查看已处理的争议(拒绝或退款) */} {isDisputeSupported && isCurrentUserPayer && (order.status === 'refused' || order.status === 'refund') && ( )} {/* 场景4:服务方处理正在进行的争议 */} {isDisputeSupported && isCurrentUserPayee && order.status === 'disputing' && ( )} {/* 场景5:服务方查看已处理的争议(拒绝或退款) */} {isDisputeSupported && isCurrentUserPayee && (order.status === 'refused' || order.status === 'refund') && ( )}
) })) interface TransactionTableListProps { loading: boolean error: Error | null transactions: Order[] onRetry: () => void emptyIcon?: LucideIcon emptyDescription?: string } /** * 交易列表容器组件 * 统一处理加载、错误、空状态 */ export const TransactionTableList = React.memo(function TransactionTableList({ loading, error, transactions, onRetry, emptyIcon = Layers, emptyDescription = "未发现积分活动" }: TransactionTableListProps) { if (loading && transactions.length === 0) { return ( ) } if (error) { return (
) } if (!transactions || transactions.length === 0) { return ( ) } return ( ) }) export { RefundReviewDialog, CancelDisputeDialog } from "./dispute-dialog"