From f9de5fcb4f027188e745c62d6efb0b534f4cd635 Mon Sep 17 00:00:00 2001 From: speakeloudest Date: Thu, 14 Aug 2025 09:00:22 -0700 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BF=AE=E6=94=B9=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + .../(main)/(content)/(user)/order/page.tsx | 2 +- .../(main)/(content)/(user)/sidebar-left.tsx | 4 +- .../(content)/(user)/subscribe/page.tsx | 103 +---- apps/user/components/Modal.tsx | 28 +- .../SubscribePlan/PlanContent/index.tsx | 351 ++++++++++++++++++ .../SubscribePlan/PlanTabs/PlanTabs.tsx | 60 +++ apps/user/components/display.tsx | 4 +- .../main/OfferDialog/TabContent.tsx | 56 --- .../components/main/OfferDialog/index.tsx | 335 +---------------- .../user/components/main/OfferDialog/types.ts | 65 ---- apps/user/components/subscribe/purchase.tsx | 61 +-- apps/user/components/user-nav.tsx | 2 +- apps/user/locales/en-US/components.json | 2 +- apps/user/locales/zh-CN/components.json | 2 +- apps/user/public/favicon.ico | Bin 9662 -> 9662 bytes apps/user/public/favicon.svg | 15 +- apps/user/services/common/typings.d.ts | 1 + packages/airo-ui/src/components/dialog.tsx | 7 +- .../custom-components/pro-list/pagination.tsx | 4 +- scripts/publish-dev.sh | 69 ++++ 21 files changed, 559 insertions(+), 613 deletions(-) create mode 100644 apps/user/components/SubscribePlan/PlanContent/index.tsx create mode 100644 apps/user/components/SubscribePlan/PlanTabs/PlanTabs.tsx delete mode 100644 apps/user/components/main/OfferDialog/TabContent.tsx delete mode 100644 apps/user/components/main/OfferDialog/types.ts create mode 100755 scripts/publish-dev.sh diff --git a/.gitignore b/.gitignore index de420fd..2937150 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ coverage # Build Outputs .next/ out/ +out-dev/ build dist diff --git a/apps/user/app/(main)/(content)/(user)/order/page.tsx b/apps/user/app/(main)/(content)/(user)/order/page.tsx index 096d175..10c16e2 100644 --- a/apps/user/app/(main)/(content)/(user)/order/page.tsx +++ b/apps/user/app/(main)/(content)/(user)/order/page.tsx @@ -31,7 +31,7 @@ export default function Page() { }} renderItem={(item) => { return ( - +
{t('orderNo')}
diff --git a/apps/user/app/(main)/(content)/(user)/sidebar-left.tsx b/apps/user/app/(main)/(content)/(user)/sidebar-left.tsx index d92a47b..0c18a44 100644 --- a/apps/user/app/(main)/(content)/(user)/sidebar-left.tsx +++ b/apps/user/app/(main)/(content)/(user)/sidebar-left.tsx @@ -83,8 +83,8 @@ export function SidebarLeft({ ...props }: React.ComponentProps) - -
+ +
diff --git a/apps/user/app/(main)/(content)/(user)/subscribe/page.tsx b/apps/user/app/(main)/(content)/(user)/subscribe/page.tsx index db1051e..0e5e14a 100644 --- a/apps/user/app/(main)/(content)/(user)/subscribe/page.tsx +++ b/apps/user/app/(main)/(content)/(user)/subscribe/page.tsx @@ -2,21 +2,23 @@ import { querySubscribeList } from '@/services/user/subscribe'; import { useQuery } from '@tanstack/react-query'; -import { Tabs, TabsList, TabsTrigger } from '@workspace/airo-ui/components/tabs'; import { useTranslations } from 'next-intl'; -import { useMemo, useRef, useState } from 'react'; +import { 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'; -import { unitConversion } from '@workspace/airo-ui/utils'; +import TabContent from '@/components/SubscribePlan/PlanContent/index'; +import PlanTabs from '@/components/SubscribePlan/PlanTabs/PlanTabs'; export default function Page() { const t = useTranslations('subscribe'); const [tabValue, setTabValue] = useState<'year' | 'month'>('year'); - const { data, isLoading, error, refetch } = useQuery({ + const { + data = [], + isLoading, + error, + refetch, + } = useQuery({ queryKey: ['querySubscribeList'], queryFn: async () => { const { data } = await querySubscribeList(); @@ -24,50 +26,6 @@ export default function Page() { }, }); - // 处理套餐数据的工具函数 - 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).toString(), // 原价 - discount_price: unitConversion( - 'centsToDollars', - item.unit_price * ((discountItem?.discount || 100) / 100), - ).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); - // 这里可以添加订阅逻辑,比如跳转到支付页面或显示确认对话框 - PurchaseRef.current.show(plan, tabValue); - }; - - const PurchaseRef = useRef<{ - show: (subscribe: API.Subscribe, tabValue: string) => void; - hide: () => void; - }>(null); - return ( <> @@ -86,52 +44,17 @@ export default function Page() { {t('description')}
- setTabValue(value as 'year' | 'month')} - > - - {tabValue === 'year' ? ( - - -20% - {/* 小三角箭头 */} - - - ) : null} - - {t('yearlyPlan')} - - - {t('monthlyPlan')} - - - +
+ +
- ); diff --git a/apps/user/components/Modal.tsx b/apps/user/components/Modal.tsx index d28369a..2f37996 100644 --- a/apps/user/components/Modal.tsx +++ b/apps/user/components/Modal.tsx @@ -11,10 +11,24 @@ import { AlertDialogTitle, } from '@workspace/airo-ui/components/alert-dialog'; import CloseSvg from '@workspace/airo-ui/components/close.svg'; -import { useImperativeHandle, useState } from 'react'; +import { Ref, useImperativeHandle, useState } from 'react'; + +interface AlertDialogComponentProps { + ref: Ref; + title: string; + description: string; + cancelText?: string; + confirmText?: string; + onConfirm?: () => void; +} + +// 定义ref可以访问的方法类型 +export interface AlertDialogRef { + show: () => void; +} // Defining the AlertDialogComponent with internal state and onShow prop -const AlertDialogComponent = ({ +const AlertDialogComponent: React.FC = ({ ref, title, description, @@ -28,9 +42,13 @@ const AlertDialogComponent = ({ setOpen(true); } - useImperativeHandle(ref, () => ({ - show, - })); + useImperativeHandle( + ref, + (): AlertDialogRef => ({ + show, + }), + [], + ); return ( diff --git a/apps/user/components/SubscribePlan/PlanContent/index.tsx b/apps/user/components/SubscribePlan/PlanContent/index.tsx new file mode 100644 index 0000000..2030f59 --- /dev/null +++ b/apps/user/components/SubscribePlan/PlanContent/index.tsx @@ -0,0 +1,351 @@ +// 加载状态组件 +import { unitConversion } from '@workspace/airo-ui/utils'; +import { useMemo, useRef } from 'react'; + +const LoadingState = () => { + const t = useTranslations('components.offerDialog'); + return ( +
+
+

