fix: 样式修改
This commit is contained in:
parent
c1aa738154
commit
816df42bc3
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
|
<state>
|
||||||
|
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
|
||||||
|
</state>
|
||||||
|
</component>
|
||||||
6
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
6
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
</profile>
|
||||||
|
</component>
|
||||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/ppanel-web.iml" filepath="$PROJECT_DIR$/.idea/ppanel-web.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
12
.idea/ppanel-web.iml
generated
Normal file
12
.idea/ppanel-web.iml
generated
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="WEB_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$">
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
||||||
|
</content>
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@ -1,47 +1,14 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Display } from '@/components/display';
|
|
||||||
import Renewal from '@/components/subscribe/renewal';
|
|
||||||
import ResetTraffic from '@/components/subscribe/reset-traffic';
|
|
||||||
import Unsubscribe from '@/components/subscribe/unsubscribe';
|
|
||||||
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 { queryApplicationConfig } from '@/services/user/subscribe';
|
import { queryApplicationConfig } from '@/services/user/subscribe';
|
||||||
import { queryUserSubscribe, resetUserSubscribeToken } from '@/services/user/user';
|
import { queryUserSubscribe } from '@/services/user/user';
|
||||||
import { getPlatform } from '@/utils/common';
|
import { getPlatform } from '@/utils/common';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import {
|
import { Card } from '@workspace/ui/components/card';
|
||||||
Accordion,
|
|
||||||
AccordionContent,
|
|
||||||
AccordionItem,
|
|
||||||
AccordionTrigger,
|
|
||||||
} from '@workspace/ui/components/accordion';
|
|
||||||
import {
|
|
||||||
AlertDialog,
|
|
||||||
AlertDialogAction,
|
|
||||||
AlertDialogCancel,
|
|
||||||
AlertDialogContent,
|
|
||||||
AlertDialogDescription,
|
|
||||||
AlertDialogFooter,
|
|
||||||
AlertDialogHeader,
|
|
||||||
AlertDialogTitle,
|
|
||||||
AlertDialogTrigger,
|
|
||||||
} from '@workspace/ui/components/alert-dialog';
|
|
||||||
import { Button } from '@workspace/ui/components/button';
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@workspace/ui/components/card';
|
|
||||||
import { Separator } from '@workspace/ui/components/separator';
|
|
||||||
import { Tabs, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
|
|
||||||
import { Icon } from '@workspace/ui/custom-components/icon';
|
|
||||||
import { cn } from '@workspace/ui/lib/utils';
|
|
||||||
import { differenceInDays, formatDate, isBrowser } from '@workspace/ui/utils';
|
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import Image from 'next/image';
|
|
||||||
import Link from 'next/link';
|
|
||||||
import { QRCodeCanvas } from 'qrcode.react';
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import CopyToClipboard from 'react-copy-to-clipboard';
|
|
||||||
import { toast } from 'sonner';
|
|
||||||
import Subscribe from '../subscribe/page';
|
|
||||||
|
|
||||||
const platforms: (keyof API.ApplicationPlatform)[] = [
|
const platforms: (keyof API.ApplicationPlatform)[] = [
|
||||||
'windows',
|
'windows',
|
||||||
@ -296,7 +263,7 @@ export default function Content() {
|
|||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
{userSubscribe.length ? (
|
{/*{userSubscribe.length ? (
|
||||||
<>
|
<>
|
||||||
<div className='flex items-center justify-between'>
|
<div className='flex items-center justify-between'>
|
||||||
<h2 className='flex items-center gap-1.5 font-semibold'>
|
<h2 className='flex items-center gap-1.5 font-semibold'>
|
||||||
@ -602,7 +569,7 @@ export default function Content() {
|
|||||||
</h2>
|
</h2>
|
||||||
<Subscribe />
|
<Subscribe />
|
||||||
</>
|
</>
|
||||||
)}
|
)}*/}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,17 +4,10 @@ import { Display } from '@/components/display';
|
|||||||
import { Empty } from '@/components/empty';
|
import { Empty } from '@/components/empty';
|
||||||
import { ProList, ProListActions } from '@/components/pro-list';
|
import { ProList, ProListActions } from '@/components/pro-list';
|
||||||
import { closeOrder, queryOrderList } from '@/services/user/order';
|
import { closeOrder, queryOrderList } from '@/services/user/order';
|
||||||
import { Button, buttonVariants } from '@workspace/ui/components/button';
|
import { Button } from '@workspace/ui/components/button';
|
||||||
import {
|
import { Card, CardContent } from '@workspace/ui/components/card';
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from '@workspace/ui/components/card';
|
|
||||||
import { formatDate } from '@workspace/ui/utils';
|
import { formatDate } from '@workspace/ui/utils';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import Link from 'next/link';
|
|
||||||
import { useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
@ -33,26 +26,20 @@ export default function Page() {
|
|||||||
}}
|
}}
|
||||||
renderItem={(item) => {
|
renderItem={(item) => {
|
||||||
return (
|
return (
|
||||||
<Card className='overflow-hidden'>
|
<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)]'>
|
||||||
<CardHeader className='bg-muted/50 flex flex-row items-center justify-between gap-2 space-y-0 p-3'>
|
<div className={'flex justify-between'}>
|
||||||
<CardTitle>
|
<div>
|
||||||
{t('orderNo')}
|
<div className={'text-[#666]'}>{t('orderNo')}</div>
|
||||||
<p className='text-sm'>{item.order_no}</p>
|
<p className='text-xs text-[#4D4D4D]'>{item.order_no}</p>
|
||||||
</CardTitle>
|
</div>
|
||||||
<CardDescription className='flex gap-2'>
|
<div>
|
||||||
{item.status === 1 ? (
|
{item.status === 1 ? (
|
||||||
<>
|
<>
|
||||||
<Link
|
<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'>
|
||||||
key='payment'
|
|
||||||
href={`/payment?order_no=${item.order_no}`}
|
|
||||||
className={buttonVariants({ size: 'sm' })}
|
|
||||||
>
|
|
||||||
{t('payment')}
|
{t('payment')}
|
||||||
</Link>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
key='cancel'
|
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'
|
||||||
size='sm'
|
|
||||||
variant='destructive'
|
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await closeOrder({ orderNo: item.order_no });
|
await closeOrder({ orderNo: item.order_no });
|
||||||
ref.current?.refresh();
|
ref.current?.refresh();
|
||||||
@ -62,35 +49,32 @@ export default function Page() {
|
|||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Link
|
<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'>
|
||||||
key='detail'
|
|
||||||
href={`/payment?order_no=${item.order_no}`}
|
|
||||||
className={buttonVariants({ size: 'sm' })}
|
|
||||||
>
|
|
||||||
{t('detail')}
|
{t('detail')}
|
||||||
</Link>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</CardDescription>
|
</div>
|
||||||
</CardHeader>
|
</div>
|
||||||
|
|
||||||
<CardContent className='p-3 text-sm'>
|
<CardContent className='p-3 text-sm'>
|
||||||
<ul className='grid grid-cols-2 gap-3 *:flex *:flex-col lg:grid-cols-4'>
|
<ul className='grid grid-cols-2 gap-3 *:flex *:flex-col lg:grid-cols-4'>
|
||||||
<li>
|
<li>
|
||||||
<span className='text-muted-foreground'>{t('name')}</span>
|
<span className='text-[#225BA9]'>{t('name')}</span>
|
||||||
<span>{item.subscribe.name || t(`type.${item.type}`)}</span>
|
<span className={'font-bold text-[#091B33]'}>
|
||||||
|
{item.subscribe.name || t(`type.${item.type}`)}
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<li className='font-semibold'>
|
<li className='font-semibold'>
|
||||||
<span className='text-muted-foreground'>{t('paymentAmount')}</span>
|
<span className='text-[#225BA9]'>{t('paymentAmount')}</span>
|
||||||
<span>
|
<span>
|
||||||
<Display type='currency' value={item.amount} />
|
<Display type='currency' value={item.amount} />
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<li className='font-semibold'>
|
<li className='font-semibold'>
|
||||||
<span className='text-muted-foreground'>{t('status.0')}</span>
|
<span className='text-[#225BA9]'>{t('status.0')}</span>
|
||||||
<span>{t(`status.${item.status}`)}</span>
|
<span>{t(`status.${item.status}`)}</span>
|
||||||
</li>
|
</li>
|
||||||
<li className='font-semibold'>
|
<li className='font-semibold'>
|
||||||
<span className='text-muted-foreground'>{t('createdAt')}</span>
|
<span className='text-[#225BA9]'>{t('createdAt')}</span>
|
||||||
<time>{formatDate(item.created_at)}</time>
|
<time>{formatDate(item.created_at)}</time>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@ -30,7 +30,7 @@ export function SidebarLeft({ ...props }: React.ComponentProps<typeof Sidebar>)
|
|||||||
<SidebarMenuItem key={item.title} className={''}>
|
<SidebarMenuItem key={item.title} className={''}>
|
||||||
<SidebarMenuButton
|
<SidebarMenuButton
|
||||||
className={
|
className={
|
||||||
'h-[60px] rounded-full px-5 py-[18px] text-xl hover:bg-[#0F2C53] active:bg-[#0F2C53] data-[active=true]:bg-[#0F2C53]'
|
'h-[60px] rounded-full px-5 py-[18px] text-xl hover:bg-[#EAEAEA] hover:text-[#0F2C53] active:bg-[#EAEAEA] active:text-[#0F2C53] data-[active=true]:bg-[#0F2C53]'
|
||||||
}
|
}
|
||||||
asChild
|
asChild
|
||||||
tooltip={t(item.title)}
|
tooltip={t(item.title)}
|
||||||
|
|||||||
@ -1,142 +1,112 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Display } from '@/components/display';
|
import { querySubscribeList } from '@/services/user/subscribe';
|
||||||
import { querySubscribeGroupList, querySubscribeList } from '@/services/user/subscribe';
|
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { Button } from '@workspace/ui/components/button';
|
|
||||||
import { Card, CardContent, CardFooter, CardHeader } from '@workspace/ui/components/card';
|
|
||||||
import { Separator } from '@workspace/ui/components/separator';
|
|
||||||
import { Tabs, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
|
import { Tabs, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
|
||||||
import { Icon } from '@workspace/ui/custom-components/icon';
|
|
||||||
import { cn } from '@workspace/ui/lib/utils';
|
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import { useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { Empty } from '@/components/empty';
|
import { TabContent } from '@/components/main/OfferDialog/TabContent';
|
||||||
import { SubscribeDetail } from '@/components/subscribe/detail';
|
import { ProcessedPlanData } from '@/components/main/OfferDialog/types';
|
||||||
import Purchase from '@/components/subscribe/purchase';
|
import Purchase from '@/components/subscribe/purchase';
|
||||||
|
import { unitConversion } from '@workspace/ui/utils';
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const t = useTranslations('subscribe');
|
const t = useTranslations('subscribe');
|
||||||
const [subscribe, setSubscribe] = useState<API.Subscribe>();
|
const [subscribe, setSubscribe] = useState<API.Subscribe | undefined>();
|
||||||
|
const [tabValue, setTabValue] = useState<'year' | 'month'>('year');
|
||||||
|
|
||||||
const [group, setGroup] = useState<string>('');
|
const { data, isLoading, error, refetch } = useQuery({
|
||||||
|
|
||||||
const { data: groups } = useQuery({
|
|
||||||
queryKey: ['querySubscribeGroupList'],
|
|
||||||
queryFn: async () => {
|
|
||||||
const { data } = await querySubscribeGroupList();
|
|
||||||
return data.data?.list || [];
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { data } = useQuery({
|
|
||||||
queryKey: ['querySubscribeList'],
|
queryKey: ['querySubscribeList'],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const { data } = await querySubscribeList();
|
const { data } = await querySubscribeList();
|
||||||
return data.data?.list || [];
|
return data.data?.list?.filter((v) => v.unit_time === 'Month') || [];
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
// 处理套餐数据的工具函数
|
||||||
<>
|
const processPlanData = (item: API.Subscribe, isYearly: boolean): ProcessedPlanData => {
|
||||||
<Tabs value={group} onValueChange={setGroup} className='space-y-4'>
|
if (isYearly) {
|
||||||
{groups && groups.length > 0 && (
|
const discountItem = item.discount?.find((v) => v.quantity === 12);
|
||||||
<>
|
return {
|
||||||
<h1 className='text-muted-foreground w-full'>{t('category')}</h1>
|
|
||||||
<TabsList>
|
|
||||||
<TabsTrigger value=''>{t('all')}</TabsTrigger>
|
|
||||||
{groups.map((group) => (
|
|
||||||
<TabsTrigger key={group.id} value={String(group.id)}>
|
|
||||||
{group.name}
|
|
||||||
</TabsTrigger>
|
|
||||||
))}
|
|
||||||
</TabsList>
|
|
||||||
<h2 className='text-muted-foreground w-full'>{t('products')}</h2>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<div className='grid gap-4 md:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3'>
|
|
||||||
{data
|
|
||||||
?.filter((item) => item.show)
|
|
||||||
?.filter((item) => (group ? item.group_id === Number(group) : true))
|
|
||||||
?.map((item) => (
|
|
||||||
<Card className='flex flex-col' key={item.id}>
|
|
||||||
<CardHeader className='bg-muted/50 text-xl font-medium'>{item.name}</CardHeader>
|
|
||||||
<CardContent className='flex flex-grow flex-col gap-3 p-6 *:!text-sm'>
|
|
||||||
{/* <div className='font-semibold'>{t('productDescription')}</div> */}
|
|
||||||
<ul className='flex flex-grow flex-col gap-3'>
|
|
||||||
{(() => {
|
|
||||||
let parsedDescription;
|
|
||||||
try {
|
|
||||||
parsedDescription = JSON.parse(item.description);
|
|
||||||
} catch {
|
|
||||||
parsedDescription = { description: '', features: [] };
|
|
||||||
}
|
|
||||||
|
|
||||||
const { description, features } = parsedDescription;
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{description && <li className='text-muted-foreground'>{description}</li>}
|
|
||||||
{features?.map(
|
|
||||||
(
|
|
||||||
feature: {
|
|
||||||
icon: string;
|
|
||||||
label: string;
|
|
||||||
type: 'default' | 'success' | 'destructive';
|
|
||||||
},
|
|
||||||
index: number,
|
|
||||||
) => (
|
|
||||||
<li
|
|
||||||
className={cn('flex items-center gap-1', {
|
|
||||||
'text-muted-foreground line-through':
|
|
||||||
feature.type === 'destructive',
|
|
||||||
})}
|
|
||||||
key={index}
|
|
||||||
>
|
|
||||||
{feature.icon && (
|
|
||||||
<Icon
|
|
||||||
icon={feature.icon}
|
|
||||||
className={cn('text-primary size-5', {
|
|
||||||
'text-green-500': feature.type === 'success',
|
|
||||||
'text-destructive': feature.type === 'destructive',
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{feature.label}
|
|
||||||
</li>
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
})()}
|
|
||||||
</ul>
|
|
||||||
<SubscribeDetail
|
|
||||||
subscribe={{
|
|
||||||
...item,
|
...item,
|
||||||
name: undefined,
|
origin_price: unitConversion('centsToDollars', item.unit_price * 12).toString(), // 原价
|
||||||
}}
|
discount_price: unitConversion(
|
||||||
/>
|
'centsToDollars',
|
||||||
</CardContent>
|
item.unit_price * ((discountItem?.discount || 100) / 100) * 12,
|
||||||
<Separator />
|
).toString(), // 优惠价格
|
||||||
<CardFooter className='relative mt-2 flex flex-col gap-2'>
|
};
|
||||||
<h2 className='pb-5 text-2xl font-semibold sm:text-3xl'>
|
} else {
|
||||||
<Display type='currency' value={item.unit_price} />
|
return {
|
||||||
<span className='text-base font-medium'>/{t(item.unit_time || 'Month')}</span>
|
...item,
|
||||||
</h2>
|
origin_price: '', // 月付没有原价
|
||||||
<Button
|
discount_price: unitConversion('centsToDollars', item.unit_price).toString(), // 月付价格
|
||||||
className='absolute bottom-0 w-full rounded-b-xl rounded-t-none'
|
};
|
||||||
onClick={() => {
|
}
|
||||||
setSubscribe(item);
|
};
|
||||||
}}
|
|
||||||
>
|
// 使用 useMemo 优化数据处理性能
|
||||||
{t('buy')}
|
const yearlyPlans: ProcessedPlanData[] = useMemo(
|
||||||
</Button>
|
() => (data || []).map((item) => processPlanData(item, true)),
|
||||||
</CardFooter>
|
[data],
|
||||||
</Card>
|
);
|
||||||
))}
|
|
||||||
|
const monthlyPlans: ProcessedPlanData[] = useMemo(
|
||||||
|
() => (data || []).map((item) => processPlanData(item, false)),
|
||||||
|
[data],
|
||||||
|
);
|
||||||
|
|
||||||
|
// 处理订阅点击
|
||||||
|
const handleSubscribe = (plan: ProcessedPlanData) => {
|
||||||
|
console.log('用户选择了套餐:', plan);
|
||||||
|
// 这里可以添加订阅逻辑,比如跳转到支付页面或显示确认对话框
|
||||||
|
setSubscribe(plan);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={'text-4xl font-bold text-[#0F2C53] md:mb-4 md:text-center md:text-5xl'}>
|
||||||
|
选择套餐
|
||||||
</div>
|
</div>
|
||||||
{data?.length === 0 && <Empty />}
|
<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>
|
</Tabs>
|
||||||
|
<TabContent
|
||||||
|
tabValue={tabValue}
|
||||||
|
yearlyPlans={yearlyPlans}
|
||||||
|
monthlyPlans={monthlyPlans}
|
||||||
|
isLoading={isLoading}
|
||||||
|
error={error}
|
||||||
|
onRetry={refetch}
|
||||||
|
onSubscribe={handleSubscribe}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<Purchase subscribe={subscribe} setSubscribe={setSubscribe} />
|
<Purchase subscribe={subscribe} setSubscribe={setSubscribe} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
54
apps/user/components/main/OfferDialog/TabContent.tsx
Normal file
54
apps/user/components/main/OfferDialog/TabContent.tsx
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { PlanList } from './index';
|
||||||
|
import { ProcessedPlanData } from './types';
|
||||||
|
|
||||||
|
interface TabContentProps {
|
||||||
|
tabValue: string;
|
||||||
|
yearlyPlans: ProcessedPlanData[];
|
||||||
|
monthlyPlans: ProcessedPlanData[];
|
||||||
|
isLoading: boolean;
|
||||||
|
error: Error | null;
|
||||||
|
onRetry: () => void;
|
||||||
|
onSubscribe: (plan: ProcessedPlanData) => void;
|
||||||
|
firstPlanCardRef?: React.RefObject<HTMLDivElement | null>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TabContent: React.FC<TabContentProps> = ({
|
||||||
|
tabValue,
|
||||||
|
yearlyPlans,
|
||||||
|
monthlyPlans,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
onRetry,
|
||||||
|
onSubscribe,
|
||||||
|
firstPlanCardRef,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{tabValue === 'year' && (
|
||||||
|
<PlanList
|
||||||
|
plans={yearlyPlans}
|
||||||
|
isLoading={isLoading}
|
||||||
|
tabValue={tabValue}
|
||||||
|
error={error}
|
||||||
|
onRetry={onRetry}
|
||||||
|
emptyMessage='暂无年付套餐'
|
||||||
|
onSubscribe={onSubscribe}
|
||||||
|
firstPlanCardRef={firstPlanCardRef}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{tabValue === 'month' && (
|
||||||
|
<PlanList
|
||||||
|
plans={monthlyPlans}
|
||||||
|
tabValue={tabValue}
|
||||||
|
isLoading={isLoading}
|
||||||
|
error={error}
|
||||||
|
onRetry={onRetry}
|
||||||
|
emptyMessage='暂无月付套餐'
|
||||||
|
onSubscribe={onSubscribe}
|
||||||
|
firstPlanCardRef={firstPlanCardRef}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -16,34 +16,8 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
||||||
// 定义数据类型
|
import { TabContent } from './TabContent';
|
||||||
interface SubscriptionData {
|
import { ProcessedPlanData } from './types';
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
price: number;
|
|
||||||
originalPrice: number;
|
|
||||||
duration: string;
|
|
||||||
unit_price: number;
|
|
||||||
discount: Array<{
|
|
||||||
quantity: number;
|
|
||||||
discount: number;
|
|
||||||
}>;
|
|
||||||
features: {
|
|
||||||
traffic: string;
|
|
||||||
duration: string;
|
|
||||||
onlineIPs: string;
|
|
||||||
connections: string;
|
|
||||||
bandwidth: string;
|
|
||||||
nodes: string;
|
|
||||||
stability: number; // 星级 1-5
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理后的套餐数据类型
|
|
||||||
interface ProcessedPlanData extends SubscriptionData {
|
|
||||||
origin_price: string;
|
|
||||||
discount_price: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载状态组件
|
// 加载状态组件
|
||||||
const LoadingState = () => (
|
const LoadingState = () => (
|
||||||
@ -150,33 +124,22 @@ const FeatureList = ({ plan }: { plan: ProcessedPlanData }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 套餐卡片组件
|
// 套餐卡片组件
|
||||||
const PlanCard = ({
|
const PlanCard = forwardRef<
|
||||||
plan,
|
HTMLDivElement,
|
||||||
onSubscribe,
|
{
|
||||||
isFirstCard = false, // 新增参数标识是否为第一项
|
|
||||||
}: {
|
|
||||||
plan: ProcessedPlanData;
|
plan: ProcessedPlanData;
|
||||||
tabValue: string;
|
tabValue: string;
|
||||||
onSubscribe?: (plan: ProcessedPlanData) => void;
|
onSubscribe?: (plan: ProcessedPlanData) => void;
|
||||||
isFirstCard?: boolean;
|
isFirstCard?: boolean;
|
||||||
}) => {
|
|
||||||
const cardRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
// 如果是第一项,将ref传递给父组件
|
|
||||||
useEffect(() => {
|
|
||||||
if (isFirstCard && cardRef.current) {
|
|
||||||
// 可以通过回调函数将高度传递给父组件
|
|
||||||
// 或者使用ref转发
|
|
||||||
}
|
}
|
||||||
}, [isFirstCard]);
|
>(({ plan, onSubscribe, isFirstCard = false }, ref) => {
|
||||||
|
|
||||||
const handleSubscribe = () => {
|
const handleSubscribe = () => {
|
||||||
onSubscribe?.(plan);
|
onSubscribe?.(plan);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={isFirstCard ? cardRef : undefined}
|
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-4 transition-all duration-300 hover:shadow-lg sm:p-6 md:p-8'
|
||||||
>
|
>
|
||||||
{/* 套餐名称 */}
|
{/* 套餐名称 */}
|
||||||
@ -192,10 +155,12 @@ const PlanCard = ({
|
|||||||
<FeatureList plan={plan} />
|
<FeatureList plan={plan} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
|
PlanCard.displayName = 'PlanCard';
|
||||||
|
|
||||||
// 套餐列表组件
|
// 套餐列表组件
|
||||||
const PlanList = ({
|
export const PlanList = ({
|
||||||
plans,
|
plans,
|
||||||
tabValue,
|
tabValue,
|
||||||
isLoading,
|
isLoading,
|
||||||
@ -212,7 +177,7 @@ const PlanList = ({
|
|||||||
onRetry: () => void;
|
onRetry: () => void;
|
||||||
emptyMessage: string;
|
emptyMessage: string;
|
||||||
onSubscribe?: (plan: ProcessedPlanData) => void;
|
onSubscribe?: (plan: ProcessedPlanData) => void;
|
||||||
firstPlanCardRef?: React.RefObject<HTMLDivElement>;
|
firstPlanCardRef?: React.RefObject<HTMLDivElement | null>;
|
||||||
}) => {
|
}) => {
|
||||||
if (isLoading) return <LoadingState />;
|
if (isLoading) return <LoadingState />;
|
||||||
if (error) return <ErrorState onRetry={onRetry} />;
|
if (error) return <ErrorState onRetry={onRetry} />;
|
||||||
@ -289,12 +254,12 @@ const OfferDialog = forwardRef<OfferDialogRef>((props, ref) => {
|
|||||||
const response = await getSubscription({ skipErrorHandler: true });
|
const response = await getSubscription({ skipErrorHandler: true });
|
||||||
// 确保返回有效的数组,避免 undefined
|
// 确保返回有效的数组,避免 undefined
|
||||||
const list = response.data?.data?.list || [];
|
const list = response.data?.data?.list || [];
|
||||||
return list.filter((v) => v.unit_time === 'Month') as unknown as SubscriptionData[];
|
return list.filter((v) => v.unit_time === 'Month') as unknown as ProcessedPlanData[];
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// 自定义错误处理
|
// 自定义错误处理
|
||||||
console.error('获取订阅数据失败:', err);
|
console.error('获取订阅数据失败:', err);
|
||||||
// 返回空数组而不是抛出错误,避免 queryFn 返回 undefined
|
// 返回空数组而不是抛出错误,避免 queryFn 返回 undefined
|
||||||
return [] as SubscriptionData[];
|
return [] as ProcessedPlanData[];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
enabled: false, // 初始不执行,手动控制
|
enabled: false, // 初始不执行,手动控制
|
||||||
@ -336,7 +301,7 @@ const OfferDialog = forwardRef<OfferDialogRef>((props, ref) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 处理套餐数据的工具函数
|
// 处理套餐数据的工具函数
|
||||||
const processPlanData = (item: SubscriptionData, isYearly: boolean): ProcessedPlanData => {
|
const processPlanData = (item: ProcessedPlanData, isYearly: boolean): ProcessedPlanData => {
|
||||||
if (isYearly) {
|
if (isYearly) {
|
||||||
const discountItem = item.discount?.find((v) => v.quantity === 12);
|
const discountItem = item.discount?.find((v) => v.quantity === 12);
|
||||||
return {
|
return {
|
||||||
@ -377,14 +342,13 @@ const OfferDialog = forwardRef<OfferDialogRef>((props, ref) => {
|
|||||||
'right-6 top-6 font-bold text-black opacity-100 focus:ring-0 focus:ring-offset-0'
|
'right-6 top-6 font-bold text-black opacity-100 focus:ring-0 focus:ring-offset-0'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<DialogTitle className={''}>
|
<DialogTitle className={'sr-only'}></DialogTitle>
|
||||||
<div className={'text-4xl font-bold text-[#0F2C53] md:mb-4 md:text-center md:text-5xl'}>
|
<div className={'text-4xl font-bold text-[#0F2C53] md:mb-4 md:text-center md:text-5xl'}>
|
||||||
选择套餐
|
选择套餐
|
||||||
</div>
|
</div>
|
||||||
<div className={'text-lg font-medium text-[#666666] md:text-center'}>
|
<div className={'text-lg font-medium text-[#666666] md:text-center'}>
|
||||||
选择最适合您的服务套餐
|
选择最适合您的服务套餐
|
||||||
</div>
|
</div>
|
||||||
</DialogTitle>
|
|
||||||
<div>
|
<div>
|
||||||
<Tabs
|
<Tabs
|
||||||
defaultValue='year'
|
defaultValue='year'
|
||||||
@ -416,32 +380,16 @@ const OfferDialog = forwardRef<OfferDialogRef>((props, ref) => {
|
|||||||
className='overflow-y-auto'
|
className='overflow-y-auto'
|
||||||
style={{ height: `calc(${scrollAreaHeight}px - 32px)` }}
|
style={{ height: `calc(${scrollAreaHeight}px - 32px)` }}
|
||||||
>
|
>
|
||||||
<div>
|
<TabContent
|
||||||
{tabValue === 'year' && (
|
|
||||||
<PlanList
|
|
||||||
plans={yearlyPlans}
|
|
||||||
isLoading={isLoading}
|
|
||||||
tabValue={tabValue}
|
|
||||||
error={error}
|
|
||||||
onRetry={refetch}
|
|
||||||
emptyMessage='暂无年付套餐'
|
|
||||||
onSubscribe={handleSubscribe}
|
|
||||||
firstPlanCardRef={firstPlanCardRef} // 传递ref给第一项
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{tabValue === 'month' && (
|
|
||||||
<PlanList
|
|
||||||
plans={monthlyPlans}
|
|
||||||
tabValue={tabValue}
|
tabValue={tabValue}
|
||||||
|
yearlyPlans={yearlyPlans}
|
||||||
|
monthlyPlans={monthlyPlans}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
error={error}
|
error={error}
|
||||||
onRetry={refetch}
|
onRetry={refetch}
|
||||||
emptyMessage='暂无月付套餐'
|
|
||||||
onSubscribe={handleSubscribe}
|
onSubscribe={handleSubscribe}
|
||||||
firstPlanCardRef={firstPlanCardRef} // 传递ref给第一项
|
firstPlanCardRef={firstPlanCardRef}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
26
apps/user/components/main/OfferDialog/types.ts
Normal file
26
apps/user/components/main/OfferDialog/types.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
export interface SubscriptionData {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
price: number;
|
||||||
|
originalPrice: number;
|
||||||
|
duration: string;
|
||||||
|
unit_price: number;
|
||||||
|
discount: Array<{
|
||||||
|
quantity: number;
|
||||||
|
discount: number;
|
||||||
|
}>;
|
||||||
|
features: {
|
||||||
|
traffic: string;
|
||||||
|
duration: string;
|
||||||
|
onlineIPs: string;
|
||||||
|
connections: string;
|
||||||
|
bandwidth: string;
|
||||||
|
nodes: string;
|
||||||
|
stability: number; // 星级 1-5
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProcessedPlanData extends SubscriptionData {
|
||||||
|
origin_price: string;
|
||||||
|
discount_price: string;
|
||||||
|
}
|
||||||
@ -4,19 +4,11 @@ export const navs = [
|
|||||||
url: '/dashboard',
|
url: '/dashboard',
|
||||||
icon: 'uil:dashboard',
|
icon: 'uil:dashboard',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: 'server',
|
|
||||||
items: [
|
|
||||||
{
|
{
|
||||||
url: '/subscribe',
|
url: '/subscribe',
|
||||||
icon: 'uil:shop',
|
icon: 'uil:shop',
|
||||||
title: 'subscribe',
|
title: 'subscribe',
|
||||||
},
|
},
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'finance',
|
|
||||||
items: [
|
|
||||||
{
|
{
|
||||||
url: '/order',
|
url: '/order',
|
||||||
icon: 'uil:notes',
|
icon: 'uil:notes',
|
||||||
@ -32,11 +24,6 @@ export const navs = [
|
|||||||
icon: 'uil:users-alt',
|
icon: 'uil:users-alt',
|
||||||
title: 'affiliate',
|
title: 'affiliate',
|
||||||
},
|
},
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'help',
|
|
||||||
items: [
|
|
||||||
{
|
{
|
||||||
url: '/document',
|
url: '/document',
|
||||||
icon: 'uil:book-alt',
|
icon: 'uil:book-alt',
|
||||||
@ -52,8 +39,6 @@ export const navs = [
|
|||||||
icon: 'uil:message',
|
icon: 'uil:message',
|
||||||
title: 'ticket',
|
title: 'ticket',
|
||||||
},
|
},
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export function findNavByUrl(url: string) {
|
export function findNavByUrl(url: string) {
|
||||||
|
|||||||
@ -132,7 +132,7 @@ export function ProList<TData, TValue extends Record<string, unknown>>({
|
|||||||
const selectedCount = selectedRows.length;
|
const selectedCount = selectedRows.length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex max-w-full flex-col gap-4 overflow-hidden'>
|
<div className='flex max-w-full flex-col gap-4'>
|
||||||
<div className='flex flex-wrap-reverse items-center justify-between gap-4'>
|
<div className='flex flex-wrap-reverse items-center justify-between gap-4'>
|
||||||
<div>
|
<div>
|
||||||
{params ? (
|
{params ? (
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user