'use client'; import { useSearchParams } from 'next/navigation'; import { useState, useEffect, Suspense, useCallback } from 'react'; import PaymentForm from '@/components/PaymentForm'; import PaymentQRCode from '@/components/PaymentQRCode'; import OrderStatus from '@/components/OrderStatus'; import PayPageLayout from '@/components/PayPageLayout'; import MobileOrderList from '@/components/MobileOrderList'; import MainTabs from '@/components/MainTabs'; import ChannelGrid from '@/components/ChannelGrid'; import SubscriptionPlanCard from '@/components/SubscriptionPlanCard'; import SubscriptionConfirm from '@/components/SubscriptionConfirm'; import UserSubscriptions from '@/components/UserSubscriptions'; import PurchaseFlow from '@/components/PurchaseFlow'; import { resolveLocale, pickLocaleText, applyLocaleToSearchParams } from '@/lib/locale'; import { detectDeviceIsMobile, applySublabelOverrides, type UserInfo, type MyOrder } from '@/lib/pay-utils'; import type { PublicOrderStatusSnapshot } from '@/lib/order/status'; import type { MethodLimitInfo } from '@/components/PaymentForm'; import type { ChannelInfo } from '@/components/ChannelGrid'; import type { PlanInfo } from '@/components/SubscriptionPlanCard'; import type { UserSub } from '@/components/UserSubscriptions'; interface OrderResult { orderId: string; amount: number; payAmount?: number; status: string; paymentType: string; payUrl?: string | null; qrCode?: string | null; clientSecret?: string | null; expiresAt: string; statusAccessToken: string; } interface AppConfig { enabledPaymentTypes: string[]; minAmount: number; maxAmount: number; maxDailyAmount: number; methodLimits?: Record; helpImageUrl?: string | null; helpText?: string | null; stripePublishableKey?: string | null; balanceDisabled?: boolean; } function PayContent() { const searchParams = useSearchParams(); const token = (searchParams.get('token') || '').trim(); const theme = searchParams.get('theme') === 'dark' ? 'dark' : 'light'; const uiMode = searchParams.get('ui_mode') || 'standalone'; 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); const [isMobile, setIsMobile] = useState(false); const [step, setStep] = useState<'form' | 'paying' | 'result'>('form'); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const [subscriptionError, setSubscriptionError] = useState(''); const [orderResult, setOrderResult] = useState(null); const [finalOrderState, setFinalOrderState] = useState(null); const [userInfo, setUserInfo] = useState(null); const [resolvedUserId, setResolvedUserId] = useState(null); const [myOrders, setMyOrders] = useState([]); const [ordersPage, setOrdersPage] = useState(1); const [ordersHasMore, setOrdersHasMore] = useState(false); const [ordersLoadingMore, setOrdersLoadingMore] = useState(false); const [activeMobileTab, setActiveMobileTab] = useState<'pay' | 'orders'>('pay'); const [pendingCount, setPendingCount] = useState(0); // 新增状态 const [mainTab, setMainTab] = useState<'topup' | 'subscribe'>('topup'); const [channels, setChannels] = useState([]); const [plans, setPlans] = useState([]); const [userSubscriptions, setUserSubscriptions] = useState([]); const [showTopUpForm, setShowTopUpForm] = useState(false); const [selectedPlan, setSelectedPlan] = useState(null); const [channelsLoaded, setChannelsLoaded] = useState(false); const [userLoaded, setUserLoaded] = useState(false); const [config, setConfig] = useState({ enabledPaymentTypes: [], minAmount: 1, maxAmount: 1000, maxDailyAmount: 0, }); const [userNotFound, setUserNotFound] = useState(false); const [helpImageOpen, setHelpImageOpen] = useState(false); const hasToken = token.length > 0; const isEmbedded = uiMode === 'embedded' && isIframeContext; const helpImageUrl = (config.helpImageUrl || '').trim(); const helpText = (config.helpText || '').trim(); const hasHelpContent = Boolean(helpImageUrl || helpText); // 通用帮助/客服信息区块 const renderHelpSection = () => { if (!hasHelpContent) return null; return (
{pickLocaleText(locale, '帮助', 'Support')}
{helpImageUrl && ( help setHelpImageOpen(true)} className={`mt-3 max-h-40 w-full cursor-zoom-in rounded-lg object-contain p-2 ${isDark ? 'bg-slate-700/50' : 'bg-white/70'}`} /> )} {helpText && (
{helpText.split('\n').map((line, i) => (

{line}

))}
)}
); }; const MAX_PENDING = 3; const pendingBlocked = pendingCount >= MAX_PENDING; // R6: 余额充值是否被禁用 const balanceDisabled = config.balanceDisabled === true; // 是否有渠道配置(决定是直接显示充值表单还是渠道卡片+弹窗) const hasChannels = channels.length > 0; // 是否有可售卖套餐 const hasPlans = plans.length > 0; // 是否可以充值(未禁用且有支付方式) const canTopUp = !balanceDisabled && config.enabledPaymentTypes.length > 0; useEffect(() => { if (typeof window === 'undefined') return; setIsIframeContext(window.self !== window.top); setIsMobile(detectDeviceIsMobile()); }, []); useEffect(() => { if (!isMobile || step !== 'form') return; if (tab === 'orders') { setActiveMobileTab('orders'); return; } setActiveMobileTab('pay'); }, [isMobile, step, tab]); const loadUserAndOrders = useCallback(async () => { if (!token) return; setUserNotFound(false); try { const meRes = await fetch(`/api/orders/my?token=${encodeURIComponent(token)}`); if (!meRes.ok) { setUserNotFound(true); return; } const meData = await meRes.json(); const meUser = meData.user || {}; const meId = Number(meUser.id); if (!Number.isInteger(meId) || meId <= 0) { setUserNotFound(true); return; } setResolvedUserId(meId); setPendingCount(meData.summary?.pending ?? 0); setUserInfo({ id: meId, username: (typeof meUser.displayName === 'string' && meUser.displayName.trim()) || (typeof meUser.username === 'string' && meUser.username.trim()) || pickLocaleText(locale, `用户 #${meId}`, `User #${meId}`), balance: typeof meUser.balance === 'number' ? meUser.balance : undefined, }); if (Array.isArray(meData.orders)) { setMyOrders(meData.orders); setOrdersPage(1); setOrdersHasMore((meData.total_pages ?? 1) > 1); } else { setMyOrders([]); setOrdersPage(1); setOrdersHasMore(false); } const cfgRes = await fetch(`/api/user?user_id=${meId}&token=${encodeURIComponent(token)}`); if (cfgRes.ok) { const cfgData = await cfgRes.json(); if (cfgData.config) { setConfig({ enabledPaymentTypes: cfgData.config.enabledPaymentTypes ?? ['alipay', 'wxpay'], minAmount: cfgData.config.minAmount ?? 1, maxAmount: cfgData.config.maxAmount ?? 1000, maxDailyAmount: cfgData.config.maxDailyAmount ?? 0, methodLimits: cfgData.config.methodLimits, helpImageUrl: cfgData.config.helpImageUrl ?? null, helpText: cfgData.config.helpText ?? null, stripePublishableKey: cfgData.config.stripePublishableKey ?? null, balanceDisabled: cfgData.config.balanceDisabled ?? false, }); if (cfgData.config.sublabelOverrides) { applySublabelOverrides(cfgData.config.sublabelOverrides); } } } } catch { } finally { setUserLoaded(true); } }, [token, locale]); // 加载渠道和订阅套餐 const loadChannelsAndPlans = useCallback(async () => { if (!token) return; try { const [chRes, plRes, subRes] = await Promise.all([ fetch(`/api/channels?token=${encodeURIComponent(token)}`), fetch(`/api/subscription-plans?token=${encodeURIComponent(token)}`), fetch(`/api/subscriptions/my?token=${encodeURIComponent(token)}`), ]); if (chRes.ok) { const chData = await chRes.json(); setChannels(chData.channels ?? []); } if (plRes.ok) { const plData = await plRes.json(); setPlans(plData.plans ?? []); } if (subRes.ok) { const subData = await subRes.json(); setUserSubscriptions(subData.subscriptions ?? []); } } catch { } finally { setChannelsLoaded(true); } }, [token]); const loadMoreOrders = async () => { if (!token || ordersLoadingMore || !ordersHasMore) return; const nextPage = ordersPage + 1; setOrdersLoadingMore(true); try { const res = await fetch(`/api/orders/my?token=${encodeURIComponent(token)}&page=${nextPage}&page_size=20`); if (!res.ok) return; const data = await res.json(); if (Array.isArray(data.orders) && data.orders.length > 0) { setMyOrders((prev) => [...prev, ...data.orders]); setOrdersPage(nextPage); setOrdersHasMore(nextPage < (data.total_pages ?? 1)); } else { setOrdersHasMore(false); } } catch { } finally { setOrdersLoadingMore(false); } }; useEffect(() => { loadUserAndOrders(); loadChannelsAndPlans(); }, [loadUserAndOrders, loadChannelsAndPlans]); useEffect(() => { if (step !== 'result' || finalOrderState?.status !== 'COMPLETED') return; loadUserAndOrders(); loadChannelsAndPlans(); const timer = setTimeout(() => { setStep('form'); setOrderResult(null); setFinalOrderState(null); setError(''); setSubscriptionError(''); setSelectedPlan(null); }, 2200); return () => clearTimeout(timer); }, [step, finalOrderState, loadUserAndOrders, loadChannelsAndPlans]); // 检查订单完成后是否是订阅分组消失的情况 useEffect(() => { if (step !== 'result' || !finalOrderState) return; if (finalOrderState.status === 'FAILED' && finalOrderState.failedReason?.includes('SUBSCRIPTION_GROUP_GONE')) { setSubscriptionError( pickLocaleText( locale, '您已成功支付,但订阅分组已下架,无法自动开通。请联系客服处理,提供订单号。', 'Payment successful, but the subscription group has been removed. Please contact support with your order ID.', ), ); } }, [step, finalOrderState, locale]); if (!hasToken) { return (

