706 lines
30 KiB
TypeScript
706 lines
30 KiB
TypeScript
'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/airo-ui/components/button';
|
|
import { Card } from '@workspace/airo-ui/components/card';
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '@workspace/airo-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 SvgIcon from '@/components/SvgIcon';
|
|
import { queryAnnouncement } from '@/services/user/announcement';
|
|
import {
|
|
AlertDialog,
|
|
AlertDialogAction,
|
|
AlertDialogCancel,
|
|
AlertDialogContent,
|
|
AlertDialogDescription,
|
|
AlertDialogFooter,
|
|
AlertDialogHeader,
|
|
AlertDialogTitle,
|
|
AlertDialogTrigger,
|
|
} from '@workspace/airo-ui/components/alert-dialog';
|
|
import { Popover, PopoverContent, PopoverTrigger } from '@workspace/airo-ui/components/popover';
|
|
import { Tabs, TabsList, TabsTrigger } from '@workspace/airo-ui/components/tabs';
|
|
import { differenceInDays, formatDate } from '@workspace/airo-ui/utils';
|
|
import { QRCodeCanvas } from 'qrcode.react';
|
|
import { CopyToClipboard } from 'react-copy-to-clipboard';
|
|
|
|
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);
|
|
if (list.length > 0) {
|
|
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}` : t('address1');
|
|
};
|
|
|
|
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'>
|
|
{t('accountOverview')}
|
|
</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'>
|
|
{t('annualPlanUser')}
|
|
</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]'>{t('accountBalance')}</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'}>{t('planStatus')}</span>
|
|
<span className={'ml-2.5 rounded-full bg-[#A8D4ED] px-2 text-[8px] text-white'}>
|
|
{t('inEffect')}
|
|
</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 || 0}
|
|
replacement={userSubscribe?.[0]?.subscribe.replacement}
|
|
/>
|
|
</h3>
|
|
<div className='mb-2 text-sm text-[#666666] sm:mb-[22px] sm:mt-1'>
|
|
{t('planExpirationTime')}
|
|
{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'>{t('availableDevices')}</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'>
|
|
{t('online')}
|
|
{userSubscribe?.[0]?.subscribe.device_limit}
|
|
</span>
|
|
</div>
|
|
<div>
|
|
<div className='mb-1 flex items-center justify-between'>
|
|
<span className='text-xs sm:text-sm'>
|
|
{t('usedTrafficTotalTraffic')}
|
|
<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'>
|
|
{t('remaining')}
|
|
{100 -
|
|
Math.round(
|
|
(((userSubscribe?.[0]?.upload || 0) + (userSubscribe?.[0]?.download || 0)) /
|
|
(userSubscribe?.[0]?.traffic || 1)) *
|
|
100,
|
|
)}
|
|
%
|
|
</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: `${Math.round((((userSubscribe?.[0]?.upload || 0) + (userSubscribe?.[0]?.download || 0)) / (userSubscribe?.[0]?.traffic || 1)) * 100)}%`,
|
|
}}
|
|
></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'>
|
|
{t('siteAnnouncements')}
|
|
</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()}
|
|
>
|
|
{t('more')}
|
|
</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 && t('pinnedAnnouncement')}{' '}
|
|
<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)}
|
|
>
|
|
{t('viewDetails')}
|
|
</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'>
|
|
{t('mySubscription')}
|
|
</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'
|
|
>
|
|
{t('beginnerTutorial')}
|
|
</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'>
|
|
{t('copySubscriptionLinkOrScanQrCode')}
|
|
</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'>
|
|
{t('totalTraffic')}
|
|
</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'>
|
|
{['all', ...(data?.protocol || [])].map((item) => (
|
|
<TabsTrigger
|
|
value={item === 'all' ? '' : item}
|
|
key={item}
|
|
className='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:text-xs'>
|
|
<div className={'flex items-center gap-4 py-1 text-[10px] sm:text-[16px]'}>
|
|
<div className={'line-clamp-2 flex-1 break-all'}>
|
|
{userSubscribeProtocolCurrent}
|
|
</div>
|
|
<CopyToClipboard
|
|
text={userSubscribeProtocolCurrent}
|
|
onCopy={(text, result) => {
|
|
if (result) {
|
|
toast.success(t('copySuccess'));
|
|
}
|
|
}}
|
|
>
|
|
<span className={'cursor-pointer p-1'}>
|
|
<SvgIcon name={'copy'}></SvgIcon>
|
|
</span>
|
|
</CopyToClipboard>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<Popover>
|
|
<PopoverTrigger asChild>
|
|
<div className={'size-10'}>
|
|
<SvgIcon name={'qrcode'} width={'100%'} height={'100%'}></SvgIcon>
|
|
</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'>
|
|
{t('scanCodeToSubscribe')}
|
|
</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 || 0,
|
|
});
|
|
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>
|
|
|
|
</>
|
|
)}*/}
|
|
</>
|
|
);
|
|
}
|