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 (
-