🐛 fix(user): Update user subscribe display
This commit is contained in:
@@ -32,6 +32,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@workspace/ui/componen
|
||||
import { Separator } from '@workspace/ui/components/separator';
|
||||
import { Tabs, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
|
||||
import { Icon } from '@workspace/ui/custom-components/icon';
|
||||
import { cn } from '@workspace/ui/lib/utils';
|
||||
import { differenceInDays, formatDate, isBrowser } from '@workspace/ui/utils';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import Image from 'next/image';
|
||||
@@ -88,6 +89,12 @@ export default function Content() {
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
|
||||
const statusWatermarks = {
|
||||
2: t('finished'),
|
||||
3: t('expired'),
|
||||
4: t('deducted'),
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{userSubscribe.length ? (
|
||||
@@ -159,209 +166,259 @@ export default function Content() {
|
||||
</Tabs>
|
||||
)}
|
||||
</div>
|
||||
{userSubscribe.map((item) => (
|
||||
<Card key={item.id}>
|
||||
<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>
|
||||
<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>
|
||||
{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);
|
||||
|
||||
<CopyToClipboard
|
||||
text={url}
|
||||
onCopy={(text, result) => {
|
||||
if (result) {
|
||||
toast.success(t('copySuccess'));
|
||||
}
|
||||
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)',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className='text-primary hover:bg-accent mr-4 flex cursor-pointer rounded p-2 text-sm'
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
{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'));
|
||||
}}
|
||||
>
|
||||
<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;
|
||||
{t('confirm')}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<ResetTraffic id={item.id} replacement={item.subscribe.replacement} />
|
||||
<Renewal id={item.id} subscribe={item.subscribe} />
|
||||
|
||||
const handleCopy = (text: string, result: boolean) => {
|
||||
<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) {
|
||||
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();
|
||||
toast.success(t('copySuccess'));
|
||||
}
|
||||
};
|
||||
|
||||
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>
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</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>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
|
||||
Reference in New Issue
Block a user