forked from 77media/video-flow
删除
This commit is contained in:
parent
2708f14378
commit
c499d14167
@ -1,176 +0,0 @@
|
||||
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;
|
||||
@ -1,137 +0,0 @@
|
||||
import React from 'react';
|
||||
import * as Tooltip from '@radix-ui/react-tooltip';
|
||||
import { characterInfoMap, sceneInfoMap, mockCharacterOptions, mockSceneOptions } from '@/app/model/enums';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
interface KeywordTextProps {
|
||||
text: string;
|
||||
className?: string;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
const KeywordText: React.FC<KeywordTextProps> = ({ text, className = '', id }) => {
|
||||
// 解析文本中的关键词
|
||||
const parseText = (text: string) => {
|
||||
const parts: React.ReactNode[] = [];
|
||||
let currentIndex = 0;
|
||||
|
||||
// 匹配 #角色# 和 [场景]
|
||||
const regex = /#([^#]+)#|\[([^\]]+)\]/g;
|
||||
let match;
|
||||
|
||||
while ((match = regex.exec(text)) !== null) {
|
||||
// 添加普通文本
|
||||
if (match.index > currentIndex) {
|
||||
parts.push(text.slice(currentIndex, match.index));
|
||||
}
|
||||
|
||||
const [fullMatch, character, scene] = match;
|
||||
if (character) {
|
||||
// 角色关键词
|
||||
const info = mockCharacterOptions.find(option => option.characterId === id);
|
||||
if (info) {
|
||||
parts.push(
|
||||
<Tooltip.Provider key={match.index}>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<motion.span
|
||||
className="text-yellow-400 font-medium cursor-help"
|
||||
whileHover={{ scale: 1.05 }}
|
||||
transition={{ type: "spring", stiffness: 400, damping: 10 }}
|
||||
>
|
||||
#{character}#
|
||||
</motion.span>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content
|
||||
className="z-[9999] bg-black/60 backdrop-blur-lg p-3 rounded-lg shadow-xl border border-white/10"
|
||||
sideOffset={5}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<img
|
||||
src={info.image}
|
||||
alt={character}
|
||||
className="w-12 h-12 rounded-full object-cover"
|
||||
/>
|
||||
<div>
|
||||
<div className="font-medium text-white">{character}</div>
|
||||
<div className="text-sm text-gray-300">
|
||||
{info.gender} · {info.age}岁
|
||||
</div>
|
||||
<div className="text-sm text-gray-400 mt-1">
|
||||
{info.description}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Tooltip.Arrow className="fill-black/90" />
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
);
|
||||
} else {
|
||||
parts.push(<span key={match.index} className="text-yellow-400">#{character}#</span>);
|
||||
}
|
||||
} else if (scene) {
|
||||
// 场景关键词
|
||||
const info = mockSceneOptions.find(option => option.sceneId === id);
|
||||
console.log('info', info);
|
||||
if (info) {
|
||||
parts.push(
|
||||
<Tooltip.Provider key={match.index}>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<motion.span
|
||||
className="text-blue-400 font-medium cursor-help"
|
||||
whileHover={{ scale: 1.05 }}
|
||||
transition={{ type: "spring", stiffness: 400, damping: 10 }}
|
||||
>
|
||||
[{scene}]
|
||||
</motion.span>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content
|
||||
className="z-[9999] bg-black/60 backdrop-blur-lg p-3 rounded-lg shadow-xl border border-white/10"
|
||||
sideOffset={5}
|
||||
>
|
||||
<div className="w-48">
|
||||
<img
|
||||
src={info.image}
|
||||
alt={scene}
|
||||
className="w-full h-24 object-cover rounded-lg mb-2"
|
||||
/>
|
||||
<div className="font-medium text-white">{scene}</div>
|
||||
<div className="text-sm text-gray-300 mt-1">
|
||||
{info.location} · {info.time}
|
||||
</div>
|
||||
</div>
|
||||
<Tooltip.Arrow className="fill-black/90" />
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
);
|
||||
} else {
|
||||
parts.push(<span key={match.index} className="text-blue-400">[{scene}]</span>);
|
||||
}
|
||||
}
|
||||
|
||||
currentIndex = match.index + fullMatch.length;
|
||||
}
|
||||
|
||||
// 添加剩余文本
|
||||
if (currentIndex < text.length) {
|
||||
parts.push(text.slice(currentIndex));
|
||||
}
|
||||
|
||||
return parts;
|
||||
};
|
||||
|
||||
return (
|
||||
<span className={className}>
|
||||
{parseText(text)}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default KeywordText;
|
||||
Loading…
x
Reference in New Issue
Block a user