2025-07-30 23:55:34 -07:00

576 lines
25 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.

'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 />
IDR20250729115302USDT ...
</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 />
IDR20250729115302USDT ...
</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 />
IDR20250729115302USDT ...
</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 />
</>
)}*/}
</>
);
}