import { ChevronRight, ReceiptText } from 'lucide-react'; import { type FocusEvent, type MouseEvent, useCallback, useEffect, useRef, useState, } from 'react'; import type { ProfileMudPointBalance } from '../contracts/runtime'; import MUD_POINT_ICON_SRC from '../icons/topbar-wallet.png'; import { formatMudPointCount } from '../utils/format'; export type PlatformMudPointWalletEntryProps = { balance: number | null; breakdown?: ProfileMudPointBalance | null; isLoading?: boolean; error?: string | null; variant?: 'desktop' | 'mobile' | 'editor'; className?: string; onRequestDetails: () => void; onRecharge: () => void; onOpenLedger: () => void; }; function MudPointBalanceRow({ label, points, detail, }: { label: string; points: number; detail?: string | null; }) { return (
{label}
{detail ? (
{detail}
) : null}
{formatMudPointCount(points)}
); } export function PlatformMudPointWalletEntry({ balance, breakdown, isLoading = false, variant = 'desktop', className, onRequestDetails, onRecharge, onOpenLedger, }: PlatformMudPointWalletEntryProps) { const rootRef = useRef(null); const isOpenRef = useRef(false); const requestedDetailsKeyForOpenRef = useRef(null); const closeTimerRef = useRef(null); const [isOpen, setIsOpen] = useState(false); const isCompact = variant === 'mobile'; const displayedBalance = balance ?? breakdown?.totalPoints ?? null; const balanceLabel = displayedBalance === null ? '--' : formatMudPointCount(displayedBalance, true); const exactBalanceLabel = displayedBalance === null ? '--' : formatMudPointCount(displayedBalance); const detailsRequestKey = `balance:${balance ?? breakdown?.totalPoints ?? 'unknown'}`; const cancelPendingClose = useCallback(() => { if (closeTimerRef.current !== null) { window.clearTimeout(closeTimerRef.current); closeTimerRef.current = null; } }, []); const closeDetails = useCallback(() => { cancelPendingClose(); isOpenRef.current = false; requestedDetailsKeyForOpenRef.current = null; setIsOpen(false); }, [cancelPendingClose]); const requestDetailsIfNeeded = useCallback(() => { if ( isLoading || requestedDetailsKeyForOpenRef.current === detailsRequestKey ) { return; } requestedDetailsKeyForOpenRef.current = detailsRequestKey; onRequestDetails(); }, [detailsRequestKey, isLoading, onRequestDetails]); const requestAndOpen = useCallback(() => { cancelPendingClose(); if (isOpenRef.current) { return; } isOpenRef.current = true; setIsOpen(true); requestDetailsIfNeeded(); }, [cancelPendingClose, requestDetailsIfNeeded]); useEffect(() => cancelPendingClose, [cancelPendingClose]); useEffect(() => { if (isOpen) { requestDetailsIfNeeded(); } }, [isOpen, requestDetailsIfNeeded]); useEffect(() => { if (!isOpen) { return; } const handlePointerDown = (event: PointerEvent) => { if (!rootRef.current?.contains(event.target as Node)) { closeDetails(); } }; const handleKeyDown = (event: KeyboardEvent) => { if (event.key === 'Escape') { closeDetails(); } }; document.addEventListener('pointerdown', handlePointerDown); document.addEventListener('keydown', handleKeyDown); return () => { document.removeEventListener('pointerdown', handlePointerDown); document.removeEventListener('keydown', handleKeyDown); }; }, [closeDetails, isOpen]); const closeAfterFocusLeaves = (event: FocusEvent) => { const nextTarget = event.relatedTarget; if ( !(nextTarget instanceof Node) || !event.currentTarget.contains(nextTarget) ) { closeDetails(); } }; const closeAfterPointerLeaves = (event: MouseEvent) => { const nextTarget = event.relatedTarget; if ( nextTarget instanceof Node && event.currentTarget.contains(nextTarget) ) { return; } cancelPendingClose(); closeTimerRef.current = window.setTimeout(() => { closeTimerRef.current = null; closeDetails(); }, 120); }; return (
{isOpen ? (
泥点 {exactBalanceLabel}
{breakdown ? ( <> ) : (
{isLoading ? '余额读取中' : '余额明细暂不可用'}
)}
) : null}
); }