feat: finish wechat pay

main
yannyang 2026-08-03 18:28:02 +08:00
parent bb4c974b74
commit b0de11aff6
8 changed files with 888 additions and 372 deletions

View File

@ -104,8 +104,53 @@ export const MembershipAPI = {
/**
* /
* @returns payUrl
* @returns URL
*/
subscribe: (payload: { tier: string; durationDays: number }) =>
api.post<ApiResponse<{ payUrl: string }>>('/membership/subscribe', payload).then(r => r.data.data),
api.post<ApiResponse<{
order_id: string;
biz_order_no: string;
pay_url: string;
pay_expire_at: string;
pay_is_expired: boolean;
message: string;
}>>('/membership/subscribe', payload).then(r => r.data.data),
/**
*
*/
queryPayStatus: (payOrderNo: string) =>
api.post<ApiResponse<{
pay_order_no: string;
status: 'PENDING' | 'SUCCESS' | 'CLOSED' | 'FAIL';
pay_expire_at: string;
pay_is_expired: boolean;
}>>('/membership/pay/query', { pay_order_no: payOrderNo }).then(r => r.data.data),
/**
*
*/
closePayOrder: (payOrderNo: string) =>
api.post<ApiResponse<any>>('/membership/pay/close', { pay_order_no: payOrderNo }).then(r => r.data),
/**
*
*/
getOrders: () =>
api.get<ApiResponse<Array<{
id: string;
pay_order_no: string;
tier: string;
amount: string;
status: string;
pay_expire_at: string;
pay_is_expired: boolean;
created_at: number;
}>>>('/membership/orders').then(r => r.data.data),
/**
*
*/
getPlans: () =>
api.get<ApiResponse<{ categories: any[] }>>('/membership/plans').then(r => r.data.data),
};

View File

