'use client'; import React, { useEffect, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; interface MultiLevelOption { label: string; value: string; } interface MultiLevelCategory { key: string; label: string; options: MultiLevelOption[]; multiSelect?: boolean; } interface MultiLevelSelectorProps { onChange: (values: Record) => void; contentType?: 'movie' | 'tv' | 'show' | 'anime-tv' | 'anime-movie'; } const MultiLevelSelector: React.FC = ({ onChange, contentType = 'movie', }) => { const [activeCategory, setActiveCategory] = useState(null); const [dropdownPosition, setDropdownPosition] = useState<{ x: number; y: number; width: number; }>({ x: 0, y: 0, width: 0 }); const [values, setValues] = useState>({}); const categoryRefs = useRef>({}); const dropdownRef = useRef(null); // 根据内容类型获取对应的类型选项 const getTypeOptions = ( contentType: 'movie' | 'tv' | 'show' | 'anime-tv' | 'anime-movie' ) => { const baseOptions = [{ label: '全部', value: 'all' }]; switch (contentType) { case 'movie': return [ ...baseOptions, { label: '喜剧', value: 'comedy' }, { label: '爱情', value: 'romance' }, { label: '动作', value: 'action' }, { label: '科幻', value: 'sci-fi' }, { label: '悬疑', value: 'suspense' }, { label: '犯罪', value: 'crime' }, { label: '惊悚', value: 'thriller' }, { label: '冒险', value: 'adventure' }, { label: '音乐', value: 'music' }, { label: '历史', value: 'history' }, { label: '奇幻', value: 'fantasy' }, { label: '恐怖', value: 'horror' }, { label: '战争', value: 'war' }, { label: '传记', value: 'biography' }, { label: '歌舞', value: 'musical' }, { label: '武侠', value: 'wuxia' }, { label: '情色', value: 'erotic' }, { label: '灾难', value: 'disaster' }, { label: '西部', value: 'western' }, { label: '纪录片', value: 'documentary' }, { label: '短片', value: 'short' }, ]; case 'tv': return [ ...baseOptions, { label: '喜剧', value: 'comedy' }, { label: '爱情', value: 'romance' }, { label: '悬疑', value: 'suspense' }, { label: '武侠', value: 'wuxia' }, { label: '古装', value: 'costume' }, { label: '家庭', value: 'family' }, { label: '犯罪', value: 'crime' }, { label: '科幻', value: 'sci-fi' }, { label: '恐怖', value: 'horror' }, { label: '历史', value: 'history' }, { label: '战争', value: 'war' }, { label: '动作', value: 'action' }, { label: '冒险', value: 'adventure' }, { label: '传记', value: 'biography' }, { label: '剧情', value: 'drama' }, { label: '奇幻', value: 'fantasy' }, { label: '惊悚', value: 'thriller' }, { label: '灾难', value: 'disaster' }, { label: '歌舞', value: 'musical' }, { label: '音乐', value: 'music' }, ]; case 'show': return [ ...baseOptions, { label: '真人秀', value: 'reality' }, { label: '脱口秀', value: 'talkshow' }, { label: '音乐', value: 'music' }, { label: '歌舞', value: 'musical' }, ]; case 'anime-tv': case 'anime-movie': default: return baseOptions; } }; // 根据内容类型获取对应的地区选项 const getRegionOptions = ( contentType: 'movie' | 'tv' | 'show' | 'anime-tv' | 'anime-movie' ) => { const baseOptions = [{ label: '全部', value: 'all' }]; switch (contentType) { case 'movie': case 'anime-movie': return [ ...baseOptions, { label: '华语', value: 'chinese' }, { label: '欧美', value: 'western' }, { label: '韩国', value: 'korean' }, { label: '日本', value: 'japanese' }, { label: '中国大陆', value: 'mainland_china' }, { label: '美国', value: 'usa' }, { label: '中国香港', value: 'hong_kong' }, { label: '中国台湾', value: 'taiwan' }, { label: '英国', value: 'uk' }, { label: '法国', value: 'france' }, { label: '德国', value: 'germany' }, { label: '意大利', value: 'italy' }, { label: '西班牙', value: 'spain' }, { label: '印度', value: 'india' }, { label: '泰国', value: 'thailand' }, { label: '俄罗斯', value: 'russia' }, { label: '加拿大', value: 'canada' }, { label: '澳大利亚', value: 'australia' }, { label: '爱尔兰', value: 'ireland' }, { label: '瑞典', value: 'sweden' }, { label: '巴西', value: 'brazil' }, { label: '丹麦', value: 'denmark' }, ]; case 'tv': case 'anime-tv': case 'show': return [ ...baseOptions, { label: '华语', value: 'chinese' }, { label: '欧美', value: 'western' }, { label: '国外', value: 'foreign' }, { label: '韩国', value: 'korean' }, { label: '日本', value: 'japanese' }, { label: '中国大陆', value: 'mainland_china' }, { label: '中国香港', value: 'hong_kong' }, { label: '美国', value: 'usa' }, { label: '英国', value: 'uk' }, { label: '泰国', value: 'thailand' }, { label: '中国台湾', value: 'taiwan' }, { label: '意大利', value: 'italy' }, { label: '法国', value: 'france' }, { label: '德国', value: 'germany' }, { label: '西班牙', value: 'spain' }, { label: '俄罗斯', value: 'russia' }, { label: '瑞典', value: 'sweden' }, { label: '巴西', value: 'brazil' }, { label: '丹麦', value: 'denmark' }, { label: '印度', value: 'india' }, { label: '加拿大', value: 'canada' }, { label: '爱尔兰', value: 'ireland' }, { label: '澳大利亚', value: 'australia' }, ]; default: return baseOptions; } }; const getLabelOptions = ( contentType: 'movie' | 'tv' | 'show' | 'anime-tv' | 'anime-movie' ) => { const baseOptions = [{ label: '全部', value: 'all' }]; switch (contentType) { case 'anime-movie': return [ ...baseOptions, { label: '定格动画', value: 'stop_motion' }, { label: '传记', value: 'biography' }, { label: '美国动画', value: 'us_animation' }, { label: '爱情', value: 'romance' }, { label: '黑色幽默', value: 'dark_humor' }, { label: '歌舞', value: 'musical' }, { label: '儿童', value: 'children' }, { label: '二次元', value: 'anime' }, { label: '动物', value: 'animal' }, { label: '青春', value: 'youth' }, { label: '历史', value: 'history' }, { label: '励志', value: 'inspirational' }, { label: '恶搞', value: 'parody' }, { label: '治愈', value: 'healing' }, { label: '运动', value: 'sports' }, { label: '后宫', value: 'harem' }, { label: '情色', value: 'erotic' }, { label: '人性', value: 'human_nature' }, { label: '悬疑', value: 'suspense' }, { label: '恋爱', value: 'love' }, { label: '魔幻', value: 'fantasy' }, { label: '科幻', value: 'sci_fi' }, ]; case 'anime-tv': return [ ...baseOptions, { label: '黑色幽默', value: 'dark_humor' }, { label: '历史', value: 'history' }, { label: '歌舞', value: 'musical' }, { label: '励志', value: 'inspirational' }, { label: '恶搞', value: 'parody' }, { label: '治愈', value: 'healing' }, { label: '运动', value: 'sports' }, { label: '后宫', value: 'harem' }, { label: '情色', value: 'erotic' }, { label: '国漫', value: 'chinese_anime' }, { label: '人性', value: 'human_nature' }, { label: '悬疑', value: 'suspense' }, { label: '恋爱', value: 'love' }, { label: '魔幻', value: 'fantasy' }, { label: '科幻', value: 'sci_fi' }, ]; default: return baseOptions; } }; // 根据内容类型获取对应的平台选项 const getPlatformOptions = ( contentType: 'movie' | 'tv' | 'show' | 'anime-tv' | 'anime-movie' ) => { const baseOptions = [{ label: '全部', value: 'all' }]; switch (contentType) { case 'movie': return baseOptions; // 电影不需要平台选项 case 'tv': case 'anime-tv': case 'show': return [ ...baseOptions, { label: '腾讯视频', value: 'tencent' }, { label: '爱奇艺', value: 'iqiyi' }, { label: '优酷', value: 'youku' }, { label: '湖南卫视', value: 'hunan_tv' }, { label: 'Netflix', value: 'netflix' }, { label: 'HBO', value: 'hbo' }, { label: 'BBC', value: 'bbc' }, { label: 'NHK', value: 'nhk' }, { label: 'CBS', value: 'cbs' }, { label: 'NBC', value: 'nbc' }, { label: 'tvN', value: 'tvn' }, ]; default: return baseOptions; } }; // 分类配置 const categories: MultiLevelCategory[] = [ ...(contentType !== 'anime-tv' && contentType !== 'anime-movie' ? [ { key: 'type', label: '类型', options: getTypeOptions(contentType), }, ] : [ { key: 'label', label: '类型', options: getLabelOptions(contentType), }, ]), { key: 'region', label: '地区', options: getRegionOptions(contentType), }, { key: 'year', label: '年代', options: [ { label: '全部', value: 'all' }, { label: '2020年代', value: '2020s' }, { label: '2025', value: '2025' }, { label: '2024', value: '2024' }, { label: '2023', value: '2023' }, { label: '2022', value: '2022' }, { label: '2021', value: '2021' }, { label: '2020', value: '2020' }, { label: '2019', value: '2019' }, { label: '2010年代', value: '2010s' }, { label: '2000年代', value: '2000s' }, { label: '90年代', value: '1990s' }, { label: '80年代', value: '1980s' }, { label: '70年代', value: '1970s' }, { label: '60年代', value: '1960s' }, { label: '更早', value: 'earlier' }, ], }, // 只在电视剧和综艺时显示平台选项 ...(contentType === 'tv' || contentType === 'show' || contentType === 'anime-tv' ? [ { key: 'platform', label: '平台', options: getPlatformOptions(contentType), }, ] : []), { key: 'sort', label: '排序', options: [ { label: '综合排序', value: 'T' }, { label: '近期热度', value: 'U' }, { label: contentType === 'tv' || contentType === 'show' ? '首播时间' : '首映时间', value: 'R', }, { label: '高分优先', value: 'S' }, ], }, ]; // 计算下拉框位置 const calculateDropdownPosition = (categoryKey: string) => { const element = categoryRefs.current[categoryKey]; if (element) { const rect = element.getBoundingClientRect(); const viewportWidth = window.innerWidth; const isMobile = viewportWidth < 768; // md breakpoint let x = rect.left; let dropdownWidth = Math.max(rect.width, 300); let useFixedWidth = false; // 标记是否使用固定宽度 // 移动端优化:防止下拉框被右侧视口截断 if (isMobile) { const padding = 16; // 左右各留16px的边距 const maxWidth = viewportWidth - padding * 2; dropdownWidth = Math.min(dropdownWidth, maxWidth); useFixedWidth = true; // 移动端使用固定宽度 // 如果右侧超出视口,则调整x位置 if (x + dropdownWidth > viewportWidth - padding) { x = viewportWidth - dropdownWidth - padding; } // 如果左侧超出视口,则贴左边 if (x < padding) { x = padding; } } setDropdownPosition({ x, y: rect.bottom, width: useFixedWidth ? dropdownWidth : rect.width, // PC端保持原有逻辑 }); } }; // 处理分类点击 const handleCategoryClick = (categoryKey: string) => { if (activeCategory === categoryKey) { setActiveCategory(null); } else { setActiveCategory(categoryKey); calculateDropdownPosition(categoryKey); } }; // 处理选项选择 const handleOptionSelect = (categoryKey: string, optionValue: string) => { // 更新本地状态 const newValues = { ...values, [categoryKey]: optionValue, }; // 更新内部状态 setValues(newValues); // 构建传递给父组件的值,排序传递 value,其他传递 label const selectionsForParent: Record = { type: 'all', region: 'all', year: 'all', platform: 'all', label: 'all', sort: 'T', }; Object.entries(newValues).forEach(([key, value]) => { if (value && value !== 'all' && (key !== 'sort' || value !== 'T')) { const category = categories.find((cat) => cat.key === key); if (category) { const option = category.options.find((opt) => opt.value === value); if (option) { // 排序传递 value,其他传递 label selectionsForParent[key] = key === 'sort' ? option.value : option.label; } } } }); // 调用父组件的回调,传递处理后的选择值 onChange(selectionsForParent); setActiveCategory(null); }; // 获取显示文本 const getDisplayText = (categoryKey: string) => { const category = categories.find((cat) => cat.key === categoryKey); if (!category) return ''; const value = values[categoryKey]; if ( !value || value === 'all' || (categoryKey === 'sort' && value === 'T') ) { return category.label; } const option = category.options.find((opt) => opt.value === value); return option?.label || category.label; }; // 检查是否为默认值 const isDefaultValue = (categoryKey: string) => { const value = values[categoryKey]; return ( !value || value === 'all' || (categoryKey === 'sort' && value === 'T') ); }; // 检查选项是否被选中 const isOptionSelected = (categoryKey: string, optionValue: string) => { let value = values[categoryKey]; if (value === undefined) { value = 'all'; if (categoryKey === 'sort') { value = 'T'; } } return value === optionValue; }; // 监听滚动和窗口大小变化事件,重新计算位置 useEffect(() => { const handleScroll = () => { if (activeCategory) { calculateDropdownPosition(activeCategory); } }; const handleResize = () => { if (activeCategory) { calculateDropdownPosition(activeCategory); } }; window.addEventListener('scroll', handleScroll); window.addEventListener('resize', handleResize); return () => { window.removeEventListener('scroll', handleScroll); window.removeEventListener('resize', handleResize); }; }, [activeCategory]); // 点击外部关闭下拉框 useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if ( dropdownRef.current && !dropdownRef.current.contains(event.target as Node) && !Object.values(categoryRefs.current).some( (ref) => ref && ref.contains(event.target as Node) ) ) { setActiveCategory(null); } }; document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, []); return ( <> {/* 胶囊样式筛选栏 */}
{categories.map((category) => (
{ categoryRefs.current[category.key] = el; }} className='relative' >
))}
{/* 展开的筛选选项 - 悬浮显示 */} {activeCategory && createPortal(
{categories .find((cat) => cat.key === activeCategory) ?.options.map((option) => ( ))}
, document.body )} ); }; export default MultiLevelSelector;