From caa2bfb5e4aa9e6f689c5450414c21b79a1d64c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=B7=E9=BE=99?= Date: Thu, 28 Aug 2025 18:27:15 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E4=BB=98=E5=88=9D=E6=AD=A5=E5=AF=B9?= =?UTF-8?q?=E6=8E=A5=EF=BC=8C=E7=AD=89=E4=BC=9A=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/layout.tsx | 60 ++++++-- app/payCallback/page.tsx | 206 ++-------------------------- app/pricing/page.tsx | 65 +++++---- components/common/CallbackModal.tsx | 205 +++++++++++++++++++++++++++ components/layout/top-bar.tsx | 12 +- components/pages/home-page2.tsx | 6 +- lib/stripe.ts | 8 +- next.config.js | 8 +- 8 files changed, 326 insertions(+), 244 deletions(-) create mode 100644 components/common/CallbackModal.tsx diff --git a/app/layout.tsx b/app/layout.tsx index d43e8ed..4cf7678 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,26 +1,53 @@ +'use client' import './globals.css'; -import type { Metadata } from 'next'; +import { createContext, useContext, useEffect, useState } from 'react'; import { Providers } from '@/components/providers'; import { ConfigProvider, theme } from 'antd'; -import { useEffect } from 'react'; import { createScreenAdapter } from '@/utils/tools'; import { ScreenAdapter } from './ScreenAdapter'; +import CallbackModal from '@/components/common/CallbackModal'; -export const metadata: Metadata = { - title: 'AI Movie Flow - Create Amazing Videos with AI', - description: 'Professional AI-powered video creation platform with advanced editing tools', -}; +// 创建上下文来传递弹窗控制方法 +const CallbackModalContext = createContext<{ + setShowCallbackModal: (show: boolean) => void +} | null>(null) + +// Hook 来使用弹窗控制方法 +export const useCallbackModal = () => { + const context = useContext(CallbackModalContext) + if (!context) { + throw new Error('useCallbackModal must be used within CallbackModalProvider') + } + return context +} export default function RootLayout({ children, }: { children: React.ReactNode }) { + const [showCallbackModal, setShowCallbackModal] = useState(false) + const openCallback = async function (ev: MessageEvent) { + console.log(ev) + if (ev.data.type === 'waiting-payment') { + setShowCallbackModal(true) + } + } + useEffect(() => { + window.addEventListener('message', openCallback) + return () => { + window.removeEventListener('message', openCallback) + } + }, []) return ( - + + AI Movie Flow - Create Amazing Videos with AI + + + - - - {/* */} -
- {children} -
-
+ + + {/* */} +
+ {children} + {showCallbackModal && setShowCallbackModal(false)} />} +
+
+
- ); + ) } diff --git a/app/payCallback/page.tsx b/app/payCallback/page.tsx index 451c32f..2581e94 100644 --- a/app/payCallback/page.tsx +++ b/app/payCallback/page.tsx @@ -1,197 +1,21 @@ -'use client' +"use client"; -import React, { useEffect, useState } from 'react' -import { Result, Button, Spin, Card } from 'antd' -import { CheckCircleOutlined, CloseCircleOutlined, LoadingOutlined } from '@ant-design/icons' -import { useRouter, useSearchParams } from 'next/navigation' -import { getCheckoutSessionStatus, PaymentStatusResponse } from '@/lib/stripe' +import { useSearchParams } from "next/navigation"; +import { useEffect } from "react"; -/** - * 支付状态枚举 - */ -enum PaymentStatus { - LOADING = 'loading', - SUCCESS = 'success', - FAILED = 'failed' -} - -/** - * 支付回调页面组件 - * 处理Stripe Checkout支付完成后的状态展示和用户操作 - */ -export default function PayCallbackPage() { - const router = useRouter() - const searchParams = useSearchParams() - const [paymentStatus, setPaymentStatus] = useState(PaymentStatus.LOADING) - const [paymentInfo, setPaymentInfo] = useState(null) - const callBackUrl = localStorage.getItem('callBackUrl') || '/' - - /** - * 获取Stripe Checkout Session支付状态 - */ - const fetchPaymentStatus = async () => { - try { - // 从URL参数获取session_id和user_id - const sessionId = searchParams.get('session_id') - const userId = searchParams.get('user_id') - - if (!sessionId || !userId) { - throw new Error('缺少必要的参数: session_id 或 user_id') - } - - // 调用真实的Stripe API获取支付状态 - const response = await getCheckoutSessionStatus(sessionId, userId) - - if (response.successful && response.data) { - const { payment_status, biz_order_no, pay_time, subscription } = response.data - - if (payment_status === 'success') { - setPaymentStatus(PaymentStatus.SUCCESS) - setPaymentInfo({ - orderId: biz_order_no, - sessionId, - paymentTime: pay_time, - subscription: subscription ? { - planName: subscription.plan_name, - planDisplayName: subscription.plan_display_name, - status: subscription.status, - currentPeriodEnd: subscription.current_period_end - } : null - }) - } else if (payment_status === 'fail') { - setPaymentStatus(PaymentStatus.FAILED) - setPaymentInfo({ - orderId: biz_order_no, - sessionId, - errorMessage: '支付处理失败,请重试' - }) - } else { - // pending状态,继续等待 - setTimeout(fetchPaymentStatus, 2000) - } - } else { - throw new Error(response.message || '获取支付状态失败') - } - } catch (error) { - console.error('获取支付状态失败:', error) - setPaymentStatus(PaymentStatus.FAILED) - setPaymentInfo({ - errorMessage: error instanceof Error ? error.message : '网络错误,无法获取支付状态' - }) - } - } - - /** - * 返回上一页的函数 - */ - const handleGoBack = () => { - router.push(callBackUrl) - } - - /** - * 重新支付的函数 - */ - const handleRetryPayment = () => { - router.push('/pricing') - } +export default function payCallback() { + const searchParams = useSearchParams(); + const sessionId = searchParams.get("session_id"); + const userId = searchParams.get("user_id"); + const canceled = searchParams.get("canceled")||false; useEffect(() => { - fetchPaymentStatus() - }, []) + window.opener.postMessage( + { type: "payment-callback", canceled, sessionId, userId }, + "*" + ); + window.close(); + }, []); - // 加载状态 - if (paymentStatus === PaymentStatus.LOADING) { - return ( -
-
- } - size="large" - /> -
正在获取支付状态...
-
请稍候,我们正在处理您的支付信息
-
-
- ) - } - - // 支付成功状态 - if (paymentStatus === PaymentStatus.SUCCESS) { - return ( -
-
-
- -

