From a918cdab686b61664117b59eba97a0f1a97aea45 Mon Sep 17 00:00:00 2001 From: speakeloudest Date: Wed, 13 Aug 2025 01:51:01 -0700 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BF=AE=E6=94=B9=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../(main)/(content)/(user)/order/page.tsx | 28 +- .../(main)/(content)/(user)/payment/page.tsx | 264 +++++++++--------- .../(main)/(content)/(user)/sidebar-left.tsx | 6 +- apps/user/components/language-switch.tsx | 29 +- .../components/main/OfferDialog/index.tsx | 138 +++------ apps/user/components/subscribe/recharge.tsx | 4 +- apps/user/components/subscribe/renewal.tsx | 89 +++++- apps/user/components/user-nav.tsx | 30 +- apps/user/config/navs.ts | 7 + packages/airo-ui/src/components/sheet.tsx | 8 +- packages/airo-ui/src/components/sidebar.tsx | 1 + 11 files changed, 299 insertions(+), 305 deletions(-) diff --git a/apps/user/app/(main)/(content)/(user)/order/page.tsx b/apps/user/app/(main)/(content)/(user)/order/page.tsx index 844e948..096d175 100644 --- a/apps/user/app/(main)/(content)/(user)/order/page.tsx +++ b/apps/user/app/(main)/(content)/(user)/order/page.tsx @@ -4,13 +4,12 @@ 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 { purchaseCheckout } from '@/services/user/portal'; import { AiroButton } from '@workspace/airo-ui/components/AiroButton'; import { Card, CardContent } from '@workspace/airo-ui/components/card'; import { formatDate } from '@workspace/airo-ui/utils'; import { useTranslations } from 'next-intl'; +import Link from 'next/link'; import { useRef } from 'react'; -import { toast } from 'sonner'; import OrderDetailDialog from './components/OrderDetailDialog'; export default function Page() { @@ -19,19 +18,6 @@ export default function Page() { const ref = useRef(null); const OrderDetailDialogRef = useRef(null); - const handlePayment = async (orderNo) => { - const data = await purchaseCheckout({ - orderNo: orderNo, - returnUrl: window.location.href, - }); - if (data.data?.type === 'url' && data.data.checkout_url) { - window.open(data.data.checkout_url, '_blank'); - } else { - toast.success(t('paymentSuccess')); - ref?.current.reset(); - } - }; - return (
> @@ -63,14 +49,10 @@ export default function Page() { > {t('cancelOrder')} - { - handlePayment(item.order_no); - }} - > - {t('payment')} + + + {t('payment')} + ) : ( diff --git a/apps/user/app/(main)/(content)/(user)/payment/page.tsx b/apps/user/app/(main)/(content)/(user)/payment/page.tsx index 46868b1..b7aab8d 100644 --- a/apps/user/app/(main)/(content)/(user)/payment/page.tsx +++ b/apps/user/app/(main)/(content)/(user)/payment/page.tsx @@ -3,26 +3,18 @@ import { Display } from '@/components/display'; import StripePayment from '@/components/payment/stripe'; import { SubscribeBilling } from '@/components/subscribe/billing'; -import { SubscribeDetail } from '@/components/subscribe/detail'; import useGlobalStore from '@/config/use-global'; import { queryOrderDetail } from '@/services/user/order'; import { purchaseCheckout } from '@/services/user/portal'; import { useQuery } from '@tanstack/react-query'; -import { Badge } from '@workspace/airo-ui/components/badge'; -import { Button } from '@workspace/airo-ui/components/button'; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from '@workspace/airo-ui/components/card'; +import { AiroButton } from '@workspace/airo-ui/components/AiroButton'; +import { Card, CardContent } from '@workspace/airo-ui/components/card'; import { Separator } from '@workspace/airo-ui/components/separator'; -import { Icon } from '@workspace/airo-ui/custom-components/icon'; import { formatDate } from '@workspace/airo-ui/utils'; import { useCountDown } from 'ahooks'; import { addMinutes, format } from 'date-fns'; import { useTranslations } from 'next-intl'; +import Image from 'next/image'; import Link from 'next/link'; import { QRCodeCanvas } from 'qrcode.react'; import { useEffect, useState } from 'react'; @@ -88,37 +80,130 @@ export default function Page() { ); return ( -
- - -
- - {t('orderNumber')} - {data?.orderNo} - - - {t('createdAt')}: {formatDate(data?.created_at)} - -
-
- -
{t('paymentMethod')}
-
-
-
- {data?.payment.name || data?.payment.platform} -
+
+ + +
+
+
+ {data?.payment.name} +
+
+ {data?.payment.name || data?.payment.platform} +
-
- + {data?.status && [2, 5].includes(data?.status) && ( +
+
+ + {t('subscribeNow')} + + + {t('viewDocument')} + +
+

+ {t('paymentSuccess')} +

+
+ )} + {data?.status === 1 && payment?.type === 'url' && ( +
+
+ { + if (payment?.checkout_url) { + window.location.href = payment?.checkout_url; + } + }} + > + {t('goToPayment')} + + + {t('productList')} + +
+
+

+ {t('waitingForPayment')} +

+

+ {countdownDisplay} +

+
+
+ )} + + {data?.status === 1 && payment?.type === 'qr' && ( +
+
+ + {t('productList')} + + + {t('orderList')} + +
+

{t('scanToPay')}

+

{countdownDisplay}

+ +
+ )} + + {data?.status === 1 && payment?.type === 'stripe' && ( +
+

{t('waitingForPayment')}

+

{countdownDisplay}

+ {payment.stripe && } +
+ )} + + {data?.status && [3, 4].includes(data?.status) && ( +
+
+ + {t('productList')} + + + {t('orderList')} + +
+

+ {t('orderClosed')} +

+
+ )} +
+ +
+
{t('orderNumber')}
+
{data?.order_no}
+
{data?.type && [1, 2].includes(data.type) && ( - +
+
名称
+
{data?.subscribe?.name}
+
)} {data?.type === 3 && ( <> @@ -151,7 +236,11 @@ export default function Page() { )} - +
+
{t('createdAt')}
+
{formatDate(data?.created_at)}
+
+ - - - {data?.status && [2, 5].includes(data?.status) && ( -
-

{t('paymentSuccess')}

- -
- - -
-
- )} - {data?.status === 1 && payment?.type === 'url' && ( -
-

{t('waitingForPayment')}

-

{countdownDisplay}

- -
- - -
-
- )} - - {data?.status === 1 && payment?.type === 'qr' && ( -
-

{t('scanToPay')}

-

{countdownDisplay}

- -
- - -
-
- )} - - {data?.status === 1 && payment?.type === 'stripe' && ( -
-

{t('waitingForPayment')}

-

{countdownDisplay}

- {payment.stripe && } - {/*
- - -
*/} -
- )} - - {data?.status && [3, 4].includes(data?.status) && ( -
-

{t('orderClosed')}

- -
- - -
-
- )} -
-
); } diff --git a/apps/user/app/(main)/(content)/(user)/sidebar-left.tsx b/apps/user/app/(main)/(content)/(user)/sidebar-left.tsx index 948da29..d92a47b 100644 --- a/apps/user/app/(main)/(content)/(user)/sidebar-left.tsx +++ b/apps/user/app/(main)/(content)/(user)/sidebar-left.tsx @@ -22,7 +22,11 @@ export function SidebarLeft({ ...props }: React.ComponentProps) const pathname = usePathname(); const { toggleSidebar } = useSidebar(); return ( - +
+
+
{ + console.log(locale, 111); + if (locale === 'en-US') { + handleLanguageChange('zh-CN'); + } else { + handleLanguageChange('en-US'); + } + }} + > + + {languages[locale as keyof typeof languages]} +
+
+ /* + */ ); } diff --git a/apps/user/components/main/OfferDialog/index.tsx b/apps/user/components/main/OfferDialog/index.tsx index 4a3defe..25b61d1 100644 --- a/apps/user/components/main/OfferDialog/index.tsx +++ b/apps/user/components/main/OfferDialog/index.tsx @@ -1,18 +1,9 @@ import { getSubscription } from '@/services/user/portal'; import { useQuery } from '@tanstack/react-query'; import { Dialog, DialogContent, DialogTitle } from '@workspace/airo-ui/components/dialog'; -import { ScrollArea } from '@workspace/airo-ui/components/scroll-area'; import { Tabs, TabsList, TabsTrigger } from '@workspace/airo-ui/components/tabs'; import { unitConversion } from '@workspace/airo-ui/utils'; -import { - forwardRef, - useCallback, - useEffect, - useImperativeHandle, - useMemo, - useRef, - useState, -} from 'react'; +import { forwardRef, useImperativeHandle, useMemo, useRef, useState } from 'react'; import { TabContent } from './TabContent'; import { ProcessedPlanData } from './types'; @@ -69,9 +60,11 @@ const PriceDisplay = ({ plan }: { plan: ProcessedPlanData }) => { {t('perYear')}
- {plan.origin_price && ( -

{t('yearlyDiscount')}

- )} +
+ {plan.origin_price && ( +

{t('yearlyDiscount')}

+ )} +
); }; @@ -161,7 +154,7 @@ const PlanCard = forwardRef< onSubscribe?: (plan: ProcessedPlanData) => void; isFirstCard?: boolean; } ->(({ plan, onSubscribe, isFirstCard = false }, ref) => { +>(({ plan, onSubscribe }, ref) => { const { user } = useGlobalStore(); const { openLoginDialog } = useLoginDialog(); const t = useTranslations('components.offerDialog'); @@ -186,7 +179,7 @@ const PlanCard = forwardRef< return (
{/* 套餐名称 */}

{plan.name}

@@ -230,7 +223,6 @@ export const PlanList = ({ onRetry, emptyMessage, onSubscribe, - firstPlanCardRef, // 新增参数 }: { plans: ProcessedPlanData[]; tabValue: string; @@ -239,24 +231,24 @@ export const PlanList = ({ onRetry: () => void; emptyMessage: string; onSubscribe?: (plan: ProcessedPlanData) => void; - firstPlanCardRef?: React.RefObject; }) => { if (isLoading) return ; if (error) return ; if (plans.length === 0) return ; return ( -
- {plans.map((plan, index) => ( - - ))} +
+
+ {plans.map((plan, index) => ( + + ))} +
); }; @@ -270,40 +262,7 @@ const OfferDialog = forwardRef((props, ref) => { const t = useTranslations('components.offerDialog'); const [open, setOpen] = useState(false); const [tabValue, setTabValue] = useState('year'); - const [selectedPlan, setSelectedPlan] = useState(null); - const [scrollAreaHeight, setScrollAreaHeight] = useState(450); - const [planCardHeight, setPlanCardHeight] = useState(600); const dialogRef = useRef(null); - const scrollAreaRef = useRef(null); - const firstPlanCardRef = useRef(null); - - // 获取PlanCard高度 - const getPlanCardHeight = useCallback(() => { - if (firstPlanCardRef.current) { - return firstPlanCardRef.current.offsetHeight; - } - return 600; // 默认高度 - }, []); - - // 计算 ScrollArea 高度 - const calculateScrollAreaHeight = useCallback(() => { - if (dialogRef.current && scrollAreaRef.current) { - const isMobile = window.innerWidth < 768; - - if (isMobile) { - // 移动端:使用动态计算 - const dialogHeight = dialogRef.current.offsetHeight; - const scrollAreaTop = scrollAreaRef.current.offsetTop; - const calculatedHeight = dialogHeight - scrollAreaTop; - setScrollAreaHeight(calculatedHeight); - } else { - // PC端:使用PlanCard第一项高度 - const cardHeight = getPlanCardHeight(); - setScrollAreaHeight(cardHeight); - } - } - }, [getPlanCardHeight]); - // 使用 useQuery 来管理请求 const { data = [], @@ -325,40 +284,20 @@ const OfferDialog = forwardRef((props, ref) => { return [] as API.Subscribe[]; } }, - enabled: false, // 初始不执行,手动控制 + enabled: true, // 初始不执行,手动控制 retry: 1, // 失败时重试1次 }); - // 监听对话框打开 - useEffect(() => { - if (open) { - // 等待 DOM 渲染完成 - const timer = setTimeout(calculateScrollAreaHeight, 0); - return () => clearTimeout(timer); - } - }, [open, calculateScrollAreaHeight]); - - // 监听窗口大小变化 - useEffect(() => { - if (open) { - refetch(); // 对话框打开时重新获取数据 - const handleResize = () => { - calculateScrollAreaHeight(); - }; - - window.addEventListener('resize', handleResize); - return () => window.removeEventListener('resize', handleResize); - } - }, [open, calculateScrollAreaHeight]); - useImperativeHandle(ref, () => ({ - show: () => setOpen(true), + show: () => { + refetch(); + setOpen(true); + }, hide: () => setOpen(false), })); const PurchaseRef = useRef<{ show: (subscribe: API.Subscribe) => void; hide: () => void }>(null); // 处理订阅点击 const handleSubscribe = (plan: ProcessedPlanData) => { - setSelectedPlan(plan); // 这里可以添加订阅逻辑,比如跳转到支付页面或显示确认对话框 PurchaseRef.current.show(plan, tabValue); }; @@ -399,7 +338,7 @@ const OfferDialog = forwardRef((props, ref) => {
@@ -444,22 +383,15 @@ const OfferDialog = forwardRef((props, ref) => { - - - +
diff --git a/apps/user/components/subscribe/recharge.tsx b/apps/user/components/subscribe/recharge.tsx index 810dbdf..9e5e550 100644 --- a/apps/user/components/subscribe/recharge.tsx +++ b/apps/user/components/subscribe/recharge.tsx @@ -40,7 +40,7 @@ export default function Recharge(props: Readonly) { {t('walletRecharge')} - + {t('balanceRecharge')} @@ -74,7 +74,7 @@ export default function Recharge(props: Readonly) {
{ startTransition(async () => { diff --git a/apps/user/components/subscribe/renewal.tsx b/apps/user/components/subscribe/renewal.tsx index 72b9318..b80125a 100644 --- a/apps/user/components/subscribe/renewal.tsx +++ b/apps/user/components/subscribe/renewal.tsx @@ -1,13 +1,11 @@ 'use client'; -import CouponInput from '@/components/subscribe/coupon-input'; -import DurationSelector from '@/components/subscribe/duration-selector'; import PaymentMethods from '@/components/subscribe/payment-methods'; import useGlobalStore from '@/config/use-global'; import { preCreateOrder, renewal } from '@/services/user/order'; import { useQuery } from '@tanstack/react-query'; +import { AiroButton } from '@workspace/airo-ui/components/AiroButton'; import { Button } from '@workspace/airo-ui/components/button'; -import { Card, CardContent } from '@workspace/airo-ui/components/card'; import { Dialog, DialogContent, @@ -16,6 +14,7 @@ import { DialogTrigger, } 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'; @@ -43,6 +42,8 @@ export default function Renewal({ id, subscribe, className }: Readonly(null); + const [tabValue, setTabValue] = useState('year'); + const { data: order } = useQuery({ enabled: !!subscribe.id && open, queryKey: ['preCreateOrder', params], @@ -106,20 +107,64 @@ export default function Renewal({ id, subscribe, className }: Readonly - + - {t('renewSubscription')} + {t('renewSubscription')} -
- - +
+ {t('renewSubscription')} +
+
+ { + if (val === 'year') { + handleChange('quantity', 12); + } else if (val === 'month') { + handleChange('quantity', 1); + } + setTabValue(val); + }} + > + + {tabValue === 'year' ? ( + + {t('discount20')} + {/* 小三角箭头 */} + + + ) : null} + + {t('yearlyPlan')} + + + {t('monthlyPlan')} + + + +
+ +
+
+
- + - - -
+ + { + handleChange('payment', value); + }} + /> +
+
+ + {loading && } + {t('buyNow')} + +
+
+ {/*
} {t('buyNow')} -
+
*/}
diff --git a/apps/user/components/user-nav.tsx b/apps/user/components/user-nav.tsx index 21b4e12..7099bad 100644 --- a/apps/user/components/user-nav.tsx +++ b/apps/user/components/user-nav.tsx @@ -28,21 +28,23 @@ export function UserNav({ from = '' }: { from?: string }) { {from === 'profile' ? ( -
- - - - {user?.auth_methods?.[0]?.auth_identifier.toUpperCase().charAt(0)} - - -
- {user?.auth_methods?.[0]?.auth_identifier.split('@')[0]} +
+
+ + + + {user?.auth_methods?.[0]?.auth_identifier.toUpperCase().charAt(0)} + + +
+ {user?.auth_methods?.[0]?.auth_identifier.split('@')[0]} +
+
-
) : ( diff --git a/apps/user/config/navs.ts b/apps/user/config/navs.ts index a787b77..2dd11e7 100644 --- a/apps/user/config/navs.ts +++ b/apps/user/config/navs.ts @@ -42,6 +42,13 @@ export const navs = [ hidden: true, image: 'profile', }, + { + url: '/payment', + icon: 'uil:megaphone', + title: 'payment', + hidden: true, + image: 'profile', + }, { url: '/ticket', icon: 'uil:message', diff --git a/packages/airo-ui/src/components/sheet.tsx b/packages/airo-ui/src/components/sheet.tsx index e0214ca..b6c324b 100644 --- a/packages/airo-ui/src/components/sheet.tsx +++ b/packages/airo-ui/src/components/sheet.tsx @@ -51,14 +51,16 @@ const sheetVariants = cva( interface SheetContentProps extends React.ComponentPropsWithoutRef, - VariantProps {} + VariantProps { + overlayClassName?: string; +} const SheetContent = React.forwardRef< React.ElementRef, SheetContentProps ->(({ side = 'right', className, children, ...props }, ref) => ( +>(({ side = 'right', className, overlayClassName, children, ...props }, ref) => ( - + diff --git a/packages/airo-ui/src/components/sidebar.tsx b/packages/airo-ui/src/components/sidebar.tsx index 092d18a..8578a68 100644 --- a/packages/airo-ui/src/components/sidebar.tsx +++ b/packages/airo-ui/src/components/sidebar.tsx @@ -209,6 +209,7 @@ const Sidebar = React.forwardRef< 'bg-sidebar text-sidebar-foreground w-[--sidebar-width] p-0 [&>button]:hidden', className, )} + overlayClassName={'bg-white/30 backdrop-blur-sm'} style={ { '--sidebar-width': SIDEBAR_WIDTH_MOBILE,