forked from 77media/video-flow
推进
This commit is contained in:
parent
0032ff1aa9
commit
bc2302d907
@ -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
197
app/payCallback/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@ -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') {
|
||||
@ -66,7 +76,7 @@ export default function PricingPage() {
|
||||
const result = await createCheckoutSession({
|
||||
user_id: String(User.id),
|
||||
plan_name: planName,
|
||||
billing_cycle: billingCycle
|
||||
billing_cycle: billingType
|
||||
});
|
||||
|
||||
if (!result.successful || !result.data) {
|
||||
@ -80,203 +90,122 @@ export default function PricingPage() {
|
||||
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>
|
||||
|
||||
<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 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>
|
||||
</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' }}
|
||||
<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]"
|
||||
>
|
||||
{/* 预留标签空间,确保所有卡片组合高度一致 */}
|
||||
<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>
|
||||
);
|
||||
})}
|
||||
<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>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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.
BIN
public/assets/module2 (1).mp4
Normal file
BIN
public/assets/module2 (1).mp4
Normal file
Binary file not shown.
BIN
public/assets/module2 (2).mp4
Normal file
BIN
public/assets/module2 (2).mp4
Normal file
Binary file not shown.
BIN
public/assets/module2 (3).mp4
Normal file
BIN
public/assets/module2 (3).mp4
Normal file
Binary file not shown.
BIN
public/assets/module4 (1).mp4
Normal file
BIN
public/assets/module4 (1).mp4
Normal file
Binary file not shown.
BIN
public/assets/module4 (2).mp4
Normal file
BIN
public/assets/module4 (2).mp4
Normal file
Binary file not shown.
BIN
public/assets/module4 (3).mp4
Normal file
BIN
public/assets/module4 (3).mp4
Normal file
Binary file not shown.
BIN
public/assets/module4 (4).mp4
Normal file
BIN
public/assets/module4 (4).mp4
Normal file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user