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>
<ScreenAdapter />
{children}
<div id="app" className='h-full w-full'>
{children}
</div>
</Providers>
</ConfigProvider>
</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';
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 (
<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[]>([]);
// 从后端获取订阅计划数据
@ -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 (
<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 (
<div className="min-h-screen bg-black text-white">
{/* Main Content */}
<main className="container mx-auto px-6 py-16">
{/* Back Button */}
<div className="mb-8">
<Button
variant="ghost"
onClick={() => router.back()}
className="text-gray-400 hover:text-white"
<div
data-alt="core-value-section"
className="home-module5 h-[1600px] relative flex flex-col items-center justify-center w-full bg-black snap-start"
>
<div
data-alt="core-value-content"
className="center z-10 flex flex-col items-center mb-[8rem]"
>
<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" />
Back
</Button>
Monthly
</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 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>
{/* Billing Toggle */}
<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 🔥
</div>
{/* 主要价格卡片 */}
<div className="flex justify-between w-[90%] px-[12rem] mb-[2rem]">
{pricingPlans.map((plan, index) => (
<div
key={index}
className=" w-[26rem] h-[38.125rem] bg-black rounded-lg p-[1.5rem] border border-white/20"
>
<h3 className="text-white text-2xl font-normal mb-[1rem]">
{plan.title}
</h3>
<div className="mb-[1rem]">
<span className="text-white text-[3.375rem] font-bold">
${plan.price}
</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
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>
<p className="w-full text-center text-white/60 text-[0.75rem] mb-[2rem]">
* Billed monthly until cancelled
</p>
<ul className="space-y-[1rem]">
{plan.features.map((feature, featureIndex) => (
<li
key={featureIndex}
className="flex items-center text-white text-[0.875rem]"
>
<span className="text-[#C73BFF] mr-[0.5rem]"></span>
{feature}
</li>
))}
</ul>
</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>
{/* Plans Section */}
<div className="mb-16">
<div className="grid md:grid-cols-3 gap-8 max-w-7xl mx-auto">
{plans.map((plan) => {
const displayPlan = transformPlanForDisplay(plan);
return (
<div
key={plan.id}
className={`flex flex-col h-full transition-all duration-300 ${
displayPlan.popular ? 'hover:scale-105 hover:shadow-2xl' : ''
}`}
style={{ minHeight: '540px' }}
>
{/* 预留标签空间,确保所有卡片组合高度一致 */}
<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 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]">
Enterprise
</h3>
<p className="text-white text-[2.5rem] mb-[1rem]">Custom</p>
<p className="text-white text-[0.875rem] mb-[1.5rem] leading-relaxed">
Custom solutions for large organizations. Advanced security and
flexible pricing based on your needs.
</p>
<p className="text-white text-[0.875rem] mb-[1.5rem] leading-relaxed">
on your needs.
</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">
Contact Us
</button>
</div>
</main>
</div>
</div>
);
}

View File

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

View File

@ -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<HTMLDivElement>(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 [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 (
<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="flex items-center space-x-4">
{isLogin && (
@ -151,7 +159,10 @@ export function TopBar({ collapsed, onToggleSidebar }: { collapsed: boolean; onT
<Button
variant="ghost"
size="sm"
onClick={() => router.push('/pricing')}
onClick={() => {
localStorage.setItem("callBackUrl", pathname);
router.push("/pricing");
}}
className="text-gray-300 hover:text-white"
>
Pricing
@ -162,8 +173,8 @@ export function TopBar({ collapsed, onToggleSidebar }: { collapsed: boolean; onT
<Bell className="h-4 w-4" />
</Button> */}
{/* Theme Toggle */}
{/* <Button
{/* Theme Toggle */}
{/* <Button
variant="ghost"
size="sm"
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
@ -173,13 +184,13 @@ export function TopBar({ collapsed, onToggleSidebar }: { collapsed: boolean; onT
</Button> */}
{/* User Menu */}
<div className="relative" style={{ isolation: 'isolate' }}>
<div className="relative" style={{ isolation: "isolate" }}>
<Button
ref={buttonRef}
variant="ghost"
size="sm"
onClick={() => {
console.log('Button clicked, current isOpen:', isOpen);
console.log("Button clicked, current isOpen:", isOpen);
setIsOpen(!isOpen);
}}
data-alt="user-menu-trigger"
@ -187,65 +198,80 @@ export function TopBar({ collapsed, onToggleSidebar }: { collapsed: boolean; onT
<User className="h-4 w-4" />
</Button>
{mounted && isOpen ? ReactDOM.createPortal(
<motion.div
ref={menuRef}
initial={{ opacity: 0, scale: 0.95, y: -20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: -20 }}
transition={{ duration: 0.2 }}
style={{
position: 'fixed',
top: '4rem',
right: '1rem',
width: '18rem',
zIndex: 9999
}}
className="bg-[#1E1E1E] rounded-lg shadow-lg overflow-hidden"
data-alt="user-menu-dropdown"
onClick={(e) => e.stopPropagation()}
>
{/* User Info */}
<div className="p-4">
<div className="flex items-center space-x-3">
<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) : ''}
{mounted && isOpen
? ReactDOM.createPortal(
<motion.div
ref={menuRef}
initial={{ opacity: 0, scale: 0.95, y: -20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: -20 }}
transition={{ duration: 0.2 }}
style={{
position: "fixed",
top: "4rem",
right: "1rem",
width: "18rem",
zIndex: 9999,
}}
className="bg-[#1E1E1E] rounded-lg shadow-lg overflow-hidden"
data-alt="user-menu-dropdown"
onClick={(e) => e.stopPropagation()}
>
{/* User Info */}
<div className="p-4">
<div className="flex items-center space-x-3">
<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 className='flex-1'>
<p className="text-sm font-medium">{currentUser.name}</p>
<p className="text-xs text-gray-500">{currentUser.email}</p>
</div>
{/* 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
className='cursor-pointer hover:text-red-400 transition-colors duration-200'
onClick={() => {
logoutUser();
}}
title="退出登录"
<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")}
>
<LogOut className="h-4 w-4" />
</div>
Upgrade
</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>
{/* 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>
<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
{/* Menu Items */}
<div className="p-2">
{/* <motion.button
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"
onClick={() => router.push('/my-library')}
@ -268,19 +294,19 @@ export function TopBar({ collapsed, onToggleSidebar }: { collapsed: boolean; onT
<span>Logout</span>
</motion.button> */}
{/* Footer */}
<div className="mt-4 px-3 py-2 text-xs text-gray-400 text-center">
<div>Privacy Policy · Terms of Service</div>
<div>250819215404 | 2025/8/20 06:00:50</div>
{/* Footer */}
<div className="mt-4 px-3 py-2 text-xs text-gray-400 text-center">
<div>Privacy Policy · Terms of Service</div>
<div>250819215404 | 2025/8/20 06:00:50</div>
</div>
</div>
</div>
</motion.div>
, document.body)
: null}
</motion.div>,
document.body
)
: null}
</div>
</div>
</div>
</div>
);
}

View File

@ -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 (
//
<div
className="w-full h-full overflow-y-auto"
className="w-full h-screen overflow-y-auto"
style={{ paddingBottom: `${hPading}rem` }}
>
<HomeModule1 />
@ -52,7 +54,7 @@ export function HomePage2() {
function HomeModule1() {
const router = useRouter();
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
src="/assets/home.mp4"
autoPlay
@ -62,7 +64,7 @@ function HomeModule1() {
className="absolute top-0 left-0 z-1 w-full h-full object-cover"
></video>
<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
</h1>
<p className="text-white text-[2rem] leading-[140%] font-normal">
@ -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 (
<div
data-alt="core-value-section"
@ -104,17 +120,30 @@ function HomeModule2() {
</p>
</div>
<div
data-alt="core-value-video"
className="relative w-[80rem] h-[45rem] flex justify-center items-center border border-white/20 rounded-lg overflow-hidden"
data-alt="core-value-videos"
className="flex justify-center gap-[1rem] w-full px-[4rem]"
>
<video
src="/assets/1.mp4"
autoPlay
loop
muted
playsInline
className="w-full h-full object-cover"
></video>
{/* 第一个视频 */}
{videoList.map((item, index) => (
<div
data-alt="core-value-video-1"
className="flex flex-col items-center"
key={index}
>
<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>
);
@ -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]"
>
<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
</h2>
</div>
@ -327,7 +360,7 @@ function HomeModule4() {
<div className="w-[80rem] h-[45rem] bg-gray-800 rounded-lg overflow-hidden border border-white/20">
<video
key={activeTab}
src="/assets/home.mp4"
src={processSteps[activeTab].video}
autoPlay
loop
muted
@ -342,73 +375,78 @@ function HomeModule4() {
}
/**价格方案 */
function HomeModule5() {
const [billingType, setBillingType] = useState<"monthly" | "yearly">(
"monthly"
const [billingType, setBillingType] = useState<'month' | 'year'>(
"month"
);
const pricingPlans = [
{
title: "Plus",
price: billingType === "monthly" ? 28 : 24,
credits: "1x Boost, 10 Credits",
buttonText: "Choose Plus",
features: [
"10 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: "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",
],
},
];
const [plans, setPlans] = useState<SubscriptionPlan[]>([]);
const pathname = usePathname()
// 从后端获取订阅计划数据
useEffect(() => {
const loadPlans = async () => {
try {
const plansData = await fetchSubscriptionPlans();
setPlans(plansData);
} catch (err) {
console.error('加载订阅计划失败:', err);
}
};
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 (
<div
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">
<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 ${
billingType === "monthly"
billingType === "month"
? "bg-white text-black"
: "text-white hover:text-gray-300"
}`}
@ -435,24 +473,24 @@ function HomeModule5() {
Monthly
</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 ${
billingType === "yearly"
billingType === "year"
? "bg-white text-black"
: "text-white hover:text-gray-300"
}`}
>
Yearly - <span className="text-[#FFCC6D]">15%</span>
Yearly - <span className="text-[#FFCC6D]">10%</span>
</button>
</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) => (
<div
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]">
{plan.title}
@ -466,7 +504,7 @@ function HomeModule5() {
<p className="text-white text-[0.875rem] mb-[1rem]">
{plan.credits}
</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}
</button>
<p className="w-full text-center text-white/60 text-[0.75rem] mb-[2rem]">
@ -488,7 +526,7 @@ function HomeModule5() {
</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">
<h3 className="text-white text-[1.5rem] font-bold mb-[0.5rem]">
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.