This commit is contained in:
海龙 2025-08-28 16:03:14 +08:00
parent 0032ff1aa9
commit bc2302d907
14 changed files with 600 additions and 408 deletions

View File

@ -34,7 +34,9 @@ export default function RootLayout({
> >
<Providers> <Providers>
<ScreenAdapter /> <ScreenAdapter />
{children} <div id="app" className='h-full w-full'>
{children}
</div>
</Providers> </Providers>
</ConfigProvider> </ConfigProvider>
</body> </body>

197
app/payCallback/page.tsx Normal file
View File

@ -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>(PaymentStatus.LOADING)
const [paymentInfo, setPaymentInfo] = useState<any>(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 (
<div className="min-h-screen flex items-center justify-center bg-black">
<div className="w-96 text-center bg-black border border-white/20 rounded-lg p-8">
<Spin
indicator={<LoadingOutlined style={{ fontSize: 48, color: 'white' }} spin />}
size="large"
/>
<div className="mt-6 text-lg text-white">...</div>
<div className="mt-2 text-sm text-white/70"></div>
</div>
</div>
)
}
// 支付成功状态
if (paymentStatus === PaymentStatus.SUCCESS) {
return (
<div className="min-h-screen flex items-center justify-center bg-black">
<div className="w-[32rem] text-center bg-black border border-white/20 rounded-lg p-8">
<div className="mb-6">
<CheckCircleOutlined className="text-white text-6xl mb-4" />
<h1 className="text-white text-2xl font-bold mb-2"></h1>
<p className="text-white/70 text-base">{`订单号: ${paymentInfo?.orderId || 'N/A'}`}</p>
</div>
<div className="text-center text-white/70 mb-8 space-y-2">
{paymentInfo?.subscription && (
<>
<p>: {paymentInfo.subscription.planDisplayName}</p>
<p>: {paymentInfo.subscription.status}</p>
{paymentInfo.subscription.currentPeriodEnd && (
<p>: {new Date(paymentInfo.subscription.currentPeriodEnd).toLocaleString()}</p>
)}
</>
)}
{paymentInfo?.paymentTime && (
<p>: {new Date(paymentInfo.paymentTime).toLocaleString()}</p>
)}
<p></p>
</div>
<Button
type="primary"
size="large"
onClick={handleGoBack}
data-alt="back-to-previous-page-button"
className="w-full h-12 bg-white text-black hover:bg-white/90 border border-white/20"
>
</Button>
</div>
</div>
)
}
// 支付失败状态
return (
<div className="min-h-screen flex items-center justify-center bg-black">
<div className="w-[32rem] text-center bg-black border border-white/20 rounded-lg p-8">
<div className="mb-6">
<CloseCircleOutlined className="text-white text-6xl mb-4" />
<h1 className="text-white text-2xl font-bold mb-2"></h1>
<p className="text-white/70 text-base">{paymentInfo?.errorMessage || '支付处理过程中发生错误'}</p>
</div>
<div className="text-center text-white/70 mb-8 space-y-2">
<p>: {paymentInfo?.orderId || 'N/A'}</p>
<p></p>
</div>
<div className="space-y-3">
<Button
type="primary"
size="large"
onClick={handleRetryPayment}
data-alt="retry-payment-button"
className="w-full h-12 bg-white text-black hover:bg-white/90 border border-white/20"
>
</Button>
<Button
size="large"
onClick={handleGoBack}
data-alt="back-to-previous-page-button"
className="w-full h-12 bg-black text-white hover:bg-white hover:text-black border border-white/20"
>
</Button>
</div>
</div>
</div>
)
}

View File

@ -1,6 +1,6 @@
'use client'; 'use client';
import { useState, useEffect } from 'react'; import { useState, useEffect, useMemo } from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { Check, ArrowLeft } from 'lucide-react'; import { Check, ArrowLeft } from 'lucide-react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
@ -8,8 +8,20 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
import { fetchSubscriptionPlans, SubscriptionPlan } from '@/lib/stripe'; import { fetchSubscriptionPlans, SubscriptionPlan } from '@/lib/stripe';
export default function PricingPage() { export default function PricingPage() {
const router = useRouter();
const [billingCycle, setBillingCycle] = useState<'month' | 'year'>('month'); return (
<div className="w-full h-full overflow-y-auto bg-black text-white pb-[10rem]">
{/* Main Content */}
<HomeModule5 />
</div>
);
}
/**价格方案 */
function HomeModule5() {
const [billingType, setBillingType] = useState<'month' | 'year'>(
"month"
);
const [plans, setPlans] = useState<SubscriptionPlan[]>([]); const [plans, setPlans] = useState<SubscriptionPlan[]>([]);
// 从后端获取订阅计划数据 // 从后端获取订阅计划数据
@ -26,25 +38,23 @@ export default function PricingPage() {
loadPlans(); loadPlans();
}, []); }, []);
// 转换后端数据为前端显示格式,保持原有的数据结构 const pricingPlans = useMemo<{
const transformPlanForDisplay = (plan: SubscriptionPlan) => { title: string;
const monthlyPrice = plan.price_month / 100; // 后端存储的是分,转换为元 price: number;
const yearlyPrice = plan.price_year / 100; credits: string;
buttonText: string;
return { features: string[];
name: plan.name, }[]>(() => {
displayName: plan.display_name, return plans.map((plan) => {
price: { return {
month: monthlyPrice, title: plan.display_name || plan.name,
year: yearlyPrice price: billingType === "month" ? plan.price_month/100 : plan.price_year/100,
}, credits: plan.description,
description: plan.description, buttonText: plan.is_free ? "Try For Free" : "Subscribe Now",
features: plan.features || [], 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 }, [plans, billingType]);
};
};
const handleSubscribe = async (planName: string) => { const handleSubscribe = async (planName: string) => {
if (planName === 'hobby') { if (planName === 'hobby') {
@ -57,226 +67,145 @@ export default function PricingPage() {
// 从localStorage获取当前用户信息 // 从localStorage获取当前用户信息
const User = JSON.parse(localStorage.getItem("currentUser") || "{}"); const User = JSON.parse(localStorage.getItem("currentUser") || "{}");
if (!User.id) { if (!User.id) {
throw new Error("无法获取用户ID请重新登录"); throw new Error("无法获取用户ID请重新登录");
} }
// 1. 创建Checkout Session // 1. 创建Checkout Session
const result = await createCheckoutSession({ const result = await createCheckoutSession({
user_id: String(User.id), user_id: String(User.id),
plan_name: planName, plan_name: planName,
billing_cycle: billingCycle billing_cycle: billingType
}); });
if (!result.successful || !result.data) { if (!result.successful || !result.data) {
throw new Error("create checkout session failed"); throw new Error("create checkout session failed");
} }
// 2. 直接跳转到Stripe托管页面就这么简单 // 2. 直接跳转到Stripe托管页面就这么简单
redirectToCheckout(result.data.checkout_url); redirectToCheckout(result.data.checkout_url);
} catch (error) { } catch (error) {
throw new Error("create checkout session failed, please try again later"); throw new Error("create checkout session failed, please try again later");
} }
}; };
// 如果还没有加载到数据,显示加载状态但保持原有样式
if (plans.length === 0) {
return (
<div className="min-h-screen bg-black text-white">
<main className="container mx-auto px-6 py-16">
<div className="mb-8">
<Button
variant="ghost"
onClick={() => router.back()}
className="text-gray-400 hover:text-white"
>
<ArrowLeft className="mr-2 h-4 w-4" />
Back
</Button>
</div>
<div className="text-center mb-16">
<h1 className="text-6xl font-bold mb-6">Pricing</h1>
<p className="text-xl text-gray-400 mb-8">Choose the plan that suits you best</p>
<div className="inline-flex items-center bg-gray-900/50 rounded-full p-1">
<button
onClick={() => setBillingCycle('year')}
className={`flex items-center justify-center px-6 py-2.5 rounded-full transition-all duration-300 ease-out font-medium text-base ${
billingCycle === 'year'
? 'bg-gradient-to-r from-pink-500 to-purple-600 text-white'
: 'text-gray-300 hover:text-white'
}`}
>
Pay Yearly
<span className={`ml-2 text-sm ${
billingCycle === 'year'
? 'text-white/90'
: 'text-gray-400'
}`}>
Up to 20% off 🔥
</span>
</button>
<button
onClick={() => setBillingCycle('month')}
className={`px-6 py-2.5 rounded-full transition-all duration-300 ease-out font-medium text-base ${
billingCycle === 'month'
? 'bg-gradient-to-r from-pink-500 to-purple-600 text-white'
: 'text-gray-300 hover:text-white'
}`}
>
Pay Monthly
</button>
</div>
</div>
<div className="mb-16">
<div className="text-center text-gray-400">
<p>...</p>
</div>
</div>
</main>
</div>
);
}
return ( return (
<div className="min-h-screen bg-black text-white"> <div
{/* Main Content */} data-alt="core-value-section"
<main className="container mx-auto px-6 py-16"> className="home-module5 h-[1600px] relative flex flex-col items-center justify-center w-full bg-black snap-start"
{/* Back Button */} >
<div className="mb-8"> <div
<Button data-alt="core-value-content"
variant="ghost" className="center z-10 flex flex-col items-center mb-[8rem]"
onClick={() => router.back()} >
className="text-gray-400 hover:text-white" <h2 className="text-white text-[3.375rem] leading-[100%] font-normal mb-[1.5rem]">
Start Creating
</h2>
{/* 计费切换 */}
<div className="h-[3.375rem] flex bg-black rounded-full p-[0.0625rem] mt-[1.5rem] border border-white/20">
<button
onClick={() => setBillingType("month")}
className={`box-border flex justify-center items-center w-[6rem] text-base rounded-full transition-all duration-300 ${
billingType === "month"
? "bg-white text-black"
: "text-white hover:text-gray-300"
}`}
> >
<ArrowLeft className="mr-2 h-4 w-4" /> Monthly
Back </button>
</Button> <button
onClick={() => setBillingType("year")}
className={`box-border flex justify-center items-center w-[7.125rem] text-base rounded-full transition-all duration-300 ${
billingType === "year"
? "bg-white text-black"
: "text-white hover:text-gray-300"
}`}
>
Yearly - <span className="text-[#FFCC6D]">10%</span>
</button>
</div> </div>
</div>
<div className="text-center mb-16">
<h1 className="text-6xl font-bold mb-6">Pricing</h1> {/* 主要价格卡片 */}
<p className="text-xl text-gray-400 mb-8">Choose the plan that suits you best</p> <div className="flex justify-between w-[90%] px-[12rem] mb-[2rem]">
{pricingPlans.map((plan, index) => (
{/* Billing Toggle */} <div
<div className="inline-flex items-center bg-gray-900/50 rounded-full p-1"> key={index}
<button className=" w-[26rem] h-[38.125rem] bg-black rounded-lg p-[1.5rem] border border-white/20"
onClick={() => setBillingCycle('year')} >
className={`flex items-center justify-center px-6 py-2.5 rounded-full transition-all duration-300 ease-out font-medium text-base ${ <h3 className="text-white text-2xl font-normal mb-[1rem]">
billingCycle === 'year' {plan.title}
? 'bg-gradient-to-r from-pink-500 to-purple-600 text-white' </h3>
: 'text-gray-300 hover:text-white' <div className="mb-[1rem]">
}`} <span className="text-white text-[3.375rem] font-bold">
> ${plan.price}
Pay Yearly
<span className={`ml-2 text-sm ${
billingCycle === 'year'
? 'text-white/90'
: 'text-gray-400'
}`}>
Up to 20% off 🔥
</span> </span>
<span className="text-white text-xs ml-[0.5rem]">/month</span>
</div>
<p className="text-white text-[0.875rem] mb-[1rem]">
{plan.credits}
</p>
<button onClick={() => handleSubscribe(plan.title)} className="w-full bg-white text-black py-[0.75rem] rounded-full mb-[1rem] hover:bg-black hover:text-white transition-colors border border-white/20">
{plan.buttonText}
</button> </button>
<button <p className="w-full text-center text-white/60 text-[0.75rem] mb-[2rem]">
onClick={() => setBillingCycle('month')} * Billed monthly until cancelled
className={`px-6 py-2.5 rounded-full transition-all duration-300 ease-out font-medium text-base ${ </p>
billingCycle === 'month' <ul className="space-y-[1rem]">
? 'bg-gradient-to-r from-pink-500 to-purple-600 text-white' {plan.features.map((feature, featureIndex) => (
: 'text-gray-300 hover:text-white' <li
}`} key={featureIndex}
> className="flex items-center text-white text-[0.875rem]"
Pay Monthly >
</button> <span className="text-[#C73BFF] mr-[0.5rem]"></span>
{feature}
</li>
))}
</ul>
</div> </div>
))}
</div>
{/* 额外价格卡片 */}
<div className="flex gap-[1.5rem] w-[90%] px-[12rem]">
<div className="flex-1 bg-black rounded-lg p-[1.5rem] border border-white/20">
<h3 className="text-white text-[1.5rem] font-bold mb-[0.5rem]">
Free
</h3>
<div className="mb-[1rem]">
<span className="text-white text-[2.5rem] font-bold">$0</span>
</div>
<p className="text-white text-[0.875rem] mb-[1.5rem] leading-relaxed">
10 Video mins and 1 AI credit per week, 1 Express avatar, 4 Exports
per week with invideo watermark.
</p>
<p className="text-white text-[0.875rem] mb-[1.5rem] leading-relaxed">
No access to generative features.
</p>
<button className="w-[9rem] bg-[#262626] text-white py-[0.75rem] rounded-full hover:bg-white hover:text-black transition-colors border border-white/20">
Try For Free
</button>
</div> </div>
{/* Plans Section */} <div className="flex-1 bg-black rounded-lg p-[1.5rem] border border-white/20">
<div className="mb-16"> <h3 className="text-white text-[1.5rem] font-bold mb-[0.5rem]">
<div className="grid md:grid-cols-3 gap-8 max-w-7xl mx-auto"> Enterprise
{plans.map((plan) => { </h3>
const displayPlan = transformPlanForDisplay(plan); <p className="text-white text-[2.5rem] mb-[1rem]">Custom</p>
<p className="text-white text-[0.875rem] mb-[1.5rem] leading-relaxed">
return ( Custom solutions for large organizations. Advanced security and
<div flexible pricing based on your needs.
key={plan.id} </p>
className={`flex flex-col h-full transition-all duration-300 ${ <p className="text-white text-[0.875rem] mb-[1.5rem] leading-relaxed">
displayPlan.popular ? 'hover:scale-105 hover:shadow-2xl' : '' on your needs.
}`} </p>
style={{ minHeight: '540px' }} <button className="w-[9rem] bg-[#262626] text-white py-[0.75rem] rounded-full hover:bg-white hover:text-black transition-colors border border-white/20">
> Contact Us
{/* 预留标签空间,确保所有卡片组合高度一致 */} </button>
<div className="h-10 flex items-center justify-center">
{displayPlan.popular && (
<div className="bg-gradient-to-r from-pink-500 to-purple-600 text-white py-2 text-center text-sm font-medium rounded-t-2xl w-full shadow-lg">
Most Popular
</div>
)}
</div>
<Card
className={`bg-gray-900/50 ${displayPlan.popular ? 'border-l border-r border-b border-gray-700/50 rounded-b-2xl rounded-t-none' : 'border border-gray-700/50 rounded-2xl'} overflow-hidden ${displayPlan.popular ? '' : 'transition-all duration-300 hover:scale-105 hover:shadow-2xl'} flex flex-col flex-grow ${
displayPlan.popular ? 'ring-2 ring-pink-500/50 shadow-pink-500/20' : ''
}`}
>
<CardHeader className="pt-8 pb-6 flex-shrink-0 text-center">
<CardTitle className="text-white text-2xl font-bold mb-4">{displayPlan.displayName}</CardTitle>
<div className="mb-4">
<div className="text-4xl font-bold text-white">
{displayPlan.price[billingCycle] === 0 ? (
'Free'
) : (
<>
<span className="text-5xl">${displayPlan.price[billingCycle]}</span>
<span className="text-lg text-gray-400 font-normal">
/{billingCycle === 'month' ? 'month' : 'year'}
</span>
</>
)}
</div>
</div>
<CardDescription className="text-gray-300 text-base px-4">
{displayPlan.description}
</CardDescription>
</CardHeader>
<CardContent className="space-y-8 pb-10 flex-grow flex flex-col justify-between">
<div className="space-y-5">
{displayPlan.features.map((feature, index) => (
<div key={index} className="flex items-start space-x-3">
<div className="w-5 h-5 bg-green-500/20 rounded-full flex items-center justify-center flex-shrink-0 mt-0.5">
<Check className="w-3 h-3 text-green-400" />
</div>
<span className="text-gray-300 text-sm leading-relaxed">{feature}</span>
</div>
))}
</div>
<Button
onClick={() => handleSubscribe(displayPlan.name)}
variant={displayPlan.buttonVariant}
className={`w-full py-4 rounded-xl font-medium text-base transition-all duration-300 mt-6 ${
displayPlan.buttonText === 'Start Free Trial'
? 'border-2 border-gray-600 text-white hover:border-pink-500 hover:text-pink-400 hover:bg-gray-800/50'
: 'bg-gradient-to-r from-pink-500 to-purple-600 text-white hover:from-pink-600 hover:to-purple-700 shadow-lg hover:shadow-xl'
}`}
>
{displayPlan.buttonText}
</Button>
</CardContent>
</Card>
</div>
);
})}
</div>
</div> </div>
</main> </div>
</div> </div>
); );
} }

