第三版-登录页

This commit is contained in:
北枳 2025-06-28 10:09:28 +08:00
parent d9ed81ca3b
commit 2dc9a34241
21 changed files with 2235 additions and 132 deletions

5
app/login/page.tsx Normal file
View File

@ -0,0 +1,5 @@
import Login from "@/components/pages/login";
export default function LoginPage() {
return <Login />;
}

View File

@ -1,6 +1,7 @@
import { useState, useRef, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Sparkles, Send, X, Lightbulb } from 'lucide-react';
import { Sparkles, Send, X, Lightbulb, ChevronUp } from 'lucide-react';
import { GlassIconButton } from "@/components/ui/glass-icon-button";
interface AISuggestionBarProps {
suggestions: string[];
@ -18,6 +19,7 @@ export function AISuggestionBar({
const [inputText, setInputText] = useState('');
const [isFocused, setIsFocused] = useState(false);
const [showSuggestions, setShowSuggestions] = useState(false);
const [isCollapsed, setIsCollapsed] = useState(false);
const inputRef = useRef<HTMLTextAreaElement>(null);
// 自动调整输入框高度
@ -47,37 +49,99 @@ export function AISuggestionBar({
}
};
// 切换折叠状态
const toggleCollapse = () => {
setIsCollapsed(!isCollapsed);
if (isCollapsed) {
// 展开时自动显示建议
setShowSuggestions(true);
}
};
return (
<motion.div
initial={{ y: 100, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
animate={{
y: isCollapsed ? 'calc(100% - 10px)' : 0,
opacity: 1,
transition: {
type: "spring",
stiffness: 300,
damping: 30
}
}}
className="fixed bottom-0 left-0 right-0 z-50 bg-gradient-to-t from-[#0C0E11] via-[#0C0E11] to-transparent pb-8"
>
{/* 折叠/展开按钮 */}
<div className="absolute -top-[1rem] left-1/2 -translate-x-1/2" style={{ zIndex: 9 }}>
<motion.div
animate={{ rotate: isCollapsed ? 180 : 0 }}
transition={{ type: "spring", stiffness: 200, damping: 20 }}
>
<GlassIconButton
icon={ChevronUp}
size='sm'
tooltip={isCollapsed ? "展开" : "收起"}
onClick={toggleCollapse}
/>
</motion.div>
</div>
<div className="max-w-5xl mx-auto px-6">
{/* 智能预设词条 */}
<AnimatePresence>
{showSuggestions && (
{showSuggestions && !isCollapsed && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
className="mb-4 overflow-hidden bg-black/30"
transition={{
height: { type: "spring", stiffness: 300, damping: 30 },
opacity: { duration: 0.2 }
}}
className="mb-4 pt-4 px-4 overflow-hidden bg-black/40 rounded-xl backdrop-blur-sm"
>
<motion.div
className="flex items-center gap-3 mb-3"
initial={{ x: -20, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
transition={{ delay: 0.1 }}
>
<motion.div
animate={{
rotate: [0, 15, -15, 0],
scale: [1, 1.2, 1.2, 1]
}}
transition={{
duration: 1,
repeat: Infinity,
repeatDelay: 2
}}
>
<div className="flex items-center gap-3 mb-3">
<Lightbulb className="w-4 h-4 text-yellow-500" />
</motion.div>
<span className="text-sm text-white/60"></span>
</div>
</motion.div>
<div className="flex flex-wrap gap-2">
{suggestions.map((suggestion, index) => (
<motion.button
key={suggestion}
initial={{ opacity: 0, scale: 0.8 }}
initial={{ opacity: 0, scale: 0.8, y: 20 }}
animate={{
opacity: 1,
scale: 1,
transition: { delay: index * 0.1 }
y: 0,
transition: {
delay: index * 0.1,
type: "spring",
stiffness: 400,
damping: 25
}
}}
whileHover={{
scale: 1.05,
backgroundColor: "rgba(255, 255, 255, 0.15)"
}}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="px-3 py-1.5 rounded-full bg-white/5 hover:bg-white/10 backdrop-blur-sm
text-sm text-white/70 hover:text-white transition-colors flex items-center gap-2"
@ -93,10 +157,15 @@ export function AISuggestionBar({
</AnimatePresence>
{/* 输入区域 */}
<div className={`
<motion.div
className={`
relative rounded-xl bg-white/5 backdrop-blur-sm transition-all duration-300
${isFocused ? 'ring-2 ring-blue-500/50 bg-white/10' : 'hover:bg-white/[0.07]'}
`}>
${isCollapsed ? 'opacity-50 hover:opacity-100' : ''}
`}
whileHover={isCollapsed ? { scale: 1.02 } : {}}
onClick={() => isCollapsed && toggleCollapse()}
>
<textarea
ref={inputRef}
value={inputText}
@ -105,37 +174,46 @@ export function AISuggestionBar({
onFocus={() => {
setIsFocused(true);
setShowSuggestions(true);
if (isCollapsed) {
toggleCollapse();
}
}}
onBlur={() => { setIsFocused(false); setShowSuggestions(false) }}
placeholder={placeholder}
onBlur={() => setIsFocused(false)}
placeholder={isCollapsed ? "点击展开..." : placeholder}
className="w-full resize-none bg-transparent border-none px-4 py-3 text-white placeholder:text-white/40
focus:outline-none min-h-[52px] max-h-[150px] pr-[100px]"
rows={1}
disabled={isCollapsed}
/>
{/* 操作按钮 */}
<div className="absolute right-2 top-1/2 -translate-y-1/2 flex items-center gap-2">
<button
<motion.button
className={`
p-2 rounded-lg transition-colors
${showSuggestions ? 'bg-white/10 text-white' : 'text-white/40 hover:text-white/60'}
`}
onClick={() => setShowSuggestions(!showSuggestions)}
onClick={() => !isCollapsed && setShowSuggestions(!showSuggestions)}
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
disabled={isCollapsed}
>
{showSuggestions ? <X className="w-5 h-5" /> : <Sparkles className="w-5 h-5" />}
</button>
<button
</motion.button>
<motion.button
className={`
p-2 rounded-lg transition-colors
${inputText.trim() ? 'bg-blue-500 text-white' : 'bg-white/5 text-white/20'}
`}
onClick={handleSubmit}
disabled={!inputText.trim()}
disabled={!inputText.trim() || isCollapsed}
whileHover={inputText.trim() ? { scale: 1.1 } : {}}
whileTap={inputText.trim() ? { scale: 0.9 } : {}}
>
<Send className="w-5 h-5" />
</button>
</div>
</motion.button>
</div>
</motion.div>
</div>
</motion.div>
);

View File

@ -0,0 +1,54 @@
'use client';
import { useState, useCallback } from 'react';
import './style/login.css';
import VantaHaloBackground from '@/components/vanta-halo-background';
export default function Login() {
const [isLoaded, setIsLoaded] = useState(false);
const handleBackgroundLoaded = useCallback(() => {
setIsLoaded(true);
}, []);
return (
<div className="main-container login-page relative">
{/* logo Movie Flow */}
<div className='login-logo'>
<span className="logo-heart">Movie Flow</span>
</div>
<div className="left-panel">
<VantaHaloBackground onLoaded={handleBackgroundLoaded} />
</div>
<div className={`right-panel ${isLoaded ? 'fade-in' : 'invisible'}`}>
<div className="auth-container">
<div className="auth-header">
<h2></h2>
<p>访</p>
</div>
<form className="">
<div className="mb-3">
<label className="form-label"></label>
<input placeholder="电子邮箱地址" required className="form-control" type="email" value="" />
</div>
<div className="mb-3">
<label className="form-label"></label>
<input placeholder="密码" required className="form-control" type="password" value="" />
<div className="d-flex justify-content-end mt-1">
<a className="auth-link small" href="/forgot-password" data-discover="true"></a>
</div>
</div>
<button type="submit" className="w-full mt-4 btn btn-primary"></button>
<div className="text-center mt-3">
<p style={{ color: "rgba(255, 255, 255, 0.6)" }}>
<a className="auth-link" href="/signup" data-discover="true"></a>
</p>
</div>
</form>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,302 @@
.main-container {
display: flex;
height: 100vh;
}
.left-panel {
width: 61.8%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
position: relative;
}
.right-panel {
width: 38.2%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: rgb(12 7 26);
backdrop-filter: blur(10px);
transition: opacity 0.3s ease, visibility 0.3s ease;
}
.auth-container {
padding: 2.5rem;
border-radius: 1.5rem;
max-width: 400px;
width: 100%;
position: relative;
z-index: 10;
background: rgba(255, 255, 255, 0.05);
box-shadow:
0 4px 24px -1px rgba(0, 0, 0, 0.2),
0 0 1px 0 rgba(255, 255, 255, 0.3) inset,
0 0 20px 0 rgba(255, 255, 255, 0.05) inset;
backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.1);
transform-style: preserve-3d;
perspective: 1000px;
animation: container-appear 0.6s ease-out;
}
@keyframes container-appear {
0% {
opacity: 0;
transform: translateY(20px) scale(0.95);
}
100% {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.auth-container::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border-radius: 1.5rem;
background: linear-gradient(135deg,
rgba(255, 255, 255, 0.1) 0%,
rgba(255, 255, 255, 0.05) 50%,
rgba(255, 255, 255, 0) 100%);
pointer-events: none;
}
.auth-container::after {
content: '';
position: absolute;
top: -1px;
left: -1px;
right: -1px;
bottom: -1px;
border-radius: 1.5rem;
background: linear-gradient(45deg,
rgba(255, 255, 255, 0.1) 0%,
rgba(255, 255, 255, 0) 60%);
pointer-events: none;
z-index: -1;
}
.auth-header {
margin-bottom: 2rem;
text-align: center;
}
.auth-header h2 {
font-size: 2rem;
font-weight: 600;
margin-bottom: 0.5rem;
background: linear-gradient(135deg, #fff, #a8a8a8);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.auth-header p {
color: rgba(255, 255, 255, 0.6);
font-size: 0.9rem;
}
.form-label {
display: block;
margin-bottom: 0.5rem;
color: rgba(255, 255, 255, 0.8);
font-size: 0.9rem;
}
.form-control {
width: 100%;
padding: 0.75rem 1rem;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 0.5rem;
color: white;
transition: all 0.3s ease;
}
.form-control:focus {
outline: none;
border-color: rgba(255, 255, 255, 0.3);
background: rgba(255, 255, 255, 0.1);
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.05);
}
.form-control::placeholder {
color: rgba(255, 255, 255, 0.3);
}
.btn-primary {
background: linear-gradient(135deg, #6366f1, #4f46e5);
border: none;
padding: 0.75rem;
border-radius: 0.5rem;
font-weight: 500;
color: white;
transition: all 0.3s ease;
}
.btn-primary:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3);
}
.auth-link {
color: #6366f1;
text-decoration: none;
transition: color 0.3s ease;
}
.auth-link:hover {
color: #818cf8;
}
.mb-3 {
margin-bottom: 1.5rem !important;
}
.text-center {
text-align: center !important;
}
#vanta-background {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
}
/* body {
background-color: transparent !important;
} */
.invisible {
opacity: 0;
visibility: hidden;
}
.fade-in {
opacity: 1;
visibility: visible;
animation: fadeIn 0.8s ease-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateX(20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.login-logo {
position: fixed;
top: 2rem;
left: 2rem;
z-index: 100;
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 1.8rem;
font-weight: 700;
letter-spacing: 0.1em;
animation: logoAppear 1s ease-out forwards;
}
.logo-text {
color: white;
text-shadow: 0 0 10px rgba(255, 255, 255, 0.5);
transition: all 0.3s ease;
display: inline-block;
}
.logo-text:hover {
transform: translateY(-2px) scale(1.1);
text-shadow: 0 0 20px rgba(255, 255, 255, 0.8);
}
.logo-heart {
display: inline-block;
font-size: 1.5rem;
animation: heartbeat 1.5s ease-in-out infinite;
transform-origin: center;
margin: 0 0.2rem;
}
@keyframes heartbeat {
0% {
transform: scale(1);
}
25% {
transform: scale(1.1);
}
40% {
transform: scale(1);
}
60% {
transform: scale(1.1);
}
100% {
transform: scale(1);
}
}
@keyframes logoAppear {
0% {
opacity: 0;
transform: translateY(-20px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
/* 为每个字母添加独特的悬停效果 */
.logo-text:nth-child(1):hover {
color: #64ffda;
}
.logo-text:nth-child(3):hover {
color: #bd93f9;
}
.logo-text:nth-child(4):hover {
color: #ff79c6;
}
/* 添加玻璃态效果 */
.login-logo::before {
content: '';
position: absolute;
top: -10px;
left: -10px;
right: -10px;
bottom: -10px;
background: rgba(255, 255, 255, 0.03);
border-radius: 10px;
backdrop-filter: blur(5px);
z-index: -1;
opacity: 0;
transition: opacity 0.3s ease;
}
.login-logo:hover::before {
opacity: 1;
}

View File

@ -4,7 +4,7 @@
border-radius: 12px;
flex-direction: column;
width: 100%;
max-width: 1000px;
max-width: 1200px;
height: fit-content;
display: flex;
overflow: hidden;
@ -120,7 +120,7 @@
display: grid;
grid-auto-flow: column;
grid-auto-columns: minmax(25%, 1fr);
overflow-x: auto;
/* overflow-x: auto; */
}
}
.image-x5Y2Sg {

View File

@ -1,12 +1,14 @@
"use client"
import { useEffect, useState, useRef } from "react";
import { Play, ChevronUp, Loader2 } from "lucide-react";
import React, { useEffect, useState, useRef, useMemo, useCallback } from "react";
import { Play, ChevronUp, Loader2, Edit3, FileText, Pause } from "lucide-react";
import "./style/work-flow.css";
import LiquidGlass from '@/plugins/liquid-glass';
import { Skeleton } from "@/components/ui/skeleton";
import { AISuggestionBar } from "@/components/ai-suggestion-bar";
import { motion, AnimatePresence } from "framer-motion";
import { debounce } from "lodash";
import { GlassIconButton } from "@/components/ui/glass-icon-button";
import { EditModal } from "@/components/ui/edit-modal";
const MOCK_SKETCH_URLS = [
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-1.jpg',
@ -20,6 +22,13 @@ const MOCK_SKETCH_SCRIPT = [
'script-123',
'script-123',
];
const MOCK_VIDEO_URLS = [
'https://cdn.qikongjian.com/videos/1750385931_99a8fb42-af89-4ae9-841a-a49869f026bd_text_to_video_0.mp4',
'https://cdn.qikongjian.com/videos/1750389908_37d4fffa-8516-43a3-a423-fc0274f40e8a_text_to_video_0.mp4',
'https://cdn.qikongjian.com/videos/1750384661_d8e30b79-828e-48cd-9025-ab62a996717c_text_to_video_0.mp4',
'https://cdn.qikongjian.com/videos/1750320040_4b47996e-7c70-490e-8433-80c7df990fdd_text_to_video_0.mp4',
];
const MOCK_SKETCH_COUNT = 8;
export default function WorkFlow() {
@ -37,6 +46,16 @@ export default function WorkFlow() {
const [isDragging, setIsDragging] = useState(false);
const [startX, setStartX] = useState(0);
const [scrollLeft, setScrollLeft] = useState(0);
const [showControls, setShowControls] = useState(false);
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
const [taskVideos, setTaskVideos] = useState<any[]>([]);
const [isGeneratingVideo, setIsGeneratingVideo] = useState(false);
const mainVideoRef = useRef<HTMLVideoElement>(null);
const [isPlaying, setIsPlaying] = useState(false);
const playTimerRef = useRef<NodeJS.Timeout | null>(null);
const [isVideoPlaying, setIsVideoPlaying] = useState(true);
const videoPlayTimerRef = useRef<NodeJS.Timeout | null>(null);
const [currentLoadingText, setCurrentLoadingText] = useState('加载中...');
// 模拟 AI 建议
const mockSuggestions = [
@ -49,16 +68,31 @@ export default function WorkFlow() {
useEffect(() => {
const taskId = localStorage.getItem("taskId") || "taskId-123";
getTaskDetail(taskId).then((data) => {
getTaskDetail(taskId).then(async (data) => {
setTaskObject(data);
setIsLoading(false);
setCurrentStep('1');
// 只在任务详情加载完成后获取分镜草图
await getTaskSketch(taskId);
await new Promise(resolve => setTimeout(resolve, 2000));
// 首先修改 taskObject 下的 taskStatus 为 '2'
setTaskObject((prev: any) => ({
...prev,
taskStatus: '2'
}));
setCurrentStep('2');
// 获取分镜草图后,开始绘制角色
await getTaskRole(taskId);
await new Promise(resolve => setTimeout(resolve, 2000));
// 首先修改 taskObject 下的 taskStatus 为 '3'
setTaskObject((prev: any) => ({
...prev,
taskStatus: '3'
}));
setCurrentStep('3');
// 获取绘制角色后,开始获取分镜视频
await getTaskVideo(taskId);
});
// 轮询获取分镜草图 防抖 1000ms
const debouncedGetTaskSketch = debounce(() => {
getTaskSketch(taskId);
}, 1000);
debouncedGetTaskSketch();
}, []);
// 监听当前选中索引变化,自动滚动到对应位置
@ -122,6 +156,8 @@ export default function WorkFlow() {
taskDescription: "Task 1 Description",
taskStatus: "1", // '1' 绘制分镜、'2' 绘制角色、'3' 生成分镜视频、'4' 视频后期制作、'5' 最终成品
taskProgress: 0,
mode: 'auto', // 托管模式、人工干预模式
resolution: '1080p', // 1080p、2160p
taskCreatedAt: new Date().toISOString(),
taskUpdatedAt: new Date().toISOString(),
};
@ -130,6 +166,9 @@ export default function WorkFlow() {
// 模拟接口请求 每次获取一个分镜草图 轮询获取
const getTaskSketch = async (taskId: string) => {
// 避免重复调用
if (isGeneratingSketch || taskSketch.length > 0) return;
setIsGeneratingSketch(true);
setTaskSketch([]);
@ -144,7 +183,13 @@ export default function WorkFlow() {
status: 'done'
};
setTaskSketch(prev => [...prev, newSketch]);
setTaskSketch(prev => {
// 避免重复添加相同id的sketch
if (prev.find(sketch => sketch.id === newSketch.id)) {
return prev;
}
return [...prev, newSketch];
});
setCurrentSketchIndex(i);
setSketchCount(i + 1);
}
@ -152,6 +197,40 @@ export default function WorkFlow() {
setIsGeneratingSketch(false);
}
// 模拟接口请求 每次获取一个角色 轮询获取
const getTaskRole = async (taskId: string) => {
await new Promise(resolve => setTimeout(resolve, 2000)); // 模拟2秒延迟
}
// 模拟接口请求 每次获取一个分镜视频 轮询获取
const getTaskVideo = async (taskId: string) => {
setIsGeneratingVideo(true);
setTaskVideos([]);
// 模拟分批获取分镜视频
for (let i = 0; i < MOCK_SKETCH_COUNT; i++) {
await new Promise(resolve => setTimeout(resolve, 2000)); // 模拟2秒延迟
const newVideo = {
id: `video-${i}`,
url: MOCK_VIDEO_URLS[i % MOCK_VIDEO_URLS.length],
script: MOCK_SKETCH_SCRIPT[i % MOCK_SKETCH_SCRIPT.length],
status: 'done'
};
setTaskVideos(prev => {
// 避免重复添加相同id的video
if (prev.find(video => video.id === newVideo.id)) {
return prev;
}
return [...prev, newVideo];
});
setCurrentSketchIndex(i);
}
setIsGeneratingVideo(false);
};
const handleSuggestionClick = (suggestion: string) => {
console.log('Selected suggestion:', suggestion);
};
@ -160,102 +239,9 @@ export default function WorkFlow() {
console.log('Submitted text:', text);
};
// 渲染分镜草图或加载动画
const renderSketchContent = () => {
if (!taskSketch[currentSketchIndex]) {
return (
<div className="w-full h-full flex items-center justify-center bg-black/20 backdrop-blur-sm rounded-lg">
<div className="flex flex-col items-center gap-4">
<Loader2 className="w-8 h-8 text-blue-500 animate-spin" />
<p className="text-sm text-white/70"> {sketchCount + 1}/{MOCK_SKETCH_COUNT}</p>
</div>
</div>
);
}
return (
<motion.img
key={currentSketchIndex}
src={taskSketch[currentSketchIndex].url}
alt={`分镜草图 ${currentSketchIndex + 1}`}
className="w-full h-full object-contain rounded-lg"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3 }}
/>
);
};
return (
<div className="w-full h-full overflow-hidden">
<div className="flex h-full flex-col p-6 justify-center items-center">
<div className="container-H2sRZG">
<div className="splashContainer-otuV_A">
<div className="content-vPGYx8">
<div className="info-UUGkPJ">
{isLoading ? (
<>
<Skeleton className="h-8 w-64 mb-2" />
<Skeleton className="h-4 w-96" />
</>
) : (
<>
<div className="title-JtMejk">{taskObject?.projectName}{taskObject?.taskName}</div>
<p className="normalS400 subtitle-had8uE">{taskObject?.taskDescription}</p>
</>
)}
</div>
</div>
<div className="media-Ocdu1O">
<div className="videoContainer-qteKNi" ref={containerRef}>
{isLoading ? (
<Skeleton className="w-full aspect-video rounded-lg" />
) : (
<>
{
currentStep === '1' ? (
<div className="heroVideo-FIzuK1" style={{ aspectRatio: "16 / 9" }}>
{renderSketchContent()}
</div>
) : (
<video
className="heroVideo-FIzuK1"
src="https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome.mp4"
style={{
aspectRatio: "16 / 9"
}}
autoPlay
muted
loop
></video>
)
}
<button className="container-kIPoeH secondary-_HxO1W large-_aHMgD videoPlaybackButton-uFNO1b">
<Play className="w-6 h-6 icon" />
</button>
</>
)}
</div>
<div className="imageGrid-ymZV9z">
{isLoading ? (
<>
<Skeleton className="w-[128px] h-[128px] rounded-lg" />
<Skeleton className="w-[128px] h-[128px] rounded-lg" />
<Skeleton className="w-[128px] h-[128px] rounded-lg" />
<Skeleton className="w-[128px] h-[128px] rounded-lg" />
</>
) : (
<div
ref={thumbnailsRef}
className="w-full grid grid-flow-col auto-cols-[25%] gap-4 overflow-x-auto hidden-scrollbar px-1 py-1 cursor-grab active:cursor-grabbing"
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseLeave={() => setIsDragging(false)}
>
{currentStep === '1' ? (
<>
{taskSketch.map((sketch, index) => (
// 缓存渲染的缩略图列表
const renderedSketches = useMemo(() =>
taskSketch.map((sketch, index) => (
<motion.div
key={sketch.id}
className={`relative aspect-video rounded-lg overflow-hidden
@ -290,16 +276,684 @@ export default function WorkFlow() {
<span className="text-xs text-white/90"> {index + 1}</span>
</div>
</motion.div>
))}
{isGeneratingSketch && sketchCount < MOCK_SKETCH_COUNT && (
<div className="relative aspect-video rounded-lg bg-black/20 backdrop-blur-sm
flex items-center justify-center">
<Loader2 className="w-6 h-6 text-blue-500 animate-spin" />
)), [taskSketch, currentSketchIndex, isDragging]
);
// 缓存渲染的视频缩略图列表
const renderedVideos = useMemo(() =>
taskVideos.map((video, index) => (
<motion.div
key={video.id}
className={`relative aspect-video rounded-lg overflow-hidden
${currentSketchIndex === index ? 'ring-2 ring-blue-500 z-10' : 'hover:ring-2 hover:ring-blue-500/50'}`}
onClick={() => !isDragging && setCurrentSketchIndex(index)}
initial={false}
animate={{
scale: currentSketchIndex === index ? 1.05 : 1,
rotateY: currentSketchIndex === index ? 5 : 0,
rotateX: currentSketchIndex === index ? -5 : 0,
translateZ: currentSketchIndex === index ? '20px' : '0px',
transition: {
type: "spring",
stiffness: 300,
damping: 20
}
}}
style={{
transformStyle: 'preserve-3d',
perspective: '1000px'
}}
>
<video
className="w-full h-full object-cover"
src={video.url}
muted
playsInline
loop
poster={taskSketch[index]?.url}
/>
<div className="absolute bottom-0 left-0 right-0 p-2 bg-gradient-to-t from-black/60 to-transparent">
<span className="text-xs text-white/90"> {index + 1}</span>
</div>
</motion.div>
)), [taskVideos, currentSketchIndex, isDragging, taskSketch]
);
// 处理播放/暂停
const togglePlay = useCallback(() => {
setIsPlaying(prev => !prev);
}, []);
// 自动播放逻辑
useEffect(() => {
if (isPlaying && taskSketch.length > 0) {
playTimerRef.current = setInterval(() => {
setCurrentSketchIndex(prev => {
const nextIndex = (prev + 1) % taskSketch.length;
return nextIndex;
});
}, 2000); // 每2秒切换一次
} else if (playTimerRef.current) {
clearInterval(playTimerRef.current);
}
return () => {
if (playTimerRef.current) {
clearInterval(playTimerRef.current);
}
};
}, [isPlaying, taskSketch.length]);
// 当切换到视频模式时,停止播放
useEffect(() => {
if (currentStep === '3') {
setIsPlaying(false);
}
}, [currentStep]);
// 处理视频播放/暂停
const toggleVideoPlay = useCallback(() => {
setIsVideoPlaying(prev => !prev);
}, []);
// 视频自动播放逻辑
useEffect(() => {
if (isVideoPlaying && taskVideos.length > 0) {
// 确保当前视频开始播放
if (mainVideoRef.current) {
mainVideoRef.current.play();
}
} else {
// 暂停当前视频
if (mainVideoRef.current) {
mainVideoRef.current.pause();
}
// 清除定时器
if (videoPlayTimerRef.current) {
clearInterval(videoPlayTimerRef.current);
}
}
return () => {
if (videoPlayTimerRef.current) {
clearInterval(videoPlayTimerRef.current);
}
};
}, [isVideoPlaying, taskVideos.length]);
// 当切换视频时重置视频播放
useEffect(() => {
if (mainVideoRef.current) {
mainVideoRef.current.currentTime = 0;
if (isVideoPlaying) {
mainVideoRef.current.play();
}
}
}, [currentSketchIndex, isVideoPlaying]);
// 当切换到分镜草图模式时,停止视频播放
useEffect(() => {
if (currentStep !== '3') {
setIsVideoPlaying(false);
}
}, [currentStep]);
// 更新加载文字
useEffect(() => {
if (isLoading) {
setCurrentLoadingText('正在加载任务信息...');
return;
}
if (currentStep === '1') {
if (isGeneratingSketch) {
setCurrentLoadingText(`正在生成分镜草图 ${sketchCount + 1}/${MOCK_SKETCH_COUNT}...`);
} else {
setCurrentLoadingText('分镜草图生成完成');
}
} else if (currentStep === '2') {
setCurrentLoadingText('正在绘制角色...');
} else if (currentStep === '3') {
if (isGeneratingVideo) {
setCurrentLoadingText(`正在生成分镜视频 ${taskVideos.length + 1}/${taskSketch.length}...`);
} else {
setCurrentLoadingText('分镜视频生成完成');
}
}
}, [isLoading, currentStep, isGeneratingSketch, sketchCount, isGeneratingVideo, taskVideos.length, taskSketch.length]);
const renderSketchContent = () => {
if (!taskObject) {
return (
<div className="w-full h-full flex items-center justify-center bg-gradient-to-br from-black/40 via-black/20 to-black/40 backdrop-blur-sm rounded-lg overflow-hidden">
<motion.div
className="flex flex-col items-center gap-4"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
<motion.div
animate={{
scale: [1, 1.2, 1],
rotate: [0, 360],
}}
transition={{
duration: 2,
repeat: Infinity,
ease: "linear"
}}
>
<Loader2 className="w-8 h-8 text-blue-500" />
</motion.div>
<motion.p
className="text-sm text-white/70"
animate={{
opacity: [0.5, 1, 0.5],
}}
transition={{
duration: 2,
repeat: Infinity,
ease: "linear"
}}
>
...
</motion.p>
</motion.div>
</div>
);
}
if (currentStep === '3') {
return (
<div
className="relative w-full h-full"
onMouseEnter={() => setShowControls(true)}
onMouseLeave={() => setShowControls(false)}
>
{taskVideos[currentSketchIndex] ? (
<motion.div className="relative w-full h-full">
<motion.video
ref={mainVideoRef}
key={taskVideos[currentSketchIndex].url}
className="w-full h-full rounded-lg object-cover object-center"
src={taskVideos[currentSketchIndex].url}
autoPlay={isVideoPlaying}
muted
loop={false}
playsInline
poster={taskSketch[currentSketchIndex]?.url}
initial={{ opacity: 0, scale: 0.95 }}
animate={{
opacity: 1,
scale: 1,
transition: {
type: "spring",
stiffness: 300,
damping: 25
}
}}
onEnded={() => {
if (isVideoPlaying) {
// 当前视频播放完成后,自动切换到下一个
setCurrentSketchIndex(prev => (prev + 1) % taskVideos.length);
}
}}
/>
{/* 播放进度指示器 */}
<AnimatePresence>
{isVideoPlaying && (
<motion.div
className="absolute bottom-0 left-0 right-0 h-1 bg-blue-500/20"
initial={{ scaleX: 0 }}
animate={{ scaleX: 1 }}
exit={{ scaleX: 0 }}
transition={{
duration: mainVideoRef.current?.duration || 6,
repeat: Infinity
}}
style={{ transformOrigin: "left" }}
/>
)}
</AnimatePresence>
</motion.div>
) : (
<div className="w-full h-full flex items-center justify-center bg-gradient-to-br from-blue-500/20 via-purple-500/10 to-pink-500/20 backdrop-blur-sm rounded-lg overflow-hidden">
<motion.div
className="flex flex-col items-center gap-4"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
<div className="relative">
<motion.div
className="absolute -inset-4 bg-gradient-to-r from-blue-500 via-purple-500 to-pink-500 rounded-full opacity-20 blur-xl"
animate={{
scale: [1, 1.2, 1],
rotate: [0, 180, 360],
}}
transition={{
duration: 4,
repeat: Infinity,
ease: "linear"
}}
/>
<motion.div
animate={{
rotate: [0, 360],
}}
transition={{
duration: 2,
repeat: Infinity,
ease: "linear"
}}
>
<Loader2 className="w-8 h-8 text-blue-500 relative z-10" />
</motion.div>
</div>
<motion.p
className="text-sm text-white/70"
animate={{
opacity: [0.5, 1, 0.5],
}}
transition={{
duration: 2,
repeat: Infinity,
ease: "linear"
}}
>
{taskVideos.length + 1}/{taskSketch.length}
</motion.p>
</motion.div>
</div>
)}
{/* 操作按钮组 */}
<AnimatePresence>
{showControls && (
<>
{/* 顶部按钮组 */}
<motion.div
className="absolute top-4 right-4 flex gap-2"
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.2 }}
>
<GlassIconButton
icon={Edit3}
tooltip="编辑分镜"
onClick={() => setIsEditModalOpen(true)}
/>
{/* <GlassIconButton
icon={FileText}
tooltip="显示脚本"
onClick={() => console.log('显示脚本')}
/> */}
</motion.div>
</>
)}
</AnimatePresence>
{/* 底部播放按钮 */}
<AnimatePresence>
<motion.div
className="absolute bottom-4 left-4"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.8 }}
transition={{ duration: 0.2 }}
>
<motion.div
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
>
<GlassIconButton
icon={isVideoPlaying ? Pause : Play}
tooltip={isVideoPlaying ? "暂停播放" : "自动播放"}
onClick={toggleVideoPlay}
size="sm"
/>
</motion.div>
</motion.div>
</AnimatePresence>
</div>
);
}
return (
<div
className="relative w-full h-full"
onMouseEnter={() => setShowControls(true)}
onMouseLeave={() => setShowControls(false)}
>
{taskSketch[currentSketchIndex] ? (
<motion.img
key={currentSketchIndex}
src={taskSketch[currentSketchIndex].url}
alt={`分镜草图 ${currentSketchIndex + 1}`}
className="w-full h-full rounded-lg"
initial={{ opacity: 0, scale: 0.95 }}
animate={{
opacity: 1,
scale: 1,
transition: {
type: "spring",
stiffness: 300,
damping: 25
}
}}
/>
) : (
<div className="w-full h-full flex items-center justify-center bg-gradient-to-br from-blue-500/20 via-purple-500/10 to-pink-500/20 backdrop-blur-sm rounded-lg overflow-hidden">
<motion.div
className="flex flex-col items-center gap-4"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
<div className="relative">
<motion.div
className="absolute -inset-4 bg-gradient-to-r from-blue-500 via-purple-500 to-pink-500 rounded-full opacity-20 blur-xl"
animate={{
scale: [1, 1.2, 1],
rotate: [0, 180, 360],
}}
transition={{
duration: 4,
repeat: Infinity,
ease: "linear"
}}
/>
<motion.div
animate={{
rotate: [0, 360],
}}
transition={{
duration: 2,
repeat: Infinity,
ease: "linear"
}}
>
<Loader2 className="w-8 h-8 text-blue-500 relative z-10" />
</motion.div>
</div>
<motion.p
className="text-sm text-white/70"
animate={{
opacity: [0.5, 1, 0.5],
}}
transition={{
duration: 2,
repeat: Infinity,
ease: "linear"
}}
>
{sketchCount + 1}/{MOCK_SKETCH_COUNT}
</motion.p>
</motion.div>
</div>
)}
{/* 操作按钮组 */}
<AnimatePresence>
{showControls && (
<>
{/* 顶部按钮组 */}
<motion.div
className="absolute top-4 right-4 flex gap-2"
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.2 }}
>
<GlassIconButton
icon={Edit3}
tooltip="编辑分镜"
onClick={() => setIsEditModalOpen(true)}
/>
{/* <GlassIconButton
icon={FileText}
tooltip="显示脚本"
onClick={() => console.log('显示脚本')}
/> */}
</motion.div>
</>
)}
</AnimatePresence>
{/* 底部播放按钮 */}
<AnimatePresence>
<motion.div
className="absolute bottom-4 left-4"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.8 }}
transition={{ duration: 0.2 }}
>
<motion.div
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
>
<GlassIconButton
icon={isPlaying ? Pause : Play}
tooltip={isPlaying ? "暂停播放" : "自动播放"}
onClick={togglePlay}
size="sm"
/>
</motion.div>
</motion.div>
</AnimatePresence>
{/* 播放进度指示器 */}
<AnimatePresence>
{isPlaying && (
<motion.div
className="absolute bottom-0 left-0 right-0 h-1 bg-blue-500/20"
initial={{ scaleX: 0 }}
animate={{ scaleX: 1 }}
exit={{ scaleX: 0 }}
transition={{ duration: 2, repeat: Infinity }}
style={{ transformOrigin: "left" }}
/>
)}
</AnimatePresence>
</div>
);
};
return (
<div className="w-full h-full overflow-hidden">
<div className="flex h-full flex-col p-6 justify-center items-center pt-0">
<div className="container-H2sRZG">
<div className="splashContainer-otuV_A">
<div className="content-vPGYx8">
<div className="info-UUGkPJ">
{isLoading ? (
<>
<Skeleton className="h-8 w-64 mb-2" />
<Skeleton className="h-4 w-96" />
</>
) : (
<>
<div className="title-JtMejk">{taskObject?.projectName}{taskObject?.taskName}</div>
{/* 实时反馈当前 currentLoadingText */}
<motion.div
className="flex items-center gap-2 justify-center"
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
>
<motion.div
className="w-1.5 h-1.5 rounded-full bg-blue-500"
animate={{
scale: [1, 1.5, 1],
opacity: [1, 0.5, 1]
}}
transition={{
duration: 1,
repeat: Infinity,
repeatDelay: 0.2
}}
/>
<motion.p
className="normalS400 subtitle-had8uE text-blue-500/80"
key={currentLoadingText}
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 10 }}
transition={{ duration: 0.3 }}
>
{currentLoadingText}
</motion.p>
<motion.div
className="w-1.5 h-1.5 rounded-full bg-blue-500"
animate={{
scale: [1, 1.5, 1],
opacity: [1, 0.5, 1]
}}
transition={{
duration: 1,
repeat: Infinity,
repeatDelay: 0.2,
delay: 0.3
}}
/>
<motion.div
className="w-1.5 h-1.5 rounded-full bg-blue-500"
animate={{
scale: [1, 1.5, 1],
opacity: [1, 0.5, 1]
}}
transition={{
duration: 1,
repeat: Infinity,
repeatDelay: 0.2,
delay: 0.6
}}
/>
</motion.div>
</>
)}
</div>
</div>
<div className="media-Ocdu1O">
<div className="videoContainer-qteKNi" ref={containerRef}>
{isLoading ? (
<Skeleton className="w-full aspect-video rounded-lg" />
) : (
<>
<div className="heroVideo-FIzuK1" style={{ aspectRatio: "16 / 9" }}>
{renderSketchContent()}
</div>
</>
)}
</div>
<div className="imageGrid-ymZV9z hidden-scrollbar">
{isLoading ? (
<>
<Skeleton className="w-[128px] h-[128px] rounded-lg" />
<Skeleton className="w-[128px] h-[128px] rounded-lg" />
<Skeleton className="w-[128px] h-[128px] rounded-lg" />
<Skeleton className="w-[128px] h-[128px] rounded-lg" />
</>
) : (
<div
ref={thumbnailsRef}
className="w-full grid grid-flow-col auto-cols-[20%] gap-4 overflow-x-auto hidden-scrollbar px-1 py-1 cursor-grab active:cursor-grabbing"
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseLeave={() => setIsDragging(false)}
>
{currentStep === '3' ? (
<>
{renderedVideos}
{isGeneratingVideo && taskVideos.length < taskSketch.length && (
<motion.div
className="relative aspect-video rounded-lg overflow-hidden bg-gradient-to-br from-blue-500/20 via-purple-500/10 to-pink-500/20 backdrop-blur-sm"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.3 }}
>
<div className="absolute inset-0 flex items-center justify-center">
<div className="relative">
<motion.div
className="absolute -inset-4 bg-gradient-to-r from-blue-500 via-purple-500 to-pink-500 rounded-full opacity-20 blur-xl"
animate={{
scale: [1, 1.2, 1],
rotate: [0, 180, 360],
}}
transition={{
duration: 4,
repeat: Infinity,
ease: "linear"
}}
/>
<motion.div
animate={{
rotate: [0, 360],
}}
transition={{
duration: 2,
repeat: Infinity,
ease: "linear"
}}
>
<Loader2 className="w-6 h-6 text-blue-500 relative z-10" />
</motion.div>
</div>
</div>
<div className="absolute bottom-0 left-0 right-0 p-2 bg-gradient-to-t from-black/60 to-transparent">
<span className="text-xs text-white/90"> {taskVideos.length + 1}</span>
</div>
</motion.div>
)}
</>
) : (
<>
{renderedSketches}
{isGeneratingSketch && sketchCount < MOCK_SKETCH_COUNT && (
<motion.div
className="relative aspect-video rounded-lg overflow-hidden bg-gradient-to-br from-blue-500/20 via-purple-500/10 to-pink-500/20 backdrop-blur-sm"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.3 }}
>
<div className="absolute inset-0 flex items-center justify-center">
<div className="relative">
<motion.div
className="absolute -inset-4 bg-gradient-to-r from-blue-500 via-purple-500 to-pink-500 rounded-full opacity-20 blur-xl"
animate={{
scale: [1, 1.2, 1],
rotate: [0, 180, 360],
}}
transition={{
duration: 4,
repeat: Infinity,
ease: "linear"
}}
/>
<motion.div
animate={{
rotate: [0, 360],
}}
transition={{
duration: 2,
repeat: Infinity,
ease: "linear"
}}
>
<Loader2 className="w-6 h-6 text-blue-500 relative z-10" />
</motion.div>
</div>
</div>
<div className="absolute bottom-0 left-0 right-0 p-2 bg-gradient-to-t from-black/60 to-transparent">
<span className="text-xs text-white/90"> {sketchCount + 1}</span>
</div>
</motion.div>
)}
</>
)}
</div>
@ -311,22 +965,21 @@ export default function WorkFlow() {
</div>
{/* AI 建议栏 */}
<AnimatePresence>
<motion.div
initial={{ y: 100, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: 100, opacity: 0 }}
transition={{ type: "spring", stiffness: 300, damping: 30 }}
className="mb-16"
>
<AISuggestionBar
suggestions={mockSuggestions}
onSuggestionClick={handleSuggestionClick}
onSubmit={handleSubmit}
placeholder="请输入你的想法,或点击预设词条获取 AI 建议..."
/>
</motion.div>
</AnimatePresence>
<EditModal
isOpen={isEditModalOpen}
onClose={() => setIsEditModalOpen(false)}
taskStatus={taskObject?.taskStatus || '1'}
taskSketch={taskSketch}
currentSketchIndex={currentSketchIndex}
onSketchSelect={setCurrentSketchIndex}
/>
</div>
)
}

221
components/parallax.tsx Normal file
View File

@ -0,0 +1,221 @@
import React, { useEffect, useRef } from 'react';
import styled from 'styled-components';
// 导入图片资源
import mono from '@/assets/3dr_mono.png';
import chihiro from '@/assets/3dr_chihiro.png';
import howlcastle from '@/assets/3dr_howlcastle.png';
import monobg from '@/assets/3dr_monobg.jpg';
import spirited from '@/assets/3dr_spirited.jpg';
import howlbg from '@/assets/3dr_howlbg.jpg';
const Parallax = () => {
const pageXRef = useRef(null);
const cardsRef = useRef(null);
useEffect(() => {
const pageX = pageXRef.current;
const cards = cardsRef.current;
const images = document.querySelectorAll('.card__img');
const backgrounds = document.querySelectorAll('.card__bg');
let timeout;
const range = 40;
// 计算旋转角度
const calcValue = (a, b) => ((a / b) * range - range / 2).toFixed(1);
// 视差动画函数
const parallax = (e) => {
const x = e.clientX;
const y = e.clientY;
if (timeout) {
window.cancelAnimationFrame(timeout);
}
timeout = window.requestAnimationFrame(() => {
const xValue = calcValue(x, window.innerWidth);
const yValue = calcValue(y, window.innerHeight);
// 设置卡片容器的旋转角度
cards.style.transform = `rotateX(${yValue}deg) rotateY(${xValue}deg)`;
// 设置所有图片的位移
images.forEach(item => {
item.style.transform = `translateX(${-xValue}px) translateY(${yValue}px)`;
});
// 设置所有背景的位置
backgrounds.forEach(item => {
item.style.backgroundPosition = `${xValue * 0.45}px ${-yValue * 0.45}px`;
});
});
};
pageX.addEventListener('mousemove', parallax, false);
return () => {
if (pageX) {
pageX.removeEventListener('mousemove', parallax);
}
};
}, []);
return (
<PageX ref={pageXRef}>
<Cards ref={cardsRef}>
<h3>Movies</h3>
<h1>Popular</h1>
{/* 幽灵公主 */}
<Card className="princess-mononoke">
<CardBg className="card__bg" bg={monobg} />
<CardImg className="card__img" src={mono} alt="Princess Mononoke" />
<CardText>
<CardTitle>Princess Mononoke</CardTitle>
</CardText>
</Card>
{/* 千与千寻 */}
<Card className="spirited-away">
<CardBg className="card__bg" bg={spirited} />
<CardImg className="card__img" src={chihiro} alt="Spirited Away" />
<CardText>
<CardTitle>Spirited Away</CardTitle>
</CardText>
</Card>
{/* 哈尔的移动城堡 */}
<Card className="howl-s-moving-castle">
<CardBg className="card__bg" bg={howlbg} />
<CardImg className="card__img" src={howlcastle} alt="Howl's Moving Castle" />
<CardText>
<CardTitle>Howl's Moving Castle</CardTitle>
</CardText>
</Card>
</Cards>
</PageX>
);
};
// 样式定义
const PageX = styled.div`
width: 1000px;
height: 700px;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
perspective: 1800px;
background: #642b73;
background: linear-gradient(to bottom, #c6426e, #642b73);
`;
const Cards = styled.div`
display: inline-block;
min-width: 595px;
padding: 30px 35px;
perspective: 1800px;
transform-origin: 50% 50%;
transform-style: preserve-3d;
border-radius: 15px;
text-align: left;
background: #fff;
box-shadow: 0px 10px 20px 20px rgba(0, 0, 0, 0.17);
h1 {
margin-bottom: 30px;
transform: translateZ(35px);
letter-spacing: -1px;
font-size: 32px;
font-weight: 800;
color: #3e3e42;
}
h3 {
margin-bottom: 6px;
transform: translateZ(25px);
font-size: 16px;
color: #eb285d;
}
`;
const Card = styled.div`
display: inline-block;
width: 175px;
height: 250px;
position: relative;
overflow: hidden;
perspective: 1200px;
transform-style: preserve-3d;
transform: translatez(35px);
transition: transform 200ms ease-out;
text-align: center;
border-radius: 15px;
box-shadow: 5px 5px 20px -5px rgba(0, 0, 0, 0.6);
&:not(:last-child) {
margin-right: 30px;
}
&.princess-mononoke .card__img {
top: 14px;
right: -10px;
height: 110%;
}
&.spirited-away .card__img {
top: 25px;
}
&.howl-s-moving-castle .card__img {
top: 5px;
left: -4px;
height: 110%;
}
`;
const CardBg = styled.div`
bottom: -50px;
left: -50px;
position: absolute;
right: -50px;
top: -50px;
transform-origin: 50% 50%;
transform: translateZ(-50px);
z-index: 0;
background: ${props => `url(${props.bg}) center/cover no-repeat`};
`;
const CardImg = styled.img`
position: relative;
height: 100%;
`;
const CardText = styled.div`
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
height: 70px;
position: absolute;
z-index: 2;
bottom: 0;
background: linear-gradient(
to bottom,
rgba(0, 0, 0, 0) 0%,
rgba(0, 0, 0, 0.55) 100%
);
`;
const CardTitle = styled.p`
margin-bottom: 3px;
padding: 0 10px;
font-size: 18px;
font-weight: 700;
color: #fff;
`;
export default Parallax;

View File

@ -0,0 +1,173 @@
'use client';
import React, { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { X, FileText, Users, Video, Music, Scissors, Settings } from 'lucide-react';
import { cn } from '@/lib/utils';
import { ScriptTabContent } from './script-tab-content';
interface EditModalProps {
isOpen: boolean;
onClose: () => void;
taskStatus: string;
taskSketch: any[];
currentSketchIndex: number;
onSketchSelect: (index: number) => void;
}
const tabs = [
{ id: '1', label: '脚本', icon: FileText },
{ id: '2', label: '角色', icon: Users },
{ id: '3', label: '分镜视频', icon: Video },
{ id: '4', label: '背景音', icon: Music },
{ id: '5', label: '剪辑', icon: Scissors },
{ id: 'settings', label: '设置', icon: Settings },
];
export function EditModal({
isOpen,
onClose,
taskStatus,
taskSketch,
currentSketchIndex,
onSketchSelect
}: EditModalProps) {
const [activeTab, setActiveTab] = useState('1');
const isTabDisabled = (tabId: string) => {
if (tabId === 'settings') return false;
return parseInt(tabId) > parseInt(taskStatus);
};
const renderTabContent = () => {
switch (activeTab) {
case '1':
return (
<ScriptTabContent
taskSketch={taskSketch}
currentSketchIndex={currentSketchIndex}
onSketchSelect={onSketchSelect}
/>
);
default:
return (
<div className="min-h-[400px] flex items-center justify-center text-white/50">
{tabs.find(tab => tab.id === activeTab)?.label}
</div>
);
}
};
return (
<AnimatePresence>
{isOpen && (
<>
{/* 背景遮罩 */}
<motion.div
className="fixed inset-0 bg-black/60 backdrop-blur-sm z-50"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={onClose}
/>
{/* 弹窗内容 */}
<div className="fixed inset-x-0 bottom-0 z-50 flex justify-center">
<motion.div
className="w-[66%] min-w-[800px] bg-[#1a1b1e] rounded-t-2xl overflow-hidden"
initial={{ y: '100%' }}
animate={{ y: 0 }}
exit={{ y: '100%' }}
transition={{
type: 'spring',
damping: 25,
stiffness: 200,
}}
>
{/* 标签栏 */}
<div className="flex items-center gap-1 p-2 overflow-x-auto hide-scrollbar bg-white/5">
<div className="flex-1 flex gap-1">
{tabs.map((tab) => {
const Icon = tab.icon;
const disabled = isTabDisabled(tab.id);
return (
<motion.button
key={tab.id}
className={cn(
'flex items-center gap-2 px-4 py-2 rounded-lg transition-colors relative',
activeTab === tab.id ? 'text-white' : 'text-white/50',
disabled ? 'opacity-50 cursor-not-allowed' : 'hover:bg-white/10',
)}
onClick={() => !disabled && setActiveTab(tab.id)}
whileHover={disabled ? undefined : { scale: 1.02 }}
whileTap={disabled ? undefined : { scale: 0.98 }}
>
<Icon className="w-4 h-4" />
<span>{tab.label}</span>
{activeTab === tab.id && (
<motion.div
className="absolute bottom-0 left-0 right-0 h-0.5 bg-blue-500"
layoutId="activeTab"
transition={{ type: 'spring', damping: 20 }}
/>
)}
</motion.button>
);
})}
</div>
{/* 关闭按钮 */}
<div className="pl-4 border-l border-white/10">
<motion.button
className="p-2 rounded-full hover:bg-white/10 transition-colors"
onClick={onClose}
whileHover={{ rotate: 90 }}
whileTap={{ scale: 0.9 }}
>
<X className="w-5 h-5 text-white/70" />
</motion.button>
</div>
</div>
{/* 内容区域 */}
<div className="h-[80vh] overflow-y-auto p-4">
<AnimatePresence mode="wait">
<motion.div
key={activeTab}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.2 }}
>
{renderTabContent()}
</motion.div>
</AnimatePresence>
</div>
{/* 底部操作栏 */}
<div className="p-4 border-t border-white/10 bg-black/20">
<div className="flex justify-end gap-3">
<motion.button
className="px-4 py-2 rounded-lg bg-white/10 text-white hover:bg-white/20 transition-colors"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
onClick={onClose}
>
</motion.button>
<motion.button
className="px-4 py-2 rounded-lg bg-blue-500 text-white hover:bg-blue-600 transition-colors"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
</motion.button>
</div>
</div>
</motion.div>
</div>
</>
)}
</AnimatePresence>
);
}

View File

@ -0,0 +1,71 @@
'use client';
import React from 'react';
import { motion } from 'framer-motion';
import { LucideIcon } from 'lucide-react';
import { cn } from '@/lib/utils';
interface GlassIconButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
icon: LucideIcon;
tooltip?: string;
variant?: 'primary' | 'secondary' | 'danger';
size?: 'sm' | 'md' | 'lg';
}
const variantStyles = {
primary: 'bg-blue-500/10 hover:bg-blue-500/20 border-blue-500/20',
secondary: 'bg-white/10 hover:bg-white/20 border-white/20',
danger: 'bg-red-500/10 hover:bg-red-500/20 border-red-500/20',
};
const sizeStyles = {
sm: 'p-2',
md: 'p-3',
lg: 'p-4',
};
const iconSizes = {
sm: 'w-4 h-4',
md: 'w-5 h-5',
lg: 'w-6 h-6',
};
export function GlassIconButton({
icon: Icon,
tooltip,
variant = 'secondary',
size = 'md',
className,
...props
}: GlassIconButtonProps) {
return (
<motion.button
className={cn(
'relative rounded-full backdrop-blur-md transition-colors shadow-lg border',
variantStyles[variant],
sizeStyles[size],
className
)}
whileHover={{
scale: 1.05,
rotateX: 10,
translateZ: 10
}}
whileTap={{ scale: 0.95 }}
style={{
transformStyle: 'preserve-3d',
perspective: '1000px'
}}
{...props}
>
<Icon className={cn('text-white', iconSizes[size])} />
{tooltip && (
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-2 py-1 text-xs
bg-black/80 text-white rounded-md opacity-0 group-hover:opacity-100 transition-opacity
pointer-events-none whitespace-nowrap">
{tooltip}
</div>
)}
</motion.button>
);
}

View File

@ -0,0 +1,202 @@
'use client';
import React, { useRef, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Trash2, RefreshCw } from 'lucide-react';
import { GlassIconButton } from './glass-icon-button';
import { cn } from '@/lib/utils';
interface ScriptTabContentProps {
taskSketch: any[];
currentSketchIndex: number;
onSketchSelect: (index: number) => void;
}
export function ScriptTabContent({
taskSketch = [],
currentSketchIndex = 0,
onSketchSelect
}: ScriptTabContentProps) {
const thumbnailsRef = useRef<HTMLDivElement>(null);
const scriptsRef = useRef<HTMLDivElement>(null);
// 确保 taskSketch 是数组
const sketches = Array.isArray(taskSketch) ? taskSketch : [];
// 模拟脚本数据
const mockScripts = sketches.map((_, index) => ({
id: `script-${index}`,
content: `这是第 ${index + 1} 个分镜的脚本内容,描述了场景中的主要动作和对话。这里可以添加更多细节来丰富场景表现。`
}));
// 自动滚动到选中项
useEffect(() => {
if (thumbnailsRef.current && scriptsRef.current) {
const thumbnailContainer = thumbnailsRef.current;
const scriptContainer = scriptsRef.current;
// 计算缩略图滚动位置
const thumbnailWidth = thumbnailContainer.children[0]?.clientWidth ?? 0;
const thumbnailGap = 16; // gap-4 = 16px
const thumbnailScrollPosition = (thumbnailWidth + thumbnailGap) * currentSketchIndex;
// 计算脚本文字滚动位置
const scriptElement = scriptContainer.children[currentSketchIndex] as HTMLElement;
const scriptScrollPosition = scriptElement?.offsetLeft ?? 0;
// 平滑滚动到目标位置
thumbnailContainer.scrollTo({
left: thumbnailScrollPosition - thumbnailContainer.clientWidth / 2 + thumbnailWidth / 2,
behavior: 'smooth'
});
scriptContainer.scrollTo({
left: scriptScrollPosition - scriptContainer.clientWidth / 2 + scriptElement?.clientWidth / 2,
behavior: 'smooth'
});
}
}, [currentSketchIndex]);
// 如果没有数据,显示空状态
if (sketches.length === 0) {
return (
<div className="flex flex-col items-center justify-center min-h-[400px] text-white/50">
<p></p>
</div>
);
}
return (
<div className="flex flex-col gap-6">
{/* 上部分 */}
<motion.div
className="space-y-6"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
>
{/* 分镜缩略图行 */}
<div className="relative">
<div
ref={thumbnailsRef}
className="flex gap-4 overflow-x-auto pb-2 pt-2 hide-scrollbar"
>
{sketches.map((sketch, index) => (
<motion.div
key={sketch.id || index}
className={cn(
'relative flex-shrink-0 w-32 aspect-video rounded-lg overflow-hidden cursor-pointer',
currentSketchIndex === index ? 'ring-2 ring-blue-500' : 'hover:ring-2 hover:ring-blue-500/50'
)}
onClick={() => onSketchSelect(index)}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<img
src={sketch.url}
alt={`分镜 ${index + 1}`}
className="w-full h-full object-cover"
/>
<div className="absolute bottom-0 left-0 right-0 p-1 bg-gradient-to-t from-black/60 to-transparent">
<span className="text-xs text-white/90"> {index + 1}</span>
</div>
</motion.div>
))}
</div>
</div>
{/* 脚本预览行 - 单行滚动 */}
<div className="relative group">
<div
ref={scriptsRef}
className="flex overflow-x-auto hide-scrollbar py-2 gap-1"
>
{mockScripts.map((script, index) => {
const isActive = currentSketchIndex === index;
return (
<motion.div
key={script.id}
className={cn(
'flex-shrink-0 cursor-pointer transition-all duration-300',
isActive ? 'text-white' : 'text-white/50 hover:text-white/80'
)}
onClick={() => onSketchSelect(index)}
initial={false}
animate={{
scale: isActive ? 1.02 : 1,
}}
>
<div className="flex items-center gap-2">
<span className="text-sm whitespace-nowrap">
{script.content}
</span>
{index < mockScripts.length - 1 && (
<span className="text-white/20">|</span>
)}
</div>
</motion.div>
);
})}
</div>
{/* 渐变遮罩 */}
<div className="absolute left-0 top-0 bottom-0 w-8 bg-gradient-to-r from-[#1a1b1e] to-transparent pointer-events-none opacity-0 group-hover:opacity-100 transition-opacity" />
<div className="absolute right-0 top-0 bottom-0 w-8 bg-gradient-to-l from-[#1a1b1e] to-transparent pointer-events-none opacity-0 group-hover:opacity-100 transition-opacity" />
</div>
</motion.div>
{/* 下部分 */}
<motion.div
className="grid grid-cols-2 gap-6"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
>
{/* 左列:脚本编辑器 */}
<div className="space-y-2">
<motion.textarea
className="w-full h-full p-4 rounded-lg bg-white/5 backdrop-blur-sm border border-white/10
text-white/90 text-sm resize-none focus:outline-none focus:ring-2 focus:ring-blue-500"
value={mockScripts[currentSketchIndex]?.content}
onChange={() => {}}
layoutId="script-editor"
/>
</div>
{/* 右列:预览和操作 */}
<div className="space-y-4">
{/* 选中的分镜预览 */}
<motion.div
className="aspect-video rounded-lg overflow-hidden"
layoutId={`sketch-preview-${currentSketchIndex}`}
>
<img
src={sketches[currentSketchIndex]?.url}
alt={`分镜 ${currentSketchIndex + 1}`}
className="w-full h-full object-cover"
/>
</motion.div>
{/* 操作按钮 */}
<div className="grid grid-cols-2 gap-2">
<button
onClick={() => console.log('删除分镜')}
className="flex items-center justify-center gap-2 px-4 py-3 bg-red-500/10 hover:bg-red-500/20
text-red-500 rounded-lg transition-colors"
>
<Trash2 className="w-4 h-4" />
<span></span>
</button>
<button
onClick={() => console.log('重新生成')}
className="flex items-center justify-center gap-2 px-4 py-3 bg-blue-500/10 hover:bg-blue-500/20
text-blue-500 rounded-lg transition-colors"
>
<RefreshCw className="w-4 h-4" />
<span></span>
</button>
</div>
</div>
</motion.div>
</div>
);
}

View File

@ -0,0 +1,111 @@
// src/components/VantaHaloBackground.jsx
'use client';
import React, { useRef, useEffect, memo } from 'react'
import dynamic from 'next/dynamic'
import * as THREE from 'three'
interface VantaHaloBackgroundProps {
onLoaded?: () => void;
}
// 预加载 Vanta 脚本
const preloadVantaScript = () => {
const link = document.createElement('link')
link.rel = 'preload'
link.as = 'script'
link.href = '/lib/vanta.halo.min.js' // 确保路径正确
document.head.appendChild(link)
}
// 使用 React.memo 来避免不必要的重渲染
const VantaHaloBackground = memo(({ onLoaded }: VantaHaloBackgroundProps) => {
const vantaRef = useRef<HTMLDivElement>(null)
const effectInstance = useRef<any>(null)
const frameId = useRef<number>()
useEffect(() => {
let canceled = false
preloadVantaScript()
const loadVanta = async () => {
try {
const VANTA = await import('../lib/vanta.halo.min.js')
if (canceled || !vantaRef.current || effectInstance.current) return
// 使用 requestAnimationFrame 来控制动画帧率
const animate = () => {
if (effectInstance.current) {
effectInstance.current.frameRequestId = requestAnimationFrame(animate)
}
}
effectInstance.current = VANTA.default({
el: vantaRef.current,
THREE,
mouseControls: true,
touchControls: true,
gyroControls: false,
scale: 1.0,
scaleMobile: 1.0,
amplitudeFactor: 1.5,
ringFactor: 1.3,
size: 1.2,
minHeight: 200.00,
minWidth: 200.00,
// 优化渲染性能的参数
fps: 30, // 限制帧率
renderCacheSize: 4, // 缓存大小
})
frameId.current = requestAnimationFrame(animate)
// 通知加载完成
if (onLoaded) {
onLoaded();
}
} catch (error) {
console.error('Failed to load Vanta effect:', error)
}
}
// 使用 requestIdleCallback 在浏览器空闲时初始化
if ('requestIdleCallback' in window) {
requestIdleCallback(() => loadVanta(), { timeout: 2000 })
} else {
setTimeout(loadVanta, 100)
}
return () => {
canceled = true
if (frameId.current) {
cancelAnimationFrame(frameId.current)
}
if (effectInstance.current) {
effectInstance.current.destroy()
effectInstance.current = null
}
}
}, [onLoaded])
return (
<div
ref={vantaRef}
style={{
width: '61.8vw',
height: '100vh',
position: 'fixed',
top: 0,
left: 0,
zIndex: -1,
willChange: 'transform', // 优化图层合成
transform: 'translateZ(0)', // 启用硬件加速
}}
/>
)
})
VantaHaloBackground.displayName = 'VantaHaloBackground'
export default VantaHaloBackground

1
lib/vanta.halo.min.js vendored Normal file

File diff suppressed because one or more lines are too long

227
package-lock.json generated
View File

@ -44,6 +44,7 @@
"@types/react": "18.2.22",
"@types/react-beautiful-dnd": "^13.1.8",
"@types/react-dom": "18.2.7",
"@types/three": "^0.177.0",
"antd": "^5.26.2",
"autoprefixer": "10.4.15",
"class-variance-authority": "^0.7.0",
@ -67,20 +68,24 @@
"react-dom": "18.2.0",
"react-grid-layout": "^1.5.1",
"react-hook-form": "^7.53.0",
"react-intersection-observer": "^9.16.0",
"react-resizable": "^3.0.5",
"react-resizable-panels": "^2.1.3",
"react-textarea-autosize": "^8.5.9",
"recharts": "^2.12.7",
"sonner": "^1.5.0",
"styled-components": "^6.1.19",
"swiper": "^11.2.8",
"tailwind-merge": "^2.5.2",
"tailwindcss": "3.3.3",
"tailwindcss-animate": "^1.0.7",
"three": "^0.177.0",
"typescript": "5.2.2",
"vaul": "^0.9.9",
"zod": "^3.23.8"
},
"devDependencies": {
"@types/lodash": "^4.17.19",
"@types/react-grid-layout": "^1.3.5"
}
},
@ -201,6 +206,12 @@
"node": ">=6.9.0"
}
},
"node_modules/@dimforge/rapier3d-compat": {
"version": "0.12.0",
"resolved": "https://registry.npmmirror.com/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz",
"integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==",
"license": "Apache-2.0"
},
"node_modules/@dnd-kit/accessibility": {
"version": "3.1.1",
"resolved": "https://registry.npmmirror.com/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
@ -260,6 +271,21 @@
"integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==",
"license": "MIT"
},
"node_modules/@emotion/is-prop-valid": {
"version": "1.2.2",
"resolved": "https://registry.npmmirror.com/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz",
"integrity": "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==",
"license": "MIT",
"dependencies": {
"@emotion/memoize": "^0.8.1"
}
},
"node_modules/@emotion/memoize": {
"version": "0.8.1",
"resolved": "https://registry.npmmirror.com/@emotion/memoize/-/memoize-0.8.1.tgz",
"integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==",
"license": "MIT"
},
"node_modules/@emotion/unitless": {
"version": "0.7.5",
"resolved": "https://registry.npmmirror.com/@emotion/unitless/-/unitless-0.7.5.tgz",
@ -2178,6 +2204,12 @@
"tslib": "^2.4.0"
}
},
"node_modules/@tweenjs/tween.js": {
"version": "23.1.3",
"resolved": "https://registry.npmmirror.com/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
"integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==",
"license": "MIT"
},
"node_modules/@types/d3-array": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz",
@ -2247,6 +2279,13 @@
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="
},
"node_modules/@types/lodash": {
"version": "4.17.19",
"resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.19.tgz",
"integrity": "sha512-NYqRyg/hIQrYPT9lbOeYc3kIRabJDn/k4qQHIXUpx88CBDww2fD15Sg5kbXlW86zm2XEW4g0QxkTI3/Kfkc7xQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/node": {
"version": "20.6.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.6.2.tgz",
@ -2311,6 +2350,39 @@
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.23.0.tgz",
"integrity": "sha512-YIoDCTH3Af6XM5VuwGG/QL/CJqga1Zm3NkU3HZ4ZHK2fRMPYP1VczsTUqtsf43PH/iJNVlPHAo2oWX7BSdB2Hw=="
},
"node_modules/@types/stats.js": {
"version": "0.17.4",
"resolved": "https://registry.npmmirror.com/@types/stats.js/-/stats.js-0.17.4.tgz",
"integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==",
"license": "MIT"
},
"node_modules/@types/stylis": {
"version": "4.2.5",
"resolved": "https://registry.npmmirror.com/@types/stylis/-/stylis-4.2.5.tgz",
"integrity": "sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==",
"license": "MIT"
},
"node_modules/@types/three": {
"version": "0.177.0",
"resolved": "https://registry.npmmirror.com/@types/three/-/three-0.177.0.tgz",
"integrity": "sha512-/ZAkn4OLUijKQySNci47lFO+4JLE1TihEjsGWPUT+4jWqxtwOPPEwJV1C3k5MEx0mcBPCdkFjzRzDOnHEI1R+A==",
"license": "MIT",
"dependencies": {
"@dimforge/rapier3d-compat": "~0.12.0",
"@tweenjs/tween.js": "~23.1.3",
"@types/stats.js": "*",
"@types/webxr": "*",
"@webgpu/types": "*",
"fflate": "~0.8.2",
"meshoptimizer": "~0.18.1"
}
},
"node_modules/@types/webxr": {
"version": "0.5.22",
"resolved": "https://registry.npmmirror.com/@types/webxr/-/webxr-0.5.22.tgz",
"integrity": "sha512-Vr6Stjv5jPRqH690f5I5GLjVk8GSsoQSYJ2FVd/3jJF7KaqfwPi3ehfBS96mlQ2kPCwZaX6U0rG2+NGHBKkA/A==",
"license": "MIT"
},
"node_modules/@typescript-eslint/parser": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz",
@ -2431,6 +2503,12 @@
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@webgpu/types": {
"version": "0.1.63",
"resolved": "https://registry.npmmirror.com/@webgpu/types/-/types-0.1.63.tgz",
"integrity": "sha512-s9Kuh0nE/2+nKrvmKNMB2fE5Zlr3DL2t3OFKM55v5jRcfCOxbkOHhQoshoFum5mmXIfEtRXtLCWmkeTJsVjE9w==",
"license": "BSD-3-Clause"
},
"node_modules/acorn": {
"version": "8.12.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
@ -2931,6 +3009,15 @@
"node": ">= 6"
}
},
"node_modules/camelize": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/camelize/-/camelize-1.0.1.tgz",
"integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001667",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001667.tgz",
@ -3463,6 +3550,26 @@
"tiny-invariant": "^1.0.6"
}
},
"node_modules/css-color-keywords": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
"integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==",
"license": "ISC",
"engines": {
"node": ">=4"
}
},
"node_modules/css-to-react-native": {
"version": "3.2.0",
"resolved": "https://registry.npmmirror.com/css-to-react-native/-/css-to-react-native-3.2.0.tgz",
"integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==",
"license": "MIT",
"dependencies": {
"camelize": "^1.0.0",
"css-color-keywords": "^1.0.0",
"postcss-value-parser": "^4.0.2"
}
},
"node_modules/cssesc": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
@ -4495,6 +4602,12 @@
"reusify": "^1.0.4"
}
},
"node_modules/fflate": {
"version": "0.8.2",
"resolved": "https://registry.npmmirror.com/fflate/-/fflate-0.8.2.tgz",
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
"license": "MIT"
},
"node_modules/file-entry-cache": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
@ -5577,6 +5690,12 @@
"node": ">= 8"
}
},
"node_modules/meshoptimizer": {
"version": "0.18.1",
"resolved": "https://registry.npmmirror.com/meshoptimizer/-/meshoptimizer-0.18.1.tgz",
"integrity": "sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==",
"license": "MIT"
},
"node_modules/micromatch": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
@ -6048,9 +6167,10 @@
}
},
"node_modules/picocolors": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
"integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw=="
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"license": "ISC"
},
"node_modules/picomatch": {
"version": "2.3.1",
@ -7015,6 +7135,21 @@
"react": "^16.8.0 || ^17 || ^18 || ^19"
}
},
"node_modules/react-intersection-observer": {
"version": "9.16.0",
"resolved": "https://registry.npmmirror.com/react-intersection-observer/-/react-intersection-observer-9.16.0.tgz",
"integrity": "sha512-w9nJSEp+DrW9KmQmeWHQyfaP6b03v+TdXynaoA964Wxt7mdR3An11z4NNCQgL4gKSK7y1ver2Fq+JKH6CWEzUA==",
"license": "MIT",
"peerDependencies": {
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
}
}
},
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
@ -7456,6 +7591,12 @@
"node": ">= 0.4"
}
},
"node_modules/shallowequal": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/shallowequal/-/shallowequal-1.1.0.tgz",
"integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==",
"license": "MIT"
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@ -7744,6 +7885,80 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/styled-components": {
"version": "6.1.19",
"resolved": "https://registry.npmmirror.com/styled-components/-/styled-components-6.1.19.tgz",
"integrity": "sha512-1v/e3Dl1BknC37cXMhwGomhO8AkYmN41CqyX9xhUDxry1ns3BFQy2lLDRQXJRdVVWB9OHemv/53xaStimvWyuA==",
"license": "MIT",
"dependencies": {
"@emotion/is-prop-valid": "1.2.2",
"@emotion/unitless": "0.8.1",
"@types/stylis": "4.2.5",
"css-to-react-native": "3.2.0",
"csstype": "3.1.3",
"postcss": "8.4.49",
"shallowequal": "1.1.0",
"stylis": "4.3.2",
"tslib": "2.6.2"
},
"engines": {
"node": ">= 16"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/styled-components"
},
"peerDependencies": {
"react": ">= 16.8.0",
"react-dom": ">= 16.8.0"
}
},
"node_modules/styled-components/node_modules/@emotion/unitless": {
"version": "0.8.1",
"resolved": "https://registry.npmmirror.com/@emotion/unitless/-/unitless-0.8.1.tgz",
"integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==",
"license": "MIT"
},
"node_modules/styled-components/node_modules/postcss": {
"version": "8.4.49",
"resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.4.49.tgz",
"integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.7",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/styled-components/node_modules/stylis": {
"version": "4.3.2",
"resolved": "https://registry.npmmirror.com/stylis/-/stylis-4.3.2.tgz",
"integrity": "sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==",
"license": "MIT"
},
"node_modules/styled-components/node_modules/tslib": {
"version": "2.6.2",
"resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.6.2.tgz",
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==",
"license": "0BSD"
},
"node_modules/styled-jsx": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz",
@ -7960,6 +8175,12 @@
"node": ">=0.8"
}
},
"node_modules/three": {
"version": "0.177.0",
"resolved": "https://registry.npmmirror.com/three/-/three-0.177.0.tgz",
"integrity": "sha512-EiXv5/qWAaGI+Vz2A+JfavwYCMdGjxVsrn3oBwllUoqYeaBO75J63ZfyaQKoiLrqNHoTlUc6PFgMXnS0kI45zg==",
"license": "MIT"
},
"node_modules/throttle-debounce": {
"version": "5.0.2",
"resolved": "https://registry.npmmirror.com/throttle-debounce/-/throttle-debounce-5.0.2.tgz",

View File

@ -45,6 +45,7 @@
"@types/react": "18.2.22",
"@types/react-beautiful-dnd": "^13.1.8",
"@types/react-dom": "18.2.7",
"@types/three": "^0.177.0",
"antd": "^5.26.2",
"autoprefixer": "10.4.15",
"class-variance-authority": "^0.7.0",
@ -68,20 +69,24 @@
"react-dom": "18.2.0",
"react-grid-layout": "^1.5.1",
"react-hook-form": "^7.53.0",
"react-intersection-observer": "^9.16.0",
"react-resizable": "^3.0.5",
"react-resizable-panels": "^2.1.3",
"react-textarea-autosize": "^8.5.9",
"recharts": "^2.12.7",
"sonner": "^1.5.0",
"styled-components": "^6.1.19",
"swiper": "^11.2.8",
"tailwind-merge": "^2.5.2",
"tailwindcss": "3.3.3",
"tailwindcss-animate": "^1.0.7",
"three": "^0.177.0",
"typescript": "5.2.2",
"vaul": "^0.9.9",
"zod": "^3.23.8"
},
"devDependencies": {
"@types/lodash": "^4.17.19",
"@types/react-grid-layout": "^1.3.5"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
public/assets/3dr_mono.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

6
public/js/three.min.js vendored Normal file

File diff suppressed because one or more lines are too long