feat: finish wechat pay
parent
bb4c974b74
commit
b0de11aff6
|
|
@ -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),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
const CUSTOM_PLAN: PricingTier = {
|
||||
id: 'custom',
|
||||
name: '按需定制',
|
||||
badge: '专享',
|
||||
description: '适合大型团队 / 行业方案',
|
||||
billing: {
|
||||
monthly: {
|
||||
price: 0,
|
||||
text: '面议',
|
||||
subtext: '专属优惠与支持',
|
||||
discountRate: 1
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'ent_standard',
|
||||
name: '企业标准版',
|
||||
price: '40000',
|
||||
unit: '人民币/年',
|
||||
description: '适合日常协作提效',
|
||||
features: ['所有子账号共享企业积分池', '全员共享插件', '企业发票 (增值税普票)', '用量报告与成员消耗排行', '成员角色管理'],
|
||||
points: '企业积分池 80000 / 月',
|
||||
models: '高质量模型配额 100次 / 3小时',
|
||||
bonus: '子账号上限 15',
|
||||
tag: '热门',
|
||||
buttonText: '立即开通',
|
||||
type: 'enterprise',
|
||||
},
|
||||
{
|
||||
id: 'custom',
|
||||
features: [
|
||||
{ icon: 'Zap', text: '独立部署 / 专属网络环境' },
|
||||
{ icon: 'Check', text: '统一登录 / 操作记录可追溯' },
|
||||
{ icon: 'Crown', text: '行业专属插件与技能' },
|
||||
{ icon: 'Infinity', text: '服务保障与 7×24 专属支持' }
|
||||
],
|
||||
details: [
|
||||
'企业积分池 面议',
|
||||
'高质量模型配额 不限量',
|
||||
'子账号上限 不限量'
|
||||
],
|
||||
limits: {
|
||||
tier: 'custom',
|
||||
name: '按需定制',
|
||||
price: '面议',
|
||||
unit: '',
|
||||
description: '适合大型团队 / 行业方案',
|
||||
features: ['独立部署 / 专属网络环境', '统一登录 / 操作记录可追溯', '行业专属插件与技能', '服务保障与 7×24 专属支持', '专属折扣'],
|
||||
points: '企业积分池 面议',
|
||||
models: '高质量模型配额 不限量',
|
||||
bonus: '子账号上限 不限量',
|
||||
tag: '专享',
|
||||
buttonText: '联系我们',
|
||||
type: 'enterprise',
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,23 +163,25 @@ export default function PricingH5({ logic }: Props) {
|
|||
<div className="pricing-header">
|
||||
<h1 className="pricing-title">选择套餐</h1>
|
||||
|
||||
<div className="pricing-tabs">
|
||||
<div
|
||||
className={`pricing-tab-item ${activeTab === 'personal' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('personal')}
|
||||
>
|
||||
个人版
|
||||
</div>
|
||||
<div
|
||||
className={`pricing-tab-item ${activeTab === 'enterprise' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('enterprise')}
|
||||
>
|
||||
企业版
|
||||
<div className="pricing-tabs-row">
|
||||
<div className="pricing-tabs">
|
||||
<div
|
||||
className={`pricing-tab-item ${activeTab === 'personal' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('personal')}
|
||||
>
|
||||
个人版
|
||||
</div>
|
||||
<div
|
||||
className={`pricing-tab-item ${activeTab === 'enterprise' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('enterprise')}
|
||||
>
|
||||
企业版
|
||||
</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,66 +204,68 @@ 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>}
|
||||
|
||||
<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>}
|
||||
</div>
|
||||
|
||||
{billingCycle === 'yearly' && tier.yearlyPrice && (
|
||||
<div className="pricing-card-price-yearly">
|
||||
¥{tier.yearlyPrice} / 年
|
||||
{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">
|
||||
<span className="price-amount">{billing.text.split(' ')[0]}</span>
|
||||
<span className="price-unit">{billing.text.split(' ')[1] + ' ' + billing.text.split(' ')[2]}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="pricing-card-desc">{tier.description}</div>
|
||||
|
||||
<Button
|
||||
className={`pricing-card-btn btn-${tier.id.split('_')[0]}`}
|
||||
onClick={() => handleSubscribe(tier)}
|
||||
disabled={tier.id === 'trial'}
|
||||
>
|
||||
{tier.buttonText}
|
||||
</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}
|
||||
{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
|
||||
type={tier.badge === '最受欢迎' ? 'primary' : 'default'}
|
||||
className="pricing-card-btn"
|
||||
onClick={() => handleSubscribe(tier)}
|
||||
block
|
||||
disabled={getTierStatus(tier.id) !== 'none'}
|
||||
>
|
||||
{(() => {
|
||||
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>
|
||||
|
||||
<ul className="pricing-card-features">
|
||||
{tier.features.map((feature, idx) => (
|
||||
<li key={idx} className="feature-item">
|
||||
<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>
|
||||
|
||||
<ul className="pricing-card-features">
|
||||
{tier.features.map((feature, idx) => (
|
||||
<li key={idx} className="feature-item">
|
||||
<CheckOutlined className="feature-icon-check" />
|
||||
<span>{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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,35 +162,37 @@ export default function PricingWeb({ logic }: Props) {
|
|||
<div className="pricing-header">
|
||||
<h1 className="pricing-title">选择适合你工作方式的套餐</h1>
|
||||
|
||||
<div className="pricing-tabs">
|
||||
<div
|
||||
className={`pricing-tab-item ${activeTab === 'personal' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('personal')}
|
||||
>
|
||||
个人版
|
||||
</div>
|
||||
<div
|
||||
className={`pricing-tab-item ${activeTab === 'enterprise' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('enterprise')}
|
||||
>
|
||||
企业版
|
||||
<div className="pricing-tabs-row">
|
||||
<div className="pricing-tabs">
|
||||
<div
|
||||
className={`pricing-tab-item ${activeTab === 'personal' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('personal')}
|
||||
>
|
||||
个人版
|
||||
</div>
|
||||
<div
|
||||
className={`pricing-tab-item ${activeTab === 'enterprise' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('enterprise')}
|
||||
>
|
||||
企业版
|
||||
</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,66 +203,71 @@ 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>}
|
||||
|
||||
<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>}
|
||||
</div>
|
||||
|
||||
{billingCycle === 'yearly' && tier.yearlyPrice && (
|
||||
<div className="pricing-card-price-yearly">
|
||||
¥{tier.yearlyPrice} / 年
|
||||
{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">
|
||||
<span className="price-amount">{showPrice}</span>
|
||||
{billing.price > 0 && <span className="price-unit">{billing.text.split(' ')[1] + ' ' + billing.text.split(' ')[2]}</span>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{billingCycle === 'monthly' && <div className="pricing-card-desc">{tier.description}</div>}
|
||||
|
||||
<Button
|
||||
className={`pricing-card-btn btn-${tier.id.split('_')[0]}`}
|
||||
onClick={() => handleSubscribe(tier)}
|
||||
disabled={tier.id === 'trial'}
|
||||
>
|
||||
{tier.buttonText}
|
||||
</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}
|
||||
{billing.subtext && (
|
||||
<div className="pricing-card-price-subtext">
|
||||
{billing.subtext}
|
||||
{billing.discountText && <Tag color="red" style={{ marginLeft: 8 }}>{billing.discountText}</Tag>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="pricing-card-desc">{tier.description}</div>
|
||||
|
||||
<div className="pricing-card-actions">
|
||||
<Button
|
||||
type={tier.badge === '最受欢迎' ? 'primary' : 'default'}
|
||||
className="pricing-card-btn"
|
||||
onClick={() => handleSubscribe(tier)}
|
||||
size="large"
|
||||
disabled={getTierStatus(tier.id) !== 'none'}
|
||||
>
|
||||
{(() => {
|
||||
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>
|
||||
|
||||
<ul className="pricing-card-features">
|
||||
{tier.features.map((feature, idx) => (
|
||||
<li key={idx} className="feature-item">
|
||||
<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>
|
||||
|
||||
<ul className="pricing-card-features">
|
||||
{tier.features.map((feature, idx) => (
|
||||
<li key={idx} className="feature-item">
|
||||
<CheckOutlined className="feature-icon-check" />
|
||||
<span>{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,81 +68,162 @@ export default function ProfileH5({ logic }: Props) {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div className="profile-section">
|
||||
<div className="section-title">
|
||||
<h3>子账号管理</h3>
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
icon={<UserAddOutlined />}
|
||||
onClick={() => setAddModalVisible(true)}
|
||||
disabled={membership?.usage?.subAccountsCount === membership?.limits?.maxSubAccounts}
|
||||
>
|
||||
添加
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<List
|
||||
loading={loading}
|
||||
dataSource={members}
|
||||
renderItem={(record: any) => (
|
||||
<Card
|
||||
size="small"
|
||||
style={{ marginBottom: 12, borderRadius: 12 }}
|
||||
actions={[
|
||||
<DashboardOutlined key="quota" onClick={() => { setSelectedMember(record); setQuotaModalVisible(true); }} />,
|
||||
<KeyOutlined
|
||||
key="auth"
|
||||
onClick={() => {
|
||||
setSelectedMember(record);
|
||||
setSelectedAgentIds(record.authorizedAgentIds || []);
|
||||
setAuthModalVisible(true);
|
||||
}}
|
||||
/>,
|
||||
<Popconfirm key="delete" title="确定移除吗?" onConfirm={() => handleRemoveChild(record.id)}>
|
||||
<DeleteOutlined style={{ color: 'var(--color-error)' }} />
|
||||
</Popconfirm>,
|
||||
]}
|
||||
>
|
||||
<Card.Meta
|
||||
avatar={<div className="member-avatar">{record.child_name?.charAt(0)}</div>}
|
||||
title={record.child_name}
|
||||
description={
|
||||
<div>
|
||||
<div style={{ fontSize: 12 }}>{record.child_phone}</div>
|
||||
<div className="quota-display" style={{ fontSize: 12, margin: '4px 0' }}>
|
||||
{record.allocatedTokens?.toLocaleString()} Tokens
|
||||
</div>
|
||||
<div className="resource-tags">
|
||||
{record.authorizedAgentIds?.map((id: string) => {
|
||||
const agent = myAgents.find(a => a.id === id);
|
||||
return (
|
||||
<Tag
|
||||
key={id}
|
||||
closable
|
||||
onClose={() => handleRevokeAgent(record.child_user_id, [id])}
|
||||
style={{ borderRadius: 4, fontSize: 10, margin: '2px', display: 'flex', width: 'fit-content' }}
|
||||
>
|
||||
🤖 {agent?.name || '未知'}
|
||||
</Tag>
|
||||
);
|
||||
})}
|
||||
{record.authorizedAgentIds?.length > 0 && (
|
||||
<Popconfirm
|
||||
title="一键取消全部授权?"
|
||||
onConfirm={() => handleRevokeAgent(record.child_user_id, record.authorizedAgentIds)}
|
||||
>
|
||||
<Button type="link" size="small" danger style={{ fontSize: 10, padding: '0 2px', height: 'auto' }}>
|
||||
全部移除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
)}
|
||||
</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>
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
icon={<UserAddOutlined />}
|
||||
onClick={() => setAddModalVisible(true)}
|
||||
disabled={membership?.usage?.subAccountsCount === membership?.limits?.maxSubAccounts}
|
||||
>
|
||||
添加
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<List
|
||||
loading={loading}
|
||||
dataSource={members}
|
||||
renderItem={(record: any) => (
|
||||
<Card
|
||||
size="small"
|
||||
style={{ marginBottom: 12, borderRadius: 12 }}
|
||||
actions={[
|
||||
<DashboardOutlined key="quota" onClick={() => { setSelectedMember(record); setQuotaModalVisible(true); }} />,
|
||||
<KeyOutlined
|
||||
key="auth"
|
||||
onClick={() => {
|
||||
setSelectedMember(record);
|
||||
setSelectedAgentIds(record.authorizedAgentIds || []);
|
||||
setAuthModalVisible(true);
|
||||
}}
|
||||
/>,
|
||||
<Popconfirm key="delete" title="确定移除吗?" onConfirm={() => handleRemoveChild(record.id)}>
|
||||
<DeleteOutlined style={{ color: 'var(--color-error)' }} />
|
||||
</Popconfirm>,
|
||||
]}
|
||||
>
|
||||
<Card.Meta
|
||||
avatar={<div className="member-avatar">{record.child_name?.charAt(0)}</div>}
|
||||
title={record.child_name}
|
||||
description={
|
||||
<div>
|
||||
<div style={{ fontSize: 12 }}>{record.child_phone}</div>
|
||||
<div className="quota-display" style={{ fontSize: 12, margin: '4px 0' }}>
|
||||
{record.allocatedTokens?.toLocaleString()} Tokens
|
||||
</div>
|
||||
<div className="resource-tags">
|
||||
{record.authorizedAgentIds?.map((id: string) => {
|
||||
const agent = myAgents.find(a => a.id === id);
|
||||
return (
|
||||
<Tag
|
||||
key={id}
|
||||
closable
|
||||
onClose={() => handleRevokeAgent(record.child_user_id, [id])}
|
||||
style={{ borderRadius: 4, fontSize: 10, margin: '2px', display: 'flex', width: 'fit-content' }}
|
||||
>
|
||||
🤖 {agent?.name || '未知'}
|
||||
</Tag>
|
||||
);
|
||||
})}
|
||||
{record.authorizedAgentIds?.length > 0 && (
|
||||
<Popconfirm
|
||||
title="一键取消全部授权?"
|
||||
onConfirm={() => handleRevokeAgent(record.child_user_id, record.authorizedAgentIds)}
|
||||
>
|
||||
<Button type="link" size="small" danger style={{ fontSize: 10, padding: '0 2px', height: 'auto' }}>
|
||||
全部移除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
)}
|
||||
/>
|
||||
</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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
@ -133,6 +134,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">
|
||||
|
|
@ -169,26 +220,69 @@ export default function ProfileWeb({ logic }: Props) {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div className="profile-section">
|
||||
<div className="section-title">
|
||||
<h3>子账号管理</h3>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<UserAddOutlined />}
|
||||
onClick={() => setAddModalVisible(true)}
|
||||
disabled={membership?.usage?.subAccountsCount === membership?.limits?.maxSubAccounts}
|
||||
>
|
||||
添加子账号
|
||||
</Button>
|
||||
</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>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<UserAddOutlined />}
|
||||
onClick={() => setAddModalVisible(true)}
|
||||
disabled={membership?.usage?.subAccountsCount === membership?.limits?.maxSubAccounts}
|
||||
>
|
||||
添加子账号
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={members}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={false}
|
||||
className="member-list-table"
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={members}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={false}
|
||||
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>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue