576 lines
25 KiB
TypeScript
576 lines
25 KiB
TypeScript
'use client';
|
||
|
||
import useGlobalStore from '@/config/use-global';
|
||
import { getStat } from '@/services/common/common';
|
||
import { queryApplicationConfig } from '@/services/user/subscribe';
|
||
import { queryUserSubscribe } from '@/services/user/user';
|
||
import { getPlatform } from '@/utils/common';
|
||
import { useQuery } from '@tanstack/react-query';
|
||
import { Card } from '@workspace/ui/components/card';
|
||
import { useTranslations } from 'next-intl';
|
||
import { useState } from 'react';
|
||
|
||
const platforms: (keyof API.ApplicationPlatform)[] = [
|
||
'windows',
|
||
'macos',
|
||
'linux',
|
||
'ios',
|
||
'android',
|
||
'harmony',
|
||
];
|
||
|
||
export default function Content() {
|
||
const t = useTranslations('dashboard');
|
||
const { getUserSubscribe, getAppSubLink } = useGlobalStore();
|
||
|
||
const [protocol, setProtocol] = useState('');
|
||
|
||
const {
|
||
data: userSubscribe = [],
|
||
refetch,
|
||
isLoading,
|
||
} = useQuery({
|
||
queryKey: ['queryUserSubscribe'],
|
||
queryFn: async () => {
|
||
const { data } = await queryUserSubscribe();
|
||
return data.data?.list || [];
|
||
},
|
||
});
|
||
const { data: applications } = useQuery({
|
||
queryKey: ['queryApplicationConfig'],
|
||
queryFn: async () => {
|
||
const { data } = await queryApplicationConfig();
|
||
return data.data?.applications || [];
|
||
},
|
||
});
|
||
const [platform, setPlatform] = useState<keyof API.ApplicationPlatform>(getPlatform());
|
||
|
||
const { data } = useQuery({
|
||
queryKey: ['getStat'],
|
||
queryFn: async () => {
|
||
const { data } = await getStat({
|
||
skipErrorHandler: true,
|
||
});
|
||
return data.data;
|
||
},
|
||
refetchOnWindowFocus: false,
|
||
});
|
||
|
||
const statusWatermarks = {
|
||
2: t('finished'),
|
||
3: t('expired'),
|
||
4: t('deducted'),
|
||
};
|
||
|
||
const { user } = useGlobalStore();
|
||
return (
|
||
<>
|
||
<div className={'grid grid-cols-1 gap-6 lg:grid-cols-2'}>
|
||
{/* 账户概况 Card */}
|
||
<Card className='rounded-[20px] border border-[#D9D9D9] p-6 shadow-[0px_0px_52.6px_1px_rgba(15,44,83,0.05)]'>
|
||
<div className='mb-4'>
|
||
<h3 className='text-xl font-medium text-[#666666]'>账户概况</h3>
|
||
<p className='mt-1 text-sm text-[#666666]'>
|
||
{user?.auth_methods?.[0]?.auth_identifier}
|
||
</p>
|
||
</div>
|
||
|
||
<div className='mb-6'>
|
||
<span className='text-3xl font-medium text-[#091B33]'>年度套餐用户</span>
|
||
</div>
|
||
|
||
<div className='rounded-[20px] bg-[#EAEAEA] p-4'>
|
||
<div className='mb-2 flex items-center justify-between'>
|
||
<span className='text-sm text-[#666666]'>账户余额</span>
|
||
<span className='text-sm text-[#225BA9]'>钱包充值</span>
|
||
</div>
|
||
<div className='text-3xl font-medium text-[#225BA9]'>$1680.00</div>
|
||
</div>
|
||
</Card>
|
||
|
||
{/* 套餐状态 Card */}
|
||
<Card className='rounded-[20px] border border-[#D9D9D9] p-6 text-[#666666] shadow-[0px_0px_52.6px_1px_rgba(15,44,83,0.05)]'>
|
||
<div className='mb-4'>
|
||
<h3 className='flex items-center justify-between text-[#666666]'>
|
||
<div>
|
||
<span className={'text-xl'}>套餐状态</span>
|
||
<span className={'ml-2.5 rounded-full bg-[#A8D4ED] px-2 text-[8px] text-white'}>
|
||
生效中
|
||
</span>
|
||
</div>
|
||
<span className={'text-sm text-[#225BA9]'}>流量重置</span>
|
||
</h3>
|
||
<div>
|
||
<p className='mb-2 text-sm text-[#666666]'>套餐到期时间:2026-07-29</p>
|
||
</div>
|
||
<div className='mt-2'>
|
||
<span className='text-3xl font-medium text-[#091B33]'>Pro Plan</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div className='space-y-4'>
|
||
<div className='space-y-3'>
|
||
<div className='mb-2 flex items-center justify-between'>
|
||
<div className='flex items-center gap-2'>
|
||
<span className='text-sm'>可用设备</span>
|
||
<div className='flex gap-2'>
|
||
<div className='h-4 w-4 rounded-full bg-[#225BA9]'></div>
|
||
<div className='h-4 w-4 rounded-full bg-[#D9D9D9]'></div>
|
||
<div className='h-4 w-4 rounded-full bg-[#D9D9D9]'></div>
|
||
<div className='h-4 w-4 rounded-full bg-[#D9D9D9]'></div>
|
||
<div className='h-4 w-4 rounded-full bg-[#D9D9D9]'></div>
|
||
<div className='h-4 w-4 rounded-full bg-[#D9D9D9]'></div>
|
||
</div>
|
||
</div>
|
||
<span className='text-sm'>在线:1/6</span>
|
||
</div>
|
||
<div>
|
||
<div className='mb-1 flex items-center justify-between'>
|
||
<span className='text-sm'>已使用流量/总流量:5.52GB/100GB</span>
|
||
<span className='text-sm'>剩余:94%</span>
|
||
</div>
|
||
<div className='flex h-5 w-full items-center rounded-[20px] bg-[#EAEAEA] p-0.5'>
|
||
<div className='h-full rounded-[20px] bg-[#225BA9]' style={{ width: '6%' }}></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Card>
|
||
{/* 网站公告 Card */}
|
||
<Card className='rounded-[20px] border border-[#EAEAEA] bg-gradient-to-b from-white to-[#EAEAEA] p-6'>
|
||
<div className='mb-4 flex items-center justify-between'>
|
||
<h3 className='text-xl font-medium text-[#666666]'>网站公告</h3>
|
||
<span className='text-sm text-[#225BA9]'>更多</span>
|
||
</div>
|
||
|
||
<div className='space-y-4'>
|
||
{/* 置顶公告 */}
|
||
<div className='flex items-center rounded-[20px] bg-[#B5C9E2] p-4'>
|
||
<p className='mb-2 line-clamp-2 flex-1 text-sm text-[#225BA9]'>
|
||
【置顶公告】Airo
|
||
Port提供IPLC/IEPL专线或BGP隧道中继,避免直连线路的拥堵,提供更低的延迟和更高的...
|
||
</p>
|
||
<div className='ml-2 w-[65px] text-right'>
|
||
<span className='text-sm text-[#225BA9]'>查看详情</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 系统通知列表 */}
|
||
<div className='space-y-3'>
|
||
<div className='rounded-[20px] border bg-white p-4'>
|
||
<p className='mb-2 line-clamp-2 text-sm text-[#225BA9]'>
|
||
【系统通知】充值成功
|
||
<br />
|
||
订单 ID:R20250729115302USDT 充值成功,钱包余额...
|
||
</p>
|
||
<div className='text-right'>
|
||
<span className='text-sm text-[#225BA9]'>查看详情</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div className='rounded-[20px] border bg-white p-4'>
|
||
<p className='mb-2 line-clamp-2 text-sm text-[#225BA9]'>
|
||
【系统通知】充值成功
|
||
<br />
|
||
订单 ID:R20250729115302USDT 充值成功,钱包余额...
|
||
</p>
|
||
<div className='text-right'>
|
||
<span className='text-sm text-[#225BA9]'>查看详情</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div className='rounded-[20px] border bg-white p-4'>
|
||
<p className='mb-2 line-clamp-2 text-sm text-[#225BA9]'>
|
||
【系统通知】充值成功
|
||
<br />
|
||
订单 ID:R20250729115302USDT 充值成功,钱包余额...
|
||
</p>
|
||
<div className='text-right'>
|
||
<span className='text-sm text-[#225BA9]'>查看详情</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Card>
|
||
|
||
{/* 我的订阅 Card */}
|
||
<Card className='rounded-[20px] border border-[#D9D9D9] p-6 shadow-[0px_0px_52.6px_1px_rgba(15,44,83,0.05)]'>
|
||
<div className='mb-4'>
|
||
<h3 className='text-xl font-medium text-[#666666]'>我的订阅</h3>
|
||
</div>
|
||
|
||
<div className='space-y-4'>
|
||
<p className='text-sm text-[#666666]'>直接复制订阅链接或点击二维码按钮扫码获取</p>
|
||
|
||
{/* 统计信息 */}
|
||
<div className='rounded-[20px] bg-[#EAEAEA] p-4'>
|
||
<div className='grid grid-cols-3 gap-4 text-center'>
|
||
<div>
|
||
<p className='text-xs text-[rgba(132,132,132,0.7)]'>总流量</p>
|
||
<p className='text-lg font-medium text-[#0F2C53]'>100.00 GB</p>
|
||
</div>
|
||
<div>
|
||
<p className='text-xs text-[rgba(132,132,132,0.7)]'>下次重置/天</p>
|
||
<p className='text-lg font-medium text-[#0F2C53]'>28</p>
|
||
</div>
|
||
<div>
|
||
<p className='text-xs text-[rgba(132,132,132,0.7)]'>到期时间/天</p>
|
||
<p className='text-lg font-medium text-[#0F2C53]'>363</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 订阅链接 */}
|
||
<div className='rounded-[26px] bg-[#EAEAEA] p-4'>
|
||
<div className='mb-2'>
|
||
{/* 协议选择 */}
|
||
<div className='flex flex-wrap gap-2'>
|
||
<button className='rounded-full bg-[#225BA9] px-4 py-1 text-xs text-white'>
|
||
ALL
|
||
</button>
|
||
<button className='rounded-full bg-[#EAEAEA] px-4 py-1 text-xs text-[#666666] shadow-[inset_0px_0px_4px_0px_rgba(0,0,0,0.25)]'>
|
||
VMESS
|
||
</button>
|
||
<button className='rounded-full bg-[#EAEAEA] px-4 py-1 text-xs text-[#666666] shadow-[inset_0px_0px_4px_0px_rgba(0,0,0,0.25)]'>
|
||
VLESS
|
||
</button>
|
||
<button className='rounded-full bg-[#EAEAEA] px-4 py-1 text-xs text-[#666666] shadow-[inset_0px_0px_4px_0px_rgba(0,0,0,0.25)]'>
|
||
TROJAN
|
||
</button>
|
||
<button className='rounded-full bg-[#EAEAEA] px-4 py-1 text-xs text-[#666666] shadow-[inset_0px_0px_4px_0px_rgba(0,0,0,0.25)]'>
|
||
SHADOW SOCKS
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div className={'mb-2 flex items-center justify-center gap-1'}>
|
||
<span className='w-20 flex-shrink-0 text-sm font-medium text-[#666666]'>
|
||
订阅地址 1
|
||
</span>
|
||
<div className='line-clamp-2 flex-1 rounded-[16px] bg-white p-3 text-xs text-[#225BA9] shadow-[inset_0px_0px_7.6px_0px_rgba(0,0,0,0.25)]'>
|
||
https://api.kxsw.us/api/subscribe?token=512ce3958ef939c668aaf8442d51dd5f
|
||
</div>
|
||
<div className={'size-6 flex-shrink-0 rounded-lg bg-black'}></div>
|
||
</div>
|
||
<div className='flex justify-between gap-2'>
|
||
<button className='rounded-full bg-[#E22C2E] px-3 py-1 text-xs text-white'>
|
||
重置订阅地址
|
||
</button>
|
||
<button className='rounded-full bg-[#A8D4ED] px-3 py-1 text-xs text-white'>
|
||
续订套餐
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Card>
|
||
</div>
|
||
{/*{userSubscribe.length ? (
|
||
<>
|
||
<div className='flex items-center justify-between'>
|
||
<h2 className='flex items-center gap-1.5 font-semibold'>
|
||
<Icon icon='uil:servers' className='size-5' />
|
||
{t('mySubscriptions')}
|
||
</h2>
|
||
<div className='flex gap-2'>
|
||
<Button
|
||
size='sm'
|
||
variant='outline'
|
||
onClick={() => {
|
||
refetch();
|
||
}}
|
||
className={isLoading ? 'animate-pulse' : ''}
|
||
>
|
||
<Icon icon='uil:sync' />
|
||
</Button>
|
||
<Button size='sm' asChild>
|
||
<Link href='/subscribe'>{t('purchaseSubscription')}</Link>
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
<div className='flex flex-wrap justify-between gap-4'>
|
||
{data?.protocol && data?.protocol.length > 1 && (
|
||
<Tabs
|
||
value={protocol}
|
||
onValueChange={setProtocol}
|
||
className='w-full max-w-full md:w-auto'
|
||
>
|
||
<TabsList className='flex *:flex-auto'>
|
||
{['all', ...(data?.protocol || [])].map((item) => (
|
||
<TabsTrigger
|
||
value={item === 'all' ? '' : item}
|
||
key={item}
|
||
className='px-1 uppercase lg:px-3'
|
||
>
|
||
{item}
|
||
</TabsTrigger>
|
||
))}
|
||
</TabsList>
|
||
</Tabs>
|
||
)}
|
||
</div>
|
||
{userSubscribe.map((item) => {
|
||
return (
|
||
<Card
|
||
key={item.id}
|
||
className={cn('relative', {
|
||
'relative opacity-80 grayscale': item.status === 3,
|
||
'relative hidden opacity-60 blur-[0.3px] grayscale': item.status === 4,
|
||
})}
|
||
>
|
||
{item.status >= 2 && (
|
||
<div
|
||
className={cn(
|
||
'pointer-events-none absolute left-0 top-0 z-10 h-full w-full overflow-hidden mix-blend-difference',
|
||
{
|
||
'text-destructive': item.status === 2,
|
||
'text-white': item.status === 3 || item.status === 4,
|
||
},
|
||
)}
|
||
style={{
|
||
filter: 'contrast(200%) brightness(150%) invert(0.2)',
|
||
}}
|
||
>
|
||
<div className='absolute inset-0'>
|
||
{Array.from({ length: 16 }).map((_, i) => {
|
||
const row = Math.floor(i / 4);
|
||
const col = i % 4;
|
||
// 计算位置百分比
|
||
const top = 10 + row * 25 + (col % 2 === 0 ? 5 : -5);
|
||
const left = 5 + col * 30 + (row % 2 === 0 ? 0 : 10);
|
||
|
||
return (
|
||
<span
|
||
key={i}
|
||
className='absolute rotate-[-30deg] whitespace-nowrap text-lg font-black opacity-40'
|
||
style={{
|
||
top: `${top}%`,
|
||
left: `${left}%`,
|
||
textShadow: '0px 0px 1px rgba(255,255,255,0.5)',
|
||
}}
|
||
>
|
||
{statusWatermarks[item.status as keyof typeof statusWatermarks]}
|
||
</span>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
)}
|
||
<CardHeader className='flex flex-row flex-wrap items-center justify-between gap-2 space-y-0'>
|
||
<CardTitle className='font-medium'>
|
||
{item.subscribe.name}
|
||
<p className='text-foreground/50 mt-1 text-sm'>{formatDate(item.start_time)}</p>
|
||
</CardTitle>
|
||
{item.status !== 4 && (
|
||
<div className='flex flex-wrap gap-2'>
|
||
<AlertDialog>
|
||
<AlertDialogTrigger asChild>
|
||
<Button size='sm' variant='destructive'>
|
||
{t('resetSubscription')}
|
||
</Button>
|
||
</AlertDialogTrigger>
|
||
<AlertDialogContent>
|
||
<AlertDialogHeader>
|
||
<AlertDialogTitle>{t('prompt')}</AlertDialogTitle>
|
||
<AlertDialogDescription>
|
||
{t('confirmResetSubscription')}
|
||
</AlertDialogDescription>
|
||
</AlertDialogHeader>
|
||
<AlertDialogFooter>
|
||
<AlertDialogCancel>{t('cancel')}</AlertDialogCancel>
|
||
<AlertDialogAction
|
||
onClick={async () => {
|
||
await resetUserSubscribeToken({
|
||
user_subscribe_id: item.id,
|
||
});
|
||
await refetch();
|
||
toast.success(t('resetSuccess'));
|
||
}}
|
||
>
|
||
{t('confirm')}
|
||
</AlertDialogAction>
|
||
</AlertDialogFooter>
|
||
</AlertDialogContent>
|
||
</AlertDialog>
|
||
<ResetTraffic id={item.id} replacement={item.subscribe.replacement} />
|
||
<Renewal id={item.id} subscribe={item.subscribe} />
|
||
|
||
<Unsubscribe id={item.id} allowDeduction={item.subscribe.allow_deduction} />
|
||
</div>
|
||
)}
|
||
</CardHeader>
|
||
<CardContent>
|
||
<ul className='grid grid-cols-2 gap-3 *:flex *:flex-col *:justify-between lg:grid-cols-4'>
|
||
<li>
|
||
<span className='text-muted-foreground'>{t('used')}</span>
|
||
<span className='text-2xl font-bold'>
|
||
<Display
|
||
type='traffic'
|
||
value={item.upload + item.download}
|
||
unlimited={!item.traffic}
|
||
/>
|
||
</span>
|
||
</li>
|
||
<li>
|
||
<span className='text-muted-foreground'>{t('totalTraffic')}</span>
|
||
<span className='text-2xl font-bold'>
|
||
<Display type='traffic' value={item.traffic} unlimited={!item.traffic} />
|
||
</span>
|
||
</li>
|
||
<li>
|
||
<span className='text-muted-foreground'>{t('nextResetDays')}</span>
|
||
<span className='text-2xl font-semibold'>
|
||
{item.reset_time
|
||
? differenceInDays(new Date(item.reset_time), new Date())
|
||
: t('noReset')}
|
||
</span>
|
||
</li>
|
||
<li>
|
||
<span className='text-muted-foreground'>{t('expirationDays')}</span>
|
||
<span className='text-2xl font-semibold'>
|
||
{}
|
||
{item.expire_time
|
||
? differenceInDays(new Date(item.expire_time), new Date()) || t('unknown')
|
||
: t('noLimit')}
|
||
</span>
|
||
</li>
|
||
</ul>
|
||
<Separator className='mt-4' />
|
||
<Accordion type='single' collapsible defaultValue='0' className='w-full'>
|
||
{getUserSubscribe(item.token, protocol)?.map((url, index) => (
|
||
<AccordionItem key={url} value={String(index)}>
|
||
<AccordionTrigger className='hover:no-underline'>
|
||
<div className='flex w-full flex-row items-center justify-between'>
|
||
<CardTitle className='text-sm font-medium'>
|
||
{t('subscriptionUrl')} {index + 1}
|
||
</CardTitle>
|
||
|
||
<CopyToClipboard
|
||
text={url}
|
||
onCopy={(text, result) => {
|
||
if (result) {
|
||
toast.success(t('copySuccess'));
|
||
}
|
||
}}
|
||
>
|
||
<span
|
||
className='text-primary hover:bg-accent mr-4 flex cursor-pointer rounded p-2 text-sm'
|
||
onClick={(e) => e.stopPropagation()}
|
||
>
|
||
<Icon icon='uil:copy' className='mr-2 size-5' />
|
||
{t('copy')}
|
||
</span>
|
||
</CopyToClipboard>
|
||
</div>
|
||
</AccordionTrigger>
|
||
<AccordionContent>
|
||
<div className='grid grid-cols-2 gap-4 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6'>
|
||
{applications
|
||
?.filter((application) => {
|
||
const platformApps = application.platform?.[platform];
|
||
return platformApps && platformApps.length > 0;
|
||
})
|
||
.map((application) => {
|
||
const platformApps = application.platform?.[platform];
|
||
const app =
|
||
platformApps?.find((item) => item.is_default) ||
|
||
platformApps?.[0];
|
||
if (!app) return null;
|
||
|
||
const handleCopy = (text: string, result: boolean) => {
|
||
if (result) {
|
||
const href = getAppSubLink(application.subscribe_type, url);
|
||
const showSuccessMessage = () => {
|
||
toast.success(
|
||
<>
|
||
<p>{t('copySuccess')}</p>
|
||
<br />
|
||
<p>{t('manualImportMessage')}</p>
|
||
</>,
|
||
);
|
||
};
|
||
|
||
if (isBrowser() && href) {
|
||
window.location.href = href;
|
||
const checkRedirect = setTimeout(() => {
|
||
if (window.location.href !== href) {
|
||
showSuccessMessage();
|
||
}
|
||
clearTimeout(checkRedirect);
|
||
}, 1000);
|
||
return;
|
||
}
|
||
|
||
showSuccessMessage();
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div
|
||
key={application.name}
|
||
className='text-muted-foreground flex size-full flex-col items-center justify-between gap-2 text-xs'
|
||
>
|
||
<span>{application.name}</span>
|
||
|
||
{application.icon && (
|
||
<Image
|
||
src={application.icon}
|
||
alt={application.name}
|
||
width={64}
|
||
height={64}
|
||
className='p-1'
|
||
/>
|
||
)}
|
||
<div className='flex'>
|
||
<Button
|
||
size='sm'
|
||
variant='secondary'
|
||
className='rounded-r-none px-1.5'
|
||
asChild
|
||
>
|
||
<Link href={app.url}>{t('download')}</Link>
|
||
</Button>
|
||
|
||
<CopyToClipboard
|
||
text={getAppSubLink(application.subscribe_type, url) || url}
|
||
onCopy={handleCopy}
|
||
>
|
||
<Button size='sm' className='rounded-l-none p-2'>
|
||
{t('import')}
|
||
</Button>
|
||
</CopyToClipboard>
|
||
</div>
|
||
</div>
|
||
);
|
||
})}
|
||
<div className='text-muted-foreground hidden size-full flex-col items-center justify-between gap-2 text-sm lg:flex'>
|
||
<span>{t('qrCode')}</span>
|
||
<QRCodeCanvas
|
||
value={url}
|
||
size={80}
|
||
bgColor='transparent'
|
||
fgColor='rgb(59, 130, 246)'
|
||
/>
|
||
<span className='text-center'>{t('scanToSubscribe')}</span>
|
||
</div>
|
||
</div>
|
||
</AccordionContent>
|
||
</AccordionItem>
|
||
))}
|
||
</Accordion>
|
||
</CardContent>
|
||
</Card>
|
||
);
|
||
})}
|
||
</>
|
||
) : (
|
||
<>
|
||
<h2 className='flex items-center gap-1.5 font-semibold'>
|
||
<Icon icon='uil:shop' className='size-5' />
|
||
{t('purchaseSubscription')}
|
||
</h2>
|
||
<Subscribe />
|
||
</>
|
||
)}*/}
|
||
</>
|
||
);
|
||
}
|