675 lines
28 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, useRef, useState } from 'react';
import { toast } from 'sonner';
import {
AnnouncementDialog,
DialogRef,
} from '@/app/(main)/(content)/(user)/dashboard/components/Announcement/Dialog';
import {
Popup,
PopupRef,
} from '@/app/(main)/(content)/(user)/dashboard/components/Announcement/Popup';
import { Empty } from '@/components/empty';
import { queryAnnouncement } from '@/services/user/announcement';
import { Popover, PopoverContent, PopoverTrigger } from '@workspace/airo-ui/components/popover';
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';
import { QRCodeCanvas } from 'qrcode.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 [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'],
};
const { data: announcementData } = useQuery({
queryKey: ['queryAnnouncement'],
queryFn: async () => {
const { data } = await queryAnnouncement({
page: 1,
size: 4,
pinned: true,
popup: true,
});
return data.data?.announcements || [];
},
});
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);
// 获取当前选中项的显示标签
const getCurrentLabel = () => {
const currentIndex = userSubscribeProtocol.findIndex(
(url) => url === userSubscribeProtocolCurrent,
);
return currentIndex !== -1 ? `${t('subscriptionUrl')}${currentIndex + 1}` : '地址1';
};
const popupRef = useRef<PopupRef>(null);
const dialogRef = useRef<DialogRef>(null);
return (
<>
<div className={'grid grid-cols-1 gap-[10px] sm: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-1 sm:mb-4'>
<h3 className='text-base font-medium text-[#666666] sm:text-xl'></h3>
<p className='mt-1 text-xs text-[#666666] sm:text-sm'>
{user?.auth_methods?.[0]?.auth_identifier}
</p>
</div>
<div className='mb-3 sm:mb-6'>
<span className='text-2xl font-medium text-[#091B33] sm:text-3xl'></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-xl font-medium text-[#225BA9] sm:text-4xl'>
<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-base font-medium sm:text-xl'}></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-2 text-sm text-[#666666] sm:mb-[22px] sm:mt-1'>
{formatDate(userSubscribe?.[0]?.expire_time, false)}
</div>
<div className='mb-3 sm:mb-6'>
<span className='text-2xl font-medium text-[#091B33] sm:text-3xl'>
{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-xs sm: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-xs sm:text-sm'>
线uu/{userSubscribe?.[0]?.subscribe.device_limit}
</span>
</div>
<div>
<div className='mb-1 flex items-center justify-between'>
<span className='text-xs sm: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-xs sm: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 order-4 rounded-[20px] border border-[#EAEAEA] bg-gradient-to-b from-white to-[#EAEAEA] p-6 pb-0 sm:order-none'>
<div
className={'absolute bottom-0 left-0 right-0 h-[60px] bg-white/30 backdrop-blur-[1px]'}
></div>
<div className='mb-3 flex items-center justify-between sm:mb-4'>
<h3 className='text-base font-medium text-[#666666] sm:text-xl'></h3>
{announcementData?.length ? (
<Button
className='border-0 bg-transparent p-0 text-sm font-normal text-[#225BA9] shadow-none outline-0 hover:bg-transparent'
onClick={() => dialogRef.current.open()}
>
</Button>
) : null}
</div>
<div className='space-y-3 sm:space-y-4'>
{/* 置顶公告 */}
{announcementData?.map((item) => {
return (
<div className='flex items-center rounded-[20px] bg-[#B5C9E2] px-4 py-2 sm:p-4'>
<p className='line-clamp-2 flex-1 text-[10px] text-[#225BA9] sm:text-sm'>
{item.pinned && '【置顶公告】'}{' '}
<span className={`${item.pinned ? 'text-white' : 'text-[#4D4D4D]'}`}>
{item.content}
</span>
</p>
<div className='ml-2 w-[65px] text-right'>
<span
className='cursor-pointer text-xs text-[#225BA9] sm:text-sm'
onClick={() => popupRef.current.open(item)}
>
</span>
</div>
</div>
);
})}
</div>
<Popup ref={popupRef} />
<AnnouncementDialog ref={dialogRef} />
</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='flex items-center justify-between sm:mb-4'>
<h3 className='text-base font-medium text-[#666666] sm:text-xl'></h3>
<Link
href={'/document'}
className='border-0 bg-transparent p-0 text-sm font-semibold text-[#225BA9] shadow-none outline-0 hover:bg-transparent sm:font-normal'
>
</Link>
</div>
{userSubscribe?.[0] && data.protocol ? (
<div className='space-y-2 sm:space-y-4'>
<p className='text-xs font-light text-[#666666] sm:text-sm sm:font-normal'>
</p>
{/* 统计信息 */}
<div className='rounded-[20px] bg-[#EAEAEA] p-4'>
<div className='grid grid-cols-3 gap-4 text-center'>
<div>
<p className='text-[10px] text-[rgba(132,132,132,0.7)] sm:text-xs'></p>
<p className='text-xs font-medium text-[#0F2C53] sm:text-lg'>
<Display
type='traffic'
value={userSubscribe?.[0]?.traffic}
unlimited={!userSubscribe?.[0]?.traffic}
/>
</p>
</div>
<div>
<p className='text-[10px] text-[rgba(132,132,132,0.7)] sm:text-xs'>
{t('nextResetDays')}
</p>
<p className='text-xs font-medium text-[#0F2C53] sm:text-lg'>
{userSubscribe?.[0]
? differenceInDays(new Date(userSubscribe?.[0].reset_time), new Date())
: t('noReset')}
</p>
</div>
<div>
<p className='text-[10px] text-[rgba(132,132,132,0.7)] sm:text-xs'>
{t('expirationDays')}
</p>
<p className='text-xs font-medium text-[#0F2C53] sm:text-lg'>
{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-2 sm: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-1 rounded-full bg-[#BABABA] pl-1 sm:gap-2 sm:rounded-[16px] sm:pl-2'
}
>
<Select
value={userSubscribeProtocolCurrent}
onValueChange={setUserSubscribeProtocolCurrent}
>
<SelectTrigger className='h-auto w-auto flex-shrink-0 rounded-[16px] border-none bg-[#D9D9D9] px-2.5 py-0.5 text-[13px] text-sm font-medium text-white shadow-none hover:bg-[#848484] focus:ring-0 sm:h-[35px] sm:rounded-[8px] sm:p-2 [&>svg]:hidden'>
<SelectValue>
<div className='flex flex-col items-center justify-center text-[10px] sm:text-sm'>
<div>{getCurrentLabel()}</div>
<div className='-mt-0.5 h-0 w-0 scale-50 border-l-[5px] border-r-[5px] border-t-[5px] border-l-transparent border-r-transparent border-t-white sm:scale-100'></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-full bg-white px-3 py-1 text-[10px] leading-tight text-[#225BA9] shadow-[inset_0px_0px_7.6px_0px_rgba(0,0,0,0.25)] sm:rounded-[16px] sm:p-3 sm:text-xs'>
<div className={'line-clamp-2 break-all text-[10px] sm:text-base'}>
{userSubscribeProtocolCurrent}
</div>
</div>
</div>
<Popover>
<PopoverTrigger asChild>
<div
className={
'ml-3 h-[40px] w-[40px] flex-shrink-0 cursor-pointer rounded-lg bg-black'
}
></div>
</PopoverTrigger>
<PopoverContent className='w-auto rounded-xl p-4' align='end'>
<div className='flex flex-col items-center gap-2'>
<p className='text-muted-foreground text-center text-xs'></p>
<QRCodeCanvas
value={userSubscribeProtocolCurrent}
size={120}
bgColor='transparent'
fgColor='rgb(34, 91, 169)'
/>
</div>
</PopoverContent>
</Popover>
</div>
<div className='flex justify-between gap-2'>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
size='sm'
className={
'h-fit rounded-full bg-[#E22C2E] px-3 py-1 text-[10px] text-white sm:h-9 sm:text-xs'
}
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='h-fit rounded-full bg-[#A8D4ED] px-3 py-1 text-[10px] text-white sm:h-9 sm:text-xs'
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>
</>
)}*/}
</>
);
}