支付成功!

-

{`订单号: ${paymentInfo?.orderId || 'N/A'}`}

-
- -
- {paymentInfo?.subscription && ( - <> -

订阅计划: {paymentInfo.subscription.planDisplayName}

-

订阅状态: {paymentInfo.subscription.status}

- {paymentInfo.subscription.currentPeriodEnd && ( -

到期时间: {new Date(paymentInfo.subscription.currentPeriodEnd).toLocaleString()}

- )} - - )} - {paymentInfo?.paymentTime && ( -

支付时间: {new Date(paymentInfo.paymentTime).toLocaleString()}

- )} -

感谢您的购买!

-
- - -
-
- ) - } - - // 支付失败状态 - return ( -
-
-
- -

支付失败

-

{paymentInfo?.errorMessage || '支付处理过程中发生错误'}

-
- -
-

订单号: {paymentInfo?.orderId || 'N/A'}

-

如果问题持续存在,请联系客服

-
- -
- - - -
-
-
- ) + return <>; } diff --git a/app/pricing/page.tsx b/app/pricing/page.tsx index 8c0f6da..e600f29 100644 --- a/app/pricing/page.tsx +++ b/app/pricing/page.tsx @@ -1,11 +1,17 @@ -'use client'; +"use client"; -import { useState, useEffect, useMemo } from 'react'; -import { useRouter } from 'next/navigation'; -import { Check, ArrowLeft } from 'lucide-react'; -import { Button } from '@/components/ui/button'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; -import { fetchSubscriptionPlans, SubscriptionPlan } from '@/lib/stripe'; +import { useState, useEffect, useMemo } from "react"; +import { useRouter, useSearchParams } from "next/navigation"; +import { Check, ArrowLeft } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { fetchSubscriptionPlans, SubscriptionPlan } from "@/lib/stripe"; export default function PricingPage() { @@ -18,9 +24,7 @@ export default function PricingPage() { } /**价格方案 */ function HomeModule5() { - const [billingType, setBillingType] = useState<'month' | 'year'>( - "month" - ); + const [billingType, setBillingType] = useState<"month" | "year">("month"); const [plans, setPlans] = useState([]); @@ -31,24 +35,29 @@ function HomeModule5() { const plansData = await fetchSubscriptionPlans(); setPlans(plansData); } catch (err) { - console.error('加载订阅计划失败:', err); + console.error("加载订阅计划失败:", err); } }; loadPlans(); }, []); - const pricingPlans = useMemo<{ - title: string; - price: number; - credits: string; - buttonText: string; - features: string[]; - }[]>(() => { + const pricingPlans = useMemo< + { + title: string; + price: number; + credits: string; + buttonText: string; + features: string[]; + }[] + >(() => { return plans.map((plan) => { return { title: plan.display_name || plan.name, - price: billingType === "month" ? plan.price_month/100 : plan.price_year/100, + price: + billingType === "month" + ? plan.price_month / 100 + : plan.price_year / 100, credits: plan.description, buttonText: plan.is_free ? "Try For Free" : "Subscribe Now", features: plan.features || [], @@ -57,13 +66,15 @@ function HomeModule5() { }, [plans, billingType]); const handleSubscribe = async (planName: string) => { - if (planName === 'hobby') { + if (planName === "hobby") { return; } try { // 使用新的Checkout Session方案(更简单!) - const { createCheckoutSession, redirectToCheckout } = await import('@/lib/stripe'); + const { createCheckoutSession, redirectToCheckout } = await import( + "@/lib/stripe" + ); // 从localStorage获取当前用户信息 const User = JSON.parse(localStorage.getItem("currentUser") || "{}"); @@ -76,16 +87,15 @@ function HomeModule5() { const result = await createCheckoutSession({ user_id: String(User.id), plan_name: planName, - billing_cycle: billingType + billing_cycle: billingType, }); if (!result.successful || !result.data) { throw new Error("create checkout session failed"); } - + window.opener.postMessage({ type: "waiting-payment" }, "*"); // 2. 直接跳转到Stripe托管页面(就这么简单!) - redirectToCheckout(result.data.checkout_url); - + window.location.href = result.data.checkout_url; } catch (error) { throw new Error("create checkout session failed, please try again later"); } @@ -147,7 +157,10 @@ function HomeModule5() {

{plan.credits}

-

diff --git a/components/common/CallbackModal.tsx b/components/common/CallbackModal.tsx new file mode 100644 index 0000000..469e07a --- /dev/null +++ b/components/common/CallbackModal.tsx @@ -0,0 +1,205 @@ + + +'use client' + +import React, { useEffect, useState } from 'react' +import { Result, Button, Spin, Card, Modal } from 'antd' +import { CheckCircleOutlined, CloseCircleOutlined, LoadingOutlined } from '@ant-design/icons' +import { useRouter, useSearchParams } from 'next/navigation' +import { getCheckoutSessionStatus, PaymentStatusResponse } from '@/lib/stripe' + +/** + * 支付状态枚举 + */ +enum PaymentStatus { + LOADING = 'loading', + SUCCESS = 'success', + FAILED = 'failed' +} + +/** + * 支付回调模态框组件 + * 处理Stripe Checkout支付完成后的状态展示和用户操作 + */ +export default function CallbackModal({ onClose }: { onClose: () => void }) { + const router = useRouter() + const searchParams = useSearchParams() + const [paymentStatus, setPaymentStatus] = useState(PaymentStatus.LOADING) + const [paymentInfo, setPaymentInfo] = useState(null) + const [isModalOpen, setIsModalOpen] = useState(true) + const callBackUrl = localStorage.getItem('callBackUrl') || '/' + + /** + * 获取Stripe Checkout Session支付状态 + */ + const fetchPaymentStatus = async () => { + try { + // 从URL参数获取session_id和user_id + const sessionId = searchParams.get('session_id') + const userId = searchParams.get('user_id') + + if (!sessionId || !userId) { + throw new Error('Missing required parameters: session_id or user_id') + } + + // 调用真实的Stripe API获取支付状态 + const response = await getCheckoutSessionStatus(sessionId, userId) + + if (response.successful && response.data) { + const { payment_status, biz_order_no, pay_time, subscription } = response.data + + if (payment_status === 'success') { + setPaymentStatus(PaymentStatus.SUCCESS) + setPaymentInfo({ + orderId: biz_order_no, + sessionId, + paymentTime: pay_time, + subscription: subscription ? { + planName: subscription.plan_name, + planDisplayName: subscription.plan_display_name, + status: subscription.status, + currentPeriodEnd: subscription.current_period_end + } : null + }) + } else if (payment_status === 'fail') { + setPaymentStatus(PaymentStatus.FAILED) + setPaymentInfo({ + orderId: biz_order_no, + sessionId, + errorMessage: 'Payment processing failed, please try again' + }) + } else { + // pending状态,继续等待 + setTimeout(fetchPaymentStatus, 2000) + } + } else { + throw new Error(response.message || 'Failed to get payment status') + } + } catch (error) { + console.error('Failed to get payment status:', error) + setPaymentStatus(PaymentStatus.FAILED) + setPaymentInfo({ + errorMessage: error instanceof Error ? error.message : 'Network error, unable to get payment status' + }) + } + } + + // 渲染模态框内容 + const renderModalContent = () => { + // 加载状态 + if (paymentStatus === PaymentStatus.LOADING) { + return ( +

+ } + size="large" + /> +
Getting payment status...
+
Please wait, we are processing your payment information
+
+ ) + } + + // 支付成功状态 + if (paymentStatus === PaymentStatus.SUCCESS) { + return ( +
+
+ +

Payment Successful!

+

{`Order ID: ${paymentInfo?.orderId || 'N/A'}`}

+
+ +
+ {paymentInfo?.subscription && ( + <> +

Subscription Plan: {paymentInfo.subscription.planDisplayName}

+

Subscription Status: {paymentInfo.subscription.status}

+ {paymentInfo.subscription.currentPeriodEnd && ( +

Expiry Date: {new Date(paymentInfo.subscription.currentPeriodEnd).toLocaleString()}

+ )} + + )} + {paymentInfo?.paymentTime && ( +

Payment Time: {new Date(paymentInfo.paymentTime).toLocaleString()}

+ )} +

Thank you for your purchase!

+
+
+ ) + } + + // 支付失败状态 + return ( +
+
+ +

Payment Failed

+

{paymentInfo?.errorMessage || 'An error occurred during payment processing'}

+
+ +
+

Order ID: {paymentInfo?.orderId || 'N/A'}

+

If the problem persists, please contact customer service

+
+
+ ) + } + const watResult = async function (ev: MessageEvent<{ + type: 'payment-callback', + canceled: boolean, + sessionId: string, + userId: string + }>) { + if (ev.data.type === 'payment-callback') { + if (ev.data.canceled) { + setPaymentStatus(PaymentStatus.FAILED) + return + } + const userId = JSON.parse(localStorage.getItem('currentUser') || '{}').id + const res = await getCheckoutSessionStatus(ev.data.sessionId, userId) + if (res.successful && res.data) { + if(res.data.payment_status === 'success'){ + setPaymentStatus(PaymentStatus.SUCCESS) + }else{ + setPaymentStatus(PaymentStatus.FAILED) + } + } + } + } + useEffect(() => { + window.addEventListener('message', watResult) + return () => { + window.removeEventListener('message', watResult) + } + }, []) + return ( + } + maskClosable={false} + keyboard={false} + footer={null} + width={500} + centered + title={null} + className="payment-callback-modal" + onCancel={onClose} + styles={{ + content: { + backgroundColor: 'black', + border: '1px solid rgba(255, 255, 255, 0.2)', + }, + mask: { + backgroundColor: 'rgba(0, 0, 0, 0.8)', + }, + header: { + borderBottom: 'none', + } + }} + > + {renderModalContent()} + + ) +} diff --git a/components/layout/top-bar.tsx b/components/layout/top-bar.tsx index d0377f4..ba2c8d1 100644 --- a/components/layout/top-bar.tsx +++ b/components/layout/top-bar.tsx @@ -41,7 +41,7 @@ export function TopBar({ ); const [mounted, setMounted] = React.useState(false); const [isLogin, setIsLogin] = useState(false); - const pathname = usePathname() + const pathname = usePathname(); useEffect(() => { const currentUser = localStorage.getItem("currentUser"); if (JSON.parse(currentUser || "{}")?.token) { @@ -161,7 +161,7 @@ export function TopBar({ size="sm" onClick={() => { localStorage.setItem("callBackUrl", pathname); - router.push("/pricing"); + window.open("/pricing", "_blank"); }} className="text-gray-300 hover:text-white" > @@ -255,7 +255,9 @@ export function TopBar({ variant="outline" size="sm" className="text-white border-white hover:bg-white/10 rounded-full px-3 py-1 text-[10px]" - onClick={() => router.push("/pricing")} + onClick={() => { + window.open("/pricing", "_blank"); + }} > Upgrade @@ -263,7 +265,9 @@ export function TopBar({ variant="outline" size="sm" className="text-white border-white hover:bg-white/10 rounded-full px-3 py-1 text-[10px]" - onClick={() => router.push("/pricing")} + onClick={() => { + window.open("/pricing", "_blank"); + }} > Manage diff --git a/components/pages/home-page2.tsx b/components/pages/home-page2.tsx index e34fd81..6735c11 100644 --- a/components/pages/home-page2.tsx +++ b/components/pages/home-page2.tsx @@ -27,6 +27,7 @@ import { getResourcesList, Resource } from "@/api/resources"; import { Carousel } from "antd"; import { TextCanvas } from "../common/TextCanvas"; import { fetchSubscriptionPlans, SubscriptionPlan } from "@/lib/stripe"; +import { useCallbackModal } from "@/app/layout"; export function HomePage2() { const [hPading, setHPading] = useState(0); @@ -407,6 +408,7 @@ function HomeModule5() { const [billingType, setBillingType] = useState<"month" | "year">("month"); const [plans, setPlans] = useState([]); + const { setShowCallbackModal } = useCallbackModal(); const pathname = usePathname(); // 从后端获取订阅计划数据 useEffect(() => { @@ -474,8 +476,8 @@ function HomeModule5() { throw new Error("create checkout session failed"); } - // 2. 直接跳转到Stripe托管页面(就这么简单!) - redirectToCheckout(result.data.checkout_url); + setShowCallbackModal(true) + window.open(result.data.checkout_url, '_blank'); } catch (error) { throw new Error("create checkout session failed, please try again later"); } diff --git a/lib/stripe.ts b/lib/stripe.ts index 49e883f..2c28a10 100644 --- a/lib/stripe.ts +++ b/lib/stripe.ts @@ -54,7 +54,7 @@ export type CreateCheckoutSessionResponse = ApiResponse { try { const response = await get>('/api/subscription/plans'); - + if (!response.successful || !response.data) { throw new Error(response.message || '获取订阅计划失败'); } @@ -71,7 +71,7 @@ export async function fetchSubscriptionPlans(): Promise { /** * 创建Checkout Session(推荐方案) - * + * * 这是更简单的支付方案: * 1. 调用此函数获取checkout_url * 2. 直接跳转到checkout_url @@ -109,6 +109,6 @@ export async function getCheckoutSessionStatus( */ export function redirectToCheckout(checkoutUrl: string) { if (typeof window !== 'undefined') { - window.location.href = checkoutUrl; + window.open(checkoutUrl, '_blank'); } -} \ No newline at end of file +} diff --git a/next.config.js b/next.config.js index a262b32..eee8db7 100644 --- a/next.config.js +++ b/next.config.js @@ -34,16 +34,20 @@ const nextConfig = { }, async rewrites() { + // 使用环境变量,如果没有则使用默认值 const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL || 'https://77.smartvideo.py.qikongjian.com' + console.log('Environment BASE_URL:', process.env) + console.log('Using BASE_URL:', BASE_URL) + return [ { source: '/api/proxy/:path*', - destination: BASE_URL+'/:path*', + destination: `${BASE_URL}/:path*`, }, { source: '/api/resources/:path*', - destination: BASE_URL+'/:path*', + destination: `${BASE_URL}/:path*`, }, ]; },