// ============================================================ // 计费管理 — 计划/订阅/用量/支付 // ============================================================ import { useState } from 'react' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { Button, message, Tag, Modal, Card, Row, Col, Statistic, Typography, Progress, Space, Radio, Spin, Empty, Divider, } from 'antd' import { CrownOutlined, CheckCircleOutlined, ThunderboltOutlined, RocketOutlined, TeamOutlined, AlipayCircleOutlined, WechatOutlined, LoadingOutlined, } from '@ant-design/icons' import { PageHeader } from '@/components/PageHeader' import { ErrorState } from '@/components/ErrorState' import { billingService } from '@/services/billing' import type { BillingPlan, SubscriptionInfo, PaymentResult } from '@/services/billing' const { Text, Title } = Typography // === 计划卡片 === const planIcons: Record = { free: , pro: , team: , } const planColors: Record = { free: '#8c8c8c', pro: '#863bff', team: '#47bfff', } function PlanCard({ plan, isCurrent, onSelect, }: { plan: BillingPlan isCurrent: boolean onSelect: (plan: BillingPlan) => void }) { const color = planColors[plan.name] || '#666' const limits = plan.limits as Record | undefined const maxRelay = (limits?.max_relay_requests_monthly as number) ?? '∞' const maxHand = (limits?.max_hand_executions_monthly as number) ?? '∞' const maxPipeline = (limits?.max_pipeline_runs_monthly as number) ?? '∞' return ( {isCurrent && (
当前计划
)}
{planIcons[plan.name] || }
{plan.display_name} {plan.description && ( {plan.description} )}
¥{plan.price_cents === 0 ? '0' : (plan.price_cents / 100).toFixed(0)} /{plan.interval === 'month' ? '月' : '年'}
中转请求: {maxRelay === Infinity ? '无限' : `${maxRelay} 次/月`}
Hand 执行: {maxHand === Infinity ? '无限' : `${maxHand} 次/月`}
Pipeline 运行: {maxPipeline === Infinity ? '无限' : `${maxPipeline} 次/月`}
知识库: {plan.name === 'free' ? '基础' : '高级'}
优先级队列: {plan.name === 'team' ? '最高' : plan.name === 'pro' ? '高' : '标准'}
) } // === 用量进度条 === function UsageBar({ label, current, max }: { label: string; current: number; max: number | null }) { const pct = max ? Math.min((current / max) * 100, 100) : 0 const displayMax = max ? max.toLocaleString() : '∞' return (
{label} {current.toLocaleString()} / {displayMax}
= 90 ? '#ff4d4f' : pct >= 70 ? '#faad14' : '#863bff'} size="small" />
) } // === 主页面 === export default function Billing() { const queryClient = useQueryClient() const [payModalOpen, setPayModalOpen] = useState(false) const [selectedPlan, setSelectedPlan] = useState(null) const [payMethod, setPayMethod] = useState<'alipay' | 'wechat'>('alipay') const [payResult, setPayResult] = useState(null) const [pollingPayment, setPollingPayment] = useState(null) const { data: plans = [], isLoading: plansLoading, error: plansError, refetch } = useQuery({ queryKey: ['billing-plans'], queryFn: ({ signal }) => billingService.listPlans(signal), }) const { data: subInfo, isLoading: subLoading } = useQuery({ queryKey: ['billing-subscription'], queryFn: ({ signal }) => billingService.getSubscription(signal), }) // 支付状态轮询 const { data: paymentStatus } = useQuery({ queryKey: ['payment-status', pollingPayment], queryFn: ({ signal }) => billingService.getPaymentStatus(pollingPayment!, signal), enabled: !!pollingPayment, refetchInterval: pollingPayment ? 3000 : false, }) // 支付成功后刷新 if (paymentStatus?.status === 'succeeded' && pollingPayment) { setPollingPayment(null) setPayModalOpen(false) setPayResult(null) message.success('支付成功!计划已更新') queryClient.invalidateQueries({ queryKey: ['billing-subscription'] }) } const createPaymentMutation = useMutation({ mutationFn: (data: { plan_id: string; payment_method: 'alipay' | 'wechat' }) => billingService.createPayment(data), onSuccess: (result) => { setPayResult(result) setPollingPayment(result.payment_id) // 打开支付链接 window.open(result.pay_url, '_blank', 'width=480,height=640') }, onError: (err: Error) => message.error(err.message || '创建支付失败'), }) const handleSelectPlan = (plan: BillingPlan) => { if (plan.price_cents === 0) return setSelectedPlan(plan) setPayResult(null) setPayModalOpen(true) } const handleConfirmPay = () => { if (!selectedPlan) return createPaymentMutation.mutate({ plan_id: selectedPlan.id, payment_method: payMethod, }) } if (plansError) { return ( <> refetch()} /> ) } const currentPlanName = subInfo?.plan?.name || 'free' const usage = subInfo?.usage return (
{/* 当前计划 + 用量 */} {subInfo && usage && ( 当前用量}> {subInfo.subscription && (
订阅周期: {new Date(subInfo.subscription.current_period_start).toLocaleDateString()} — {new Date(subInfo.subscription.current_period_end).toLocaleDateString()}
)}
)} {/* 计划选择 */} 选择计划 {plansLoading ? (
) : ( {plans.map((plan) => ( ))} )} {/* 支付弹窗 */} { setPayModalOpen(false) setPollingPayment(null) setPayResult(null) }} footer={payResult ? null : undefined} onOk={handleConfirmPay} okText={createPaymentMutation.isPending ? '处理中...' : '确认支付'} confirmLoading={createPaymentMutation.isPending} > {payResult ? (
等待支付确认... 支付窗口已打开,请在新窗口完成支付。
支付金额: ¥{(payResult.amount_cents / 100).toFixed(2)}
) : (
{selectedPlan && (
¥{(selectedPlan.price_cents / 100).toFixed(0)}
/{selectedPlan.interval === 'month' ? '月' : '年'}
)} 选择支付方式 setPayMethod(e.target.value)} className="w-full" >
支付宝
推荐个人用户
微信支付
扫码支付
)}
) }