diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -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 diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..a55e7a1 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..03d9549 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..fd4586b --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/ppanel-web.iml b/.idea/ppanel-web.iml new file mode 100644 index 0000000..24643cc --- /dev/null +++ b/.idea/ppanel-web.iml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/apps/user/app/(main)/(content)/(user)/dashboard/content.tsx b/apps/user/app/(main)/(content)/(user)/dashboard/content.tsx index d023bf8..1a6667f 100644 --- a/apps/user/app/(main)/(content)/(user)/dashboard/content.tsx +++ b/apps/user/app/(main)/(content)/(user)/dashboard/content.tsx @@ -1,47 +1,14 @@ '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 { getStat } from '@/services/common/common'; 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 { useQuery } from '@tanstack/react-query'; -import { - 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 { Card } from '@workspace/ui/components/card'; 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 CopyToClipboard from 'react-copy-to-clipboard'; -import { toast } from 'sonner'; -import Subscribe from '../subscribe/page'; const platforms: (keyof API.ApplicationPlatform)[] = [ 'windows', @@ -296,7 +263,7 @@ export default function Content() { - {userSubscribe.length ? ( + {/*{userSubscribe.length ? ( <>

@@ -602,7 +569,7 @@ export default function Content() {

- )} + )}*/} ); } diff --git a/apps/user/app/(main)/(content)/(user)/order/page.tsx b/apps/user/app/(main)/(content)/(user)/order/page.tsx index fbdadd6..8e43846 100644 --- a/apps/user/app/(main)/(content)/(user)/order/page.tsx +++ b/apps/user/app/(main)/(content)/(user)/order/page.tsx @@ -4,17 +4,10 @@ import { Display } from '@/components/display'; import { Empty } from '@/components/empty'; import { ProList, ProListActions } from '@/components/pro-list'; import { closeOrder, queryOrderList } from '@/services/user/order'; -import { Button, buttonVariants } from '@workspace/ui/components/button'; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from '@workspace/ui/components/card'; +import { Button } from '@workspace/ui/components/button'; +import { Card, CardContent } from '@workspace/ui/components/card'; import { formatDate } from '@workspace/ui/utils'; import { useTranslations } from 'next-intl'; -import Link from 'next/link'; import { useRef } from 'react'; export default function Page() { @@ -33,26 +26,20 @@ export default function Page() { }} renderItem={(item) => { return ( - - - - {t('orderNo')} -

{item.order_no}

-
- + +
+
+
{t('orderNo')}
+

{item.order_no}

+
+
{item.status === 1 ? ( <> - + ) : ( - + )} - - - +
+
  • - {t('name')} - {item.subscribe.name || t(`type.${item.type}`)} + {t('name')} + + {item.subscribe.name || t(`type.${item.type}`)} +
  • - {t('paymentAmount')} + {t('paymentAmount')}
  • - {t('status.0')} + {t('status.0')} {t(`status.${item.status}`)}
  • - {t('createdAt')} + {t('createdAt')}
diff --git a/apps/user/app/(main)/(content)/(user)/sidebar-left.tsx b/apps/user/app/(main)/(content)/(user)/sidebar-left.tsx index 087710d..269142b 100644 --- a/apps/user/app/(main)/(content)/(user)/sidebar-left.tsx +++ b/apps/user/app/(main)/(content)/(user)/sidebar-left.tsx @@ -30,7 +30,7 @@ export function SidebarLeft({ ...props }: React.ComponentProps) (); + const [subscribe, setSubscribe] = useState(); + const [tabValue, setTabValue] = useState<'year' | 'month'>('year'); - const [group, setGroup] = useState(''); - - const { data: groups } = useQuery({ - queryKey: ['querySubscribeGroupList'], - queryFn: async () => { - const { data } = await querySubscribeGroupList(); - return data.data?.list || []; - }, - }); - - const { data } = useQuery({ + const { data, isLoading, error, refetch } = useQuery({ queryKey: ['querySubscribeList'], queryFn: async () => { const { data } = await querySubscribeList(); - return data.data?.list || []; + return data.data?.list?.filter((v) => v.unit_time === 'Month') || []; }, }); + // 处理套餐数据的工具函数 + const processPlanData = (item: API.Subscribe, isYearly: boolean): ProcessedPlanData => { + if (isYearly) { + const discountItem = item.discount?.find((v) => v.quantity === 12); + return { + ...item, + origin_price: unitConversion('centsToDollars', item.unit_price * 12).toString(), // 原价 + discount_price: unitConversion( + 'centsToDollars', + item.unit_price * ((discountItem?.discount || 100) / 100) * 12, + ).toString(), // 优惠价格 + }; + } else { + return { + ...item, + origin_price: '', // 月付没有原价 + discount_price: unitConversion('centsToDollars', item.unit_price).toString(), // 月付价格 + }; + } + }; + + // 使用 useMemo 优化数据处理性能 + const yearlyPlans: ProcessedPlanData[] = useMemo( + () => (data || []).map((item) => processPlanData(item, true)), + [data], + ); + + const monthlyPlans: ProcessedPlanData[] = useMemo( + () => (data || []).map((item) => processPlanData(item, false)), + [data], + ); + + // 处理订阅点击 + const handleSubscribe = (plan: ProcessedPlanData) => { + console.log('用户选择了套餐:', plan); + // 这里可以添加订阅逻辑,比如跳转到支付页面或显示确认对话框 + setSubscribe(plan); + }; + return ( <> - - {groups && groups.length > 0 && ( - <> -

{t('category')}

- - {t('all')} - {groups.map((group) => ( - - {group.name} - - ))} - -

{t('products')}

- - )} -
- {data - ?.filter((item) => item.show) - ?.filter((item) => (group ? item.group_id === Number(group) : true)) - ?.map((item) => ( - - {item.name} - - {/*
{t('productDescription')}
*/} -
    - {(() => { - let parsedDescription; - try { - parsedDescription = JSON.parse(item.description); - } catch { - parsedDescription = { description: '', features: [] }; - } - - const { description, features } = parsedDescription; - return ( - <> - {description &&
  • {description}
  • } - {features?.map( - ( - feature: { - icon: string; - label: string; - type: 'default' | 'success' | 'destructive'; - }, - index: number, - ) => ( -
  • - {feature.icon && ( - - )} - {feature.label} -
  • - ), - )} - - ); - })()} -
- -
- - -

- - /{t(item.unit_time || 'Month')} -

- -
-
- ))} -
- {data?.length === 0 && } -
+
+ 选择套餐 +
+
+ 选择最适合您的服务套餐 +
+
+ setTabValue(value as 'year' | 'month')} + > + + + 年付套餐 + + + 月付套餐 + + + + +
); diff --git a/apps/user/components/main/OfferDialog/TabContent.tsx b/apps/user/components/main/OfferDialog/TabContent.tsx new file mode 100644 index 0000000..df034aa --- /dev/null +++ b/apps/user/components/main/OfferDialog/TabContent.tsx @@ -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; +} + +export const TabContent: React.FC = ({ + tabValue, + yearlyPlans, + monthlyPlans, + isLoading, + error, + onRetry, + onSubscribe, + firstPlanCardRef, +}) => { + return ( +
+ {tabValue === 'year' && ( + + )} + {tabValue === 'month' && ( + + )} +
+ ); +}; diff --git a/apps/user/components/main/OfferDialog/index.tsx b/apps/user/components/main/OfferDialog/index.tsx index 701d3b1..998ef33 100644 --- a/apps/user/components/main/OfferDialog/index.tsx +++ b/apps/user/components/main/OfferDialog/index.tsx @@ -16,34 +16,8 @@ import { useState, } from 'react'; -// 定义数据类型 -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 - }; -} - -// 处理后的套餐数据类型 -interface ProcessedPlanData extends SubscriptionData { - origin_price: string; - discount_price: string; -} +import { TabContent } from './TabContent'; +import { ProcessedPlanData } from './types'; // 加载状态组件 const LoadingState = () => ( @@ -150,33 +124,22 @@ const FeatureList = ({ plan }: { plan: ProcessedPlanData }) => { }; // 套餐卡片组件 -const PlanCard = ({ - plan, - onSubscribe, - isFirstCard = false, // 新增参数标识是否为第一项 -}: { - plan: ProcessedPlanData; - tabValue: string; - onSubscribe?: (plan: ProcessedPlanData) => void; - isFirstCard?: boolean; -}) => { - const cardRef = useRef(null); - - // 如果是第一项,将ref传递给父组件 - useEffect(() => { - if (isFirstCard && cardRef.current) { - // 可以通过回调函数将高度传递给父组件 - // 或者使用ref转发 - } - }, [isFirstCard]); - +const PlanCard = forwardRef< + HTMLDivElement, + { + plan: ProcessedPlanData; + tabValue: string; + onSubscribe?: (plan: ProcessedPlanData) => void; + isFirstCard?: boolean; + } +>(({ plan, onSubscribe, isFirstCard = false }, ref) => { const handleSubscribe = () => { onSubscribe?.(plan); }; return (
{/* 套餐名称 */} @@ -192,10 +155,12 @@ const PlanCard = ({
); -}; +}); + +PlanCard.displayName = 'PlanCard'; // 套餐列表组件 -const PlanList = ({ +export const PlanList = ({ plans, tabValue, isLoading, @@ -212,7 +177,7 @@ const PlanList = ({ onRetry: () => void; emptyMessage: string; onSubscribe?: (plan: ProcessedPlanData) => void; - firstPlanCardRef?: React.RefObject; + firstPlanCardRef?: React.RefObject; }) => { if (isLoading) return ; if (error) return ; @@ -289,12 +254,12 @@ const OfferDialog = forwardRef((props, ref) => { const response = await getSubscription({ skipErrorHandler: true }); // 确保返回有效的数组,避免 undefined 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) { // 自定义错误处理 console.error('获取订阅数据失败:', err); // 返回空数组而不是抛出错误,避免 queryFn 返回 undefined - return [] as SubscriptionData[]; + return [] as ProcessedPlanData[]; } }, enabled: false, // 初始不执行,手动控制 @@ -336,7 +301,7 @@ const OfferDialog = forwardRef((props, ref) => { }; // 处理套餐数据的工具函数 - const processPlanData = (item: SubscriptionData, isYearly: boolean): ProcessedPlanData => { + const processPlanData = (item: ProcessedPlanData, isYearly: boolean): ProcessedPlanData => { if (isYearly) { const discountItem = item.discount?.find((v) => v.quantity === 12); return { @@ -377,14 +342,13 @@ const OfferDialog = forwardRef((props, ref) => { 'right-6 top-6 font-bold text-black opacity-100 focus:ring-0 focus:ring-offset-0' } > - -
- 选择套餐 -
-
- 选择最适合您的服务套餐 -
-
+ +
+ 选择套餐 +
+
+ 选择最适合您的服务套餐 +
((props, ref) => { className='overflow-y-auto' style={{ height: `calc(${scrollAreaHeight}px - 32px)` }} > -
- {tabValue === 'year' && ( - - )} - {tabValue === 'month' && ( - - )} -
+
diff --git a/apps/user/components/main/OfferDialog/types.ts b/apps/user/components/main/OfferDialog/types.ts new file mode 100644 index 0000000..ca9c2f5 --- /dev/null +++ b/apps/user/components/main/OfferDialog/types.ts @@ -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; +} diff --git a/apps/user/config/navs.ts b/apps/user/config/navs.ts index f5ed6bf..ca63770 100644 --- a/apps/user/config/navs.ts +++ b/apps/user/config/navs.ts @@ -5,54 +5,39 @@ export const navs = [ icon: 'uil:dashboard', }, { - title: 'server', - items: [ - { - url: '/subscribe', - icon: 'uil:shop', - title: 'subscribe', - }, - ], + url: '/subscribe', + icon: 'uil:shop', + title: 'subscribe', }, { - title: 'finance', - items: [ - { - url: '/order', - icon: 'uil:notes', - title: 'order', - }, - { - url: '/wallet', - icon: 'uil:wallet', - title: 'wallet', - }, - { - url: '/affiliate', - icon: 'uil:users-alt', - title: 'affiliate', - }, - ], + url: '/order', + icon: 'uil:notes', + title: 'order', }, { - title: 'help', - items: [ - { - url: '/document', - icon: 'uil:book-alt', - title: 'document', - }, - /*{ - url: '/announcement', - icon: 'uil:megaphone', - title: 'announcement', - },*/ - { - url: '/ticket', - icon: 'uil:message', - title: 'ticket', - }, - ], + url: '/wallet', + icon: 'uil:wallet', + title: 'wallet', + }, + { + url: '/affiliate', + icon: 'uil:users-alt', + title: 'affiliate', + }, + { + url: '/document', + icon: 'uil:book-alt', + title: 'document', + }, + /*{ + url: '/announcement', + icon: 'uil:megaphone', + title: 'announcement', + },*/ + { + url: '/ticket', + icon: 'uil:message', + title: 'ticket', }, ]; diff --git a/packages/ui/src/custom-components/pro-list/pro-list.tsx b/packages/ui/src/custom-components/pro-list/pro-list.tsx index 91bab33..cff03e2 100644 --- a/packages/ui/src/custom-components/pro-list/pro-list.tsx +++ b/packages/ui/src/custom-components/pro-list/pro-list.tsx @@ -132,7 +132,7 @@ export function ProList>({ const selectedCount = selectedRows.length; return ( -
+
{params ? (