feat: 修改样式
This commit is contained in:
parent
cc522d0754
commit
265519a03d
@ -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;
|
||||||
@ -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>
|
|
||||||
|
|
||||||
</>
|
|
||||||
)}*/}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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')}
|
||||||
|
|||||||
@ -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')}
|
||||||
|
|||||||
63
apps/user/components/Modal.tsx
Normal file
63
apps/user/components/Modal.tsx
Normal 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;
|
||||||
@ -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'>
|
||||||
|
|||||||
@ -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)' }}
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
3
apps/user/services/common/typings.d.ts
vendored
3
apps/user/services/common/typings.d.ts
vendored
@ -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 = {
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user