@ -6,112 +6,85 @@ import { MembershipAPI, MembershipInfo } from '../../api/membership';
export interface PricingTier {
id: string;
name: string;
price: string;
yearlyPrice?: string;
yearlyMonthlyPrice?: string;
unit: string;
badge?: string;
description: string;
features: string[];
points?: string;
models?: string;
bonus?: string;
tag?: string;
buttonText: 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';
}
export const PERSONAL_TIERS: PricingTier[] = [
{
id: 'trial',
name: 'Trial',
price: '0',
unit: '人民币/月',
description: '适合首次探索和轻量使用',
features: ['首次探索和轻量任务。', '试用期后,可以升级至更高级版本。'],
points: '每日登录积分 10',
bonus: 'Aura Work: 7天试用积分',
tag: '体验',
buttonText: '当前订阅',
type: 'personal',
},
{
id: 'pro',
name: 'Pro',
price: '139',
yearlyPrice: '1390',
yearlyMonthlyPrice: '116',
unit: '人民币/月',
description: '每周工作与日常执行',
features: ['适合每周轻量调研、数据分析和任务跟进。', '为稳定的每周产出而设,无需顾虑额外消耗', '限时优惠:订阅用户可享受更多专属权益。'],
points: '每日登录积分 30',
models: '高质量模型 50次 / 3小时',
bonus: '会员积分 4000 / 月',
tag: '热门',
buttonText: '开始',
type: 'personal',
},
{
id: 'ultra',
name: 'Ultra',
price: '1399',
yearlyPrice: '13990',
yearlyMonthlyPrice: '1116',
unit: '人民币/月',
description: '超大项目与更高配置',
features: ['适合高强度任务与重度使用场景', '面向需要更高积分与更大灵活性的资源用户', '限时优惠:订阅用户可享受更多专属权益。', '个人套餐顶配版,包含全部能力'],
points: '每日登录积分 100',
models: '无限制使用高质量模型',
bonus: '会员积分 40000 / 月',
tag: '旗舰',
buttonText: '开始',
type: 'personal',
},
];
const TIER_WEIGHTS: Record<string, number> = {
'trial': 1,
'pro': 2,
'ultra': 3,
'ent_basic': 10,
'ent_standard': 11,
'custom': 99
};
export const ENTERPRISE_TIERS: PricingTier[] = [
{
id: 'ent_basic',
name: '企业入门版',
price: '20000',
unit: '人民币/年',
description: '适合快速起步的小团队',
features: ['所有子账号共享企业积分池', '全员共享插件', '企业发票 (增值税普票)', '用量报告与成员消耗排行'],
points: '企业积分池 35000 / 月',
models: '高质量模型配额 100次 / 3小时',
bonus: '子账号上限 5',
tag: '基础',
buttonText: '立即开通',
type: 'enterprise',
},
{
id: 'ent_standard',
name: '企业标准版',
price: '40000',
unit: '人民币/年',
description: '适合日常协作提效',
features: ['所有子账号共享企业积分池', '全员共享插件', '企业发票 (增值税普票)', '用量报告与成员消耗排行', '成员角色管理'],
points: '企业积分池 80000 / 月',
models: '高质量模型配额 100次 / 3小时',
bonus: '子账号上限 15',
tag: '热门',
buttonText: '立即开通',
type: 'enterprise',
},
{
const CUSTOM_PLAN: PricingTier = {
id: 'custom',
name: '按需定制',
price: '面议',
unit: '',
badge: '专享',
description: '适合大型团队 / 行业方案',
features: ['独立部署 / 专属网络环境', '统一登录 / 操作记录可追溯', '行业专属插件与技能', '服务保障与 7×24 专属支持', '专属折扣'],
points: '企业积分池 面议',
models: '高质量模型配额 不限量',
bonus: '子账号上限 不限量',
tag: '专享',
buttonText: '联系我们',
type: 'enterprise',
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();
@ -122,38 +95,164 @@ export function usePricingLogic() {
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 [timeLeft, setTimeLeft] = useState(600); // 10 minutes in seconds
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 paymentInfo = useMemo(() => {
if (!tierId) return null;
const allTiers = [...PERSONAL_TIERS, ...ENTERPRISE_TIERS];
const tier = allTiers.find(t => t.id === tierId);
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: cycle === 'monthly' ? 30 : 365
duration: billingCycle === 'monthly' ? 30 : 365
};
}, [tierId, cycle]);
}, [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 (paymentInfo && timeLeft > 0) {
if (orderInfo && timeLeft > 0 && payStatus === 'PENDING') {
timer = setInterval(() => {
setTimeLeft((prev) => prev - 1);
setTimeLeft((prev) => {
if (prev <= 1) {
setPayStatus('EXPIRED');
return 0;
}
return prev - 1;
});
}, 1000);
} else if (timeLeft === 0) {
navigate('/pricing', { replace: true });
message.error('支付超时,请重新发起');
}
return () => clearInterval(timer);
}, [paymentInfo, timeLeft, navigate]);
}, [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 {
@ -165,17 +264,48 @@ export function usePricingLogic() {
};
const handleSubscribe = async (tier: PricingTier) => {
if (tier.price === '面议' || tier.id === 'trial') return;
// 基础校验
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}`);
setTimeLeft(600);
};
const cancelPayment = () => {
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,
@ -189,6 +319,7 @@ export function usePricingLogic() {
cancelPayment,
PERSONAL_TIERS,
ENTERPRISE_TIERS,
getTierStatus,
};
}

View File

@ -1,5 +1,16 @@
import { CheckOutlined, ArrowLeftOutlined, LoadingOutlined } from '@ant-design/icons';
import { Button, Spin, QRCode, Divider } from 'antd';
import {
CheckOutlined,
ArrowLeftOutlined,
LoadingOutlined,
CheckCircleOutlined,
CloseCircleOutlined,
ExclamationCircleOutlined,
ThunderboltOutlined,
CrownOutlined,
TeamOutlined,
RadiusSettingOutlined
} from '@ant-design/icons';
import { Button, Spin, QRCode, Divider, Result, Tag } from 'antd';
import type { PricingLogicOutput } from '../PricingLogic';
import '../styles/pricing.css';
@ -7,8 +18,19 @@ interface Props {
logic: PricingLogicOutput;
}
const FeatureIcon = ({ name }: { name: string }) => {
switch (name) {
case 'Check': return <CheckOutlined className="feature-icon-check" />;
case 'Zap': return <ThunderboltOutlined className="feature-icon-zap" />;
case 'Crown': return <CrownOutlined className="feature-icon-crown" />;
case 'Users': return <TeamOutlined className="feature-icon-users" />;
case 'Infinity': return <RadiusSettingOutlined className="feature-icon-infinity" />;
default: return <CheckOutlined className="feature-icon-check" />;
}
};
const DiamondIcon = () => (
<span style={{ fontSize: 16, color: 'var(--color-text-tertiary)' }}></span>
<span style={{ fontSize: 16, color: 'var(--color-text-tertiary)', marginRight: 8 }}></span>
);
export default function PricingH5({ logic }: Props) {
@ -17,6 +39,7 @@ export default function PricingH5({ logic }: Props) {
setActiveTab,
billingCycle,
setBillingCycle,
membership,
loading,
handleSubscribe,
paymentInfo,
@ -24,13 +47,53 @@ export default function PricingH5({ logic }: Props) {
cancelPayment,
PERSONAL_TIERS,
ENTERPRISE_TIERS,
getTierStatus,
} = logic;
if (paymentInfo) {
const { tier, duration } = paymentInfo;
const { tier, duration, status, pay_url } = paymentInfo;
const minutes = Math.floor(timeLeft / 60);
const seconds = timeLeft % 60;
const price = duration === 30 ? tier.price : (tier.yearlyPrice || tier.price);
const billing = billingCycle === 'yearly' && tier.billing.yearly ? tier.billing.yearly : tier.billing.monthly;
const price = billing.price;
if (status === 'SUCCESS') {
return (
<div className="pricing-page pricing-page-h5">
<div className="payment-container">
<Result
status="success"
title="支付成功"
subTitle={`${tier.name} 已开通`}
extra={[
<Button type="primary" key="back" block onClick={cancelPayment}>
</Button>,
]}
/>
</div>
</div>
);
}
if (status === 'CLOSED' || status === 'FAIL' || status === 'EXPIRED') {
return (
<div className="pricing-page pricing-page-h5">
<div className="payment-container">
<Result
status="error"
title={status === 'EXPIRED' ? '支付已过期' : '支付失败'}
subTitle={status === 'EXPIRED' ? '请重新发起下单' : '请重试'}
extra={[
<Button type="primary" key="retry" block onClick={cancelPayment}>
</Button>,
]}
/>
</div>
</div>
);
}
return (
<div className="pricing-page pricing-page-h5">
@ -69,7 +132,7 @@ export default function PricingH5({ logic }: Props) {
<div className="payment-qr-card">
<div className="qr-wrapper">
<QRCode value="https://aura-ai.com/mock-pay" size={180} />
<QRCode value={pay_url} size={180} />
<div className="qr-status">
<LoadingOutlined /> ...
</div>
@ -100,6 +163,7 @@ export default function PricingH5({ logic }: Props) {
<div className="pricing-header">
<h1 className="pricing-title"></h1>
<div className="pricing-tabs-row">
<div className="pricing-tabs">
<div
className={`pricing-tab-item ${activeTab === 'personal' ? 'active' : ''}`}
@ -114,9 +178,10 @@ export default function PricingH5({ logic }: Props) {
</div>
</div>
</div>
{activeTab === 'personal' && (
<div>
<div className="billing-toggle-row">
<div className="billing-toggle">
<div
className={`billing-toggle-item ${billingCycle === 'monthly' ? 'active' : ''}`}
@ -128,7 +193,7 @@ export default function PricingH5({ logic }: Props) {
className={`billing-toggle-item ${billingCycle === 'yearly' ? 'active' : ''}`}
onClick={() => setBillingCycle('yearly')}
>
<span className="discount-tag">-2</span>
<span className="discount-tag"></span>
</div>
</div>
</div>
@ -139,67 +204,69 @@ export default function PricingH5({ logic }: Props) {
<div style={{ padding: '60px 0' }}><Spin size="large" /></div>
) : (
<div className="pricing-grid">
{tiers.map((tier) => (
<div key={tier.id} className={`pricing-card ${tier.tag === '热门' ? 'highlight' : ''}`}>
{tier.tag && <div className="pricing-card-tag">{tier.tag}</div>}
{tiers.map((tier) => {
const billing = billingCycle === 'yearly' && tier.billing.yearly ? tier.billing.yearly : tier.billing.monthly;
return (
<div key={tier.id} className={`pricing-card ${tier.badge === '最受欢迎' ? 'highlight' : ''}`}>
{tier.badge && <div className="pricing-card-tag">{tier.badge}</div>}
<div className="pricing-card-name">{tier.name}</div>
<div className="pricing-card-price">
{tier.price !== '面议' && <span className="price-symbol">¥</span>}
<span className="price-amount">
{billingCycle === 'yearly' && tier.yearlyMonthlyPrice
? tier.yearlyMonthlyPrice
: tier.price}
</span>
{tier.unit && <span className="price-unit">{tier.unit}</span>}
<span className="price-amount">{billing.text.split(' ')[0]}</span>
<span className="price-unit">{billing.text.split(' ')[1] + ' ' + billing.text.split(' ')[2]}</span>
</div>
{billingCycle === 'yearly' && tier.yearlyPrice && (
<div className="pricing-card-price-yearly">
¥{tier.yearlyPrice} /
{billing.subtext && (
<div className="pricing-card-price-subtext">
{billing.subtext}
{billing.discountText && <Tag color="red" style={{ marginLeft: 4 }}>{billing.discountText}</Tag>}
</div>
)}
<div className="pricing-card-desc">{tier.description}</div>
<Button
className={`pricing-card-btn btn-${tier.id.split('_')[0]}`}
type={tier.badge === '最受欢迎' ? 'primary' : 'default'}
className="pricing-card-btn"
onClick={() => handleSubscribe(tier)}
disabled={tier.id === 'trial'}
block
disabled={getTierStatus(tier.id) !== 'none'}
>
{tier.buttonText}
{(() => {
const status = getTierStatus(tier.id);
if (status === 'current') return '当前订阅';
if (status === 'included') return '已包含';
if (tier.id === 'trial') return '立即开启';
if (tier.id === 'custom') return '联系客服';
return '立即订阅';
})()}
</Button>
<div className="pricing-card-metrics">
{tier.points && (
<div className="metric-item">
<DiamondIcon /> {tier.points}
</div>
)}
{tier.bonus && (
<div className="metric-item">
<DiamondIcon /> {tier.bonus}
</div>
)}
{tier.models && (
<div className="metric-item">
<DiamondIcon /> {tier.models}
</div>
)}
</div>
<ul className="pricing-card-features">
{tier.features.map((feature, idx) => (
<li key={idx} className="feature-item">
<CheckOutlined className="feature-icon-check" />
<span>{feature}</span>
<FeatureIcon name={feature.icon} />
<span>{feature.text}</span>
</li>
))}
</ul>
<Divider style={{ margin: '12px 0' }} />
<div className="pricing-card-details">
{tier.details.map((detail, idx) => (
<div key={idx} className="detail-item">
<DiamondIcon />
<span>{detail}</span>
</div>
))}
</div>
</div>
);
})}
</div>
)}
</div>
);

View File

@ -1,22 +1,44 @@
import { CheckOutlined, ArrowLeftOutlined, LoadingOutlined } from '@ant-design/icons';
import { Button, Spin, QRCode, Space, Divider } from 'antd';
import type { PricingLogicOutput, PricingTier } from '../PricingLogic';
import {
CheckOutlined,
ArrowLeftOutlined,
LoadingOutlined,
ThunderboltOutlined,
CrownOutlined,
TeamOutlined,
RadiusSettingOutlined
} from '@ant-design/icons';
import { Button, Spin, QRCode, Divider, Result, Tag } from 'antd';
import type { PricingLogicOutput } from '../PricingLogic';
import '../styles/pricing.css';
import { useNavigate } from 'react-router-dom';
interface Props {
logic: PricingLogicOutput;
}
const FeatureIcon = ({ name }: { name: string }) => {
switch (name) {
case 'Check': return <CheckOutlined className="feature-icon-check" />;
case 'Zap': return <ThunderboltOutlined className="feature-icon-zap" />;
case 'Crown': return <CrownOutlined className="feature-icon-crown" />;
case 'Users': return <TeamOutlined className="feature-icon-users" />;
case 'Infinity': return <RadiusSettingOutlined className="feature-icon-infinity" />;
default: return <CheckOutlined className="feature-icon-check" />;
}
};
const DiamondIcon = () => (
<span style={{ fontSize: 16, color: 'var(--color-text-tertiary)' }}></span>
<span style={{ fontSize: 16, color: 'var(--color-text-tertiary)', marginRight: 8 }}></span>
);
export default function PricingWeb({ logic }: Props) {
const navigate = useNavigate();
const {
activeTab,
setActiveTab,
billingCycle,
setBillingCycle,
membership,
loading,
handleSubscribe,
paymentInfo,
@ -24,13 +46,53 @@ export default function PricingWeb({ logic }: Props) {
cancelPayment,
PERSONAL_TIERS,
ENTERPRISE_TIERS,
getTierStatus,
} = logic;
if (paymentInfo) {
const { tier, duration } = paymentInfo;
const { tier, duration, status, pay_url } = paymentInfo;
const minutes = Math.floor(timeLeft / 60);
const seconds = timeLeft % 60;
const price = duration === 30 ? tier.price : (tier.yearlyPrice || tier.price);
const billing = billingCycle === 'yearly' && tier.billing.yearly ? tier.billing.yearly : tier.billing.monthly;
const price = billing.price;
if (status === 'SUCCESS') {
return (
<div className="pricing-page">
<div className="payment-container">
<Result
status="success"
title="支付成功"
subTitle={`您已成功订阅 ${tier.name},会员权限已即时生效。`}
extra={[
<Button type="primary" key="back" onClick={() => navigate('/chat')}>
</Button>,
]}
/>
</div>
</div>
);
}
if (status === 'CLOSED' || status === 'FAIL' || status === 'EXPIRED') {
return (
<div className="pricing-page">
<div className="payment-container">
<Result
status="error"
title={status === 'EXPIRED' ? '支付已过期' : '支付失败'}
subTitle={status === 'EXPIRED' ? '订单已超时,请重新发起订阅。' : '支付过程中遇到问题,请重试。'}
extra={[
<Button type="primary" key="retry" onClick={cancelPayment}>
</Button>,
]}
/>
</div>
</div>
);
}
return (
<div className="pricing-page">
@ -69,7 +131,7 @@ export default function PricingWeb({ logic }: Props) {
<div className="payment-qr-card">
<div className="qr-wrapper">
<QRCode value="https://aura-ai.com/mock-pay" size={200} />
<QRCode value={pay_url} size={200} />
<div className="qr-status">
<LoadingOutlined /> ...
</div>
@ -100,6 +162,7 @@ export default function PricingWeb({ logic }: Props) {
<div className="pricing-header">
<h1 className="pricing-title"></h1>
<div className="pricing-tabs-row">
<div className="pricing-tabs">
<div
className={`pricing-tab-item ${activeTab === 'personal' ? 'active' : ''}`}
@ -114,21 +177,22 @@ export default function PricingWeb({ logic }: Props) {
</div>
</div>
</div>
{activeTab === 'personal' && (
<div>
<div className="billing-toggle-row">
<div className="billing-toggle">
<div
className={`billing-toggle-item ${billingCycle === 'monthly' ? 'active' : ''}`}
onClick={() => setBillingCycle('monthly')}
>
</div>
<div
className={`billing-toggle-item ${billingCycle === 'yearly' ? 'active' : ''}`}
onClick={() => setBillingCycle('yearly')}
>
<span className="discount-tag"></span>
<span className="discount-tag"></span>
</div>
</div>
</div>
@ -139,67 +203,72 @@ export default function PricingWeb({ logic }: Props) {
<div style={{ padding: '100px 0' }}><Spin size="large" /></div>
) : (
<div className="pricing-grid">
{tiers.map((tier) => (
<div key={tier.id} className={`pricing-card ${tier.tag === '热门' ? 'highlight' : ''}`}>
{tier.tag && <div className="pricing-card-tag">{tier.tag}</div>}
{tiers.map((tier) => {
const billing = billingCycle === 'yearly' && tier.billing.yearly ? tier.billing.yearly : tier.billing.monthly;
const showPrice = tier.type === 'enterprise' ? (billing.price > 0 ? billing.price : '面议') : (billing.price > 0 ? billing.price : '免费');
return (
<div key={tier.id} className={`pricing-card ${['pro', 'ent_standard'].includes(tier.id) ? 'highlight' : ''}`}>
{tier.badge && <div className="pricing-card-tag">{tier.badge}</div>}
<div className="pricing-card-name">{tier.name}</div>
<div className="pricing-card-price">
{tier.price !== '面议' && <span className="price-symbol">¥</span>}
<span className="price-amount">
{billingCycle === 'yearly' && tier.yearlyMonthlyPrice
? tier.yearlyMonthlyPrice
: tier.price}
</span>
{tier.unit && <span className="price-unit">{tier.unit}</span>}
<span className="price-amount">{showPrice}</span>
{billing.price > 0 && <span className="price-unit">{billing.text.split(' ')[1] + ' ' + billing.text.split(' ')[2]}</span>}
</div>
{billingCycle === 'yearly' && tier.yearlyPrice && (
<div className="pricing-card-price-yearly">
¥{tier.yearlyPrice} /
{billing.subtext && (
<div className="pricing-card-price-subtext">
{billing.subtext}
{billing.discountText && <Tag color="red" style={{ marginLeft: 8 }}>{billing.discountText}</Tag>}
</div>
)}
{billingCycle === 'monthly' && <div className="pricing-card-desc">{tier.description}</div>}
<div className="pricing-card-desc">{tier.description}</div>
<div className="pricing-card-actions">
<Button
className={`pricing-card-btn btn-${tier.id.split('_')[0]}`}
type={tier.badge === '最受欢迎' ? 'primary' : 'default'}
className="pricing-card-btn"
onClick={() => handleSubscribe(tier)}
disabled={tier.id === 'trial'}
size="large"
disabled={getTierStatus(tier.id) !== 'none'}
>
{tier.buttonText}
{(() => {
const status = getTierStatus(tier.id);
if (status === 'current') return '当前订阅';
if (status === 'included') return '已包含';
if (tier.id === 'trial') return '立即开启';
if (tier.id === 'custom') return '联系客服';
return '立即订阅';
})()}
</Button>
<div className="pricing-card-metrics">
{tier.points && (
<div className="metric-item">
<DiamondIcon /> {tier.points}
</div>
)}
{tier.bonus && (
<div className="metric-item">
<DiamondIcon /> {tier.bonus}
</div>
)}
{tier.models && (
<div className="metric-item">
<DiamondIcon /> {tier.models}
</div>
)}
</div>
<ul className="pricing-card-features">
{tier.features.map((feature, idx) => (
<li key={idx} className="feature-item">
<CheckOutlined className="feature-icon-check" />
<span>{feature}</span>
<FeatureIcon name={feature.icon} />
<span>{feature.text}</span>
</li>
))}
</ul>
<Divider style={{ margin: '16px 0' }} />
<div className="pricing-card-details">
{tier.details.map((detail, idx) => (
<div key={idx} className="detail-item">
<DiamondIcon />
<span>{detail}</span>
</div>
))}
</div>
</div>
);
})}
</div>
)}
</div>
);

View File

@ -150,23 +150,41 @@
border-radius: 24px;
font-weight: 600;
margin-bottom: 24px;
border: none;
border: 1px solid transparent;
cursor: pointer;
transition: all 0.3s;
background: #1f2937;
color: #fff;
}
.pricing-card.highlight .pricing-card-btn {
background: var(--color-brand);
.pricing-card-btn:hover:not(:disabled) {
background: #374151 !important;
color: #fff !important;
border-color: #374151 !important;
}
.btn-trial {
background: var(--color-fill-secondary);
color: var(--color-text-tertiary);
cursor: not-allowed;
.pricing-card.highlight .pricing-card-btn {
background: var(--color-brand);
border-color: var(--color-brand);
}
.pricing-card.highlight .pricing-card-btn:hover:not(:disabled) {
background: #059669 !important;
border-color: #059669 !important;
color: #fff !important;
}
.pricing-card-btn:disabled,
.pricing-card-btn.ant-btn-disabled {
background: #f3f4f6 !important;
color: #9ca3af !important;
border-color: #e5e7eb !important;
cursor: not-allowed !important;
opacity: 1 !important;
pointer-events: none;
}
.pricing-card-features {
margin-top: 24px;
padding-top: 24px;

View File

@ -9,6 +9,7 @@ export function useProfileLogic() {
const [membership, setMembership] = useState<MembershipInfo | null>(null);
const [members, setMembers] = useState<ChildMember[]>([]);
const [myAgents, setMyAgents] = useState<Agent[]>([]);
const [orders, setOrders] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const { message, modal } = AntApp.useApp();
@ -50,6 +51,14 @@ export function useProfileLogic() {
console.error('Failed to fetch agents', e);
}
}
// 4. 获取订单记录
try {
const orderList = await MembershipAPI.getOrders();
setOrders(orderList);
} catch (e) {
console.error('Failed to fetch orders', e);
}
} catch (e) {
console.error('Failed to init profile', e);
} finally {
@ -125,6 +134,7 @@ export function useProfileLogic() {
membership,
members,
myAgents,
orders,
loading,
init,
handleAddChild,

View File

@ -1,6 +1,6 @@
import { useState } from 'react';
import { Button, Modal, Input, Select, Space, Tag, InputNumber, Popconfirm, List, Card, Checkbox, Divider } from 'antd';
import { UserAddOutlined, DeleteOutlined, KeyOutlined, DashboardOutlined } from '@ant-design/icons';
import { Button, Modal, Input, Select, Space, Tag, InputNumber, Popconfirm, List, Card, Checkbox, Divider, Tabs } from 'antd';
import { UserAddOutlined, DeleteOutlined, KeyOutlined, DashboardOutlined, HistoryOutlined, TeamOutlined } from '@ant-design/icons';
import type { ProfileLogicOutput } from '../ProfileLogic';
import '../styles/profile.css';
@ -14,6 +14,7 @@ export default function ProfileH5({ logic }: Props) {
membership,
members,
myAgents,
orders,
loading,
handleAddChild,
handleRemoveChild,
@ -67,6 +68,20 @@ export default function ProfileH5({ logic }: Props) {
</div>
</div>
<div className="profile-content-tabs">
<Tabs
defaultActiveKey="members"
centered
items={[
{
key: 'members',
label: (
<span>
<TeamOutlined />
</span>
),
children: (
<div className="profile-section">
<div className="section-title">
<h3></h3>
@ -144,6 +159,73 @@ export default function ProfileH5({ logic }: Props) {
)}
/>
</div>
),
},
{
key: 'orders',
label: (
<span>
<HistoryOutlined />
</span>
),
children: (
<div className="profile-section">
<div className="section-title">
<h3></h3>
</div>
<List
loading={loading}
dataSource={orders}
renderItem={(record: any) => {
const tierNames: Record<string, string> = {
trial: '个人试用版',
pro: '个人专业版',
ultra: '个人旗舰版',
ent_basic: '企业入门版',
ent_standard: '企业标准版'
};
return (
<Card size="small" style={{ marginBottom: 12, borderRadius: 12 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<div>
<div style={{ fontWeight: 600 }}>{tierNames[record.tier] || record.tier}</div>
<div style={{ fontSize: 12, color: 'var(--color-text-tertiary)', marginTop: 4 }}>
: {record.pay_order_no}
</div>
<div style={{ fontSize: 12, color: 'var(--color-text-tertiary)', marginTop: 2 }}>
{new Date(record.created_at).toLocaleString()}
</div>
</div>
<div style={{ textAlign: 'right' }}>
<div style={{ fontWeight: 600, color: 'var(--color-primary)' }}>¥{record.amount}</div>
{(() => {
const statusMap: Record<string, { color: string; text: string }> = {
PENDING: { color: 'processing', text: '待支付' },
SUCCESS: { color: 'success', text: '已支付' },
FAIL: { color: 'error', text: '失败' },
CLOSED: { color: 'default', text: '关闭' }
};
const config = statusMap[record.status] || { color: 'default', text: record.status };
return (
<Tag color={config.color} style={{ marginTop: 8, marginRight: 0 }}>
{config.text}
</Tag>
);
})()}
</div>
</div>
</Card>
);
}}
/>
</div>
),
},
]}
/>
</div>
{/* Modals are shared with Web version, but could be optimized for H5 if needed */}
<Modal

View File

@ -1,6 +1,6 @@
import { useState } from 'react';
import { Button, Table, Modal, Input, Select, Space, Tag, InputNumber, Popconfirm, Checkbox, Divider } from 'antd';
import { UserAddOutlined, DeleteOutlined, KeyOutlined, DashboardOutlined } from '@ant-design/icons';
import { Button, Table, Modal, Input, Select, Space, Tag, InputNumber, Popconfirm, Checkbox, Divider, Tabs } from 'antd';
import { UserAddOutlined, DeleteOutlined, KeyOutlined, DashboardOutlined, HistoryOutlined, TeamOutlined } from '@ant-design/icons';
import type { ProfileLogicOutput } from '../ProfileLogic';
import '../styles/profile.css';
@ -14,6 +14,7 @@ export default function ProfileWeb({ logic }: Props) {
membership,
members,
myAgents,
orders,
loading,
handleAddChild,
handleRemoveChild,
@ -134,6 +135,56 @@ export default function ProfileWeb({ logic }: Props) {
},
];
const orderColumns = [
{
title: '订单编号',
dataIndex: 'id',
key: 'id',
},
{
title: '订阅方案',
dataIndex: 'tier',
key: 'tier',
render: (tier: string) => {
const tierNames: Record<string, string> = {
trial: '个人试用版',
pro: '个人专业版',
ultra: '个人旗舰版',
ent_basic: '企业入门版',
ent_standard: '企业标准版'
};
return tierNames[tier] || tier;
}
},
{
title: '金额',
dataIndex: 'amount',
key: 'amount',
render: (amount: string) => `¥${amount}`,
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
render: (status: string) => {
const statusMap: Record<string, { color: string; text: string }> = {
PENDING: { color: 'processing', text: '待支付' },
SUCCESS: { color: 'success', text: '支付成功' },
FAIL: { color: 'error', text: '支付失败' },
CLOSED: { color: 'default', text: '已关闭' }
};
const config = statusMap[status] || { color: 'default', text: status };
return <Tag color={config.color}>{config.text}</Tag>;
},
},
{
title: '下单时间',
dataIndex: 'created_at',
key: 'created_at',
render: (val: number) => val ? new Date(val).toLocaleString() : '-',
},
];
return (
<div className="profile-page">
<div className="profile-header-card">
@ -169,6 +220,19 @@ export default function ProfileWeb({ logic }: Props) {
</div>
</div>
<div className="profile-content-tabs">
<Tabs
defaultActiveKey="members"
items={[
{
key: 'members',
label: (
<span>
<TeamOutlined />
</span>
),
children: (
<div className="profile-section">
<div className="section-title">
<h3></h3>
@ -191,6 +255,36 @@ export default function ProfileWeb({ logic }: Props) {
className="member-list-table"
/>
</div>
),
},
{
key: 'orders',
label: (
<span>
<HistoryOutlined />
</span>
),
children: (
<div className="profile-section">
<div className="section-title">
<h3></h3>
</div>
<Table
columns={orderColumns}
dataSource={orders}
rowKey="id"
loading={loading}
pagination={{ pageSize: 10 }}
className="order-list-table"
/>
</div>
),
},
]}
/>
</div>
{/* Add Member Modal */}
<Modal