{pickLocaleText(locale, '缺少认证信息', 'Missing authentication info')}

{pickLocaleText( locale, '请从 Sub2API 平台正确访问充值页面', 'Please open the recharge page from the Sub2API platform', )}

); } if (userNotFound) { return (

{pickLocaleText(locale, '用户不存在', 'User not found')}

{pickLocaleText( locale, '请检查链接是否正确,或联系管理员', 'Please check whether the link is correct or contact the administrator', )}

); } const buildScopedUrl = (path: string, forceOrdersTab = false) => { const params = new URLSearchParams(); if (token) params.set('token', token); 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()}`; }; const pcOrdersUrl = buildScopedUrl('/pay/orders'); const mobileOrdersUrl = buildScopedUrl('/pay', true); const ordersUrl = isMobile ? mobileOrdersUrl : pcOrdersUrl; // ── 余额充值提交 ── const handleSubmit = async (amount: number, paymentType: string) => { if (pendingBlocked) { setError( pickLocaleText( locale, `您有 ${pendingCount} 个待支付订单,请先完成或取消后再试(最多 ${MAX_PENDING} 个)`, `You have ${pendingCount} pending orders. Please complete or cancel them first (maximum ${MAX_PENDING}).`, ), ); return; } setLoading(true); setError(''); try { const res = await fetch('/api/orders', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token, amount, payment_type: paymentType, is_mobile: isMobile, src_host: srcHost, src_url: srcUrl, }), }); const data = await res.json(); if (!res.ok) { const codeMessages: Record = { INVALID_TOKEN: pickLocaleText(locale, '认证已失效,请重新从平台进入充值页面', 'Authentication expired'), USER_INACTIVE: pickLocaleText(locale, '账户已被禁用,无法充值', 'Account is disabled'), TOO_MANY_PENDING: pickLocaleText(locale, '待支付订单过多,请先处理', 'Too many pending orders'), USER_NOT_FOUND: pickLocaleText(locale, '用户不存在', 'User not found'), DAILY_LIMIT_EXCEEDED: data.error, METHOD_DAILY_LIMIT_EXCEEDED: data.error, PAYMENT_GATEWAY_ERROR: data.error, }; setError( codeMessages[data.code] || data.error || pickLocaleText(locale, '创建订单失败', 'Failed to create order'), ); return; } setOrderResult({ orderId: data.orderId, amount: data.amount, payAmount: data.payAmount, status: data.status, paymentType: data.paymentType || paymentType, payUrl: data.payUrl, qrCode: data.qrCode, clientSecret: data.clientSecret, expiresAt: data.expiresAt, statusAccessToken: data.statusAccessToken, }); setStep('paying'); } catch { setError(pickLocaleText(locale, '网络错误,请稍后重试', 'Network error')); } finally { setLoading(false); } }; // ── 订阅下单 ── const handleSubscriptionSubmit = async (paymentType: string) => { if (!selectedPlan) return; setLoading(true); setError(''); try { const res = await fetch('/api/orders', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token, amount: selectedPlan.price, payment_type: paymentType, is_mobile: isMobile, src_host: srcHost, src_url: srcUrl, order_type: 'subscription', plan_id: selectedPlan.id, }), }); const data = await res.json(); if (!res.ok) { setError(data.error || pickLocaleText(locale, '创建订阅订单失败', 'Failed to create subscription order')); return; } setOrderResult({ orderId: data.orderId, amount: data.amount, payAmount: data.payAmount, status: data.status, paymentType: data.paymentType || paymentType, payUrl: data.payUrl, qrCode: data.qrCode, clientSecret: data.clientSecret, expiresAt: data.expiresAt, statusAccessToken: data.statusAccessToken, }); setStep('paying'); } catch { setError(pickLocaleText(locale, '网络错误,请稍后重试', 'Network error')); } finally { setLoading(false); } }; const handleStatusChange = (order: PublicOrderStatusSnapshot) => { setFinalOrderState(order); setStep('result'); if (isMobile) setActiveMobileTab('orders'); }; const handleBack = () => { setStep('form'); setOrderResult(null); setFinalOrderState(null); setError(''); setSubscriptionError(''); setSelectedPlan(null); setShowTopUpForm(false); }; // ── 渲染 ── // R7: 检查是否所有入口都关闭(无可用充值方式 且 无订阅套餐) const allEntriesClosed = channelsLoaded && userLoaded && !canTopUp && !hasPlans; const showMainTabs = channelsLoaded && userLoaded && !allEntriesClosed && (hasChannels || hasPlans); const pageTitle = showMainTabs ? pickLocaleText(locale, '选择适合你的 充值/订阅服务', 'Choose Your Recharge / Subscription') : pickLocaleText(locale, 'Sub2API 余额充值', 'Sub2API Balance Recharge'); const pageSubtitle = showMainTabs ? pickLocaleText(locale, '充值余额或者订阅套餐', 'Top up balance or subscribe to a plan') : pickLocaleText(locale, '安全支付,自动到账', 'Secure payment, automatic crediting'); return ( {pickLocaleText(locale, '我的订单', 'My Orders')} ) : undefined } > {/* 订阅分组消失的常驻错误 */} {subscriptionError && (
{pickLocaleText(locale, '订阅开通失败', 'Subscription Failed')}
{subscriptionError}
{orderResult && (
{pickLocaleText(locale, '订单号', 'Order ID')}: {orderResult.orderId}
)}
)} {error && (
{error}
)} {/* ── 表单阶段 ── */} {step === 'form' && ( <> {/* 移动端 Tab:充值/订单 */} {isMobile && (
)} {/* 加载中 */} {(!channelsLoaded || !userLoaded) && !allEntriesClosed && (
{pickLocaleText(locale, '加载中...', 'Loading...')}
)} {/* R7: 所有入口关闭提示 */} {allEntriesClosed && (activeMobileTab === 'pay' || !isMobile) && (

{pickLocaleText(locale, '充值/订阅 入口未开放', 'Recharge / Subscription entry is not available')}

{pickLocaleText( locale, '如有疑问,请联系管理员', 'Please contact the administrator if you have questions', )}

)} {/* ── 有渠道配置:新版UI ── */} {channelsLoaded && showMainTabs && (activeMobileTab === 'pay' || !isMobile) && !selectedPlan && !showTopUpForm && ( <> {mainTab === 'topup' && canTopUp && (
{/* 按量付费说明 banner */}

{pickLocaleText(locale, '按量付费模式', 'Pay-as-you-go')}

{pickLocaleText( locale, '无需订阅,充值即用,按实际消耗扣费。余额所有渠道通用,可自由切换。价格以美元计价(当前比例:1美元≈1人民币)', 'No subscription needed. Top up and use. Charged by actual usage. Balance works across all channels. Priced in USD (current rate: 1 USD ≈ 1 CNY)', )}

{pickLocaleText(locale, '倍率越低越划算', 'Lower rate = better value')}
{pickLocaleText( locale, '0.15倍率 = 1元可用约6.67美元额度', '0.15 rate = 1 CNY ≈ $6.67 quota', )}
{hasChannels ? ( setShowTopUpForm(true)} isDark={isDark} locale={locale} userBalance={userInfo?.balance} /> ) : ( )} {renderHelpSection()}
)} {mainTab === 'subscribe' && (
{plans.map((plan) => ( setSelectedPlan(plan)} isDark={isDark} locale={locale} /> ))}
{renderHelpSection()}
)} {/* 用户已有订阅 — 所有 tab 共用 */} {userSubscriptions.length > 0 && (

{pickLocaleText(locale, '我的订阅', 'My Subscriptions')}

{ const plan = plans.find((p) => p.groupId === groupId); if (plan) { setSelectedPlan(plan); setMainTab('subscribe'); } }} isDark={isDark} locale={locale} />
)} )} {/* 点击"立即充值"后:直接显示 PaymentForm(含金额选择) */} {showTopUpForm && step === 'form' && (
{renderHelpSection()}
)} {/* 订阅确认页 */} {selectedPlan && step === 'form' && ( <> setSelectedPlan(null)} onSubmit={handleSubscriptionSubmit} loading={loading} isDark={isDark} locale={locale} /> {renderHelpSection()} )} {/* ── 无渠道配置:传统充值UI ── */} {channelsLoaded && userLoaded && !showMainTabs && canTopUp && !selectedPlan && ( <> {isMobile ? ( activeMobileTab === 'pay' ? ( ) : ( ) ) : (
{pickLocaleText(locale, '支付说明', 'Payment Notes')}
  • {pickLocaleText(locale, '订单完成后会自动到账', 'Balance will be credited automatically')}
  • {pickLocaleText(locale, '如需历史记录请查看「我的订单」', 'Check "My Orders" for history')}
  • {config.maxDailyAmount > 0 && (
  • {pickLocaleText(locale, '每日最大充值', 'Max daily recharge')} ¥ {config.maxDailyAmount.toFixed(2)}
  • )}
{hasHelpContent && (
{pickLocaleText(locale, '帮助', 'Support')}
{helpImageUrl && ( help setHelpImageOpen(true)} className={`mt-3 max-h-40 w-full cursor-zoom-in rounded-lg object-contain p-2 ${isDark ? 'bg-slate-700/50' : 'bg-white/70'}`} /> )} {helpText && (
{helpText.split('\n').map((line, i) => (

{line}

))}
)}
)}
)} )} {/* 移动端订单列表 */} {isMobile && activeMobileTab === 'orders' && showMainTabs && ( )} )} {/* ── 支付阶段 ── */} {step === 'paying' && orderResult && ( <> {renderHelpSection()} )} {/* ── 结果阶段 ── */} {step === 'result' && orderResult && finalOrderState && ( )} {/* 帮助图片放大 */} {helpImageOpen && helpImageUrl && (
setHelpImageOpen(false)} > help e.stopPropagation()} />
)} ); } function PayPageFallback() { const searchParams = useSearchParams(); const locale = resolveLocale(searchParams.get('lang')); const isDark = searchParams.get('theme') === 'dark'; return (
{pickLocaleText(locale, '加载中...', 'Loading...')}
); } export default function PayPage() { return ( }> ); }