View File

@ -12,7 +12,7 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
const [sidebarCollapsed, setSidebarCollapsed] = useState(true); const [sidebarCollapsed, setSidebarCollapsed] = useState(true);
return ( return (
<div className=" min-h-screen bg-background" id="app"> <div className=" min-h-screen bg-background">
<Sidebar collapsed={sidebarCollapsed} onToggle={setSidebarCollapsed} /> <Sidebar collapsed={sidebarCollapsed} onToggle={setSidebarCollapsed} />
<TopBar collapsed={sidebarCollapsed} onToggleSidebar={() => setSidebarCollapsed(!sidebarCollapsed)} /> <TopBar collapsed={sidebarCollapsed} onToggleSidebar={() => setSidebarCollapsed(!sidebarCollapsed)} />
{children} {children}

View File

@ -1,9 +1,9 @@
"use client"; "use client";
import '../pages/style/top-bar.css'; import "../pages/style/top-bar.css";
import { Button } from '@/components/ui/button'; import { Button } from "@/components/ui/button";
import { GradientText } from '@/components/ui/gradient-text'; import { GradientText } from "@/components/ui/gradient-text";
import { useTheme } from 'next-themes'; import { useTheme } from "next-themes";
import { import {
Sun, Sun,
Moon, Moon,
@ -11,13 +11,12 @@ import {
Sparkles, Sparkles,
LogOut, LogOut,
PanelsLeftBottom, PanelsLeftBottom,
} from 'lucide-react'; } from "lucide-react";
import { motion } from 'framer-motion'; import { motion } from "framer-motion";
import ReactDOM from 'react-dom'; import ReactDOM from "react-dom";
import { useRouter } from 'next/navigation'; import { usePathname, useRouter } from "next/navigation";
import React, { useRef, useEffect, useLayoutEffect, useState } from 'react'; import React, { useRef, useEffect, useLayoutEffect, useState } from "react";
import { logoutUser } from '@/lib/auth'; import { logoutUser } from "@/lib/auth";
interface User { interface User {
id: string; id: string;
@ -26,14 +25,23 @@ interface User {
avatar: string; avatar: string;
} }
export function TopBar({ collapsed, onToggleSidebar }: { collapsed: boolean; onToggleSidebar: () => void }) { export function TopBar({
collapsed,
onToggleSidebar,
}: {
collapsed: boolean;
onToggleSidebar: () => void;
}) {
const router = useRouter(); const router = useRouter();
const [isOpen, setIsOpen] = React.useState(false); const [isOpen, setIsOpen] = React.useState(false);
const menuRef = useRef<HTMLDivElement>(null); const menuRef = useRef<HTMLDivElement>(null);
const buttonRef = useRef<HTMLButtonElement>(null); const buttonRef = useRef<HTMLButtonElement>(null);
const currentUser: User = JSON.parse(localStorage.getItem('currentUser') || '{}'); const currentUser: User = JSON.parse(
localStorage.getItem("currentUser") || "{}"
);
const [mounted, setMounted] = React.useState(false); const [mounted, setMounted] = React.useState(false);
const [isLogin, setIsLogin] = useState(false); const [isLogin, setIsLogin] = useState(false);
const pathname = usePathname()
useEffect(() => { useEffect(() => {
const currentUser = localStorage.getItem("currentUser"); const currentUser = localStorage.getItem("currentUser");
if (JSON.parse(currentUser || "{}")?.token) { if (JSON.parse(currentUser || "{}")?.token) {
@ -43,12 +51,11 @@ export function TopBar({ collapsed, onToggleSidebar }: { collapsed: boolean; onT
} }
}); });
useLayoutEffect(() => { useLayoutEffect(() => {
console.log('Setting mounted state'); console.log("Setting mounted state");
setMounted(true); setMounted(true);
return () => console.log('Cleanup mounted effect'); return () => console.log("Cleanup mounted effect");
}, []); }, []);
// 处理点击事件 // 处理点击事件
useEffect(() => { useEffect(() => {
if (!isOpen) return; if (!isOpen) return;
@ -58,16 +65,14 @@ export function TopBar({ collapsed, onToggleSidebar }: { collapsed: boolean; onT
const handleMouseDown = (event: MouseEvent) => { const handleMouseDown = (event: MouseEvent) => {
const target = event.target as Node; const target = event.target as Node;
isClickStartedInside = !!( isClickStartedInside = !!(
menuRef.current?.contains(target) || menuRef.current?.contains(target) || buttonRef.current?.contains(target)
buttonRef.current?.contains(target)
); );
}; };
const handleMouseUp = (event: MouseEvent) => { const handleMouseUp = (event: MouseEvent) => {
const target = event.target as Node; const target = event.target as Node;
const isClickEndedInside = !!( const isClickEndedInside = !!(
menuRef.current?.contains(target) || menuRef.current?.contains(target) || buttonRef.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("mousedown", handleMouseDown);
document.addEventListener('mouseup', handleMouseUp); document.addEventListener("mouseup", handleMouseUp);
return () => { return () => {
document.removeEventListener('mousedown', handleMouseDown); document.removeEventListener("mousedown", handleMouseDown);
document.removeEventListener('mouseup', handleMouseUp); document.removeEventListener("mouseup", handleMouseUp);
}; };
}, [isOpen]); }, [isOpen]);
@ -98,7 +103,10 @@ export function TopBar({ collapsed, onToggleSidebar }: { collapsed: boolean; onT
}; };
return ( return (
<div className="fixed right-0 top-0 left-0 h-16 header z-[999]" style={{ isolation: 'isolate' }}> <div
className="fixed right-0 top-0 left-0 h-16 header z-[999]"
style={{ isolation: "isolate" }}
>
<div className="h-full flex items-center justify-between pr-6 pl-6"> <div className="h-full flex items-center justify-between pr-6 pl-6">
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
{isLogin && ( {isLogin && (
@ -151,7 +159,10 @@ export function TopBar({ collapsed, onToggleSidebar }: { collapsed: boolean; onT
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
onClick={() => router.push('/pricing')} onClick={() => {
localStorage.setItem("callBackUrl", pathname);
router.push("/pricing");
}}
className="text-gray-300 hover:text-white" className="text-gray-300 hover:text-white"
> >
Pricing Pricing
@ -162,8 +173,8 @@ export function TopBar({ collapsed, onToggleSidebar }: { collapsed: boolean; onT
<Bell className="h-4 w-4" /> <Bell className="h-4 w-4" />
</Button> */} </Button> */}
{/* Theme Toggle */} {/* Theme Toggle */}
{/* <Button {/* <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')} onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
@ -173,13 +184,13 @@ export function TopBar({ collapsed, onToggleSidebar }: { collapsed: boolean; onT
</Button> */} </Button> */}
{/* User Menu */} {/* User Menu */}
<div className="relative" style={{ isolation: 'isolate' }}> <div className="relative" style={{ isolation: "isolate" }}>
<Button <Button
ref={buttonRef} ref={buttonRef}
variant="ghost" variant="ghost"
size="sm" size="sm"
onClick={() => { onClick={() => {
console.log('Button clicked, current isOpen:', isOpen); console.log("Button clicked, current isOpen:", isOpen);
setIsOpen(!isOpen); setIsOpen(!isOpen);
}} }}
data-alt="user-menu-trigger" data-alt="user-menu-trigger"
@ -187,65 +198,80 @@ export function TopBar({ collapsed, onToggleSidebar }: { collapsed: boolean; onT
<User className="h-4 w-4" /> <User className="h-4 w-4" />
</Button> </Button>
{mounted && isOpen ? ReactDOM.createPortal( {mounted && isOpen
<motion.div ? ReactDOM.createPortal(
ref={menuRef} <motion.div
initial={{ opacity: 0, scale: 0.95, y: -20 }} ref={menuRef}
animate={{ opacity: 1, scale: 1, y: 0 }} initial={{ opacity: 0, scale: 0.95, y: -20 }}
exit={{ opacity: 0, scale: 0.95, y: -20 }} animate={{ opacity: 1, scale: 1, y: 0 }}
transition={{ duration: 0.2 }} exit={{ opacity: 0, scale: 0.95, y: -20 }}
style={{ transition={{ duration: 0.2 }}
position: 'fixed', style={{
top: '4rem', position: "fixed",
right: '1rem', top: "4rem",
width: '18rem', right: "1rem",
zIndex: 9999 width: "18rem",
}} zIndex: 9999,
className="bg-[#1E1E1E] rounded-lg shadow-lg overflow-hidden" }}
data-alt="user-menu-dropdown" className="bg-[#1E1E1E] rounded-lg shadow-lg overflow-hidden"
onClick={(e) => e.stopPropagation()} data-alt="user-menu-dropdown"
> onClick={(e) => e.stopPropagation()}
{/* User Info */} >
<div className="p-4"> {/* User Info */}
<div className="flex items-center space-x-3"> <div className="p-4">
<div className="h-10 w-10 rounded-full bg-[#1E4D3E] flex items-center justify-center text-white font-semibold"> <div className="flex items-center space-x-3">
{currentUser.name ? currentUser.name.charAt(0) : ''} <div className="h-10 w-10 rounded-full bg-[#1E4D3E] flex items-center justify-center text-white font-semibold">
{currentUser.name ? currentUser.name.charAt(0) : ""}
</div>
<div className="flex-1">
<p className="text-sm font-medium">
{currentUser.name}
</p>
<p className="text-xs text-gray-500">
{currentUser.email}
</p>
</div>
<div
className="cursor-pointer hover:text-red-400 transition-colors duration-200"
onClick={() => {
logoutUser();
}}
title="退出登录"
>
<LogOut className="h-4 w-4" />
</div>
</div> </div>
<div className='flex-1'> </div>
<p className="text-sm font-medium">{currentUser.name}</p>
<p className="text-xs text-gray-500">{currentUser.email}</p> {/* AI Points */}
<div className="px-4 py-3 flex items-center justify-between">
<div className="flex items-center space-x-2">
<Sparkles className="h-4 w-4" />
<span className="text-white underline text-sm">
100 credits
</span>
</div> </div>
<div <Button
className='cursor-pointer hover:text-red-400 transition-colors duration-200' variant="outline"
onClick={() => { size="sm"
logoutUser(); className="text-white border-white hover:bg-white/10 rounded-full px-3 py-1 text-[10px]"
}} onClick={() => router.push("/pricing")}
title="退出登录"
> >
<LogOut className="h-4 w-4" /> Upgrade
</div> </Button>
<Button
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")}
>
Manage
</Button>
</div> </div>
</div>
{/* AI Points */} {/* Menu Items */}
<div className="px-4 py-3 flex items-center justify-between"> <div className="p-2">
<div className="flex items-center space-x-2"> {/* <motion.button
<Sparkles className="h-4 w-4" />
<span className="text-white underline text-sm">100 credits</span>
</div>
<Button
variant="outline"
size="sm"
className="text-white border-white hover:bg-white/10 rounded-full px-8"
onClick={() => router.push('/pricing')}
>
Upgrade
</Button>
</div>
{/* Menu Items */}
<div className="p-2">
{/* <motion.button
whileHover={{ backgroundColor: 'rgba(255,255,255,0.1)' }} whileHover={{ backgroundColor: 'rgba(255,255,255,0.1)' }}
className="w-full flex items-center space-x-2 px-3 py-2 rounded-md text-sm text-white" className="w-full flex items-center space-x-2 px-3 py-2 rounded-md text-sm text-white"
onClick={() => router.push('/my-library')} onClick={() => router.push('/my-library')}
@ -268,19 +294,19 @@ export function TopBar({ collapsed, onToggleSidebar }: { collapsed: boolean; onT
<span>Logout</span> <span>Logout</span>
</motion.button> */} </motion.button> */}
{/* Footer */} {/* Footer */}
<div className="mt-4 px-3 py-2 text-xs text-gray-400 text-center"> <div className="mt-4 px-3 py-2 text-xs text-gray-400 text-center">
<div>Privacy Policy · Terms of Service</div> <div>Privacy Policy · Terms of Service</div>
<div>250819215404 | 2025/8/20 06:00:50</div> <div>250819215404 | 2025/8/20 06:00:50</div>
</div>
</div> </div>
</div> </motion.div>,
</motion.div> document.body
, document.body) )
: null} : null}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
); );
} }

View File

@ -9,7 +9,7 @@ import {
CircleArrowRight, CircleArrowRight,
} from "lucide-react"; } from "lucide-react";
import "./style/home-page2.css"; import "./style/home-page2.css";
import { useRouter } from "next/navigation"; import { usePathname, useRouter } from "next/navigation";
import { Swiper, SwiperSlide } from "swiper/react"; import { Swiper, SwiperSlide } from "swiper/react";
import "swiper/swiper-bundle.css"; // 引入样式 import "swiper/swiper-bundle.css"; // 引入样式
import { Autoplay } from "swiper/modules"; import { Autoplay } from "swiper/modules";
@ -26,16 +26,18 @@ import { ProjectTypeEnum, ModeEnum, ResolutionEnum } from "@/app/model/enums";
import { getResourcesList, Resource } from "@/api/resources"; import { getResourcesList, Resource } from "@/api/resources";
import { Carousel } from "antd"; import { Carousel } from "antd";
import { TextCanvas } from "../common/TextCanvas"; import { TextCanvas } from "../common/TextCanvas";
import { fetchSubscriptionPlans, SubscriptionPlan } from "@/lib/stripe";
export function HomePage2() { export function HomePage2() {
const [hPading, setHPading] = useState(0); const [hPading, setHPading] = useState(0);
useEffect(() => { useEffect(() => {
setHPading((window as any).Scale.hScale * 11); // 为兼容不同屏幕的padding进行三次方处理
setHPading(((window as any).Scale?.hScale || 1)**3 * 10);
}, []); }, []);
return ( return (
// //
<div <div
className="w-full h-full overflow-y-auto" className="w-full h-screen overflow-y-auto"
style={{ paddingBottom: `${hPading}rem` }} style={{ paddingBottom: `${hPading}rem` }}
> >
<HomeModule1 /> <HomeModule1 />
@ -52,7 +54,7 @@ export function HomePage2() {
function HomeModule1() { function HomeModule1() {
const router = useRouter(); const router = useRouter();
return ( return (
<div className="home-module1 relative flex justify-center items-center w-full h-[1280px] bg-black snap-start"> <div className="home-module1 relative flex justify-center items-start pt-[14rem] w-full h-[1280px] bg-black snap-start">
<video <video
src="/assets/home.mp4" src="/assets/home.mp4"
autoPlay autoPlay
@ -62,7 +64,7 @@ function HomeModule1() {
className="absolute top-0 left-0 z-1 w-full h-full object-cover" className="absolute top-0 left-0 z-1 w-full h-full object-cover"
></video> ></video>
<div className="center z-10 flex flex-col items-center"> <div className="center z-10 flex flex-col items-center">
<h1 className="text-white text-[5.75rem] leading-[100%] font-bold mb-[5rem]"> <h1 className="text-white text-[5.75rem] leading-[100%] font-bold mb-[28rem]">
Ideas Become Movies Ideas Become Movies
</h1> </h1>
<p className="text-white text-[2rem] leading-[140%] font-normal"> <p className="text-white text-[2rem] leading-[140%] font-normal">
@ -84,6 +86,20 @@ function HomeModule1() {
} }
/**核心价值 */ /**核心价值 */
function HomeModule2() { 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 ( return (
<div <div
data-alt="core-value-section" data-alt="core-value-section"
@ -104,17 +120,30 @@ function HomeModule2() {
</p> </p>
</div> </div>
<div <div
data-alt="core-value-video" data-alt="core-value-videos"
className="relative w-[80rem] h-[45rem] flex justify-center items-center border border-white/20 rounded-lg overflow-hidden" className="flex justify-center gap-[1rem] w-full px-[4rem]"
> >
<video {/* 第一个视频 */}
src="/assets/1.mp4" {videoList.map((item, index) => (
autoPlay <div
loop data-alt="core-value-video-1"
muted className="flex flex-col items-center"
playsInline key={index}
className="w-full h-full object-cover" >
></video> <video
src={item.video}
autoPlay
loop
muted
playsInline
className=" h-[20rem] object-cover border border-white/20 rounded-lg"
/>
<h3 className="mt-[1rem] text-white text-[1.5rem] font-medium">
{item.title}
</h3>
</div>
))}
</div> </div>
</div> </div>
); );
@ -250,24 +279,28 @@ function HomeModule4() {
const processSteps = [ const processSteps = [
{ {
title: "Pre-Production", title: "The Narrative Engine",
description: 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: 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: 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: 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" data-alt="core-value-content"
className="center z-10 flex flex-col items-center mb-[14rem]" className="center z-10 flex flex-col items-center mb-[14rem]"
> >
<h2 className="text-white text-[3.375rem] leading-[100%] font-normal mb-[3rem]"> <h2 className="text-white text-[3.375rem] leading-[100%] font-normal ">
Edit like you think Edit like you think
</h2> </h2>
</div> </div>
@ -327,7 +360,7 @@ function HomeModule4() {
<div className="w-[80rem] h-[45rem] bg-gray-800 rounded-lg overflow-hidden border border-white/20"> <div className="w-[80rem] h-[45rem] bg-gray-800 rounded-lg overflow-hidden border border-white/20">
<video <video
key={activeTab} key={activeTab}
src="/assets/home.mp4" src={processSteps[activeTab].video}
autoPlay autoPlay
loop loop
muted muted
@ -342,73 +375,78 @@ function HomeModule4() {
} }
/**价格方案 */ /**价格方案 */
function HomeModule5() { function HomeModule5() {
const [billingType, setBillingType] = useState<"monthly" | "yearly">( const [billingType, setBillingType] = useState<'month' | 'year'>(
"monthly" "month"
); );
const pricingPlans = [ const [plans, setPlans] = useState<SubscriptionPlan[]>([]);
{ const pathname = usePathname()
title: "Plus", // 从后端获取订阅计划数据
price: billingType === "monthly" ? 28 : 24, useEffect(() => {
credits: "1x Boost, 10 Credits", const loadPlans = async () => {
buttonText: "Choose Plus", try {
features: [ const plansData = await fetchSubscriptionPlans();
"10 Credits", setPlans(plansData);
"50 Video mins + 95 iStock", } catch (err) {
"2 UGC product asset ads", console.error('加载订阅计划失败:', err);
"30 secs of generative video", }
"2 express clones", };
"3 users, 100GB storage",
"Unlimited exports",
],
},
{
title: "Max",
price: billingType === "monthly" ? 50 : 43,
credits: "1x Boost, 40 Credits",
buttonText: "Choose Max",
features: [
"40 Credits",
"50 Video mins + 95 iStock",
"2 UGC product asset ads",
"30 secs of generative video",
"2 express clones",
"3 users, 100GB storage",
"Unlimited exports",
],
},
{
title: "Generative",
price: billingType === "monthly" ? 100 : 85,
credits: "1x Boost, 100 Credits",
buttonText: "Choose Generative",
features: [
"100 Credits",
"50 Video mins + 95 iStock",
"2 UGC product asset ads",
"30 secs of generative video",
"2 express clones",
"3 users, 100GB storage",
"Unlimited exports",
],
},
{
title: "Team",
price: billingType === "monthly" ? 899 : 764,
credits: "1x Boost, 1000 Credits",
buttonText: "Choose Team",
features: [
"1000 Credits",
"50 Video mins + 95 iStock",
"2 UGC product asset ads",
"30 secs of generative video",
"2 express clones",
"3 users, 100GB storage",
"Unlimited exports",
],
},
];
loadPlans();
}, []);
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') {
return;
}
localStorage.setItem('callBackUrl', pathname)
try {
// 使用新的Checkout Session方案更简单
const { createCheckoutSession, redirectToCheckout } = await import('@/lib/stripe');
// 从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: 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");
}
};
return ( return (
<div <div
data-alt="core-value-section" data-alt="core-value-section"
@ -425,9 +463,9 @@ function HomeModule5() {
{/* 计费切换 */} {/* 计费切换 */}
<div className="h-[3.375rem] flex bg-black rounded-full p-[0.0625rem] mt-[1.5rem] border border-white/20"> <div className="h-[3.375rem] flex bg-black rounded-full p-[0.0625rem] mt-[1.5rem] border border-white/20">
<button <button
onClick={() => setBillingType("monthly")} onClick={() => setBillingType("month")}
className={`box-border flex justify-center items-center w-[6rem] text-base rounded-full transition-all duration-300 ${ className={`box-border flex justify-center items-center w-[6rem] text-base rounded-full transition-all duration-300 ${
billingType === "monthly" billingType === "month"
? "bg-white text-black" ? "bg-white text-black"
: "text-white hover:text-gray-300" : "text-white hover:text-gray-300"
}`} }`}
@ -435,24 +473,24 @@ function HomeModule5() {
Monthly Monthly
</button> </button>
<button <button
onClick={() => setBillingType("yearly")} onClick={() => setBillingType("year")}
className={`box-border flex justify-center items-center w-[7.125rem] text-base rounded-full transition-all duration-300 ${ className={`box-border flex justify-center items-center w-[7.125rem] text-base rounded-full transition-all duration-300 ${
billingType === "yearly" billingType === "year"
? "bg-white text-black" ? "bg-white text-black"
: "text-white hover:text-gray-300" : "text-white hover:text-gray-300"
}`} }`}
> >
Yearly - <span className="text-[#FFCC6D]">15%</span> Yearly - <span className="text-[#FFCC6D]">10%</span>
</button> </button>
</div> </div>
</div> </div>
{/* 主要价格卡片 */} {/* 主要价格卡片 */}
<div className="grid grid-cols-4 gap-[1.5rem] w-full px-[12rem] mb-[2rem]"> <div className="flex justify-between w-[90%] px-[12rem] mb-[2rem]">
{pricingPlans.map((plan, index) => ( {pricingPlans.map((plan, index) => (
<div <div
key={index} key={index}
className=" h-[38.125rem] bg-black rounded-lg p-[1.5rem] border border-white/20" className=" w-[24rem] h-[38.125rem] bg-black rounded-lg p-[1.5rem] border border-white/20"
> >
<h3 className="text-white text-2xl font-normal mb-[1rem]"> <h3 className="text-white text-2xl font-normal mb-[1rem]">
{plan.title} {plan.title}
@ -466,7 +504,7 @@ function HomeModule5() {
<p className="text-white text-[0.875rem] mb-[1rem]"> <p className="text-white text-[0.875rem] mb-[1rem]">
{plan.credits} {plan.credits}
</p> </p>
<button className="w-full bg-white text-black py-[0.75rem] rounded-full mb-[1rem] hover:bg-black hover:text-white transition-colors border border-white/20"> <button onClick={() => handleSubscribe(plan.title)} className="w-full bg-white text-black py-[0.75rem] rounded-full mb-[1rem] hover:bg-black hover:text-white transition-colors border border-white/20">
{plan.buttonText} {plan.buttonText}
</button> </button>
<p className="w-full text-center text-white/60 text-[0.75rem] mb-[2rem]"> <p className="w-full text-center text-white/60 text-[0.75rem] mb-[2rem]">
@ -488,7 +526,7 @@ function HomeModule5() {
</div> </div>
{/* 额外价格卡片 */} {/* 额外价格卡片 */}
<div className="flex gap-[1.5rem] w-full px-[12rem]"> <div className="flex gap-[1.5rem] w-[90%] px-[12rem]">
<div className="flex-1 bg-black rounded-lg p-[1.5rem] border border-white/20"> <div className="flex-1 bg-black rounded-lg p-[1.5rem] border border-white/20">
<h3 className="text-white text-[1.5rem] font-bold mb-[0.5rem]"> <h3 className="text-white text-[1.5rem] font-bold mb-[0.5rem]">
Free Free

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.