138 lines
4.9 KiB
TypeScript
138 lines
4.9 KiB
TypeScript
import type { RefObject, MouseEvent } from "react";
|
||
import { RefreshCw, Play, Pause } from "lucide-react";
|
||
|
||
interface BgmItem {
|
||
id: string;
|
||
name: string;
|
||
ext?: string;
|
||
}
|
||
|
||
interface BgmPanelProps {
|
||
bgmList: BgmItem[];
|
||
bgmLoading: boolean;
|
||
bgmError: string;
|
||
enableBgm: boolean;
|
||
onToggleEnable: (value: boolean) => void;
|
||
onRefresh: () => void;
|
||
selectedBgmId: string;
|
||
onSelectBgm: (id: string) => void;
|
||
playingBgmId: string | null;
|
||
onTogglePreview: (bgm: BgmItem, event: MouseEvent) => void;
|
||
bgmVolume: number;
|
||
onVolumeChange: (value: number) => void;
|
||
bgmListContainerRef: RefObject<HTMLDivElement | null>;
|
||
registerBgmItemRef: (id: string, element: HTMLDivElement | null) => void;
|
||
}
|
||
|
||
export function BgmPanel({
|
||
bgmList,
|
||
bgmLoading,
|
||
bgmError,
|
||
enableBgm,
|
||
onToggleEnable,
|
||
onRefresh,
|
||
selectedBgmId,
|
||
onSelectBgm,
|
||
playingBgmId,
|
||
onTogglePreview,
|
||
bgmVolume,
|
||
onVolumeChange,
|
||
bgmListContainerRef,
|
||
registerBgmItemRef,
|
||
}: BgmPanelProps) {
|
||
return (
|
||
<div className="bg-white/5 rounded-2xl p-6 border border-white/10 backdrop-blur-sm">
|
||
<div className="flex items-center justify-between mb-4">
|
||
<h2 className="text-lg font-semibold text-white flex items-center gap-2">五、背景音乐</h2>
|
||
<div className="flex items-center gap-2">
|
||
<button
|
||
onClick={onRefresh}
|
||
className="px-2 py-1 text-xs bg-white/10 hover:bg-white/20 rounded text-gray-300 flex items-center gap-1"
|
||
>
|
||
<RefreshCw className="h-3.5 w-3.5" />
|
||
刷新
|
||
</button>
|
||
<label className="relative inline-flex items-center cursor-pointer">
|
||
<input
|
||
type="checkbox"
|
||
checked={enableBgm}
|
||
onChange={(e) => onToggleEnable(e.target.checked)}
|
||
className="sr-only peer"
|
||
/>
|
||
<div className="w-11 h-6 bg-gray-600 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-purple-600"></div>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
{bgmLoading ? (
|
||
<div className="text-center py-4 text-gray-400 text-sm">正在加载背景音乐...</div>
|
||
) : bgmError ? (
|
||
<div className="text-center py-4 text-red-300 text-sm">
|
||
加载失败:{bgmError}
|
||
<button
|
||
onClick={onRefresh}
|
||
className="ml-2 px-2 py-1 text-xs bg-white/10 hover:bg-white/20 rounded text-gray-300"
|
||
>
|
||
重试
|
||
</button>
|
||
</div>
|
||
) : bgmList.length === 0 ? (
|
||
<div className="text-center py-4 text-gray-500 text-sm">暂无背景音乐,请先导入素材</div>
|
||
) : (
|
||
<div
|
||
ref={bgmListContainerRef}
|
||
className={`space-y-2 max-h-64 overflow-y-auto hide-scrollbar ${enableBgm ? '' : 'opacity-70'}`}
|
||
>
|
||
{bgmList.map((bgm) => (
|
||
<div
|
||
key={bgm.id}
|
||
ref={(el) => registerBgmItemRef(bgm.id, el)}
|
||
className={`p-3 rounded-lg border transition-all flex items-center justify-between group ${selectedBgmId === bgm.id
|
||
? "border-purple-500 bg-purple-500/20"
|
||
: "border-white/10 bg-white/5 hover:border-white/30"
|
||
}`}
|
||
>
|
||
<button onClick={() => onSelectBgm(bgm.id)} className="flex-1 text-left">
|
||
<div className="text-white text-sm truncate">{bgm.name}</div>
|
||
<div className="text-xs text-gray-400">.{bgm.ext || 'audio'}</div>
|
||
</button>
|
||
<div className="flex items-center gap-2 pl-2">
|
||
<button
|
||
onClick={(e) => onTogglePreview(bgm, e)}
|
||
className="p-1 text-gray-500 hover:text-purple-400 transition-colors"
|
||
title="试听"
|
||
>
|
||
{playingBgmId === bgm.id ? (
|
||
<Pause className="h-4 w-4" />
|
||
) : (
|
||
<Play className="h-4 w-4" />
|
||
)}
|
||
</button>
|
||
{selectedBgmId === bgm.id && (
|
||
<span className="text-xs text-purple-300">已选</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
|
||
{enableBgm && (
|
||
<div className="mt-4">
|
||
<label className="text-sm text-gray-300 mb-2 block">音量</label>
|
||
<input
|
||
type="range"
|
||
min="0"
|
||
max="1"
|
||
step="0.05"
|
||
value={bgmVolume}
|
||
onChange={(e) => onVolumeChange(parseFloat(e.target.value))}
|
||
className="w-full accent-purple-500"
|
||
/>
|
||
<div className="text-xs text-gray-400 mt-1">当前: {Math.round(bgmVolume * 100)}%</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|