2026-03-01 03:04:24 +08:00
|
|
|
'use client';
|
|
|
|
|
|
|
|
|
|
import { useSearchParams } from 'next/navigation';
|
|
|
|
|
import { useEffect, useState, Suspense } from 'react';
|
2026-03-09 18:33:57 +08:00
|
|
|
import { applyLocaleToSearchParams, pickLocaleText, resolveLocale } from '@/lib/locale';
|
2026-03-01 03:04:24 +08:00
|
|
|
|
|
|
|
|
function ResultContent() {
|
|
|
|
|
const searchParams = useSearchParams();
|
2026-03-01 17:58:08 +08:00
|
|
|
const outTradeNo = searchParams.get('out_trade_no') || searchParams.get('order_id');
|
2026-03-04 10:58:07 +08:00
|
|
|
const isPopup = searchParams.get('popup') === '1';
|
2026-03-07 04:16:01 +08:00
|
|
|
const theme = searchParams.get('theme') === 'dark' ? 'dark' : 'light';
|
2026-03-09 18:33:57 +08:00
|
|
|
const locale = resolveLocale(searchParams.get('lang'));
|
2026-03-07 04:16:01 +08:00
|
|
|
const isDark = theme === 'dark';
|
2026-03-01 03:04:24 +08:00
|
|
|
|
2026-03-09 18:33:57 +08:00
|
|
|
const text = {
|
|
|
|
|
checking: pickLocaleText(locale, '查询支付结果中...', 'Checking payment result...'),
|
|
|
|
|
success: pickLocaleText(locale, '充值成功', 'Top-up successful'),
|
|
|
|
|
processing: pickLocaleText(locale, '充值处理中', 'Top-up processing'),
|
|
|
|
|
successMessage: pickLocaleText(locale, '余额已成功到账!', 'Balance has been credited successfully!'),
|
|
|
|
|
processingMessage: pickLocaleText(locale, '支付成功,余额正在充值中...', 'Payment succeeded, balance is being credited...'),
|
|
|
|
|
returning: pickLocaleText(locale, '正在返回...', 'Returning...'),
|
|
|
|
|
returnNow: pickLocaleText(locale, '立即返回', 'Return now'),
|
|
|
|
|
pending: pickLocaleText(locale, '等待支付', 'Awaiting payment'),
|
|
|
|
|
pendingMessage: pickLocaleText(locale, '订单尚未完成支付', 'The order has not been paid yet'),
|
|
|
|
|
expired: pickLocaleText(locale, '订单已超时', 'Order expired'),
|
|
|
|
|
cancelled: pickLocaleText(locale, '订单已取消', 'Order cancelled'),
|
|
|
|
|
abnormal: pickLocaleText(locale, '支付异常', 'Payment error'),
|
|
|
|
|
expiredMessage: pickLocaleText(locale, '订单已超时,请重新充值', 'This order has expired. Please create a new one.'),
|
|
|
|
|
cancelledMessage: pickLocaleText(locale, '订单已被取消', 'This order has been cancelled.'),
|
|
|
|
|
abnormalMessage: pickLocaleText(locale, '请联系管理员处理', 'Please contact the administrator.'),
|
|
|
|
|
back: pickLocaleText(locale, '返回', 'Back'),
|
|
|
|
|
orderId: pickLocaleText(locale, '订单号', 'Order ID'),
|
|
|
|
|
unknown: pickLocaleText(locale, '未知', 'Unknown'),
|
|
|
|
|
loading: pickLocaleText(locale, '加载中...', 'Loading...'),
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-01 03:04:24 +08:00
|
|
|
const [status, setStatus] = useState<string | null>(null);
|
|
|
|
|
const [loading, setLoading] = useState(true);
|
2026-03-04 10:58:07 +08:00
|
|
|
const [isInPopup, setIsInPopup] = useState(false);
|
2026-03-07 16:55:49 +08:00
|
|
|
const [countdown, setCountdown] = useState(5);
|
2026-03-04 10:58:07 +08:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (isPopup || window.opener) {
|
|
|
|
|
setIsInPopup(true);
|
|
|
|
|
}
|
|
|
|
|
}, [isPopup]);
|
2026-03-01 03:04:24 +08:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!outTradeNo) {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const checkOrder = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch(`/api/orders/${outTradeNo}`);
|
|
|
|
|
if (res.ok) {
|
|
|
|
|
const data = await res.json();
|
|
|
|
|
setStatus(data.status);
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
checkOrder();
|
|
|
|
|
const timer = setInterval(checkOrder, 3000);
|
|
|
|
|
const timeout = setTimeout(() => clearInterval(timer), 30000);
|
|
|
|
|
return () => {
|
|
|
|
|
clearInterval(timer);
|
|
|
|
|
clearTimeout(timeout);
|
|
|
|
|
};
|
|
|
|
|
}, [outTradeNo]);
|
|
|
|
|
|
2026-03-04 10:58:07 +08:00
|
|
|
const isSuccess = status === 'COMPLETED' || status === 'PAID' || status === 'RECHARGING';
|
|
|
|
|
|
2026-03-07 16:55:49 +08:00
|
|
|
const goBack = () => {
|
|
|
|
|
if (isInPopup) {
|
|
|
|
|
window.close();
|
2026-03-09 18:33:57 +08:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (window.history.length > 1) {
|
2026-03-07 16:55:49 +08:00
|
|
|
window.history.back();
|
2026-03-09 18:33:57 +08:00
|
|
|
return;
|
2026-03-07 16:55:49 +08:00
|
|
|
}
|
2026-03-09 18:33:57 +08:00
|
|
|
|
|
|
|
|
const params = new URLSearchParams();
|
|
|
|
|
params.set('theme', theme);
|
|
|
|
|
applyLocaleToSearchParams(params, locale);
|
|
|
|
|
window.location.replace(`/pay?${params.toString()}`);
|
2026-03-07 16:55:49 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!isSuccess) return;
|
|
|
|
|
setCountdown(5);
|
|
|
|
|
const timer = setInterval(() => {
|
|
|
|
|
setCountdown((prev) => {
|
|
|
|
|
if (prev <= 1) {
|
|
|
|
|
clearInterval(timer);
|
|
|
|
|
goBack();
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
return prev - 1;
|
|
|
|
|
});
|
|
|
|
|
}, 1000);
|
|
|
|
|
return () => clearInterval(timer);
|
|
|
|
|
}, [isSuccess, isInPopup]);
|
2026-03-04 10:58:07 +08:00
|
|
|
|
2026-03-01 03:04:24 +08:00
|
|
|
if (loading) {
|
|
|
|
|
return (
|
2026-03-07 04:16:01 +08:00
|
|
|
<div className={`flex min-h-screen items-center justify-center ${isDark ? 'bg-slate-950' : 'bg-slate-50'}`}>
|
2026-03-09 18:33:57 +08:00
|
|
|
<div className={isDark ? 'text-slate-400' : 'text-gray-500'}>{text.checking}</div>
|
2026-03-01 03:04:24 +08:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const isPending = status === 'PENDING';
|
2026-03-09 18:33:57 +08:00
|
|
|
const countdownText = countdown > 0 ? pickLocaleText(locale, `${countdown} 秒后自动返回`, `${countdown} seconds before returning`) : text.returning;
|
2026-03-01 03:04:24 +08:00
|
|
|
|
|
|
|
|
return (
|
2026-03-07 04:16:01 +08:00
|
|
|
<div className={`flex min-h-screen items-center justify-center p-4 ${isDark ? 'bg-slate-950' : 'bg-slate-50'}`}>
|
|
|
|
|
<div
|
|
|
|
|
className={[
|
|
|
|
|
'w-full max-w-md rounded-xl p-8 text-center shadow-lg',
|
|
|
|
|
isDark ? 'bg-slate-900 text-slate-100' : 'bg-white',
|
|
|
|
|
].join(' ')}
|
|
|
|
|
>
|
2026-03-01 03:04:24 +08:00
|
|
|
{isSuccess ? (
|
|
|
|
|
<>
|
|
|
|
|
<div className="text-6xl text-green-500">✓</div>
|
2026-03-09 18:33:57 +08:00
|
|
|
<h1 className="mt-4 text-xl font-bold text-green-600">{status === 'COMPLETED' ? text.success : text.processing}</h1>
|
2026-03-07 04:16:01 +08:00
|
|
|
<p className={isDark ? 'mt-2 text-slate-400' : 'mt-2 text-gray-500'}>
|
2026-03-09 18:33:57 +08:00
|
|
|
{status === 'COMPLETED' ? text.successMessage : text.processingMessage}
|
2026-03-01 03:04:24 +08:00
|
|
|
</p>
|
2026-03-07 16:55:49 +08:00
|
|
|
<div className="mt-4 space-y-2">
|
2026-03-09 18:33:57 +08:00
|
|
|
<p className={isDark ? 'text-sm text-slate-500' : 'text-sm text-gray-400'}>{countdownText}</p>
|
2026-03-07 16:55:49 +08:00
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={goBack}
|
|
|
|
|
className="text-sm text-blue-600 underline hover:text-blue-700"
|
|
|
|
|
>
|
2026-03-09 18:33:57 +08:00
|
|
|
{text.returnNow}
|
2026-03-07 16:55:49 +08:00
|
|
|
</button>
|
|
|
|
|
</div>
|
2026-03-01 03:04:24 +08:00
|
|
|
</>
|
|
|
|
|
) : isPending ? (
|
|
|
|
|
<>
|
|
|
|
|
<div className="text-6xl text-yellow-500">⏳</div>
|
2026-03-09 18:33:57 +08:00
|
|
|
<h1 className="mt-4 text-xl font-bold text-yellow-600">{text.pending}</h1>
|
|
|
|
|
<p className={isDark ? 'mt-2 text-slate-400' : 'mt-2 text-gray-500'}>{text.pendingMessage}</p>
|
2026-03-07 16:55:49 +08:00
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={goBack}
|
|
|
|
|
className="mt-4 text-sm text-blue-600 underline hover:text-blue-700"
|
|
|
|
|
>
|
2026-03-09 18:33:57 +08:00
|
|
|
{text.back}
|
2026-03-07 16:55:49 +08:00
|
|
|
</button>
|
2026-03-01 03:04:24 +08:00
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
<>
|
|
|
|
|
<div className="text-6xl text-red-500">✗</div>
|
|
|
|
|
<h1 className="mt-4 text-xl font-bold text-red-600">
|
2026-03-09 18:33:57 +08:00
|
|
|
{status === 'EXPIRED' ? text.expired : status === 'CANCELLED' ? text.cancelled : text.abnormal}
|
2026-03-01 03:04:24 +08:00
|
|
|
</h1>
|
2026-03-07 04:16:01 +08:00
|
|
|
<p className={isDark ? 'mt-2 text-slate-400' : 'mt-2 text-gray-500'}>
|
2026-03-01 03:04:24 +08:00
|
|
|
{status === 'EXPIRED'
|
2026-03-09 18:33:57 +08:00
|
|
|
? text.expiredMessage
|
2026-03-01 03:04:24 +08:00
|
|
|
: status === 'CANCELLED'
|
2026-03-09 18:33:57 +08:00
|
|
|
? text.cancelledMessage
|
|
|
|
|
: text.abnormalMessage}
|
2026-03-01 03:04:24 +08:00
|
|
|
</p>
|
2026-03-07 16:55:49 +08:00
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={goBack}
|
|
|
|
|
className="mt-4 text-sm text-blue-600 underline hover:text-blue-700"
|
|
|
|
|
>
|
2026-03-09 18:33:57 +08:00
|
|
|
{text.back}
|
2026-03-07 16:55:49 +08:00
|
|
|
</button>
|
2026-03-01 03:04:24 +08:00
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
|
2026-03-07 04:16:01 +08:00
|
|
|
<p className={isDark ? 'mt-4 text-xs text-slate-500' : 'mt-4 text-xs text-gray-400'}>
|
2026-03-09 18:33:57 +08:00
|
|
|
{text.orderId}: {outTradeNo || text.unknown}
|
2026-03-07 04:16:01 +08:00
|
|
|
</p>
|
2026-03-01 03:04:24 +08:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-09 18:33:57 +08:00
|
|
|
function ResultPageFallback() {
|
|
|
|
|
const searchParams = useSearchParams();
|
|
|
|
|
const locale = resolveLocale(searchParams.get('lang'));
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex min-h-screen items-center justify-center bg-slate-50">
|
|
|
|
|
<div className="text-gray-500">{pickLocaleText(locale, '加载中...', 'Loading...')}</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-01 03:04:24 +08:00
|
|
|
export default function PayResultPage() {
|
|
|
|
|
return (
|
2026-03-09 18:33:57 +08:00
|
|
|
<Suspense fallback={<ResultPageFallback />}>
|
2026-03-01 03:04:24 +08:00
|
|
|
<ResultContent />
|
|
|
|
|
</Suspense>
|
|
|
|
|
);
|
|
|
|
|
}
|