forked from 77media/video-flow
第三版-登录页
This commit is contained in:
parent
d9ed81ca3b
commit
2dc9a34241
5
app/login/page.tsx
Normal file
5
app/login/page.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
import Login from "@/components/pages/login";
|
||||
|
||||
export default function LoginPage() {
|
||||
return <Login />;
|
||||
}
|
||||
@ -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"
|
||||
>
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<Lightbulb className="w-4 h-4 text-yellow-500" />
|
||||
<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
|
||||
}}
|
||||
>
|
||||
<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={`
|
||||
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]'}
|
||||
`}>
|
||||
<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>
|
||||
</motion.button>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
54
components/pages/login.tsx
Normal file
54
components/pages/login.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
302
components/pages/style/login.css
Normal file
302
components/pages/style/login.css
Normal 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;
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
@ -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,35 +239,533 @@ export default function WorkFlow() {
|
||||
console.log('Submitted text:', text);
|
||||
};
|
||||
|
||||
// 渲染分镜草图或加载动画
|
||||
// 缓存渲染的缩略图列表
|
||||
const renderedSketches = useMemo(() =>
|
||||
taskSketch.map((sketch, index) => (
|
||||
<motion.div
|
||||
key={sketch.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'
|
||||
}}
|
||||
>
|
||||
<motion.img
|
||||
className="w-full h-full object-cover"
|
||||
src={sketch.url}
|
||||
alt={`缩略图 ${index + 1}`}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
/>
|
||||
<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>
|
||||
)), [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 (!taskSketch[currentSketchIndex]) {
|
||||
if (!taskObject) {
|
||||
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 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 (
|
||||
<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 }}
|
||||
/>
|
||||
<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">
|
||||
<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">
|
||||
@ -201,7 +778,62 @@ export default function WorkFlow() {
|
||||
) : (
|
||||
<>
|
||||
<div className="title-JtMejk">{taskObject?.projectName}:{taskObject?.taskName}</div>
|
||||
<p className="normalS400 subtitle-had8uE">{taskObject?.taskDescription}</p>
|
||||
{/* 实时反馈当前 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>
|
||||
@ -212,31 +844,13 @@ export default function WorkFlow() {
|
||||
<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 className="heroVideo-FIzuK1" style={{ aspectRatio: "16 / 9" }}>
|
||||
{renderSketchContent()}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="imageGrid-ymZV9z">
|
||||
<div className="imageGrid-ymZV9z hidden-scrollbar">
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Skeleton className="w-[128px] h-[128px] rounded-lg" />
|
||||
@ -247,59 +861,99 @@ export default function WorkFlow() {
|
||||
) : (
|
||||
<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"
|
||||
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 === '1' ? (
|
||||
{currentStep === '3' ? (
|
||||
<>
|
||||
{taskSketch.map((sketch, index) => (
|
||||
<motion.div
|
||||
key={sketch.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'
|
||||
}}
|
||||
{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 }}
|
||||
>
|
||||
<motion.img
|
||||
className="w-full h-full object-cover"
|
||||
src={sketch.url}
|
||||
alt={`缩略图 ${index + 1}`}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 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">场景 {index + 1}</span>
|
||||
<span className="text-xs text-white/90">场景 {taskVideos.length + 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" />
|
||||
</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>
|
||||
<AISuggestionBar
|
||||
suggestions={mockSuggestions}
|
||||
onSuggestionClick={handleSuggestionClick}
|
||||
onSubmit={handleSubmit}
|
||||
placeholder="请输入你的想法,或点击预设词条获取 AI 建议..."
|
||||
/>
|
||||
|
||||
<EditModal
|
||||
isOpen={isEditModalOpen}
|
||||
onClose={() => setIsEditModalOpen(false)}
|
||||
taskStatus={taskObject?.taskStatus || '1'}
|
||||
taskSketch={taskSketch}
|
||||
currentSketchIndex={currentSketchIndex}
|
||||
onSketchSelect={setCurrentSketchIndex}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
221
components/parallax.tsx
Normal file
221
components/parallax.tsx
Normal 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;
|
||||
173
components/ui/edit-modal.tsx
Normal file
173
components/ui/edit-modal.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
71
components/ui/glass-icon-button.tsx
Normal file
71
components/ui/glass-icon-button.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
202
components/ui/script-tab-content.tsx
Normal file
202
components/ui/script-tab-content.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
111
components/vanta-halo-background.tsx
Normal file
111
components/vanta-halo-background.tsx
Normal 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
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
227
package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
BIN
public/assets/3dr_chihiro.png
Normal file
BIN
public/assets/3dr_chihiro.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
BIN
public/assets/3dr_howlbg.jpg
Normal file
BIN
public/assets/3dr_howlbg.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.3 KiB |
BIN
public/assets/3dr_howlcastle.png
Normal file
BIN
public/assets/3dr_howlcastle.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
BIN
public/assets/3dr_mono.png
Normal file
BIN
public/assets/3dr_mono.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
BIN
public/assets/3dr_monobg.jpg
Normal file
BIN
public/assets/3dr_monobg.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.0 KiB |
BIN
public/assets/3dr_spirited.jpg
Normal file
BIN
public/assets/3dr_spirited.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.9 KiB |
6
public/js/three.min.js
vendored
Normal file
6
public/js/three.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user