video-flow-b/components/ui/filter-bar.tsx
2025-07-29 10:57:25 +08:00

176 lines
5.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { X, Check, ChevronDown } from 'lucide-react';
import * as Popover from '@radix-ui/react-popover';
import { mockSceneOptions, mockCharacterOptions } from '@/app/model/enums';
interface FilterBarProps {
selectedScenes: string[];
selectedCharacters: string[];
onScenesChange: (scenes: string[]) => void;
onCharactersChange: (characters: string[]) => void;
onReset: () => void;
}
const MultiSelect: React.FC<{
label: string;
options: { id: string; name: string }[];
selected: string[];
onChange: (values: string[]) => void;
}> = ({ label, options, selected, onChange }) => {
return (
<div className="flex items-center min-w-[200px]">
<label className="block text-sm text-gray-400 mr-2 flex-shrink-0 w-[2rem]">{label}</label>
<Popover.Root>
<Popover.Trigger asChild>
<button
className="flex-1 h-10 px-3 rounded-lg bg-black/30 border border-white/10
flex items-center justify-between text-sm text-gray-200
focus:outline-none focus:ring-2 focus:ring-blue-500/30"
>
<span>
{selected.length
? `已选择 ${selected.length}${label}`
: `选择${label}`}
</span>
<ChevronDown className="w-4 h-4 text-gray-400" />
</button>
</Popover.Trigger>
<Popover.Portal>
<Popover.Content
className="w-[var(--radix-popover-trigger-width)] overflow-hidden bg-black/90 backdrop-blur-xl
rounded-lg border border-white/10 shadow-xl animate-in fade-in-80 z-50"
align="start"
sideOffset={5}
>
<div className="p-2">
{options.map((option) => (
<motion.button
key={option.id}
onClick={() => {
const newSelected = selected.includes(option.id)
? selected.filter(id => id !== option.id)
: [...selected, option.id];
onChange(newSelected);
}}
className="relative w-full flex items-center px-8 py-2 rounded-md text-sm text-gray-200
hover:bg-white/5 cursor-pointer outline-none"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<span className="absolute left-2">
{selected.includes(option.id) && (
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
className="w-4 h-4 text-blue-400"
>
<Check className="w-4 h-4" />
</motion.div>
)}
</span>
{option.name}
</motion.button>
))}
</div>
</Popover.Content>
</Popover.Portal>
</Popover.Root>
</div>
);
};
const FilterBar: React.FC<FilterBarProps> = ({
selectedScenes,
selectedCharacters,
onScenesChange,
onCharactersChange,
onReset,
}) => {
const sceneOptions = mockSceneOptions.map(scene => ({
id: scene.sceneId,
name: scene.name,
}));
const characterOptions = mockCharacterOptions.map(char => ({
id: char.characterId,
name: char.name,
}));
return (
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="w-full bg-black/20 backdrop-blur-sm rounded-xl p-4 border border-white/10"
>
<div className="flex flex-wrap gap-4 items-center">
<MultiSelect
label="场景"
options={sceneOptions}
selected={selectedScenes}
onChange={onScenesChange}
/>
<MultiSelect
label="角色"
options={characterOptions}
selected={selectedCharacters}
onChange={onCharactersChange}
/>
<button
onClick={onReset}
className="h-10 px-4 rounded-lg bg-white/5 hover:bg-white/10
text-sm text-gray-300 transition-colors flex items-center gap-2"
>
<X className="w-4 h-4" />
</button>
</div>
{/* 当前筛选条件 */}
<AnimatePresence>
{(selectedScenes.length > 0 || selectedCharacters.length > 0) && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
className="mt-4 flex flex-wrap gap-2"
>
{selectedScenes.map((sceneId) => {
const scene = mockSceneOptions.find(s => s.sceneId === sceneId);
return scene && (
<motion.span
key={sceneId}
initial={{ scale: 0 }}
animate={{ scale: 1 }}
exit={{ scale: 0 }}
className="px-2 py-1 rounded-md bg-blue-500/20 text-blue-400 text-sm"
>
{scene.name}
</motion.span>
);
})}
{selectedCharacters.map((charId) => {
const char = mockCharacterOptions.find(c => c.characterId === charId);
return char && (
<motion.span
key={charId}
initial={{ scale: 0 }}
animate={{ scale: 1 }}
exit={{ scale: 0 }}
className="px-2 py-1 rounded-md bg-yellow-500/20 text-yellow-400 text-sm"
>
{char.name}
</motion.span>
);
})}
</motion.div>
)}
</AnimatePresence>
</motion.div>
);
};
export default FilterBar;