video-flow-b/components/ui/music-tab-content.tsx
2025-06-30 12:48:25 +08:00

326 lines
11 KiB
TypeScript

'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 '@/public/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>
);
}