{t('loading')}

+
+ ); +}; + +// 错误状态组件 +const ErrorState = ({ onRetry }: { onRetry: () => void }) => { + const t = useTranslations('components.offerDialog'); + return ( +
+

{t('loadFailed')}

+ +
+ ); +}; + +// 空状态组件 +const EmptyState = ({ message }: { message: string }) => ( +
+

{message}

+
+); + +import { useLocale } from 'next-intl'; + +interface PriceDisplayProps { + plan: PlanProps; +} +interface PlanProps extends API.Subscribe { + origin_price: string; + discount_price: string; +} +// 价格显示组件 +const PriceDisplay: React.FC = ({ plan }) => { + const t = useTranslations('components.offerDialog'); + const locale = useLocale(); // 获取当前语言环境 + const { common } = useGlobalStore(); + const discountItem = plan.discount.find((v) => v.quantity === 12) ?? { discount: 0 }; + const discount = + locale === 'zh-CN' ? discountItem?.discount / 10 : `${100 - discountItem?.discount}%`; + return ( +
+
+ {plan.origin_price && ( + + {common?.currency?.currency_symbol} + {plan.origin_price} + + )} + + {common?.currency?.currency_symbol} + {plan.discount_price} + + + {t('perYear')} + +
+
+ {plan.origin_price && ( +

+ {t('yearlyDiscount', { discount })} +

+ )} +
+
+ ); +}; + +import { useLoginDialog } from '@/app/auth/LoginDialogContext'; +import { Display } from '@/components/display'; +import Modal, { AlertDialogRef } from '@/components/Modal'; +import Purchase from '@/components/subscribe/purchase'; +import useGlobalStore from '@/config/use-global'; +import { queryUserSubscribe } from '@/services/user/user'; +import { useTranslations } from 'next-intl'; +import React from 'react'; + +// 星级评分组件 +const StarRating = ({ rating, maxRating = 5 }: { rating: number; maxRating?: number }) => ( +
+ {Array.from({ length: Math.min(rating, maxRating) }, (_, i) => ( + + ✭ + + ))} +
+); + +// 功能列表组件 +const FeatureList = ({ plan }: { plan: API.Subscribe }) => { + const t = useTranslations('subscribe.detail'); + const tOffer = useTranslations('components.offerDialog'); + const features = [{ label: tOffer('availableNodes'), value: plan?.server_count }]; + + return ( +
+
    +
  • +
    + {t('availableTraffic')} + + + +
    +
  • +
  • +
    + {t('connectionSpeed')} + + + +
    +
  • +
  • +
    + {t('connectedDevices')} + + + +
    +
  • + {features.map((feature) => ( +
  • +
    + {feature.label}: + {feature.value} +
    +
  • + ))} +
  • +
    + + {tOffer('networkStabilityIndex')} + + +
    +
  • +
