feat: 全站多语言支持 (i18n),lang=en 显示英文,其余默认中文
新增 src/lib/locale.ts 作为统一多语言入口,覆盖前台支付链路、 管理后台、API/服务层错误文案,共 35 个文件。URL 参数 lang 全链路透传, 包括 Stripe return_url、页面跳转、layout html lang 属性等。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@ import OrderFilterBar from '@/components/OrderFilterBar';
|
||||
import OrderSummaryCards from '@/components/OrderSummaryCards';
|
||||
import OrderTable from '@/components/OrderTable';
|
||||
import PaginationBar from '@/components/PaginationBar';
|
||||
import { applyLocaleToSearchParams, pickLocaleText, resolveLocale } from '@/lib/locale';
|
||||
import { detectDeviceIsMobile, type UserInfo, type MyOrder, type OrderStatusFilter } from '@/lib/pay-utils';
|
||||
|
||||
const PAGE_SIZE_OPTIONS = [20, 50, 100];
|
||||
@@ -24,8 +25,24 @@ function OrdersContent() {
|
||||
const theme = searchParams.get('theme') === 'dark' ? 'dark' : 'light';
|
||||
const uiMode = searchParams.get('ui_mode') || 'standalone';
|
||||
const srcHost = searchParams.get('src_host') || '';
|
||||
const locale = resolveLocale(searchParams.get('lang'));
|
||||
const isDark = theme === 'dark';
|
||||
|
||||
const text = {
|
||||
missingAuth: pickLocaleText(locale, '缺少认证信息', 'Missing authentication information'),
|
||||
visitOrders: pickLocaleText(locale, '请从 Sub2API 平台正确访问订单页面', 'Please open the orders page from Sub2API'),
|
||||
sessionExpired: pickLocaleText(locale, '登录态已失效,请从 Sub2API 重新进入支付页。', 'Session expired. Please re-enter from Sub2API.'),
|
||||
loadFailed: pickLocaleText(locale, '订单加载失败,请稍后重试。', 'Failed to load orders. Please try again later.'),
|
||||
networkError: pickLocaleText(locale, '网络错误,请稍后重试。', 'Network error. Please try again later.'),
|
||||
switchingMobileTab: pickLocaleText(locale, '正在切换到移动端订单 Tab...', 'Switching to mobile orders tab...'),
|
||||
myOrders: pickLocaleText(locale, '我的订单', 'My Orders'),
|
||||
refresh: pickLocaleText(locale, '刷新', 'Refresh'),
|
||||
backToPay: pickLocaleText(locale, '返回充值', 'Back to Top Up'),
|
||||
loading: pickLocaleText(locale, '加载中...', 'Loading...'),
|
||||
userPrefix: pickLocaleText(locale, '用户', 'User'),
|
||||
authError: pickLocaleText(locale, '缺少认证信息,请从 Sub2API 平台正确访问订单页面', 'Missing authentication information. Please open the orders page from Sub2API.'),
|
||||
};
|
||||
|
||||
const [isIframeContext, setIsIframeContext] = useState(true);
|
||||
const [isMobile, setIsMobile] = useState(false);
|
||||
const [userInfo, setUserInfo] = useState<UserInfo | null>(null);
|
||||
@@ -56,9 +73,9 @@ function OrdersContent() {
|
||||
params.set('theme', theme);
|
||||
params.set('ui_mode', uiMode);
|
||||
params.set('tab', 'orders');
|
||||
applyLocaleToSearchParams(params, locale);
|
||||
window.location.replace(`/pay?${params.toString()}`);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isMobile, isEmbedded]);
|
||||
}, [isMobile, isEmbedded, token, theme, uiMode, locale]);
|
||||
|
||||
const loadOrders = async (targetPage = page, targetPageSize = pageSize) => {
|
||||
setLoading(true);
|
||||
@@ -66,7 +83,7 @@ function OrdersContent() {
|
||||
try {
|
||||
if (!hasToken) {
|
||||
setOrders([]);
|
||||
setError('缺少认证信息,请从 Sub2API 平台正确访问订单页面。');
|
||||
setError(text.authError);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -77,7 +94,7 @@ function OrdersContent() {
|
||||
});
|
||||
const res = await fetch(`/api/orders/my?${params}`);
|
||||
if (!res.ok) {
|
||||
setError(res.status === 401 ? '登录态已失效,请从 Sub2API 重新进入支付页。' : '订单加载失败,请稍后重试。');
|
||||
setError(res.status === 401 ? text.sessionExpired : text.loadFailed);
|
||||
setOrders([]);
|
||||
return;
|
||||
}
|
||||
@@ -92,7 +109,7 @@ function OrdersContent() {
|
||||
username:
|
||||
(typeof meUser.displayName === 'string' && meUser.displayName.trim()) ||
|
||||
(typeof meUser.username === 'string' && meUser.username.trim()) ||
|
||||
`用户 #${meId}`,
|
||||
`${text.userPrefix} #${meId}`,
|
||||
balance: typeof meUser.balance === 'number' ? meUser.balance : 0,
|
||||
});
|
||||
|
||||
@@ -102,7 +119,7 @@ function OrdersContent() {
|
||||
setTotalPages(data.total_pages ?? 1);
|
||||
} catch {
|
||||
setOrders([]);
|
||||
setError('网络错误,请稍后重试。');
|
||||
setError(text.networkError);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -111,7 +128,6 @@ function OrdersContent() {
|
||||
useEffect(() => {
|
||||
if (isMobile && !isEmbedded) return;
|
||||
loadOrders(1, pageSize);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [token, isMobile, isEmbedded]);
|
||||
|
||||
const handlePageChange = (newPage: number) => {
|
||||
@@ -139,7 +155,7 @@ function OrdersContent() {
|
||||
<div
|
||||
className={`flex min-h-screen items-center justify-center p-4 ${isDark ? 'bg-slate-950 text-slate-100' : 'bg-slate-50 text-slate-900'}`}
|
||||
>
|
||||
正在切换到移动端订单 Tab...
|
||||
{text.switchingMobileTab}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -148,8 +164,8 @@ function OrdersContent() {
|
||||
return (
|
||||
<div className={`flex min-h-screen items-center justify-center p-4 ${isDark ? 'bg-slate-950' : 'bg-slate-50'}`}>
|
||||
<div className="text-center text-red-500">
|
||||
<p className="text-lg font-medium">缺少认证信息</p>
|
||||
<p className="mt-2 text-sm text-gray-500">请从 Sub2API 平台正确访问订单页面</p>
|
||||
<p className="text-lg font-medium">{text.missingAuth}</p>
|
||||
<p className="mt-2 text-sm text-gray-500">{text.visitOrders}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -160,6 +176,7 @@ function OrdersContent() {
|
||||
if (token) params.set('token', token);
|
||||
params.set('theme', theme);
|
||||
params.set('ui_mode', uiMode);
|
||||
applyLocaleToSearchParams(params, locale);
|
||||
return `${path}?${params.toString()}`;
|
||||
};
|
||||
|
||||
@@ -167,28 +184,28 @@ function OrdersContent() {
|
||||
<PayPageLayout
|
||||
isDark={isDark}
|
||||
isEmbedded={isEmbedded}
|
||||
title="我的订单"
|
||||
subtitle={userInfo?.username || '我的订单'}
|
||||
title={text.myOrders}
|
||||
subtitle={userInfo?.username || text.myOrders}
|
||||
actions={
|
||||
<>
|
||||
<button type="button" onClick={() => loadOrders(page, pageSize)} className={btnClass}>
|
||||
刷新
|
||||
{text.refresh}
|
||||
</button>
|
||||
{!srcHost && (
|
||||
<a href={buildScopedUrl('/pay')} className={btnClass}>
|
||||
返回充值
|
||||
{text.backToPay}
|
||||
</a>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
>
|
||||
<OrderSummaryCards isDark={isDark} summary={summary} />
|
||||
<OrderSummaryCards isDark={isDark} locale={locale} summary={summary} />
|
||||
|
||||
<div className="mb-4 flex flex-wrap items-center justify-between gap-2">
|
||||
<OrderFilterBar isDark={isDark} activeFilter={activeFilter} onChange={setActiveFilter} />
|
||||
<OrderFilterBar isDark={isDark} locale={locale} activeFilter={activeFilter} onChange={setActiveFilter} />
|
||||
</div>
|
||||
|
||||
<OrderTable isDark={isDark} loading={loading} error={error} orders={filteredOrders} />
|
||||
<OrderTable isDark={isDark} locale={locale} loading={loading} error={error} orders={filteredOrders} />
|
||||
|
||||
<PaginationBar
|
||||
page={page}
|
||||
@@ -196,6 +213,7 @@ function OrdersContent() {
|
||||
total={summary.total}
|
||||
pageSize={pageSize}
|
||||
pageSizeOptions={PAGE_SIZE_OPTIONS}
|
||||
locale={locale}
|
||||
isDark={isDark}
|
||||
loading={loading}
|
||||
onPageChange={handlePageChange}
|
||||
@@ -205,15 +223,20 @@ function OrdersContent() {
|
||||
);
|
||||
}
|
||||
|
||||
function OrdersPageFallback() {
|
||||
const searchParams = useSearchParams();
|
||||
const locale = resolveLocale(searchParams.get('lang'));
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center">
|
||||
<div className="text-gray-500">{pickLocaleText(locale, '加载中...', 'Loading...')}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function OrdersPage() {
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="flex min-h-screen items-center justify-center">
|
||||
<div className="text-gray-500">加载中...</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Suspense fallback={<OrdersPageFallback />}>
|
||||
<OrdersContent />
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
@@ -7,6 +7,7 @@ import PaymentQRCode from '@/components/PaymentQRCode';
|
||||
import OrderStatus from '@/components/OrderStatus';
|
||||
import PayPageLayout from '@/components/PayPageLayout';
|
||||
import MobileOrderList from '@/components/MobileOrderList';
|
||||
import { resolveLocale, pickLocaleText, applyLocaleToSearchParams } from '@/lib/locale';
|
||||
import { detectDeviceIsMobile, applySublabelOverrides, type UserInfo, type MyOrder } from '@/lib/pay-utils';
|
||||
import type { MethodLimitInfo } from '@/components/PaymentForm';
|
||||
|
||||
@@ -41,6 +42,7 @@ function PayContent() {
|
||||
const tab = searchParams.get('tab');
|
||||
const srcHost = searchParams.get('src_host') || undefined;
|
||||
const srcUrl = searchParams.get('src_url') || undefined;
|
||||
const locale = resolveLocale(searchParams.get('lang'));
|
||||
const isDark = theme === 'dark';
|
||||
|
||||
const [isIframeContext, setIsIframeContext] = useState(true);
|
||||
@@ -97,7 +99,6 @@ function PayContent() {
|
||||
|
||||
setUserNotFound(false);
|
||||
try {
|
||||
// 通过 token 获取用户详情和订单
|
||||
const meRes = await fetch(`/api/orders/my?token=${encodeURIComponent(token)}`);
|
||||
if (!meRes.ok) {
|
||||
setUserNotFound(true);
|
||||
@@ -120,7 +121,7 @@ function PayContent() {
|
||||
username:
|
||||
(typeof meUser.displayName === 'string' && meUser.displayName.trim()) ||
|
||||
(typeof meUser.username === 'string' && meUser.username.trim()) ||
|
||||
`用户 #${meId}`,
|
||||
pickLocaleText(locale, `用户 #${meId}`, `User #${meId}`),
|
||||
balance: typeof meUser.balance === 'number' ? meUser.balance : undefined,
|
||||
});
|
||||
|
||||
@@ -134,7 +135,6 @@ function PayContent() {
|
||||
setOrdersHasMore(false);
|
||||
}
|
||||
|
||||
// 获取服务端支付配置
|
||||
const cfgRes = await fetch(`/api/user?user_id=${meId}&token=${encodeURIComponent(token)}`);
|
||||
if (cfgRes.ok) {
|
||||
const cfgData = await cfgRes.json();
|
||||
@@ -155,7 +155,6 @@ function PayContent() {
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore and keep page usable
|
||||
}
|
||||
};
|
||||
|
||||
@@ -175,7 +174,6 @@ function PayContent() {
|
||||
setOrdersHasMore(false);
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
} finally {
|
||||
setOrdersLoadingMore(false);
|
||||
}
|
||||
@@ -183,12 +181,10 @@ function PayContent() {
|
||||
|
||||
useEffect(() => {
|
||||
loadUserAndOrders();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [token]);
|
||||
}, [token, locale]);
|
||||
|
||||
useEffect(() => {
|
||||
if (step !== 'result' || finalStatus !== 'COMPLETED') return;
|
||||
// 立即在后台刷新余额,2.2s 显示结果页后再切回表单(届时余额已更新)
|
||||
loadUserAndOrders();
|
||||
const timer = setTimeout(() => {
|
||||
setStep('form');
|
||||
@@ -197,15 +193,16 @@ function PayContent() {
|
||||
setError('');
|
||||
}, 2200);
|
||||
return () => clearTimeout(timer);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [step, finalStatus]);
|
||||
|
||||
if (!hasToken) {
|
||||
return (
|
||||
<div className={`flex min-h-screen items-center justify-center p-4 ${isDark ? 'bg-slate-950' : 'bg-slate-50'}`}>
|
||||
<div className="text-center text-red-500">
|
||||
<p className="text-lg font-medium">缺少认证信息</p>
|
||||
<p className="mt-2 text-sm text-gray-500">请从 Sub2API 平台正确访问充值页面</p>
|
||||
<p className="text-lg font-medium">{pickLocaleText(locale, '缺少认证信息', 'Missing authentication info')}</p>
|
||||
<p className="mt-2 text-sm text-gray-500">
|
||||
{pickLocaleText(locale, '请从 Sub2API 平台正确访问充值页面', 'Please open the recharge page from the Sub2API platform')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -215,8 +212,10 @@ function PayContent() {
|
||||
return (
|
||||
<div className={`flex min-h-screen items-center justify-center p-4 ${isDark ? 'bg-slate-950' : 'bg-slate-50'}`}>
|
||||
<div className="text-center text-red-500">
|
||||
<p className="text-lg font-medium">用户不存在</p>
|
||||
<p className="mt-2 text-sm text-gray-500">请检查链接是否正确,或联系管理员</p>
|
||||
<p className="text-lg font-medium">{pickLocaleText(locale, '用户不存在', 'User not found')}</p>
|
||||
<p className="mt-2 text-sm text-gray-500">
|
||||
{pickLocaleText(locale, '请检查链接是否正确,或联系管理员', 'Please check whether the link is correct or contact the administrator')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -228,6 +227,9 @@ function PayContent() {
|
||||
params.set('theme', theme);
|
||||
params.set('ui_mode', uiMode);
|
||||
if (forceOrdersTab) params.set('tab', 'orders');
|
||||
if (srcHost) params.set('src_host', srcHost);
|
||||
if (srcUrl) params.set('src_url', srcUrl);
|
||||
applyLocaleToSearchParams(params, locale);
|
||||
return `${path}?${params.toString()}`;
|
||||
};
|
||||
|
||||
@@ -237,7 +239,13 @@ function PayContent() {
|
||||
|
||||
const handleSubmit = async (amount: number, paymentType: string) => {
|
||||
if (pendingBlocked) {
|
||||
setError(`您有 ${pendingCount} 个待支付订单,请先完成或取消后再试(最多 ${MAX_PENDING} 个)`);
|
||||
setError(
|
||||
pickLocaleText(
|
||||
locale,
|
||||
`您有 ${pendingCount} 个待支付订单,请先完成或取消后再试(最多 ${MAX_PENDING} 个)`,
|
||||
`You have ${pendingCount} pending orders. Please complete or cancel them first (maximum ${MAX_PENDING}).`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -262,15 +270,15 @@ function PayContent() {
|
||||
|
||||
if (!res.ok) {
|
||||
const codeMessages: Record<string, string> = {
|
||||
INVALID_TOKEN: '认证已失效,请重新从平台进入充值页面',
|
||||
USER_INACTIVE: '账户已被禁用,无法充值,请联系管理员',
|
||||
TOO_MANY_PENDING: '您有过多待支付订单,请先完成或取消现有订单后再试',
|
||||
USER_NOT_FOUND: '用户不存在,请检查链接是否正确',
|
||||
INVALID_TOKEN: pickLocaleText(locale, '认证已失效,请重新从平台进入充值页面', 'Authentication expired. Please re-enter the recharge page from the platform'),
|
||||
USER_INACTIVE: pickLocaleText(locale, '账户已被禁用,无法充值,请联系管理员', 'This account is disabled and cannot be recharged. Please contact the administrator'),
|
||||
TOO_MANY_PENDING: pickLocaleText(locale, '您有过多待支付订单,请先完成或取消现有订单后再试', 'You have too many pending orders. Please complete or cancel existing orders first'),
|
||||
USER_NOT_FOUND: pickLocaleText(locale, '用户不存在,请检查链接是否正确', 'User not found. Please check whether the link is correct'),
|
||||
DAILY_LIMIT_EXCEEDED: data.error,
|
||||
METHOD_DAILY_LIMIT_EXCEEDED: data.error,
|
||||
PAYMENT_GATEWAY_ERROR: data.error,
|
||||
};
|
||||
setError(codeMessages[data.code] || data.error || '创建订单失败');
|
||||
setError(codeMessages[data.code] || data.error || pickLocaleText(locale, '创建订单失败', 'Failed to create order'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -288,7 +296,7 @@ function PayContent() {
|
||||
|
||||
setStep('paying');
|
||||
} catch {
|
||||
setError('网络错误,请稍后重试');
|
||||
setError(pickLocaleText(locale, '网络错误,请稍后重试', 'Network error. Please try again later'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -314,8 +322,9 @@ function PayContent() {
|
||||
isDark={isDark}
|
||||
isEmbedded={isEmbedded}
|
||||
maxWidth={isMobile ? 'sm' : 'lg'}
|
||||
title="Sub2API 余额充值"
|
||||
subtitle="安全支付,自动到账"
|
||||
title={pickLocaleText(locale, 'Sub2API 余额充值', 'Sub2API Balance Recharge')}
|
||||
subtitle={pickLocaleText(locale, '安全支付,自动到账', 'Secure payment, automatic crediting')}
|
||||
locale={locale}
|
||||
actions={
|
||||
!isMobile ? (
|
||||
<>
|
||||
@@ -329,7 +338,7 @@ function PayContent() {
|
||||
: 'border-slate-300 text-slate-700 hover:bg-slate-100',
|
||||
].join(' ')}
|
||||
>
|
||||
刷新
|
||||
{pickLocaleText(locale, '刷新', 'Refresh')}
|
||||
</button>
|
||||
<a
|
||||
href={ordersUrl}
|
||||
@@ -340,7 +349,7 @@ function PayContent() {
|
||||
: 'border-slate-300 text-slate-700 hover:bg-slate-100',
|
||||
].join(' ')}
|
||||
>
|
||||
我的订单
|
||||
{pickLocaleText(locale, '我的订单', 'My Orders')}
|
||||
</a>
|
||||
</>
|
||||
) : undefined
|
||||
@@ -378,7 +387,7 @@ function PayContent() {
|
||||
: 'text-slate-500 hover:text-slate-700',
|
||||
].join(' ')}
|
||||
>
|
||||
充值
|
||||
{pickLocaleText(locale, '充值', 'Recharge')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -394,7 +403,7 @@ function PayContent() {
|
||||
: 'text-slate-500 hover:text-slate-700',
|
||||
].join(' ')}
|
||||
>
|
||||
我的订单
|
||||
{pickLocaleText(locale, '我的订单', 'My Orders')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -402,7 +411,9 @@ function PayContent() {
|
||||
{step === 'form' && config.enabledPaymentTypes.length === 0 && (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<div className="h-6 w-6 animate-spin rounded-full border-2 border-blue-500 border-t-transparent" />
|
||||
<span className={['ml-3 text-sm', isDark ? 'text-slate-400' : 'text-gray-500'].join(' ')}>加载中...</span>
|
||||
<span className={['ml-3 text-sm', isDark ? 'text-slate-400' : 'text-gray-500'].join(' ')}>
|
||||
{pickLocaleText(locale, '加载中...', 'Loading...')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -423,6 +434,7 @@ function PayContent() {
|
||||
dark={isDark}
|
||||
pendingBlocked={pendingBlocked}
|
||||
pendingCount={pendingCount}
|
||||
locale={locale}
|
||||
/>
|
||||
) : (
|
||||
<MobileOrderList
|
||||
@@ -433,6 +445,7 @@ function PayContent() {
|
||||
loadingMore={ordersLoadingMore}
|
||||
onRefresh={loadUserAndOrders}
|
||||
onLoadMore={loadMoreOrders}
|
||||
locale={locale}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
@@ -451,6 +464,7 @@ function PayContent() {
|
||||
dark={isDark}
|
||||
pendingBlocked={pendingBlocked}
|
||||
pendingCount={pendingCount}
|
||||
locale={locale}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
@@ -460,11 +474,17 @@ function PayContent() {
|
||||
isDark ? 'border-slate-700 bg-slate-800/70' : 'border-slate-200 bg-slate-50',
|
||||
].join(' ')}
|
||||
>
|
||||
<div className={['text-xs', isDark ? 'text-slate-400' : 'text-slate-500'].join(' ')}>支付说明</div>
|
||||
<div className={['text-xs', isDark ? 'text-slate-400' : 'text-slate-500'].join(' ')}>
|
||||
{pickLocaleText(locale, '支付说明', 'Payment Notes')}
|
||||
</div>
|
||||
<ul className={['mt-2 space-y-1 text-sm', isDark ? 'text-slate-300' : 'text-slate-600'].join(' ')}>
|
||||
<li>订单完成后会自动到账</li>
|
||||
<li>如需历史记录请查看「我的订单」</li>
|
||||
{config.maxDailyAmount > 0 && <li>每日最大充值 ¥{config.maxDailyAmount.toFixed(2)}</li>}
|
||||
<li>{pickLocaleText(locale, '订单完成后会自动到账', 'Balance will be credited automatically after the order completes')}</li>
|
||||
<li>{pickLocaleText(locale, '如需历史记录请查看「我的订单」', 'Check "My Orders" for payment history')}</li>
|
||||
{config.maxDailyAmount > 0 && (
|
||||
<li>
|
||||
{pickLocaleText(locale, '每日最大充值', 'Maximum daily recharge')} ¥{config.maxDailyAmount.toFixed(2)}
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -475,7 +495,9 @@ function PayContent() {
|
||||
isDark ? 'border-slate-700 bg-slate-800/70' : 'border-slate-200 bg-slate-50',
|
||||
].join(' ')}
|
||||
>
|
||||
<div className={['text-xs', isDark ? 'text-slate-400' : 'text-slate-500'].join(' ')}>Support</div>
|
||||
<div className={['text-xs', isDark ? 'text-slate-400' : 'text-slate-500'].join(' ')}>
|
||||
{pickLocaleText(locale, '帮助', 'Support')}
|
||||
</div>
|
||||
{helpImageUrl && (
|
||||
<img
|
||||
src={helpImageUrl}
|
||||
@@ -491,7 +513,7 @@ function PayContent() {
|
||||
isDark ? 'text-slate-300' : 'text-slate-600',
|
||||
].join(' ')}
|
||||
>
|
||||
{helpText.split('\\n').map((line, i) => (
|
||||
{helpText.split('\n').map((line, i) => (
|
||||
<p key={i}>{line}</p>
|
||||
))}
|
||||
</div>
|
||||
@@ -521,10 +543,11 @@ function PayContent() {
|
||||
dark={isDark}
|
||||
isEmbedded={isEmbedded}
|
||||
isMobile={isMobile}
|
||||
locale={locale}
|
||||
/>
|
||||
)}
|
||||
|
||||
{step === 'result' && <OrderStatus status={finalStatus} onBack={handleBack} dark={isDark} />}
|
||||
{step === 'result' && <OrderStatus status={finalStatus} onBack={handleBack} dark={isDark} locale={locale} />}
|
||||
|
||||
{helpImageOpen && helpImageUrl && (
|
||||
<div
|
||||
@@ -543,14 +566,21 @@ function PayContent() {
|
||||
);
|
||||
}
|
||||
|
||||
function PayPageFallback() {
|
||||
const searchParams = useSearchParams();
|
||||
const locale = resolveLocale(searchParams.get('lang'));
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center">
|
||||
<div className="text-gray-500">{pickLocaleText(locale, '加载中...', 'Loading...')}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function PayPage() {
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="flex min-h-screen items-center justify-center">
|
||||
<div className="text-gray-500">加载中...</div>
|
||||
</div>
|
||||
}
|
||||
fallback={<PayPageFallback />}
|
||||
>
|
||||
<PayContent />
|
||||
</Suspense>
|
||||
|
||||
@@ -2,22 +2,43 @@
|
||||
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { useEffect, useState, Suspense } from 'react';
|
||||
import { applyLocaleToSearchParams, pickLocaleText, resolveLocale } from '@/lib/locale';
|
||||
|
||||
function ResultContent() {
|
||||
const searchParams = useSearchParams();
|
||||
// Support both ZPAY (out_trade_no) and Stripe (order_id) callback params
|
||||
const outTradeNo = searchParams.get('out_trade_no') || searchParams.get('order_id');
|
||||
const tradeStatus = searchParams.get('trade_status') || searchParams.get('status');
|
||||
const isPopup = searchParams.get('popup') === '1';
|
||||
const theme = searchParams.get('theme') === 'dark' ? 'dark' : 'light';
|
||||
const locale = resolveLocale(searchParams.get('lang'));
|
||||
const isDark = theme === 'dark';
|
||||
|
||||
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...'),
|
||||
};
|
||||
|
||||
const [status, setStatus] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [isInPopup, setIsInPopup] = useState(false);
|
||||
const [countdown, setCountdown] = useState(5);
|
||||
|
||||
// Detect if opened as a popup window (from stripe-popup or via popup=1 param)
|
||||
useEffect(() => {
|
||||
if (isPopup || window.opener) {
|
||||
setIsInPopup(true);
|
||||
@@ -38,14 +59,12 @@ function ResultContent() {
|
||||
setStatus(data.status);
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
checkOrder();
|
||||
// Poll a few times in case status hasn't updated yet
|
||||
const timer = setInterval(checkOrder, 3000);
|
||||
const timeout = setTimeout(() => clearInterval(timer), 30000);
|
||||
return () => {
|
||||
@@ -59,14 +78,20 @@ function ResultContent() {
|
||||
const goBack = () => {
|
||||
if (isInPopup) {
|
||||
window.close();
|
||||
} else if (window.history.length > 1) {
|
||||
window.history.back();
|
||||
} else {
|
||||
window.close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (window.history.length > 1) {
|
||||
window.history.back();
|
||||
return;
|
||||
}
|
||||
|
||||
const params = new URLSearchParams();
|
||||
params.set('theme', theme);
|
||||
applyLocaleToSearchParams(params, locale);
|
||||
window.location.replace(`/pay?${params.toString()}`);
|
||||
};
|
||||
|
||||
// Countdown auto-return on success
|
||||
useEffect(() => {
|
||||
if (!isSuccess) return;
|
||||
setCountdown(5);
|
||||
@@ -81,18 +106,18 @@ function ResultContent() {
|
||||
});
|
||||
}, 1000);
|
||||
return () => clearInterval(timer);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isSuccess, isInPopup]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className={`flex min-h-screen items-center justify-center ${isDark ? 'bg-slate-950' : 'bg-slate-50'}`}>
|
||||
<div className={isDark ? 'text-slate-400' : 'text-gray-500'}>查询支付结果中...</div>
|
||||
<div className={isDark ? 'text-slate-400' : 'text-gray-500'}>{text.checking}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const isPending = status === 'PENDING';
|
||||
const countdownText = countdown > 0 ? pickLocaleText(locale, `${countdown} 秒后自动返回`, `${countdown} seconds before returning`) : text.returning;
|
||||
|
||||
return (
|
||||
<div className={`flex min-h-screen items-center justify-center p-4 ${isDark ? 'bg-slate-950' : 'bg-slate-50'}`}>
|
||||
@@ -105,78 +130,79 @@ function ResultContent() {
|
||||
{isSuccess ? (
|
||||
<>
|
||||
<div className="text-6xl text-green-500">✓</div>
|
||||
<h1 className="mt-4 text-xl font-bold text-green-600">
|
||||
{status === 'COMPLETED' ? '充值成功' : '充值处理中'}
|
||||
</h1>
|
||||
<h1 className="mt-4 text-xl font-bold text-green-600">{status === 'COMPLETED' ? text.success : text.processing}</h1>
|
||||
<p className={isDark ? 'mt-2 text-slate-400' : 'mt-2 text-gray-500'}>
|
||||
{status === 'COMPLETED' ? '余额已成功到账!' : '支付成功,余额正在充值中...'}
|
||||
{status === 'COMPLETED' ? text.successMessage : text.processingMessage}
|
||||
</p>
|
||||
<div className="mt-4 space-y-2">
|
||||
<p className={isDark ? 'text-sm text-slate-500' : 'text-sm text-gray-400'}>
|
||||
{countdown > 0 ? `${countdown} 秒后自动返回` : '正在返回...'}
|
||||
</p>
|
||||
<p className={isDark ? 'text-sm text-slate-500' : 'text-sm text-gray-400'}>{countdownText}</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={goBack}
|
||||
className="text-sm text-blue-600 underline hover:text-blue-700"
|
||||
>
|
||||
立即返回
|
||||
{text.returnNow}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
) : isPending ? (
|
||||
<>
|
||||
<div className="text-6xl text-yellow-500">⏳</div>
|
||||
<h1 className="mt-4 text-xl font-bold text-yellow-600">等待支付</h1>
|
||||
<p className={isDark ? 'mt-2 text-slate-400' : 'mt-2 text-gray-500'}>订单尚未完成支付</p>
|
||||
<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>
|
||||
<button
|
||||
type="button"
|
||||
onClick={goBack}
|
||||
className="mt-4 text-sm text-blue-600 underline hover:text-blue-700"
|
||||
>
|
||||
返回
|
||||
{text.back}
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="text-6xl text-red-500">✗</div>
|
||||
<h1 className="mt-4 text-xl font-bold text-red-600">
|
||||
{status === 'EXPIRED' ? '订单已超时' : status === 'CANCELLED' ? '订单已取消' : '支付异常'}
|
||||
{status === 'EXPIRED' ? text.expired : status === 'CANCELLED' ? text.cancelled : text.abnormal}
|
||||
</h1>
|
||||
<p className={isDark ? 'mt-2 text-slate-400' : 'mt-2 text-gray-500'}>
|
||||
{status === 'EXPIRED'
|
||||
? '订单已超时,请重新充值'
|
||||
? text.expiredMessage
|
||||
: status === 'CANCELLED'
|
||||
? '订单已被取消'
|
||||
: '请联系管理员处理'}
|
||||
? text.cancelledMessage
|
||||
: text.abnormalMessage}
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={goBack}
|
||||
className="mt-4 text-sm text-blue-600 underline hover:text-blue-700"
|
||||
>
|
||||
返回
|
||||
{text.back}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
<p className={isDark ? 'mt-4 text-xs text-slate-500' : 'mt-4 text-xs text-gray-400'}>
|
||||
订单号: {outTradeNo || '未知'}
|
||||
{text.orderId}: {outTradeNo || text.unknown}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
export default function PayResultPage() {
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="flex min-h-screen items-center justify-center bg-slate-50">
|
||||
<div className="text-gray-500">加载中...</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Suspense fallback={<ResultPageFallback />}>
|
||||
<ResultContent />
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { useEffect, useState, useCallback, Suspense } from 'react';
|
||||
import { applyLocaleToSearchParams, pickLocaleText, resolveLocale } from '@/lib/locale';
|
||||
import { getPaymentMeta } from '@/lib/pay-utils';
|
||||
|
||||
function StripePopupContent() {
|
||||
@@ -10,10 +11,24 @@ function StripePopupContent() {
|
||||
const amount = parseFloat(searchParams.get('amount') || '0') || 0;
|
||||
const theme = searchParams.get('theme') === 'dark' ? 'dark' : 'light';
|
||||
const method = searchParams.get('method') || '';
|
||||
const locale = resolveLocale(searchParams.get('lang'));
|
||||
const isDark = theme === 'dark';
|
||||
const isAlipay = method === 'alipay';
|
||||
|
||||
// Sensitive data received via postMessage from parent, NOT from URL
|
||||
const text = {
|
||||
init: pickLocaleText(locale, '正在初始化...', 'Initializing...'),
|
||||
orderId: pickLocaleText(locale, '订单号', 'Order ID'),
|
||||
loadFailed: pickLocaleText(locale, '支付组件加载失败,请关闭窗口重试', 'Failed to load payment component. Please close the window and try again.'),
|
||||
payFailed: pickLocaleText(locale, '支付失败,请重试', 'Payment failed. Please try again.'),
|
||||
closeWindow: pickLocaleText(locale, '关闭窗口', 'Close window'),
|
||||
redirecting: pickLocaleText(locale, '正在跳转到支付页面...', 'Redirecting to payment page...'),
|
||||
loadingForm: pickLocaleText(locale, '正在加载支付表单...', 'Loading payment form...'),
|
||||
successClosing: pickLocaleText(locale, '支付成功,窗口即将自动关闭...', 'Payment successful. This window will close automatically...'),
|
||||
closeWindowManually: pickLocaleText(locale, '手动关闭窗口', 'Close window manually'),
|
||||
processing: pickLocaleText(locale, '处理中...', 'Processing...'),
|
||||
payAmount: pickLocaleText(locale, `支付 ¥${amount.toFixed(2)}`, `Pay ¥${amount.toFixed(2)}`),
|
||||
};
|
||||
|
||||
const [credentials, setCredentials] = useState<{
|
||||
clientSecret: string;
|
||||
publishableKey: string;
|
||||
@@ -34,10 +49,11 @@ function StripePopupContent() {
|
||||
returnUrl.searchParams.set('order_id', orderId);
|
||||
returnUrl.searchParams.set('status', 'success');
|
||||
returnUrl.searchParams.set('popup', '1');
|
||||
returnUrl.searchParams.set('theme', theme);
|
||||
applyLocaleToSearchParams(returnUrl.searchParams, locale);
|
||||
return returnUrl.toString();
|
||||
}, [orderId]);
|
||||
}, [orderId, theme, locale]);
|
||||
|
||||
// Listen for credentials from parent window via postMessage
|
||||
useEffect(() => {
|
||||
const handler = (event: MessageEvent) => {
|
||||
if (event.origin !== window.location.origin) return;
|
||||
@@ -48,14 +64,12 @@ function StripePopupContent() {
|
||||
}
|
||||
};
|
||||
window.addEventListener('message', handler);
|
||||
// Signal parent that popup is ready to receive data
|
||||
if (window.opener) {
|
||||
window.opener.postMessage({ type: 'STRIPE_POPUP_READY' }, window.location.origin);
|
||||
}
|
||||
return () => window.removeEventListener('message', handler);
|
||||
}, []);
|
||||
|
||||
// Initialize Stripe once credentials are received
|
||||
useEffect(() => {
|
||||
if (!credentials) return;
|
||||
let cancelled = false;
|
||||
@@ -65,14 +79,13 @@ function StripePopupContent() {
|
||||
loadStripe(publishableKey).then((stripe) => {
|
||||
if (cancelled || !stripe) {
|
||||
if (!cancelled) {
|
||||
setStripeError('支付组件加载失败,请关闭窗口重试');
|
||||
setStripeError(text.loadFailed);
|
||||
setStripeLoaded(true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (isAlipay) {
|
||||
// Alipay: confirm directly and redirect, no Payment Element needed
|
||||
stripe
|
||||
.confirmAlipayPayment(clientSecret, {
|
||||
return_url: buildReturnUrl(),
|
||||
@@ -80,15 +93,13 @@ function StripePopupContent() {
|
||||
.then((result) => {
|
||||
if (cancelled) return;
|
||||
if (result.error) {
|
||||
setStripeError(result.error.message || '支付失败,请重试');
|
||||
setStripeError(result.error.message || text.payFailed);
|
||||
setStripeLoaded(true);
|
||||
}
|
||||
// If no error, the page has already been redirected
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback: create Elements for Payment Element flow
|
||||
const elements = stripe.elements({
|
||||
clientSecret,
|
||||
appearance: {
|
||||
@@ -103,9 +114,8 @@ function StripePopupContent() {
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [credentials, isDark, isAlipay, buildReturnUrl]);
|
||||
}, [credentials, isDark, isAlipay, buildReturnUrl, text.loadFailed, text.payFailed]);
|
||||
|
||||
// Mount Payment Element (only for non-alipay methods)
|
||||
const stripeContainerRef = useCallback(
|
||||
(node: HTMLDivElement | null) => {
|
||||
if (!node || !stripeLib) return;
|
||||
@@ -135,7 +145,7 @@ function StripePopupContent() {
|
||||
});
|
||||
|
||||
if (error) {
|
||||
setStripeError(error.message || '支付失败,请重试');
|
||||
setStripeError(error.message || text.payFailed);
|
||||
setStripeSubmitting(false);
|
||||
} else {
|
||||
setStripeSuccess(true);
|
||||
@@ -143,7 +153,6 @@ function StripePopupContent() {
|
||||
}
|
||||
};
|
||||
|
||||
// Auto-close after success
|
||||
useEffect(() => {
|
||||
if (!stripeSuccess) return;
|
||||
const timer = setTimeout(() => {
|
||||
@@ -152,7 +161,6 @@ function StripePopupContent() {
|
||||
return () => clearTimeout(timer);
|
||||
}, [stripeSuccess]);
|
||||
|
||||
// Waiting for credentials from parent
|
||||
if (!credentials) {
|
||||
return (
|
||||
<div className={`flex min-h-screen items-center justify-center p-4 ${isDark ? 'bg-slate-950' : 'bg-slate-50'}`}>
|
||||
@@ -161,14 +169,13 @@ function StripePopupContent() {
|
||||
>
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-2 border-[#635bff] border-t-transparent" />
|
||||
<span className={`ml-3 text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>正在初始化...</span>
|
||||
<span className={`ml-3 text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>{text.init}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Alipay direct confirm: show loading/redirecting state
|
||||
if (isAlipay) {
|
||||
return (
|
||||
<div className={`flex min-h-screen items-center justify-center p-4 ${isDark ? 'bg-slate-950' : 'bg-slate-50'}`}>
|
||||
@@ -180,7 +187,7 @@ function StripePopupContent() {
|
||||
{'¥'}
|
||||
{amount.toFixed(2)}
|
||||
</div>
|
||||
<p className={`mt-1 text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>订单号: {orderId}</p>
|
||||
<p className={`mt-1 text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>{text.orderId}: {orderId}</p>
|
||||
</div>
|
||||
{stripeError ? (
|
||||
<div className="space-y-3">
|
||||
@@ -190,14 +197,14 @@ function StripePopupContent() {
|
||||
onClick={() => window.close()}
|
||||
className="w-full text-sm text-blue-600 underline hover:text-blue-700"
|
||||
>
|
||||
关闭窗口
|
||||
{text.closeWindow}
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-2 border-[#635bff] border-t-transparent" />
|
||||
<span className={`ml-3 text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
|
||||
正在跳转到支付页面...
|
||||
{text.redirecting}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -216,26 +223,26 @@ function StripePopupContent() {
|
||||
{'¥'}
|
||||
{amount.toFixed(2)}
|
||||
</div>
|
||||
<p className={`mt-1 text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>订单号: {orderId}</p>
|
||||
<p className={`mt-1 text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>{text.orderId}: {orderId}</p>
|
||||
</div>
|
||||
|
||||
{!stripeLoaded ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-2 border-[#635bff] border-t-transparent" />
|
||||
<span className={`ml-3 text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>正在加载支付表单...</span>
|
||||
<span className={`ml-3 text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>{text.loadingForm}</span>
|
||||
</div>
|
||||
) : stripeSuccess ? (
|
||||
<div className="py-6 text-center">
|
||||
<div className="text-5xl text-green-600">{'✓'}</div>
|
||||
<p className={`mt-3 text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
|
||||
支付成功,窗口即将自动关闭...
|
||||
{text.successClosing}
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => window.close()}
|
||||
className="mt-4 text-sm text-blue-600 underline hover:text-blue-700"
|
||||
>
|
||||
手动关闭窗口
|
||||
{text.closeWindowManually}
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
@@ -261,10 +268,10 @@ function StripePopupContent() {
|
||||
{stripeSubmitting ? (
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<span className="h-4 w-4 animate-spin rounded-full border-2 border-white border-t-transparent" />
|
||||
处理中...
|
||||
{text.processing}
|
||||
</span>
|
||||
) : (
|
||||
`支付 ¥${amount.toFixed(2)}`
|
||||
text.payAmount
|
||||
)}
|
||||
</button>
|
||||
</>
|
||||
@@ -274,15 +281,20 @@ function StripePopupContent() {
|
||||
);
|
||||
}
|
||||
|
||||
function StripePopupFallback() {
|
||||
const searchParams = useSearchParams();
|
||||
const locale = resolveLocale(searchParams.get('lang'));
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center">
|
||||
<div className="text-gray-500">{pickLocaleText(locale, '加载中...', 'Loading...')}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function StripePopupPage() {
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="flex min-h-screen items-center justify-center">
|
||||
<div className="text-gray-500">加载中...</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Suspense fallback={<StripePopupFallback />}>
|
||||
<StripePopupContent />
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user