import { useCallback, useEffect, useState } from 'react'; import { createPortal } from 'react-dom'; import { getAvailablePlans, purchaseSubscription, setAutoRenew } from '../api/client'; import type { ActiveSubscription, PlanDefinition } from '../types'; // Plan tier order (matches PLAN_ORDER in billing-subscription.mjs) const PLAN_ORDER: Record = { free: 0, lite: 1, standard: 2, pro: 3 }; function formatYuan(cents: number) { return `¥${(cents / 100).toFixed(cents % 100 === 0 ? 0 : 2)}`; } function formatTokens(tokens: number) { if (tokens === 0) return '不限'; if (tokens >= 10_000) return `${(tokens / 10_000).toFixed(0)} 万`; if (tokens >= 1_000) return `${(tokens / 1_000).toFixed(0)}k`; return tokens.toLocaleString('zh-CN'); } function callsApprox(tokens: number) { if (tokens === 0) return '不限'; return `约 ${Math.floor(tokens / 3_000).toLocaleString()} 次`; } function imageCallsApprox(images: number) { if (images === 0) return '不限'; return `约 ${images.toLocaleString()} 次`; } const MODEL_TIER_LABELS: Record = { basic: '基础模型', standard: '标准模型', premium: '旗舰模型', }; const PLAN_HIGHLIGHT: Record = { lite: '', standard: '推荐', pro: '旗舰', }; type SubscribeModalProps = { open: boolean; onClose: () => void; onSuccess: (subscription: ActiveSubscription, balanceCents: number) => void; onRechargeNeeded: (shortfallCents: number) => void; }; export function SubscribeModal({ open, onClose, onSuccess, onRechargeNeeded }: SubscribeModalProps) { const [plans, setPlans] = useState([]); const [currentSub, setCurrentSub] = useState(null); const [balanceCents, setBalanceCents] = useState(0); const [loading, setLoading] = useState(false); const [purchasing, setPurchasing] = useState(null); const [error, setError] = useState(null); const [successMessage, setSuccessMessage] = useState(null); const [shortfall, setShortfall] = useState<{ planType: string; shortfallCents: number } | null>(null); const [autoRenew, setAutoRenewState] = useState(false); const [togglingAutoRenew, setTogglingAutoRenew] = useState(false); const reset = useCallback(() => { setPurchasing(null); setError(null); setSuccessMessage(null); setShortfall(null); }, []); useEffect(() => { if (!open) { reset(); return; } setLoading(true); setError(null); void getAvailablePlans() .then((data) => { setPlans(data.plans); setCurrentSub(data.subscription); setBalanceCents(data.balanceCents); setAutoRenewState(data.subscription?.autoRenew ?? false); }) .catch((err) => setError(err instanceof Error ? err.message : '无法加载套餐列表')) .finally(() => setLoading(false)); }, [open, reset]); const handlePurchase = async (planType: string) => { setPurchasing(planType); setError(null); setShortfall(null); setSuccessMessage(null); try { const result = await purchaseSubscription(planType, autoRenew); const plan = plans.find((p) => p.key === planType); const renewNote = autoRenew ? ',已开启自动续费' : ''; setSuccessMessage(`已开通 ${plan?.name ?? planType}${renewNote},余额剩余 ${formatYuan(result.balanceCents)}`); setCurrentSub(result.subscription); setBalanceCents(result.balanceCents); setAutoRenewState(result.subscription.autoRenew); window.setTimeout(() => onSuccess(result.subscription, result.balanceCents), 1000); } catch (err: unknown) { type E = { status?: number; code?: string; message?: string; details?: Record }; const e = err as E; if (e.status === 402 || e.code === 'INSUFFICIENT_BALANCE') { const plan = plans.find((p) => p.key === planType); const sf = Math.max(0, (plan?.priceCents ?? 0) - balanceCents); setShortfall({ planType, shortfallCents: sf }); } else { setError(e.message ?? '订阅失败,请稍后重试'); } } finally { setPurchasing(null); } }; const handleToggleAutoRenew = async (enabled: boolean) => { setTogglingAutoRenew(true); try { await setAutoRenew(enabled); setAutoRenewState(enabled); setCurrentSub((s) => s ? { ...s, autoRenew: enabled } : s); } catch (err: unknown) { const e = err as { message?: string }; setError(e.message ?? '设置失败'); } finally { setTogglingAutoRenew(false); } }; if (!open) return null; return createPortal(
e.stopPropagation()} > {/* Header */}

升级套餐

选择适合你的方案

{/* Balance row */}
当前余额 {formatYuan(balanceCents)} {currentSub && currentSub.planType !== 'free' && ( 当前:{currentSub.planType} · 至 {new Date(currentSub.expiresAt).toLocaleDateString('zh-CN')} )}
{/* Plan grid */} {loading ? (

加载套餐中…

) : (
{plans.map((plan) => { const canAfford = balanceCents >= plan.priceCents; const isActive = currentSub?.planType === plan.key; const badge = PLAN_HIGHLIGHT[plan.key]; const isBuying = purchasing === plan.key; const currentOrder = PLAN_ORDER[currentSub?.planType ?? 'free'] ?? 0; const planOrder = PLAN_ORDER[plan.key] ?? 0; const isDowngrade = !!currentSub && planOrder < currentOrder; const isUpgrade = !!currentSub && !isActive && planOrder > currentOrder; return (
{badge && {badge}}
{plan.name}
{formatYuan(plan.priceCents)} /月
  • 对话 {callsApprox(plan.periodTokens)}
  • 图片 {imageCallsApprox(plan.periodImages)}
  • Token {formatTokens(plan.periodTokens)}
  • 模型 {MODEL_TIER_LABELS[plan.modelTier] ?? plan.modelTier}
  • {plan.overageRate < 1 && (
  • 超量 {Math.round(plan.overageRate * 10)} 折
  • )}
{isActive ? ( ) : isDowngrade ? ( ) : ( )}
); })}
)} {/* Feedback messages */} {shortfall && !successMessage && (
余额不足,还差 {formatYuan(shortfall.shortfallCents)} 才能订阅该套餐
)} {error &&

{error}

} {successMessage &&

✓ {successMessage}

} {/* Auto-renew toggle */}
{currentSub && currentSub.planType !== 'free' ? autoRenew ? '到期自动扣费续订,余额不足时将不续订' : '到期后自动降回免费版' : autoRenew ? '订阅后将自动续费' : '订阅后每月需手动续订'}

余额直接扣除 · 订阅立即生效

, document.body, ); }