+
+ ); +}; + +// 套餐卡片组件 +const PlanCard: React.FC<{ + plan: PlanProps; + tabValue: string; + onSubscribe?: (plan: API.Subscribe) => void; + isFirstCard?: boolean; +}> = ({ plan, onSubscribe }) => { + const { user } = useGlobalStore(); + const { openLoginDialog } = useLoginDialog(); + const t = useTranslations('components.offerDialog'); + const ModalRef = useRef(null); + async function handleSubscribe() { + if (!user) { + // 强制登陆 + openLoginDialog(false); + return; + } + + // 有生效套餐进行弹窗提示 + const { data } = await queryUserSubscribe(); + if (data?.data?.list?.[0]?.status === 1) { + ModalRef.current?.show(); + return; + } + + onSubscribe?.(plan); + } + + return ( +
+ {/* 套餐名称 */} +

{plan.name}

+ + {/* 价格区域 */} + + + {/* 订阅按钮 */} + + + {/* 功能列表 */} + + + onSubscribe?.(plan)} + /> +
+ ); +}; + +PlanCard.displayName = 'PlanCard'; + +// 套餐列表组件 +export const PlanList = ({ + plans, + tabValue, + isLoading, + error, + onRetry, + emptyMessage, + onSubscribe, +}: { + plans: PlanProps[]; + tabValue: string; + isLoading: boolean; + error: any; + onRetry: () => void; + emptyMessage: string; + onSubscribe?: (plan: API.Subscribe) => void; +}) => { + if (isLoading) return ; + if (error) return ; + if (plans.length === 0) return ; + + return ( +
+
+ {plans.map((plan, index) => ( + + ))} +
+
+ ); +}; + +interface TabContentProps { + tabValue: string; + subscribeData: API.Subscribe[]; + isLoading: boolean; + error: Error | null; + onRetry: () => void; + firstPlanCardRef?: React.RefObject; +} + +const TabContent: React.FC = ({ + tabValue, + subscribeData, + isLoading, + error, + onRetry, +}) => { + const t = useTranslations('components.offerDialog'); + + // 处理套餐数据的工具函数 + const processPlanData = (item: API.Subscribe, isYearly: boolean): PlanProps => { + if (isYearly) { + const discountItem = item.discount?.find((v) => v.quantity === 12); + return { + ...item, + origin_price: unitConversion('centsToDollars', item.unit_price).toString(), // 原价 + discount_price: unitConversion( + 'centsToDollars', + item.unit_price * ((discountItem?.discount || 100) / 100), + ).toString(), // 优惠价格 + }; + } else { + return { + ...item, + origin_price: '', // 月付没有原价 + discount_price: unitConversion('centsToDollars', item.unit_price).toString(), // 月付价格 + }; + } + }; + + // 使用 useMemo 优化数据处理性能 + const yearlyPlans: PlanProps[] = useMemo( + () => (subscribeData || []).map((item) => processPlanData(item, true)), + [subscribeData], + ); + + const monthlyPlans: PlanProps[] = useMemo( + () => (subscribeData || []).map((item) => processPlanData(item, false)), + [subscribeData], + ); + + const PurchaseRef = useRef<{ + show: (subscribe: API.Subscribe, tabValue: string) => void; + hide: () => void; + }>(null); + // 处理订阅点击 + const handleSubscribe = (plan: API.Subscribe) => { + // 这里可以添加订阅逻辑,比如跳转到支付页面或显示确认对话框 + PurchaseRef.current?.show(plan, tabValue); + }; + + return ( +
+ {tabValue === 'year' && ( + + )} + {tabValue === 'month' && ( + + )} + +
+ ); +}; + +export default TabContent; diff --git a/apps/user/components/SubscribePlan/PlanTabs/PlanTabs.tsx b/apps/user/components/SubscribePlan/PlanTabs/PlanTabs.tsx new file mode 100644 index 0000000..9d7b149 --- /dev/null +++ b/apps/user/components/SubscribePlan/PlanTabs/PlanTabs.tsx @@ -0,0 +1,60 @@ +import { Tabs, TabsList, TabsTrigger } from '@workspace/airo-ui/components/tabs'; +import { cn } from '@workspace/airo-ui/lib/utils'; +import { useTranslations } from 'next-intl'; +import React from 'react'; + +export type TabValueType = 'year' | 'month'; +interface PlanTabProps { + tabValue: TabValueType; + setTabValue: (val: TabValueType) => void; + discount: number; + className?: string; +} + +const PlanTabs: React.FC = (props) => { + const t = useTranslations('components.offerDialog'); + const { tabValue, className } = props; + const TAB_LIST: { label: string; value: TabValueType }[] = [ + { + label: t('yearlyPlan'), + value: 'year', + }, + { + label: t('monthlyPlan'), + value: 'month', + }, + ]; + + return ( + props.setTabValue(value as 'year' | 'month')} + > + + {tabValue === 'year' ? ( + + -{props.discount}%{/* 小三角箭头 */} + + + ) : null} + {TAB_LIST.map((val, key) => ( + + {val.label} + + ))} + + + ); +}; +export default PlanTabs; diff --git a/apps/user/components/display.tsx b/apps/user/components/display.tsx index 2e419be..fc8a01b 100644 --- a/apps/user/components/display.tsx +++ b/apps/user/components/display.tsx @@ -19,10 +19,10 @@ export function Display({ }: DisplayProps): string { const t = useTranslations('common'); const { common } = useGlobalStore(); - // const { currency } = common; + const { currency } = common; if (type === 'currency') { - const formattedValue = `$ ${unitConversion('centsToDollars', value as number)?.toFixed(2) ?? '0.00'}`; + const formattedValue = `${currency?.currency_symbol ?? ''}${unitConversion('centsToDollars', value as number)?.toFixed(2) ?? '0.00'}`; return formattedValue; } diff --git a/apps/user/components/main/OfferDialog/TabContent.tsx b/apps/user/components/main/OfferDialog/TabContent.tsx deleted file mode 100644 index 152c776..0000000 --- a/apps/user/components/main/OfferDialog/TabContent.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { useTranslations } from 'next-intl'; -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, -}) => { - const t = useTranslations('components.offerDialog'); - return ( -
- {tabValue === 'year' && ( - - )} - {tabValue === 'month' && ( - - )} -
- ); -}; diff --git a/apps/user/components/main/OfferDialog/index.tsx b/apps/user/components/main/OfferDialog/index.tsx index 734abe0..23f94fc 100644 --- a/apps/user/components/main/OfferDialog/index.tsx +++ b/apps/user/components/main/OfferDialog/index.tsx @@ -1,258 +1,12 @@ import { getSubscription } from '@/services/user/portal'; import { useQuery } from '@tanstack/react-query'; import { Dialog, DialogContent, DialogTitle } from '@workspace/airo-ui/components/dialog'; -import { Tabs, TabsList, TabsTrigger } from '@workspace/airo-ui/components/tabs'; -import { unitConversion } from '@workspace/airo-ui/utils'; -import { forwardRef, useImperativeHandle, useMemo, useRef, useState } from 'react'; +import { forwardRef, useImperativeHandle, useRef, useState } from 'react'; -import { TabContent } from './TabContent'; -import { ProcessedPlanData } from './types'; - -// 加载状态组件 -const LoadingState = () => { - const t = useTranslations('components.offerDialog'); - return ( -
-
-

