feat: 修改样式

This commit is contained in:
speakeloudest 2025-08-12 02:58:18 -07:00
parent cc522d0754
commit 265519a03d
13 changed files with 567 additions and 648 deletions

View File

@ -0,0 +1,248 @@
import { Display } from '@/components/display';
import Renewal from '@/components/subscribe/renewal';
import { resetUserSubscribeToken } from '@/services/user/user';
import { Button } from '@workspace/airo-ui/components/button';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@workspace/airo-ui/components/select';
import { useTranslations } from 'next-intl';
import { toast } from 'sonner';
import SvgIcon from '@/components/SvgIcon';
import useGlobalStore from '@/config/use-global';
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 } from '@workspace/airo-ui/utils';
import { QRCodeCanvas } from 'qrcode.react';
import { useEffect, useState } from 'react';
import { CopyToClipboard } from 'react-copy-to-clipboard';
interface SubscribeCardProps {
userSubscribeData: API.UserSubscribe;
protocol: string[];
refetch: () => Promise<void>;
}
const SubscribeCard = (props: SubscribeCardProps) => {
const t = useTranslations('dashboard');
const { userSubscribeData } = props;
const { getUserSubscribe } = useGlobalStore();
const [protocol, setProtocol] = useState('');
const [userSubscribeProtocol, setUserSubscribeProtocol] = useState<string[]>([]);
const [userSubscribeProtocolCurrent, setUserSubscribeProtocolCurrent] = useState<number>(0);
useEffect(() => {
const list = getUserSubscribe(userSubscribeData.token, protocol);
setUserSubscribeProtocol(list);
if (list.length > 0) {
setUserSubscribeProtocolCurrent(0);
}
}, [props.userSubscribeData.token, protocol]);
return (
<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={userSubscribeData.traffic}
unlimited={!userSubscribeData.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'>
{userSubscribeData.reset_time
? differenceInDays(new Date(userSubscribeData.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'>
{userSubscribeData.expire_time
? differenceInDays(new Date(userSubscribeData.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'>
{props.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', ...(props.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 flex-1 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>
{t('subscriptionUrl')}
{userSubscribeProtocolCurrent + 1}
</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={index}
value={index}
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'}>
{userSubscribeProtocol[userSubscribeProtocolCurrent]}
</div>
<CopyToClipboard
text={userSubscribeProtocol[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={userSubscribeProtocol[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: userSubscribeData.id || 0,
});
await props.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={userSubscribeData.id}
subscribe={userSubscribeData.subscribe}
/>
</div>
</div>
</div>
);
};
export default SubscribeCard;

View File

@ -2,25 +2,17 @@
import { Display } from '@/components/display'; import { Display } from '@/components/display';
import Recharge from '@/components/subscribe/recharge'; import Recharge from '@/components/subscribe/recharge';
import Renewal from '@/components/subscribe/renewal';
import ResetTraffic from '@/components/subscribe/reset-traffic'; import ResetTraffic from '@/components/subscribe/reset-traffic';
import useGlobalStore from '@/config/use-global'; import useGlobalStore from '@/config/use-global';
import { getStat } from '@/services/common/common'; import { getStat } from '@/services/common/common';
import { queryUserSubscribe, resetUserSubscribeToken } from '@/services/user/user'; import { queryUserSubscribe } from '@/services/user/user';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { Button } from '@workspace/airo-ui/components/button'; import { Button } from '@workspace/airo-ui/components/button';
import { Card } from '@workspace/airo-ui/components/card'; 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 { useTranslations } from 'next-intl';
import Link from 'next/link'; import Link from 'next/link';
import { useEffect, useRef, useState } from 'react'; import { useRef } from 'react';
import { toast } from 'sonner'; import SubScribeCard from './components/SubscribeCard/index';
import { import {
AnnouncementDialog, AnnouncementDialog,
@ -31,25 +23,10 @@ import {
PopupRef, PopupRef,
} from '@/app/(main)/(content)/(user)/dashboard/components/Announcement/Popup'; } from '@/app/(main)/(content)/(user)/dashboard/components/Announcement/Popup';
import { Empty } from '@/components/empty'; import { Empty } from '@/components/empty';
import SvgIcon from '@/components/SvgIcon';
import { queryAnnouncement } from '@/services/user/announcement'; import { queryAnnouncement } from '@/services/user/announcement';
import { import { queryOrderList } from '@/services/user/order';
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 { default as Airo_Empty } from '@workspace/airo-ui/custom-components/empty'; import { default as Airo_Empty } from '@workspace/airo-ui/custom-components/empty';
import { differenceInDays, formatDate } from '@workspace/airo-ui/utils'; import { formatDate } from '@workspace/airo-ui/utils';
import { QRCodeCanvas } from 'qrcode.react';
import { CopyToClipboard } from 'react-copy-to-clipboard';
const platforms: (keyof API.ApplicationPlatform)[] = [ const platforms: (keyof API.ApplicationPlatform)[] = [
'windows', 'windows',
@ -62,16 +39,8 @@ const platforms: (keyof API.ApplicationPlatform)[] = [
export default function Content() { export default function Content() {
const t = useTranslations('dashboard'); const t = useTranslations('dashboard');
const { getUserSubscribe, getAppSubLink } = useGlobalStore();
const [protocol, setProtocol] = useState(''); const { data: userSubscribe = [], refetch } = useQuery({
const [userSubscribeProtocol, setUserSubscribeProtocol] = useState<string[]>([]);
const [userSubscribeProtocolCurrent, setUserSubscribeProtocolCurrent] = useState<string>('');
const {
data: userSubscribe = [],
refetch,
isLoading,
} = useQuery({
queryKey: ['queryUserSubscribe'], queryKey: ['queryUserSubscribe'],
queryFn: async () => { queryFn: async () => {
const { data } = await queryUserSubscribe(); const { data } = await queryUserSubscribe();
@ -103,15 +72,13 @@ export default function Content() {
}, },
}); });
useEffect(() => { const { data: orderData } = useQuery({
if (data && userSubscribe?.length > 0 && !userSubscribeProtocol.length) { queryKey: ['orderData'],
const list = getUserSubscribe(userSubscribe[0]?.token, data.protocol); queryFn: async () => {
setUserSubscribeProtocol(list); const { data } = await queryOrderList({ status: 5 });
if (list.length > 0) { return data?.[0] ?? {};
setUserSubscribeProtocolCurrent(list[0]); },
} });
}
}, [data, userSubscribe, userSubscribeProtocol.length]);
const statusWatermarks = { const statusWatermarks = {
2: t('finished'), 2: t('finished'),
@ -122,14 +89,6 @@ export default function Content() {
const { user } = useGlobalStore(); const { user } = useGlobalStore();
const totalAssets = (user?.balance || 0) + (user?.commission || 0) + (user?.gift_amount || 0); 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 popupRef = useRef<PopupRef>(null);
const dialogRef = useRef<DialogRef>(null); const dialogRef = useRef<DialogRef>(null);
return ( return (
@ -148,7 +107,11 @@ export default function Content() {
<div className='mb-3 sm:mb-6'> <div className='mb-3 sm:mb-6'>
<span className='text-2xl font-medium text-[#091B33] sm:text-3xl'> <span className='text-2xl font-medium text-[#091B33] sm:text-3xl'>
{userSubscribe?.length > 0 ? <div>1</div> : t('noYPlan')} {userSubscribe?.length > 0 && userSubscribe[0]?.status === 1 && orderData
? orderData?.quantity === 1
? t('annualMonthPlanUser')
: t('annualYearPlanUser')
: t('noYPlan')}
</span> </span>
</div> </div>
@ -157,7 +120,7 @@ export default function Content() {
<span className='text-sm font-light text-[#666666]'>{t('accountBalance')}</span> <span className='text-sm font-light text-[#666666]'>{t('accountBalance')}</span>
<Recharge <Recharge
className={ className={
'border-0 bg-transparent p-0 text-sm font-normal text-[#225BA9] shadow-none outline-0 hover:bg-transparent' 'min-w-[50px] border-0 bg-transparent p-0 text-sm font-normal text-[#225BA9] shadow-none outline-0 hover:bg-transparent'
} }
/> />
</div> </div>
@ -330,391 +293,16 @@ export default function Content() {
</div> </div>
{userSubscribe?.[0] && data?.protocol ? ( {userSubscribe?.[0] && data?.protocol ? (
<div className='space-y-2 sm:space-y-4'> <SubScribeCard
<p className='text-xs font-light text-[#666666] sm:text-sm sm:font-normal'> userSubscribeData={userSubscribe?.[0]}
{t('copySubscriptionLinkOrScanQrCode')} protocol={data.protocol}
</p> refetch={refetch}
/>
{/* 统计信息 */}
<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].reset_time
? 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 /> <Empty />
)} )}
</Card> </Card>
</div> </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>
</>
)}*/}
</> </>
); );
} }

View File

@ -2,8 +2,8 @@
import { getTutorial } from '@/utils/tutorial'; import { getTutorial } from '@/utils/tutorial';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { buttonVariants } from '@workspace/airo-ui/components/AiroButton';
import { Avatar, AvatarFallback, AvatarImage } from '@workspace/airo-ui/components/avatar'; import { Avatar, AvatarFallback, AvatarImage } from '@workspace/airo-ui/components/avatar';
import { buttonVariants } from '@workspace/airo-ui/components/button';
import { Markdown } from '@workspace/airo-ui/custom-components/markdown'; import { Markdown } from '@workspace/airo-ui/custom-components/markdown';
import { useOutsideClick } from '@workspace/airo-ui/hooks/use-outside-click'; import { useOutsideClick } from '@workspace/airo-ui/hooks/use-outside-click';
import { cn } from '@workspace/airo-ui/lib/utils'; import { cn } from '@workspace/airo-ui/lib/utils';
@ -153,9 +153,9 @@ export function TutorialButton({ items }: { items: Item[] }) {
layoutId={`button-${item.title}-${id}`} layoutId={`button-${item.title}-${id}`}
className={cn( className={cn(
buttonVariants({ buttonVariants({
variant: 'secondary', variant: 'primary',
}), }),
'rounded-full border-[#A8D4ED] bg-[#A8D4ED] px-[35px] py-[9px] text-center text-xs font-bold text-white hover:bg-[#225BA9] hover:text-white sm:min-w-[100px]', 'sm:min-w-[100px]',
)} )}
> >
{t('read')} {t('read')}

View File

@ -191,7 +191,7 @@ export default function Page() {
<Button <Button
variant='destructive' variant='destructive'
className={ className={
'h-8 rounded-full border-white bg-transparent px-6 text-center text-sm font-bold text-[#FF4248] shadow-none hover:bg-transparent sm:min-w-[100px] sm:border-[#F8BFD2] sm:bg-[#F8BFD2] sm:text-white sm:shadow sm:hover:border-[#F8BFD2] sm:hover:bg-[#FF4248]' 'h-8 rounded-full border-white bg-transparent px-6 text-center text-sm font-bold text-[#FF4248] shadow-none hover:bg-transparent sm:min-w-[100px] sm:border-[#FF4248] sm:bg-[#FF4248] sm:text-white sm:shadow sm:hover:border-[#E22C2E] sm:hover:bg-[#E22C2E]'
} }
> >
{t('close')} {t('close')}

View File

@ -0,0 +1,63 @@
import { AiroButton, buttonVariants } from '@workspace/airo-ui/components/AiroButton';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogPortal,
AlertDialogTitle,
} from '@workspace/airo-ui/components/alert-dialog';
import CloseSvg from '@workspace/airo-ui/components/close.svg';
import { useImperativeHandle, useState } from 'react';
// Defining the AlertDialogComponent with internal state and onShow prop
const AlertDialogComponent = ({
ref,
title,
description,
cancelText = 'Cancel',
confirmText = 'Confirm',
onConfirm,
}) => {
const [open, setOpen] = useState(false);
function show() {
setOpen(true);
}
useImperativeHandle(ref, () => ({
show,
}));
return (
<AlertDialog open={open} onOpenChange={setOpen}>
<AlertDialogPortal>
<AlertDialogContent className={'py-[30px] sm:rounded-[25px]'}>
<div className={'absolute right-4 top-6'} onClick={() => setOpen(false)}>
<CloseSvg />
</div>
<AlertDialogHeader>
<AlertDialogTitle className={'text-[#E22C2E]'}>{title}</AlertDialogTitle>
<AlertDialogDescription className={'text-base font-light'}>
{description}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter className={'sm:justify-center'}>
<AlertDialogAction asChild className={buttonVariants({ variant: 'primary' })}>
<AiroButton variant={'primary'} onClick={onConfirm}>
{confirmText}
</AiroButton>
</AlertDialogAction>
<AlertDialogCancel asChild className={buttonVariants({ variant: 'danger' })}>
<AiroButton variant={'danger'}>{cancelText}</AiroButton>
</AlertDialogCancel>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialogPortal>
</AlertDialog>
);
};
export default AlertDialogComponent;

View File

@ -182,14 +182,17 @@ export default function Affiliate() {
const MONTHLY_PRICE = proPlan?.unit_price; const MONTHLY_PRICE = proPlan?.unit_price;
const discountItem = proPlan?.discount?.find((v) => v.quantity === 12); const discountItem = proPlan?.discount?.find((v) => v.quantity === 12);
const YEARLY_PRICE = proPlan?.unit_price * ((discountItem?.discount || 100) / 100) * 12; const YEARLY_PRICE = proPlan?.unit_price * ((discountItem?.discount || 100) / 100) * 12;
const {
common: { invite },
} = useGlobalStore();
const [count, setCount] = useState<number>(10); const [count, setCount] = useState<number>(10);
const clamp = (n: number) => Math.max(0, Math.min(10000, Math.floor(n))); const clamp = (n: number) => Math.max(0, Math.min(10000, Math.floor(n)));
const firstMonth = count * MONTHLY_PRICE * 0.5; // 50% const firstMonth = count * MONTHLY_PRICE * (invite.first_purchase_percentage / 100); // 50%
const firstYear = count * YEARLY_PRICE * 0.3; // 30% const firstYear =
const recurMonth = count * MONTHLY_PRICE * 0.2; // 20% count * YEARLY_PRICE * (invite.first_yearly_purchase_percentage / 100); // 30%
const recurYear = count * YEARLY_PRICE * 0.2; // 20% const recurMonth = count * MONTHLY_PRICE * (invite.non_first_purchase_percentage / 100); // 20%
const recurYear = count * YEARLY_PRICE * (invite.non_first_purchase_percentage / 100); // 20%
return ( return (
<div className='space-y-4'> <div className='space-y-4'>

View File

@ -78,36 +78,12 @@ const PriceDisplay = ({ plan }: { plan: ProcessedPlanData }) => {
import { useLoginDialog } from '@/app/auth/LoginDialogContext'; import { useLoginDialog } from '@/app/auth/LoginDialogContext';
import { Display } from '@/components/display'; import { Display } from '@/components/display';
import Modal from '@/components/Modal';
import Purchase from '@/components/subscribe/purchase'; import Purchase from '@/components/subscribe/purchase';
import useGlobalStore from '@/config/use-global'; import useGlobalStore from '@/config/use-global';
import { queryUserSubscribe } from '@/services/user/user';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
// 订阅按钮组件
const SubscribeButton = ({ onClick }: { onClick?: () => void }) => {
const { user } = useGlobalStore();
const { openLoginDialog } = useLoginDialog();
const t = useTranslations('components.offerDialog');
function handleClick() {
console.log('click', user);
if (!user) {
// 强制登陆
openLoginDialog(false);
return;
}
onClick();
}
return (
<button
onClick={handleClick}
className='h-10 w-full rounded-full bg-[#0F2C53] text-sm font-medium text-white shadow-md transition-all duration-300 hover:bg-[#225BA9] sm:h-10 sm:text-sm md:h-[40px] md:text-[14px]'
>
{t('subscribe')}
</button>
);
};
// 星级评分组件 // 星级评分组件
const StarRating = ({ rating, maxRating = 5 }: { rating: number; maxRating?: number }) => ( const StarRating = ({ rating, maxRating = 5 }: { rating: number; maxRating?: number }) => (
<div className='text-right text-xs font-light leading-[1.8461538461538463em] text-black sm:text-[13px]'> <div className='text-right text-xs font-light leading-[1.8461538461538463em] text-black sm:text-[13px]'>
@ -186,9 +162,26 @@ const PlanCard = forwardRef<
isFirstCard?: boolean; isFirstCard?: boolean;
} }
>(({ plan, onSubscribe, isFirstCard = false }, ref) => { >(({ plan, onSubscribe, isFirstCard = false }, ref) => {
const handleSubscribe = () => { const { user } = useGlobalStore();
const { openLoginDialog } = useLoginDialog();
const t = useTranslations('components.offerDialog');
const ModalRef = useRef(null);
async function handleSubscribe() {
if (!user) {
// 强制登陆
openLoginDialog(false);
return;
}
// 有生效套餐进行弹窗提示
const { data } = await queryUserSubscribe();
if (data?.data?.list?.[0].status === 1) {
ModalRef.current.show();
return;
}
onSubscribe?.(plan); onSubscribe?.(plan);
}; }
return ( return (
<div <div
@ -202,10 +195,26 @@ const PlanCard = forwardRef<
<PriceDisplay plan={plan} /> <PriceDisplay plan={plan} />
{/* 订阅按钮 */} {/* 订阅按钮 */}
<SubscribeButton onClick={handleSubscribe} /> <button
onClick={handleSubscribe}
className='h-10 w-full rounded-full bg-[#0F2C53] text-sm font-medium text-white shadow-md transition-all duration-300 hover:bg-[#225BA9] sm:h-10 sm:text-sm md:h-[40px] md:text-[14px]'
>
{t('subscribe')}
</button>
{/* 功能列表 */} {/* 功能列表 */}
<FeatureList plan={plan} /> <FeatureList plan={plan} />
<Modal
ref={ModalRef}
title={'【重要提示】'}
description={
'您已有正在生效的套餐,如继续购买新的套餐,原套餐将自动失效。账户套餐将自动转为最新套餐。未使用部分不支持退款或叠加。'
}
confirmText={'同意'}
cancelText={'取消'}
onConfirm={() => onSubscribe?.(plan)}
/>
</div> </div>
); );
}); });
@ -350,7 +359,6 @@ const OfferDialog = forwardRef<OfferDialogRef>((props, ref) => {
// 处理订阅点击 // 处理订阅点击
const handleSubscribe = (plan: ProcessedPlanData) => { const handleSubscribe = (plan: ProcessedPlanData) => {
setSelectedPlan(plan); setSelectedPlan(plan);
console.log('用户选择了套餐:', plan);
// 这里可以添加订阅逻辑,比如跳转到支付页面或显示确认对话框 // 这里可以添加订阅逻辑,比如跳转到支付页面或显示确认对话框
PurchaseRef.current.show(plan, tabValue); PurchaseRef.current.show(plan, tabValue);
}; };
@ -412,12 +420,6 @@ const OfferDialog = forwardRef<OfferDialogRef>((props, ref) => {
<span className='absolute -top-8 left-16 z-10 rounded-md bg-[#E22C2E] px-2 py-0.5 text-[10px] font-bold leading-none text-white shadow sm:text-xs'> <span className='absolute -top-8 left-16 z-10 rounded-md bg-[#E22C2E] px-2 py-0.5 text-[10px] font-bold leading-none text-white shadow sm:text-xs'>
-20% -20%
{/* 小三角箭头 */} {/* 小三角箭头 */}
{/* <span className="
absolute right-0 top-full
block h-0 w-0
border-l-[6px] border-r-[6px] border-t-[16px]
border-l-transparent border-r-transparent border-t-[#E22C2E]
" />*/}
<span <span
className='absolute right-0 top-[80%] h-10 w-2 bg-[#E22C2E]' className='absolute right-0 top-[80%] h-10 w-2 bg-[#E22C2E]'
style={{ clipPath: 'polygon(100% 0, 100% 100%, 0 0)' }} style={{ clipPath: 'polygon(100% 0, 100% 100%, 0 0)' }}

View File

@ -109,11 +109,11 @@ const Purchase = forwardRef<PurchaseDialogRef, PurchaseProps>((props, ref) => {
const response = await purchase(params as API.PurchaseOrderRequest); const response = await purchase(params as API.PurchaseOrderRequest);
const orderNo = response.data.data?.order_no; const orderNo = response.data.data?.order_no;
if (orderNo) { if (orderNo) {
await getUserInfo();
const data = await purchaseCheckout({ const data = await purchaseCheckout({
orderNo: orderNo, orderNo: orderNo,
returnUrl: window.location.href, returnUrl: window.location.href,
}); });
await getUserInfo();
if (data.data?.type === 'url' && data.data.checkout_url) { if (data.data?.type === 'url' && data.data.checkout_url) {
window.open(data.data.checkout_url, '_blank'); window.open(data.data.checkout_url, '_blank');
} else { } else {

View File

@ -3,7 +3,7 @@
import useGlobalStore from '@/config/use-global'; import useGlobalStore from '@/config/use-global';
import { recharge } from '@/services/user/order'; import { recharge } from '@/services/user/order';
import { AiroButton } from '@workspace/airo-ui/components/AiroButton'; import { AiroButton } from '@workspace/airo-ui/components/AiroButton';
import { Button, ButtonProps } from '@workspace/airo-ui/components/button'; import { ButtonProps } from '@workspace/airo-ui/components/button';
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@ -43,13 +43,13 @@ export default function Recharge(props: Readonly<ButtonProps>) {
</DialogTrigger> </DialogTrigger>
<DialogContent className='flex h-full flex-col overflow-hidden md:h-auto'> <DialogContent className='flex h-full flex-col overflow-hidden md:h-auto'>
<DialogHeader> <DialogHeader>
<DialogTitle>{t('balanceRecharge')}</DialogTitle> <DialogTitle className={'text-4xl'}>{t('balanceRecharge')}</DialogTitle>
<DialogDescription>{t('rechargeDescription')}</DialogDescription> <DialogDescription>{t('rechargeDescription')}</DialogDescription>
</DialogHeader> </DialogHeader>
<div className='flex flex-col justify-between text-sm'> <div className='mt-8 flex flex-col justify-between text-sm'>
<div className='grid gap-3'> <div className='grid gap-3'>
<div className='font-semibold'>{t('rechargeAmount')}</div> <div className='font-semibold'>{t('rechargeAmount')}</div>
<div className='flex'> <div className='mb-8 flex'>
<EnhancedInput <EnhancedInput
type='number' type='number'
placeholder={t('enterAmount')} placeholder={t('enterAmount')}
@ -73,27 +73,30 @@ export default function Recharge(props: Readonly<ButtonProps>) {
onChange={(value) => setParams({ ...params, payment: value })} onChange={(value) => setParams({ ...params, payment: value })}
/> />
</div> </div>
<Button <div className={'flex items-center justify-center'}>
className='fixed bottom-0 left-0 w-full rounded-none md:relative md:mt-6' <AiroButton
disabled={loading || !params.amount} variant={'primary'}
onClick={() => { className='fixed bottom-0 left-0 md:relative md:mt-6'
startTransition(async () => { disabled={loading || !params.amount}
try { onClick={() => {
const response = await recharge(params); startTransition(async () => {
const orderNo = response.data.data?.order_no; try {
if (orderNo) { const response = await recharge(params);
router.push(`/payment?order_no=${orderNo}`); const orderNo = response.data.data?.order_no;
setOpen(false); if (orderNo) {
router.push(`/payment?order_no=${orderNo}`);
setOpen(false);
}
} catch (error) {
/* empty */
} }
} catch (error) { });
/* empty */ }}
} >
}); {loading && <LoaderCircle className='mr-2 animate-spin' />}
}} {t('rechargeNow')}
> </AiroButton>
{loading && <LoaderCircle className='mr-2 animate-spin' />} </div>
{t('rechargeNow')}
</Button>
</div> </div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>

View File

@ -2,6 +2,7 @@ import { NEXT_PUBLIC_API_URL, NEXT_PUBLIC_SITE_URL } from '@/config/constants';
import { queryUserInfo } from '@/services/user/user'; import { queryUserInfo } from '@/services/user/user';
import { extractDomain } from '@workspace/airo-ui/utils'; import { extractDomain } from '@workspace/airo-ui/utils';
import { create } from 'zustand'; import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
export interface GlobalStore { export interface GlobalStore {
common: API.GetGlobalConfigResponse; common: API.GetGlobalConfigResponse;
@ -13,139 +14,141 @@ export interface GlobalStore {
getAppSubLink: (type: string, url: string) => string; getAppSubLink: (type: string, url: string) => string;
} }
export const useGlobalStore = create<GlobalStore>((set, get) => ({ export const useGlobalStore = create<GlobalStore>()(
common: { devtools((set, get) => ({
site: { common: {
host: '', site: {
site_name: '', host: '',
site_desc: '', site_name: '',
site_logo: '', site_desc: '',
keywords: '', site_logo: '',
custom_html: '', keywords: '',
custom_data: '', custom_html: '',
}, custom_data: '',
verify: {
turnstile_site_key: '',
enable_login_verify: false,
enable_register_verify: false,
enable_reset_password_verify: false,
},
auth: {
mobile: {
enable: false,
enable_whitelist: false,
whitelist: [],
}, },
email: { verify: {
enable: false, turnstile_site_key: '',
enable_verify: false, enable_login_verify: false,
enable_domain_suffix: false, enable_register_verify: false,
domain_suffix_list: '', enable_reset_password_verify: false,
}, },
register: { auth: {
stop_register: false, mobile: {
enable_ip_register_limit: false, enable: false,
ip_register_limit: 0, enable_whitelist: false,
ip_register_limit_duration: 0, whitelist: [],
},
email: {
enable: false,
enable_verify: false,
enable_domain_suffix: false,
domain_suffix_list: '',
},
register: {
stop_register: false,
enable_ip_register_limit: false,
ip_register_limit: 0,
ip_register_limit_duration: 0,
},
}, },
}, invite: {
invite: { forced_invite: false,
forced_invite: false, referral_percentage: 0,
referral_percentage: 0, only_first_purchase: false,
only_first_purchase: false,
},
currency: {
currency_unit: 'USD',
currency_symbol: '$',
},
subscribe: {
single_model: false,
subscribe_path: '',
subscribe_domain: '',
pan_domain: false,
},
verify_code: {
verify_code_expire_time: 5,
verify_code_limit: 15,
verify_code_interval: 60,
},
oauth_methods: [],
web_ad: false,
},
user: undefined,
setCommon: (common) =>
set((state) => ({
common: {
...state.common,
...common,
}, },
})), currency: {
setUser: (user) => set({ user }), currency_unit: 'USD',
getUserInfo: async () => { currency_symbol: '$',
try { },
const { data } = await queryUserInfo(); subscribe: {
set({ user: data.data }); single_model: false,
} catch (error) { subscribe_path: '',
console.error('Failed to refresh user:', error); subscribe_domain: '',
} pan_domain: false,
}, },
getUserSubscribe: (uuid: string, type?: string) => { verify_code: {
const { pan_domain, subscribe_domain, subscribe_path } = get().common.subscribe || {}; verify_code_expire_time: 5,
const domains = subscribe_domain verify_code_limit: 15,
? subscribe_domain.split('\n') verify_code_interval: 60,
: [extractDomain(NEXT_PUBLIC_API_URL || NEXT_PUBLIC_SITE_URL || '', pan_domain)]; },
oauth_methods: [],
return domains.map((domain) => { web_ad: false,
if (pan_domain) { },
if (type) return `https://${uuid}.${type}.${domain}`; user: undefined,
return `https://${uuid}.${domain}`; setCommon: (common) =>
} else { set((state) => ({
if (type) return `https://${domain}${subscribe_path}?token=${uuid}&type=${type}`; common: {
return `https://${domain}${subscribe_path}?token=${uuid}`; ...state.common,
...common,
},
})),
setUser: (user) => set({ user }),
getUserInfo: async () => {
try {
const { data } = await queryUserInfo();
set({ user: data.data });
} catch (error) {
console.error('Failed to refresh user:', error);
} }
}); },
}, getUserSubscribe: (uuid: string, type?: string) => {
getAppSubLink: (type: string, url: string) => { const { pan_domain, subscribe_domain, subscribe_path } = get().common.subscribe || {};
const name = get().common?.site?.site_name || ''; const domains = subscribe_domain
switch (type) { ? subscribe_domain.split('\n')
case 'Clash': : [extractDomain(NEXT_PUBLIC_API_URL || NEXT_PUBLIC_SITE_URL || '', pan_domain)];
return `clash://install-config?url=${url}&name=${name}`;
case 'Hiddify': return domains.map((domain) => {
return `hiddify://import/${url}#${name}`; if (pan_domain) {
case 'Loon': if (type) return `https://${uuid}.${type}.${domain}`;
return `loon://import?sub=${encodeURIComponent(url)}`; return `https://${uuid}.${domain}`;
case 'NekoBox': } else {
return `sn://subscription?url=${url}&name=${name}`; if (type) return `https://${domain}${subscribe_path}?token=${uuid}&type=${type}`;
case 'NekoRay': return `https://${domain}${subscribe_path}?token=${uuid}`;
return `sn://subscription?url=${url}&name=${name}`; }
// case 'Netch': });
// return ``; },
case 'Quantumult X': getAppSubLink: (type: string, url: string) => {
return `quantumult-x://add-resource?remote-resource=${encodeURIComponent( const name = get().common?.site?.site_name || '';
JSON.stringify({ switch (type) {
server_remote: [`${url}, tag=${name}`], case 'Clash':
}), return `clash://install-config?url=${url}&name=${name}`;
)}`; case 'Hiddify':
case 'Shadowrocket': return `hiddify://import/${url}#${name}`;
return `shadowrocket://add/sub://${window.btoa(url)}?remark=${encodeURIComponent(name)}`; case 'Loon':
case 'Singbox': return `loon://import?sub=${encodeURIComponent(url)}`;
return `sing-box://import-remote-profile?url=${encodeURIComponent(url)}#${name}`; case 'NekoBox':
case 'Surfboard': return `sn://subscription?url=${url}&name=${name}`;
return `surfboard:///install-config?url=${encodeURIComponent(url)}`; case 'NekoRay':
case 'Surge': return `sn://subscription?url=${url}&name=${name}`;
return `surge:///install-config?url=${encodeURIComponent(url)}`; // case 'Netch':
case 'V2box': // return ``;
return `v2box://install-sub?url=${encodeURIComponent(url)}&name=${name}`; case 'Quantumult X':
// case 'V2rayN': return `quantumult-x://add-resource?remote-resource=${encodeURIComponent(
// return `v2rayn://install-sub?url=${encodeURIComponent(url)}&name=${name}`; JSON.stringify({
case 'V2rayNg': server_remote: [`${url}, tag=${name}`],
return `v2rayng://install-sub?url=${encodeURIComponent(url)}#${name}`; }),
case 'Stash': )}`;
return `stash://install-config?url=${encodeURIComponent(url)}&name=${name}`; case 'Shadowrocket':
default: return `shadowrocket://add/sub://${window.btoa(url)}?remark=${encodeURIComponent(name)}`;
return ''; case 'Singbox':
} return `sing-box://import-remote-profile?url=${encodeURIComponent(url)}#${name}`;
}, case 'Surfboard':
})); return `surfboard:///install-config?url=${encodeURIComponent(url)}`;
case 'Surge':
return `surge:///install-config?url=${encodeURIComponent(url)}`;
case 'V2box':
return `v2box://install-sub?url=${encodeURIComponent(url)}&name=${name}`;
// case 'V2rayN':
// return `v2rayn://install-sub?url=${encodeURIComponent(url)}&name=${name}`;
case 'V2rayNg':
return `v2rayng://install-sub?url=${encodeURIComponent(url)}#${name}`;
case 'Stash':
return `stash://install-config?url=${encodeURIComponent(url)}&name=${name}`;
default:
return '';
}
},
})),
);
export default useGlobalStore; export default useGlobalStore;

View File

@ -319,6 +319,9 @@ declare namespace API {
forced_invite: boolean; forced_invite: boolean;
referral_percentage: number; referral_percentage: number;
only_first_purchase: boolean; only_first_purchase: boolean;
first_purchase_percentage: number;
first_yearly_purchase_percentage: number;
non_first_purchase_percentage: number;
}; };
type LoginResponse = { type LoginResponse = {

View File

@ -10,8 +10,8 @@ const buttonVariants = cva(
variants: { variants: {
variant: { variant: {
default: 'bg-[#0F2C53] text-primary-foreground shadow hover:bg-[#225BA9]', default: 'bg-[#0F2C53] text-primary-foreground shadow hover:bg-[#225BA9]',
primary: 'bg-[#A8D4ED] text-primary-foreground shadow hover:bg-[#225BA9] ', primary: 'bg-[#225BA9] text-primary-foreground shadow hover:bg-[#0F2C53] ',
danger: 'bg-[#F8BFD2] text-primary-foreground shadow hover:bg-[#FF4248]', danger: 'bg-[#FF4248] text-primary-foreground shadow hover:bg-[#E22C2E]',
destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90', destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
outline: outline:
'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground', 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',

View File

@ -116,7 +116,9 @@ export function EnhancedInput({
const renderPrefix = () => { const renderPrefix = () => {
return typeof prefix === 'string' ? ( return typeof prefix === 'string' ? (
<div className='bg-muted relative mr-px flex h-9 items-center text-nowrap px-3'>{prefix}</div> <div className='relative mr-px flex h-9 min-w-[50px] items-center justify-center text-nowrap bg-[#0F2C53] pl-3 pr-1 font-bold text-white'>
{prefix}
</div>
) : ( ) : (
prefix prefix
); );
@ -124,7 +126,9 @@ export function EnhancedInput({
const renderSuffix = () => { const renderSuffix = () => {
return typeof suffix === 'string' ? ( return typeof suffix === 'string' ? (
<div className='bg-muted relative ml-px flex h-9 items-center text-nowrap px-3'>{suffix}</div> <div className='relative ml-px flex h-9 items-center text-nowrap bg-[#0F2C53] px-3 font-bold text-white'>
{suffix}
</div>
) : ( ) : (
suffix suffix
); );
@ -133,20 +137,22 @@ export function EnhancedInput({
return ( return (
<div <div
className={cn( className={cn(
'border-input flex w-full items-center overflow-hidden rounded-md border', 'border-input flex w-full items-center overflow-hidden rounded-full border bg-[#0F2C53]',
className, className,
)} )}
suppressHydrationWarning suppressHydrationWarning
> >
{renderPrefix()} {renderPrefix()}
<Input <div className={'flex-1 py-[3px]'}>
step={0.01} <Input
{...props} step={0.01}
value={value} {...props}
className='block rounded-none border-none' value={value}
onChange={handleChange} className='block h-[44px] rounded-full border-none bg-white shadow-[inset_0_0_7.6px_0_rgba(0,0,0,0.25)]'
onBlur={handleBlur} onChange={handleChange}
/> onBlur={handleBlur}
/>
</div>
{renderSuffix()} {renderSuffix()}
</div> </div>
); );