aura-web/src/pages/Pricing/PricingLogic.ts

327 lines
8.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import { useState, useEffect, useMemo } from 'react';
import { App as AntApp } from 'antd';
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
import { MembershipAPI, MembershipInfo } from '../../api/membership';
export interface PricingTier {
id: string;
name: string;
badge?: string;
description: string;
billing: {
monthly: {
price: number;
originalPrice?: number;
text: string;
subtext?: string;
discountText?: string;
discountRate?: number;
};
yearly?: {
price: number;
originalPrice?: number;
text: string;
subtext?: string;
discountText?: string;
discountRate?: number;
};
};
features: Array<{
icon: string;
text: string;
}>;
details: string[];
limits: {
tier: string;
name: string;
maxSubAccounts: number;
maxTokens: number;
maxAgents: number;
maxKBSize: number;
};
type: 'personal' | 'enterprise';
}
const TIER_WEIGHTS: Record<string, number> = {
'trial': 1,
'pro': 2,
'ultra': 3,
'ent_basic': 10,
'ent_standard': 11,
'custom': 99
};
const CUSTOM_PLAN: PricingTier = {
id: 'custom',
name: '按需定制',
badge: '专享',
description: '适合大型团队 / 行业方案',
billing: {
monthly: {
price: 0,
text: '面议',
subtext: '专属优惠与支持',
discountRate: 1
}
},
features: [
{ icon: 'Zap', text: '独立部署 / 专属网络环境' },
{ icon: 'Check', text: '统一登录 / 操作记录可追溯' },
{ icon: 'Crown', text: '行业专属插件与技能' },
{ icon: 'Infinity', text: '服务保障与 7×24 专属支持' }
],
details: [
'企业积分池 面议',
'高质量模型配额 不限量',
'子账号上限 不限量'
],
limits: {
tier: 'custom',
name: '按需定制',
maxSubAccounts: -1,
maxTokens: -1,
maxAgents: -1,
maxKBSize: -1
},
type: 'enterprise'
};
export function usePricingLogic() {
const navigate = useNavigate();
const { tierId } = useParams();
const [searchParams] = useSearchParams();
const cycle = searchParams.get('cycle') || 'monthly';
const [activeTab, setActiveTab] = useState<'personal' | 'enterprise'>('personal');
const [billingCycle, setBillingCycle] = useState<'monthly' | 'yearly'>(cycle as any);
const [membership, setMembership] = useState<MembershipInfo | null>(null);
const [plans, setPlans] = useState<PricingTier[]>([]);
const [loading, setLoading] = useState(false);
const [orderInfo, setOrderInfo] = useState<{
order_id: string;
pay_url: string;
pay_expire_at: string;
pay_is_expired: boolean;
} | null>(null);
const [payStatus, setPayStatus] = useState<'PENDING' | 'SUCCESS' | 'CLOSED' | 'FAIL' | 'EXPIRED'>('PENDING');
const [timeLeft, setTimeLeft] = useState(0);
const { message } = AntApp.useApp();
// 分类方案
const PERSONAL_TIERS = useMemo(() => plans.filter(p => p.type === 'personal'), [plans]);
const ENTERPRISE_TIERS = useMemo(() => plans.filter(p => p.type === 'enterprise'), [plans]);
// 根据 URL 参数计算当前的支付信息
const tierInfo = useMemo(() => {
if (!tierId || plans.length === 0) return null;
const tier = plans.find(t => t.id === tierId);
if (!tier) return null;
return {
tier,
duration: billingCycle === 'monthly' ? 30 : 365
};
}, [tierId, billingCycle, plans]);
// 综合支付信息
const paymentInfo = useMemo(() => {
if (!tierInfo || !orderInfo) return null;
return {
...tierInfo,
...orderInfo,
status: payStatus,
};
}, [tierInfo, orderInfo, payStatus]);
useEffect(() => {
loadMembership();
loadPlans();
}, []);
useEffect(() => {
if (activeTab === 'enterprise') {
setBillingCycle('monthly');
}
}, [activeTab]);
const loadPlans = async () => {
setLoading(true);
try {
const res = await MembershipAPI.getPlans();
// res 结构: { categories: [ { id: 'personal', plans: [...] }, ... ] }
const categories = res.categories || [];
const allPlans: PricingTier[] = [];
categories.forEach((cat: any) => {
const catPlans = (cat.plans || []).map((p: any) => ({
...p,
type: cat.id // 注入 personal 或 enterprise
}));
allPlans.push(...catPlans);
});
// 追加静态的“按需定制”方案到企业版
if (!allPlans.find(p => p.id === 'custom')) {
allPlans.push(CUSTOM_PLAN);
}
setPlans(allPlans);
} catch (e) {
console.error('Failed to load plans', e);
message.error('加载会员方案失败');
} finally {
setLoading(false);
}
};
// 当进入支付路由时,发起订阅请求
useEffect(() => {
if (tierId && tierInfo && !orderInfo) {
createOrder();
}
}, [tierId, tierInfo]);
// 倒计时逻辑
useEffect(() => {
let timer: any;
if (orderInfo && timeLeft > 0 && payStatus === 'PENDING') {
timer = setInterval(() => {
setTimeLeft((prev) => {
if (prev <= 1) {
setPayStatus('EXPIRED');
return 0;
}
return prev - 1;
});
}, 1000);
}
return () => clearInterval(timer);
}, [orderInfo, timeLeft, payStatus]);
// 轮询支付状态
useEffect(() => {
let pollTimer: any;
if (orderInfo && payStatus === 'PENDING') {
pollTimer = setInterval(async () => {
try {
const res = await MembershipAPI.queryPayStatus(orderInfo.order_id);
if (res.status === 'SUCCESS') {
setPayStatus('SUCCESS');
message.success('支付成功!');
loadMembership(); // 刷新会员信息
clearInterval(pollTimer);
} else if (res.status === 'CLOSED' || res.status === 'FAIL') {
setPayStatus(res.status);
message.error(`支付失败: ${res.status}`);
clearInterval(pollTimer);
} else if (res.pay_is_expired) {
setPayStatus('EXPIRED');
message.warning('支付已过期,请重新下单');
clearInterval(pollTimer);
}
} catch (e) {
console.error('Polling payment status failed', e);
}
}, 2000);
}
return () => clearInterval(pollTimer);
}, [orderInfo, payStatus]);
const createOrder = async () => {
if (!tierId || !tierInfo) return;
setLoading(true);
try {
const res = await MembershipAPI.subscribe({
tier: tierId,
durationDays: tierInfo.duration,
});
setOrderInfo(res);
setPayStatus('PENDING');
// 计算剩余秒数
const expireTime = new Date(res.pay_expire_at).getTime();
const now = new Date().getTime();
const diff = Math.floor((expireTime - now) / 1000);
setTimeLeft(diff > 0 ? diff : 0);
if (res.pay_is_expired) {
setPayStatus('EXPIRED');
}
} catch (e: any) {
message.error(e.response?.data?.message || '创建订单失败,请稍后重试');
navigate('/pricing');
} finally {
setLoading(false);
}
};
const loadMembership = async () => {
try {
const info = await MembershipAPI.getMe();
setMembership(info);
} catch (e) {
console.error('Failed to load membership', e);
}
};
const handleSubscribe = async (tier: PricingTier) => {
// 基础校验
if (tier.id === 'custom') {
message.info('请联系您的专属大客户经理或拨打客服热线进行面议');
return;
}
const price = billingCycle === 'yearly' && tier.billing.yearly
? tier.billing.yearly.price
: tier.billing.monthly.price;
if (tier.id === 'trial' && membership?.tier === 'trial') {
message.info('您当前已在试用期内');
return;
}
// 使用子路由跳转
navigate(`/pricing/pay/${tier.id}?cycle=${billingCycle}`);
};
const cancelPayment = async () => {
if (orderInfo) {
try {
await MembershipAPI.closePayOrder(orderInfo.order_id);
} catch (e) {
console.error('Failed to close order', e);
}
}
setOrderInfo(null);
setPayStatus('PENDING');
navigate('/pricing');
};
const getTierStatus = (targetTierId: string) => {
if (!membership) return 'none';
const currentWeight = TIER_WEIGHTS[membership.tier] || 0;
const targetWeight = TIER_WEIGHTS[targetTierId] || 0;
if (membership.tier === targetTierId) return 'current';
if (currentWeight > targetWeight) return 'included';
return 'none';
};
return {
activeTab,
setActiveTab,
billingCycle,
setBillingCycle,
membership,
loading,
handleSubscribe,
paymentInfo,
timeLeft,
cancelPayment,
PERSONAL_TIERS,
ENTERPRISE_TIERS,
getTierStatus,
};
}
export type PricingLogicOutput = ReturnType<typeof usePricingLogic>;