{t('loading')}

-
- ); -}; - -// 错误状态组件 -const ErrorState = ({ onRetry }: { onRetry: () => void }) => { - const t = useTranslations('components.offerDialog'); - return ( -
-

{t('loadFailed')}

- -
- ); -}; - -// 空状态组件 -const EmptyState = ({ message }: { message: string }) => ( -
-

{message}

-
-); - -// 价格显示组件 -const PriceDisplay = ({ plan }: { plan: ProcessedPlanData }) => { - const t = useTranslations('components.offerDialog'); - return ( -
-
- {plan.origin_price && ( - - ${plan.origin_price} - - )} - - ${plan.discount_price} - - - {t('perYear')} - -
-
- {plan.origin_price && ( -

{t('yearlyDiscount')}

- )} -
-
- ); -}; - -import { useLoginDialog } from '@/app/auth/LoginDialogContext'; -import { Display } from '@/components/display'; -import Modal from '@/components/Modal'; -import Purchase from '@/components/subscribe/purchase'; -import useGlobalStore from '@/config/use-global'; -import { queryUserSubscribe } from '@/services/user/user'; +import TabContent from '@/components/SubscribePlan/PlanContent/index'; +import PlanTabs from '@/components/SubscribePlan/PlanTabs/PlanTabs'; import { useTranslations } from 'next-intl'; -// 星级评分组件 -const StarRating = ({ rating, maxRating = 5 }: { rating: number; maxRating?: number }) => ( -
- {Array.from({ length: Math.min(rating, maxRating) }, (_, i) => ( - - ✭ - - ))} -
-); - -// 功能列表组件 -const FeatureList = ({ plan }: { plan: ProcessedPlanData }) => { - const t = useTranslations('subscribe.detail'); - const tOffer = useTranslations('components.offerDialog'); - const features = [{ label: tOffer('availableNodes'), value: plan?.server_count }]; - - return ( -
-
    -
  • -
    - {t('availableTraffic')} - - - -
    -
  • -
  • -
    - {t('connectionSpeed')} - - - -
    -
  • -
  • -
    - {t('connectedDevices')} - - - -
    -
  • - {features.map((feature) => ( -
  • -
    - {feature.label}: - {feature.value} -
    -
  • - ))} -
  • -
    - - {tOffer('networkStabilityIndex')} - - -
    -
  • -
