diff --git a/app/layout.tsx b/app/layout.tsx index 44acd72..e1618c5 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -34,7 +34,9 @@ export default function RootLayout({ > - {children} +
+ {children} +
diff --git a/app/payCallback/page.tsx b/app/payCallback/page.tsx new file mode 100644 index 0000000..451c32f --- /dev/null +++ b/app/payCallback/page.tsx @@ -0,0 +1,197 @@ +'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' + +/** + * 支付状态枚举 + */ +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') + } + + useEffect(() => { + fetchPaymentStatus() + }, []) + + // 加载状态 + 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'}

+

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

+
+ +
+ + + +
+
+
+ ) +} diff --git a/app/pricing/page.tsx b/app/pricing/page.tsx index 7f61a3a..8c0f6da 100644 --- a/app/pricing/page.tsx +++ b/app/pricing/page.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState, useEffect } from 'react'; +import { useState, useEffect, useMemo } from 'react'; import { useRouter } from 'next/navigation'; import { Check, ArrowLeft } from 'lucide-react'; import { Button } from '@/components/ui/button'; @@ -8,8 +8,20 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com import { fetchSubscriptionPlans, SubscriptionPlan } from '@/lib/stripe'; export default function PricingPage() { - const router = useRouter(); - const [billingCycle, setBillingCycle] = useState<'month' | 'year'>('month'); + + return ( +
+ {/* Main Content */} + +
+ ); +} +/**价格方案 */ +function HomeModule5() { + const [billingType, setBillingType] = useState<'month' | 'year'>( + "month" + ); + const [plans, setPlans] = useState([]); // 从后端获取订阅计划数据 @@ -26,25 +38,23 @@ export default function PricingPage() { loadPlans(); }, []); - // 转换后端数据为前端显示格式,保持原有的数据结构 - const transformPlanForDisplay = (plan: SubscriptionPlan) => { - const monthlyPrice = plan.price_month / 100; // 后端存储的是分,转换为元 - const yearlyPrice = plan.price_year / 100; - - return { - name: plan.name, - displayName: plan.display_name, - price: { - month: monthlyPrice, - year: yearlyPrice - }, - description: plan.description, - features: plan.features || [], - popular: plan.is_popular, // 使用后端返回的 is_popular 字段 - buttonText: plan.is_free ? 'Start Free Trial' : 'Subscribe', - buttonVariant: plan.is_free ? 'outline' as const : 'default' as const - }; - }; + 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, + credits: plan.description, + buttonText: plan.is_free ? "Try For Free" : "Subscribe Now", + features: plan.features || [], + }; + }); + }, [plans, billingType]); const handleSubscribe = async (planName: string) => { if (planName === 'hobby') { @@ -57,226 +67,145 @@ export default function PricingPage() { // 从localStorage获取当前用户信息 const User = JSON.parse(localStorage.getItem("currentUser") || "{}"); - + if (!User.id) { throw new Error("无法获取用户ID,请重新登录"); } - + // 1. 创建Checkout Session const result = await createCheckoutSession({ user_id: String(User.id), plan_name: planName, - billing_cycle: billingCycle + billing_cycle: billingType }); - + if (!result.successful || !result.data) { throw new Error("create checkout session failed"); } // 2. 直接跳转到Stripe托管页面(就这么简单!) redirectToCheckout(result.data.checkout_url); - + } catch (error) { throw new Error("create checkout session failed, please try again later"); } }; - - // 如果还没有加载到数据,显示加载状态但保持原有样式 - if (plans.length === 0) { - return ( -
-
-
- -
- -
-

Pricing

-

Choose the plan that suits you best

- -
- - -
-
- -
-
-

正在加载订阅计划...

-
-
-
-
- ); - } - return ( -
- {/* Main Content */} -
- {/* Back Button */} -
- + Monthly + +
- -
-

Pricing

-

Choose the plan that suits you best

- - {/* Billing Toggle */} -
-
+ + {/* 主要价格卡片 */} +
+ {pricingPlans.map((plan, index) => ( +
+

+ {plan.title} +

+
+ + ${plan.price} + /month +
+

+ {plan.credits} +

+ - +

+ * Billed monthly until cancelled +

+
    + {plan.features.map((feature, featureIndex) => ( +
  • + + {feature} +
  • + ))} +
+ ))} +
+ + {/* 额外价格卡片 */} +
+
+

+ Free +

+
+ $0 +
+

+ 10 Video mins and 1 AI credit per week, 1 Express avatar, 4 Exports + per week with invideo watermark. +

+

+ No access to generative features. +

+
- {/* Plans Section */} -
-
- {plans.map((plan) => { - const displayPlan = transformPlanForDisplay(plan); - - return ( -
- {/* 预留标签空间,确保所有卡片组合高度一致 */} -
- {displayPlan.popular && ( -
- Most Popular -
- )} -
- - - - - {displayPlan.displayName} - -
-
- {displayPlan.price[billingCycle] === 0 ? ( - 'Free' - ) : ( - <> - ${displayPlan.price[billingCycle]} - - /{billingCycle === 'month' ? 'month' : 'year'} - - - )} -
-
- - - {displayPlan.description} - -
- - -
- {displayPlan.features.map((feature, index) => ( -
-
- -
- {feature} -
- ))} -
- - -
-
-
- ); - })} -
+
+

+ Enterprise +

+

Custom

+

+ Custom solutions for large organizations. Advanced security and + flexible pricing based on your needs. +

+

+ on your needs. +

+
-
+
); } diff --git a/components/layout/dashboard-layout.tsx b/components/layout/dashboard-layout.tsx index 8395a88..d8c27fb 100644 --- a/components/layout/dashboard-layout.tsx +++ b/components/layout/dashboard-layout.tsx @@ -12,7 +12,7 @@ export function DashboardLayout({ children }: DashboardLayoutProps) { const [sidebarCollapsed, setSidebarCollapsed] = useState(true); return ( -
+
setSidebarCollapsed(!sidebarCollapsed)} /> {children} diff --git a/components/layout/top-bar.tsx b/components/layout/top-bar.tsx index 023990c..d0377f4 100644 --- a/components/layout/top-bar.tsx +++ b/components/layout/top-bar.tsx @@ -1,9 +1,9 @@ "use client"; -import '../pages/style/top-bar.css'; -import { Button } from '@/components/ui/button'; -import { GradientText } from '@/components/ui/gradient-text'; -import { useTheme } from 'next-themes'; +import "../pages/style/top-bar.css"; +import { Button } from "@/components/ui/button"; +import { GradientText } from "@/components/ui/gradient-text"; +import { useTheme } from "next-themes"; import { Sun, Moon, @@ -11,13 +11,12 @@ import { Sparkles, LogOut, PanelsLeftBottom, -} from 'lucide-react'; -import { motion } from 'framer-motion'; -import ReactDOM from 'react-dom'; -import { useRouter } from 'next/navigation'; -import React, { useRef, useEffect, useLayoutEffect, useState } from 'react'; -import { logoutUser } from '@/lib/auth'; - +} from "lucide-react"; +import { motion } from "framer-motion"; +import ReactDOM from "react-dom"; +import { usePathname, useRouter } from "next/navigation"; +import React, { useRef, useEffect, useLayoutEffect, useState } from "react"; +import { logoutUser } from "@/lib/auth"; interface User { id: string; @@ -26,14 +25,23 @@ interface User { avatar: string; } -export function TopBar({ collapsed, onToggleSidebar }: { collapsed: boolean; onToggleSidebar: () => void }) { +export function TopBar({ + collapsed, + onToggleSidebar, +}: { + collapsed: boolean; + onToggleSidebar: () => void; +}) { const router = useRouter(); const [isOpen, setIsOpen] = React.useState(false); const menuRef = useRef(null); const buttonRef = useRef(null); - const currentUser: User = JSON.parse(localStorage.getItem('currentUser') || '{}'); + const currentUser: User = JSON.parse( + localStorage.getItem("currentUser") || "{}" + ); const [mounted, setMounted] = React.useState(false); const [isLogin, setIsLogin] = useState(false); + const pathname = usePathname() useEffect(() => { const currentUser = localStorage.getItem("currentUser"); if (JSON.parse(currentUser || "{}")?.token) { @@ -43,12 +51,11 @@ export function TopBar({ collapsed, onToggleSidebar }: { collapsed: boolean; onT } }); useLayoutEffect(() => { - console.log('Setting mounted state'); + console.log("Setting mounted state"); setMounted(true); - return () => console.log('Cleanup mounted effect'); + return () => console.log("Cleanup mounted effect"); }, []); - // 处理点击事件 useEffect(() => { if (!isOpen) return; @@ -58,16 +65,14 @@ export function TopBar({ collapsed, onToggleSidebar }: { collapsed: boolean; onT const handleMouseDown = (event: MouseEvent) => { const target = event.target as Node; isClickStartedInside = !!( - menuRef.current?.contains(target) || - buttonRef.current?.contains(target) + menuRef.current?.contains(target) || buttonRef.current?.contains(target) ); }; const handleMouseUp = (event: MouseEvent) => { const target = event.target as Node; const isClickEndedInside = !!( - menuRef.current?.contains(target) || - buttonRef.current?.contains(target) + menuRef.current?.contains(target) || buttonRef.current?.contains(target) ); // 只有当点击开始和结束都在外部时才关闭 @@ -78,12 +83,12 @@ export function TopBar({ collapsed, onToggleSidebar }: { collapsed: boolean; onT }; // 在冒泡阶段处理事件 - document.addEventListener('mousedown', handleMouseDown); - document.addEventListener('mouseup', handleMouseUp); + document.addEventListener("mousedown", handleMouseDown); + document.addEventListener("mouseup", handleMouseUp); return () => { - document.removeEventListener('mousedown', handleMouseDown); - document.removeEventListener('mouseup', handleMouseUp); + document.removeEventListener("mousedown", handleMouseDown); + document.removeEventListener("mouseup", handleMouseUp); }; }, [isOpen]); @@ -98,7 +103,10 @@ export function TopBar({ collapsed, onToggleSidebar }: { collapsed: boolean; onT }; return ( -
+
{isLogin && ( @@ -151,7 +159,10 @@ export function TopBar({ collapsed, onToggleSidebar }: { collapsed: boolean; onT */} - {/* Theme Toggle */} - {/* */} {/* User Menu */} -
+
- {mounted && isOpen ? ReactDOM.createPortal( - e.stopPropagation()} - > - {/* User Info */} -
-
-
- {currentUser.name ? currentUser.name.charAt(0) : ''} + {mounted && isOpen + ? ReactDOM.createPortal( + e.stopPropagation()} + > + {/* User Info */} +
+
+
+ {currentUser.name ? currentUser.name.charAt(0) : ""} +
+
+

+ {currentUser.name} +

+

+ {currentUser.email} +

+
+
{ + logoutUser(); + }} + title="退出登录" + > + +
-
-

{currentUser.name}

-

{currentUser.email}

+
+ + {/* AI Points */} +
+
+ + + 100 credits +
-
{ - logoutUser(); - }} - title="退出登录" +
+ Upgrade + +
-
- {/* AI Points */} -
-
- - 100 credits -
- -
- - {/* Menu Items */} -
- {/* + {/* router.push('/my-library')} @@ -268,19 +294,19 @@ export function TopBar({ collapsed, onToggleSidebar }: { collapsed: boolean; onT Logout */} - {/* Footer */} -
-
Privacy Policy · Terms of Service
-
250819215404 | 2025/8/20 06:00:50
+ {/* Footer */} +
+
Privacy Policy · Terms of Service
+
250819215404 | 2025/8/20 06:00:50
+
-
-
- , document.body) - : null} + , + document.body + ) + : null}
-
); } diff --git a/components/pages/home-page2.tsx b/components/pages/home-page2.tsx index b194e0a..0bf9d83 100644 --- a/components/pages/home-page2.tsx +++ b/components/pages/home-page2.tsx @@ -9,7 +9,7 @@ import { CircleArrowRight, } from "lucide-react"; import "./style/home-page2.css"; -import { useRouter } from "next/navigation"; +import { usePathname, useRouter } from "next/navigation"; import { Swiper, SwiperSlide } from "swiper/react"; import "swiper/swiper-bundle.css"; // 引入样式 import { Autoplay } from "swiper/modules"; @@ -26,16 +26,18 @@ import { ProjectTypeEnum, ModeEnum, ResolutionEnum } from "@/app/model/enums"; import { getResourcesList, Resource } from "@/api/resources"; import { Carousel } from "antd"; import { TextCanvas } from "../common/TextCanvas"; +import { fetchSubscriptionPlans, SubscriptionPlan } from "@/lib/stripe"; export function HomePage2() { const [hPading, setHPading] = useState(0); useEffect(() => { - setHPading((window as any).Scale.hScale * 11); + // 为兼容不同屏幕的padding进行三次方处理 + setHPading(((window as any).Scale?.hScale || 1)**3 * 10); }, []); return ( //
@@ -52,7 +54,7 @@ export function HomePage2() { function HomeModule1() { const router = useRouter(); return ( -
+
-

+

Ideas Become Movies

@@ -84,6 +86,20 @@ function HomeModule1() { } /**核心价值 */ function HomeModule2() { + const videoList = [ + { + title: "Text to Movie", + video: "/assets/module2 (1).mp4", + }, + { + title: "Image to Movie", + video: "/assets/module2 (2).mp4", + }, + { + title: "Template to Movie", + video: "/assets/module2 (3).mp4", + } + ] return (

- + {/* 第一个视频 */} + {videoList.map((item, index) => ( +
+
+ ))} +
); @@ -250,24 +279,28 @@ function HomeModule4() { const processSteps = [ { - title: "Pre-Production", + title: "The Narrative Engine", description: - "Developing the story, writing the script, securing funding, assembling the crew, and planning the shoot.", + " From a single thought, it builds entire worlds and compelling plots.", + video: "/assets/module4 (3).mp4", }, { - title: "Production", + title: "AI Character Engine", description: - "Developing the story, writing the script, securing funding, assembling the crew, and planning the shoot.", + "Cast your virtual actors. Lock them in once, for the entire story.", + video: "/assets/module4 (1).mp4", }, { - title: "Visual Effects", + title: "AI vision engine", description: - "Developing the story, writing the script, securing funding, assembling the crew, and planning the shoot.", + "It translates your aesthetic into art, light, and cinematography for every single shot.", + video: "/assets/module4 (4).mp4", }, { - title: "Voice", + title: "Intelligent Editing Engine", description: - "Developing the story, writing the script, securing funding, assembling the crew, and planning the shoot.", + "An editing AI drives the final cut, for a story told seamlessly.", + video: "/assets/module4 (2).mp4", }, ]; @@ -284,7 +317,7 @@ function HomeModule4() { data-alt="core-value-content" className="center z-10 flex flex-col items-center mb-[14rem]" > -

+

Edit like you think

@@ -327,7 +360,7 @@ function HomeModule4() {
{/* 主要价格卡片 */} -
+
{pricingPlans.map((plan, index) => (

{plan.title} @@ -466,7 +504,7 @@ function HomeModule5() {

{plan.credits}

-

@@ -488,7 +526,7 @@ function HomeModule5() {

{/* 额外价格卡片 */} -
+

Free diff --git a/public/assets/home.mp4 b/public/assets/home.mp4 index 72f55ee..4845c00 100644 Binary files a/public/assets/home.mp4 and b/public/assets/home.mp4 differ diff --git a/public/assets/module2 (1).mp4 b/public/assets/module2 (1).mp4 new file mode 100644 index 0000000..d4fe2a5 Binary files /dev/null and b/public/assets/module2 (1).mp4 differ diff --git a/public/assets/module2 (2).mp4 b/public/assets/module2 (2).mp4 new file mode 100644 index 0000000..3055bd3 Binary files /dev/null and b/public/assets/module2 (2).mp4 differ diff --git a/public/assets/module2 (3).mp4 b/public/assets/module2 (3).mp4 new file mode 100644 index 0000000..070f1ea Binary files /dev/null and b/public/assets/module2 (3).mp4 differ diff --git a/public/assets/module4 (1).mp4 b/public/assets/module4 (1).mp4 new file mode 100644 index 0000000..cd969ab Binary files /dev/null and b/public/assets/module4 (1).mp4 differ diff --git a/public/assets/module4 (2).mp4 b/public/assets/module4 (2).mp4 new file mode 100644 index 0000000..7864a15 Binary files /dev/null and b/public/assets/module4 (2).mp4 differ diff --git a/public/assets/module4 (3).mp4 b/public/assets/module4 (3).mp4 new file mode 100644 index 0000000..4de3e15 Binary files /dev/null and b/public/assets/module4 (3).mp4 differ diff --git a/public/assets/module4 (4).mp4 b/public/assets/module4 (4).mp4 new file mode 100644 index 0000000..01f3bcd Binary files /dev/null and b/public/assets/module4 (4).mp4 differ