This commit is contained in:
Zixin Zhou 2025-08-28 17:06:13 +08:00
commit dc10dc6ed9
21 changed files with 1477 additions and 521 deletions

View File

@ -33,8 +33,10 @@ 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') {
@ -66,7 +76,7 @@ export default function PricingPage() {
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) {
@ -80,203 +90,122 @@ export default function PricingPage() {
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> <div className="flex justify-between w-[90%] px-[12rem] mb-[2rem]">
<p className="text-xl text-gray-400 mb-8">Choose the plan that suits you best</p> {pricingPlans.map((plan, index) => (
<div
{/* Billing Toggle */} key={index}
<div className="inline-flex items-center bg-gray-900/50 rounded-full p-1"> className=" w-[26rem] h-[38.125rem] bg-black rounded-lg p-[1.5rem] border border-white/20"
<button >
onClick={() => setBillingCycle('year')} <h3 className="text-white text-2xl font-normal mb-[1rem]">
className={`flex items-center justify-center px-6 py-2.5 rounded-full transition-all duration-300 ease-out font-medium text-base ${ {plan.title}
billingCycle === 'year' </h3>
? 'bg-gradient-to-r from-pink-500 to-purple-600 text-white' <div className="mb-[1rem]">
: 'text-gray-300 hover:text-white' <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>
</div>
</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' }}
> >
{/* 预留标签空间,确保所有卡片组合高度一致 */} <span className="text-[#C73BFF] mr-[0.5rem]"></span>
<div className="h-10 flex items-center justify-center"> {feature}
{displayPlan.popular && ( </li>
<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 </ul>
</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>
{/* 额外价格卡片 */}
<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>
</main>
<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>
</div>
</div> </div>
); );
} }

View File

@ -173,8 +173,8 @@ const RenderTemplateStoryMode = ({
// 模板列表渲染 // 模板列表渲染
const templateListRender = () => { const templateListRender = () => {
return ( return (
<div className="w-1/3 p-4 border-r border-white/[0.1]"> <div className="w-1/3 p-4 border-r border-white/[0.1] overflow-y-auto">
<div className="space-y-4 max-h-[700px] overflow-y-auto template-list-scroll"> <div className="space-y-4 overflow-y-auto template-list-scroll">
{templateStoryList.map((template, index) => ( {templateStoryList.map((template, index) => (
<div <div
key={template.id} key={template.id}
@ -562,7 +562,7 @@ const RenderTemplateStoryMode = ({
className="photo-story-modal !pb-0 rounded-lg bg-white/[0.08] backdrop-blur-[20px] [&_.ant-modal-content]:bg-white/[0.00]" className="photo-story-modal !pb-0 rounded-lg bg-white/[0.08] backdrop-blur-[20px] [&_.ant-modal-content]:bg-white/[0.00]"
> >
<GlobalLoad show={isLoading} progress={localLoading}> <GlobalLoad show={isLoading} progress={localLoading}>
<div className="rounded-2xl min-h-min transition-all duration-700 ease-out"> <div className="rounded-2xl h-[70vh] overflow-y-hidden flex flex-col">
{/* 弹窗头部 */} {/* 弹窗头部 */}
<div className="flex items-center justify-between px-4 pb-6 border-b border-white/[0.1]"> <div className="flex items-center justify-between px-4 pb-6 border-b border-white/[0.1]">
<h2 className="text-2xl font-bold text-white"> <h2 className="text-2xl font-bold text-white">
@ -576,7 +576,7 @@ const RenderTemplateStoryMode = ({
</button> </button>
</div> </div>
<div className="flex gap-4 pb-8 "> <div className="flex gap-4 pb-8 flex-1 overflow-y-hidden">
{templateListRender()} {templateListRender()}
<div className="flex-1">{storyEditorRender()}</div> <div className="flex-1">{storyEditorRender()}</div>
</div> </div>
@ -591,7 +591,7 @@ const RenderTemplateStoryMode = ({
* *
* / * /
*/ */
export function ChatInputBox() { export function ChatInputBox({ noData }: { noData: boolean }) {
// 控制面板展开/收起状态 // 控制面板展开/收起状态
const [isExpanded, setIsExpanded] = useState(false); const [isExpanded, setIsExpanded] = useState(false);
@ -670,28 +670,36 @@ export function ChatInputBox() {
}; };
return ( return (
<div className="video-tool-component relative max-w-[1080px] w-full left-[50%] translate-x-[-50%]"> <div className="video-tool-component relative max-w-[1080px] w-full left-[50%] translate-x-[-50%]" style={noData ? {
top: '50%'
} : {}}>
{/* 视频故事板工具面板 - 毛玻璃效果背景 */} {/* 视频故事板工具面板 - 毛玻璃效果背景 */}
<div className="video-storyboard-tools rounded-[20px] bg-white/[0.08] backdrop-blur-[20px] border border-white/[0.12] shadow-[0_8px_32px_rgba(0,0,0,0.3)]"> <div className="video-storyboard-tools rounded-[20px] bg-white/[0.08] backdrop-blur-[20px] border border-white/[0.12] shadow-[0_8px_32px_rgba(0,0,0,0.3)]">
{/* 展开/收起控制区域 */} {/* 展开/收起控制区域 */}
{isExpanded ? ( {
// 展开状态:显示收起按钮和提示 !noData && (
<div <>
className="absolute top-0 bottom-0 left-0 right-0 z-[1] flex flex-col items-center justify-center rounded-[20px] bg-[#191B1E] bg-opacity-[0.3] backdrop-blur-[1.5px] cursor-pointer" {isExpanded ? (
onClick={() => setIsExpanded(false)} // 展开状态:显示收起按钮和提示
> <div
<ChevronUp className="w-4 h-4 text-white/80" /> className="absolute top-0 bottom-0 left-0 right-0 z-[1] flex flex-col items-center justify-center rounded-[20px] bg-[#191B1E] bg-opacity-[0.3] backdrop-blur-[1.5px] cursor-pointer"
<span className="text-sm text-white/80 mt-1">Click to action</span> onClick={() => setIsExpanded(false)}
</div> >
) : ( <ChevronUp className="w-4 h-4 text-white/80" />
// 收起状态:显示展开按钮 <span className="text-sm text-white/80 mt-1">Click to action</span>
<div </div>
className="absolute top-[-8px] left-[50%] z-[2] flex items-center justify-center w-[46px] h-4 rounded-[15px] bg-[#fff3] translate-x-[-50%] cursor-pointer hover:bg-[#ffffff1a]" ) : (
onClick={() => setIsExpanded(true)} // 收起状态:显示展开按钮
> <div
<ChevronDown className="w-4 h-4" /> className="absolute top-[-8px] left-[50%] z-[2] flex items-center justify-center w-[46px] h-4 rounded-[15px] bg-[#fff3] translate-x-[-50%] cursor-pointer hover:bg-[#ffffff1a]"
</div> onClick={() => setIsExpanded(true)}
)} >
<ChevronDown className="w-4 h-4" />
</div>
)}
</>
)
}
{/* 主要内容区域 - 简化层级,垂直居中 */} {/* 主要内容区域 - 简化层级,垂直居中 */}
<div <div
@ -744,6 +752,9 @@ export function ChatInputBox() {
onChange={(e) => setScript(e.target.value)} onChange={(e) => setScript(e.target.value)}
placeholder="Describe the content you want to action..." placeholder="Describe the content you want to action..."
className="w-full pl-[10px] pr-[10px] py-[14px] rounded-[10px] leading-[20px] text-sm border-none bg-transparent text-white placeholder:text-white/[0.40] focus:outline-none resize-none min-h-[48px] max-h-[120px] overflow-y-auto" className="w-full pl-[10px] pr-[10px] py-[14px] rounded-[10px] leading-[20px] text-sm border-none bg-transparent text-white placeholder:text-white/[0.40] focus:outline-none resize-none min-h-[48px] max-h-[120px] overflow-y-auto"
style={noData ? {
minHeight: '128px'
} : {}}
rows={1} rows={1}
onInput={(e) => { onInput={(e) => {
const target = e.target as HTMLTextAreaElement; const target = e.target as HTMLTextAreaElement;

View File

@ -0,0 +1,183 @@
import { notification } from 'antd';
const darkGlassStyle = {
background: 'rgba(30, 32, 40, 0.95)',
backdropFilter: 'blur(10px)',
WebkitBackdropFilter: 'blur(10px)',
border: '1px solid rgba(255, 255, 255, 0.08)',
borderRadius: '8px',
boxShadow: '0 4px 16px rgba(0, 0, 0, 0.4)',
padding: '12px 16px',
};
/** 胶片容器样式 */
const filmStripContainerStyle = {
position: 'relative' as const,
width: '100%',
height: '80px',
marginBottom: '16px',
overflow: 'hidden',
};
/** 胶片样式 */
const filmStripStyle = {
display: 'flex',
alignItems: 'center',
animation: 'filmScroll 20s linear infinite',
};
/** 文字样式 */
const textStyle = {
fontSize: '13px',
color: 'rgba(255, 255, 255, 0.9)',
marginBottom: '12px',
};
/** 胶片帧组件 */
const FilmFrame = () => (
<div style={{ margin: '0 4px' }}>
<svg width="60" height="80" viewBox="0 0 60 80" fill="none" xmlns="http://www.w3.org/2000/svg">
{/* 胶片外框 */}
<rect x="0" y="0" width="60" height="80" fill="#1A1B1E" stroke="#F6B266" strokeWidth="1"/>
{/* 齿孔 */}
<circle cx="10" cy="5" r="3" fill="none" stroke="#F6B266" strokeWidth="1"/>
<circle cx="50" cy="5" r="3" fill="none" stroke="#F6B266" strokeWidth="1"/>
<circle cx="10" cy="75" r="3" fill="none" stroke="#F6B266" strokeWidth="1"/>
<circle cx="50" cy="75" r="3" fill="none" stroke="#F6B266" strokeWidth="1"/>
{/* 胶片画面区域 */}
<rect x="5" y="15" width="50" height="50" fill="#2A2B2E" stroke="#F6B266" strokeWidth="1"/>
</svg>
</div>
);
/** 放映机音效组件 */
const ProjectorSound = () => (
<audio
src="/assets/audio/projector.mp3"
autoPlay
loop
style={{ display: 'none' }}
/>
);
/**
*
* @param position -
* @param estimatedMinutes -
*/
export const showQueueNotification = (position: number, estimatedMinutes: number) => {
notification.open({
message: null,
description: (
<div data-alt="queue-notification" style={{ minWidth: '320px' }}>
{/* 胶片动画区域 */}
<div style={filmStripContainerStyle}>
<div style={filmStripStyle}>
{[...Array(6)].map((_, i) => (
<FilmFrame key={i} />
))}
</div>
<div style={filmStripStyle} className="film-strip-2">
{[...Array(6)].map((_, i) => (
<FilmFrame key={i} />
))}
</div>
</div>
{/* 队列信息 */}
<div style={textStyle}>
<span style={{ marginRight: '8px' }}>🎬</span>
{position}
</div>
{/* 预计等待时间 */}
<div style={{ ...textStyle, color: 'rgba(255, 255, 255, 0.65)' }}>
{estimatedMinutes}
</div>
{/* 取消按钮 */}
<button
onClick={() => notification.destroy()}
style={{
color: 'rgb(250 173 20 / 90%)',
background: 'transparent',
border: 'none',
cursor: 'pointer',
padding: 0,
fontSize: '12px',
fontWeight: 500,
textDecoration: 'underline',
textUnderlineOffset: '2px',
textDecorationColor: 'rgb(250 173 20 / 60%)',
transition: 'all 0.2s ease',
}}
data-alt="cancel-queue-button"
>
</button>
{/* 放映机音效 */}
<ProjectorSound />
</div>
),
duration: 0,
placement: 'topRight',
style: {
...darkGlassStyle,
border: '1px solid rgba(246, 178, 102, 0.2)',
},
className: 'movie-queue-notification',
closeIcon: (
<button
className="hover:text-white"
style={{
background: 'transparent',
border: 'none',
padding: '2px',
cursor: 'pointer',
color: 'rgba(255, 255, 255, 0.45)',
transition: 'color 0.2s ease',
}}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 6L6 18M6 6L18 18" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</button>
),
});
};
// 添加必要的CSS动画
const styles = `
@keyframes filmScroll {
0% { transform: translateX(0); }
100% { transform: translateX(-360px); }
}
.film-strip-2 {
position: absolute;
top: 0;
left: 360px;
}
.movie-queue-notification {
animation: slideIn 0.3s ease-out;
}
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
`;
// 将样式注入到页面
if (typeof document !== 'undefined') {
const styleSheet = document.createElement('style');
styleSheet.textContent = styles;
document.head.appendChild(styleSheet);
}
// 配置通知
notification.config({
maxCount: 3,
});

View File

@ -0,0 +1,238 @@
import { notification } from 'antd';
const darkGlassStyle = {
background: 'rgba(30, 32, 40, 0.95)',
backdropFilter: 'blur(10px)',
WebkitBackdropFilter: 'blur(10px)',
border: '1px solid rgba(255, 255, 255, 0.08)',
borderRadius: '8px',
boxShadow: '0 4px 16px rgba(0, 0, 0, 0.4)',
padding: '12px 16px',
};
/** AI导演工作室容器样式 */
const studioContainerStyle = {
position: 'relative' as const,
width: '100%',
height: '120px',
marginBottom: '16px',
background: 'rgba(26, 27, 30, 0.6)',
borderRadius: '8px',
overflow: 'hidden',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
};
/** AI导演组件 */
const AIDirector = () => (
<div className="ai-director">
<svg width="80" height="80" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
{/* AI导演的圆形头部 */}
<circle cx="50" cy="40" r="25" fill="#F6B266"/>
{/* 眼睛 */}
<circle cx="40" cy="35" r="5" fill="#2A2B2E"/>
<circle cx="60" cy="35" r="5" fill="#2A2B2E"/>
{/* 笑容 */}
<path d="M40 45 Q50 55 60 45" stroke="#2A2B2E" strokeWidth="3" strokeLinecap="round"/>
{/* 导演帽 */}
<path d="M25 30 H75 V25 H25" fill="#2A2B2E"/>
{/* 身体 */}
<rect x="35" y="65" width="30" height="25" fill="#F6B266"/>
{/* 手臂 - 动画中会移动 */}
<rect className="director-arm" x="25" y="70" width="15" height="5" fill="#F6B266"/>
<rect className="director-arm" x="60" y="70" width="15" height="5" fill="#F6B266"/>
</svg>
</div>
);
/** 工作进度条组件 */
const ProgressTimeline = () => (
<div className="progress-timeline" style={{
position: 'absolute',
bottom: '10px',
left: '20px',
right: '20px',
height: '4px',
background: 'rgba(255, 255, 255, 0.1)',
borderRadius: '2px',
}}>
<div className="progress-indicator" style={{
width: '30%',
height: '100%',
background: '#F6B266',
borderRadius: '2px',
animation: 'progress 2s ease-in-out infinite',
}}/>
</div>
);
/** 工作台元素组件 */
const Workstation = () => (
<div className="workstation" style={{
position: 'absolute',
bottom: '20px',
width: '100%',
display: 'flex',
justifyContent: 'space-around',
}}>
{/* 小型场景图标,会在动画中浮动 */}
{[...Array(3)].map((_, i) => (
<div key={i} className={`scene-icon scene-${i}`} style={{
width: '20px',
height: '20px',
background: 'rgba(246, 178, 102, 0.3)',
borderRadius: '4px',
animation: `float ${1 + i * 0.5}s ease-in-out infinite alternate`,
}}/>
))}
</div>
);
/**
*
* @param position -
* @param estimatedMinutes -
*/
export const showQueueNotification = (position: number, estimatedMinutes: number) => {
notification.open({
message: null,
description: (
<div data-alt="queue-notification" style={{ minWidth: '320px' }}>
{/* AI导演工作室场景 */}
<div style={studioContainerStyle}>
<AIDirector />
<Workstation />
<ProgressTimeline />
</div>
{/* 队列信息 */}
<div style={{
fontSize: '13px',
color: 'rgba(255, 255, 255, 0.9)',
marginBottom: '12px',
display: 'flex',
alignItems: 'center',
background: 'rgba(246, 178, 102, 0.1)',
padding: '8px 12px',
borderRadius: '6px',
}}>
<span style={{ marginRight: '8px' }}>🎬</span>
{position}
</div>
{/* 预计等待时间 */}
<div style={{
fontSize: '12px',
color: 'rgba(255, 255, 255, 0.65)',
marginBottom: '12px',
}}>
{estimatedMinutes}
</div>
{/* 取消按钮 */}
<button
onClick={() => notification.destroy()}
style={{
color: 'rgb(250 173 20 / 90%)',
background: 'transparent',
border: 'none',
cursor: 'pointer',
padding: 0,
fontSize: '12px',
fontWeight: 500,
textDecoration: 'underline',
textUnderlineOffset: '2px',
textDecorationColor: 'rgb(250 173 20 / 60%)',
transition: 'all 0.2s ease',
}}
data-alt="cancel-queue-button"
>
</button>
</div>
),
duration: 0,
placement: 'topRight',
style: {
...darkGlassStyle,
border: '1px solid rgba(246, 178, 102, 0.2)',
},
className: 'director-studio-notification',
closeIcon: (
<button
className="hover:text-white"
style={{
background: 'transparent',
border: 'none',
padding: '2px',
cursor: 'pointer',
color: 'rgba(255, 255, 255, 0.45)',
transition: 'color 0.2s ease',
}}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 6L6 18M6 6L18 18" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</button>
),
});
};
// 添加必要的CSS动画
const styles = `
.ai-director {
animation: bounce 2s ease-in-out infinite;
}
.director-arm {
transform-origin: center;
animation: wave 1s ease-in-out infinite alternate;
}
@keyframes bounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-5px); }
}
@keyframes wave {
0% { transform: rotate(-5deg); }
100% { transform: rotate(5deg); }
}
@keyframes float {
0% { transform: translateY(0); }
100% { transform: translateY(-10px); }
}
@keyframes progress {
0% { width: 0%; }
50% { width: 60%; }
100% { width: 30%; }
}
.director-studio-notification {
animation: slideIn 0.3s ease-out;
}
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
.scene-0 { animation-delay: 0s; }
.scene-1 { animation-delay: 0.2s; }
.scene-2 { animation-delay: 0.4s; }
`;
// 将样式注入到页面
if (typeof document !== 'undefined') {
const styleSheet = document.createElement('style');
styleSheet.textContent = styles;
document.head.appendChild(styleSheet);
}
// 配置通知
notification.config({
maxCount: 3,
});

View File

@ -0,0 +1,209 @@
import { notification } from 'antd';
import { useRouter } from 'next/router';
type NotificationType = 'success' | 'info' | 'warning' | 'error';
const darkGlassStyle = {
background: 'rgba(30, 32, 40, 0.95)',
backdropFilter: 'blur(10px)',
WebkitBackdropFilter: 'blur(10px)',
border: '1px solid rgba(255, 255, 255, 0.08)',
borderRadius: '8px',
boxShadow: '0 4px 16px rgba(0, 0, 0, 0.4)',
padding: '12px 16px',
};
const messageStyle = {
fontSize: '13px',
fontWeight: 500,
color: '#ffffff',
marginBottom: '6px',
display: 'flex',
alignItems: 'center',
gap: '6px',
};
const iconStyle = {
color: '#F6B266', // 警告图标颜色
background: 'rgba(246, 178, 102, 0.15)',
padding: '4px',
borderRadius: '6px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
};
const descriptionStyle = {
fontSize: '12px',
color: 'rgba(255, 255, 255, 0.65)',
marginBottom: '12px',
lineHeight: '1.5',
};
const btnStyle = {
color: 'rgb(250 173 20 / 90%)',
background: 'transparent',
border: 'none',
cursor: 'pointer',
padding: 0,
fontSize: '12px',
fontWeight: 500,
textDecoration: 'underline',
textUnderlineOffset: '2px',
textDecorationColor: 'rgb(250 173 20 / 60%)',
transition: 'all 0.2s ease',
};
/** 场记板动画样式 */
const clapperboardStyle = {
position: 'relative' as const,
width: '40px',
height: '40px',
marginRight: '12px',
animation: 'clap 2s infinite',
};
/** 场记板文字样式 */
const sceneTextStyle = {
fontSize: '14px',
fontFamily: 'monospace',
color: '#F6B266',
marginBottom: '8px',
letterSpacing: '0.5px',
};
/** 队列信息样式 */
const queueInfoStyle = {
display: 'flex',
alignItems: 'center',
fontSize: '13px',
color: 'rgba(255, 255, 255, 0.9)',
marginBottom: '12px',
background: 'rgba(246, 178, 102, 0.1)',
padding: '8px 12px',
borderRadius: '6px',
};
/**
* SVG组件
*/
const Clapperboard = () => (
<div style={clapperboardStyle}>
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 4H20V18C20 19.1046 19.1046 20 18 20H6C4.89543 20 4 19.1046 4 18V4Z"
stroke="#F6B266" strokeWidth="1.5"/>
<path d="M4 8H20" stroke="#F6B266" strokeWidth="1.5"/>
<path d="M9 4L11 8M15 4L17 8" stroke="#F6B266" strokeWidth="1.5"/>
<path className="clap-top" d="M4 4L20 4L17 8L4 8L4 4Z"
fill="rgba(246, 178, 102, 0.2)" stroke="#F6B266" strokeWidth="1.5"/>
</svg>
</div>
);
/**
*
* @param position -
* @param estimatedMinutes -
*/
export const showQueueNotification = (position: number, estimatedMinutes: number) => {
// 生成场景号和镜次号
const sceneNumber = Math.floor(Math.random() * 5) + 1;
const takeNumber = Math.floor(Math.random() * 3) + 1;
notification.open({
message: null,
description: (
<div data-alt="queue-notification" style={{ minWidth: '320px' }}>
{/* 场记板和场景信息 */}
<div style={{ display: 'flex', alignItems: 'center', marginBottom: '12px' }}>
<Clapperboard />
<div>
<div style={sceneTextStyle}>
Scene {sceneNumber} - Take {takeNumber}
</div>
<div style={{ fontSize: '12px', color: 'rgba(255, 255, 255, 0.6)' }}>
AI Director's Cut
</div>
</div>
</div>
{/* 队列信息 */}
<div style={queueInfoStyle}>
<span style={{ marginRight: '8px' }}>🎬</span>
{position}
</div>
{/* 预计等待时间 */}
<div style={descriptionStyle}>
{estimatedMinutes}
</div>
{/* 取消按钮 */}
<button
onClick={() => notification.destroy()}
style={{
...btnStyle,
marginTop: '8px',
}}
data-alt="cancel-queue-button"
>
</button>
</div>
),
duration: 0, // 保持通知直到用户关闭
placement: 'topRight',
style: {
...darkGlassStyle,
border: '1px solid rgba(246, 178, 102, 0.2)',
},
className: 'movie-queue-notification',
closeIcon: (
<button
className="hover:text-white"
style={{
background: 'transparent',
border: 'none',
padding: '2px',
cursor: 'pointer',
color: 'rgba(255, 255, 255, 0.45)',
transition: 'color 0.2s ease',
}}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 6L6 18M6 6L18 18" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</button>
),
});
};
// 添加必要的CSS动画
const styles = `
@keyframes clap {
0%, 100% { transform: rotate(0deg); }
5% { transform: rotate(-15deg); }
10% { transform: rotate(0deg); }
}
.movie-queue-notification {
animation: slideIn 0.3s ease-out;
}
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
`;
// 将样式注入到页面
if (typeof document !== 'undefined') {
const styleSheet = document.createElement('style');
styleSheet.textContent = styles;
document.head.appendChild(styleSheet);
}
// 保持现有的notification配置
notification.config({
maxCount: 3,
});

View File

@ -12,8 +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} />
<TopBar collapsed={sidebarCollapsed} onToggleSidebar={() => setSidebarCollapsed(!sidebarCollapsed)} /> <TopBar collapsed={sidebarCollapsed} onToggleSidebar={() => setSidebarCollapsed(!sidebarCollapsed)} />
{children} {children}
</div> </div>

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,14 +11,14 @@ import {
Sparkles, Sparkles,
LogOut, LogOut,
PanelsLeftBottom, PanelsLeftBottom,
Bell,
} from 'lucide-react'; } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion'; import { motion } from 'framer-motion';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { useRouter } from 'next/navigation'; import { useRouter, usePathname } 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';
import { createPortalSession, redirectToPortal } from '@/lib/stripe'; import { showQueueNotification } from '@/components/QueueBox/QueueNotification2';
interface User { interface User {
id: string; id: string;
@ -27,15 +27,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 [isManagingSubscription, setIsManagingSubscription] = 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) {
@ -45,40 +53,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");
}, []); }, []);
// 处理订阅管理
const handleManageSubscription = async () => {
if (!currentUser?.id) {
console.error('用户未登录');
return;
}
setIsManagingSubscription(true);
try {
const response = await createPortalSession({
user_id: String(currentUser.id),
return_url: window.location.origin + '/dashboard'
});
if (response.successful && response.data?.portal_url) {
redirectToPortal(response.data.portal_url);
} else {
console.error('创建订阅管理会话失败:', response.message);
alert('无法打开订阅管理页面,请稍后重试');
}
} catch (error) {
console.error('打开订阅管理页面失败:', error);
alert('无法打开订阅管理页面,请稍后重试');
} finally {
setIsManagingSubscription(false);
}
};
// 处理点击事件 // 处理点击事件
useEffect(() => { useEffect(() => {
if (!isOpen) return; if (!isOpen) return;
@ -88,16 +67,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)
); );
// 只有当点击开始和结束都在外部时才关闭 // 只有当点击开始和结束都在外部时才关闭
@ -108,12 +85,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]);
@ -128,21 +105,14 @@ 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 && (
<Button
className="button-NxtqWZ"
variant="ghost"
size="sm"
onClick={onToggleSidebar}
>
<PanelsLeftBottom className="h-4 w-4" />
</Button>
)}
<div <div
className={`flex items-center cursor-pointer space-x-4 link-logo roll event-on`} className={`flex items-center cursor-pointer space-x-1 link-logo roll event-on`}
onClick={() => router.push("/")} onClick={() => router.push("/")}
onMouseEnter={handleMouseEnter} onMouseEnter={handleMouseEnter}
onAnimationEnd={handleAnimationEnd} onAnimationEnd={handleAnimationEnd}
@ -181,19 +151,22 @@ 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
</Button> </Button>
{/* Notifications */} {/* Notifications */}
{/* <Button variant="ghost" size="sm" onClick={() => setOpenModal(true)}> {/* <Button variant="ghost" size="sm" onClick={() => showQueueNotification(3, 10)}>
<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')}
@ -203,13 +176,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"
@ -217,79 +190,80 @@ export function TopBar({ collapsed, onToggleSidebar }: { collapsed: boolean; onT
<User className="h-4 w-4" /> <User className="h-4 w-4" />
</Button> </Button>
{mounted && ( {mounted && isOpen
ReactDOM.createPortal( ? ReactDOM.createPortal(
(<AnimatePresence> <motion.div
{isOpen && ( 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"> </div>
{currentUser.name ? currentUser.name.charAt(0) : ''} <div className="flex-1">
</div> <p className="text-sm font-medium">
<div className='flex-1'> {currentUser.name}
<p className="text-sm font-medium">{currentUser.name}</p> </p>
<p className="text-xs text-gray-500">{currentUser.email}</p> <p className="text-xs text-gray-500">
</div> {currentUser.email}
<div </p>
className='cursor-pointer hover:text-red-400 transition-colors duration-200' </div>
onClick={() => { <div
logoutUser(); className="cursor-pointer hover:text-red-400 transition-colors duration-200"
}} onClick={() => {
title="退出登录" logoutUser();
> }}
<LogOut className="h-4 w-4" /> title="退出登录"
>
<LogOut className="h-4 w-4" />
</div>
</div> </div>
</div> </div>
</div>
{/* AI Points */} {/* AI Points */}
<div className="px-4 py-3 flex items-center justify-between"> <div className="px-4 py-3 flex items-center justify-between">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Sparkles className="h-4 w-4" /> <Sparkles className="h-4 w-4" />
<span className="text-white underline text-sm">100 credits</span> <span className="text-white underline text-sm">
</div> 100 credits
<div className="flex flex-col gap-2"> </span>
</div>
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
className="text-white border-white hover:bg-white/10 rounded-full px-8" className="text-white border-white hover:bg-white/10 rounded-full px-3 py-1 text-[10px]"
onClick={() => router.push('/pricing')} onClick={() => router.push("/pricing")}
> >
Upgrade Upgrade
</Button> </Button>
<Button <Button
variant="ghost" variant="outline"
size="sm" size="sm"
className="text-white hover:bg-white/10 rounded-full px-4 text-xs" className="text-white border-white hover:bg-white/10 rounded-full px-3 py-1 text-[10px]"
onClick={handleManageSubscription} onClick={() => router.push("/pricing")}
disabled={isManagingSubscription}
> >
{isManagingSubscription ? 'Loading...' : 'Manage Subscription'} Manage
</Button> </Button>
</div> </div>
</div>
{/* Menu Items */} {/* Menu Items */}
<div className="p-2"> <div className="p-2">
{/* <motion.button {/* <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')}
@ -312,22 +286,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
)} )
</AnimatePresence>) as React.ReactElement, : null}
document.body
) as React.ReactNode
)}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
); );
} }

View File

@ -8,6 +8,8 @@ import './style/create-to-video2.css';
import { createScriptEpisodeNew, getScriptEpisodeListNew, CreateScriptEpisodeRequest } from "@/api/script_episode"; import { createScriptEpisodeNew, getScriptEpisodeListNew, CreateScriptEpisodeRequest } from "@/api/script_episode";
import { EmptyStateAnimation } from '@/components/common/EmptyStateAnimation2'; import { EmptyStateAnimation } from '@/components/common/EmptyStateAnimation2';
import { ChatInputBox } from '@/components/ChatInputBox/ChatInputBox'; import { ChatInputBox } from '@/components/ChatInputBox/ChatInputBox';
import cover_image1 from '@/public/assets/cover_image1.jpg';
import { motion } from 'framer-motion';
// ideaText已迁移到ChatInputBox组件中 // ideaText已迁移到ChatInputBox组件中
@ -98,6 +100,139 @@ export default function CreateToVideo2() {
setIsClient(true); setIsClient(true);
}, []); }, []);
const StatusBadge = (status: string) => {
return (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4 }}
className="flex items-center gap-2 rounded-full
bg-white/10 border border-white/20
px-3 py-1 backdrop-blur-md shadow-[0_0_8px_rgba(255,255,255,0.3)]"
>
{/* 进行中 脉冲小圆点 */}
{status === 'pending' && (
<>
<motion.span
className="w-2 h-2 rounded-full bg-yellow-400 shadow-[0_0_8px_rgba(255,220,100,0.9)]"
animate={{ scale: [1, 1.4, 1] }}
transition={{ repeat: Infinity, duration: 1.5 }}
/>
{/* 状态文字 */}
<span className="text-xs tracking-widest text-yellow-300 font-medium drop-shadow-[0_0_6px_rgba(255,220,100,0.6)]">
PROCESSING
</span>
</>
)}
{/* 已完成 */}
{status === 'completed' && (
<>
<motion.span
className="w-2 h-2 rounded-full bg-green-400 shadow-[0_0_8px_rgba(0,255,120,0.9)]"
/>
<span className="text-xs tracking-widest text-green-300 font-medium drop-shadow-[0_0_6px_rgba(0,255,120,0.6)]">
COMPLETED
</span>
</>
)}
{/* 失败 */}
{status === 'failed' && (
<>
<motion.span
className="w-2 h-2 rounded-full bg-red-500 shadow-[0_0_8px_rgba(255,0,80,0.9)]"
/>
<span className="text-xs tracking-widest text-red-400 font-medium drop-shadow-[0_0_6px_rgba(255,0,80,0.6)]">
FAILED
</span>
</>
)}
</motion.div>
)
}
// 创建一个视频引用Map
const videoRefs = useRef<Map<string, HTMLVideoElement>>(new Map());
const handleMouseEnter = (projectId: string) => {
const videoElement = videoRefs.current.get(projectId);
if (videoElement) {
videoElement.play().catch(() => {
console.log('Video autoplay prevented');
});
}
};
const handleMouseLeave = (projectId: string) => {
const videoElement = videoRefs.current.get(projectId);
if (videoElement) {
videoElement.pause();
videoElement.currentTime = 0;
}
};
const setVideoRef = (projectId: string, element: HTMLVideoElement | null) => {
if (element) {
videoRefs.current.set(projectId, element);
} else {
videoRefs.current.delete(projectId);
}
};
const renderProjectCard = (project: any) => {
return (
<div
key={project.project_id}
className="group relative aspect-video bg-black/20 rounded-lg overflow-hidden cursor-pointer"
onClick={() => router.push(`/create/work-flow?episodeId=${project.project_id}`)}
onMouseEnter={() => handleMouseEnter(project.project_id)}
onMouseLeave={() => handleMouseLeave(project.project_id)}
data-alt="project-card"
>
{/* 视频/图片区域 */}
{project.final_video_url ? (
<video
ref={(el) => setVideoRef(project.project_id, el)}
src={project.final_video_url}
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
muted
loop
playsInline
preload="none"
poster={`${project.final_video_url}?vframe/jpg/offset/1`}
/>
) : (
<div
className="w-full h-full bg-cover bg-center group-hover:scale-105 transition-transform duration-500"
style={{
backgroundImage: `url(${cover_image1.src})`,
}}
/>
)}
{/* 渐变遮罩 */}
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-transparent to-transparent" />
{/* 状态标签 - 左上角 */}
<div className="absolute top-3 left-3">
{StatusBadge(project.status === 'COMPLETED' ? 'completed' : project.status === 'FAILED' ? 'failed' : 'pending')}
</div>
{/* 底部信息 */}
<div className="absolute bottom-0 left-0 right-0 p-4">
<div className="flex items-center justify-between">
<h2 className="text-lg font-medium text-white group-hover:text-purple-400 transition-colors duration-300 line-clamp-1">
{project.name || "未命名项目"}
</h2>
<button className="p-1.5 hover:bg-white/10 rounded-full transition-colors opacity-0 group-hover:opacity-100">
<MoreHorizontal className="w-4 h-4 text-white/80" />
</button>
</div>
</div>
</div>
);
};
// 渲染剧集卡片 // 渲染剧集卡片
const renderEpisodeCard = (episode: any) => { const renderEpisodeCard = (episode: any) => {
return ( return (
@ -190,12 +325,12 @@ export default function CreateToVideo2() {
return ( return (
<> <>
<div className="container mx-auto h-[calc(100vh-6rem)] flex flex-col absolute top-[4rem] left-0 right-0 px-[1rem]"> <div className="flex flex-col absolute top-[5rem] left-0 right-0 bottom-[1rem] px-6">
{/* 优化后的主要内容区域 */} {/* 优化后的主要内容区域 */}
<div className="flex-1 min-h-0"> <div className="flex-1 min-h-0">
<div <div
ref={scrollContainerRef} ref={scrollContainerRef}
className="h-full overflow-y-auto custom-scrollbar" className="h-full overflow-y-auto overflow-x-hidden custom-scrollbar"
style={{ style={{
scrollbarWidth: 'thin', scrollbarWidth: 'thin',
scrollbarColor: 'rgba(255,255,255,0.1) transparent' scrollbarColor: 'rgba(255,255,255,0.1) transparent'
@ -203,39 +338,51 @@ export default function CreateToVideo2() {
> >
{isLoading && episodeList.length === 0 ? ( {isLoading && episodeList.length === 0 ? (
/* 优化的加载状态 */ /* 优化的加载状态 */
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-6 pb-6"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 pb-6">
{[...Array(10)].map((_, index) => ( {[...Array(6)].map((_, index) => (
<div <div
key={index} key={index}
className="bg-white/[0.05] backdrop-blur-[20px] border border-white/[0.08] rounded-2xl overflow-hidden animate-pulse" className="group relative aspect-video bg-black/20 rounded-lg overflow-hidden animate-pulse"
> >
<div className="h-[200px] bg-gradient-to-br from-white/[0.08] to-white/[0.04]"> {/* 背景占位 */}
<div className="h-full bg-white/[0.06] animate-pulse"></div> <div className="w-full h-full bg-gradient-to-br from-white/[0.04] to-white/[0.02]" />
</div>
<div className="p-5"> {/* 渐变遮罩 */}
<div className="h-4 bg-white/[0.08] rounded-lg mb-3 animate-pulse"></div> <div className="absolute inset-0 bg-gradient-to-t from-black/80 via-transparent to-transparent" />
<div className="h-3 bg-white/[0.06] rounded-lg mb-4 w-3/4 animate-pulse"></div>
<div className="flex justify-between"> {/* 状态标签占位 */}
<div className="h-3 bg-white/[0.06] rounded-lg w-20 animate-pulse"></div> <div className="absolute top-3 left-3">
<div className="h-3 bg-white/[0.06] rounded-lg w-16 animate-pulse"></div> <div className="flex items-center gap-2 rounded-full bg-white/10 px-3 py-1">
<div className="w-2 h-2 rounded-full bg-white/20"></div>
<div className="w-16 h-3 bg-white/20 rounded-full"></div>
</div> </div>
</div> </div>
{/* 项目ID占位 */}
<div className="absolute top-3 right-3">
<div className="w-20 h-3 bg-white/10 rounded-full"></div>
</div>
{/* 底部信息占位 */}
<div className="absolute bottom-0 left-0 right-0 p-4">
<div className="w-2/3 h-5 bg-white/10 rounded-lg"></div>
</div>
</div> </div>
))} ))}
</div> </div>
) : episodeList.length > 0 ? ( ) : episodeList.length > 0 ? (
/* 优化的剧集网格 */ /* 优化的剧集网格 */
<div className="pb-8"> <div className="pb-8">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-6"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{episodeList.map(renderEpisodeCard)} {episodeList.map(renderProjectCard)}
</div> </div>
{/* 加载更多指示器 */} {/* 加载更多指示器 */}
{isLoadingMore && ( {isLoadingMore && (
<div className="flex justify-center py-12"> <div className="flex justify-center py-12">
<div className="flex items-center gap-3 px-6 py-3 bg-white/[0.05] backdrop-blur-[20px] border border-white/[0.08] rounded-xl"> <div className="flex items-center gap-3 px-6 py-3 bg-black/30 backdrop-blur-xl border border-white/10 rounded-full">
<Loader2 className="w-5 h-5 animate-spin text-blue-400" /> <Loader2 className="w-5 h-5 animate-spin text-purple-400" />
<span className="text-white/70 font-medium">Loading more episodes...</span> <span className="text-white/90 font-medium">...</span>
</div> </div>
</div> </div>
)} )}
@ -244,10 +391,10 @@ export default function CreateToVideo2() {
{!hasMore && episodeList.length > 0 && ( {!hasMore && episodeList.length > 0 && (
<div className="flex justify-center py-12"> <div className="flex justify-center py-12">
<div className="text-center"> <div className="text-center">
<div className="w-12 h-12 bg-white/[0.05] backdrop-blur-[20px] border border-white/[0.08] rounded-xl flex items-center justify-center mx-auto mb-3"> <div className="w-12 h-12 bg-black/30 backdrop-blur-xl border border-white/10 rounded-full flex items-center justify-center mx-auto mb-3">
<Check className="w-6 h-6 text-green-400" /> <Check className="w-6 h-6 text-purple-400" />
</div> </div>
<p className="text-white/50 text-sm">All episodes loaded</p> <p className="text-white/70 text-sm"></p>
</div> </div>
</div> </div>
)} )}
@ -260,13 +407,9 @@ export default function CreateToVideo2() {
</div> </div>
{/* 视频工具组件 - 使用独立组件 */} {/* 视频工具组件 - 使用独立组件 */}
<ChatInputBox /> {!isLoading &&
<ChatInputBox noData={episodeList.length === 0} />
{episodeList.length === 0 && !isLoading && ( }
<div className='h-full flex flex-col items-center fixed top-[4rem] left-0 right-0 bottom-0'>
<EmptyStateAnimation className='' />
</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,17 +26,47 @@ 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); // 获取当前窗口尺寸
const currentWidth = window.innerWidth;
const currentHeight = window.innerHeight;
// 计算缩放比例 (1920x1080)
const wScale = currentWidth / 1920;
const hScale = currentHeight / 1080;
// 检查app节点是否存在
const homePage = document.getElementById("home-page");
if (!homePage) {
console.error("未找到app节点");
return;
}
// setHPading((hScale || 1) * 10);
// 创建样式元素
const style = document.createElement("style");
// 设置CSS样式
style.textContent = `
#home-page {
transform-origin: top left;
transform: scale(${wScale}, ${hScale});
width: 1920px;
height: 1080px;
}
`;
// 将样式添加到head
document.head.appendChild(style);
}, []); }, []);
return ( return (
// //
<div <div
className="w-full h-full overflow-y-auto" className="w-full h-screen overflow-y-auto"
style={{ paddingBottom: `${hPading}rem` }} id="home-page"
style={{ paddingBottom: `2rem` }}
> >
<HomeModule1 /> <HomeModule1 />
<HomeModule2 /> <HomeModule2 />
@ -52,7 +82,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-[28rem] w-full h-[1280px] bg-black snap-start">
<video <video
src="/assets/home.mp4" src="/assets/home.mp4"
autoPlay autoPlay
@ -62,7 +92,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-[1rem]">
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 +114,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 +148,29 @@ 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>
); );
@ -168,15 +224,16 @@ function HomeModule3() {
data-alt="vertical-grid-shadow" data-alt="vertical-grid-shadow"
className="grid grid-cols-3 gap-[1rem] w-full h-[64rem] px-[5rem] relative" className="grid grid-cols-3 gap-[1rem] w-full h-[64rem] px-[5rem] relative"
> >
{/* 上方阴影遮罩 - 使用 mask 实现真正的渐变模糊,加重黑色 */} {/* 上方阴影遮罩 - 使用 mask 实现真正的渐变模糊,加重黑色 */}
<div <div
className="absolute -top-[1rem] -left-0 w-full h-[20rem] z-20 pointer-events-none" className="absolute -top-[1rem] -left-0 w-full h-[20rem] z-20 pointer-events-none"
style={{ style={{
backdropFilter: 'blur(12px)', backdropFilter: "blur(12px)",
WebkitBackdropFilter: 'blur(12px)', WebkitBackdropFilter: "blur(12px)",
backgroundColor: 'rgba(0,0,0,0.9)', backgroundColor: "rgba(0,0,0,0.9)",
mask: 'linear-gradient(to bottom, black 0%, black 30%, rgba(0,0,0,0.9) 50%, rgba(0,0,0,0.6) 75%, transparent 100%)', mask: "linear-gradient(to bottom, black 0%, black 30%, rgba(0,0,0,0.9) 50%, rgba(0,0,0,0.6) 75%, transparent 100%)",
WebkitMask: 'linear-gradient(to bottom, black 0%, black 30%, rgba(0,0,0,0.9) 50%, rgba(0,0,0,0.6) 75%, transparent 100%)' WebkitMask:
"linear-gradient(to bottom, black 0%, black 30%, rgba(0,0,0,0.9) 50%, rgba(0,0,0,0.6) 75%, transparent 100%)",
}} }}
></div> ></div>
@ -184,11 +241,12 @@ function HomeModule3() {
<div <div
className="absolute -bottom-[1rem] -left-0 w-full h-[20rem] z-20 pointer-events-none" className="absolute -bottom-[1rem] -left-0 w-full h-[20rem] z-20 pointer-events-none"
style={{ style={{
backdropFilter: 'blur(12px)', backdropFilter: "blur(12px)",
WebkitBackdropFilter: 'blur(12px)', WebkitBackdropFilter: "blur(12px)",
backgroundColor: 'rgba(0,0,0,0.9)', backgroundColor: "rgba(0,0,0,0.9)",
mask: 'linear-gradient(to top, black 0%, black 30%, rgba(0,0,0,0.9) 50%, rgba(0,0,0,0.6) 75%, transparent 100%)', mask: "linear-gradient(to top, black 0%, black 30%, rgba(0,0,0,0.9) 50%, rgba(0,0,0,0.6) 75%, transparent 100%)",
WebkitMask: 'linear-gradient(to top, black 0%, black 20%, rgba(0,0,0,0.9) 50%, rgba(0,0,0,0.6) 75%, transparent 100%)' WebkitMask:
"linear-gradient(to top, black 0%, black 20%, rgba(0,0,0,0.9) 50%, rgba(0,0,0,0.6) 75%, transparent 100%)",
}} }}
></div> ></div>
{videoList.map((column, columnIndex) => ( {videoList.map((column, columnIndex) => (
@ -250,24 +308,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 +346,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 +389,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 +404,82 @@ function HomeModule4() {
} }
/**价格方案 */ /**价格方案 */
function HomeModule5() { function HomeModule5() {
const [billingType, setBillingType] = useState<"monthly" | "yearly">( const [billingType, setBillingType] = useState<"month" | "year">("month");
"monthly"
);
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 +496,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 +506,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 +537,10 @@ 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 +562,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.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

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.