-
- ); -}; - -// 套餐卡片组件 -const PlanCard = forwardRef< - HTMLDivElement, - { - plan: ProcessedPlanData; - tabValue: string; - onSubscribe?: (plan: ProcessedPlanData) => void; - isFirstCard?: boolean; - } ->(({ plan, onSubscribe }, ref) => { - const { user } = useGlobalStore(); - const { openLoginDialog } = useLoginDialog(); - const t = useTranslations('components.offerDialog'); - const ModalRef = useRef(null); - async function handleSubscribe() { - if (!user) { - // 强制登陆 - openLoginDialog(false); - return; - } - - // 有生效套餐进行弹窗提示 - const { data } = await queryUserSubscribe(); - if (data?.data?.list?.[0]?.status === 1) { - ModalRef.current.show(); - return; - } - - onSubscribe?.(plan); - } - - return ( -
- {/* 套餐名称 */} -

{plan.name}

- - {/* 价格区域 */} - - - {/* 订阅按钮 */} - - - {/* 功能列表 */} - - - onSubscribe?.(plan)} - /> -
- ); -}); - -PlanCard.displayName = 'PlanCard'; - -// 套餐列表组件 -export const PlanList = ({ - plans, - tabValue, - isLoading, - error, - onRetry, - emptyMessage, - onSubscribe, -}: { - plans: ProcessedPlanData[]; - tabValue: string; - isLoading: boolean; - error: any; - onRetry: () => void; - emptyMessage: string; - onSubscribe?: (plan: ProcessedPlanData) => void; -}) => { - if (isLoading) return ; - if (error) return ; - if (plans.length === 0) return ; - - return ( -
-
- {plans.map((plan, index) => ( - - ))} -
-
- ); -}; - export interface OfferDialogRef { show: () => void; hide: () => void; @@ -261,7 +15,7 @@ export interface OfferDialogRef { const OfferDialog = forwardRef((props, ref) => { const t = useTranslations('components.offerDialog'); const [open, setOpen] = useState(false); - const [tabValue, setTabValue] = useState('year'); + const [tabValue, setTabValue] = useState<'year' | 'month'>('year'); const dialogRef = useRef(null); // 使用 useQuery 来管理请求 const { @@ -295,44 +49,6 @@ const OfferDialog = forwardRef((props, ref) => { }, hide: () => setOpen(false), })); - const PurchaseRef = useRef<{ show: (subscribe: API.Subscribe) => void; hide: () => void }>(null); - // 处理订阅点击 - const handleSubscribe = (plan: ProcessedPlanData) => { - // 这里可以添加订阅逻辑,比如跳转到支付页面或显示确认对话框 - PurchaseRef.current.show(plan, tabValue); - }; - - // 处理套餐数据的工具函数 - 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).toString(), // 原价 - discount_price: unitConversion( - 'centsToDollars', - item.unit_price * ((discountItem?.discount || 100) / 100), - ).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], - ); return ( @@ -350,52 +66,17 @@ const OfferDialog = forwardRef((props, ref) => {
- - - {tabValue === 'year' ? ( - - -20% - {/* 小三角箭头 */} - - - ) : null} - - {t('yearlyPlan')} - - - {t('monthlyPlan')} - - - +
+ +
- ); diff --git a/apps/user/components/main/OfferDialog/types.ts b/apps/user/components/main/OfferDialog/types.ts deleted file mode 100644 index 99b9a82..0000000 --- a/apps/user/components/main/OfferDialog/types.ts +++ /dev/null @@ -1,65 +0,0 @@ -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 - }; -} - -// 以 API.Subscribe 为准的类型定义 -export interface ProcessedPlanData { - id: number; - name: string; - description: string; - unit_price: number; - unit_time: string; - discount: Array<{ - quantity: number; - discount: number; - }>; - replacement: number; - inventory: number; - traffic: number; - speed_limit: number; - device_limit: number; - quota: number; - group_id: number; - server_group: number[]; - server: number[]; - show: boolean; - sell: boolean; - sort: number; - deduction_ratio: number; - allow_deduction: boolean; - reset_cycle: number; - renewal_reset: boolean; - created_at: number; - updated_at: number; - // 处理后的价格字段 - origin_price: string; - discount_price: string; - // 添加features属性以兼容现有代码 - features?: { - traffic: string; - duration: string; - onlineIPs: string; - connections: string; - bandwidth: string; - nodes: string; - stability: number; - }; -} diff --git a/apps/user/components/subscribe/purchase.tsx b/apps/user/components/subscribe/purchase.tsx index 79315d9..8d75985 100644 --- a/apps/user/components/subscribe/purchase.tsx +++ b/apps/user/components/subscribe/purchase.tsx @@ -1,6 +1,7 @@ 'use client'; import PaymentMethods from '@/components/subscribe/payment-methods'; +import PlanTabs, { TabValueType } from '@/components/SubscribePlan/PlanTabs/PlanTabs'; import useGlobalStore from '@/config/use-global'; import { preCreateOrder, purchase } from '@/services/user/order'; import { useQuery } from '@tanstack/react-query'; @@ -12,7 +13,6 @@ import { DialogTitle, } from '@workspace/airo-ui/components/dialog'; import { Separator } from '@workspace/airo-ui/components/separator'; -import { Tabs, TabsList, TabsTrigger } from '@workspace/airo-ui/components/tabs'; import { LoaderCircle } from 'lucide-react'; import { useTranslations } from 'next-intl'; import { useRouter } from 'next/navigation'; @@ -32,7 +32,7 @@ interface PurchaseProps { } interface PurchaseDialogRef { - show: (subscribe: API.Subscribe, tabValue: string) => void; + show: (subscribe: API.Subscribe, tabValue: TabValueType) => void; hide: () => void; } @@ -50,10 +50,10 @@ const Purchase = forwardRef((props, ref) => { const [loading, startTransition] = useTransition(); const [open, setOpen] = useState(false); const lastSuccessOrderRef = useRef(null); - const [tabValue, setTabValue] = useState('year'); + const [tabValue, setTabValue] = useState<'year' | 'month'>('year'); useImperativeHandle(ref, () => ({ - show: (newSubscribe: API.Subscribe, tabValue: string) => { + show: (newSubscribe: API.Subscribe, tabValue: TabValueType) => { setSubscribe(newSubscribe); setParams((prev) => ({ ...prev, @@ -127,46 +127,19 @@ const Purchase = forwardRef((props, ref) => {
{t('purchaseTitle')}
-
- { - if (val === 'year') { - handleChange('quantity', 12); - } else if (val === 'month') { - handleChange('quantity', 1); - } - setTabValue(val); - }} - > - - {tabValue === 'year' ? ( - - {t('discount20')} - {/* 小三角箭头 */} - - - ) : null} - - {t('yearlyPlan')} - - - {t('monthlyPlan')} - - - -
+ { + if (val === 'year') { + handleChange('quantity', 12); + } else if (val === 'month') { + handleChange('quantity', 1); + } + setTabValue(val); + }} + discount={20} + className={'sm:mt-6'} + />
{from === 'profile' ? ( -
+
4SyTVhKa_EG{&3<1ev9vaC=QKW^1j77x4f8O*hpUj~G_e%8r6LuoiYe z985Zi*gn3}$)UbZr#EKWrc^6zBwPoN!AEcqy5I!d2*j!D6ZM&9$~2*J&S_3H+I);A zY&JX&pTa*N&0#jAld=AU|8M%!aaYbc-I+vvKQx>LZLk`4!(j;P4L|QC&Ui3$n`=&4 z9#ZidlS$_E>U)_)-D%iy&<;<+7jOi^8VfxiBmNRFnUmax+T$^kO3H7pZO~4u_*Mff zgmv&G9ECy}dwRE?HdF<@F8_3*-i`&qaf5PE`&1pPoYuoP@GtbFtJwGQr!p~fn}(E} zhs+epH(qD!Pd+^A24E+F#)s!Y?NoJLtfo@psVo)%-!6U}Qo9JYY07yZJ{sX>coFu& zaVSMgxpCLhR@I++EdTa|ok#p~w&mSGx?5l)><5j@<>>gEbl-x;_zysR<4=&UuVEX! z=i6P_V0|j?IAC0e&O_VOZez$9#J?T(K)ITw?MGM%SHsy*2Qe4}m%)S3nn}h^hZb@e`eC)P?-8#Zd~!>=F@+KcddG#g7D?1 zQ@c?gDx|Hacgp)B>rZ2^uNylIZh|$i7Sy+F{j`j$ru@A!B}{4Z>DPBxEtr|GJ|pyN z9e4=JZo%v7NWjWOt{9|b_i;@g6F99y?m-oEe@r*l;`mH>AwF7??x#8 zoXNwTeA}l9TL~^+IaXSh$hXD_S(paDL6kp@E3?2XZXV&%X#R@ohboI}6<_Nccj*~! zg{bco8?HWN)~B*D)&BM2XS9$tabinA5uCu7YF*=MT8gXZ$TeX_s8+^ zD0uBCSGKNyY&{2k>t82&!->^vFj!|HFHMfwQ(3*R5b#}i=ZORUwL zN@}lf=Ndl`He7Yp0h$ZKexjbWceosKbd^^6D!Z;YnS&B)it$OD= z%3B)V0L>jPO|kZV(r}L8ubpydAeuka4~zBFQ#|cqzk*Hh6s&=b@EH`_Uh;k8UDDTr zZ9*y5(AV}Dlx zOB=HDzQzQNQI(=IO2<|-cH2;DFGQHo*+5jfN(tYF##{%WB7E(#nxi#WSBlnrIy9yY za_z@i!lYmOVXas4rK^-MwGk&fYwcRr>&vWFD^~wOwAr;{R=9toJo#LMEwq2?%X)YT zZCW#j<(t+Hi5m^R;Hyx!pYW zKKU-?%gdkLpB}1^JZSGTYu+^b+ojc)PrJg}6T_YUQKZ?`hx*k|wT{*PU@X`+6V+~l z`BB`3qikMRa-Xjqo~p1v_@2(^9)K3ep6R8=Grux67ITr#)q&15R1T*~ul)WF?|{xQ zW&nH5>{@{Kz}|=7S4z18ziH@P{W&<<`hUge7PuQO0lS{Z+Hy;AE~vB7MH7dXBD-5$ z=L?&F-O$@S{EUw`LGAfM+OhVmuAcRK2KTwMscvG*cu2)LE6V*Yrge2WdC-`*8Sa4d z!OlBaPq)8rFGF5YN$huWXDq4*ot3JOMb(AQAap+UGTa6_*HIhHmUH^tG4|g_v|mtP zl!o3$vhzV>c23aQ$ZSwPz60%JwFg(*cpKC^a7CgsdK0{2{N?x9&M>{Z(*_wbiDjN~yTQ zwx1WwKWOrzEf4ftOF3A(+!z(#pf1nO N#PUC73>_vW`yX8{gRKAn literal 9662 zcmc(keT-d28HcAuz#_YMyD795VK=n2mAkuLgTz#WO(a1nQg-jXTVxeok;Eu!QLTi= za3$1-BIjX00ej&PJO}&Wldv6T;90mD9?H+}#a;(5z+sq#X0ST`JGS&4dc|c9b5!I2Q{d(h}!;@m(5#$Z5SJEj zwwtR1)@uy)F2Zhu(=Y^1p4YT3J*> z+gv}|*w2-)CvX2N?0JyJE!me-mhSzr>K~--<7*)RX*TJ7$?Ryij5Nn#GxeVV z_52e)2V-y*I4{%q{fFZT`lP0$_eYItujBU~_#f!wN%$CStXF%~w*_qLHGSz*-^MVO zF}v^ZtF+XlJt0f5T=>0-2dJ_j?;ee0DJK1oI7UQ;dw|t zSR=g^aL#N6eWYux{*8ST^GhwP>-jx{t($Wy_U+)f7ULY^+-J}ApiX_O^#iaCu19~G z(=X%vgYbJe43Fl0HHP;{ANE_HsUOBNrm@u!^>myxZ7!rXxm}LE9Nr5zz?-44XS*Jv z{u?R372d6W$i|@>=eFx(9IgiXoL$S;gubo-eH&vF7&B{+J)WQYSZ&z7c-GI0j)&DH zQU6k|c~2a}2hsl0sNY;_HlK>VoCg_wVQhAl84%L3WO$`R_NxFC5+j##LN`5mwx(jpQ#hNm`{c2>edhS?nLL{-UxYny2+|&3w7XIcbh7j}oEtHpo1Gi= zYM*O(5UzpSVJ|!fMXl|QpP&wFKE6A3tqb*`{v-DCEO)4^BKEa#2ke7lj?qqUYnl1Q z+kodncjl133z!?YSH2VOg8fj;aa#3xZtjrEJ$@l4m4Ah?@cxxP{`!s6AMGxf2J?t1a9!B;F&F^WdwG3G`*v-8oadAHe+t@ZZ7nmG@ExX^ ze{t+MoLZ}9?<;)MG57lk*w#5`pW;}RbrX_5>;`xeT6MNNf1G-+fv6?Rk2d^-3`V1_i^42_MvtB-D~S!^Z8fCPhapV_&XGJbUJ>R zdgju$y9ClZY^wh(WpBu@x9YdfZS2!Nugj3H2C2edA!#_8E$V+gZPGh#YNP&3LBHf~ zm0v)*ru`kP1nxoq3HL!i;CMa+#WV-@9FVoirEAUBB-0>D~=;{3-YW+yZg^V!eI?w0nl^*ff~EBb1^)i}glg`8}Yh zwbSuF>Rk-654BhNE>JnGwl3OO#<^?C{idA{pB>AAx;-bv+(z5hj{HYSobpa z5%=DtS?js$*LBjaon@{O-{DKyhkOqokKa@)pE*j*c3S>HeeHQj zE`T!RM<4p)o!8vt3($=*FZ$e>zB`xIf$Te3yblfZn=2OMm3eHgvKrz&FH8T#?6+L= z>AhXh@7{bj3`6{$RpXtt%%6VA+ZSVRhcd?8b!yIe8<@BCKxmBe|5aW1p5^xq{lit@ zbN+bxV7uSI4p8&T+J(zv&d+i5KTh7*jo;*Yz7NJ2y=P$Wfcfe-_cJHj4nD`1U%sLF zT#RGwc$+hH`$z0_4}4esI&@mgK0{rLH^XIcR(<5m)V68Otxo1|^SUL?*l6uv7p5 diff --git a/apps/user/public/favicon.svg b/apps/user/public/favicon.svg index 7cfdf95..f00266b 100644 --- a/apps/user/public/favicon.svg +++ b/apps/user/public/favicon.svg @@ -1,15 +1,4 @@ - - - - - - - - - - - - - + + diff --git a/apps/user/services/common/typings.d.ts b/apps/user/services/common/typings.d.ts index 963ab1e..89478ac 100644 --- a/apps/user/services/common/typings.d.ts +++ b/apps/user/services/common/typings.d.ts @@ -756,6 +756,7 @@ declare namespace API { quota: number; group_id: number; server_group: number[]; + server_count: number; server: number[]; show: boolean; sell: boolean; diff --git a/packages/airo-ui/src/components/dialog.tsx b/packages/airo-ui/src/components/dialog.tsx index 2c748d0..4aef616 100644 --- a/packages/airo-ui/src/components/dialog.tsx +++ b/packages/airo-ui/src/components/dialog.tsx @@ -41,15 +41,16 @@ const DialogContent = React.forwardRef< - {children} +
{children}
+
diff --git a/packages/airo-ui/src/custom-components/pro-list/pagination.tsx b/packages/airo-ui/src/custom-components/pro-list/pagination.tsx index 4d12a4e..30f523e 100644 --- a/packages/airo-ui/src/custom-components/pro-list/pagination.tsx +++ b/packages/airo-ui/src/custom-components/pro-list/pagination.tsx @@ -26,12 +26,12 @@ interface PaginationProps { export function Pagination({ table, text }: PaginationProps) { return ( -
+
{text?.textPageOf?.(table.getState().pagination.pageIndex + 1, table.getPageCount()) || `Page ${table.getState().pagination.pageIndex + 1} of ${table.getPageCount()}`}
-
+
{/*

{text?.textRowsPerPage || 'Rows per page'}