353 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import CloseSvg from '@/components/CustomIcon/icons/close.svg';
import { getSubscription } from '@/services/user/portal';
import { useQuery } from '@tanstack/react-query';
import { Dialog, DialogContent, DialogTitle } from '@workspace/ui/components/dialog';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
import Image from 'next/image';
import { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
// 定义数据类型
interface SubscriptionData {
id: string;
name: string;
price: number;
originalPrice: number;
duration: string;
features: {
traffic: string;
duration: string;
onlineIPs: string;
connections: string;
bandwidth: string;
nodes: string;
stability: number; // 星级 1-5
};
}
// 套餐卡片组件
const PlanCard = ({ plan, isSelected }: { plan: SubscriptionData; isSelected?: boolean }) => {
return (
<div
className={`relative w-full max-w-[345px] cursor-pointer rounded-[20px] border border-[#D9D9D9] bg-white p-4 transition-all duration-300 sm:p-6 md:p-8 ${
isSelected
? 'border-[#0F2C53] shadow-[0px_0px_52.6px_1px_rgba(15,44,83,0.05)]'
: 'shadow-[0px_0px_52.6px_1px_rgba(15,44,83,0.05)] hover:border-[#0F2C53]'
} `}
>
{/* 套餐名称 */}
<h3 className='mb-4 text-left text-sm font-normal sm:mb-6 sm:text-base'>{plan.name}</h3>
{/* 价格区域 */}
<div className='mb-6 sm:mb-8'>
<div className='mb-2 flex items-baseline gap-2'>
<span className='text-lg font-bold leading-[1.125em] text-[#666666] line-through sm:text-xl md:text-[24px]'>
¥{plan.originalPrice}
</span>
<span className='text-lg font-bold leading-[1.125em] text-[#091B33] sm:text-xl md:text-[24px]'>
${plan.price}
</span>
<span className='text-sm font-normal leading-[1.8em] text-[#4D4D4D] sm:text-[15px]'>
/
</span>
</div>
<p className='text-left text-[10px] font-normal text-black'>8</p>
</div>
{/* 订阅按钮 */}
<button
className={`h-8 w-full rounded-full bg-[#0F2C53] text-xs font-medium leading-[1.9285714285714286em] text-white shadow-md transition-all duration-300 hover:bg-[#0A2C47] sm:h-10 sm:text-sm md:h-[40px] md:text-[14px]`}
>
</button>
{/* 功能列表 */}
<div className='mt-6 space-y-0 sm:mt-8'>
<div className='flex items-start justify-between py-1'>
<span className='text-xs font-light leading-[1.8461538461538463em] text-black sm:text-[13px]'>
</span>
<span className='text-right text-xs font-light leading-[1.8461538461538463em] text-black sm:text-[13px]'>
{plan.features.traffic}
</span>
</div>
<div className='flex items-start justify-between py-1'>
<span className='text-xs font-light leading-[1.8461538461538463em] text-black sm:text-[13px]'>
</span>
<span className='text-right text-xs font-light leading-[1.8461538461538463em] text-black sm:text-[13px]'>
{plan.features.duration}
</span>
</div>
<div className='flex items-start justify-between py-1'>
<span className='text-xs font-light leading-[1.8461538461538463em] text-black sm:text-[13px]'>
线IP
</span>
<span className='text-right text-xs font-light leading-[1.8461538461538463em] text-black sm:text-[13px]'>
{plan.features.onlineIPs}
</span>
</div>
<div className='flex items-start justify-between py-1'>
<span className='text-xs font-light leading-[1.8461538461538463em] text-black sm:text-[13px]'>
线
</span>
<span className='text-right text-xs font-light leading-[1.8461538461538463em] text-black sm:text-[13px]'>
{plan.features.connections}
</span>
</div>
<div className='flex items-start justify-between py-1'>
<span className='text-xs font-light leading-[1.8461538461538463em] text-black sm:text-[13px]'>
</span>
<span className='text-right text-xs font-light leading-[1.8461538461538463em] text-black sm:text-[13px]'>
{plan.features.bandwidth}
</span>
</div>
<div className='flex items-start justify-between py-1'>
<span className='text-xs font-light leading-[1.8461538461538463em] text-black sm:text-[13px]'>
</span>
<span className='text-right text-xs font-light leading-[1.8461538461538463em] text-black sm:text-[13px]'>
{plan.features.nodes}
</span>
</div>
<div className='flex items-start justify-between py-1'>
<span className='text-xs font-light leading-[1.8461538461538463em] text-black sm:text-[13px]'>
:
</span>
<div className='text-right text-xs font-light leading-[1.8461538461538463em] text-black sm:text-[13px]'>
{Array.from({ length: plan.features.stability }, (_, i) => (
<span key={i} className='text-black'>
</span>
))}
</div>
</div>
</div>
</div>
);
};
export interface OfferDialogRef {
show: () => void;
hide: () => void;
}
const OfferDialog = forwardRef<OfferDialogRef>((props, ref) => {
const [open, setOpen] = useState(false);
// 使用 useQuery 来管理请求
const { data, isLoading, error, refetch } = useQuery({
queryKey: ['subscription'],
queryFn: async () => {
try {
const response = await getSubscription({ skipErrorHandler: true });
// 确保返回有效的数组,避免 undefined
const list = response.data?.data?.list || [];
return list as SubscriptionData[];
} catch (err) {
// 自定义错误处理
console.error('获取订阅数据失败:', err);
// 返回空数组而不是抛出错误,避免 queryFn 返回 undefined
return [] as SubscriptionData[];
}
},
enabled: false, // 初始不执行,手动控制
retry: 1, // 失败时重试1次
});
// 监听对话框打开状态,触发请求
useEffect(() => {
if (open) {
refetch(); // 对话框打开时重新获取数据
}
}, [open, refetch]);
useImperativeHandle(ref, () => ({
show: () => setOpen(true),
hide: () => setOpen(false),
}));
// 处理数据
const processedData =
data?.map((item) => ({
...item,
displayPrice: `¥${item.price}`,
displayDuration: item.duration === 'year' ? '年付' : '月付',
})) || [];
// 如果没有数据,使用模拟数据
const mockData: SubscriptionData[] = [
{
id: '1',
name: 'Basic Plan',
price: 8,
originalPrice: 10,
duration: 'year',
isPopular: false,
features: {
traffic: '140G',
duration: '30天',
onlineIPs: '3个',
connections: '300',
bandwidth: '200Mbps',
nodes: '15个',
stability: 3,
},
},
{
id: '2',
name: 'Standard Plan',
price: 24,
originalPrice: 30,
duration: 'year',
features: {
traffic: '160G',
duration: '30天',
onlineIPs: '3个',
connections: '300',
bandwidth: '300Mbps',
nodes: '15个',
stability: 4,
},
},
{
id: '3',
name: 'Pro Plan',
price: 48,
originalPrice: 60,
duration: 'year',
isPopular: false,
features: {
traffic: '180G',
duration: '30天',
onlineIPs: '3个',
connections: '300',
bandwidth: '500Mbps',
nodes: '29个',
stability: 5,
},
},
];
// 使用真实数据或模拟数据
const displayData = mockData;
// 按类型分组数据
const yearlyPlans = displayData.filter((item) => item.duration === 'year');
const monthlyPlans = displayData.filter((item) => item.duration === 'month');
console.log(processedData);
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent
className={
'rounded-0 !container h-full w-full px-8 py-8 md:h-auto md:w-[1000px] md:!rounded-[32px] md:px-12 md:py-12'
}
closeIcon={<Image src={CloseSvg} alt={'close'} />}
closeClassName={
'right-6 top-6 font-bold text-black opacity-100 focus:ring-0 focus:ring-offset-0'
}
>
<DialogTitle className={'mb-4 text-center text-4xl font-bold text-[#0F2C53] md:text-5xl'}>
</DialogTitle>
<div className={'min-h-[600px]'}>
<div className={'mb-8 text-center text-lg font-medium text-[#666666]'}>
</div>
<div className={'mt-8'}>
<Tabs defaultValue='year' className={'text-center'}>
<TabsList className='mb-8 h-[74px] flex-wrap rounded-full bg-[#EAEAEA] p-2.5'>
<TabsTrigger
className={
'rounded-full px-12 py-3.5 text-xl data-[state=active]:bg-[#0F2C53] data-[state=active]:text-white'
}
value='year'
>
</TabsTrigger>
<TabsTrigger
className={
'rounded-full px-12 py-3.5 text-xl data-[state=active]:bg-[#0F2C53] data-[state=active]:text-white'
}
value='month'
>
</TabsTrigger>
</TabsList>
<TabsContent value='year'>
{isLoading ? (
<div className='py-12 text-center'>
<div className='mx-auto h-12 w-12 animate-spin rounded-full border-b-2 border-[#0F2C53]'></div>
<p className='mt-4 text-gray-600'>...</p>
</div>
) : error ? (
<div className='py-12 text-center'>
<p className='text-lg text-red-500'></p>
<button
onClick={() => refetch()}
className='mt-4 rounded-lg bg-[#0F2C53] px-6 py-2 text-white transition-colors hover:bg-[#0A2C47]'
>
</button>
</div>
) : yearlyPlans.length > 0 ? (
<div className='relative'>
{/* 卡片容器 */}
<div className='mt-8 grid grid-cols-1 justify-items-center gap-4 sm:grid-cols-2 sm:gap-6 lg:grid-cols-3 lg:gap-8'>
{yearlyPlans.map((plan, index) => (
<PlanCard key={plan.id} plan={plan} />
))}
</div>
</div>
) : (
<div className='py-12 text-center'>
<p className='text-lg text-gray-500'></p>
</div>
)}
</TabsContent>
<TabsContent value='month'>
{isLoading ? (
<div className='py-12 text-center'>
<div className='mx-auto h-12 w-12 animate-spin rounded-full border-b-2 border-[#0F2C53]'></div>
<p className='mt-4 text-gray-600'>...</p>
</div>
) : error ? (
<div className='py-12 text-center'>
<p className='text-lg text-red-500'></p>
<button
onClick={() => refetch()}
className='mt-4 rounded-lg bg-[#0F2C53] px-6 py-2 text-white transition-colors hover:bg-[#0A2C47]'
>
</button>
</div>
) : monthlyPlans.length > 0 ? (
<div className='relative'>
{/* 连接线 */}
<div className='absolute left-1/2 top-0 h-8 w-0.5 -translate-x-1/2 transform bg-gradient-to-b from-[#0F2C53] to-transparent opacity-60'></div>
{/* 卡片容器 */}
<div className='mt-8 grid grid-cols-1 justify-items-center gap-4 sm:grid-cols-2 sm:gap-6 lg:grid-cols-3 lg:gap-8'>
{monthlyPlans.map((plan, index) => (
<PlanCard key={plan.id} plan={plan} />
))}
</div>
</div>
) : (
<div className='py-12 text-center'>
<p className='text-lg text-gray-500'></p>
</div>
)}
</TabsContent>
</Tabs>
</div>
</div>
</DialogContent>
</Dialog>
);
});
export default OfferDialog;