forked from 77media/video-flow
版本三继续中
This commit is contained in:
parent
2dc9a34241
commit
8d85eee872
@ -9,6 +9,7 @@ import { motion, AnimatePresence } from "framer-motion";
|
|||||||
import { debounce } from "lodash";
|
import { debounce } from "lodash";
|
||||||
import { GlassIconButton } from "@/components/ui/glass-icon-button";
|
import { GlassIconButton } from "@/components/ui/glass-icon-button";
|
||||||
import { EditModal } from "@/components/ui/edit-modal";
|
import { EditModal } from "@/components/ui/edit-modal";
|
||||||
|
import { ProgressiveReveal, presets } from '@/components/ui/progressive-reveal';
|
||||||
|
|
||||||
const MOCK_SKETCH_URLS = [
|
const MOCK_SKETCH_URLS = [
|
||||||
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-1.jpg',
|
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-1.jpg',
|
||||||
@ -31,6 +32,12 @@ const MOCK_VIDEO_URLS = [
|
|||||||
|
|
||||||
const MOCK_SKETCH_COUNT = 8;
|
const MOCK_SKETCH_COUNT = 8;
|
||||||
|
|
||||||
|
const MOCK_FINAL_VIDEO = {
|
||||||
|
id: 'final-video',
|
||||||
|
url: 'https://cdn.qikongjian.com/videos/1750389908_37d4fffa-8516-43a3-a423-fc0274f40e8a_text_to_video_0.mp4',
|
||||||
|
thumbnail: 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-1.jpg',
|
||||||
|
};
|
||||||
|
|
||||||
export default function WorkFlow() {
|
export default function WorkFlow() {
|
||||||
const [taskObject, setTaskObject] = useState<any>(null);
|
const [taskObject, setTaskObject] = useState<any>(null);
|
||||||
const [projectObject, setProjectObject] = useState<any>(null);
|
const [projectObject, setProjectObject] = useState<any>(null);
|
||||||
@ -48,6 +55,7 @@ export default function WorkFlow() {
|
|||||||
const [scrollLeft, setScrollLeft] = useState(0);
|
const [scrollLeft, setScrollLeft] = useState(0);
|
||||||
const [showControls, setShowControls] = useState(false);
|
const [showControls, setShowControls] = useState(false);
|
||||||
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
||||||
|
const [activeEditTab, setActiveEditTab] = useState('1');
|
||||||
const [taskVideos, setTaskVideos] = useState<any[]>([]);
|
const [taskVideos, setTaskVideos] = useState<any[]>([]);
|
||||||
const [isGeneratingVideo, setIsGeneratingVideo] = useState(false);
|
const [isGeneratingVideo, setIsGeneratingVideo] = useState(false);
|
||||||
const mainVideoRef = useRef<HTMLVideoElement>(null);
|
const mainVideoRef = useRef<HTMLVideoElement>(null);
|
||||||
@ -92,6 +100,32 @@ export default function WorkFlow() {
|
|||||||
setCurrentStep('3');
|
setCurrentStep('3');
|
||||||
// 获取绘制角色后,开始获取分镜视频
|
// 获取绘制角色后,开始获取分镜视频
|
||||||
await getTaskVideo(taskId);
|
await getTaskVideo(taskId);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||||
|
// 首先修改 taskObject 下的 taskStatus 为 '4'
|
||||||
|
setTaskObject((prev: any) => ({
|
||||||
|
...prev,
|
||||||
|
taskStatus: '4'
|
||||||
|
}));
|
||||||
|
setCurrentStep('4');
|
||||||
|
// 获取分镜视频后,开始获取背景音
|
||||||
|
await getTaskBackgroundAudio(taskId);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||||
|
// 首先修改 taskObject 下的 taskStatus 为 '5'
|
||||||
|
setTaskObject((prev: any) => ({
|
||||||
|
...prev,
|
||||||
|
taskStatus: '5'
|
||||||
|
}));
|
||||||
|
setCurrentStep('5');
|
||||||
|
// 获取背景音后,开始获取最终成品
|
||||||
|
await getTaskFinalProduct(taskId);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||||
|
// 首先修改 taskObject 下的 taskStatus 为 '6'
|
||||||
|
setTaskObject((prev: any) => ({
|
||||||
|
...prev,
|
||||||
|
taskStatus: '6'
|
||||||
|
}));
|
||||||
|
setCurrentStep('6');
|
||||||
|
// 获取最终成品后,任务完成
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@ -109,6 +143,15 @@ export default function WorkFlow() {
|
|||||||
}
|
}
|
||||||
}, [currentSketchIndex, taskSketch.length]);
|
}, [currentSketchIndex, taskSketch.length]);
|
||||||
|
|
||||||
|
const handleEditModalOpen = (tab: string) => {
|
||||||
|
// 停止循环播放
|
||||||
|
setIsPlaying(false);
|
||||||
|
// 停止分镜视频播放
|
||||||
|
setIsVideoPlaying(false);
|
||||||
|
setActiveEditTab(tab);
|
||||||
|
setIsEditModalOpen(true);
|
||||||
|
}
|
||||||
|
|
||||||
// 处理鼠标/触摸拖动事件
|
// 处理鼠标/触摸拖动事件
|
||||||
const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
|
const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
setIsDragging(true);
|
setIsDragging(true);
|
||||||
@ -156,7 +199,7 @@ export default function WorkFlow() {
|
|||||||
taskDescription: "Task 1 Description",
|
taskDescription: "Task 1 Description",
|
||||||
taskStatus: "1", // '1' 绘制分镜、'2' 绘制角色、'3' 生成分镜视频、'4' 视频后期制作、'5' 最终成品
|
taskStatus: "1", // '1' 绘制分镜、'2' 绘制角色、'3' 生成分镜视频、'4' 视频后期制作、'5' 最终成品
|
||||||
taskProgress: 0,
|
taskProgress: 0,
|
||||||
mode: 'auto', // 托管模式、人工干预模式
|
mode: 'auto', // 全自动模式、人工干预模式
|
||||||
resolution: '1080p', // 1080p、2160p
|
resolution: '1080p', // 1080p、2160p
|
||||||
taskCreatedAt: new Date().toISOString(),
|
taskCreatedAt: new Date().toISOString(),
|
||||||
taskUpdatedAt: new Date().toISOString(),
|
taskUpdatedAt: new Date().toISOString(),
|
||||||
@ -202,6 +245,16 @@ export default function WorkFlow() {
|
|||||||
await new Promise(resolve => setTimeout(resolve, 2000)); // 模拟2秒延迟
|
await new Promise(resolve => setTimeout(resolve, 2000)); // 模拟2秒延迟
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 模拟接口请求 获取背景音
|
||||||
|
const getTaskBackgroundAudio = async (taskId: string) => {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2000)); // 模拟2秒延迟
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模拟接口请求 获取最终成品
|
||||||
|
const getTaskFinalProduct = async (taskId: string) => {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2000)); // 模拟2秒延迟
|
||||||
|
}
|
||||||
|
|
||||||
// 模拟接口请求 每次获取一个分镜视频 轮询获取
|
// 模拟接口请求 每次获取一个分镜视频 轮询获取
|
||||||
const getTaskVideo = async (taskId: string) => {
|
const getTaskVideo = async (taskId: string) => {
|
||||||
setIsGeneratingVideo(true);
|
setIsGeneratingVideo(true);
|
||||||
@ -209,7 +262,7 @@ export default function WorkFlow() {
|
|||||||
|
|
||||||
// 模拟分批获取分镜视频
|
// 模拟分批获取分镜视频
|
||||||
for (let i = 0; i < MOCK_SKETCH_COUNT; i++) {
|
for (let i = 0; i < MOCK_SKETCH_COUNT; i++) {
|
||||||
await new Promise(resolve => setTimeout(resolve, 2000)); // 模拟2秒延迟
|
await new Promise(resolve => setTimeout(resolve, 5000)); // 模拟5秒延迟
|
||||||
|
|
||||||
const newVideo = {
|
const newVideo = {
|
||||||
id: `video-${i}`,
|
id: `video-${i}`,
|
||||||
@ -242,80 +295,107 @@ export default function WorkFlow() {
|
|||||||
// 缓存渲染的缩略图列表
|
// 缓存渲染的缩略图列表
|
||||||
const renderedSketches = useMemo(() =>
|
const renderedSketches = useMemo(() =>
|
||||||
taskSketch.map((sketch, index) => (
|
taskSketch.map((sketch, index) => (
|
||||||
<motion.div
|
<div
|
||||||
key={sketch.id}
|
key={`sketch-${index}`}
|
||||||
className={`relative aspect-video rounded-lg overflow-hidden
|
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'}`}
|
${currentSketchIndex === index ? 'ring-2 ring-blue-500 z-10' : 'hover:ring-2 hover:ring-blue-500/50'}`}
|
||||||
onClick={() => !isDragging && setCurrentSketchIndex(index)}
|
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
|
<ProgressiveReveal
|
||||||
className="w-full h-full object-cover"
|
{...presets.thumbnail}
|
||||||
src={sketch.url}
|
delay={index * 0.1}
|
||||||
alt={`缩略图 ${index + 1}`}
|
customVariants={{
|
||||||
initial={{ opacity: 0 }}
|
hidden: {
|
||||||
animate={{ opacity: 1 }}
|
opacity: 0,
|
||||||
transition={{ duration: 0.3 }}
|
scale: 0.95,
|
||||||
/>
|
filter: "blur(10px)"
|
||||||
|
},
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
scale: 1,
|
||||||
|
filter: "blur(0px)",
|
||||||
|
transition: {
|
||||||
|
duration: 0.8,
|
||||||
|
ease: [0.23, 1, 0.32, 1], // cubic-bezier
|
||||||
|
opacity: { duration: 0.6, ease: "easeInOut" },
|
||||||
|
scale: { duration: 1, ease: "easeOut" },
|
||||||
|
filter: { duration: 0.8, ease: "easeOut", delay: 0.2 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
loadingBgConfig={{
|
||||||
|
...presets.thumbnail.loadingBgConfig,
|
||||||
|
glowOpacity: 0.4,
|
||||||
|
duration: 4
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="w-full h-full transform hover:scale-105 transition-transform duration-500">
|
||||||
|
<img
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
src={sketch.url}
|
||||||
|
alt={`缩略图 ${index + 1}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ProgressiveReveal>
|
||||||
<div className="absolute bottom-0 left-0 right-0 p-2 bg-gradient-to-t from-black/60 to-transparent">
|
<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">场景 {index + 1}</span>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</div>
|
||||||
)), [taskSketch, currentSketchIndex, isDragging]
|
)), [taskSketch, currentSketchIndex, isDragging]
|
||||||
);
|
);
|
||||||
|
|
||||||
// 缓存渲染的视频缩略图列表
|
// 缓存渲染的视频缩略图列表
|
||||||
const renderedVideos = useMemo(() =>
|
const renderedVideos = useMemo(() =>
|
||||||
taskVideos.map((video, index) => (
|
taskVideos.map((video, index) => (
|
||||||
<motion.div
|
<div
|
||||||
key={video.id}
|
key={`video-${index}`}
|
||||||
className={`relative aspect-video rounded-lg overflow-hidden
|
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'}`}
|
${currentSketchIndex === index ? 'ring-2 ring-blue-500 z-10' : 'hover:ring-2 hover:ring-blue-500/50'}`}
|
||||||
onClick={() => !isDragging && setCurrentSketchIndex(index)}
|
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
|
<ProgressiveReveal
|
||||||
className="w-full h-full object-cover"
|
{...presets.thumbnail}
|
||||||
src={video.url}
|
delay={index * 0.1}
|
||||||
muted
|
customVariants={{
|
||||||
playsInline
|
hidden: {
|
||||||
loop
|
opacity: 0,
|
||||||
poster={taskSketch[index]?.url}
|
scale: 0.95,
|
||||||
/>
|
filter: "blur(10px)"
|
||||||
|
},
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
scale: 1,
|
||||||
|
filter: "blur(0px)",
|
||||||
|
transition: {
|
||||||
|
duration: 0.8,
|
||||||
|
ease: [0.23, 1, 0.32, 1], // cubic-bezier
|
||||||
|
opacity: { duration: 0.6, ease: "easeInOut" },
|
||||||
|
scale: { duration: 1, ease: "easeOut" },
|
||||||
|
filter: { duration: 0.8, ease: "easeOut", delay: 0.2 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
loadingBgConfig={{
|
||||||
|
...presets.thumbnail.loadingBgConfig,
|
||||||
|
glowOpacity: 0.4,
|
||||||
|
duration: 4
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="w-full h-full transform hover:scale-105 transition-transform duration-500">
|
||||||
|
<video
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
src={video.url}
|
||||||
|
muted
|
||||||
|
playsInline
|
||||||
|
loop
|
||||||
|
poster={taskSketch[index]?.url}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ProgressiveReveal>
|
||||||
<div className="absolute bottom-0 left-0 right-0 p-2 bg-gradient-to-t from-black/60 to-transparent">
|
<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">场景 {index + 1}</span>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</div>
|
||||||
)), [taskVideos, currentSketchIndex, isDragging, taskSketch]
|
)), [taskVideos, currentSketchIndex, isDragging, taskSketch]
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -353,6 +433,14 @@ export default function WorkFlow() {
|
|||||||
|
|
||||||
// 处理视频播放/暂停
|
// 处理视频播放/暂停
|
||||||
const toggleVideoPlay = useCallback(() => {
|
const toggleVideoPlay = useCallback(() => {
|
||||||
|
if (mainVideoRef.current) {
|
||||||
|
if (isVideoPlaying) {
|
||||||
|
mainVideoRef.current.pause();
|
||||||
|
} else {
|
||||||
|
// 从暂停位置继续播放
|
||||||
|
mainVideoRef.current.play();
|
||||||
|
}
|
||||||
|
}
|
||||||
setIsVideoPlaying(prev => !prev);
|
setIsVideoPlaying(prev => !prev);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@ -361,7 +449,10 @@ export default function WorkFlow() {
|
|||||||
if (isVideoPlaying && taskVideos.length > 0) {
|
if (isVideoPlaying && taskVideos.length > 0) {
|
||||||
// 确保当前视频开始播放
|
// 确保当前视频开始播放
|
||||||
if (mainVideoRef.current) {
|
if (mainVideoRef.current) {
|
||||||
mainVideoRef.current.play();
|
mainVideoRef.current.play().catch(error => {
|
||||||
|
console.log('视频播放失败:', error);
|
||||||
|
setIsVideoPlaying(false);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 暂停当前视频
|
// 暂停当前视频
|
||||||
@ -384,12 +475,16 @@ export default function WorkFlow() {
|
|||||||
// 当切换视频时重置视频播放
|
// 当切换视频时重置视频播放
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (mainVideoRef.current) {
|
if (mainVideoRef.current) {
|
||||||
|
// 只有在切换视频时才重置时间
|
||||||
mainVideoRef.current.currentTime = 0;
|
mainVideoRef.current.currentTime = 0;
|
||||||
if (isVideoPlaying) {
|
if (isVideoPlaying) {
|
||||||
mainVideoRef.current.play();
|
mainVideoRef.current.play().catch(error => {
|
||||||
|
console.log('视频播放失败:', error);
|
||||||
|
setIsVideoPlaying(false);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [currentSketchIndex, isVideoPlaying]);
|
}, [currentSketchIndex]);
|
||||||
|
|
||||||
// 当切换到分镜草图模式时,停止视频播放
|
// 当切换到分镜草图模式时,停止视频播放
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -419,6 +514,12 @@ export default function WorkFlow() {
|
|||||||
} else {
|
} else {
|
||||||
setCurrentLoadingText('分镜视频生成完成');
|
setCurrentLoadingText('分镜视频生成完成');
|
||||||
}
|
}
|
||||||
|
} else if (currentStep === '4') {
|
||||||
|
setCurrentLoadingText('正在生成背景音...');
|
||||||
|
} else if (currentStep === '5') {
|
||||||
|
setCurrentLoadingText('正在生成最终成品...');
|
||||||
|
} else {
|
||||||
|
setCurrentLoadingText('任务完成');
|
||||||
}
|
}
|
||||||
}, [isLoading, currentStep, isGeneratingSketch, sketchCount, isGeneratingVideo, taskVideos.length, taskSketch.length]);
|
}, [isLoading, currentStep, isGeneratingSketch, sketchCount, isGeneratingVideo, taskVideos.length, taskSketch.length]);
|
||||||
|
|
||||||
@ -463,16 +564,19 @@ export default function WorkFlow() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentStep === '3') {
|
if (Number(currentStep) > 2 && Number(currentStep) < 6) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="relative w-full h-full"
|
className="relative w-full h-full rounded-lg"
|
||||||
onMouseEnter={() => setShowControls(true)}
|
onMouseEnter={() => setShowControls(true)}
|
||||||
onMouseLeave={() => setShowControls(false)}
|
onMouseLeave={() => setShowControls(false)}
|
||||||
>
|
>
|
||||||
{taskVideos[currentSketchIndex] ? (
|
{taskVideos[currentSketchIndex] ? (
|
||||||
<motion.div className="relative w-full h-full">
|
<ProgressiveReveal
|
||||||
<motion.video
|
className="w-full h-full rounded-lg"
|
||||||
|
{...presets.main}
|
||||||
|
>
|
||||||
|
<video
|
||||||
ref={mainVideoRef}
|
ref={mainVideoRef}
|
||||||
key={taskVideos[currentSketchIndex].url}
|
key={taskVideos[currentSketchIndex].url}
|
||||||
className="w-full h-full rounded-lg object-cover object-center"
|
className="w-full h-full rounded-lg object-cover object-center"
|
||||||
@ -482,52 +586,54 @@ export default function WorkFlow() {
|
|||||||
loop={false}
|
loop={false}
|
||||||
playsInline
|
playsInline
|
||||||
poster={taskSketch[currentSketchIndex]?.url}
|
poster={taskSketch[currentSketchIndex]?.url}
|
||||||
initial={{ opacity: 0, scale: 0.95 }}
|
|
||||||
animate={{
|
|
||||||
opacity: 1,
|
|
||||||
scale: 1,
|
|
||||||
transition: {
|
|
||||||
type: "spring",
|
|
||||||
stiffness: 300,
|
|
||||||
damping: 25
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onEnded={() => {
|
onEnded={() => {
|
||||||
if (isVideoPlaying) {
|
if (isVideoPlaying) {
|
||||||
// 当前视频播放完成后,自动切换到下一个
|
|
||||||
setCurrentSketchIndex(prev => (prev + 1) % taskVideos.length);
|
setCurrentSketchIndex(prev => (prev + 1) % taskVideos.length);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
</ProgressiveReveal>
|
||||||
{/* 播放进度指示器 */}
|
|
||||||
<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">
|
<div className="w-full h-full flex items-center justify-center rounded-lg overflow-hidden relative">
|
||||||
|
{/* 动态渐变背景 */}
|
||||||
<motion.div
|
<motion.div
|
||||||
className="flex flex-col items-center gap-4"
|
className="absolute inset-0 bg-gradient-to-r from-cyan-300 via-sky-400 to-blue-500"
|
||||||
|
animate={{
|
||||||
|
backgroundPosition: ["0% 50%", "100% 50%", "0% 50%"],
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
duration: 5,
|
||||||
|
repeat: Infinity,
|
||||||
|
ease: "linear"
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
backgroundSize: "200% 200%",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/* 动态光效 */}
|
||||||
|
<motion.div
|
||||||
|
className="absolute inset-0 opacity-50"
|
||||||
|
style={{
|
||||||
|
background: "radial-gradient(circle at center, rgba(255,255,255,0.8) 0%, transparent 50%)",
|
||||||
|
}}
|
||||||
|
animate={{
|
||||||
|
scale: [1, 1.2, 1],
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
duration: 2,
|
||||||
|
repeat: Infinity,
|
||||||
|
ease: "easeInOut"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<motion.div
|
||||||
|
className="flex flex-col items-center gap-4 relative z-10"
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.5 }}
|
transition={{ duration: 0.5 }}
|
||||||
>
|
>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<motion.div
|
<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"
|
className="absolute -inset-4 bg-gradient-to-r from-white via-sky-200 to-cyan-200 rounded-full opacity-60 blur-xl"
|
||||||
animate={{
|
animate={{
|
||||||
scale: [1, 1.2, 1],
|
scale: [1, 1.2, 1],
|
||||||
rotate: [0, 180, 360],
|
rotate: [0, 180, 360],
|
||||||
@ -548,13 +654,12 @@ export default function WorkFlow() {
|
|||||||
ease: "linear"
|
ease: "linear"
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Loader2 className="w-8 h-8 text-blue-500 relative z-10" />
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
<motion.p
|
<motion.p
|
||||||
className="text-sm text-white/70"
|
className="text-sm text-white font-medium"
|
||||||
animate={{
|
animate={{
|
||||||
opacity: [0.5, 1, 0.5],
|
opacity: [0.7, 1, 0.7],
|
||||||
}}
|
}}
|
||||||
transition={{
|
transition={{
|
||||||
duration: 2,
|
duration: 2,
|
||||||
@ -583,7 +688,7 @@ export default function WorkFlow() {
|
|||||||
<GlassIconButton
|
<GlassIconButton
|
||||||
icon={Edit3}
|
icon={Edit3}
|
||||||
tooltip="编辑分镜"
|
tooltip="编辑分镜"
|
||||||
onClick={() => setIsEditModalOpen(true)}
|
onClick={() => handleEditModalOpen('3')}
|
||||||
/>
|
/>
|
||||||
{/* <GlassIconButton
|
{/* <GlassIconButton
|
||||||
icon={FileText}
|
icon={FileText}
|
||||||
@ -621,40 +726,160 @@ export default function WorkFlow() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 展示最终成片
|
||||||
|
if (Number(currentStep) === 6) {
|
||||||
|
return (
|
||||||
|
<div className="relative w-full h-full rounded-lg overflow-hidden">
|
||||||
|
<ProgressiveReveal
|
||||||
|
className="w-full h-full rounded-lg"
|
||||||
|
customVariants={{
|
||||||
|
hidden: {
|
||||||
|
opacity: 0,
|
||||||
|
filter: "blur(20px)",
|
||||||
|
clipPath: "inset(0 50% 0 50%)"
|
||||||
|
},
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
filter: "blur(0px)",
|
||||||
|
clipPath: "inset(0 0% 0 0%)",
|
||||||
|
transition: {
|
||||||
|
duration: 1.2,
|
||||||
|
ease: [0.43, 0.13, 0.23, 0.96], // 自定义缓动函数
|
||||||
|
opacity: { duration: 0.8, ease: "easeOut" },
|
||||||
|
filter: { duration: 0.6, ease: "easeOut" },
|
||||||
|
clipPath: { duration: 1, ease: "easeInOut", delay: 0.2 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
loadingBgConfig={{
|
||||||
|
fromColor: 'from-emerald-300',
|
||||||
|
viaColor: 'via-cyan-400',
|
||||||
|
toColor: 'to-blue-500',
|
||||||
|
glowOpacity: 0.6,
|
||||||
|
duration: 6
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="relative w-full h-full">
|
||||||
|
{/* 最终成片视频 */}
|
||||||
|
<video
|
||||||
|
className="w-full h-full object-cover rounded-lg"
|
||||||
|
src={MOCK_FINAL_VIDEO.url}
|
||||||
|
poster={MOCK_FINAL_VIDEO.thumbnail}
|
||||||
|
autoPlay
|
||||||
|
loop
|
||||||
|
muted
|
||||||
|
playsInline
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 视频信息浮层 */}
|
||||||
|
<motion.div
|
||||||
|
className="absolute bottom-0 left-0 right-0 p-4 bg-gradient-to-t from-black/80 via-black/40 to-transparent"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 1, duration: 0.6 }}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<motion.div
|
||||||
|
className="w-2 h-2 rounded-full bg-emerald-500"
|
||||||
|
animate={{
|
||||||
|
scale: [1, 1.2, 1],
|
||||||
|
opacity: [1, 0.6, 1]
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
duration: 2,
|
||||||
|
repeat: Infinity,
|
||||||
|
ease: "easeInOut"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className="text-sm font-medium text-white/90">最终成片</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<GlassIconButton
|
||||||
|
icon={Edit3}
|
||||||
|
tooltip="编辑成片"
|
||||||
|
onClick={() => handleEditModalOpen('4')}
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* 完成标记 */}
|
||||||
|
<motion.div
|
||||||
|
className="absolute top-4 right-4 px-3 py-1.5 rounded-full bg-emerald-500/20 backdrop-blur-sm
|
||||||
|
border border-emerald-500/30 text-emerald-400 text-sm font-medium"
|
||||||
|
initial={{ opacity: 0, scale: 0.8, x: 20 }}
|
||||||
|
animate={{ opacity: 1, scale: 1, x: 0 }}
|
||||||
|
transition={{ delay: 1.2, duration: 0.6 }}
|
||||||
|
>
|
||||||
|
制作完成
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</ProgressiveReveal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="relative w-full h-full"
|
className="relative w-full h-full rounded-lg"
|
||||||
onMouseEnter={() => setShowControls(true)}
|
onMouseEnter={() => setShowControls(true)}
|
||||||
onMouseLeave={() => setShowControls(false)}
|
onMouseLeave={() => setShowControls(false)}
|
||||||
>
|
>
|
||||||
{taskSketch[currentSketchIndex] ? (
|
{taskSketch[currentSketchIndex] ? (
|
||||||
<motion.img
|
<ProgressiveReveal
|
||||||
key={currentSketchIndex}
|
className="w-full h-full rounded-lg"
|
||||||
src={taskSketch[currentSketchIndex].url}
|
{...presets.main}
|
||||||
alt={`分镜草图 ${currentSketchIndex + 1}`}
|
>
|
||||||
className="w-full h-full rounded-lg"
|
<img
|
||||||
initial={{ opacity: 0, scale: 0.95 }}
|
key={currentSketchIndex}
|
||||||
animate={{
|
src={taskSketch[currentSketchIndex].url}
|
||||||
opacity: 1,
|
alt={`分镜草图 ${currentSketchIndex + 1}`}
|
||||||
scale: 1,
|
className="w-full h-full rounded-lg object-cover"
|
||||||
transition: {
|
/>
|
||||||
type: "spring",
|
</ProgressiveReveal>
|
||||||
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">
|
<div className="w-full h-full flex items-center justify-center rounded-lg overflow-hidden relative">
|
||||||
|
{/* 动态渐变背景 */}
|
||||||
<motion.div
|
<motion.div
|
||||||
className="flex flex-col items-center gap-4"
|
className="absolute inset-0 bg-gradient-to-r from-cyan-300 via-sky-400 to-blue-500"
|
||||||
|
animate={{
|
||||||
|
backgroundPosition: ["0% 50%", "100% 50%", "0% 50%"],
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
duration: 5,
|
||||||
|
repeat: Infinity,
|
||||||
|
ease: "linear"
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
backgroundSize: "200% 200%",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/* 动态光效 */}
|
||||||
|
<motion.div
|
||||||
|
className="absolute inset-0 opacity-50"
|
||||||
|
style={{
|
||||||
|
background: "radial-gradient(circle at center, rgba(255,255,255,0.8) 0%, transparent 50%)",
|
||||||
|
}}
|
||||||
|
animate={{
|
||||||
|
scale: [1, 1.2, 1],
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
duration: 2,
|
||||||
|
repeat: Infinity,
|
||||||
|
ease: "easeInOut"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<motion.div
|
||||||
|
className="flex flex-col items-center gap-4 relative z-10"
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.5 }}
|
transition={{ duration: 0.5 }}
|
||||||
>
|
>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<motion.div
|
<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"
|
className="absolute -inset-4 bg-gradient-to-r from-white via-sky-200 to-cyan-200 rounded-full opacity-60 blur-xl"
|
||||||
animate={{
|
animate={{
|
||||||
scale: [1, 1.2, 1],
|
scale: [1, 1.2, 1],
|
||||||
rotate: [0, 180, 360],
|
rotate: [0, 180, 360],
|
||||||
@ -675,13 +900,12 @@ export default function WorkFlow() {
|
|||||||
ease: "linear"
|
ease: "linear"
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Loader2 className="w-8 h-8 text-blue-500 relative z-10" />
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
<motion.p
|
<motion.p
|
||||||
className="text-sm text-white/70"
|
className="text-sm text-white font-medium"
|
||||||
animate={{
|
animate={{
|
||||||
opacity: [0.5, 1, 0.5],
|
opacity: [0.7, 1, 0.7],
|
||||||
}}
|
}}
|
||||||
transition={{
|
transition={{
|
||||||
duration: 2,
|
duration: 2,
|
||||||
@ -710,7 +934,7 @@ export default function WorkFlow() {
|
|||||||
<GlassIconButton
|
<GlassIconButton
|
||||||
icon={Edit3}
|
icon={Edit3}
|
||||||
tooltip="编辑分镜"
|
tooltip="编辑分镜"
|
||||||
onClick={() => setIsEditModalOpen(true)}
|
onClick={() => handleEditModalOpen('1')}
|
||||||
/>
|
/>
|
||||||
{/* <GlassIconButton
|
{/* <GlassIconButton
|
||||||
icon={FileText}
|
icon={FileText}
|
||||||
@ -867,20 +1091,50 @@ export default function WorkFlow() {
|
|||||||
onMouseUp={handleMouseUp}
|
onMouseUp={handleMouseUp}
|
||||||
onMouseLeave={() => setIsDragging(false)}
|
onMouseLeave={() => setIsDragging(false)}
|
||||||
>
|
>
|
||||||
{currentStep === '3' ? (
|
{(Number(currentStep) > 2 && Number(currentStep) < 6) ? (
|
||||||
<>
|
<>
|
||||||
{renderedVideos}
|
{renderedVideos}
|
||||||
{isGeneratingVideo && taskVideos.length < taskSketch.length && (
|
{isGeneratingVideo && taskVideos.length < taskSketch.length && (
|
||||||
<motion.div
|
<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"
|
className="relative aspect-video rounded-lg overflow-hidden"
|
||||||
initial={{ opacity: 0, scale: 0.8 }}
|
initial={{ opacity: 0, scale: 0.8 }}
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
transition={{ duration: 0.3 }}
|
transition={{ duration: 0.3 }}
|
||||||
>
|
>
|
||||||
|
{/* 动态渐变背景 */}
|
||||||
|
<motion.div
|
||||||
|
className="absolute inset-0 bg-gradient-to-r from-cyan-300 via-sky-400 to-blue-500"
|
||||||
|
animate={{
|
||||||
|
backgroundPosition: ["0% 50%", "100% 50%", "0% 50%"],
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
duration: 5,
|
||||||
|
repeat: Infinity,
|
||||||
|
ease: "linear"
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
backgroundSize: "200% 200%",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/* 动态光效 */}
|
||||||
|
<motion.div
|
||||||
|
className="absolute inset-0 opacity-50"
|
||||||
|
style={{
|
||||||
|
background: "radial-gradient(circle at center, rgba(255,255,255,0.8) 0%, transparent 50%)",
|
||||||
|
}}
|
||||||
|
animate={{
|
||||||
|
scale: [1, 1.2, 1],
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
duration: 2,
|
||||||
|
repeat: Infinity,
|
||||||
|
ease: "easeInOut"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<div className="absolute inset-0 flex items-center justify-center">
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<motion.div
|
<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"
|
className="absolute -inset-4 bg-gradient-to-r from-white via-sky-200 to-cyan-200 rounded-full opacity-60 blur-xl"
|
||||||
animate={{
|
animate={{
|
||||||
scale: [1, 1.2, 1],
|
scale: [1, 1.2, 1],
|
||||||
rotate: [0, 180, 360],
|
rotate: [0, 180, 360],
|
||||||
@ -901,7 +1155,6 @@ export default function WorkFlow() {
|
|||||||
ease: "linear"
|
ease: "linear"
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Loader2 className="w-6 h-6 text-blue-500 relative z-10" />
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -911,20 +1164,69 @@ export default function WorkFlow() {
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
) : Number(currentStep) === 6 ? (
|
||||||
|
<motion.div
|
||||||
|
className="relative aspect-video rounded-lg overflow-hidden"
|
||||||
|
initial={{ opacity: 0, scale: 0.9 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
transition={{ duration: 0.6, delay: 1.4 }}
|
||||||
|
>
|
||||||
|
<video
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
src={MOCK_FINAL_VIDEO.url}
|
||||||
|
poster={MOCK_FINAL_VIDEO.thumbnail}
|
||||||
|
muted
|
||||||
|
playsInline
|
||||||
|
loop
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center bg-black/40 backdrop-blur-sm">
|
||||||
|
<span className="text-sm font-medium text-white">最终成片</span>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{renderedSketches}
|
{renderedSketches}
|
||||||
{isGeneratingSketch && sketchCount < MOCK_SKETCH_COUNT && (
|
{isGeneratingSketch && sketchCount < MOCK_SKETCH_COUNT && (
|
||||||
<motion.div
|
<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"
|
className="relative aspect-video rounded-lg overflow-hidden"
|
||||||
initial={{ opacity: 0, scale: 0.8 }}
|
initial={{ opacity: 0, scale: 0.8 }}
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
transition={{ duration: 0.3 }}
|
transition={{ duration: 0.3 }}
|
||||||
>
|
>
|
||||||
|
{/* 动态渐变背景 */}
|
||||||
|
<motion.div
|
||||||
|
className="absolute inset-0 bg-gradient-to-r from-cyan-300 via-sky-400 to-blue-500"
|
||||||
|
animate={{
|
||||||
|
backgroundPosition: ["0% 50%", "100% 50%", "0% 50%"],
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
duration: 5,
|
||||||
|
repeat: Infinity,
|
||||||
|
ease: "linear"
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
backgroundSize: "200% 200%",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/* 动态光效 */}
|
||||||
|
<motion.div
|
||||||
|
className="absolute inset-0 opacity-50"
|
||||||
|
style={{
|
||||||
|
background: "radial-gradient(circle at center, rgba(255,255,255,0.8) 0%, transparent 50%)",
|
||||||
|
}}
|
||||||
|
animate={{
|
||||||
|
scale: [1, 1.2, 1],
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
duration: 2,
|
||||||
|
repeat: Infinity,
|
||||||
|
ease: "easeInOut"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<div className="absolute inset-0 flex items-center justify-center">
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<motion.div
|
<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"
|
className="absolute -inset-4 bg-gradient-to-r from-white via-sky-200 to-cyan-200 rounded-full opacity-60 blur-xl"
|
||||||
animate={{
|
animate={{
|
||||||
scale: [1, 1.2, 1],
|
scale: [1, 1.2, 1],
|
||||||
rotate: [0, 180, 360],
|
rotate: [0, 180, 360],
|
||||||
@ -945,7 +1247,6 @@ export default function WorkFlow() {
|
|||||||
ease: "linear"
|
ease: "linear"
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Loader2 className="w-6 h-6 text-blue-500 relative z-10" />
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -974,9 +1275,11 @@ export default function WorkFlow() {
|
|||||||
|
|
||||||
<EditModal
|
<EditModal
|
||||||
isOpen={isEditModalOpen}
|
isOpen={isEditModalOpen}
|
||||||
|
activeEditTab={activeEditTab}
|
||||||
onClose={() => setIsEditModalOpen(false)}
|
onClose={() => setIsEditModalOpen(false)}
|
||||||
taskStatus={taskObject?.taskStatus || '1'}
|
taskStatus={taskObject?.taskStatus || '1'}
|
||||||
taskSketch={taskSketch}
|
taskSketch={taskSketch}
|
||||||
|
sketchVideo={taskVideos}
|
||||||
currentSketchIndex={currentSketchIndex}
|
currentSketchIndex={currentSketchIndex}
|
||||||
onSketchSelect={setCurrentSketchIndex}
|
onSketchSelect={setCurrentSketchIndex}
|
||||||
/>
|
/>
|
||||||
|
|||||||
292
components/ui/character-tab-content.tsx
Normal file
292
components/ui/character-tab-content.tsx
Normal file
@ -0,0 +1,292 @@
|
|||||||
|
import React, { useState, useRef } from 'react';
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
|
import { Upload, Library, Play, Pause, RefreshCw, Wand2 } from 'lucide-react';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { GlassIconButton } from './glass-icon-button';
|
||||||
|
import { ReplaceCharacterModal } from './replace-character-modal';
|
||||||
|
|
||||||
|
interface CharacterTabContentProps {
|
||||||
|
taskSketch: any[];
|
||||||
|
currentSketchIndex: number;
|
||||||
|
onSketchSelect: (index: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模拟角色数据
|
||||||
|
const MOCK_CHARACTERS = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: '雪 (YUKI)',
|
||||||
|
avatar: '/assets/3dr_chihiro.png',
|
||||||
|
voiceDescription: '年轻女性,温柔而坚定的声线,语速适中,带有轻微的感性色彩。',
|
||||||
|
characterDescription: '一位接近二十岁或二十出头的年轻女性,东亚裔,拥有深色长发和刘海,五官柔和且富有表现力。',
|
||||||
|
voiceUrl: 'https://example.com/voice-sample.mp3'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: '春 (HARU)',
|
||||||
|
avatar: '/assets/3dr_mono.png',
|
||||||
|
voiceDescription: '年轻男性,清澈而温和的声线,语速从容,带有知性的特质。',
|
||||||
|
characterDescription: '一位接近二十岁或二十出头的年轻男性,东亚裔,拥有深色、发型整洁的中长发和深思的气质。',
|
||||||
|
voiceUrl: 'https://example.com/voice-sample.mp3'
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function CharacterTabContent({
|
||||||
|
taskSketch,
|
||||||
|
currentSketchIndex,
|
||||||
|
onSketchSelect
|
||||||
|
}: CharacterTabContentProps) {
|
||||||
|
const [selectedCharacterIndex, setSelectedCharacterIndex] = useState(0);
|
||||||
|
const [isReplaceModalOpen, setIsReplaceModalOpen] = useState(false);
|
||||||
|
const [activeReplaceMethod, setActiveReplaceMethod] = useState('upload');
|
||||||
|
const [isPlaying, setIsPlaying] = useState(false);
|
||||||
|
const [progress, setProgress] = useState(0);
|
||||||
|
const [editingField, setEditingField] = useState<{
|
||||||
|
type: 'name' | 'voiceDescription' | 'characterDescription' | null;
|
||||||
|
value: string;
|
||||||
|
}>({ type: null, value: '' });
|
||||||
|
const audioRef = useRef<HTMLAudioElement>(null);
|
||||||
|
|
||||||
|
// 处理音频播放进度
|
||||||
|
const handleTimeUpdate = () => {
|
||||||
|
if (audioRef.current) {
|
||||||
|
const progress = (audioRef.current.currentTime / audioRef.current.duration) * 100;
|
||||||
|
setProgress(progress);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理播放/暂停
|
||||||
|
const togglePlay = () => {
|
||||||
|
if (audioRef.current) {
|
||||||
|
if (isPlaying) {
|
||||||
|
audioRef.current.pause();
|
||||||
|
} else {
|
||||||
|
audioRef.current.play();
|
||||||
|
}
|
||||||
|
setIsPlaying(!isPlaying);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理进度条点击
|
||||||
|
const handleProgressClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
if (audioRef.current) {
|
||||||
|
const rect = e.currentTarget.getBoundingClientRect();
|
||||||
|
const x = e.clientX - rect.left;
|
||||||
|
const percentage = (x / rect.width) * 100;
|
||||||
|
const time = (percentage / 100) * audioRef.current.duration;
|
||||||
|
audioRef.current.currentTime = time;
|
||||||
|
setProgress(percentage);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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 className="flex gap-4 overflow-x-auto p-2 hide-scrollbar">
|
||||||
|
{MOCK_CHARACTERS.map((character, index) => (
|
||||||
|
<motion.div
|
||||||
|
key={character.id}
|
||||||
|
className={cn(
|
||||||
|
'relative flex-shrink-0 w-24 rounded-lg overflow-hidden cursor-pointer',
|
||||||
|
'aspect-[9/16]',
|
||||||
|
selectedCharacterIndex === index ? 'ring-2 ring-blue-500' : 'hover:ring-2 hover:ring-blue-500/50'
|
||||||
|
)}
|
||||||
|
onClick={() => setSelectedCharacterIndex(index)}
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={character.avatar}
|
||||||
|
alt={character.name}
|
||||||
|
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 line-clamp-1">{character.name}</span>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* 中间部分:替换角色 */}
|
||||||
|
<motion.div
|
||||||
|
className="p-4 rounded-lg bg-white/5"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.1 }}
|
||||||
|
>
|
||||||
|
<h3 className="text-sm font-medium mb-2">替换角色</h3>
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<motion.button
|
||||||
|
className="flex-1 flex flex-col items-center gap-2 p-4 rounded-lg border border-white/10 hover:border-white/20 transition-colors"
|
||||||
|
onClick={() => {
|
||||||
|
setActiveReplaceMethod('upload');
|
||||||
|
setIsReplaceModalOpen(true);
|
||||||
|
}}
|
||||||
|
whileHover={{ scale: 1.02 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
<Upload className="w-6 h-6" />
|
||||||
|
<span>上传角色</span>
|
||||||
|
</motion.button>
|
||||||
|
|
||||||
|
<motion.button
|
||||||
|
className="flex-1 flex flex-col items-center gap-2 p-4 rounded-lg border border-white/10 hover:border-white/20 transition-colors"
|
||||||
|
onClick={() => {
|
||||||
|
setActiveReplaceMethod('library');
|
||||||
|
setIsReplaceModalOpen(true);
|
||||||
|
}}
|
||||||
|
whileHover={{ scale: 1.02 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
<Library className="w-6 h-6" />
|
||||||
|
<span>角色库</span>
|
||||||
|
</motion.button>
|
||||||
|
</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.2 }}
|
||||||
|
>
|
||||||
|
{/* 左列:角色信息 */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* 角色姓名 */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm text-white/70">角色姓名</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={MOCK_CHARACTERS[selectedCharacterIndex].name}
|
||||||
|
onChange={(e) => console.log('name changed:', e.target.value)}
|
||||||
|
className="w-full px-3 py-2 bg-white/5 border border-white/10 rounded-lg
|
||||||
|
focus:outline-none focus:border-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/* 声音描述 */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm text-white/70">声音描述</label>
|
||||||
|
<textarea
|
||||||
|
value={MOCK_CHARACTERS[selectedCharacterIndex].voiceDescription}
|
||||||
|
onChange={(e) => console.log('voice description changed:', e.target.value)}
|
||||||
|
className="w-full h-24 px-3 py-2 bg-white/5 border border-white/10 rounded-lg
|
||||||
|
focus:outline-none focus:border-blue-500 resize-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 声音预览 */}
|
||||||
|
<div className="p-4 rounded-lg bg-white/5 space-y-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-white/70">声音预览</span>
|
||||||
|
<GlassIconButton
|
||||||
|
icon={RefreshCw}
|
||||||
|
tooltip="重新生成声音"
|
||||||
|
onClick={() => console.log('regenerate voice')}
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative">
|
||||||
|
<audio
|
||||||
|
ref={audioRef}
|
||||||
|
src={MOCK_CHARACTERS[selectedCharacterIndex].voiceUrl}
|
||||||
|
onTimeUpdate={handleTimeUpdate}
|
||||||
|
onEnded={() => setIsPlaying(false)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 进度条 */}
|
||||||
|
<div
|
||||||
|
className="w-full h-1 bg-white/10 rounded-full cursor-pointer overflow-hidden"
|
||||||
|
onClick={handleProgressClick}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
className="h-full bg-blue-500 rounded-full"
|
||||||
|
style={{ width: `${progress}%` }}
|
||||||
|
transition={{ type: "spring", stiffness: 300, damping: 25 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 播放控制 */}
|
||||||
|
<div className="mt-2 flex items-center gap-2">
|
||||||
|
<motion.button
|
||||||
|
className="p-2 rounded-full hover:bg-white/10"
|
||||||
|
onClick={togglePlay}
|
||||||
|
whileHover={{ scale: 1.1 }}
|
||||||
|
whileTap={{ scale: 0.9 }}
|
||||||
|
>
|
||||||
|
{isPlaying ? (
|
||||||
|
<Pause className="w-4 h-4" />
|
||||||
|
) : (
|
||||||
|
<Play className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
</motion.button>
|
||||||
|
<div className="text-xs text-white/50">
|
||||||
|
{audioRef.current ? (
|
||||||
|
`${Math.floor(audioRef.current.currentTime)}s / ${Math.floor(audioRef.current.duration)}s`
|
||||||
|
) : '0:00 / 0:00'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 右列:角色信息 */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* 角色描述 */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm text-white/70">角色描述</label>
|
||||||
|
<textarea
|
||||||
|
value={MOCK_CHARACTERS[selectedCharacterIndex].characterDescription}
|
||||||
|
onChange={(e) => console.log('character description changed:', e.target.value)}
|
||||||
|
className="w-full h-24 px-3 py-2 bg-white/5 border border-white/10 rounded-lg
|
||||||
|
focus:outline-none focus:border-blue-500 resize-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 角色预览 */}
|
||||||
|
<div className="w-full max-w-[280px] mx-auto aspect-[9/16] rounded-lg overflow-hidden relative group">
|
||||||
|
<img
|
||||||
|
src={MOCK_CHARACTERS[selectedCharacterIndex].avatar}
|
||||||
|
alt={MOCK_CHARACTERS[selectedCharacterIndex].name}
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent
|
||||||
|
opacity-100 transition-opacity">
|
||||||
|
<div className="absolute bottom-4 left-4">
|
||||||
|
<GlassIconButton
|
||||||
|
icon={Wand2}
|
||||||
|
tooltip="重新生成角色形象"
|
||||||
|
onClick={() => console.log('regenerate character')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* 替换角色弹窗 */}
|
||||||
|
<ReplaceCharacterModal
|
||||||
|
isOpen={isReplaceModalOpen}
|
||||||
|
activeReplaceMethod={activeReplaceMethod}
|
||||||
|
onClose={() => setIsReplaceModalOpen(false)}
|
||||||
|
onCharacterSelect={(character) => {
|
||||||
|
console.log('Selected character:', character);
|
||||||
|
setIsReplaceModalOpen(false);
|
||||||
|
// TODO: 处理角色选择逻辑
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,16 +1,22 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import { X, FileText, Users, Video, Music, Scissors, Settings } from 'lucide-react';
|
import { X, FileText, Users, Video, Music, Scissors, Settings } from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { ScriptTabContent } from './script-tab-content';
|
import { ScriptTabContent } from './script-tab-content';
|
||||||
|
import { VideoTabContent } from './video-tab-content';
|
||||||
|
import { SettingsTabContent } from './settings-tab-content';
|
||||||
|
import { CharacterTabContent } from './character-tab-content';
|
||||||
|
import { MusicTabContent } from './music-tab-content';
|
||||||
|
|
||||||
interface EditModalProps {
|
interface EditModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
activeEditTab: string;
|
||||||
taskStatus: string;
|
taskStatus: string;
|
||||||
taskSketch: any[];
|
taskSketch: any[];
|
||||||
|
sketchVideo: any[];
|
||||||
currentSketchIndex: number;
|
currentSketchIndex: number;
|
||||||
onSketchSelect: (index: number) => void;
|
onSketchSelect: (index: number) => void;
|
||||||
}
|
}
|
||||||
@ -19,20 +25,27 @@ const tabs = [
|
|||||||
{ id: '1', label: '脚本', icon: FileText },
|
{ id: '1', label: '脚本', icon: FileText },
|
||||||
{ id: '2', label: '角色', icon: Users },
|
{ id: '2', label: '角色', icon: Users },
|
||||||
{ id: '3', label: '分镜视频', icon: Video },
|
{ id: '3', label: '分镜视频', icon: Video },
|
||||||
{ id: '4', label: '背景音', icon: Music },
|
{ id: '4', label: '音乐', icon: Music },
|
||||||
{ id: '5', label: '剪辑', icon: Scissors },
|
// { id: '5', label: '剪辑', icon: Scissors },
|
||||||
{ id: 'settings', label: '设置', icon: Settings },
|
{ id: 'settings', label: '设置', icon: Settings },
|
||||||
];
|
];
|
||||||
|
|
||||||
export function EditModal({
|
export function EditModal({
|
||||||
isOpen,
|
isOpen,
|
||||||
onClose,
|
onClose,
|
||||||
|
activeEditTab,
|
||||||
taskStatus,
|
taskStatus,
|
||||||
taskSketch,
|
taskSketch,
|
||||||
|
sketchVideo,
|
||||||
currentSketchIndex,
|
currentSketchIndex,
|
||||||
onSketchSelect
|
onSketchSelect
|
||||||
}: EditModalProps) {
|
}: EditModalProps) {
|
||||||
const [activeTab, setActiveTab] = useState('1');
|
const [activeTab, setActiveTab] = useState(activeEditTab);
|
||||||
|
|
||||||
|
// 当 activeEditTab 改变时更新 activeTab
|
||||||
|
useEffect(() => {
|
||||||
|
setActiveTab(activeEditTab);
|
||||||
|
}, [activeEditTab]);
|
||||||
|
|
||||||
const isTabDisabled = (tabId: string) => {
|
const isTabDisabled = (tabId: string) => {
|
||||||
if (tabId === 'settings') return false;
|
if (tabId === 'settings') return false;
|
||||||
@ -49,6 +62,40 @@ export function EditModal({
|
|||||||
onSketchSelect={onSketchSelect}
|
onSketchSelect={onSketchSelect}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
case '2':
|
||||||
|
return (
|
||||||
|
<CharacterTabContent
|
||||||
|
taskSketch={taskSketch}
|
||||||
|
currentSketchIndex={currentSketchIndex}
|
||||||
|
onSketchSelect={onSketchSelect}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case '3':
|
||||||
|
return (
|
||||||
|
<VideoTabContent
|
||||||
|
taskSketch={sketchVideo}
|
||||||
|
currentSketchIndex={currentSketchIndex}
|
||||||
|
onSketchSelect={onSketchSelect}
|
||||||
|
isPlaying={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case '4':
|
||||||
|
return (
|
||||||
|
<MusicTabContent
|
||||||
|
taskSketch={taskSketch}
|
||||||
|
currentSketchIndex={currentSketchIndex}
|
||||||
|
onSketchSelect={onSketchSelect}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case 'settings':
|
||||||
|
return (
|
||||||
|
<SettingsTabContent
|
||||||
|
onSettingChange={(key, value) => {
|
||||||
|
console.log('Setting changed:', key, value);
|
||||||
|
// TODO: 实现设置更新逻辑
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
return (
|
return (
|
||||||
<div className="min-h-[400px] flex items-center justify-center text-white/50">
|
<div className="min-h-[400px] flex items-center justify-center text-white/50">
|
||||||
@ -153,7 +200,7 @@ export function EditModal({
|
|||||||
whileTap={{ scale: 0.98 }}
|
whileTap={{ scale: 0.98 }}
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
>
|
>
|
||||||
取消
|
重置
|
||||||
</motion.button>
|
</motion.button>
|
||||||
<motion.button
|
<motion.button
|
||||||
className="px-4 py-2 rounded-lg bg-blue-500 text-white hover:bg-blue-600 transition-colors"
|
className="px-4 py-2 rounded-lg bg-blue-500 text-white hover:bg-blue-600 transition-colors"
|
||||||
|
|||||||
151
components/ui/generate-video-modal.tsx
Normal file
151
components/ui/generate-video-modal.tsx
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
|
import { X, ChevronDown } from 'lucide-react';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
interface GenerateVideoModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onGenerate: (params: { text: string; duration: string }) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GenerateVideoModal({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
onGenerate
|
||||||
|
}: GenerateVideoModalProps) {
|
||||||
|
const [text, setText] = useState('');
|
||||||
|
const [duration, setDuration] = useState('5');
|
||||||
|
const [videoUrl, setVideoUrl] = useState('');
|
||||||
|
|
||||||
|
const handleGenerate = () => {
|
||||||
|
onGenerate({ text, duration });
|
||||||
|
};
|
||||||
|
|
||||||
|
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 justify-between p-4 border-b border-white/10">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
className="p-1 hover:bg-white/10 rounded-full transition-colors"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
<ChevronDown className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
<h2 className="text-lg font-medium">生成视频</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 主要内容区域 */}
|
||||||
|
<div className="p-6 space-y-6 h-[80vh] flex flex-col overflow-y-auto hidden-scrollbar">
|
||||||
|
{/* 文本输入区域 */}
|
||||||
|
<div className="space-y-2 flex-shrink-0">
|
||||||
|
<label className="text-sm text-white/70">描述场景</label>
|
||||||
|
<textarea
|
||||||
|
className="w-full h-32 p-4 bg-white/5 border border-white/10 rounded-lg
|
||||||
|
text-white placeholder-white/30 resize-none focus:outline-none focus:border-blue-500"
|
||||||
|
placeholder="描述你想要生成的视频场景..."
|
||||||
|
value={text}
|
||||||
|
onChange={(e) => setText(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 时长选择 */}
|
||||||
|
{/* <div className="space-y-2">
|
||||||
|
<label className="text-sm text-white/70">视频时长(秒)</label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{['5', '10', '15', '30'].map((value) => (
|
||||||
|
<motion.button
|
||||||
|
key={value}
|
||||||
|
className={cn(
|
||||||
|
'px-4 py-2 rounded-lg transition-colors',
|
||||||
|
duration === value
|
||||||
|
? 'bg-blue-500 text-white'
|
||||||
|
: 'bg-white/5 hover:bg-white/10'
|
||||||
|
)}
|
||||||
|
onClick={() => setDuration(value)}
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
>
|
||||||
|
{value}秒
|
||||||
|
</motion.button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div> */}
|
||||||
|
|
||||||
|
{/* 生成按钮 */}
|
||||||
|
<div className="flex justify-end flex-shrink-0">
|
||||||
|
<motion.button
|
||||||
|
className="px-6 py-2 bg-blue-500 hover:bg-blue-600 rounded-lg
|
||||||
|
transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
onClick={handleGenerate}
|
||||||
|
disabled={!text.trim()}
|
||||||
|
whileHover={{ scale: 1.02 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
生成视频
|
||||||
|
</motion.button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 生成视频预览 */}
|
||||||
|
<div className="flex justify-center flex-1 min-h-[450px]">
|
||||||
|
<div className="w-[80%] bg-white/5 rounded-lg">
|
||||||
|
<video src={videoUrl} className="w-full object-cover" autoPlay />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
326
components/ui/music-tab-content.tsx
Normal file
326
components/ui/music-tab-content.tsx
Normal file
@ -0,0 +1,326 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useState, useRef } from 'react';
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
|
import { Upload, Library, Play, Pause, RefreshCw, Music2, Volume2 } from 'lucide-react';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { GlassIconButton } from './glass-icon-button';
|
||||||
|
import { ReplaceMusicModal } from './replace-music-modal';
|
||||||
|
|
||||||
|
interface MusicTabContentProps {
|
||||||
|
taskSketch: any[];
|
||||||
|
currentSketchIndex: number;
|
||||||
|
onSketchSelect: (index: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模拟音乐数据
|
||||||
|
const MOCK_MUSICS = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'Music 1 / Rocka',
|
||||||
|
duration: '00m : 21s : 000ms',
|
||||||
|
totalDuration: '01m : 35s : 765ms',
|
||||||
|
isLooped: true,
|
||||||
|
url: 'https://example.com/music1.mp3'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export function MusicTabContent({
|
||||||
|
taskSketch,
|
||||||
|
currentSketchIndex,
|
||||||
|
onSketchSelect
|
||||||
|
}: MusicTabContentProps) {
|
||||||
|
const [selectedMusicIndex, setSelectedMusicIndex] = useState(0);
|
||||||
|
const [isReplaceModalOpen, setIsReplaceModalOpen] = useState(false);
|
||||||
|
const [activeMethod, setActiveMethod] = useState('upload');
|
||||||
|
const [isPlaying, setIsPlaying] = useState(false);
|
||||||
|
const [progress, setProgress] = useState(0);
|
||||||
|
const [volume, setVolume] = useState(75);
|
||||||
|
const audioRef = useRef<HTMLAudioElement>(null);
|
||||||
|
|
||||||
|
// 处理音频播放进度
|
||||||
|
const handleTimeUpdate = () => {
|
||||||
|
if (audioRef.current) {
|
||||||
|
const progress = (audioRef.current.currentTime / audioRef.current.duration) * 100;
|
||||||
|
setProgress(progress);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理播放/暂停
|
||||||
|
const togglePlay = () => {
|
||||||
|
if (audioRef.current) {
|
||||||
|
if (isPlaying) {
|
||||||
|
audioRef.current.pause();
|
||||||
|
} else {
|
||||||
|
audioRef.current.play();
|
||||||
|
}
|
||||||
|
setIsPlaying(!isPlaying);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理进度条点击
|
||||||
|
const handleProgressClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
if (audioRef.current) {
|
||||||
|
const rect = e.currentTarget.getBoundingClientRect();
|
||||||
|
const x = e.clientX - rect.left;
|
||||||
|
const percentage = (x / rect.width) * 100;
|
||||||
|
const time = (percentage / 100) * audioRef.current.duration;
|
||||||
|
audioRef.current.currentTime = time;
|
||||||
|
setProgress(percentage);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理音量变化
|
||||||
|
const handleVolumeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const newVolume = parseInt(e.target.value);
|
||||||
|
setVolume(newVolume);
|
||||||
|
if (audioRef.current) {
|
||||||
|
audioRef.current.volume = newVolume / 100;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-6">
|
||||||
|
{/* 上部分:音乐列表 */}
|
||||||
|
<motion.div
|
||||||
|
className="space-y-4"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
>
|
||||||
|
{MOCK_MUSICS.map((music, index) => (
|
||||||
|
<motion.div
|
||||||
|
key={music.id}
|
||||||
|
className={cn(
|
||||||
|
'group relative p-4 rounded-lg overflow-hidden cursor-pointer transition-colors w-[272px]',
|
||||||
|
selectedMusicIndex === index ? 'border border-blue-500 bg-blue-500/10' : ''
|
||||||
|
)}
|
||||||
|
onClick={() => setSelectedMusicIndex(index)}
|
||||||
|
whileHover={{ scale: 1.01 }}
|
||||||
|
whileTap={{ scale: 0.99 }}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<Music2 className="w-8 h-8 text-white/70" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="text-sm font-medium truncate">{music.name}</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* 中间部分:替换区 */}
|
||||||
|
<motion.div
|
||||||
|
className="p-4 rounded-lg bg-white/5"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.1 }}
|
||||||
|
>
|
||||||
|
<h3 className="text-sm font-medium mb-2">替换音乐</h3>
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<motion.button
|
||||||
|
className="flex-1 flex flex-col items-center gap-2 p-4 rounded-lg border border-white/10 hover:border-white/20 transition-colors"
|
||||||
|
onClick={() => setIsReplaceModalOpen(true)}
|
||||||
|
whileHover={{ scale: 1.02 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
<Upload className="w-6 h-6" />
|
||||||
|
<span>上传音乐</span>
|
||||||
|
</motion.button>
|
||||||
|
|
||||||
|
<motion.button
|
||||||
|
className="flex-1 flex flex-col items-center gap-2 p-4 rounded-lg border border-white/10 hover:border-white/20 transition-colors"
|
||||||
|
onClick={() => setIsReplaceModalOpen(true)}
|
||||||
|
whileHover={{ scale: 1.02 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
<Library className="w-6 h-6" />
|
||||||
|
<span>音乐库</span>
|
||||||
|
</motion.button>
|
||||||
|
</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.2 }}
|
||||||
|
>
|
||||||
|
{/* 左列:音乐编辑选项 */}
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Loop Music */}
|
||||||
|
<motion.div
|
||||||
|
className="space-y-2"
|
||||||
|
whileHover={{ scale: 1.01 }}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<label className="text-sm text-white/70">Loop music</label>
|
||||||
|
<motion.div
|
||||||
|
className={cn(
|
||||||
|
"w-12 h-6 rounded-full p-1 cursor-pointer",
|
||||||
|
MOCK_MUSICS[selectedMusicIndex].isLooped ? "bg-blue-500" : "bg-white/10"
|
||||||
|
)}
|
||||||
|
onClick={() => console.log('loop toggled')}
|
||||||
|
layout
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
className="w-4 h-4 bg-white rounded-full"
|
||||||
|
layout
|
||||||
|
animate={{
|
||||||
|
x: MOCK_MUSICS[selectedMusicIndex].isLooped ? "100%" : "0%"
|
||||||
|
}}
|
||||||
|
transition={{ type: "spring", stiffness: 300, damping: 25 }}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Trim */}
|
||||||
|
<motion.div
|
||||||
|
className="space-y-2"
|
||||||
|
whileHover={{ scale: 1.01 }}
|
||||||
|
>
|
||||||
|
<label className="text-sm text-white/70">Trim</label>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-sm text-white/50">from</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value="00 : 00"
|
||||||
|
className="w-20 px-2 py-1 bg-white/5 border border-white/10 rounded text-center
|
||||||
|
focus:outline-none focus:border-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-sm text-white/50">to</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value="01 : 35"
|
||||||
|
className="w-20 px-2 py-1 bg-white/5 border border-white/10 rounded text-center
|
||||||
|
focus:outline-none focus:border-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Fade in & out */}
|
||||||
|
<motion.div
|
||||||
|
className="space-y-4"
|
||||||
|
whileHover={{ scale: 1.01 }}
|
||||||
|
>
|
||||||
|
<label className="text-sm text-white/70">Fade in & out (max 10s)</label>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<span className="text-xs text-white/50">Fade in:</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value="0s"
|
||||||
|
className="w-full px-2 py-1 bg-white/5 border border-white/10 rounded text-center
|
||||||
|
focus:outline-none focus:border-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<span className="text-xs text-white/50">Fade out:</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value="3s"
|
||||||
|
className="w-full px-2 py-1 bg-white/5 border border-white/10 rounded text-center
|
||||||
|
focus:outline-none focus:border-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Music volume */}
|
||||||
|
<motion.div
|
||||||
|
className="space-y-2"
|
||||||
|
whileHover={{ scale: 1.01 }}
|
||||||
|
>
|
||||||
|
<label className="text-sm text-white/70">Music volume</label>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Volume2 className="w-4 h-4 text-white/50" />
|
||||||
|
<div className="flex-1 h-1 bg-white/10 rounded-full relative">
|
||||||
|
<motion.div
|
||||||
|
className="absolute h-full bg-blue-500 rounded-full"
|
||||||
|
style={{ width: `${volume}%` }}
|
||||||
|
layout
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
value={volume}
|
||||||
|
onChange={handleVolumeChange}
|
||||||
|
className="absolute w-full h-full opacity-0 cursor-pointer"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="text-sm text-white/50 w-12 text-right">{volume}%</span>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 右列:音频预览 */}
|
||||||
|
<motion.div
|
||||||
|
className="p-6 rounded-lg bg-white/5 flex flex-col items-center justify-center gap-6"
|
||||||
|
whileHover={{ scale: 1.01 }}
|
||||||
|
>
|
||||||
|
<div className="w-24 h-24 rounded-full bg-white/10 flex items-center justify-center relative">
|
||||||
|
<motion.div
|
||||||
|
className="absolute w-full h-full rounded-full border-2 border-blue-500 border-t-transparent"
|
||||||
|
animate={{ rotate: 360 }}
|
||||||
|
transition={{ duration: 3, repeat: Infinity, ease: "linear" }}
|
||||||
|
style={{ opacity: isPlaying ? 1 : 0 }}
|
||||||
|
/>
|
||||||
|
<motion.button
|
||||||
|
className="w-16 h-16 rounded-full bg-blue-500/20 hover:bg-blue-500/30
|
||||||
|
flex items-center justify-center transition-colors"
|
||||||
|
onClick={togglePlay}
|
||||||
|
whileHover={{ scale: 1.1 }}
|
||||||
|
whileTap={{ scale: 0.9 }}
|
||||||
|
>
|
||||||
|
{isPlaying ? (
|
||||||
|
<Pause className="w-8 h-8 text-blue-500" />
|
||||||
|
) : (
|
||||||
|
<Play className="w-8 h-8 text-blue-500" />
|
||||||
|
)}
|
||||||
|
</motion.button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full space-y-3">
|
||||||
|
<div
|
||||||
|
className="w-full h-1 bg-white/10 rounded-full cursor-pointer overflow-hidden"
|
||||||
|
onClick={handleProgressClick}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
className="h-full bg-blue-500 rounded-full"
|
||||||
|
style={{ width: `${progress}%` }}
|
||||||
|
layout
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-white/50 text-center">
|
||||||
|
{audioRef.current ? (
|
||||||
|
`${Math.floor(audioRef.current.currentTime)}s / ${Math.floor(audioRef.current.duration)}s`
|
||||||
|
) : '0:00 / 0:00'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* 替换音乐弹窗 */}
|
||||||
|
<ReplaceMusicModal
|
||||||
|
isOpen={isReplaceModalOpen}
|
||||||
|
activeReplaceMethod={activeMethod}
|
||||||
|
onClose={() => setIsReplaceModalOpen(false)}
|
||||||
|
onMusicSelect={(music) => {
|
||||||
|
console.log('Selected music:', music);
|
||||||
|
setIsReplaceModalOpen(false);
|
||||||
|
// TODO: 处理音乐选择逻辑
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
242
components/ui/progressive-reveal.tsx
Normal file
242
components/ui/progressive-reveal.tsx
Normal file
@ -0,0 +1,242 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
|
||||||
|
interface ProgressiveRevealProps {
|
||||||
|
/** 要显示的内容 */
|
||||||
|
children: React.ReactNode;
|
||||||
|
/** 容器类名 */
|
||||||
|
className?: string;
|
||||||
|
/** 是否为视频内容 */
|
||||||
|
isVideo?: boolean;
|
||||||
|
/** 动画延迟时间(秒) */
|
||||||
|
delay?: number;
|
||||||
|
/** 渐进显示动画时长(秒) */
|
||||||
|
revealDuration?: number;
|
||||||
|
/** 模糊过渡动画时长(秒) */
|
||||||
|
blurDuration?: number;
|
||||||
|
/** 初始模糊度 */
|
||||||
|
initialBlur?: number;
|
||||||
|
/** 自定义动画变体 */
|
||||||
|
customVariants?: {
|
||||||
|
hidden: any;
|
||||||
|
visible: any;
|
||||||
|
};
|
||||||
|
/** 自定义过渡效果 */
|
||||||
|
customTransition?: any;
|
||||||
|
/** 是否显示加载背景 */
|
||||||
|
showLoadingBg?: boolean;
|
||||||
|
/** 加载背景配置 */
|
||||||
|
loadingBgConfig?: {
|
||||||
|
/** 渐变色起始颜色 */
|
||||||
|
fromColor?: string;
|
||||||
|
/** 渐变色中间颜色 */
|
||||||
|
viaColor?: string;
|
||||||
|
/** 渐变色结束颜色 */
|
||||||
|
toColor?: string;
|
||||||
|
/** 光效不透明度 */
|
||||||
|
glowOpacity?: number;
|
||||||
|
/** 背景动画持续时间 */
|
||||||
|
duration?: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProgressiveReveal: React.FC<ProgressiveRevealProps> = ({
|
||||||
|
children,
|
||||||
|
className = '',
|
||||||
|
isVideo = false,
|
||||||
|
delay = 0,
|
||||||
|
revealDuration = 0.8,
|
||||||
|
blurDuration = 0.5,
|
||||||
|
initialBlur = 20,
|
||||||
|
customVariants,
|
||||||
|
customTransition,
|
||||||
|
showLoadingBg = true,
|
||||||
|
loadingBgConfig = {
|
||||||
|
fromColor: 'from-cyan-300',
|
||||||
|
viaColor: 'via-sky-400',
|
||||||
|
toColor: 'to-blue-500',
|
||||||
|
glowOpacity: 0.5,
|
||||||
|
duration: 5,
|
||||||
|
},
|
||||||
|
}) => {
|
||||||
|
// 默认动画变体
|
||||||
|
const defaultVariants = {
|
||||||
|
hidden: { opacity: 0 },
|
||||||
|
visible: { opacity: 1 }
|
||||||
|
};
|
||||||
|
|
||||||
|
// 默认过渡效果
|
||||||
|
const defaultTransition = {
|
||||||
|
duration: 0.3,
|
||||||
|
delay
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
className={`relative overflow-hidden ${className}`}
|
||||||
|
initial="hidden"
|
||||||
|
animate="visible"
|
||||||
|
variants={customVariants || defaultVariants}
|
||||||
|
transition={customTransition || defaultTransition}
|
||||||
|
>
|
||||||
|
{/* 加载背景 */}
|
||||||
|
{showLoadingBg && (
|
||||||
|
<>
|
||||||
|
{/* 动态渐变背景 */}
|
||||||
|
<motion.div
|
||||||
|
className={`absolute inset-0 bg-gradient-to-r ${loadingBgConfig.fromColor} ${loadingBgConfig.viaColor} ${loadingBgConfig.toColor}`}
|
||||||
|
animate={{
|
||||||
|
backgroundPosition: ["0% 50%", "100% 50%", "0% 50%"],
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
duration: loadingBgConfig.duration,
|
||||||
|
repeat: Infinity,
|
||||||
|
ease: "linear"
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
backgroundSize: "200% 200%",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/* 动态光效 */}
|
||||||
|
<motion.div
|
||||||
|
className="absolute inset-0"
|
||||||
|
style={{
|
||||||
|
background: "radial-gradient(circle at center, rgba(255,255,255,0.8) 0%, transparent 50%)",
|
||||||
|
opacity: loadingBgConfig.glowOpacity,
|
||||||
|
}}
|
||||||
|
animate={{
|
||||||
|
scale: [1, 1.2, 1],
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
duration: 2,
|
||||||
|
repeat: Infinity,
|
||||||
|
ease: "easeInOut"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 内容显示动画 */}
|
||||||
|
<motion.div
|
||||||
|
className="relative w-full h-full"
|
||||||
|
initial={{ clipPath: "inset(0 100% 0 0)" }}
|
||||||
|
animate={{ clipPath: "inset(0 0% 0 0)" }}
|
||||||
|
transition={{
|
||||||
|
duration: revealDuration,
|
||||||
|
ease: "easeInOut",
|
||||||
|
delay
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
className="w-full h-full"
|
||||||
|
initial={{ filter: `blur(${initialBlur}px)` }}
|
||||||
|
animate={{ filter: "blur(0px)" }}
|
||||||
|
transition={{
|
||||||
|
duration: blurDuration,
|
||||||
|
delay: delay + revealDuration,
|
||||||
|
ease: "easeOut"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 预设配置
|
||||||
|
export const presets = {
|
||||||
|
// 快速显示
|
||||||
|
fast: {
|
||||||
|
revealDuration: 0.4,
|
||||||
|
blurDuration: 0.3,
|
||||||
|
initialBlur: 10,
|
||||||
|
loadingBgConfig: {
|
||||||
|
fromColor: 'from-cyan-300',
|
||||||
|
viaColor: 'via-sky-400',
|
||||||
|
toColor: 'to-blue-500',
|
||||||
|
glowOpacity: 0.5,
|
||||||
|
duration: 3,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 标准显示
|
||||||
|
standard: {
|
||||||
|
revealDuration: 0.8,
|
||||||
|
blurDuration: 0.5,
|
||||||
|
initialBlur: 20,
|
||||||
|
loadingBgConfig: {
|
||||||
|
fromColor: 'from-cyan-300',
|
||||||
|
viaColor: 'via-sky-400',
|
||||||
|
toColor: 'to-blue-500',
|
||||||
|
glowOpacity: 0.5,
|
||||||
|
duration: 5,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 缓慢显示
|
||||||
|
slow: {
|
||||||
|
revealDuration: 1.2,
|
||||||
|
blurDuration: 0.7,
|
||||||
|
initialBlur: 30,
|
||||||
|
loadingBgConfig: {
|
||||||
|
fromColor: 'from-cyan-300',
|
||||||
|
viaColor: 'via-sky-400',
|
||||||
|
toColor: 'to-blue-500',
|
||||||
|
glowOpacity: 0.5,
|
||||||
|
duration: 7,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 缩略图
|
||||||
|
thumbnail: {
|
||||||
|
revealDuration: 0.6,
|
||||||
|
blurDuration: 0.4,
|
||||||
|
initialBlur: 10,
|
||||||
|
loadingBgConfig: {
|
||||||
|
fromColor: 'from-cyan-300',
|
||||||
|
viaColor: 'via-sky-400',
|
||||||
|
toColor: 'to-blue-500',
|
||||||
|
glowOpacity: 0.3,
|
||||||
|
duration: 4,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 主内容
|
||||||
|
main: {
|
||||||
|
revealDuration: 0.8,
|
||||||
|
blurDuration: 0.5,
|
||||||
|
initialBlur: 20,
|
||||||
|
loadingBgConfig: {
|
||||||
|
fromColor: 'from-cyan-300',
|
||||||
|
viaColor: 'via-sky-400',
|
||||||
|
toColor: 'to-blue-500',
|
||||||
|
glowOpacity: 0.5,
|
||||||
|
duration: 5,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 温暖色调
|
||||||
|
warm: {
|
||||||
|
revealDuration: 0.8,
|
||||||
|
blurDuration: 0.5,
|
||||||
|
initialBlur: 20,
|
||||||
|
loadingBgConfig: {
|
||||||
|
fromColor: 'from-orange-300',
|
||||||
|
viaColor: 'via-red-400',
|
||||||
|
toColor: 'to-rose-500',
|
||||||
|
glowOpacity: 0.5,
|
||||||
|
duration: 5,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 冷色调
|
||||||
|
cool: {
|
||||||
|
revealDuration: 0.8,
|
||||||
|
blurDuration: 0.5,
|
||||||
|
initialBlur: 20,
|
||||||
|
loadingBgConfig: {
|
||||||
|
fromColor: 'from-emerald-300',
|
||||||
|
viaColor: 'via-teal-400',
|
||||||
|
toColor: 'to-cyan-500',
|
||||||
|
glowOpacity: 0.5,
|
||||||
|
duration: 5,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
266
components/ui/replace-character-modal.tsx
Normal file
266
components/ui/replace-character-modal.tsx
Normal file
@ -0,0 +1,266 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
|
import { X, Upload, Library, Wand2, Search, Image, Plus, ChevronDown } from 'lucide-react';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
interface ReplaceCharacterModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
activeReplaceMethod: string;
|
||||||
|
onClose: () => void;
|
||||||
|
onCharacterSelect: (character: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ReplaceCharacterModal({
|
||||||
|
isOpen,
|
||||||
|
activeReplaceMethod,
|
||||||
|
onClose,
|
||||||
|
onCharacterSelect
|
||||||
|
}: ReplaceCharacterModalProps) {
|
||||||
|
const [activeMethod, setActiveMethod] = useState(activeReplaceMethod);
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setActiveMethod(activeReplaceMethod);
|
||||||
|
}, [activeReplaceMethod]);
|
||||||
|
|
||||||
|
// 模拟角色库数据
|
||||||
|
const mockLibraryCharacters = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
avatar: '/assets/3dr_chihiro.png',
|
||||||
|
name: '雪 (YUKI)',
|
||||||
|
style: '动漫风格'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
avatar: '/assets/3dr_mono.png',
|
||||||
|
name: '春 (HARU)',
|
||||||
|
style: '写实风格'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
avatar: '/assets/3dr_chihiro.png',
|
||||||
|
name: '夏 (NATSU)',
|
||||||
|
style: '二次元风格'
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = event.target.files?.[0];
|
||||||
|
if (file) {
|
||||||
|
// 处理文件上传
|
||||||
|
console.log('Uploading file:', file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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 justify-between p-4 border-b border-white/10">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
className="p-1 hover:bg-white/10 rounded-full transition-colors"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
<ChevronDown className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
<h2 className="text-lg font-medium">替换角色</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 主要内容区域 */}
|
||||||
|
<div className="p-6 space-y-6 h-[80vh]">
|
||||||
|
{/* 替换方式选择 */}
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<motion.button
|
||||||
|
className={cn(
|
||||||
|
'flex-1 flex flex-col items-center gap-2 p-4 rounded-lg border-2 transition-colors',
|
||||||
|
activeMethod === 'upload'
|
||||||
|
? 'border-blue-500 bg-blue-500/10'
|
||||||
|
: 'border-white/10 hover:border-white/20'
|
||||||
|
)}
|
||||||
|
onClick={() => setActiveMethod('upload')}
|
||||||
|
whileHover={{ scale: 1.02 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
<Upload className="w-6 h-6" />
|
||||||
|
<span>上传角色</span>
|
||||||
|
</motion.button>
|
||||||
|
|
||||||
|
<motion.button
|
||||||
|
className={cn(
|
||||||
|
'flex-1 flex flex-col items-center gap-2 p-4 rounded-lg border-2 transition-colors',
|
||||||
|
activeMethod === 'library'
|
||||||
|
? 'border-blue-500 bg-blue-500/10'
|
||||||
|
: 'border-white/10 hover:border-white/20'
|
||||||
|
)}
|
||||||
|
onClick={() => setActiveMethod('library')}
|
||||||
|
whileHover={{ scale: 1.02 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
<Library className="w-6 h-6" />
|
||||||
|
<span>角色库</span>
|
||||||
|
</motion.button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 内容区域 */}
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
{activeMethod === 'upload' && (
|
||||||
|
<motion.div
|
||||||
|
key="upload"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -20 }}
|
||||||
|
className="flex flex-col items-center justify-center p-8 border-2 border-dashed border-white/10 rounded-lg"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
className="hidden"
|
||||||
|
id="character-upload"
|
||||||
|
onChange={handleFileUpload}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
htmlFor="character-upload"
|
||||||
|
className="flex flex-col items-center gap-4 cursor-pointer"
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
className="p-4 rounded-full bg-white/5"
|
||||||
|
whileHover={{ scale: 1.1, backgroundColor: 'rgba(255,255,255,0.1)' }}
|
||||||
|
whileTap={{ scale: 0.9 }}
|
||||||
|
>
|
||||||
|
<Image className="w-8 h-8 text-white/70" />
|
||||||
|
</motion.div>
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-white/70">点击上传或拖拽角色图片到此处</p>
|
||||||
|
<p className="text-sm text-white/50 mt-2">支持 PNG, JPG, WEBP 格式</p>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeMethod === 'library' && (
|
||||||
|
<motion.div
|
||||||
|
key="library"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -20 }}
|
||||||
|
className="space-y-4"
|
||||||
|
>
|
||||||
|
{/* 搜索框 */}
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-white/50" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="搜索角色..."
|
||||||
|
className="w-full pl-10 pr-4 py-2 bg-white/5 border border-white/10 rounded-lg
|
||||||
|
focus:outline-none focus:border-blue-500 transition-colors"
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 角色列表 */}
|
||||||
|
<div className="grid grid-cols-4 gap-4">
|
||||||
|
{mockLibraryCharacters.map((character) => (
|
||||||
|
<motion.div
|
||||||
|
key={character.id}
|
||||||
|
className="group relative aspect-[9/16] rounded-lg overflow-hidden cursor-pointer"
|
||||||
|
whileHover={{ scale: 1.02 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
onClick={() => onCharacterSelect(character)}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={character.avatar}
|
||||||
|
alt={character.name}
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent
|
||||||
|
opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
|
<div className="absolute bottom-0 left-0 right-0 p-2">
|
||||||
|
<p className="text-sm font-medium">{character.name}</p>
|
||||||
|
<p className="text-xs text-white/70">{character.style}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeMethod === 'generate' && (
|
||||||
|
<motion.div
|
||||||
|
key="generate"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -20 }}
|
||||||
|
className="flex flex-col items-center gap-4"
|
||||||
|
>
|
||||||
|
<motion.button
|
||||||
|
className="flex items-start gap-2 px-6 py-3 bg-blue-500 hover:bg-blue-600
|
||||||
|
rounded-lg transition-colors"
|
||||||
|
onClick={() => console.log('Generate character')}
|
||||||
|
whileHover={{ scale: 1.02 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
<Plus className="w-5 h-5" />
|
||||||
|
<span>生成新角色</span>
|
||||||
|
</motion.button>
|
||||||
|
</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
249
components/ui/replace-music-modal.tsx
Normal file
249
components/ui/replace-music-modal.tsx
Normal file
@ -0,0 +1,249 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
|
import { Upload, Library, Search, Music2, ChevronDown } from 'lucide-react';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
interface ReplaceMusicModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
activeReplaceMethod: string;
|
||||||
|
onClose: () => void;
|
||||||
|
onMusicSelect: (music: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ReplaceMusicModal({
|
||||||
|
isOpen,
|
||||||
|
activeReplaceMethod,
|
||||||
|
onClose,
|
||||||
|
onMusicSelect
|
||||||
|
}: ReplaceMusicModalProps) {
|
||||||
|
const [activeMethod, setActiveMethod] = useState(activeReplaceMethod);
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setActiveMethod(activeReplaceMethod);
|
||||||
|
}, [activeReplaceMethod]);
|
||||||
|
|
||||||
|
// 模拟音乐库数据
|
||||||
|
const mockLibraryMusics = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'Upbeat Pop',
|
||||||
|
duration: '02:35',
|
||||||
|
genre: '流行音乐',
|
||||||
|
url: 'https://example.com/music1.mp3'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: 'Chill Lofi',
|
||||||
|
duration: '03:15',
|
||||||
|
genre: '轻音乐',
|
||||||
|
url: 'https://example.com/music2.mp3'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: 'Epic Orchestra',
|
||||||
|
duration: '04:20',
|
||||||
|
genre: '管弦乐',
|
||||||
|
url: 'https://example.com/music3.mp3'
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = event.target.files?.[0];
|
||||||
|
if (file) {
|
||||||
|
// 处理文件上传
|
||||||
|
console.log('Uploading file:', file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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 justify-between p-4 border-b border-white/10">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
className="p-1 hover:bg-white/10 rounded-full transition-colors"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
<ChevronDown className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
<h2 className="text-lg font-medium">替换音乐</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 主要内容区域 */}
|
||||||
|
<div className="p-6 space-y-6 h-[80vh]">
|
||||||
|
{/* 替换方式选择 */}
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<motion.button
|
||||||
|
className={cn(
|
||||||
|
'flex-1 flex flex-col items-center gap-2 p-4 rounded-lg border-2 transition-colors',
|
||||||
|
activeMethod === 'upload'
|
||||||
|
? 'border-blue-500 bg-blue-500/10'
|
||||||
|
: 'border-white/10 hover:border-white/20'
|
||||||
|
)}
|
||||||
|
onClick={() => setActiveMethod('upload')}
|
||||||
|
whileHover={{ scale: 1.02 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
<Upload className="w-6 h-6" />
|
||||||
|
<span>上传音乐</span>
|
||||||
|
</motion.button>
|
||||||
|
|
||||||
|
<motion.button
|
||||||
|
className={cn(
|
||||||
|
'flex-1 flex flex-col items-center gap-2 p-4 rounded-lg border-2 transition-colors',
|
||||||
|
activeMethod === 'library'
|
||||||
|
? 'border-blue-500 bg-blue-500/10'
|
||||||
|
: 'border-white/10 hover:border-white/20'
|
||||||
|
)}
|
||||||
|
onClick={() => setActiveMethod('library')}
|
||||||
|
whileHover={{ scale: 1.02 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
<Library className="w-6 h-6" />
|
||||||
|
<span>音乐库</span>
|
||||||
|
</motion.button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 内容区域 */}
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
{activeMethod === 'upload' && (
|
||||||
|
<motion.div
|
||||||
|
key="upload"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -20 }}
|
||||||
|
className="flex flex-col items-center justify-center p-8 border-2 border-dashed border-white/10 rounded-lg"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="audio/*"
|
||||||
|
className="hidden"
|
||||||
|
id="music-upload"
|
||||||
|
onChange={handleFileUpload}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
htmlFor="music-upload"
|
||||||
|
className="flex flex-col items-center gap-4 cursor-pointer"
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
className="p-4 rounded-full bg-white/5"
|
||||||
|
whileHover={{ scale: 1.1, backgroundColor: 'rgba(255,255,255,0.1)' }}
|
||||||
|
whileTap={{ scale: 0.9 }}
|
||||||
|
>
|
||||||
|
<Music2 className="w-8 h-8 text-white/70" />
|
||||||
|
</motion.div>
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-white/70">点击上传或拖拽音乐文件到此处</p>
|
||||||
|
<p className="text-sm text-white/50 mt-2">支持 MP3, WAV, M4A 格式</p>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeMethod === 'library' && (
|
||||||
|
<motion.div
|
||||||
|
key="library"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -20 }}
|
||||||
|
className="space-y-4"
|
||||||
|
>
|
||||||
|
{/* 搜索框 */}
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-white/50" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="搜索音乐..."
|
||||||
|
className="w-full pl-10 pr-4 py-2 bg-white/5 border border-white/10 rounded-lg
|
||||||
|
focus:outline-none focus:border-blue-500 transition-colors"
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 音乐列表 */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
{mockLibraryMusics.map((music) => (
|
||||||
|
<motion.div
|
||||||
|
key={music.id}
|
||||||
|
className="group relative p-4 rounded-lg bg-white/5 hover:bg-white/10
|
||||||
|
cursor-pointer transition-colors"
|
||||||
|
whileHover={{ scale: 1.01 }}
|
||||||
|
whileTap={{ scale: 0.99 }}
|
||||||
|
onClick={() => onMusicSelect(music)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<Music2 className="w-8 h-8 text-white/70" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="text-sm font-medium truncate">{music.name}</h3>
|
||||||
|
<span className="text-xs text-white/50">{music.duration}</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-white/50 mt-1">{music.genre}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
282
components/ui/replace-video-modal.tsx
Normal file
282
components/ui/replace-video-modal.tsx
Normal file
@ -0,0 +1,282 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
|
import { X, Upload, Library, Wand2, Search, FileVideo, Plus, ChevronDown } from 'lucide-react';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { GenerateVideoModal } from './generate-video-modal';
|
||||||
|
|
||||||
|
interface ReplaceVideoModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
activeReplaceMethod: string;
|
||||||
|
onClose: () => void;
|
||||||
|
onVideoSelect: (video: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ReplaceVideoModal({
|
||||||
|
isOpen,
|
||||||
|
activeReplaceMethod,
|
||||||
|
onClose,
|
||||||
|
onVideoSelect
|
||||||
|
}: ReplaceVideoModalProps) {
|
||||||
|
const [activeMethod, setActiveMethod] = useState(activeReplaceMethod);
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
const [isGenerateModalOpen, setIsGenerateModalOpen] = React.useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setActiveMethod(activeReplaceMethod);
|
||||||
|
}, [activeReplaceMethod]);
|
||||||
|
|
||||||
|
// 模拟素材库视频数据
|
||||||
|
const mockLibraryVideos = [
|
||||||
|
{ id: 1, url: 'https://example.com/video1.mp4', title: '烟花绽放', duration: '00:15' },
|
||||||
|
{ id: 2, url: 'https://example.com/video2.mp4', title: '城市夜景', duration: '00:20' },
|
||||||
|
{ id: 3, url: 'https://example.com/video3.mp4', title: '海浪声音', duration: '00:30' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = event.target.files?.[0];
|
||||||
|
if (file) {
|
||||||
|
// 处理文件上传
|
||||||
|
console.log('Uploading file:', file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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 justify-between p-4 border-b border-white/10">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
className="p-1 hover:bg-white/10 rounded-full transition-colors"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
<ChevronDown className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
<h2 className="text-lg font-medium">视频替换替换</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 主要内容区域 */}
|
||||||
|
<div className="p-6 space-y-6 h-[80vh]">
|
||||||
|
{/* 替换方式选择 */}
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<motion.button
|
||||||
|
className={cn(
|
||||||
|
'flex-1 flex flex-col items-center gap-2 p-4 rounded-lg border-2 transition-colors',
|
||||||
|
activeMethod === 'upload'
|
||||||
|
? 'border-blue-500 bg-blue-500/10'
|
||||||
|
: 'border-white/10 hover:border-white/20'
|
||||||
|
)}
|
||||||
|
onClick={() => setActiveMethod('upload')}
|
||||||
|
whileHover={{ scale: 1.02 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
<Upload className="w-6 h-6" />
|
||||||
|
<span>上传视频</span>
|
||||||
|
</motion.button>
|
||||||
|
|
||||||
|
<motion.button
|
||||||
|
className={cn(
|
||||||
|
'flex-1 flex flex-col items-center gap-2 p-4 rounded-lg border-2 transition-colors',
|
||||||
|
activeMethod === 'library'
|
||||||
|
? 'border-blue-500 bg-blue-500/10'
|
||||||
|
: 'border-white/10 hover:border-white/20'
|
||||||
|
)}
|
||||||
|
onClick={() => setActiveMethod('library')}
|
||||||
|
whileHover={{ scale: 1.02 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
<Library className="w-6 h-6" />
|
||||||
|
<span>素材库</span>
|
||||||
|
</motion.button>
|
||||||
|
|
||||||
|
<motion.button
|
||||||
|
className={cn(
|
||||||
|
'flex-1 flex flex-col items-center gap-2 p-4 rounded-lg border-2 transition-colors',
|
||||||
|
activeMethod === 'generate'
|
||||||
|
? 'border-blue-500 bg-blue-500/10'
|
||||||
|
: 'border-white/10 hover:border-white/20'
|
||||||
|
)}
|
||||||
|
onClick={() => setActiveMethod('generate')}
|
||||||
|
whileHover={{ scale: 1.02 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
<Wand2 className="w-6 h-6" />
|
||||||
|
<span>生成视频</span>
|
||||||
|
</motion.button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 内容区域 */}
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
{activeMethod === 'upload' && (
|
||||||
|
<motion.div
|
||||||
|
key="upload"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -20 }}
|
||||||
|
className="flex flex-col items-center justify-center p-8 border-2 border-dashed border-white/10 rounded-lg"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="video/*"
|
||||||
|
className="hidden"
|
||||||
|
id="video-upload"
|
||||||
|
onChange={handleFileUpload}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
htmlFor="video-upload"
|
||||||
|
className="flex flex-col items-center gap-4 cursor-pointer"
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
className="p-4 rounded-full bg-white/5"
|
||||||
|
whileHover={{ scale: 1.1, backgroundColor: 'rgba(255,255,255,0.1)' }}
|
||||||
|
whileTap={{ scale: 0.9 }}
|
||||||
|
>
|
||||||
|
<FileVideo className="w-8 h-8 text-white/70" />
|
||||||
|
</motion.div>
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-white/70">点击上传或拖拽视频文件到此处</p>
|
||||||
|
<p className="text-sm text-white/50 mt-2">支持 MP4, MOV, AVI 格式</p>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeMethod === 'library' && (
|
||||||
|
<motion.div
|
||||||
|
key="library"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -20 }}
|
||||||
|
className="space-y-4"
|
||||||
|
>
|
||||||
|
{/* 搜索框 */}
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-white/50" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="搜索视频..."
|
||||||
|
className="w-full pl-10 pr-4 py-2 bg-white/5 border border-white/10 rounded-lg
|
||||||
|
focus:outline-none focus:border-blue-500 transition-colors"
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 视频列表 */}
|
||||||
|
<div className="grid grid-cols-3 gap-4">
|
||||||
|
{mockLibraryVideos.map((video) => (
|
||||||
|
<motion.div
|
||||||
|
key={video.id}
|
||||||
|
className="group relative aspect-video rounded-lg overflow-hidden cursor-pointer"
|
||||||
|
whileHover={{ scale: 1.02 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
onClick={() => onVideoSelect(video)}
|
||||||
|
>
|
||||||
|
<video
|
||||||
|
src={video.url}
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
muted
|
||||||
|
loop
|
||||||
|
playsInline
|
||||||
|
onMouseEnter={(e) => e.currentTarget.play()}
|
||||||
|
onMouseLeave={(e) => e.currentTarget.pause()}
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent
|
||||||
|
opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
|
<div className="absolute bottom-0 left-0 right-0 p-2">
|
||||||
|
<p className="text-sm font-medium">{video.title}</p>
|
||||||
|
<p className="text-xs text-white/70">{video.duration}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeMethod === 'generate' && (
|
||||||
|
<motion.div
|
||||||
|
key="generate"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -20 }}
|
||||||
|
className="flex flex-col items-center gap-4"
|
||||||
|
>
|
||||||
|
<motion.button
|
||||||
|
className="flex items-start gap-2 px-6 py-3 bg-blue-500 hover:bg-blue-600
|
||||||
|
rounded-lg transition-colors"
|
||||||
|
onClick={() => setIsGenerateModalOpen(true)}
|
||||||
|
whileHover={{ scale: 1.02 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
<Plus className="w-5 h-5" />
|
||||||
|
<span>生成新视频</span>
|
||||||
|
</motion.button>
|
||||||
|
</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>
|
||||||
|
|
||||||
|
{/* 生成视频弹窗 */}
|
||||||
|
<GenerateVideoModal
|
||||||
|
isOpen={isGenerateModalOpen}
|
||||||
|
onClose={() => setIsGenerateModalOpen(false)}
|
||||||
|
onGenerate={(params) => {
|
||||||
|
console.log('Generate video with params:', params);
|
||||||
|
setIsGenerateModalOpen(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
);
|
||||||
|
}
|
||||||
298
components/ui/settings-tab-content.tsx
Normal file
298
components/ui/settings-tab-content.tsx
Normal file
@ -0,0 +1,298 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
|
import { Check, ChevronDown, Volume2, Music, Mic, Radio } from 'lucide-react';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
interface SettingsTabContentProps {
|
||||||
|
onSettingChange?: (key: string, value: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SettingOption {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const modeOptions: SettingOption[] = [
|
||||||
|
{ label: 'Auto Mode', value: 'auto' },
|
||||||
|
{ label: 'Manual Mode', value: 'manual' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const resolutionOptions: SettingOption[] = [
|
||||||
|
{ label: '720P', value: '720p' },
|
||||||
|
{ label: '1080P', value: '1080p' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const overlayOptions: SettingOption[] = [
|
||||||
|
{ label: 'None', value: 'none' },
|
||||||
|
{ label: 'Light Leaks', value: 'light_leaks' },
|
||||||
|
{ label: 'Film Grain', value: 'film_grain' },
|
||||||
|
{ label: 'Dust', value: 'dust' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const transitionOptions: SettingOption[] = [
|
||||||
|
{ label: 'None', value: 'none' },
|
||||||
|
{ label: 'Dynamic Cut', value: 'dynamic_cut' },
|
||||||
|
{ label: 'Zoom Pop', value: 'zoom_pop' },
|
||||||
|
{ label: 'Smooth Fade', value: 'smooth_fade' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const subtitleOptions: SettingOption[] = [
|
||||||
|
{ label: 'None', value: 'none' },
|
||||||
|
{ label: 'Hormozi', value: 'hormozi' },
|
||||||
|
{ label: 'Castor', value: 'castor' },
|
||||||
|
{ label: 'Modern', value: 'modern' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const watermarkOptions: SettingOption[] = [
|
||||||
|
{ label: 'None', value: 'none' },
|
||||||
|
{ label: 'Simple', value: 'simple' },
|
||||||
|
{ label: 'Branded', value: 'branded' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const textEffectOptions: SettingOption[] = [
|
||||||
|
{ label: 'Minimal', value: 'minimal' },
|
||||||
|
{ label: 'Plain', value: 'plain' },
|
||||||
|
{ label: 'Dramatic', value: 'dramatic' },
|
||||||
|
{ label: 'Corporate', value: 'corporate' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function SettingsTabContent({ onSettingChange }: SettingsTabContentProps) {
|
||||||
|
const [selectedMode, setSelectedMode] = useState('auto');
|
||||||
|
const [selectedResolution, setSelectedResolution] = useState('1080p');
|
||||||
|
const [selectedOverlay, setSelectedOverlay] = useState('light_leaks');
|
||||||
|
const [selectedTransition, setSelectedTransition] = useState('none');
|
||||||
|
const [selectedSubtitle, setSelectedSubtitle] = useState('hormozi');
|
||||||
|
const [selectedWatermark, setSelectedWatermark] = useState('none');
|
||||||
|
const [selectedTextEffect, setSelectedTextEffect] = useState('minimal');
|
||||||
|
const [sfxVolume, setSfxVolume] = useState(75);
|
||||||
|
const [mediaVolume, setMediaVolume] = useState(75);
|
||||||
|
const [musicVolume, setMusicVolume] = useState(80);
|
||||||
|
const [openDropdown, setOpenDropdown] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const handleDropdownToggle = (key: string) => {
|
||||||
|
setOpenDropdown(openDropdown === key ? null : key);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderDropdown = (
|
||||||
|
key: string,
|
||||||
|
label: string,
|
||||||
|
options: SettingOption[],
|
||||||
|
value: string,
|
||||||
|
onChange: (value: string) => void
|
||||||
|
) => (
|
||||||
|
<div className="relative">
|
||||||
|
<motion.button
|
||||||
|
className={cn(
|
||||||
|
"w-full px-4 py-2 rounded-lg border text-left flex items-center justify-between",
|
||||||
|
openDropdown === key
|
||||||
|
? "border-blue-500 bg-blue-500/10"
|
||||||
|
: "border-white/10 hover:border-white/20"
|
||||||
|
)}
|
||||||
|
onClick={() => handleDropdownToggle(key)}
|
||||||
|
whileHover={{ scale: 1.01 }}
|
||||||
|
whileTap={{ scale: 0.99 }}
|
||||||
|
>
|
||||||
|
<span>{options.find(opt => opt.value === value)?.label || value}</span>
|
||||||
|
<motion.div
|
||||||
|
animate={{ rotate: openDropdown === key ? 180 : 0 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
>
|
||||||
|
<ChevronDown className="w-4 h-4" />
|
||||||
|
</motion.div>
|
||||||
|
</motion.button>
|
||||||
|
|
||||||
|
<AnimatePresence>
|
||||||
|
{openDropdown === key && (
|
||||||
|
<motion.div
|
||||||
|
className="absolute z-10 w-full mt-1 py-1 rounded-lg border border-white/10 bg-black/90 backdrop-blur-xl"
|
||||||
|
initial={{ opacity: 0, y: -10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -10 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
>
|
||||||
|
{options.map((option) => (
|
||||||
|
<motion.button
|
||||||
|
key={option.value}
|
||||||
|
className={cn(
|
||||||
|
"w-full px-4 py-2 text-left flex items-center justify-between hover:bg-white/5",
|
||||||
|
value === option.value && "text-blue-500"
|
||||||
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
onChange(option.value);
|
||||||
|
handleDropdownToggle(key);
|
||||||
|
onSettingChange?.(key, option.value);
|
||||||
|
}}
|
||||||
|
whileHover={{ x: 4 }}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
{value === option.value && <Check className="w-4 h-4" />}
|
||||||
|
</motion.button>
|
||||||
|
))}
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderVolumeSlider = (
|
||||||
|
icon: React.ReactNode,
|
||||||
|
label: string,
|
||||||
|
value: number,
|
||||||
|
onChange: (value: number) => void
|
||||||
|
) => (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center gap-2 text-sm text-white/70">
|
||||||
|
{icon}
|
||||||
|
<span>{label}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
value={value}
|
||||||
|
className="flex-1 h-2 rounded-full bg-white/10 cursor-pointer
|
||||||
|
[&::-webkit-slider-thumb]:w-4 [&::-webkit-slider-thumb]:h-4
|
||||||
|
[&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-blue-500
|
||||||
|
[&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:cursor-pointer
|
||||||
|
[&::-webkit-slider-thumb]:transition-all [&::-webkit-slider-thumb]:duration-200
|
||||||
|
[&::-webkit-slider-thumb]:hover:scale-110"
|
||||||
|
onChange={(e) => {
|
||||||
|
const newValue = parseInt(e.target.value);
|
||||||
|
onChange(newValue);
|
||||||
|
onSettingChange?.(label, newValue);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className="w-12 text-right text-sm">{value}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
className="space-y-6"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
>
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* 工作模式 */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm text-white/70">Mode</label>
|
||||||
|
{renderDropdown('mode', 'Mode', modeOptions, selectedMode, setSelectedMode)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 分辨率 */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm text-white/70">Resolution</label>
|
||||||
|
{renderDropdown('resolution', 'Resolution', resolutionOptions, selectedResolution, setSelectedResolution)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 叠加效果 */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm text-white/70">Overlay Preset</label>
|
||||||
|
{renderDropdown('overlay', 'Overlay Preset', overlayOptions, selectedOverlay, setSelectedOverlay)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 转场设定 */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm text-white/70">Transition Preset</label>
|
||||||
|
{renderDropdown('transition', 'Transition Preset', transitionOptions, selectedTransition, setSelectedTransition)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 字幕风格 */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm text-white/70">Subtitle Preset</label>
|
||||||
|
{renderDropdown('subtitle', 'Subtitle Preset', subtitleOptions, selectedSubtitle, setSelectedSubtitle)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 贴纸预设 */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm text-white/70">Sticker Preset</label>
|
||||||
|
{renderDropdown('watermark', 'Sticker Preset', watermarkOptions, selectedWatermark, setSelectedWatermark)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 文字效果 */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm text-white/70">Text Preset</label>
|
||||||
|
{renderDropdown('textEffect', 'Text Preset', textEffectOptions, selectedTextEffect, setSelectedTextEffect)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 音效主音量 */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm text-white/70">SFX Master Volume</label>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
value={sfxVolume}
|
||||||
|
className="flex-1 h-1 rounded-full bg-white/10 cursor-pointer
|
||||||
|
[&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:h-3
|
||||||
|
[&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-blue-500
|
||||||
|
[&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:cursor-pointer
|
||||||
|
[&::-webkit-slider-thumb]:transition-all [&::-webkit-slider-thumb]:duration-200
|
||||||
|
[&::-webkit-slider-thumb]:hover:scale-110"
|
||||||
|
onChange={(e) => {
|
||||||
|
const newValue = parseInt(e.target.value);
|
||||||
|
setSfxVolume(newValue);
|
||||||
|
onSettingChange?.('SFX Master Volume', newValue);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className="w-12 text-right text-sm">{sfxVolume}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 媒体音频主音量 */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm text-white/70">Media Audio Master Volume</label>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
value={mediaVolume}
|
||||||
|
className="flex-1 h-1 rounded-full bg-white/10 cursor-pointer
|
||||||
|
[&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:h-3
|
||||||
|
[&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-blue-500
|
||||||
|
[&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:cursor-pointer
|
||||||
|
[&::-webkit-slider-thumb]:transition-all [&::-webkit-slider-thumb]:duration-200
|
||||||
|
[&::-webkit-slider-thumb]:hover:scale-110"
|
||||||
|
onChange={(e) => {
|
||||||
|
const newValue = parseInt(e.target.value);
|
||||||
|
setMediaVolume(newValue);
|
||||||
|
onSettingChange?.('Media Audio Master Volume', newValue);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className="w-12 text-right text-sm">{mediaVolume}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 音乐主音量 */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm text-white/70">Music Master Volume</label>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
value={musicVolume}
|
||||||
|
className="flex-1 h-1 rounded-full bg-white/10 cursor-pointer
|
||||||
|
[&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:h-3
|
||||||
|
[&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-blue-500
|
||||||
|
[&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:cursor-pointer
|
||||||
|
[&::-webkit-slider-thumb]:transition-all [&::-webkit-slider-thumb]:duration-200
|
||||||
|
[&::-webkit-slider-thumb]:hover:scale-110"
|
||||||
|
onChange={(e) => {
|
||||||
|
const newValue = parseInt(e.target.value);
|
||||||
|
setMusicVolume(newValue);
|
||||||
|
onSettingChange?.('Music Master Volume', newValue);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className="w-12 text-right text-sm">{musicVolume}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
422
components/ui/video-tab-content.tsx
Normal file
422
components/ui/video-tab-content.tsx
Normal file
@ -0,0 +1,422 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useRef, useEffect } from 'react';
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
|
import { Trash2, RefreshCw, Play, Pause, Volume2, VolumeX, Upload, Library, Wand2 } from 'lucide-react';
|
||||||
|
import { GlassIconButton } from './glass-icon-button';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { ReplaceVideoModal } from './replace-video-modal';
|
||||||
|
|
||||||
|
interface VideoTabContentProps {
|
||||||
|
taskSketch: any[];
|
||||||
|
currentSketchIndex: number;
|
||||||
|
onSketchSelect: (index: number) => void;
|
||||||
|
isPlaying?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function VideoTabContent({
|
||||||
|
taskSketch = [],
|
||||||
|
currentSketchIndex = 0,
|
||||||
|
onSketchSelect,
|
||||||
|
isPlaying: externalIsPlaying = true
|
||||||
|
}: VideoTabContentProps) {
|
||||||
|
const thumbnailsRef = useRef<HTMLDivElement>(null);
|
||||||
|
const videosRef = useRef<HTMLDivElement>(null);
|
||||||
|
const videoPlayerRef = useRef<HTMLVideoElement>(null);
|
||||||
|
const [isPlaying, setIsPlaying] = React.useState(externalIsPlaying);
|
||||||
|
const [isMuted, setIsMuted] = React.useState(false);
|
||||||
|
const [progress, setProgress] = React.useState(0);
|
||||||
|
const [isReplaceModalOpen, setIsReplaceModalOpen] = React.useState(false);
|
||||||
|
const [activeReplaceMethod, setActiveReplaceMethod] = React.useState<'upload' | 'library' | 'generate'>('upload');
|
||||||
|
|
||||||
|
// 监听外部播放状态变化
|
||||||
|
useEffect(() => {
|
||||||
|
setIsPlaying(externalIsPlaying);
|
||||||
|
}, [externalIsPlaying]);
|
||||||
|
|
||||||
|
// 确保 taskSketch 是数组
|
||||||
|
const sketches = Array.isArray(taskSketch) ? taskSketch : [];
|
||||||
|
|
||||||
|
// 模拟视频数据
|
||||||
|
const mockVideos = sketches.map((_, index) => ({
|
||||||
|
id: `video-${index}`,
|
||||||
|
url: 'https://example.com/video.mp4', // 替换为实际视频URL
|
||||||
|
duration: '00:21',
|
||||||
|
description: `这是第 ${index + 1} 个分镜的视频描述,包含了场景中的主要动作和表现。`
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 自动滚动到选中项
|
||||||
|
useEffect(() => {
|
||||||
|
if (thumbnailsRef.current && videosRef.current) {
|
||||||
|
const thumbnailContainer = thumbnailsRef.current;
|
||||||
|
const videoContainer = videosRef.current;
|
||||||
|
|
||||||
|
const thumbnailWidth = thumbnailContainer.children[0]?.clientWidth ?? 0;
|
||||||
|
const thumbnailGap = 16; // gap-4 = 16px
|
||||||
|
const thumbnailScrollPosition = (thumbnailWidth + thumbnailGap) * currentSketchIndex;
|
||||||
|
|
||||||
|
const videoElement = videoContainer.children[currentSketchIndex] as HTMLElement;
|
||||||
|
const videoScrollPosition = videoElement?.offsetLeft ?? 0;
|
||||||
|
|
||||||
|
thumbnailContainer.scrollTo({
|
||||||
|
left: thumbnailScrollPosition - thumbnailContainer.clientWidth / 2 + thumbnailWidth / 2,
|
||||||
|
behavior: 'smooth'
|
||||||
|
});
|
||||||
|
|
||||||
|
videoContainer.scrollTo({
|
||||||
|
left: videoScrollPosition - videoContainer.clientWidth / 2 + videoElement?.clientWidth / 2,
|
||||||
|
behavior: 'smooth'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [currentSketchIndex]);
|
||||||
|
|
||||||
|
// 视频播放控制
|
||||||
|
useEffect(() => {
|
||||||
|
if (videoPlayerRef.current) {
|
||||||
|
if (isPlaying) {
|
||||||
|
videoPlayerRef.current.play().catch(() => {
|
||||||
|
// 处理自动播放策略限制
|
||||||
|
setIsPlaying(false);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// videoPlayerRef.current.pause();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [isPlaying, currentSketchIndex]);
|
||||||
|
|
||||||
|
// 更新进度条
|
||||||
|
const handleTimeUpdate = () => {
|
||||||
|
if (videoPlayerRef.current) {
|
||||||
|
const progress = (videoPlayerRef.current.currentTime / videoPlayerRef.current.duration) * 100;
|
||||||
|
setProgress(progress);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 如果没有数据,显示空状态
|
||||||
|
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 }}
|
||||||
|
>
|
||||||
|
<video
|
||||||
|
src={sketch.url}
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
muted
|
||||||
|
loop
|
||||||
|
playsInline
|
||||||
|
onMouseEnter={(e) => e.currentTarget.play()}
|
||||||
|
onMouseLeave={(e) => e.currentTarget.pause()}
|
||||||
|
/>
|
||||||
|
<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={videosRef}
|
||||||
|
className="flex overflow-x-auto hide-scrollbar py-2 gap-1"
|
||||||
|
>
|
||||||
|
{mockVideos.map((video, index) => {
|
||||||
|
const isActive = currentSketchIndex === index;
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
key={video.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">
|
||||||
|
{video.description}
|
||||||
|
</span>
|
||||||
|
{index < mockVideos.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="p-4 rounded-lg bg-white/5"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.1 }}
|
||||||
|
>
|
||||||
|
<h3 className="text-sm font-medium mb-2">替换视频</h3>
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<motion.button
|
||||||
|
className={cn(
|
||||||
|
'flex-1 flex flex-col items-center gap-2 p-4 rounded-lg border-2 transition-colors',
|
||||||
|
activeReplaceMethod === 'upload' && isReplaceModalOpen
|
||||||
|
? 'border-blue-500 bg-blue-500/10'
|
||||||
|
: 'border-white/10 hover:border-white/20'
|
||||||
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
setActiveReplaceMethod('upload');
|
||||||
|
setIsReplaceModalOpen(true);
|
||||||
|
}}
|
||||||
|
whileHover={{ scale: 1.02 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
<Upload className="w-6 h-6" />
|
||||||
|
<span>上传视频</span>
|
||||||
|
</motion.button>
|
||||||
|
|
||||||
|
<motion.button
|
||||||
|
className={cn(
|
||||||
|
'flex-1 flex flex-col items-center gap-2 p-4 rounded-lg border-2 transition-colors',
|
||||||
|
activeReplaceMethod === 'library' && isReplaceModalOpen
|
||||||
|
? 'border-blue-500 bg-blue-500/10'
|
||||||
|
: 'border-white/10 hover:border-white/20'
|
||||||
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
setActiveReplaceMethod('library');
|
||||||
|
setIsReplaceModalOpen(true);
|
||||||
|
}}
|
||||||
|
whileHover={{ scale: 1.02 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
<Library className="w-6 h-6" />
|
||||||
|
<span>素材库</span>
|
||||||
|
</motion.button>
|
||||||
|
|
||||||
|
<motion.button
|
||||||
|
className={cn(
|
||||||
|
'flex-1 flex flex-col items-center gap-2 p-4 rounded-lg border-2 transition-colors',
|
||||||
|
activeReplaceMethod === 'generate' && isReplaceModalOpen
|
||||||
|
? 'border-blue-500 bg-blue-500/10'
|
||||||
|
: 'border-white/10 hover:border-white/20'
|
||||||
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
setActiveReplaceMethod('generate');
|
||||||
|
setIsReplaceModalOpen(true);
|
||||||
|
}}
|
||||||
|
whileHover={{ scale: 1.02 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
<Wand2 className="w-6 h-6" />
|
||||||
|
<span>生成视频</span>
|
||||||
|
</motion.button>
|
||||||
|
</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.2 }}
|
||||||
|
>
|
||||||
|
{/* 左列:编辑项 截取视频、设置转场、调节音量 */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* 视频截取 */}
|
||||||
|
<div className="p-4 rounded-lg bg-white/5">
|
||||||
|
<h3 className="text-sm font-medium mb-2">视频截取</h3>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="00:00"
|
||||||
|
className="w-20 px-3 py-1 bg-white/5 border border-white/10 rounded-lg
|
||||||
|
text-center focus:outline-none focus:border-blue-500"
|
||||||
|
/>
|
||||||
|
<span className="text-white/50">至</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="00:00"
|
||||||
|
className="w-20 px-3 py-1 bg-white/5 border border-white/10 rounded-lg
|
||||||
|
text-center focus:outline-none focus:border-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 转场设置 */}
|
||||||
|
<div className="p-4 rounded-lg bg-white/5">
|
||||||
|
<h3 className="text-sm font-medium mb-2">转场设置</h3>
|
||||||
|
<div className="grid grid-cols-3 gap-2">
|
||||||
|
{['淡入淡出', '滑动', '缩放'].map((transition) => (
|
||||||
|
<motion.button
|
||||||
|
key={transition}
|
||||||
|
className="px-3 py-1 bg-white/5 hover:bg-white/10 rounded-lg text-sm"
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
>
|
||||||
|
{transition}
|
||||||
|
</motion.button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 音量调节 */}
|
||||||
|
<div className="p-4 rounded-lg bg-white/5">
|
||||||
|
<h3 className="text-sm font-medium mb-2">音量调节</h3>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
className="w-full"
|
||||||
|
onChange={(e) => console.log('Volume:', e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 右列:视频预览和操作 */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* 选中的视频预览 */}
|
||||||
|
<motion.div
|
||||||
|
className="aspect-video rounded-lg overflow-hidden relative group"
|
||||||
|
layoutId={`video-preview-${currentSketchIndex}`}
|
||||||
|
>
|
||||||
|
<video
|
||||||
|
ref={videoPlayerRef}
|
||||||
|
src={sketches[currentSketchIndex]?.url}
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
loop
|
||||||
|
autoPlay
|
||||||
|
playsInline
|
||||||
|
muted={isMuted}
|
||||||
|
onTimeUpdate={handleTimeUpdate}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 视频控制层 */}
|
||||||
|
<div className="absolute inset-0 bg-black/30 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
|
<div className="absolute bottom-0 left-0 right-0 p-4 bg-gradient-to-t from-black/60 to-transparent">
|
||||||
|
{/* 进度条 */}
|
||||||
|
<div className="w-full h-1 bg-white/20 rounded-full mb-4 cursor-pointer"
|
||||||
|
onClick={(e) => {
|
||||||
|
const rect = e.currentTarget.getBoundingClientRect();
|
||||||
|
const x = e.clientX - rect.left;
|
||||||
|
const percentage = (x / rect.width) * 100;
|
||||||
|
if (videoPlayerRef.current) {
|
||||||
|
videoPlayerRef.current.currentTime = (percentage / 100) * videoPlayerRef.current.duration;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
className="h-full bg-blue-500 rounded-full"
|
||||||
|
style={{ width: `${progress}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 控制按钮 */}
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<motion.button
|
||||||
|
className="p-2 rounded-full hover:bg-white/10"
|
||||||
|
onClick={() => setIsPlaying(!isPlaying)}
|
||||||
|
whileHover={{ scale: 1.1 }}
|
||||||
|
whileTap={{ scale: 0.9 }}
|
||||||
|
>
|
||||||
|
{isPlaying ? (
|
||||||
|
<Pause className="w-5 h-5" />
|
||||||
|
) : (
|
||||||
|
<Play className="w-5 h-5" />
|
||||||
|
)}
|
||||||
|
</motion.button>
|
||||||
|
|
||||||
|
<motion.button
|
||||||
|
className="p-2 rounded-full hover:bg-white/10"
|
||||||
|
onClick={() => setIsMuted(!isMuted)}
|
||||||
|
whileHover={{ scale: 1.1 }}
|
||||||
|
whileTap={{ scale: 0.9 }}
|
||||||
|
>
|
||||||
|
{isMuted ? (
|
||||||
|
<VolumeX className="w-5 h-5" />
|
||||||
|
) : (
|
||||||
|
<Volume2 className="w-5 h-5" />
|
||||||
|
)}
|
||||||
|
</motion.button>
|
||||||
|
|
||||||
|
<span className="text-sm">{mockVideos[currentSketchIndex]?.duration}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* 操作按钮 */}
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<motion.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"
|
||||||
|
whileHover={{ scale: 1.02 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4" />
|
||||||
|
<span>删除分镜</span>
|
||||||
|
</motion.button>
|
||||||
|
<motion.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"
|
||||||
|
whileHover={{ scale: 1.02 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
<RefreshCw className="w-4 h-4" />
|
||||||
|
<span>重新生成</span>
|
||||||
|
</motion.button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* 替换视频弹窗 */}
|
||||||
|
<ReplaceVideoModal
|
||||||
|
isOpen={isReplaceModalOpen}
|
||||||
|
activeReplaceMethod={activeReplaceMethod}
|
||||||
|
onClose={() => setIsReplaceModalOpen(false)}
|
||||||
|
onVideoSelect={(video) => {
|
||||||
|
console.log('Selected video:', video);
|
||||||
|
setIsReplaceModalOpen(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
821
package-lock.json
generated
821
package-lock.json
generated
@ -30,9 +30,9 @@
|
|||||||
"@radix-ui/react-progress": "^1.1.0",
|
"@radix-ui/react-progress": "^1.1.0",
|
||||||
"@radix-ui/react-radio-group": "^1.2.0",
|
"@radix-ui/react-radio-group": "^1.2.0",
|
||||||
"@radix-ui/react-scroll-area": "^1.1.0",
|
"@radix-ui/react-scroll-area": "^1.1.0",
|
||||||
"@radix-ui/react-select": "^2.1.1",
|
"@radix-ui/react-select": "^2.2.5",
|
||||||
"@radix-ui/react-separator": "^1.1.0",
|
"@radix-ui/react-separator": "^1.1.0",
|
||||||
"@radix-ui/react-slider": "^1.2.0",
|
"@radix-ui/react-slider": "^1.3.5",
|
||||||
"@radix-ui/react-slot": "^1.1.0",
|
"@radix-ui/react-slot": "^1.1.0",
|
||||||
"@radix-ui/react-switch": "^1.1.0",
|
"@radix-ui/react-switch": "^1.1.0",
|
||||||
"@radix-ui/react-tabs": "^1.1.0",
|
"@radix-ui/react-tabs": "^1.1.0",
|
||||||
@ -1599,31 +1599,32 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@radix-ui/react-select": {
|
"node_modules/@radix-ui/react-select": {
|
||||||
"version": "2.1.2",
|
"version": "2.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.1.2.tgz",
|
"resolved": "https://registry.npmmirror.com/@radix-ui/react-select/-/react-select-2.2.5.tgz",
|
||||||
"integrity": "sha512-rZJtWmorC7dFRi0owDmoijm6nSJH1tVw64QGiNIZ9PNLyBDtG+iAq+XGsya052At4BfarzY/Dhv9wrrUr6IMZA==",
|
"integrity": "sha512-HnMTdXEVuuyzx63ME0ut4+sEMYW6oouHWNGUZc7ddvUWIcfCva/AMoqEW/3wnEllriMWBa0RHspCYnfCWJQYmA==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/number": "1.1.0",
|
"@radix-ui/number": "1.1.1",
|
||||||
"@radix-ui/primitive": "1.1.0",
|
"@radix-ui/primitive": "1.1.2",
|
||||||
"@radix-ui/react-collection": "1.1.0",
|
"@radix-ui/react-collection": "1.1.7",
|
||||||
"@radix-ui/react-compose-refs": "1.1.0",
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
"@radix-ui/react-context": "1.1.1",
|
"@radix-ui/react-context": "1.1.2",
|
||||||
"@radix-ui/react-direction": "1.1.0",
|
"@radix-ui/react-direction": "1.1.1",
|
||||||
"@radix-ui/react-dismissable-layer": "1.1.1",
|
"@radix-ui/react-dismissable-layer": "1.1.10",
|
||||||
"@radix-ui/react-focus-guards": "1.1.1",
|
"@radix-ui/react-focus-guards": "1.1.2",
|
||||||
"@radix-ui/react-focus-scope": "1.1.0",
|
"@radix-ui/react-focus-scope": "1.1.7",
|
||||||
"@radix-ui/react-id": "1.1.0",
|
"@radix-ui/react-id": "1.1.1",
|
||||||
"@radix-ui/react-popper": "1.2.0",
|
"@radix-ui/react-popper": "1.2.7",
|
||||||
"@radix-ui/react-portal": "1.1.2",
|
"@radix-ui/react-portal": "1.1.9",
|
||||||
"@radix-ui/react-primitive": "2.0.0",
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
"@radix-ui/react-slot": "1.1.0",
|
"@radix-ui/react-slot": "1.2.3",
|
||||||
"@radix-ui/react-use-callback-ref": "1.1.0",
|
"@radix-ui/react-use-callback-ref": "1.1.1",
|
||||||
"@radix-ui/react-use-controllable-state": "1.1.0",
|
"@radix-ui/react-use-controllable-state": "1.2.2",
|
||||||
"@radix-ui/react-use-layout-effect": "1.1.0",
|
"@radix-ui/react-use-layout-effect": "1.1.1",
|
||||||
"@radix-ui/react-use-previous": "1.1.0",
|
"@radix-ui/react-use-previous": "1.1.1",
|
||||||
"@radix-ui/react-visually-hidden": "1.1.0",
|
"@radix-ui/react-visually-hidden": "1.2.3",
|
||||||
"aria-hidden": "^1.1.1",
|
"aria-hidden": "^1.2.4",
|
||||||
"react-remove-scroll": "2.6.0"
|
"react-remove-scroll": "^2.6.3"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "*",
|
"@types/react": "*",
|
||||||
@ -1640,6 +1641,466 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/number": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@radix-ui/number/-/number-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/primitive": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@radix-ui/primitive/-/primitive-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-arrow": {
|
||||||
|
"version": "1.1.7",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz",
|
||||||
|
"integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-primitive": "2.1.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-collection": {
|
||||||
|
"version": "1.1.7",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
|
||||||
|
"integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
|
"@radix-ui/react-context": "1.1.2",
|
||||||
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
|
"@radix-ui/react-slot": "1.2.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-compose-refs": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-context": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@radix-ui/react-context/-/react-context-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-direction": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-dismissable-layer": {
|
||||||
|
"version": "1.1.10",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.10.tgz",
|
||||||
|
"integrity": "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.2",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.1",
|
||||||
|
"@radix-ui/react-use-escape-keydown": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-focus-guards": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-focus-scope": {
|
||||||
|
"version": "1.1.7",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz",
|
||||||
|
"integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-id": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@radix-ui/react-id/-/react-id-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-popper": {
|
||||||
|
"version": "1.2.7",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@radix-ui/react-popper/-/react-popper-1.2.7.tgz",
|
||||||
|
"integrity": "sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/react-dom": "^2.0.0",
|
||||||
|
"@radix-ui/react-arrow": "1.1.7",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
|
"@radix-ui/react-context": "1.1.2",
|
||||||
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.1",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.1",
|
||||||
|
"@radix-ui/react-use-rect": "1.1.1",
|
||||||
|
"@radix-ui/react-use-size": "1.1.1",
|
||||||
|
"@radix-ui/rect": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-portal": {
|
||||||
|
"version": "1.1.9",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@radix-ui/react-portal/-/react-portal-1.1.9.tgz",
|
||||||
|
"integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-primitive": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-slot": "1.2.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": {
|
||||||
|
"version": "1.2.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||||
|
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-callback-ref": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-controllable-state": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-use-effect-event": "0.0.2",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-escape-keydown": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-layout-effect": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-previous": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-rect": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/rect": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-size": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-visually-hidden": {
|
||||||
|
"version": "1.2.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz",
|
||||||
|
"integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-primitive": "2.1.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/rect": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@radix-ui/rect/-/rect-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-select/node_modules/react-remove-scroll": {
|
||||||
|
"version": "2.7.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz",
|
||||||
|
"integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"react-remove-scroll-bar": "^2.3.7",
|
||||||
|
"react-style-singleton": "^2.2.3",
|
||||||
|
"tslib": "^2.1.0",
|
||||||
|
"use-callback-ref": "^1.3.3",
|
||||||
|
"use-sidecar": "^1.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-separator": {
|
"node_modules/@radix-ui/react-separator": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.0.tgz",
|
||||||
@ -1663,21 +2124,22 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@radix-ui/react-slider": {
|
"node_modules/@radix-ui/react-slider": {
|
||||||
"version": "1.2.1",
|
"version": "1.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.2.1.tgz",
|
"resolved": "https://registry.npmmirror.com/@radix-ui/react-slider/-/react-slider-1.3.5.tgz",
|
||||||
"integrity": "sha512-bEzQoDW0XP+h/oGbutF5VMWJPAl/UU8IJjr7h02SOHDIIIxq+cep8nItVNoBV+OMmahCdqdF38FTpmXoqQUGvw==",
|
"integrity": "sha512-rkfe2pU2NBAYfGaxa3Mqosi7VZEWX5CxKaanRv0vZd4Zhl9fvQrg0VM93dv3xGLGfrHuoTRF3JXH8nb9g+B3fw==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/number": "1.1.0",
|
"@radix-ui/number": "1.1.1",
|
||||||
"@radix-ui/primitive": "1.1.0",
|
"@radix-ui/primitive": "1.1.2",
|
||||||
"@radix-ui/react-collection": "1.1.0",
|
"@radix-ui/react-collection": "1.1.7",
|
||||||
"@radix-ui/react-compose-refs": "1.1.0",
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
"@radix-ui/react-context": "1.1.1",
|
"@radix-ui/react-context": "1.1.2",
|
||||||
"@radix-ui/react-direction": "1.1.0",
|
"@radix-ui/react-direction": "1.1.1",
|
||||||
"@radix-ui/react-primitive": "2.0.0",
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
"@radix-ui/react-use-controllable-state": "1.1.0",
|
"@radix-ui/react-use-controllable-state": "1.2.2",
|
||||||
"@radix-ui/react-use-layout-effect": "1.1.0",
|
"@radix-ui/react-use-layout-effect": "1.1.1",
|
||||||
"@radix-ui/react-use-previous": "1.1.0",
|
"@radix-ui/react-use-previous": "1.1.1",
|
||||||
"@radix-ui/react-use-size": "1.1.0"
|
"@radix-ui/react-use-size": "1.1.1"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "*",
|
"@types/react": "*",
|
||||||
@ -1694,6 +2156,197 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-slider/node_modules/@radix-ui/number": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@radix-ui/number/-/number-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-slider/node_modules/@radix-ui/primitive": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@radix-ui/primitive/-/primitive-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-collection": {
|
||||||
|
"version": "1.1.7",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
|
||||||
|
"integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
|
"@radix-ui/react-context": "1.1.2",
|
||||||
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
|
"@radix-ui/react-slot": "1.2.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-compose-refs": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-context": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@radix-ui/react-context/-/react-context-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-direction": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-primitive": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-slot": "1.2.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-slot": {
|
||||||
|
"version": "1.2.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||||
|
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-use-controllable-state": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-use-effect-event": "0.0.2",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-use-layout-effect": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-use-previous": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-use-size": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-slot": {
|
"node_modules/@radix-ui/react-slot": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz",
|
||||||
@ -1931,6 +2584,39 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-use-effect-event": {
|
||||||
|
"version": "0.0.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz",
|
||||||
|
"integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-use-effect-event/node_modules/@radix-ui/react-use-layout-effect": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-use-escape-keydown": {
|
"node_modules/@radix-ui/react-use-escape-keydown": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz",
|
||||||
@ -5093,14 +5779,6 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/invariant": {
|
|
||||||
"version": "2.2.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
|
|
||||||
"integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
|
|
||||||
"dependencies": {
|
|
||||||
"loose-envify": "^1.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/is-arguments": {
|
"node_modules/is-arguments": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz",
|
||||||
@ -7211,19 +7889,20 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-remove-scroll-bar": {
|
"node_modules/react-remove-scroll-bar": {
|
||||||
"version": "2.3.6",
|
"version": "2.3.8",
|
||||||
"resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.6.tgz",
|
"resolved": "https://registry.npmmirror.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz",
|
||||||
"integrity": "sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==",
|
"integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react-style-singleton": "^2.2.1",
|
"react-style-singleton": "^2.2.2",
|
||||||
"tslib": "^2.0.0"
|
"tslib": "^2.0.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
"@types/react": "*",
|
||||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
"@types/react": {
|
"@types/react": {
|
||||||
@ -7268,20 +7947,20 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-style-singleton": {
|
"node_modules/react-style-singleton": {
|
||||||
"version": "2.2.1",
|
"version": "2.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz",
|
"resolved": "https://registry.npmmirror.com/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
|
||||||
"integrity": "sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==",
|
"integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"get-nonce": "^1.0.0",
|
"get-nonce": "^1.0.0",
|
||||||
"invariant": "^2.2.4",
|
|
||||||
"tslib": "^2.0.0"
|
"tslib": "^2.0.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
"@types/react": "*",
|
||||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
"@types/react": {
|
"@types/react": {
|
||||||
@ -8399,9 +9078,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/use-callback-ref": {
|
"node_modules/use-callback-ref": {
|
||||||
"version": "1.3.2",
|
"version": "1.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.2.tgz",
|
"resolved": "https://registry.npmmirror.com/use-callback-ref/-/use-callback-ref-1.3.3.tgz",
|
||||||
"integrity": "sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA==",
|
"integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "^2.0.0"
|
"tslib": "^2.0.0"
|
||||||
},
|
},
|
||||||
@ -8409,8 +9089,8 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
"@types/react": "*",
|
||||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
"@types/react": {
|
"@types/react": {
|
||||||
@ -8473,9 +9153,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/use-sidecar": {
|
"node_modules/use-sidecar": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz",
|
"resolved": "https://registry.npmmirror.com/use-sidecar/-/use-sidecar-1.1.3.tgz",
|
||||||
"integrity": "sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==",
|
"integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"detect-node-es": "^1.1.0",
|
"detect-node-es": "^1.1.0",
|
||||||
"tslib": "^2.0.0"
|
"tslib": "^2.0.0"
|
||||||
@ -8484,8 +9165,8 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^16.9.0 || ^17.0.0 || ^18.0.0",
|
"@types/react": "*",
|
||||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
"@types/react": {
|
"@types/react": {
|
||||||
|
|||||||
@ -31,9 +31,9 @@
|
|||||||
"@radix-ui/react-progress": "^1.1.0",
|
"@radix-ui/react-progress": "^1.1.0",
|
||||||
"@radix-ui/react-radio-group": "^1.2.0",
|
"@radix-ui/react-radio-group": "^1.2.0",
|
||||||
"@radix-ui/react-scroll-area": "^1.1.0",
|
"@radix-ui/react-scroll-area": "^1.1.0",
|
||||||
"@radix-ui/react-select": "^2.1.1",
|
"@radix-ui/react-select": "^2.2.5",
|
||||||
"@radix-ui/react-separator": "^1.1.0",
|
"@radix-ui/react-separator": "^1.1.0",
|
||||||
"@radix-ui/react-slider": "^1.2.0",
|
"@radix-ui/react-slider": "^1.3.5",
|
||||||
"@radix-ui/react-slot": "^1.1.0",
|
"@radix-ui/react-slot": "^1.1.0",
|
||||||
"@radix-ui/react-switch": "^1.1.0",
|
"@radix-ui/react-switch": "^1.1.0",
|
||||||
"@radix-ui/react-tabs": "^1.1.0",
|
"@radix-ui/react-tabs": "^1.1.0",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user