fix: pc端修改
This commit is contained in:
+1
-1
@@ -3,7 +3,7 @@ NEXT_PUBLIC_DEFAULT_LANGUAGE=en-US
|
||||
|
||||
# Site URL and API URL
|
||||
NEXT_PUBLIC_SITE_URL=https://user.ppanel.dev
|
||||
NEXT_PUBLIC_API_URL=https://api.kxsw.us
|
||||
NEXT_PUBLIC_API_URL=https://api.ppanel.dev
|
||||
NEXT_PUBLIC_CDN_URL=https://cdn.jsdelivr.net
|
||||
|
||||
# Home Page Settings
|
||||
|
||||
@@ -1,14 +1,40 @@
|
||||
'use client';
|
||||
|
||||
import { Display } from '@/components/display';
|
||||
import Recharge from '@/components/subscribe/recharge';
|
||||
import Renewal from '@/components/subscribe/renewal';
|
||||
import ResetTraffic from '@/components/subscribe/reset-traffic';
|
||||
import useGlobalStore from '@/config/use-global';
|
||||
import { getStat } from '@/services/common/common';
|
||||
import { queryApplicationConfig } from '@/services/user/subscribe';
|
||||
import { queryUserSubscribe } from '@/services/user/user';
|
||||
import { getPlatform } from '@/utils/common';
|
||||
import { queryUserSubscribe, resetUserSubscribeToken } from '@/services/user/user';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Button } from '@workspace/ui/components/button';
|
||||
import { Card } from '@workspace/ui/components/card';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@workspace/ui/components/select';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { Empty } from '@/components/empty';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from '@workspace/ui/components/alert-dialog';
|
||||
import { Tabs, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
|
||||
import { differenceInDays, formatDate } from '@workspace/ui/utils';
|
||||
|
||||
const platforms: (keyof API.ApplicationPlatform)[] = [
|
||||
'windows',
|
||||
@@ -24,7 +50,8 @@ export default function Content() {
|
||||
const { getUserSubscribe, getAppSubLink } = useGlobalStore();
|
||||
|
||||
const [protocol, setProtocol] = useState('');
|
||||
|
||||
const [userSubscribeProtocol, setUserSubscribeProtocol] = useState<string[]>([]);
|
||||
const [userSubscribeProtocolCurrent, setUserSubscribeProtocolCurrent] = useState<string>('');
|
||||
const {
|
||||
data: userSubscribe = [],
|
||||
refetch,
|
||||
@@ -36,16 +63,8 @@ export default function Content() {
|
||||
return data.data?.list || [];
|
||||
},
|
||||
});
|
||||
const { data: applications } = useQuery({
|
||||
queryKey: ['queryApplicationConfig'],
|
||||
queryFn: async () => {
|
||||
const { data } = await queryApplicationConfig();
|
||||
return data.data?.applications || [];
|
||||
},
|
||||
});
|
||||
const [platform, setPlatform] = useState<keyof API.ApplicationPlatform>(getPlatform());
|
||||
|
||||
const { data } = useQuery({
|
||||
/*const { data } = useQuery({
|
||||
queryKey: ['getStat'],
|
||||
queryFn: async () => {
|
||||
const { data } = await getStat({
|
||||
@@ -54,7 +73,21 @@ export default function Content() {
|
||||
return data.data;
|
||||
},
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
});*/
|
||||
const data = {
|
||||
user: 1,
|
||||
node: 2,
|
||||
country: 0,
|
||||
protocol: ['vmess', 'vless'],
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (data && userSubscribe?.length > 0 && !userSubscribeProtocol.length) {
|
||||
const list = getUserSubscribe(userSubscribe[0]?.token, data.protocol);
|
||||
setUserSubscribeProtocol(list);
|
||||
setUserSubscribeProtocolCurrent(list[0]);
|
||||
}
|
||||
}, [data, userSubscribe, userSubscribeProtocol.length]);
|
||||
|
||||
const statusWatermarks = {
|
||||
2: t('finished'),
|
||||
@@ -63,6 +96,7 @@ export default function Content() {
|
||||
};
|
||||
|
||||
const { user } = useGlobalStore();
|
||||
const totalAssets = (user?.balance || 0) + (user?.commission || 0) + (user?.gift_amount || 0);
|
||||
return (
|
||||
<>
|
||||
<div className={'grid grid-cols-1 gap-6 lg:grid-cols-2'}>
|
||||
@@ -79,12 +113,18 @@ export default function Content() {
|
||||
<span className='text-3xl font-medium text-[#091B33]'>年度套餐用户</span>
|
||||
</div>
|
||||
|
||||
<div className='rounded-[20px] bg-[#EAEAEA] p-4'>
|
||||
<div className='mb-2 flex items-center justify-between'>
|
||||
<span className='text-sm text-[#666666]'>账户余额</span>
|
||||
<span className='text-sm text-[#225BA9]'>钱包充值</span>
|
||||
<div className='rounded-[20px] bg-[#EAEAEA] px-4 py-[10px]'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<span className='text-sm font-light text-[#666666]'>账户余额</span>
|
||||
<Recharge
|
||||
className={
|
||||
'border-0 bg-transparent p-0 text-sm font-normal text-[#225BA9] shadow-none outline-0 hover:bg-transparent'
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className='text-4xl font-medium text-[#225BA9]'>
|
||||
<Display type='currency' value={totalAssets} />
|
||||
</div>
|
||||
<div className='text-3xl font-medium text-[#225BA9]'>$1680.00</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -92,55 +132,92 @@ export default function Content() {
|
||||
<Card className='rounded-[20px] border border-[#D9D9D9] p-6 text-[#666666] shadow-[0px_0px_52.6px_1px_rgba(15,44,83,0.05)]'>
|
||||
<div className='mb-4'>
|
||||
<h3 className='flex items-center justify-between text-[#666666]'>
|
||||
<div>
|
||||
<span className={'text-xl'}>套餐状态</span>
|
||||
<div className={'flex items-center justify-between'}>
|
||||
<span className={'text-xl font-medium'}>套餐状态</span>
|
||||
<span className={'ml-2.5 rounded-full bg-[#A8D4ED] px-2 text-[8px] text-white'}>
|
||||
生效中
|
||||
</span>
|
||||
</div>
|
||||
<span className={'text-sm text-[#225BA9]'}>流量重置</span>
|
||||
<ResetTraffic
|
||||
className={
|
||||
'border-0 bg-transparent p-0 text-sm font-normal text-[#225BA9] shadow-none outline-0 hover:bg-transparent'
|
||||
}
|
||||
id={userSubscribe?.[0]?.id}
|
||||
replacement={userSubscribe?.[0]?.subscribe.replacement}
|
||||
/>
|
||||
</h3>
|
||||
<div>
|
||||
<p className='mb-2 text-sm text-[#666666]'>套餐到期时间:2026-07-29</p>
|
||||
<div className='mb-[22px] mt-1 text-sm text-[#666666]'>
|
||||
套餐到期时间:{formatDate(userSubscribe?.[0]?.expire_time, false)}
|
||||
</div>
|
||||
<div className='mt-2'>
|
||||
<span className='text-3xl font-medium text-[#091B33]'>Pro Plan</span>
|
||||
<div className='mb-6'>
|
||||
<span className='text-3xl font-medium text-[#091B33]'>
|
||||
{userSubscribe?.[0]?.subscribe.name}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='space-y-4'>
|
||||
<div className='space-y-3'>
|
||||
<div className='mb-2 flex items-center justify-between'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<span className='text-sm'>可用设备</span>
|
||||
<div className='flex gap-2'>
|
||||
<div className='h-4 w-4 rounded-full bg-[#225BA9]'></div>
|
||||
<div className='h-4 w-4 rounded-full bg-[#D9D9D9]'></div>
|
||||
<div className='h-4 w-4 rounded-full bg-[#D9D9D9]'></div>
|
||||
<div className='h-4 w-4 rounded-full bg-[#D9D9D9]'></div>
|
||||
<div className='h-4 w-4 rounded-full bg-[#D9D9D9]'></div>
|
||||
<div className='h-4 w-4 rounded-full bg-[#D9D9D9]'></div>
|
||||
</div>
|
||||
</div>
|
||||
<span className='text-sm'>在线:1/6</span>
|
||||
</div>
|
||||
<div>
|
||||
<div className='mb-1 flex items-center justify-between'>
|
||||
<span className='text-sm'>已使用流量/总流量:5.52GB/100GB</span>
|
||||
<span className='text-sm'>剩余:94%</span>
|
||||
</div>
|
||||
<div className='flex h-5 w-full items-center rounded-[20px] bg-[#EAEAEA] p-0.5'>
|
||||
<div className='h-full rounded-[20px] bg-[#225BA9]' style={{ width: '6%' }}></div>
|
||||
</div>
|
||||
<div className='mb-4 flex items-center justify-between'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<span className='text-sm'>可用设备</span>
|
||||
<div className='flex gap-2'>
|
||||
<div className='h-4 w-4 rounded-full bg-[#225BA9]'></div>
|
||||
<div className='h-4 w-4 rounded-full bg-[#D9D9D9]'></div>
|
||||
<div className='h-4 w-4 rounded-full bg-[#D9D9D9]'></div>
|
||||
<div className='h-4 w-4 rounded-full bg-[#D9D9D9]'></div>
|
||||
<div className='h-4 w-4 rounded-full bg-[#D9D9D9]'></div>
|
||||
<div className='h-4 w-4 rounded-full bg-[#D9D9D9]'></div>
|
||||
</div>
|
||||
</div>
|
||||
<span className='text-sm'>
|
||||
在线:undefined/{userSubscribe?.[0]?.subscribe.device_limit}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<div className='mb-1 flex items-center justify-between'>
|
||||
<span className='text-sm'>
|
||||
已使用流量/总流量:
|
||||
<Display
|
||||
type='traffic'
|
||||
value={userSubscribe?.[0]?.upload + userSubscribe?.[0]?.download}
|
||||
unlimited={!userSubscribe?.[0]?.traffic}
|
||||
/>
|
||||
/{' '}
|
||||
<Display
|
||||
type='traffic'
|
||||
value={userSubscribe?.[0]?.traffic}
|
||||
unlimited={!userSubscribe?.[0]?.traffic}
|
||||
/>
|
||||
</span>
|
||||
<span className='text-sm'>
|
||||
剩余:
|
||||
{100 -
|
||||
(
|
||||
(userSubscribe?.[0]?.upload + userSubscribe?.[0]?.download) /
|
||||
userSubscribe?.[0]?.traffic
|
||||
).toFixed(0)}
|
||||
%
|
||||
</span>
|
||||
</div>
|
||||
<div className='flex h-5 w-full items-center rounded-[20px] bg-[#EAEAEA] p-0.5'>
|
||||
<div
|
||||
className='h-full rounded-[20px] bg-[#225BA9]'
|
||||
style={{
|
||||
width: `${((userSubscribe?.[0]?.upload + userSubscribe?.[0]?.download) / userSubscribe?.[0]?.traffic).toFixed(0)}%`,
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
{/* 网站公告 Card */}
|
||||
<Card className='rounded-[20px] border border-[#EAEAEA] bg-gradient-to-b from-white to-[#EAEAEA] p-6'>
|
||||
<Card className='relative rounded-[20px] border border-[#EAEAEA] bg-gradient-to-b from-white to-[#EAEAEA] p-6 pb-0'>
|
||||
<div
|
||||
className={'absolute bottom-0 left-0 right-0 h-[60px] bg-white/30 backdrop-blur-[1px]'}
|
||||
></div>
|
||||
<div className='mb-4 flex items-center justify-between'>
|
||||
<h3 className='text-xl font-medium text-[#666666]'>网站公告</h3>
|
||||
<span className='text-sm text-[#225BA9]'>更多</span>
|
||||
<Button className='border-0 bg-transparent p-0 text-sm font-normal text-[#225BA9] shadow-none outline-0 hover:bg-transparent'>
|
||||
更多
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className='space-y-4'>
|
||||
@@ -157,8 +234,8 @@ export default function Content() {
|
||||
|
||||
{/* 系统通知列表 */}
|
||||
<div className='space-y-3'>
|
||||
<div className='rounded-[20px] border bg-white p-4'>
|
||||
<p className='mb-2 line-clamp-2 text-sm text-[#225BA9]'>
|
||||
<div className='flex items-center gap-2 rounded-[20px] border bg-white p-4'>
|
||||
<p className='mb-2 line-clamp-2 flex-1 text-sm text-[#225BA9]'>
|
||||
【系统通知】充值成功
|
||||
<br />
|
||||
订单 ID:R20250729115302USDT 充值成功,钱包余额...
|
||||
@@ -169,18 +246,7 @@ export default function Content() {
|
||||
</div>
|
||||
|
||||
<div className='rounded-[20px] border bg-white p-4'>
|
||||
<p className='mb-2 line-clamp-2 text-sm text-[#225BA9]'>
|
||||
【系统通知】充值成功
|
||||
<br />
|
||||
订单 ID:R20250729115302USDT 充值成功,钱包余额...
|
||||
</p>
|
||||
<div className='text-right'>
|
||||
<span className='text-sm text-[#225BA9]'>查看详情</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='rounded-[20px] border bg-white p-4'>
|
||||
<p className='mb-2 line-clamp-2 text-sm text-[#225BA9]'>
|
||||
<p className='mb-2 line-clamp-2 flex-1 text-sm text-[#225BA9]'>
|
||||
【系统通知】充值成功
|
||||
<br />
|
||||
订单 ID:R20250729115302USDT 充值成功,钱包余额...
|
||||
@@ -195,79 +261,165 @@ export default function Content() {
|
||||
|
||||
{/* 我的订阅 Card */}
|
||||
<Card className='rounded-[20px] border border-[#D9D9D9] p-6 shadow-[0px_0px_52.6px_1px_rgba(15,44,83,0.05)]'>
|
||||
<div className='mb-4'>
|
||||
<div className='mb-4 flex items-center justify-between'>
|
||||
<h3 className='text-xl font-medium text-[#666666]'>我的订阅</h3>
|
||||
<Link
|
||||
href={'/document'}
|
||||
className='border-0 bg-transparent p-0 text-sm font-normal text-[#225BA9] shadow-none outline-0 hover:bg-transparent'
|
||||
>
|
||||
新手教程
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className='space-y-4'>
|
||||
<p className='text-sm text-[#666666]'>直接复制订阅链接或点击二维码按钮扫码获取</p>
|
||||
{userSubscribe?.[0] && data.protocol ? (
|
||||
<div className='space-y-4'>
|
||||
<p className='text-sm text-[#666666]'>直接复制订阅链接或点击二维码按钮扫码获取</p>
|
||||
|
||||
{/* 统计信息 */}
|
||||
<div className='rounded-[20px] bg-[#EAEAEA] p-4'>
|
||||
<div className='grid grid-cols-3 gap-4 text-center'>
|
||||
<div>
|
||||
<p className='text-xs text-[rgba(132,132,132,0.7)]'>总流量</p>
|
||||
<p className='text-lg font-medium text-[#0F2C53]'>100.00 GB</p>
|
||||
{/* 统计信息 */}
|
||||
<div className='rounded-[20px] bg-[#EAEAEA] p-4'>
|
||||
<div className='grid grid-cols-3 gap-4 text-center'>
|
||||
<div>
|
||||
<p className='text-xs text-[rgba(132,132,132,0.7)]'>总流量</p>
|
||||
<p className='text-lg font-medium text-[#0F2C53]'>
|
||||
<Display
|
||||
type='traffic'
|
||||
value={userSubscribe?.[0]?.traffic}
|
||||
unlimited={!userSubscribe?.[0]?.traffic}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className='text-xs text-[rgba(132,132,132,0.7)]'>{t('nextResetDays')}</p>
|
||||
<p className='text-lg font-medium text-[#0F2C53]'>
|
||||
{userSubscribe?.[0]
|
||||
? differenceInDays(new Date(userSubscribe?.[0].reset_time), new Date())
|
||||
: t('noReset')}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className='text-xs text-[rgba(132,132,132,0.7)]'>{t('expirationDays')}</p>
|
||||
<p className='text-lg font-medium text-[#0F2C53]'>
|
||||
{userSubscribe?.[0]?.expire_time
|
||||
? differenceInDays(new Date(userSubscribe?.[0].expire_time), new Date()) ||
|
||||
t('unknown')
|
||||
: t('noLimit')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className='text-xs text-[rgba(132,132,132,0.7)]'>下次重置/天</p>
|
||||
<p className='text-lg font-medium text-[#0F2C53]'>28</p>
|
||||
</div>
|
||||
|
||||
{/* 订阅链接 */}
|
||||
<div className='rounded-[26px] bg-[#EAEAEA] p-4'>
|
||||
<div className='mb-3 flex flex-wrap justify-between gap-4'>
|
||||
{data?.protocol && data?.protocol.length > 1 && (
|
||||
<Tabs
|
||||
value={protocol}
|
||||
onValueChange={setProtocol}
|
||||
className='w-full max-w-full md:w-auto'
|
||||
>
|
||||
<TabsList className='flex h-full flex-wrap gap-2 bg-transparent p-0 *:flex-auto'>
|
||||
{['all', ...(data?.protocol || [])].map((item) => (
|
||||
<TabsTrigger
|
||||
value={item === 'all' ? '' : item}
|
||||
key={item}
|
||||
className='un rounded-full bg-[#EAEAEA] px-6 py-1 text-[10px] uppercase text-[#66666673] shadow-[inset_0px_0px_4px_0px_rgba(0,0,0,0.25)] data-[state=active]:bg-[#225BA9] data-[state=active]:text-white'
|
||||
>
|
||||
{item}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<p className='text-xs text-[rgba(132,132,132,0.7)]'>到期时间/天</p>
|
||||
<p className='text-lg font-medium text-[#0F2C53]'>363</p>
|
||||
<div className={'mb-3 flex items-center justify-center gap-1'}>
|
||||
<div className={'flex items-center gap-2 rounded-[16px] bg-[#BABABA] pl-2'}>
|
||||
<Select
|
||||
value={userSubscribeProtocolCurrent}
|
||||
onValueChange={setUserSubscribeProtocolCurrent}
|
||||
>
|
||||
<SelectTrigger className='h-[35px] w-20 flex-shrink-0 rounded-[8px] border-none bg-[#D9D9D9] bg-transparent p-2 text-[13px] text-sm font-medium text-white shadow-none hover:bg-[#848484] focus:ring-0 [&>svg]:hidden'>
|
||||
<SelectValue>
|
||||
<div className='flex flex-col items-center justify-center'>
|
||||
<div>{t('subscriptionUrl')}1</div>
|
||||
<div className='h-0 w-0 border-l-[5px] border-r-[5px] border-t-[5px] border-l-transparent border-r-transparent border-t-white'></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-[16px] bg-white p-3 text-xs text-[#225BA9] shadow-[inset_0px_0px_7.6px_0px_rgba(0,0,0,0.25)]'>
|
||||
<div className={'line-clamp-2 break-all'}>{userSubscribeProtocolCurrent}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={'ml-3 h-[40px] w-[40px] flex-shrink-0 rounded-lg bg-black'}></div>
|
||||
</div>
|
||||
<div className='flex justify-between gap-2'>
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button
|
||||
size='sm'
|
||||
className={'rounded-full bg-[#E22C2E] px-3 py-1 text-xs text-white'}
|
||||
variant='destructive'
|
||||
>
|
||||
{t('resetSubscription')}
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{t('prompt')}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{t('confirmResetSubscription')}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>{t('cancel')}</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={async () => {
|
||||
await resetUserSubscribeToken({
|
||||
user_subscribe_id: userSubscribe?.[0]?.id,
|
||||
});
|
||||
await refetch();
|
||||
toast.success(t('resetSuccess'));
|
||||
}}
|
||||
>
|
||||
{t('confirm')}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<Renewal
|
||||
className='rounded-full bg-[#A8D4ED] px-3 py-1 text-xs text-white'
|
||||
id={userSubscribe?.[0]?.id}
|
||||
subscribe={userSubscribe?.[0]?.subscribe}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 订阅链接 */}
|
||||
<div className='rounded-[26px] bg-[#EAEAEA] p-4'>
|
||||
<div className='mb-2'>
|
||||
{/* 协议选择 */}
|
||||
<div className='flex flex-wrap gap-2'>
|
||||
<button className='rounded-full bg-[#225BA9] px-4 py-1 text-xs text-white'>
|
||||
ALL
|
||||
</button>
|
||||
<button className='rounded-full bg-[#EAEAEA] px-4 py-1 text-xs text-[#666666] shadow-[inset_0px_0px_4px_0px_rgba(0,0,0,0.25)]'>
|
||||
VMESS
|
||||
</button>
|
||||
<button className='rounded-full bg-[#EAEAEA] px-4 py-1 text-xs text-[#666666] shadow-[inset_0px_0px_4px_0px_rgba(0,0,0,0.25)]'>
|
||||
VLESS
|
||||
</button>
|
||||
<button className='rounded-full bg-[#EAEAEA] px-4 py-1 text-xs text-[#666666] shadow-[inset_0px_0px_4px_0px_rgba(0,0,0,0.25)]'>
|
||||
TROJAN
|
||||
</button>
|
||||
<button className='rounded-full bg-[#EAEAEA] px-4 py-1 text-xs text-[#666666] shadow-[inset_0px_0px_4px_0px_rgba(0,0,0,0.25)]'>
|
||||
SHADOW SOCKS
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className={'mb-2 flex items-center justify-center gap-1'}>
|
||||
<span className='w-20 flex-shrink-0 text-sm font-medium text-[#666666]'>
|
||||
订阅地址 1
|
||||
</span>
|
||||
<div className='line-clamp-2 flex-1 rounded-[16px] bg-white p-3 text-xs text-[#225BA9] shadow-[inset_0px_0px_7.6px_0px_rgba(0,0,0,0.25)]'>
|
||||
https://api.kxsw.us/api/subscribe?token=512ce3958ef939c668aaf8442d51dd5f
|
||||
</div>
|
||||
<div className={'size-6 flex-shrink-0 rounded-lg bg-black'}></div>
|
||||
</div>
|
||||
<div className='flex justify-between gap-2'>
|
||||
<button className='rounded-full bg-[#E22C2E] px-3 py-1 text-xs text-white'>
|
||||
重置订阅地址
|
||||
</button>
|
||||
<button className='rounded-full bg-[#A8D4ED] px-3 py-1 text-xs text-white'>
|
||||
续订套餐
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Empty />
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
{/*{userSubscribe.length ? (
|
||||
<>
|
||||
<div className='flex items-center justify-between'>
|
||||
<h2 className='flex items-center gap-1.5 font-semibold'>
|
||||
<Icon icon='uil:servers' className='size-5' />
|
||||
|
||||
{t('mySubscriptions')}
|
||||
</h2>
|
||||
<div className='flex gap-2'>
|
||||
@@ -286,27 +438,7 @@ export default function Content() {
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className='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 *:flex-auto'>
|
||||
{['all', ...(data?.protocol || [])].map((item) => (
|
||||
<TabsTrigger
|
||||
value={item === 'all' ? '' : item}
|
||||
key={item}
|
||||
className='px-1 uppercase lg:px-3'
|
||||
>
|
||||
{item}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{userSubscribe.map((item) => {
|
||||
return (
|
||||
<Card
|
||||
@@ -426,7 +558,6 @@ export default function Content() {
|
||||
<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')}
|
||||
@@ -443,104 +574,11 @@ export default function Content() {
|
||||
{t('subscriptionUrl')} {index + 1}
|
||||
</CardTitle>
|
||||
|
||||
<CopyToClipboard
|
||||
text={url}
|
||||
onCopy={(text, result) => {
|
||||
if (result) {
|
||||
toast.success(t('copySuccess'));
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className='text-primary hover:bg-accent mr-4 flex cursor-pointer rounded p-2 text-sm'
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Icon icon='uil:copy' className='mr-2 size-5' />
|
||||
{t('copy')}
|
||||
</span>
|
||||
</CopyToClipboard>
|
||||
111
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className='grid grid-cols-2 gap-4 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6'>
|
||||
{applications
|
||||
?.filter((application) => {
|
||||
const platformApps = application.platform?.[platform];
|
||||
return platformApps && platformApps.length > 0;
|
||||
})
|
||||
.map((application) => {
|
||||
const platformApps = application.platform?.[platform];
|
||||
const app =
|
||||
platformApps?.find((item) => item.is_default) ||
|
||||
platformApps?.[0];
|
||||
if (!app) return null;
|
||||
|
||||
const handleCopy = (text: string, result: boolean) => {
|
||||
if (result) {
|
||||
const href = getAppSubLink(application.subscribe_type, url);
|
||||
const showSuccessMessage = () => {
|
||||
toast.success(
|
||||
<>
|
||||
<p>{t('copySuccess')}</p>
|
||||
<br />
|
||||
<p>{t('manualImportMessage')}</p>
|
||||
</>,
|
||||
);
|
||||
};
|
||||
|
||||
if (isBrowser() && href) {
|
||||
window.location.href = href;
|
||||
const checkRedirect = setTimeout(() => {
|
||||
if (window.location.href !== href) {
|
||||
showSuccessMessage();
|
||||
}
|
||||
clearTimeout(checkRedirect);
|
||||
}, 1000);
|
||||
return;
|
||||
}
|
||||
|
||||
showSuccessMessage();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
key={application.name}
|
||||
className='text-muted-foreground flex size-full flex-col items-center justify-between gap-2 text-xs'
|
||||
>
|
||||
<span>{application.name}</span>
|
||||
|
||||
{application.icon && (
|
||||
<Image
|
||||
src={application.icon}
|
||||
alt={application.name}
|
||||
width={64}
|
||||
height={64}
|
||||
className='p-1'
|
||||
/>
|
||||
)}
|
||||
<div className='flex'>
|
||||
<Button
|
||||
size='sm'
|
||||
variant='secondary'
|
||||
className='rounded-r-none px-1.5'
|
||||
asChild
|
||||
>
|
||||
<Link href={app.url}>{t('download')}</Link>
|
||||
</Button>
|
||||
|
||||
<CopyToClipboard
|
||||
text={getAppSubLink(application.subscribe_type, url) || url}
|
||||
onCopy={handleCopy}
|
||||
>
|
||||
<Button size='sm' className='rounded-l-none p-2'>
|
||||
{t('import')}
|
||||
</Button>
|
||||
</CopyToClipboard>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className='text-muted-foreground hidden size-full flex-col items-center justify-between gap-2 text-sm lg:flex'>
|
||||
<span>{t('qrCode')}</span>
|
||||
<QRCodeCanvas
|
||||
@@ -567,7 +605,7 @@ export default function Content() {
|
||||
<Icon icon='uil:shop' className='size-5' />
|
||||
{t('purchaseSubscription')}
|
||||
</h2>
|
||||
<Subscribe />
|
||||
|
||||
</>
|
||||
)}*/}
|
||||
</>
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import Announcement from '@/components/announcement';
|
||||
import { cookies } from 'next/headers';
|
||||
import Content from './content';
|
||||
|
||||
export default async function Page() {
|
||||
return (
|
||||
<div className='flex min-h-[calc(100vh-64px-58px-32px-114px)] w-full flex-col gap-4 overflow-hidden'>
|
||||
<Announcement type='pinned' Authorization={(await cookies()).get('Authorization')?.value} />
|
||||
<Content />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
'use client';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Dialog, DialogContent } from '@workspace/airo-ui/components/dialog';
|
||||
import { Separator } from '@workspace/ui/components/separator';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
|
||||
import { SubscribeBilling } from '@/components/subscribe/billing';
|
||||
import { SubscribeDetail } from '@/components/subscribe/detail';
|
||||
import useGlobalStore from '@/config/use-global';
|
||||
import { queryOrderDetail } from '@/services/user/order';
|
||||
|
||||
interface OrderDetailDialogProps {
|
||||
orderNo?: string;
|
||||
}
|
||||
|
||||
interface OrderDetailDialogRef {
|
||||
show: (orderNo: string) => void;
|
||||
hide: () => void;
|
||||
}
|
||||
|
||||
const OrderDetailDialog = forwardRef<OrderDetailDialogRef, OrderDetailDialogProps>((props, ref) => {
|
||||
const t = useTranslations('subscribe');
|
||||
const { getUserInfo } = useGlobalStore();
|
||||
const router = useRouter();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [orderNo, setOrderNo] = useState<string | undefined>(props.orderNo);
|
||||
const [enabled, setEnabled] = useState(true);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
show: (newOrderNo: string) => {
|
||||
setOrderNo(newOrderNo);
|
||||
setEnabled(true);
|
||||
setOpen(true);
|
||||
},
|
||||
hide: () => {
|
||||
setOpen(false);
|
||||
setOrderNo(undefined);
|
||||
setEnabled(false);
|
||||
},
|
||||
}));
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
enabled: enabled && !!orderNo,
|
||||
queryKey: ['queryOrderDetail', orderNo],
|
||||
queryFn: async () => {
|
||||
const { data } = await queryOrderDetail({ order_no: orderNo! });
|
||||
if (data?.data?.status !== 1) {
|
||||
getUserInfo();
|
||||
setEnabled(false);
|
||||
}
|
||||
return data?.data;
|
||||
},
|
||||
refetchInterval: 3000,
|
||||
});
|
||||
|
||||
const handleClose = () => {
|
||||
setOpen(false);
|
||||
setOrderNo(undefined);
|
||||
setEnabled(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent className='sm:w-[675px]'>
|
||||
<div className='text-4xl font-bold text-[#0F2C53] sm:mb-8 sm:text-center sm:text-4xl'>
|
||||
订单详情
|
||||
</div>
|
||||
|
||||
<div className='text-[16px] font-bold text-[#666]'>
|
||||
<div>订单号</div>
|
||||
<div className='text-[12px] font-light text-[#4D4D4D]'>{orderNo}</div>
|
||||
</div>
|
||||
<Separator className='mb-3 mt-2 h-[2px] bg-[#225BA9]' />
|
||||
<div className='text-[15px] text-[#225BA9]'>
|
||||
<div>支付方式</div>
|
||||
<div className='font-light text-[#666]'>钱包余额</div>
|
||||
</div>
|
||||
<Separator className='mb-3 mt-4 bg-[#225BA9]' />
|
||||
<div>
|
||||
<SubscribeDetail
|
||||
subscribe={{
|
||||
...data?.subscribe,
|
||||
quantity: data?.quantity,
|
||||
}}
|
||||
/>
|
||||
<Separator className='mb-3 mt-4 bg-[#225BA9]' />
|
||||
<SubscribeBilling
|
||||
order={{
|
||||
...data,
|
||||
unit_price: data?.subscribe?.unit_price,
|
||||
}}
|
||||
/>
|
||||
<Separator className='mb-3 mt-4 bg-[#225BA9]' />
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
});
|
||||
|
||||
OrderDetailDialog.displayName = 'OrderDetailDialog';
|
||||
export default OrderDetailDialog;
|
||||
@@ -9,80 +9,88 @@ import { Card, CardContent } from '@workspace/ui/components/card';
|
||||
import { formatDate } from '@workspace/ui/utils';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useRef } from 'react';
|
||||
import OrderDetailDialog from './components/OrderDetailDialog';
|
||||
|
||||
export default function Page() {
|
||||
const t = useTranslations('order');
|
||||
|
||||
const ref = useRef<ProListActions>(null);
|
||||
const OrderDetailDialogRef = useRef<typeof OrderDetailDialog>(null);
|
||||
return (
|
||||
<ProList<API.OrderDetail, Record<string, unknown>>
|
||||
action={ref}
|
||||
request={async (pagination, filter) => {
|
||||
const response = await queryOrderList({ ...pagination, ...filter });
|
||||
return {
|
||||
list: response.data.data?.list || [],
|
||||
total: response.data.data?.total || 0,
|
||||
};
|
||||
}}
|
||||
renderItem={(item) => {
|
||||
return (
|
||||
<Card className='rounded-[32px] border border-[#D9D9D9] px-3 pb-5 pt-3 shadow-[0px_0px_4.5px_0px_rgba(0,0,0,0.25)]'>
|
||||
<div className={'flex justify-between'}>
|
||||
<div>
|
||||
<div className={'text-[#666]'}>{t('orderNo')}</div>
|
||||
<p className='text-xs text-[#4D4D4D]'>{item.order_no}</p>
|
||||
</div>
|
||||
<div>
|
||||
{item.status === 1 ? (
|
||||
<>
|
||||
<Button className='min-w-[150px] rounded-full border-[#0F2C53] bg-[#0F2C53] px-[35px] py-[9px] text-center text-xl font-bold hover:bg-[#225BA9] hover:text-white'>
|
||||
{t('payment')}
|
||||
</Button>
|
||||
<div>
|
||||
<ProList<API.OrderDetail, Record<string, unknown>>
|
||||
action={ref}
|
||||
request={async (pagination, filter) => {
|
||||
const response = await queryOrderList({ ...pagination, ...filter });
|
||||
return {
|
||||
list: response.data.data?.list || [],
|
||||
total: response.data.data?.total || 0,
|
||||
};
|
||||
}}
|
||||
renderItem={(item) => {
|
||||
return (
|
||||
<Card className='rounded-[32px] border border-[#D9D9D9] pb-5 pl-16 pr-3 pt-3 shadow-[0px_0px_4.5px_0px_rgba(0,0,0,0.25)]'>
|
||||
<div className={'flex justify-between'}>
|
||||
<div>
|
||||
<div className={'text-[#666]'}>{t('orderNo')}</div>
|
||||
<p className='text-xs text-[#4D4D4D]'>{item.order_no}</p>
|
||||
</div>
|
||||
<div>
|
||||
{item.status === 1 ? (
|
||||
<>
|
||||
<Button
|
||||
className='min-w-[150px] rounded-full border-[#F8BFD2] bg-[#F8BFD2] px-[35px] py-[9px] text-center text-xl font-bold hover:border-[#F8BFD2] hover:bg-[#FF4248] hover:text-white'
|
||||
onClick={async () => {
|
||||
await closeOrder({ orderNo: item.order_no });
|
||||
ref.current?.refresh();
|
||||
}}
|
||||
>
|
||||
取消订单
|
||||
</Button>
|
||||
<Button className='ml-3 min-w-[150px] rounded-full border-[#A8D4ED] bg-[#A8D4ED] px-[35px] py-[9px] text-center text-xl font-bold hover:border-[#225BA9] hover:bg-[#225BA9] hover:text-white'>
|
||||
{t('payment')}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<Button
|
||||
onClick={() => OrderDetailDialogRef?.current?.show(item.order_no)}
|
||||
className='min-w-[150px] rounded-full border-[#0F2C53] bg-[#0F2C53] px-[35px] py-[9px] text-center text-xl font-bold hover:bg-[#225BA9] hover:text-white'
|
||||
onClick={async () => {
|
||||
await closeOrder({ orderNo: item.order_no });
|
||||
ref.current?.refresh();
|
||||
}}
|
||||
>
|
||||
{t('cancel')}
|
||||
{t('detail')}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<Button className='min-w-[150px] rounded-full border-[#0F2C53] bg-[#0F2C53] px-[35px] py-[9px] text-center text-xl font-bold hover:bg-[#225BA9] hover:text-white'>
|
||||
{t('detail')}
|
||||
</Button>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<CardContent className='p-3 text-sm'>
|
||||
<ul className='grid grid-cols-2 gap-3 *:flex *:flex-col lg:grid-cols-4'>
|
||||
<li>
|
||||
<span className='text-[#225BA9]'>{t('name')}</span>
|
||||
<span className={'font-bold text-[#091B33]'}>
|
||||
{item.subscribe.name || t(`type.${item.type}`)}
|
||||
</span>
|
||||
</li>
|
||||
<li className='font-semibold'>
|
||||
<span className='text-[#225BA9]'>{t('paymentAmount')}</span>
|
||||
<span>
|
||||
<Display type='currency' value={item.amount} />
|
||||
</span>
|
||||
</li>
|
||||
<li className='font-semibold'>
|
||||
<span className='text-[#225BA9]'>{t('status.0')}</span>
|
||||
<span>{t(`status.${item.status}`)}</span>
|
||||
</li>
|
||||
<li className='font-semibold'>
|
||||
<span className='text-[#225BA9]'>{t('createdAt')}</span>
|
||||
<time>{formatDate(item.created_at)}</time>
|
||||
</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}}
|
||||
empty={<Empty />}
|
||||
/>
|
||||
<CardContent className='px-0 py-3 text-sm'>
|
||||
<ul className='grid grid-cols-2 gap-3 *:flex *:flex-col lg:grid-cols-4'>
|
||||
<li>
|
||||
<span className='text-[#225BA9]'>{t('name')}</span>
|
||||
<span className={'font-bold text-[#091B33]'}>
|
||||
{item.subscribe.name || t(`type.${item.type}`)}
|
||||
</span>
|
||||
</li>
|
||||
<li className='font-semibold'>
|
||||
<span className='text-[#225BA9]'>{t('paymentAmount')}</span>
|
||||
<span>
|
||||
<Display type='currency' value={item.amount} />
|
||||
</span>
|
||||
</li>
|
||||
<li className='font-semibold'>
|
||||
<span className='text-[#225BA9]'>{t('status.0')}</span>
|
||||
<span>{t(`status.${item.status}`)}</span>
|
||||
</li>
|
||||
<li className='font-semibold'>
|
||||
<span className='text-[#225BA9]'>{t('createdAt')}</span>
|
||||
<time>{formatDate(item.created_at)}</time>
|
||||
</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}}
|
||||
empty={<Empty />}
|
||||
/>
|
||||
<OrderDetailDialog ref={OrderDetailDialogRef} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,8 +4,9 @@ import { querySubscribeList } from '@/services/user/subscribe';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Tabs, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { LoginDialogProvider } from '@/app/auth/LoginDialogContext';
|
||||
import { TabContent } from '@/components/main/OfferDialog/TabContent';
|
||||
import { ProcessedPlanData } from '@/components/main/OfferDialog/types';
|
||||
import Purchase from '@/components/subscribe/purchase';
|
||||
@@ -13,7 +14,6 @@ import { unitConversion } from '@workspace/ui/utils';
|
||||
|
||||
export default function Page() {
|
||||
const t = useTranslations('subscribe');
|
||||
const [subscribe, setSubscribe] = useState<API.Subscribe | undefined>();
|
||||
const [tabValue, setTabValue] = useState<'year' | 'month'>('year');
|
||||
|
||||
const { data, isLoading, error, refetch } = useQuery({
|
||||
@@ -60,54 +60,61 @@ export default function Page() {
|
||||
const handleSubscribe = (plan: ProcessedPlanData) => {
|
||||
console.log('用户选择了套餐:', plan);
|
||||
// 这里可以添加订阅逻辑,比如跳转到支付页面或显示确认对话框
|
||||
setSubscribe(plan);
|
||||
PurchaseRef.current.show(plan, tabValue);
|
||||
};
|
||||
|
||||
const PurchaseRef = useRef<{
|
||||
show: (subscribe: API.Subscribe, tabValue: string) => void;
|
||||
hide: () => void;
|
||||
}>(null);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={'text-4xl font-bold text-[#0F2C53] md:mb-4 md:text-center md:text-5xl'}>
|
||||
选择套餐
|
||||
</div>
|
||||
<div className={'text-lg font-medium text-[#666666] md:text-center'}>
|
||||
选择最适合您的服务套餐
|
||||
</div>
|
||||
<div>
|
||||
<Tabs
|
||||
defaultValue='year'
|
||||
className={'mt-8 text-center md:mt-16'}
|
||||
value={tabValue}
|
||||
onValueChange={(value) => setTabValue(value as 'year' | 'month')}
|
||||
>
|
||||
<TabsList className='mb-8 h-[74px] flex-wrap rounded-full bg-[#EAEAEA] p-2.5 md:mb-16'>
|
||||
<TabsTrigger
|
||||
className={
|
||||
'rounded-full px-10 py-3.5 text-xl data-[state=active]:bg-[#0F2C53] data-[state=active]:text-white md:px-12'
|
||||
}
|
||||
value='year'
|
||||
>
|
||||
年付套餐
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
className={
|
||||
'rounded-full px-10 py-3.5 text-xl data-[state=active]:bg-[#0F2C53] data-[state=active]:text-white md:px-12'
|
||||
}
|
||||
value='month'
|
||||
>
|
||||
月付套餐
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
<TabContent
|
||||
tabValue={tabValue}
|
||||
yearlyPlans={yearlyPlans}
|
||||
monthlyPlans={monthlyPlans}
|
||||
isLoading={isLoading}
|
||||
error={error}
|
||||
onRetry={refetch}
|
||||
onSubscribe={handleSubscribe}
|
||||
/>
|
||||
</div>
|
||||
<Purchase subscribe={subscribe} setSubscribe={setSubscribe} />
|
||||
<LoginDialogProvider>
|
||||
<div className={'text-4xl font-bold text-[#0F2C53] md:mb-4 md:text-center md:text-5xl'}>
|
||||
选择套餐
|
||||
</div>
|
||||
<div className={'text-lg font-medium text-[#666666] md:text-center'}>
|
||||
选择最适合您的服务套餐
|
||||
</div>
|
||||
<div>
|
||||
<Tabs
|
||||
defaultValue='year'
|
||||
className={'mt-8 text-center md:mt-16'}
|
||||
value={tabValue}
|
||||
onValueChange={(value) => setTabValue(value as 'year' | 'month')}
|
||||
>
|
||||
<TabsList className='mb-8 h-[74px] flex-wrap rounded-full bg-[#EAEAEA] p-2.5 md:mb-16'>
|
||||
<TabsTrigger
|
||||
className={
|
||||
'rounded-full px-10 py-3.5 text-xl data-[state=active]:bg-[#0F2C53] data-[state=active]:text-white md:px-12'
|
||||
}
|
||||
value='year'
|
||||
>
|
||||
年付套餐
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
className={
|
||||
'rounded-full px-10 py-3.5 text-xl data-[state=active]:bg-[#0F2C53] data-[state=active]:text-white md:px-12'
|
||||
}
|
||||
value='month'
|
||||
>
|
||||
月付套餐
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
<TabContent
|
||||
tabValue={tabValue}
|
||||
yearlyPlans={yearlyPlans}
|
||||
monthlyPlans={monthlyPlans}
|
||||
isLoading={isLoading}
|
||||
error={error}
|
||||
onRetry={refetch}
|
||||
onSubscribe={handleSubscribe}
|
||||
/>
|
||||
</div>
|
||||
<Purchase ref={PurchaseRef} />
|
||||
</LoginDialogProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -88,7 +88,13 @@ export default function Page() {
|
||||
toolbar: (
|
||||
<Dialog open={create?.open} onOpenChange={(open) => setCreate({ open })}>
|
||||
<DialogTrigger asChild>
|
||||
<Button size='sm'>{t('createTicket')}</Button>
|
||||
<Button
|
||||
className={
|
||||
'min-w-[150px] rounded-full border-[#0F2C53] bg-[#0F2C53] px-[35px] py-[9px] text-center text-xl font-bold hover:bg-[#225BA9] hover:text-white'
|
||||
}
|
||||
>
|
||||
{t('createTicket')}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className='sm:max-w-[425px]'>
|
||||
<DialogHeader>
|
||||
@@ -156,8 +162,8 @@ export default function Page() {
|
||||
}}
|
||||
renderItem={(item) => {
|
||||
return (
|
||||
<Card className='overflow-hidden'>
|
||||
<CardHeader className='bg-muted/50 flex flex-row items-center justify-between gap-2 space-y-0 p-3'>
|
||||
<Card className='overflow-hidden pl-16'>
|
||||
<CardHeader className='flex flex-row items-center justify-between gap-2 space-y-0 bg-transparent p-3'>
|
||||
<CardTitle>
|
||||
<span
|
||||
className={cn(
|
||||
@@ -176,13 +182,25 @@ export default function Page() {
|
||||
<CardDescription className='flex gap-2'>
|
||||
{item.status !== 4 ? (
|
||||
<>
|
||||
<Button key='reply' size='sm' onClick={() => setTicketId(item.id)}>
|
||||
<Button
|
||||
key='reply'
|
||||
className={
|
||||
'ml-3 min-w-[150px] rounded-full border-[#A8D4ED] bg-[#A8D4ED] px-[35px] py-[9px] text-center text-xl font-bold hover:border-[#225BA9] hover:bg-[#225BA9] hover:text-white'
|
||||
}
|
||||
onClick={() => setTicketId(item.id)}
|
||||
>
|
||||
{t('reply')}
|
||||
</Button>
|
||||
<ConfirmButton
|
||||
key='close'
|
||||
trigger={
|
||||
<Button variant='destructive' size='sm'>
|
||||
<Button
|
||||
variant='destructive'
|
||||
className={
|
||||
'min-w-[150px] rounded-full border-[#F8BFD2] bg-[#F8BFD2] px-[35px] py-[9px] text-center text-xl font-bold hover:border-[#F8BFD2] hover:bg-[#FF4248] hover:text-white'
|
||||
}
|
||||
size='sm'
|
||||
>
|
||||
{t('close')}
|
||||
</Button>
|
||||
}
|
||||
@@ -208,16 +226,18 @@ export default function Page() {
|
||||
<CardContent className='p-3 text-sm'>
|
||||
<ul className='grid gap-3 *:flex *:flex-col lg:grid-cols-3'>
|
||||
<li>
|
||||
<span className='text-muted-foreground'>{t('title')}</span>
|
||||
<span> {item.title}</span>
|
||||
<span className='text-[15px] font-normal text-[#225BA9]'>{t('title')}</span>
|
||||
<span className={'font-bold'}> {item.title}</span>
|
||||
</li>
|
||||
<li className='font-semibold'>
|
||||
<span className='text-muted-foreground'>{t('description')}</span>
|
||||
<time>{item.description}</time>
|
||||
<li className=''>
|
||||
<span className='text-[15px] font-normal text-[#225BA9]'>
|
||||
{t('description')}
|
||||
</span>
|
||||
<time className={'font-bold'}>{item.description}</time>
|
||||
</li>
|
||||
<li className='font-semibold'>
|
||||
<span className='text-muted-foreground'>{t('updatedAt')}</span>
|
||||
<time>{formatDate(item.updated_at)}</time>
|
||||
<li className=''>
|
||||
<span className='text-[15px] font-normal text-[#225BA9]'>{t('updatedAt')}</span>
|
||||
<time className={'font-bold'}>{formatDate(item.updated_at)}</time>
|
||||
</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
|
||||
@@ -11,8 +11,9 @@ import { useRef } from 'react';
|
||||
import { Empty } from '@/components/empty';
|
||||
import Recharge from '@/components/subscribe/recharge';
|
||||
import { Button } from '@workspace/ui/components/button';
|
||||
import { formatDate, isBrowser } from '@workspace/ui/utils';
|
||||
import { formatDate } from '@workspace/ui/utils';
|
||||
import { Copy } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { CopyToClipboard } from 'react-copy-to-clipboard';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
@@ -62,27 +63,27 @@ export default function Page() {
|
||||
<Display type='currency' value={user?.commission} />
|
||||
</p>
|
||||
</div>
|
||||
<div className='rounded-[20px] bg-[#EAEAEA] p-4 shadow-sm transition-all duration-300 hover:shadow-md'>
|
||||
<div className='rounded-[20px] border-2 border-[#D9D9D9] p-4 shadow-sm transition-all duration-300 hover:shadow-md'>
|
||||
<p className='mb-3 flex justify-between text-sm font-medium text-[#666] opacity-80'>
|
||||
<span>返佣邀请码</span>
|
||||
<span className={'text-[#225BA9]'}>返佣详情</span>
|
||||
<Link href='/affiliate' className={'text-[#225BA9]'}>
|
||||
返佣详情
|
||||
</Link>
|
||||
</p>
|
||||
<p className='flex justify-between text-2xl font-bold text-[#225BA9]'>
|
||||
<span> {user?.refer_code}</span>
|
||||
{isBrowser() && (
|
||||
<CopyToClipboard
|
||||
text={`${location?.origin}/auth?invite=${user?.refer_code}`}
|
||||
onCopy={(text, result) => {
|
||||
if (result) {
|
||||
toast.success(t('copySuccess'));
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Button variant='secondary' size='sm' className='gap-2'>
|
||||
<Copy className='h-4 w-4' />
|
||||
</Button>
|
||||
</CopyToClipboard>
|
||||
)}
|
||||
<CopyToClipboard
|
||||
text={`${location?.origin}/?invite=${user?.refer_code}`}
|
||||
onCopy={(text, result) => {
|
||||
if (result) {
|
||||
toast.success(t('copySuccess'));
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Button variant='secondary' size='sm' className='gap-2'>
|
||||
<Copy className='h-4 w-4' />
|
||||
</Button>
|
||||
</CopyToClipboard>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
import { Hero } from '@/components/main/hero';
|
||||
import { ProductShowcase } from '@/components/main/product-showcase/index';
|
||||
import { Stats } from '@/components/main/stats';*/
|
||||
import NewHeader from '@/components/Header/NewHeader';
|
||||
import Header from '@/components/Header/Header';
|
||||
import { queryUserInfo } from '@/services/user/user';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
import { LoginDialogProvider } from '@/app/auth/LoginDialogContext';
|
||||
import FooterCopyright from '@/components/main/FooterCopyright';
|
||||
import FullScreenVideoBackground from '@/components/main/FullScreenVideoBackground';
|
||||
import HomeContent from '@/components/main/HomeContent';
|
||||
@@ -30,13 +31,13 @@ export default async function Home() {
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<NewHeader />
|
||||
<LoginDialogProvider>
|
||||
<Header />
|
||||
<FullScreenVideoBackground />
|
||||
<main className='fixed inset-0 z-10 flex items-center justify-center'>
|
||||
<HomeContent />
|
||||
</main>
|
||||
<FooterCopyright />
|
||||
</>
|
||||
</LoginDialogProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import EmailAuthForm from '@/app/auth/email/auth-form';
|
||||
import CloseSvg from '@/components/CustomIcon/icons/close.svg';
|
||||
import { Dialog, DialogContent, DialogTitle } from '@workspace/ui/components/dialog';
|
||||
import Image from 'next/image';
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
|
||||
export interface EmailAuthDialogRef {
|
||||
show: () => void;
|
||||
hide: () => void;
|
||||
}
|
||||
|
||||
const EmailAuthDialog = forwardRef<EmailAuthDialogRef>((props, ref) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
show: () => setOpen(true),
|
||||
hide: () => setOpen(false),
|
||||
}));
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent
|
||||
className={
|
||||
'rounded-0 h-full w-full px-12 py-[4.5rem] sm:h-auto sm:w-[496px] sm:!rounded-[50px]'
|
||||
}
|
||||
closeIcon={<Image src={CloseSvg} alt={'close'} />}
|
||||
closeClassName={
|
||||
'right-[40px] top-[30px] font-bold text-black opacity-100 focus:ring-0 focus:ring-offset-0'
|
||||
}
|
||||
>
|
||||
<DialogTitle className={'sr-only'}>title</DialogTitle>
|
||||
<div className={'min-h-[524px]'}>
|
||||
<EmailAuthForm />
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
});
|
||||
|
||||
export default EmailAuthDialog;
|
||||
@@ -0,0 +1,83 @@
|
||||
'use client';
|
||||
import EmailAuthForm from '@/app/auth/email/auth-form';
|
||||
import CloseSvg from '@/components/CustomIcon/icons/close.svg';
|
||||
import { Dialog, DialogContent, DialogTitle } from '@workspace/ui/components/dialog';
|
||||
import Image from 'next/image';
|
||||
import {
|
||||
createContext,
|
||||
forwardRef,
|
||||
ReactNode,
|
||||
useContext,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
type LoginDialogContextType = {
|
||||
openLoginDialog: (isRedirect?: boolean) => void;
|
||||
closeLoginDialog: () => void;
|
||||
};
|
||||
|
||||
const LoginDialogContext = createContext<LoginDialogContextType | undefined>(undefined);
|
||||
|
||||
export function LoginDialogProvider({ children }: { children: ReactNode }) {
|
||||
const dialogRef = useRef<{ show: (isRedirect?: boolean) => void; hide: () => void }>(null);
|
||||
|
||||
const openLoginDialog = (isRedirect = true) => dialogRef.current?.show(isRedirect);
|
||||
const closeLoginDialog = () => dialogRef.current?.hide();
|
||||
|
||||
return (
|
||||
<LoginDialogContext.Provider value={{ openLoginDialog, closeLoginDialog }}>
|
||||
<LoginDialog ref={dialogRef} />
|
||||
{children}
|
||||
</LoginDialogContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useLoginDialog() {
|
||||
const context = useContext(LoginDialogContext);
|
||||
if (!context) {
|
||||
throw new Error('useLoginDialog must be used within a LoginDialogProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
interface LoginDialogRef {
|
||||
show: () => void;
|
||||
hide: () => void;
|
||||
}
|
||||
|
||||
const LoginDialog = forwardRef<LoginDialogRef>((props, ref) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [isRedirect, setIsRedirect] = useState(false);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
show: (isRedirect = ture) => {
|
||||
setOpen(true);
|
||||
setIsRedirect(isRedirect);
|
||||
},
|
||||
hide,
|
||||
}));
|
||||
|
||||
function hide() {
|
||||
setIsRedirect(false);
|
||||
setOpen(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent
|
||||
className='rounded-0 h-full w-full px-12 py-[4.5rem] sm:h-auto sm:w-[496px] sm:!rounded-[50px]'
|
||||
closeIcon={<Image src={CloseSvg} alt='close' />}
|
||||
closeClassName='right-[40px] top-[30px] font-bold text-black opacity-100 focus:ring-0 focus:ring-offset-0'
|
||||
>
|
||||
<DialogTitle className='sr-only'>Login</DialogTitle>
|
||||
<div className='min-h-[524px]'>
|
||||
<EmailAuthForm hide={hide} isRedirect={isRedirect} />
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
});
|
||||
|
||||
LoginDialog.displayName = 'LoginDialog';
|
||||
@@ -10,12 +10,13 @@ import {
|
||||
NEXT_PUBLIC_DEFAULT_USER_EMAIL,
|
||||
NEXT_PUBLIC_DEFAULT_USER_PASSWORD,
|
||||
} from '@/config/constants';
|
||||
import useGlobalStore from '@/config/use-global';
|
||||
import { getRedirectUrl, setAuthorization } from '@/utils/common';
|
||||
import LoginForm from './login-form';
|
||||
import RegisterForm from './register-form';
|
||||
import ResetForm from './reset-form';
|
||||
|
||||
export default function EmailAuthForm() {
|
||||
export default function EmailAuthForm(props: { isRedirect: boolean; hide: () => void }) {
|
||||
const t = useTranslations('auth');
|
||||
const router = useRouter();
|
||||
const [type, setType] = useState<'login' | 'register' | 'reset'>('login');
|
||||
@@ -27,13 +28,19 @@ export default function EmailAuthForm() {
|
||||
email: NEXT_PUBLIC_DEFAULT_USER_EMAIL,
|
||||
password: NEXT_PUBLIC_DEFAULT_USER_PASSWORD,
|
||||
});
|
||||
|
||||
const { getUserInfo } = useGlobalStore();
|
||||
const handleFormSubmit = async (params: any) => {
|
||||
const onLogin = async (token?: string) => {
|
||||
if (!token) return;
|
||||
setAuthorization(token);
|
||||
router.replace(getRedirectUrl());
|
||||
router.refresh();
|
||||
|
||||
if (props.isRedirect) {
|
||||
router.replace(getRedirectUrl());
|
||||
router.refresh();
|
||||
} else {
|
||||
await getUserInfo();
|
||||
props.hide();
|
||||
}
|
||||
};
|
||||
startTransition(async () => {
|
||||
try {
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { bindOAuthCallback } from '@/services/user/user';
|
||||
import { getAllUrlParams } from '@/utils/common';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
interface CertificationProps {
|
||||
platform: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function Certification({ platform, children }: CertificationProps) {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
|
||||
useEffect(() => {
|
||||
const searchParams = getAllUrlParams();
|
||||
bindOAuthCallback({
|
||||
method: platform,
|
||||
callback: searchParams,
|
||||
})
|
||||
.then((res) => {
|
||||
router.replace('/profile');
|
||||
router.refresh();
|
||||
})
|
||||
.catch((error) => {
|
||||
router.replace('/');
|
||||
});
|
||||
}, [pathname]);
|
||||
|
||||
return children;
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
import HyperText from '@workspace/ui/components/hyper-text';
|
||||
import { OrbitingCircles } from '@workspace/ui/components/orbiting-circles';
|
||||
import { Icon } from '@workspace/ui/custom-components/icon';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
import Certification from './certification';
|
||||
|
||||
export async function generateStaticParams() {
|
||||
return [
|
||||
{
|
||||
platform: 'telegram',
|
||||
},
|
||||
{
|
||||
platform: 'apple',
|
||||
},
|
||||
{
|
||||
platform: 'facebook',
|
||||
},
|
||||
{
|
||||
platform: 'google',
|
||||
},
|
||||
{
|
||||
platform: 'github',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export default async function Page({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{
|
||||
platform: string;
|
||||
}>;
|
||||
}) {
|
||||
const { platform } = await params;
|
||||
const t = await getTranslations('auth');
|
||||
return (
|
||||
<Certification platform={platform}>
|
||||
<div className='bg-background relative flex h-screen w-full flex-col items-center justify-center overflow-hidden'>
|
||||
<div className='pointer-events-none flex animate-pulse flex-col items-center whitespace-pre-wrap bg-gradient-to-r from-blue-500 via-indigo-500 to-violet-500 bg-clip-text text-center font-black tracking-tight text-transparent dark:from-blue-400 dark:via-indigo-300 dark:to-violet-400'>
|
||||
<HyperText className='text-xl uppercase md:text-2xl'>{platform}</HyperText>
|
||||
<HyperText className='text-lg md:text-xl'>{t('authenticating')}</HyperText>
|
||||
</div>
|
||||
|
||||
<OrbitingCircles iconSize={40} speed={0.8}>
|
||||
<Icon icon='logos:telegram' className='size-12' />
|
||||
<Icon icon='uil:apple' className='size-12' />
|
||||
<Icon icon='logos:google-icon' className='size-12' />
|
||||
<Icon icon='logos:facebook' className='size-12' />
|
||||
<Icon icon='uil:github' className='size-12' />
|
||||
</OrbitingCircles>
|
||||
<OrbitingCircles iconSize={30} radius={100} reverse speed={0.4}>
|
||||
<Icon icon='logos:telegram' className='size-10' />
|
||||
<Icon icon='uil:apple' className='size-10' />
|
||||
<Icon icon='logos:google-icon' className='size-10' />
|
||||
<Icon icon='logos:facebook' className='size-10' />
|
||||
<Icon icon='uil:github' className='size-10' />
|
||||
</OrbitingCircles>
|
||||
</div>
|
||||
</Certification>
|
||||
);
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { oAuthLoginGetToken } from '@/services/common/oauth';
|
||||
import { getAllUrlParams, getRedirectUrl, setAuthorization } from '@/utils/common';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
interface CertificationProps {
|
||||
platform: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function Certification({ platform, children }: CertificationProps) {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
|
||||
useEffect(() => {
|
||||
const searchParams = getAllUrlParams();
|
||||
oAuthLoginGetToken({
|
||||
method: platform,
|
||||
callback: searchParams,
|
||||
})
|
||||
.then((res) => {
|
||||
const token = res?.data?.data?.token;
|
||||
if (!token) {
|
||||
throw new Error('Invalid token');
|
||||
}
|
||||
setAuthorization(token);
|
||||
router.replace(getRedirectUrl());
|
||||
router.refresh();
|
||||
})
|
||||
.catch((error) => {
|
||||
router.replace('/');
|
||||
});
|
||||
}, [pathname]);
|
||||
|
||||
return children;
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
import HyperText from '@workspace/ui/components/hyper-text';
|
||||
import { OrbitingCircles } from '@workspace/ui/components/orbiting-circles';
|
||||
import { Icon } from '@workspace/ui/custom-components/icon';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
import Certification from './certification';
|
||||
|
||||
export async function generateStaticParams() {
|
||||
return [
|
||||
{
|
||||
platform: 'telegram',
|
||||
},
|
||||
{
|
||||
platform: 'apple',
|
||||
},
|
||||
{
|
||||
platform: 'facebook',
|
||||
},
|
||||
{
|
||||
platform: 'google',
|
||||
},
|
||||
{
|
||||
platform: 'github',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export default async function Page({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{
|
||||
platform: string;
|
||||
}>;
|
||||
}) {
|
||||
const { platform } = await params;
|
||||
const t = await getTranslations('auth');
|
||||
return (
|
||||
<Certification platform={platform}>
|
||||
<div className='bg-background relative flex h-screen w-full flex-col items-center justify-center overflow-hidden'>
|
||||
<div className='pointer-events-none flex animate-pulse flex-col items-center whitespace-pre-wrap bg-gradient-to-r from-blue-500 via-indigo-500 to-violet-500 bg-clip-text text-center font-black tracking-tight text-transparent dark:from-blue-400 dark:via-indigo-300 dark:to-violet-400'>
|
||||
<HyperText className='text-xl uppercase md:text-2xl'>{platform}</HyperText>
|
||||
<HyperText className='text-lg md:text-xl'>{t('authenticating')}</HyperText>
|
||||
</div>
|
||||
|
||||
<OrbitingCircles iconSize={40} speed={0.8}>
|
||||
<Icon icon='logos:telegram' className='size-12' />
|
||||
<Icon icon='uil:apple' className='size-12' />
|
||||
<Icon icon='logos:google-icon' className='size-12' />
|
||||
<Icon icon='logos:facebook' className='size-12' />
|
||||
<Icon icon='uil:github' className='size-12' />
|
||||
</OrbitingCircles>
|
||||
<OrbitingCircles iconSize={30} radius={100} reverse speed={0.4}>
|
||||
<Icon icon='logos:telegram' className='size-10' />
|
||||
<Icon icon='uil:apple' className='size-10' />
|
||||
<Icon icon='logos:google-icon' className='size-10' />
|
||||
<Icon icon='logos:facebook' className='size-10' />
|
||||
<Icon icon='uil:github' className='size-10' />
|
||||
</OrbitingCircles>
|
||||
</div>
|
||||
</Certification>
|
||||
);
|
||||
}
|
||||
@@ -8,8 +8,7 @@ import Image from 'next/legacy/image';
|
||||
import Link from 'next/link';
|
||||
import LanguageSwitch from '../language-switch';
|
||||
// import ThemeSwitch from '../theme-switch';
|
||||
import EmailAuthDialog, { EmailAuthDialogRef } from '@/app/auth/EmailAuthDialog/EmailAuthDialog';
|
||||
import { useRef } from 'react';
|
||||
import { useLoginDialog } from '@/app/auth/LoginDialogContext';
|
||||
import { UserNav } from '../user-nav';
|
||||
|
||||
export default function Header() {
|
||||
@@ -18,20 +17,11 @@ export default function Header() {
|
||||
const { user } = useGlobalStore();
|
||||
const Logo = (
|
||||
<Link href='/' className='-mt-2.5 flex items-center gap-2 font-bold'>
|
||||
<Image
|
||||
src={'image.png'}
|
||||
width={102}
|
||||
height={49}
|
||||
alt='logo'
|
||||
fill={true}
|
||||
objectFit='cover'
|
||||
unoptimized
|
||||
/>
|
||||
<Image src={'image.png'} width={102} height={49} alt='logo' objectFit='cover' unoptimized />
|
||||
</Link>
|
||||
);
|
||||
|
||||
const dialogRef = useRef<EmailAuthDialogRef>(null);
|
||||
|
||||
const { openLoginDialog } = useLoginDialog();
|
||||
return (
|
||||
<>
|
||||
<header className='fixed top-10 z-50 w-full'>
|
||||
@@ -47,7 +37,7 @@ export default function Header() {
|
||||
{!user && (
|
||||
<Link
|
||||
href='#'
|
||||
onClick={() => dialogRef.current?.show()}
|
||||
onClick={() => openLoginDialog()}
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
size: 'lg',
|
||||
@@ -63,8 +53,6 @@ export default function Header() {
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
{/* 登录注册弹窗 */}
|
||||
<EmailAuthDialog ref={dialogRef} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import useGlobalStore from '@/config/use-global';
|
||||
import { buttonVariants } from '@workspace/ui/components/button';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import Image from 'next/legacy/image';
|
||||
import Link from 'next/link';
|
||||
import LanguageSwitch from '../language-switch';
|
||||
import { UserNav } from '../user-nav';
|
||||
|
||||
export default function Header() {
|
||||
const t = useTranslations('common');
|
||||
|
||||
const { common, user } = useGlobalStore();
|
||||
const { site } = common;
|
||||
const Logo = (
|
||||
<Link href='/' className='flex items-center gap-2 text-lg font-bold'>
|
||||
{site.site_logo && (
|
||||
<Image src={site.site_logo} width={48} height={48} alt='logo' unoptimized />
|
||||
)}
|
||||
<span className=''>{site.site_name}</span>
|
||||
</Link>
|
||||
);
|
||||
return (
|
||||
<header className='sticky top-0 z-50 border-b backdrop-blur-md'>
|
||||
<div className='container flex h-16 items-center justify-between'>
|
||||
<nav className='flex-col gap-6 text-lg font-medium md:flex md:flex-row md:items-center md:gap-5 md:text-sm lg:gap-6'>
|
||||
{Logo}
|
||||
</nav>
|
||||
<div className='flex flex-1 items-center justify-end gap-2'>
|
||||
<LanguageSwitch />
|
||||
{/*<ThemeSwitch />*/}
|
||||
<UserNav />
|
||||
{!user && (
|
||||
<Link
|
||||
href='/'
|
||||
className={buttonVariants({
|
||||
size: 'sm',
|
||||
})}
|
||||
>
|
||||
{t('login')}
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -2,19 +2,12 @@
|
||||
|
||||
import { Display } from '@/components/display';
|
||||
import { Empty } from '@/components/empty';
|
||||
import { ProList } from '@/components/pro-list';
|
||||
import useGlobalStore from '@/config/use-global';
|
||||
import { queryUserAffiliate, queryUserAffiliateList } from '@/services/user/user';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Button } from '@workspace/ui/components/button';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@workspace/ui/components/card';
|
||||
import { formatDate, isBrowser } from '@workspace/ui/utils';
|
||||
import { Card, CardContent } from '@workspace/ui/components/card';
|
||||
import { formatDate } from '@workspace/ui/utils';
|
||||
import { Copy } from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useState } from 'react';
|
||||
@@ -33,52 +26,97 @@ export default function Affiliate() {
|
||||
},
|
||||
});
|
||||
|
||||
const { data: inviteList = [] } = useQuery({
|
||||
queryKey: ['queryUserAffiliateList'],
|
||||
queryFn: async () => {
|
||||
const response = await queryUserAffiliateList({
|
||||
page: 1,
|
||||
size: 3,
|
||||
});
|
||||
return response.data.data?.list || [];
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className='flex flex-col gap-4'>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{t('totalCommission')}</CardTitle>
|
||||
<CardDescription>{t('commissionInfo')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className='flex items-baseline gap-2'>
|
||||
<span className='text-3xl font-bold'>
|
||||
<Display type='currency' value={data?.total_commission} />
|
||||
</span>
|
||||
<span className='text-muted-foreground text-sm'>
|
||||
({t('commissionRate')}: {common?.invite?.referral_percentage}%)
|
||||
</span>
|
||||
<div className='grid grid-cols-2 gap-4'>
|
||||
<Card
|
||||
className={
|
||||
'rounded-[20px] border border-[#D9D9D9] p-6 shadow-[0px_0px_52.6px_1px_rgba(15,44,83,0.05)]'
|
||||
}
|
||||
>
|
||||
<CardContent className={'p-0 text-[#666]'}>
|
||||
<div className={'mb-6'}>
|
||||
<div className={'text-xl font-bold'}>{t('totalCommission')}</div>
|
||||
<div className={'text-[15px] font-light'}>佣金金额,邀请成功后自动转入钱包余额</div>
|
||||
</div>
|
||||
<div className={'text-[32px] font-bold text-[#091B33]'}>历史推荐用户:7</div>
|
||||
<div className={'grid grid-cols-2 gap-5'}>
|
||||
<div className='rounded-[20px] bg-[#EAEAEA] p-4 shadow-sm transition-all duration-300 hover:shadow-md'>
|
||||
<p className='mb-3 text-sm font-medium text-[#666] opacity-80'>佣金总额</p>
|
||||
<p className='text-2xl font-bold text-[#225BA9]'>
|
||||
<Display type='currency' value={data?.total_commission} />
|
||||
</p>
|
||||
</div>
|
||||
<div className='rounded-[20px] border-2 border-[#D9D9D9] p-4 shadow-sm transition-all duration-300 hover:shadow-md'>
|
||||
<p className='mb-3 flex justify-between text-sm font-medium text-[#666] opacity-80'>
|
||||
<span>返佣邀请码</span>
|
||||
</p>
|
||||
<p className='flex justify-between text-2xl font-bold text-[#225BA9]'>
|
||||
<span> {user?.refer_code}</span>
|
||||
<CopyToClipboard
|
||||
text={`${location?.origin}/?invite=${user?.refer_code}`}
|
||||
onCopy={(text, result) => {
|
||||
if (result) {
|
||||
toast.success(t('copySuccess'));
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Button variant='secondary' size='sm' className='gap-2'>
|
||||
<Copy className='h-4 w-4' />
|
||||
</Button>
|
||||
</CopyToClipboard>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className='flex flex-row items-center justify-between space-y-0'>
|
||||
<CardTitle className='text-lg font-medium'>{t('inviteCode')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className='flex items-center justify-between'>
|
||||
<code className='bg-muted rounded px-2 py-1 text-2xl font-bold'>
|
||||
{user?.refer_code}
|
||||
</code>
|
||||
{isBrowser() && (
|
||||
<CopyToClipboard
|
||||
text={`${location?.origin}/auth?invite=${user?.refer_code}`}
|
||||
onCopy={(text, result) => {
|
||||
if (result) {
|
||||
toast.success(t('copySuccess'));
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Button variant='secondary' size='sm' className='gap-2'>
|
||||
<Copy className='h-4 w-4' />
|
||||
{t('copyInviteLink')}
|
||||
</Button>
|
||||
</CopyToClipboard>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
<Card className='rounded-[20px] border border-[#EAEAEA] bg-gradient-to-b from-white to-[#EAEAEA] p-6'>
|
||||
<div className='mb-4 flex items-center justify-between'>
|
||||
<h3 className='text-xl font-medium text-[#666666]'>邀请记录</h3>
|
||||
<span className='text-sm text-[#225BA9]'>更多</span>
|
||||
</div>
|
||||
|
||||
<div className='space-y-4'>
|
||||
{inviteList?.length ? (
|
||||
<div className='relative space-y-3'>
|
||||
<div
|
||||
className={
|
||||
'absolute bottom-0 left-0 right-0 h-[60px] bg-white/30 backdrop-blur-[1px]'
|
||||
}
|
||||
></div>
|
||||
{inviteList?.map((invite) => {
|
||||
return (
|
||||
<div className='flex flex-wrap justify-between gap-2 rounded-[20px] bg-white px-6 py-2'>
|
||||
<div>
|
||||
<div className={'text-[#225BA9]'}>用户识别代码</div>
|
||||
<div className={'font-bold text-[#091B33]'}>{invite.identifier}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className={'text-[#225BA9]'}>时间</div>
|
||||
<div className={'font-bold text-[#091B33]'}>
|
||||
{formatDate(invite.registered_at)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<Empty />
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
<ProList<API.UserAffiliate, Record<string, unknown>>
|
||||
{/*<ProList<API.UserAffiliate, Record<string, unknown>>
|
||||
request={async (pagination, filter) => {
|
||||
const response = await queryUserAffiliateList({ ...pagination, ...filter });
|
||||
setSum(response.data.data?.sum);
|
||||
@@ -109,7 +147,7 @@ export default function Affiliate() {
|
||||
);
|
||||
}}
|
||||
empty={<Empty />}
|
||||
/>
|
||||
/>*/}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { queryAnnouncement } from '@/services/user/announcement';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@workspace/ui/components/dialog';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@workspace/airo-ui/components/dialog';
|
||||
import { Markdown } from '@workspace/ui/custom-components/markdown';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
|
||||
|
||||
@@ -19,10 +19,10 @@ export function Display<T extends number | undefined | null>({
|
||||
}: DisplayProps<T>): string {
|
||||
const t = useTranslations('common');
|
||||
const { common } = useGlobalStore();
|
||||
const { currency } = common;
|
||||
// const { currency } = common;
|
||||
|
||||
if (type === 'currency') {
|
||||
const formattedValue = `${currency?.currency_symbol ?? ''}${unitConversion('centsToDollars', value as number)?.toFixed(2) ?? '0.00'}`;
|
||||
const formattedValue = `$ ${unitConversion('centsToDollars', value as number)?.toFixed(2) ?? '0.00'}`;
|
||||
return formattedValue;
|
||||
}
|
||||
|
||||
|
||||
@@ -67,15 +67,33 @@ const PriceDisplay = ({ plan }: { plan: ProcessedPlanData }) => (
|
||||
</div>
|
||||
);
|
||||
|
||||
import { useLoginDialog } from '@/app/auth/LoginDialogContext';
|
||||
import Purchase from '@/components/subscribe/purchase';
|
||||
import useGlobalStore from '@/config/use-global';
|
||||
// 订阅按钮组件
|
||||
const SubscribeButton = ({ onClick }: { onClick?: () => void }) => (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className='h-10 w-full rounded-full bg-[#0F2C53] text-sm font-medium text-white shadow-md transition-all duration-300 hover:bg-[#0A2C47] sm:h-10 sm:text-sm md:h-[40px] md:text-[14px]'
|
||||
>
|
||||
订阅
|
||||
</button>
|
||||
);
|
||||
const SubscribeButton = ({ onClick }: { onClick?: () => void }) => {
|
||||
const { user } = useGlobalStore();
|
||||
const { openLoginDialog } = useLoginDialog();
|
||||
|
||||
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]'
|
||||
>
|
||||
订阅
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
// 星级评分组件
|
||||
const StarRating = ({ rating, maxRating = 5 }: { rating: number; maxRating?: number }) => (
|
||||
@@ -101,7 +119,7 @@ const FeatureList = ({ plan }: { plan: ProcessedPlanData }) => {
|
||||
|
||||
return (
|
||||
<div className='mt-6 space-y-0 sm:mt-6'>
|
||||
<ul className='list-disc space-y-1'>
|
||||
<ul className='list-disc space-y-1 pl-5'>
|
||||
{features.map((feature) => (
|
||||
<li
|
||||
key={feature.label}
|
||||
@@ -143,7 +161,7 @@ const PlanCard = forwardRef<
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className='relative w-full max-w-[345px] cursor-pointer rounded-[20px] border border-[#D9D9D9] bg-white p-4 transition-all duration-300 hover:shadow-lg sm:p-6 md:p-8'
|
||||
className='relative w-full max-w-[345px] cursor-pointer rounded-[20px] border border-[#D9D9D9] bg-white p-8 transition-all duration-300 hover:shadow-lg sm:p-10'
|
||||
>
|
||||
{/* 套餐名称 */}
|
||||
<h3 className='mb-4 text-left text-sm font-normal sm:mb-6 sm:text-base'>{plan.name}</h3>
|
||||
@@ -295,12 +313,13 @@ const OfferDialog = forwardRef<OfferDialogRef>((props, ref) => {
|
||||
show: () => setOpen(true),
|
||||
hide: () => setOpen(false),
|
||||
}));
|
||||
|
||||
const PurchaseRef = useRef<{ show: (subscribe: API.Subscribe) => void; hide: () => void }>(null);
|
||||
// 处理订阅点击
|
||||
const handleSubscribe = (plan: ProcessedPlanData) => {
|
||||
setSelectedPlan(plan);
|
||||
console.log('用户选择了套餐:', plan);
|
||||
// 这里可以添加订阅逻辑,比如跳转到支付页面或显示确认对话框
|
||||
PurchaseRef.current.show(plan, tabValue);
|
||||
};
|
||||
|
||||
// 处理套餐数据的工具函数
|
||||
@@ -395,6 +414,7 @@ const OfferDialog = forwardRef<OfferDialogRef>((props, ref) => {
|
||||
/>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
<Purchase ref={PurchaseRef} />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import { ProList as _ProList, ProListProps } from '@workspace/ui/custom-components/pro-list';
|
||||
import { ProList as _ProList, ProListProps } from '@workspace/airo-ui/custom-components/pro-list';
|
||||
import { useTranslations } from 'next-intl';
|
||||
export { type ProListActions, type ProListProps } from '@workspace/ui/custom-components/pro-list';
|
||||
export {
|
||||
type ProListActions,
|
||||
type ProListProps,
|
||||
} from '@workspace/airo-ui/custom-components/pro-list';
|
||||
|
||||
export function ProList<TData, TValue extends Record<string, unknown>>(
|
||||
props: ProListProps<TData, TValue>,
|
||||
|
||||
@@ -19,30 +19,37 @@ export function SubscribeBilling({ order }: Readonly<SubscribeBillingProps>) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='font-semibold'>{t('billing.billingTitle')}</div>
|
||||
<ul className='grid grid-cols-2 gap-3 *:flex *:items-center *:justify-between lg:grid-cols-1'>
|
||||
<div className='mb-1 font-semibold text-[#225BA9]'>{t('billing.billingTitle')}</div>
|
||||
<ul className='grid grid-cols-1 gap-1 text-[15px] font-light text-[#666] *:flex *:items-center *:justify-between lg:grid-cols-1'>
|
||||
<li>
|
||||
<span className=''>套餐时长</span>
|
||||
<span>
|
||||
{order?.quantity === 1 ? '30天' : ''}
|
||||
{order?.quantity === 12 ? '365天' : ''}
|
||||
</span>
|
||||
</li>
|
||||
{order?.type && [1, 2].includes(order?.type) && (
|
||||
<li>
|
||||
<span className='text-muted-foreground'>{t('billing.duration')}</span>
|
||||
<span className=''>{t('billing.duration')}</span>
|
||||
<span>
|
||||
{order?.quantity || 1} {t(order?.unit_time || 'Month')}
|
||||
</span>
|
||||
</li>
|
||||
)}
|
||||
<li>
|
||||
<span className='text-muted-foreground'>{t('billing.price')}</span>
|
||||
<span className=''>{t('billing.price')}</span>
|
||||
<span>
|
||||
<Display type='currency' value={order?.price || order?.unit_price} />
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<span className='text-muted-foreground'>{t('billing.productDiscount')}</span>
|
||||
<span className=''>{t('billing.productDiscount')}</span>
|
||||
<span>
|
||||
<Display type='currency' value={order?.discount} />
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<span className='text-muted-foreground'>{t('billing.couponDiscount')}</span>
|
||||
<span className=''>{t('billing.couponDiscount')}</span>
|
||||
<span>
|
||||
<Display type='currency' value={order?.coupon_discount} />
|
||||
</span>
|
||||
@@ -60,9 +67,9 @@ export function SubscribeBilling({ order }: Readonly<SubscribeBillingProps>) {
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<Separator />
|
||||
<div className='flex items-center justify-between font-semibold'>
|
||||
<span className='text-muted-foreground'>{t('billing.total')}</span>
|
||||
<Separator className={'mb-3 mt-4 bg-[#225BA9]'} />
|
||||
<div className='flex items-center justify-between font-semibold text-[#666]'>
|
||||
<span className=''>支付金额</span>
|
||||
<span>
|
||||
<Display type='currency' value={order?.amount} />
|
||||
</span>
|
||||
|
||||
@@ -17,30 +17,30 @@ export function SubscribeDetail({ subscribe }: Readonly<SubscribeDetailProps>) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='font-semibold'>{t('productDetail')}</div>
|
||||
<ul className='grid grid-cols-1 gap-3 *:flex *:items-center *:justify-between lg:grid-cols-1'>
|
||||
<div className='mb-1 font-semibold text-[#225BA9]'>{t('productDetail')}</div>
|
||||
<ul className='grid grid-cols-1 gap-2 text-[15px] font-light text-[#666] *:flex *:items-center *:justify-between lg:grid-cols-1'>
|
||||
{subscribe?.name && (
|
||||
<li className='flex items-center justify-between'>
|
||||
<span className='text-muted-foreground line-clamp-2 flex-1'>{subscribe?.name}</span>
|
||||
<span className='line-clamp-2 flex-1'>{subscribe?.name}</span>
|
||||
<span>
|
||||
x <span>{subscribe?.quantity || 1}</span>
|
||||
x <span>1</span>
|
||||
</span>
|
||||
</li>
|
||||
)}
|
||||
<li>
|
||||
<span className='text-muted-foreground'>{t('availableTraffic')}</span>
|
||||
<span className=''>{t('availableTraffic')}</span>
|
||||
<span>
|
||||
<Display type='traffic' value={subscribe?.traffic} unlimited />
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<span className='text-muted-foreground'>{t('connectionSpeed')}</span>
|
||||
<span className=''>{t('connectionSpeed')}</span>
|
||||
<span>
|
||||
<Display type='trafficSpeed' value={subscribe?.speed_limit} unlimited />
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<span className='text-muted-foreground'>{t('connectedDevices')}</span>
|
||||
<span className=''>{t('connectedDevices')}</span>
|
||||
<span>
|
||||
<Display value={subscribe?.device_limit} type='number' unlimited />
|
||||
</span>
|
||||
|
||||
@@ -1,28 +1,38 @@
|
||||
'use client';
|
||||
|
||||
import CouponInput from '@/components/subscribe/coupon-input';
|
||||
import DurationSelector from '@/components/subscribe/duration-selector';
|
||||
import PaymentMethods from '@/components/subscribe/payment-methods';
|
||||
import CloseSvg from '@/components/CustomIcon/icons/close.svg';
|
||||
import useGlobalStore from '@/config/use-global';
|
||||
import { preCreateOrder, purchase } from '@/services/user/order';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Button } from '@workspace/ui/components/button';
|
||||
import { Card, CardContent } from '@workspace/ui/components/card';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@workspace/ui/components/dialog';
|
||||
import { Separator } from '@workspace/ui/components/separator';
|
||||
import { Tabs, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
|
||||
import { LoaderCircle } from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useCallback, useEffect, useRef, useState, useTransition } from 'react';
|
||||
import {
|
||||
forwardRef,
|
||||
useCallback,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
useState,
|
||||
useTransition,
|
||||
} from 'react';
|
||||
import { SubscribeBilling } from './billing';
|
||||
import { SubscribeDetail } from './detail';
|
||||
|
||||
interface PurchaseProps {
|
||||
subscribe?: API.Subscribe;
|
||||
setSubscribe: (subscribe?: API.Subscribe) => void;
|
||||
}
|
||||
|
||||
export default function Purchase({ subscribe, setSubscribe }: Readonly<PurchaseProps>) {
|
||||
interface PurchaseDialogRef {
|
||||
show: (subscribe: API.Subscribe) => void;
|
||||
hide: () => void;
|
||||
}
|
||||
|
||||
const Purchase = forwardRef<PurchaseDialogRef, PurchaseProps>((props, ref) => {
|
||||
const t = useTranslations('subscribe');
|
||||
const { getUserInfo } = useGlobalStore();
|
||||
const router = useRouter();
|
||||
@@ -32,17 +42,39 @@ export default function Purchase({ subscribe, setSubscribe }: Readonly<PurchaseP
|
||||
payment: -1,
|
||||
coupon: '',
|
||||
});
|
||||
const [subscribe, setSubscribe] = useState<API.Subscribe | undefined>(props.subscribe);
|
||||
const [loading, startTransition] = useTransition();
|
||||
const [open, setOpen] = useState(false);
|
||||
const lastSuccessOrderRef = useRef<any>(null);
|
||||
const [tabValue, setTabValue] = useState('year');
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
show: (newSubscribe: API.Subscribe, tabValue: string) => {
|
||||
setSubscribe(newSubscribe);
|
||||
setParams((prev) => ({
|
||||
...prev,
|
||||
subscribe_id: newSubscribe.id,
|
||||
quantity: tabValue === 'year' ? 12 : 1,
|
||||
}));
|
||||
setTabValue(tabValue);
|
||||
setOpen(true);
|
||||
},
|
||||
hide: () => {
|
||||
setOpen(false);
|
||||
setSubscribe(undefined);
|
||||
setParams({ quantity: 1, subscribe_id: 0, payment: -1, coupon: '' });
|
||||
},
|
||||
}));
|
||||
|
||||
const { data: order } = useQuery({
|
||||
enabled: !!subscribe?.id,
|
||||
queryKey: ['preCreateOrder', params],
|
||||
enabled: !!subscribe?.id && params.quantity !== undefined,
|
||||
queryKey: ['preCreateOrder', subscribe?.id, params.quantity],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
const { data } = await preCreateOrder({
|
||||
...params,
|
||||
subscribe_id: subscribe?.id as number,
|
||||
quantity: params.quantity as number,
|
||||
} as API.PurchaseOrderRequest);
|
||||
const result = data.data;
|
||||
if (result) {
|
||||
@@ -58,16 +90,6 @@ export default function Purchase({ subscribe, setSubscribe }: Readonly<PurchaseP
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (subscribe) {
|
||||
setParams((prev) => ({
|
||||
...prev,
|
||||
quantity: 1,
|
||||
subscribe_id: subscribe?.id,
|
||||
}));
|
||||
}
|
||||
}, [subscribe]);
|
||||
|
||||
const handleChange = useCallback((field: keyof typeof params, value: string | number) => {
|
||||
setParams((prev) => ({
|
||||
...prev,
|
||||
@@ -81,8 +103,8 @@ export default function Purchase({ subscribe, setSubscribe }: Readonly<PurchaseP
|
||||
const response = await purchase(params as API.PurchaseOrderRequest);
|
||||
const orderNo = response.data.data?.order_no;
|
||||
if (orderNo) {
|
||||
getUserInfo();
|
||||
router.push(`/payment?order_no=${orderNo}`);
|
||||
await getUserInfo();
|
||||
router.push(`/dashboard`);
|
||||
}
|
||||
} catch (error) {
|
||||
/* empty */
|
||||
@@ -91,58 +113,73 @@ export default function Purchase({ subscribe, setSubscribe }: Readonly<PurchaseP
|
||||
}, [params, router, getUserInfo]);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={!!subscribe?.id}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) setSubscribe(undefined);
|
||||
}}
|
||||
>
|
||||
<DialogContent className='flex h-full max-w-screen-lg flex-col overflow-hidden border-none p-0 md:h-auto'>
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent
|
||||
className='rounded-0 flex h-full w-full max-w-full flex-col gap-0 overflow-hidden border-none px-[120px] py-8 sm:h-auto sm:w-[675px] sm:!rounded-[32px] sm:py-12'
|
||||
closeIcon={<Image src={CloseSvg} alt='close' />}
|
||||
closeClassName='right-6 top-6 font-bold text-black opacity-100 focus:ring-0 focus:ring-offset-0'
|
||||
>
|
||||
<DialogHeader className='p-6 pb-0'>
|
||||
<DialogTitle>{t('buySubscription')}</DialogTitle>
|
||||
<DialogTitle className='sr-only'>{t('buySubscription')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className='grid w-full flex-grow gap-3 overflow-auto p-6 pt-0 lg:grid-cols-2'>
|
||||
<Card className='border-transparent shadow-none md:border-inherit md:shadow'>
|
||||
<CardContent className='grid gap-3 p-0 text-sm md:p-6'>
|
||||
<SubscribeDetail
|
||||
subscribe={{
|
||||
...subscribe,
|
||||
quantity: params.quantity,
|
||||
}}
|
||||
/>
|
||||
<Separator />
|
||||
<SubscribeBilling
|
||||
order={{
|
||||
...order,
|
||||
quantity: params.quantity,
|
||||
unit_price: subscribe?.unit_price,
|
||||
}}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className='flex flex-col justify-between text-sm'>
|
||||
<div className='mb-6 grid gap-3'>
|
||||
<DurationSelector
|
||||
quantity={params.quantity!}
|
||||
unitTime={subscribe?.unit_time}
|
||||
discounts={subscribe?.discount}
|
||||
onChange={(value) => {
|
||||
handleChange('quantity', value);
|
||||
}}
|
||||
/>
|
||||
<CouponInput
|
||||
coupon={params.coupon}
|
||||
onChange={(value) => handleChange('coupon', value)}
|
||||
/>
|
||||
<PaymentMethods
|
||||
value={params.payment!}
|
||||
onChange={(value) => {
|
||||
handleChange('payment', value);
|
||||
}}
|
||||
/>
|
||||
<div>
|
||||
<div className='text-4xl font-bold text-[#0F2C53] sm:mb-8 sm:text-center sm:text-4xl'>
|
||||
购买套餐
|
||||
</div>
|
||||
<div>
|
||||
<Tabs
|
||||
defaultValue='year'
|
||||
className='mt-8 text-center sm:mt-6'
|
||||
value={tabValue}
|
||||
onValueChange={(val) => {
|
||||
if (val === 'year') {
|
||||
handleChange('quantity', 12);
|
||||
} else if (val === 'month') {
|
||||
handleChange('quantity', 1);
|
||||
}
|
||||
setTabValue(val);
|
||||
}}
|
||||
>
|
||||
<TabsList className='mb-8 h-[74px] flex-wrap rounded-full bg-[#EAEAEA] p-2.5'>
|
||||
<TabsTrigger
|
||||
className='rounded-full px-10 py-3.5 text-xl data-[state=active]:bg-[#0F2C53] data-[state=active]:text-white md:px-12'
|
||||
value='year'
|
||||
>
|
||||
年付套餐
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
className='rounded-full px-10 py-3.5 text-xl data-[state=active]:bg-[#0F2C53] data-[state=active]:text-white md:px-12'
|
||||
value='month'
|
||||
>
|
||||
月付套餐
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</div>
|
||||
<div>
|
||||
<SubscribeDetail
|
||||
subscribe={{
|
||||
...subscribe,
|
||||
quantity: params.quantity,
|
||||
}}
|
||||
/>
|
||||
<Separator className='mb-3 mt-4 bg-[#225BA9]' />
|
||||
<SubscribeBilling
|
||||
order={{
|
||||
...order,
|
||||
quantity: params.quantity,
|
||||
unit_price: subscribe?.unit_price,
|
||||
}}
|
||||
/>
|
||||
<Separator className='mb-3 mt-4 bg-[#225BA9]' />
|
||||
<div className='flex items-center justify-between text-[15px] text-[#225BA9]'>
|
||||
<div>支付方式</div>
|
||||
<div className='font-light text-[#666]'>钱包余额</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='mt-8 flex items-center justify-center'>
|
||||
<Button
|
||||
className='fixed bottom-0 left-0 w-full rounded-none md:relative md:mt-6'
|
||||
className='w-[150px] rounded-full border-[#A8D4ED] bg-[#A8D4ED] text-xl hover:bg-[#225BA9] hover:text-white'
|
||||
disabled={loading}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
@@ -154,4 +191,7 @@ export default function Purchase({ subscribe, setSubscribe }: Readonly<PurchaseP
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
Purchase.displayName = 'Purchase';
|
||||
export default Purchase;
|
||||
|
||||
@@ -26,9 +26,10 @@ import { SubscribeDetail } from './detail';
|
||||
interface RenewalProps {
|
||||
id: number;
|
||||
subscribe: API.Subscribe;
|
||||
className: string;
|
||||
}
|
||||
|
||||
export default function Renewal({ id, subscribe }: Readonly<RenewalProps>) {
|
||||
export default function Renewal({ id, subscribe, className }: Readonly<RenewalProps>) {
|
||||
const t = useTranslations('subscribe');
|
||||
const { getUserInfo } = useGlobalStore();
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
@@ -101,7 +102,9 @@ export default function Renewal({ id, subscribe }: Readonly<RenewalProps>) {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button size='sm'>{t('renew')}</Button>
|
||||
<Button size='sm' className={className}>
|
||||
续订套餐
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className='flex h-full max-w-screen-lg flex-col overflow-hidden md:h-auto'>
|
||||
<DialogHeader>
|
||||
|
||||
@@ -22,7 +22,7 @@ interface ResetTrafficProps {
|
||||
id: number;
|
||||
replacement?: number;
|
||||
}
|
||||
export default function ResetTraffic({ id, replacement }: Readonly<ResetTrafficProps>) {
|
||||
export default function ResetTraffic({ id, replacement, className }: Readonly<ResetTrafficProps>) {
|
||||
const t = useTranslations('subscribe');
|
||||
const { getUserInfo } = useGlobalStore();
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
@@ -48,7 +48,7 @@ export default function ResetTraffic({ id, replacement }: Readonly<ResetTrafficP
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant='secondary' size='sm'>
|
||||
<Button variant='secondary' size='sm' className={className}>
|
||||
{t('resetTraffic')}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
|
||||
@@ -40,7 +40,7 @@ export function UserNav({ from = '' }: { from?: string }) {
|
||||
<Icon icon='lucide:ellipsis' className='text-muted-foreground !size-6' />
|
||||
</div>
|
||||
) : (
|
||||
<Avatar className='h-16 w-16 cursor-pointer'>
|
||||
<Avatar className='h-14 w-14 cursor-pointer sm:h-16 sm:w-16'>
|
||||
<AvatarImage
|
||||
alt={user?.avatar ?? ''}
|
||||
src={user?.auth_methods?.[0]?.auth_identifier ?? ''}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"@stripe/stripe-js": "^6.0.0",
|
||||
"@tanstack/react-query": "^5.63.0",
|
||||
"@tanstack/react-query-next-experimental": "^5.63.0",
|
||||
"@workspace/airo-ui": "workspace:*",
|
||||
"@workspace/ui": "workspace:*",
|
||||
"ahooks": "^3.8.4",
|
||||
"axios": "^1.7.9",
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./*"],
|
||||
"@workspace/ui/*": ["../../packages/ui/src/*"]
|
||||
"@workspace/ui/*": ["../../packages/ui/src/*"],
|
||||
"@workspace/airo-ui/*": ["../../packages/airo-ui/src/*"]
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user