video-flow-b/components/pages/home-page2.tsx

717 lines
24 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useState, useRef, useEffect, useMemo } from "react";
import {
Table,
AlignHorizontalSpaceAround,
Loader2,
Clapperboard,
CircleArrowRight,
} from "lucide-react";
import "./style/home-page2.css";
import { usePathname, useRouter } from "next/navigation";
import { Swiper, SwiperSlide } from "swiper/react";
import "swiper/swiper-bundle.css"; // 引入样式
import { Autoplay } from "swiper/modules";
import { VideoCarouselLayout } from "@/components/video-carousel-layout";
import { VideoGridLayout } from "@/components/video-grid-layout";
import { motion, AnimatePresence } from "framer-motion";
import { LiquidButton } from "@/components/ui/liquid-glass-button";
import {
createScriptProject,
CreateScriptProjectRequest,
} from "@/api/script_project";
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";
import { useCallbackModal } from "@/app/layout";
/** 视频预加载系统 - 后台静默运行 */
function useVideoPreloader() {
/** 预加载单个视频 */
const preloadVideo = (src: string): Promise<void> => {
return new Promise((resolve) => {
const video = document.createElement("video");
video.muted = true;
video.preload = "auto";
// 设置超时,避免某个视频卡住整个预加载过程
const timeout = setTimeout(() => {
console.warn(`视频预加载超时: ${src}`);
resolve();
}, 10000); // 10秒超时
video.onloadeddata = () => {
clearTimeout(timeout);
resolve();
};
video.onerror = () => {
clearTimeout(timeout);
console.warn(`视频预加载失败: ${src}`);
resolve(); // 即使失败也继续,不影响其他视频
};
video.src = src;
video.load();
});
};
/** 预加载所有视频 */
const preloadAllVideos = async () => {
const allVideos = [
// HomeModule1 - 首屏视频(最高优先级)
"https://cdn.qikongjian.com/videos/home.mp4",
// HomeModule2 - 核心价值视频(高优先级)
"https://cdn.qikongjian.com/videos/module2 (1).mp4",
"https://cdn.qikongjian.com/videos/module2 (2).mp4",
"https://cdn.qikongjian.com/videos/module2 (3).mp4",
// HomeModule4 - 制作工序视频(中优先级)
"https://cdn.qikongjian.com/videos/module4 (3).mp4",
"https://cdn.qikongjian.com/videos/module4 (1).mp4",
"https://cdn.qikongjian.com/videos/module4 (4).mp4",
"https://cdn.qikongjian.com/videos/module4 (2).mp4",
// HomeModule3 - 案例展示视频(低优先级,数量多)
"https://cdn.qikongjian.com/videos/show (1).mp4",
"https://cdn.qikongjian.com/videos/show (2).mp4",
"https://cdn.qikongjian.com/videos/show (3).mp4",
"https://cdn.qikongjian.com/videos/show (4).mp4",
"https://cdn.qikongjian.com/videos/show (5).mp4",
"https://cdn.qikongjian.com/videos/show (6).mp4",
"https://cdn.qikongjian.com/videos/show (7).mp4",
"https://cdn.qikongjian.com/videos/show (8).mp4",
"https://cdn.qikongjian.com/videos/show (9).mp4",
"https://cdn.qikongjian.com/videos/show (10).mp4",
"https://cdn.qikongjian.com/videos/show (11).mp4",
"https://cdn.qikongjian.com/videos/show (12).mp4",
"https://cdn.qikongjian.com/videos/show (13).mp4",
"https://cdn.qikongjian.com/videos/show (14).mp4",
"https://cdn.qikongjian.com/videos/show (15).mp4",
];
try {
// 分阶段预加载:先加载关键视频,再加载其他视频
const criticalVideos = allVideos.slice(0, 8); // 前8个是关键视频
const otherVideos = allVideos.slice(8);
// 第一阶段:预加载关键视频
for (let i = 0; i < criticalVideos.length; i += 2) {
const batch = criticalVideos.slice(i, i + 2);
await Promise.all(batch.map(preloadVideo));
}
// 第二阶段:后台预加载其他视频
if (otherVideos.length > 0) {
// 使用 requestIdleCallback 在浏览器空闲时预加载
const preloadRemaining = () => {
let index = 0;
const processBatch = () => {
if (index >= otherVideos.length) return;
const batch = otherVideos.slice(
index,
Math.min(index + 3, otherVideos.length)
);
batch.forEach(preloadVideo);
index += batch.length;
if (index < otherVideos.length) {
requestIdleCallback(processBatch, { timeout: 1000 });
}
};
requestIdleCallback(processBatch, { timeout: 1000 });
};
// 如果浏览器不支持 requestIdleCallback使用 setTimeout
if (typeof requestIdleCallback !== "undefined") {
preloadRemaining();
} else {
setTimeout(() => {
for (let i = 0; i < otherVideos.length; i += 3) {
const batch = otherVideos.slice(i, i + 3);
batch.forEach(preloadVideo);
}
}, 100);
}
}
} catch (error) {
console.error("视频预加载过程中出现错误:", error);
}
};
useEffect(() => {
// 使用 requestIdleCallback 在浏览器空闲时开始预加载
// 如果浏览器不支持,则使用 setTimeout 延迟执行
if (typeof requestIdleCallback !== "undefined") {
requestIdleCallback(() => preloadAllVideos(), { timeout: 2000 });
} else {
setTimeout(() => preloadAllVideos(), 100);
}
}, []);
// 这个 hook 不需要返回任何值,它只是后台静默运行
return;
}
export function HomePage2() {
// 后台静默预加载视频,不显示任何加载界面
useVideoPreloader();
const [hPading, setHPading] = useState(0);
useEffect(() => {
// 获取当前窗口尺寸
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 (
//
<div
className="w-full h-screen overflow-y-auto"
id="home-page"
style={{ paddingBottom: `2rem` }}
>
<HomeModule1 />
<HomeModule2 />
<HomeModule3 />
<HomeModule4 />
<HomeModule5 />
<HomeModule6 />
</div>
);
}
/** 首屏 */
function HomeModule1() {
const router = useRouter();
return (
<div className="home-module1 relative flex justify-center items-start pt-[28rem] w-full h-[1280px] bg-black snap-start">
<video
src="https://cdn.qikongjian.com/1756474503099_cudgy8.mp4"
autoPlay
loop
muted
playsInline
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-[1rem]">
Ideas Spark Movies
</h1>
<p className="text-white text-[2rem] leading-[140%] font-normal">
One line, one filmyour story, your scene.
</p>
<p className="text-white text-[2rem] leading-[140%] font-normal">
Everyone is a movie master.
</p>
<div
className="w-[11.5rem] h-[3.75rem] mt-[4rem] text-base flex justify-center items-center font-normal border border-white rounded-full bg-white/30 cursor-pointer"
onClick={() => {
if (localStorage.getItem("token")) {
router.push("/create");
} else {
router.push("/login");
}
}}
>
Make a Movie
</div>
</div>
</div>
);
}
/**核心价值 */
function HomeModule2() {
const videoList = [
{
title: "Text to Movie",
video: "https://cdn.qikongjian.com/videos/module2 (1).mp4",
},
{
title: "Image to Movie",
video: "https://cdn.qikongjian.com/videos/module2 (2).mp4",
},
{
title: "Template to Movie",
video: "https://cdn.qikongjian.com/videos/module2 (3).mp4",
},
];
return (
<div
data-alt="core-value-section"
className="home-module2 relative flex flex-col items-center justify-center w-full h-[1300px] 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-[3rem]">
Just Drop A Thought
</h2>
<p className="text-white text-[1.125rem] leading-[140%] font-normal text-center">
Say your idea in a single line,and MovieFlow will bring it to life.
</p>
</div>
<div
data-alt="core-value-videos"
className="flex justify-center gap-[1rem] w-full px-[4rem]"
>
{/* 第一个视频 */}
{videoList.map((item, index) => (
<div
data-alt="core-value-video-1"
className="flex flex-col items-center"
key={index}
>
<video
src={item.video}
loop
muted
playsInline
onMouseEnter={(e) => {
const videoElement = e.currentTarget;
videoElement.play();
}}
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>
);
}
/**案例展示 */
function HomeModule3() {
const videoList = [
[
"https://cdn.qikongjian.com/1756474023656_60twk5.mp4",
"https://cdn.qikongjian.com/1756474023644_14n7is.mp4",
"https://cdn.qikongjian.com/1756474023648_kocq6z.mp4",
"https://cdn.qikongjian.com/1756474023657_w10boo.mp4",
"https://cdn.qikongjian.com/1756474023657_nf8799.mp4",
"https://cdn.qikongjian.com/1756474230992_vw0ubf.mp4",
],
[
"https://cdn.qikongjian.com/1756474023655_pov4c3.mp4",
"https://cdn.qikongjian.com/1756474023663_yohi7a.mp4",
"https://cdn.qikongjian.com/1756474023661_348dx3.mp4",
"https://cdn.qikongjian.com/1756474023683_xlb34s.mp4",
"https://cdn.qikongjian.com/1756474023683_xlb34s.mp4",
"https://cdn.qikongjian.com/1756474230987_63ooji.mp4",
],
[
"https://cdn.qikongjian.com/1756474230997_zysje8.mp4",
"https://cdn.qikongjian.com/1756474230988_tgqzln.mp4",
"https://cdn.qikongjian.com/1756474231007_qneeia.mp4",
"https://cdn.qikongjian.com/1756474231008_qyqtka.mp4",
"https://cdn.qikongjian.com/1756474231009_vs49d9.mp4",
"https://cdn.qikongjian.com/1756474231010_2a48p0.mp4",
],
];
// 定义内容样式
const contentStyle: React.CSSProperties = {
height: "100%",
lineHeight: "10rem",
textAlign: "center",
background: "#364d79",
};
return (
<div className="home-module3 h-[1360px] relative flex flex-col items-center justify-center w-full bg-black snap-start">
<div className="center z-10 flex flex-col items-center mb-[4rem]">
<h2 className="text-white text-[3.375rem] leading-[100%] font-normal mb-[3rem]">
Ideas Made Real
</h2>
<p className="text-white text-[1.125rem] leading-[140%] font-normal text-center">
High-quality films, any style, made with MovieFlow.
</p>
</div>
{/* 3x3网格布局 */}
<div
data-alt="vertical-grid-shadow"
className="grid grid-cols-3 gap-[1rem] w-full h-[64rem] px-[5rem] relative"
>
{/* 上方阴影遮罩 - 使用 mask 实现真正的渐变模糊,加重黑色 */}
<div
className="absolute -top-[1rem] -left-0 w-full h-[20rem] z-20 pointer-events-none"
style={{
backdropFilter: "blur(12px)",
WebkitBackdropFilter: "blur(12px)",
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%)",
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>
{/* 下方阴影遮罩 - 使用 mask 实现真正的渐变模糊,加重黑色 */}
<div
className="absolute -bottom-[1rem] -left-0 w-full h-[20rem] z-20 pointer-events-none"
style={{
backdropFilter: "blur(12px)",
WebkitBackdropFilter: "blur(12px)",
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%)",
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>
{videoList.map((column, columnIndex) => (
<div
key={columnIndex}
className="w-[35.5rem] h-[64rem] relative z-10"
>
<Swiper
modules={[Autoplay]}
direction="vertical"
loop={true}
spaceBetween={0}
slidesPerView={3}
autoplay={{
delay: 0,
pauseOnMouseEnter: true,
disableOnInteraction: false,
reverseDirection: columnIndex % 2 === 0,
}}
speed={6000}
grabCursor={true}
className="w-full h-full"
cssMode={false}
freeMode={true}
watchSlidesProgress={false}
style={
{
"--swiper-wrapper-transition-timing-function": "linear",
} as React.CSSProperties
}
>
{column.map((video, videoIndex) => (
<SwiperSlide key={videoIndex}>
<div className="w-full h-[20rem] bg-gray-800 rounded-2xl overflow-hidden">
<video
src={video}
loop
muted
playsInline
className="w-full h-full object-cover"
onMouseEnter={(e) => {
const videoElement = e.currentTarget;
videoElement.play();
}}
/>
</div>
</SwiperSlide>
))}
</Swiper>
</div>
))}
</div>
</div>
);
}
/**电影制作工序介绍 */
function HomeModule4() {
const [activeTab, setActiveTab] = useState(0);
const processSteps = [
{
title: " The Story Agent",
description:
" From a single thought, it builds entire worlds and compelling plots.",
video: "https://cdn.qikongjian.com/videos/module4 (3).mp4",
},
{
title: " AI Character Agent",
description:
"Cast your virtual actors. Lock them in once, for the entire story.",
video: "https://cdn.qikongjian.com/videos/module4 (1).mp4",
},
{
title: " The Shot Agent",
description:
"It translates your aesthetic into art, light, and cinematography for every single shot.",
video: "https://cdn.qikongjian.com/videos/module4 (4).mp4",
},
{
title: " Intelligent Clip Agent",
description:
"An editing AI drives the final cut, for a story told seamlessly.",
video: "https://cdn.qikongjian.com/videos/module4 (2).mp4",
},
];
const handleTabClick = (index: number) => {
setActiveTab(index);
};
return (
<div
data-alt="core-value-section"
className="home-module4 h-[1280px] 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-[14rem]"
>
<h2 className="text-white text-[3.375rem] leading-[100%] font-normal ">
Create Your Way
</h2>
</div>
<div className="flex w-full px-[4rem] gap-[2rem]">
{/* 左侧四个切换tab */}
<div className="flex flex-col gap-[1rem]">
{processSteps.map((step, index) => (
<div
key={index}
onClick={() => handleTabClick(index)}
className={`w-[31.75rem] h-[10.5rem] rounded-lg cursor-pointer transition-all duration-300 border ${
activeTab === index
? "bg-[#262626] border-white/20 hover:border-white/40"
: "bg-black border-white/10 hover:border-white/40"
}`}
>
<div className="p-[1.5rem] h-full flex flex-col justify-center">
<h3
className={`text-[2rem] font-normal mb-[1.5rem] ${
activeTab === index ? "text-white" : "text-white/70"
}`}
>
{step.title}
</h3>
<p
className={`text-[0.875rem] leading-[140%] ${
activeTab === index ? "text-white" : "text-white/70"
}`}
>
{step.description}
</p>
</div>
</div>
))}
</div>
{/* 右侧视频播放区域 */}
<div className="flex-1 flex justify-center">
<div className="w-[80rem] h-[45rem] bg-gray-800 rounded-lg overflow-hidden border border-white/20">
<video
key={activeTab}
src={processSteps[activeTab].video}
autoPlay
loop
muted
playsInline
className="w-full h-full object-cover"
/>
</div>
</div>
</div>
</div>
);
}
/**价格方案 */
function HomeModule5() {
const [billingType, setBillingType] = useState<"month" | "year">("month");
const [plans, setPlans] = useState<SubscriptionPlan[]>([]);
const { setShowCallbackModal } = useCallbackModal();
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");
}
setShowCallbackModal(true);
window.open(result.data.checkout_url, "_blank");
} catch (error) {
throw new Error("create checkout session failed, please try again later");
}
};
return (
<div
data-alt="core-value-section"
className="home-module5 h-[1300px] 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-[4rem]"
>
<h2 className="text-white text-[3.375rem] leading-[100%] font-normal mb-[1.5rem]">
Pick a plan and make it yours
</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"
}`}
>
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]">20%</span>
</button>
</div>
</div>
{/* 主要价格卡片 */}
<div className="flex justify-between w-[90%] px-[12rem] mb-[2rem]">
{pricingPlans.map((plan, index) => (
<div
key={index}
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}
</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]">
/{billingType === "month" ? "mo" : "year"}
</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>
<p className="w-full text-center text-white/60 text-[0.75rem] mb-[2rem]">
* Billed monthly until cancelled
</p>
<ul className="space-y-[1rem]">
{plan.features.map((feature, featureIndex) => (
<li
key={featureIndex}
className="flex items-center text-white text-[0.875rem]"
>
<span className="text-[#C73BFF] mr-[0.5rem]"></span>
{feature}
</li>
))}
</ul>
</div>
))}
</div>
</div>
);
}
function HomeModule6() {
return (
<div className="home-module6 flex justify-center items-center w-full h-min text-white/50 text-lg bg-black snap-start">
© 2025 MovieFlow. All rights reserved.
</div>
);
}