2025-08-06 00:38:29 -07:00

614 lines
26 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 { Display } from '@/components/display';
import Recharge from '@/components/subscribe/recharge';
import Renewal from '@/components/subscribe/renewal';
import ResetTraffic from '@/components/subscribe/reset-traffic';
import useGlobalStore from '@/config/use-global';
import { queryUserSubscribe, resetUserSubscribeToken } from '@/services/user/user';
import { useQuery } from '@tanstack/react-query';
import { Button } from '@workspace/ui/components/button';
import { Card } from '@workspace/ui/components/card';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@workspace/ui/components/select';
import { useTranslations } from 'next-intl';
import Link from 'next/link';
import { useEffect, useState } from 'react';
import { toast } from 'sonner';
import { Empty } from '@/components/empty';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from '@workspace/ui/components/alert-dialog';
import { Tabs, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
import { differenceInDays, formatDate } from '@workspace/ui/utils';
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 [userSubscribeProtocol, setUserSubscribeProtocol] = useState<string[]>([]);
const [userSubscribeProtocolCurrent, setUserSubscribeProtocolCurrent] = useState<string>('');
const {
data: userSubscribe = [],
refetch,
isLoading,
} = useQuery({
queryKey: ['queryUserSubscribe'],
queryFn: async () => {
const { data } = await queryUserSubscribe();
return data.data?.list || [];
},
});
/*const { data } = useQuery({
queryKey: ['getStat'],
queryFn: async () => {
const { data } = await getStat({
skipErrorHandler: true,
});
return data.data;
},
refetchOnWindowFocus: false,
});*/
const data = {
user: 1,
node: 2,
country: 0,
protocol: ['vmess', 'vless'],
};
useEffect(() => {
if (data && userSubscribe?.length > 0 && !userSubscribeProtocol.length) {
const list = getUserSubscribe(userSubscribe[0]?.token, data.protocol);
setUserSubscribeProtocol(list);
setUserSubscribeProtocolCurrent(list[0]);
}
}, [data, userSubscribe, userSubscribeProtocol.length]);
const statusWatermarks = {
2: t('finished'),
3: t('expired'),
4: t('deducted'),
};
const { user } = useGlobalStore();
const totalAssets = (user?.balance || 0) + (user?.commission || 0) + (user?.gift_amount || 0);
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] px-4 py-[10px]'>
<div className='flex items-center justify-between'>
<span className='text-sm font-light text-[#666666]'></span>
<Recharge
className={
'border-0 bg-transparent p-0 text-sm font-normal text-[#225BA9] shadow-none outline-0 hover:bg-transparent'
}
/>
</div>
<div className='text-4xl font-medium text-[#225BA9]'>
<Display type='currency' value={totalAssets} />
</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 className={'flex items-center justify-between'}>
<span className={'text-xl font-medium'}></span>
<span className={'ml-2.5 rounded-full bg-[#A8D4ED] px-2 text-[8px] text-white'}>
</span>
</div>
<ResetTraffic
className={
'border-0 bg-transparent p-0 text-sm font-normal text-[#225BA9] shadow-none outline-0 hover:bg-transparent'
}
id={userSubscribe?.[0]?.id}
replacement={userSubscribe?.[0]?.subscribe.replacement}
/>
</h3>
<div className='mb-[22px] mt-1 text-sm text-[#666666]'>
{formatDate(userSubscribe?.[0]?.expire_time, false)}
</div>
<div className='mb-6'>
<span className='text-3xl font-medium text-[#091B33]'>
{userSubscribe?.[0]?.subscribe.name}
</span>
</div>
</div>
<div className='mb-4 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'>
线undefined/{userSubscribe?.[0]?.subscribe.device_limit}
</span>
</div>
<div>
<div className='mb-1 flex items-center justify-between'>
<span className='text-sm'>
使/
<Display
type='traffic'
value={userSubscribe?.[0]?.upload + userSubscribe?.[0]?.download}
unlimited={!userSubscribe?.[0]?.traffic}
/>
/{' '}
<Display
type='traffic'
value={userSubscribe?.[0]?.traffic}
unlimited={!userSubscribe?.[0]?.traffic}
/>
</span>
<span className='text-sm'>
{100 -
(
(userSubscribe?.[0]?.upload + userSubscribe?.[0]?.download) /
userSubscribe?.[0]?.traffic
).toFixed(0)}
%
</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: `${((userSubscribe?.[0]?.upload + userSubscribe?.[0]?.download) / userSubscribe?.[0]?.traffic).toFixed(0)}%`,
}}
></div>
</div>
</div>
</Card>
{/* 网站公告 Card */}
<Card className='relative rounded-[20px] border border-[#EAEAEA] bg-gradient-to-b from-white to-[#EAEAEA] p-6 pb-0'>
<div
className={'absolute bottom-0 left-0 right-0 h-[60px] bg-white/30 backdrop-blur-[1px]'}
></div>
<div className='mb-4 flex items-center justify-between'>
<h3 className='text-xl font-medium text-[#666666]'></h3>
<Button className='border-0 bg-transparent p-0 text-sm font-normal text-[#225BA9] shadow-none outline-0 hover:bg-transparent'>
</Button>
</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='flex items-center gap-2 rounded-[20px] border bg-white p-4'>
<p className='mb-2 line-clamp-2 flex-1 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 flex-1 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 flex items-center justify-between'>
<h3 className='text-xl font-medium text-[#666666]'></h3>
<Link
href={'/document'}
className='border-0 bg-transparent p-0 text-sm font-normal text-[#225BA9] shadow-none outline-0 hover:bg-transparent'
>
</Link>
</div>
{userSubscribe?.[0] && data.protocol ? (
<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]'>
<Display
type='traffic'
value={userSubscribe?.[0]?.traffic}
unlimited={!userSubscribe?.[0]?.traffic}
/>
</p>
</div>
<div>
<p className='text-xs text-[rgba(132,132,132,0.7)]'>{t('nextResetDays')}</p>
<p className='text-lg font-medium text-[#0F2C53]'>
{userSubscribe?.[0]
? differenceInDays(new Date(userSubscribe?.[0].reset_time), new Date())
: t('noReset')}
</p>
</div>
<div>
<p className='text-xs text-[rgba(132,132,132,0.7)]'>{t('expirationDays')}</p>
<p className='text-lg font-medium text-[#0F2C53]'>
{userSubscribe?.[0]?.expire_time
? differenceInDays(new Date(userSubscribe?.[0].expire_time), new Date()) ||
t('unknown')
: t('noLimit')}
</p>
</div>
</div>
</div>
{/* 订阅链接 */}
<div className='rounded-[26px] bg-[#EAEAEA] p-4'>
<div className='mb-3 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 h-full flex-wrap gap-2 bg-transparent p-0 *:flex-auto'>
{['all', ...(data?.protocol || [])].map((item) => (
<TabsTrigger
value={item === 'all' ? '' : item}
key={item}
className='un rounded-full bg-[#EAEAEA] px-6 py-1 text-[10px] uppercase text-[#66666673] shadow-[inset_0px_0px_4px_0px_rgba(0,0,0,0.25)] data-[state=active]:bg-[#225BA9] data-[state=active]:text-white'
>
{item}
</TabsTrigger>
))}
</TabsList>
</Tabs>
)}
</div>
<div className={'mb-3 flex items-center justify-center gap-1'}>
<div className={'flex items-center gap-2 rounded-[16px] bg-[#BABABA] pl-2'}>
<Select
value={userSubscribeProtocolCurrent}
onValueChange={setUserSubscribeProtocolCurrent}
>
<SelectTrigger className='h-[35px] w-20 flex-shrink-0 rounded-[8px] border-none bg-[#D9D9D9] bg-transparent p-2 text-[13px] text-sm font-medium text-white shadow-none hover:bg-[#848484] focus:ring-0 [&>svg]:hidden'>
<SelectValue>
<div className='flex flex-col items-center justify-center'>
<div>{t('subscriptionUrl')}1</div>
<div className='h-0 w-0 border-l-[5px] border-r-[5px] border-t-[5px] border-l-transparent border-r-transparent border-t-white'></div>
</div>
</SelectValue>
</SelectTrigger>
<SelectContent>
{userSubscribeProtocol.map((url, index) => (
<SelectItem
key={url}
value={url}
className={
'focus:text-accent-foreground bg-[#D9D9D9] focus:bg-[#A8D4ED]'
}
>
{t('subscriptionUrl')}
{index + 1}
</SelectItem>
))}
</SelectContent>
</Select>
<div className='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)]'>
<div className={'line-clamp-2 break-all'}>{userSubscribeProtocolCurrent}</div>
</div>
</div>
<div className={'ml-3 h-[40px] w-[40px] flex-shrink-0 rounded-lg bg-black'}></div>
</div>
<div className='flex justify-between gap-2'>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
size='sm'
className={'rounded-full bg-[#E22C2E] px-3 py-1 text-xs text-white'}
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: userSubscribe?.[0]?.id,
});
await refetch();
toast.success(t('resetSuccess'));
}}
>
{t('confirm')}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<Renewal
className='rounded-full bg-[#A8D4ED] px-3 py-1 text-xs text-white'
id={userSubscribe?.[0]?.id}
subscribe={userSubscribe?.[0]?.subscribe}
/>
</div>
</div>
</div>
) : (
<Empty />
)}
</Card>
</div>
{/*{userSubscribe.length ? (
<>
<div className='flex items-center justify-between'>
<h2 className='flex items-center gap-1.5 font-semibold'>
{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>
{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>
111
</div>
</AccordionTrigger>
<AccordionContent>
<div className='grid grid-cols-2 gap-4 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6'>
<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>
</>
)}*/}
</>
);
}