'use client'; import { useEffect, useMemo, useState, useCallback } from 'react'; import QRCode from 'qrcode'; interface PaymentQRCodeProps { orderId: string; payUrl?: string | null; qrCode?: string | null; paymentType?: 'alipay' | 'wxpay'; amount: number; expiresAt: string; onStatusChange: (status: string) => void; onBack: () => void; dark?: boolean; } const TEXT_EXPIRED = '\u8BA2\u5355\u5DF2\u8D85\u65F6'; const TEXT_REMAINING = '\u5269\u4F59\u652F\u4ED8\u65F6\u95F4'; const TEXT_GO_PAY = '\u70B9\u51FB\u524D\u5F80\u652F\u4ED8'; const TEXT_SCAN_PAY = '\u8BF7\u4F7F\u7528\u652F\u4ED8\u5E94\u7528\u626B\u7801\u652F\u4ED8'; const TEXT_BACK = '\u8FD4\u56DE'; const TEXT_CANCEL_ORDER = '\u53D6\u6D88\u8BA2\u5355'; const TERMINAL_STATUSES = new Set(['COMPLETED', 'FAILED', 'CANCELLED', 'EXPIRED', 'REFUNDED', 'REFUND_FAILED']); export default function PaymentQRCode({ orderId, payUrl, qrCode, paymentType, amount, expiresAt, onStatusChange, onBack, dark = false, }: PaymentQRCodeProps) { const [timeLeft, setTimeLeft] = useState(''); const [expired, setExpired] = useState(false); const [qrDataUrl, setQrDataUrl] = useState(''); const [imageLoading, setImageLoading] = useState(false); const qrPayload = useMemo(() => { const value = (qrCode || payUrl || '').trim(); return value; }, [qrCode, payUrl]); useEffect(() => { let cancelled = false; if (!qrPayload) { setQrDataUrl(''); return; } setImageLoading(true); QRCode.toDataURL(qrPayload, { width: 224, margin: 1, errorCorrectionLevel: 'M', }) .then((url) => { if (!cancelled) { setQrDataUrl(url); } }) .catch(() => { if (!cancelled) { setQrDataUrl(''); } }) .finally(() => { if (!cancelled) { setImageLoading(false); } }); return () => { cancelled = true; }; }, [qrPayload]); useEffect(() => { const updateTimer = () => { const now = Date.now(); const expiry = new Date(expiresAt).getTime(); const diff = expiry - now; if (diff <= 0) { setTimeLeft(TEXT_EXPIRED); setExpired(true); return; } const minutes = Math.floor(diff / 60000); const seconds = Math.floor((diff % 60000) / 1000); setTimeLeft(`${minutes}:${seconds.toString().padStart(2, '0')}`); }; updateTimer(); const timer = setInterval(updateTimer, 1000); return () => clearInterval(timer); }, [expiresAt]); const pollStatus = useCallback(async () => { try { const res = await fetch(`/api/orders/${orderId}`); if (res.ok) { const data = await res.json(); if (TERMINAL_STATUSES.has(data.status)) { onStatusChange(data.status); } } } catch { // ignore polling errors } }, [orderId, onStatusChange]); useEffect(() => { if (expired) return; pollStatus(); const timer = setInterval(pollStatus, 2000); return () => clearInterval(timer); }, [pollStatus, expired]); const handleCancel = async () => { try { const res = await fetch(`/api/orders/${orderId}`); if (res.ok) { const data = await res.json(); await fetch(`/api/orders/${orderId}/cancel`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ user_id: data.user_id }), }); onStatusChange('CANCELLED'); } } catch { // ignore } }; const isWx = paymentType === 'wxpay'; const iconSrc = isWx ? '/icons/wxpay.svg' : '/icons/alipay.svg'; const channelLabel = isWx ? '\u5FAE\u4FE1' : '\u652F\u4ED8\u5B9D'; const iconBgClass = isWx ? 'bg-[#07C160]' : 'bg-[#1677FF]'; return (
{TEXT_SCAN_PAY}
{`\u8BF7\u6253\u5F00${channelLabel}\u626B\u4E00\u626B\u5B8C\u6210\u652F\u4ED8`}
> )}