mirror of
https://github.com/MoonTechLab/LunaTV.git
synced 2026-02-15 20:34:41 +08:00
568 lines
19 KiB
TypeScript
568 lines
19 KiB
TypeScript
/* eslint-disable react-hooks/exhaustive-deps */
|
|
|
|
'use client';
|
|
|
|
import React, { useEffect, useRef, useState } from 'react';
|
|
|
|
import MultiLevelSelector from './MultiLevelSelector';
|
|
import WeekdaySelector from './WeekdaySelector';
|
|
|
|
interface SelectorOption {
|
|
label: string;
|
|
value: string;
|
|
}
|
|
|
|
interface DoubanSelectorProps {
|
|
type: 'movie' | 'tv' | 'show' | 'anime';
|
|
primarySelection?: string;
|
|
secondarySelection?: string;
|
|
onPrimaryChange: (value: string) => void;
|
|
onSecondaryChange: (value: string) => void;
|
|
onMultiLevelChange?: (values: Record<string, string>) => void;
|
|
onWeekdayChange: (weekday: string) => void;
|
|
}
|
|
|
|
const DoubanSelector: React.FC<DoubanSelectorProps> = ({
|
|
type,
|
|
primarySelection,
|
|
secondarySelection,
|
|
onPrimaryChange,
|
|
onSecondaryChange,
|
|
onMultiLevelChange,
|
|
onWeekdayChange,
|
|
}) => {
|
|
// 为不同的选择器创建独立的refs和状态
|
|
const primaryContainerRef = useRef<HTMLDivElement>(null);
|
|
const primaryButtonRefs = useRef<(HTMLButtonElement | null)[]>([]);
|
|
const [primaryIndicatorStyle, setPrimaryIndicatorStyle] = useState<{
|
|
left: number;
|
|
width: number;
|
|
}>({ left: 0, width: 0 });
|
|
|
|
const secondaryContainerRef = useRef<HTMLDivElement>(null);
|
|
const secondaryButtonRefs = useRef<(HTMLButtonElement | null)[]>([]);
|
|
const [secondaryIndicatorStyle, setSecondaryIndicatorStyle] = useState<{
|
|
left: number;
|
|
width: number;
|
|
}>({ left: 0, width: 0 });
|
|
|
|
// 电影的一级选择器选项
|
|
const moviePrimaryOptions: SelectorOption[] = [
|
|
{ label: '全部', value: '全部' },
|
|
{ label: '热门电影', value: '热门' },
|
|
{ label: '最新电影', value: '最新' },
|
|
{ label: '豆瓣高分', value: '豆瓣高分' },
|
|
{ label: '冷门佳片', value: '冷门佳片' },
|
|
];
|
|
|
|
// 电影的二级选择器选项
|
|
const movieSecondaryOptions: SelectorOption[] = [
|
|
{ label: '全部', value: '全部' },
|
|
{ label: '华语', value: '华语' },
|
|
{ label: '欧美', value: '欧美' },
|
|
{ label: '韩国', value: '韩国' },
|
|
{ label: '日本', value: '日本' },
|
|
];
|
|
|
|
// 电视剧一级选择器选项
|
|
const tvPrimaryOptions: SelectorOption[] = [
|
|
{ label: '全部', value: '全部' },
|
|
{ label: '最近热门', value: '最近热门' },
|
|
];
|
|
|
|
// 电视剧二级选择器选项
|
|
const tvSecondaryOptions: SelectorOption[] = [
|
|
{ label: '全部', value: 'tv' },
|
|
{ label: '国产', value: 'tv_domestic' },
|
|
{ label: '欧美', value: 'tv_american' },
|
|
{ label: '日本', value: 'tv_japanese' },
|
|
{ label: '韩国', value: 'tv_korean' },
|
|
{ label: '动漫', value: 'tv_animation' },
|
|
{ label: '纪录片', value: 'tv_documentary' },
|
|
];
|
|
|
|
// 综艺一级选择器选项
|
|
const showPrimaryOptions: SelectorOption[] = [
|
|
{ label: '全部', value: '全部' },
|
|
{ label: '最近热门', value: '最近热门' },
|
|
];
|
|
|
|
// 综艺二级选择器选项
|
|
const showSecondaryOptions: SelectorOption[] = [
|
|
{ label: '全部', value: 'show' },
|
|
{ label: '国内', value: 'show_domestic' },
|
|
{ label: '国外', value: 'show_foreign' },
|
|
];
|
|
|
|
// 动漫一级选择器选项
|
|
const animePrimaryOptions: SelectorOption[] = [
|
|
{ label: '每日放送', value: '每日放送' },
|
|
{ label: '番剧', value: '番剧' },
|
|
{ label: '剧场版', value: '剧场版' },
|
|
];
|
|
|
|
// 处理多级选择器变化
|
|
const handleMultiLevelChange = (values: Record<string, string>) => {
|
|
onMultiLevelChange?.(values);
|
|
};
|
|
|
|
// 更新指示器位置的通用函数
|
|
const updateIndicatorPosition = (
|
|
activeIndex: number,
|
|
containerRef: React.RefObject<HTMLDivElement>,
|
|
buttonRefs: React.MutableRefObject<(HTMLButtonElement | null)[]>,
|
|
setIndicatorStyle: React.Dispatch<
|
|
React.SetStateAction<{ left: number; width: number }>
|
|
>
|
|
) => {
|
|
if (
|
|
activeIndex >= 0 &&
|
|
buttonRefs.current[activeIndex] &&
|
|
containerRef.current
|
|
) {
|
|
const timeoutId = setTimeout(() => {
|
|
const button = buttonRefs.current[activeIndex];
|
|
const container = containerRef.current;
|
|
if (button && container) {
|
|
const buttonRect = button.getBoundingClientRect();
|
|
const containerRect = container.getBoundingClientRect();
|
|
|
|
if (buttonRect.width > 0) {
|
|
setIndicatorStyle({
|
|
left: buttonRect.left - containerRect.left,
|
|
width: buttonRect.width,
|
|
});
|
|
}
|
|
}
|
|
}, 0);
|
|
return () => clearTimeout(timeoutId);
|
|
}
|
|
};
|
|
|
|
// 组件挂载时立即计算初始位置
|
|
useEffect(() => {
|
|
// 主选择器初始位置
|
|
if (type === 'movie') {
|
|
const activeIndex = moviePrimaryOptions.findIndex(
|
|
(opt) =>
|
|
opt.value === (primarySelection || moviePrimaryOptions[0].value)
|
|
);
|
|
updateIndicatorPosition(
|
|
activeIndex,
|
|
primaryContainerRef,
|
|
primaryButtonRefs,
|
|
setPrimaryIndicatorStyle
|
|
);
|
|
} else if (type === 'tv') {
|
|
const activeIndex = tvPrimaryOptions.findIndex(
|
|
(opt) => opt.value === (primarySelection || tvPrimaryOptions[1].value)
|
|
);
|
|
updateIndicatorPosition(
|
|
activeIndex,
|
|
primaryContainerRef,
|
|
primaryButtonRefs,
|
|
setPrimaryIndicatorStyle
|
|
);
|
|
} else if (type === 'anime') {
|
|
const activeIndex = animePrimaryOptions.findIndex(
|
|
(opt) =>
|
|
opt.value === (primarySelection || animePrimaryOptions[0].value)
|
|
);
|
|
updateIndicatorPosition(
|
|
activeIndex,
|
|
primaryContainerRef,
|
|
primaryButtonRefs,
|
|
setPrimaryIndicatorStyle
|
|
);
|
|
} else if (type === 'show') {
|
|
const activeIndex = showPrimaryOptions.findIndex(
|
|
(opt) => opt.value === (primarySelection || showPrimaryOptions[1].value)
|
|
);
|
|
updateIndicatorPosition(
|
|
activeIndex,
|
|
primaryContainerRef,
|
|
primaryButtonRefs,
|
|
setPrimaryIndicatorStyle
|
|
);
|
|
}
|
|
|
|
// 副选择器初始位置
|
|
let secondaryActiveIndex = -1;
|
|
if (type === 'movie') {
|
|
secondaryActiveIndex = movieSecondaryOptions.findIndex(
|
|
(opt) =>
|
|
opt.value === (secondarySelection || movieSecondaryOptions[0].value)
|
|
);
|
|
} else if (type === 'tv') {
|
|
secondaryActiveIndex = tvSecondaryOptions.findIndex(
|
|
(opt) =>
|
|
opt.value === (secondarySelection || tvSecondaryOptions[0].value)
|
|
);
|
|
} else if (type === 'show') {
|
|
secondaryActiveIndex = showSecondaryOptions.findIndex(
|
|
(opt) =>
|
|
opt.value === (secondarySelection || showSecondaryOptions[0].value)
|
|
);
|
|
}
|
|
|
|
if (secondaryActiveIndex >= 0) {
|
|
updateIndicatorPosition(
|
|
secondaryActiveIndex,
|
|
secondaryContainerRef,
|
|
secondaryButtonRefs,
|
|
setSecondaryIndicatorStyle
|
|
);
|
|
}
|
|
}, [type]); // 只在type变化时重新计算
|
|
|
|
// 监听主选择器变化
|
|
useEffect(() => {
|
|
if (type === 'movie') {
|
|
const activeIndex = moviePrimaryOptions.findIndex(
|
|
(opt) => opt.value === primarySelection
|
|
);
|
|
const cleanup = updateIndicatorPosition(
|
|
activeIndex,
|
|
primaryContainerRef,
|
|
primaryButtonRefs,
|
|
setPrimaryIndicatorStyle
|
|
);
|
|
return cleanup;
|
|
} else if (type === 'tv') {
|
|
const activeIndex = tvPrimaryOptions.findIndex(
|
|
(opt) => opt.value === primarySelection
|
|
);
|
|
const cleanup = updateIndicatorPosition(
|
|
activeIndex,
|
|
primaryContainerRef,
|
|
primaryButtonRefs,
|
|
setPrimaryIndicatorStyle
|
|
);
|
|
return cleanup;
|
|
} else if (type === 'anime') {
|
|
const activeIndex = animePrimaryOptions.findIndex(
|
|
(opt) => opt.value === primarySelection
|
|
);
|
|
const cleanup = updateIndicatorPosition(
|
|
activeIndex,
|
|
primaryContainerRef,
|
|
primaryButtonRefs,
|
|
setPrimaryIndicatorStyle
|
|
);
|
|
return cleanup;
|
|
} else if (type === 'show') {
|
|
const activeIndex = showPrimaryOptions.findIndex(
|
|
(opt) => opt.value === primarySelection
|
|
);
|
|
const cleanup = updateIndicatorPosition(
|
|
activeIndex,
|
|
primaryContainerRef,
|
|
primaryButtonRefs,
|
|
setPrimaryIndicatorStyle
|
|
);
|
|
return cleanup;
|
|
}
|
|
}, [primarySelection]);
|
|
|
|
// 监听副选择器变化
|
|
useEffect(() => {
|
|
let activeIndex = -1;
|
|
let options: SelectorOption[] = [];
|
|
|
|
if (type === 'movie') {
|
|
activeIndex = movieSecondaryOptions.findIndex(
|
|
(opt) => opt.value === secondarySelection
|
|
);
|
|
options = movieSecondaryOptions;
|
|
} else if (type === 'tv') {
|
|
activeIndex = tvSecondaryOptions.findIndex(
|
|
(opt) => opt.value === secondarySelection
|
|
);
|
|
options = tvSecondaryOptions;
|
|
} else if (type === 'show') {
|
|
activeIndex = showSecondaryOptions.findIndex(
|
|
(opt) => opt.value === secondarySelection
|
|
);
|
|
options = showSecondaryOptions;
|
|
}
|
|
|
|
if (options.length > 0) {
|
|
const cleanup = updateIndicatorPosition(
|
|
activeIndex,
|
|
secondaryContainerRef,
|
|
secondaryButtonRefs,
|
|
setSecondaryIndicatorStyle
|
|
);
|
|
return cleanup;
|
|
}
|
|
}, [secondarySelection]);
|
|
|
|
// 渲染胶囊式选择器
|
|
const renderCapsuleSelector = (
|
|
options: SelectorOption[],
|
|
activeValue: string | undefined,
|
|
onChange: (value: string) => void,
|
|
isPrimary = false
|
|
) => {
|
|
const containerRef = isPrimary
|
|
? primaryContainerRef
|
|
: secondaryContainerRef;
|
|
const buttonRefs = isPrimary ? primaryButtonRefs : secondaryButtonRefs;
|
|
const indicatorStyle = isPrimary
|
|
? primaryIndicatorStyle
|
|
: secondaryIndicatorStyle;
|
|
|
|
return (
|
|
<div
|
|
ref={containerRef}
|
|
className='relative inline-flex bg-gray-200/60 rounded-full p-0.5 sm:p-1 dark:bg-gray-700/60 backdrop-blur-sm'
|
|
>
|
|
{/* 滑动的白色背景指示器 */}
|
|
{indicatorStyle.width > 0 && (
|
|
<div
|
|
className='absolute top-0.5 bottom-0.5 sm:top-1 sm:bottom-1 bg-white dark:bg-gray-500 rounded-full shadow-sm transition-all duration-300 ease-out'
|
|
style={{
|
|
left: `${indicatorStyle.left}px`,
|
|
width: `${indicatorStyle.width}px`,
|
|
}}
|
|
/>
|
|
)}
|
|
|
|
{options.map((option, index) => {
|
|
const isActive = activeValue === option.value;
|
|
return (
|
|
<button
|
|
key={option.value}
|
|
ref={(el) => {
|
|
buttonRefs.current[index] = el;
|
|
}}
|
|
onClick={() => onChange(option.value)}
|
|
className={`relative z-10 px-2 py-1 sm:px-4 sm:py-2 text-xs sm:text-sm font-medium rounded-full transition-all duration-200 whitespace-nowrap ${
|
|
isActive
|
|
? 'text-gray-900 dark:text-gray-100 cursor-default'
|
|
: 'text-gray-700 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-100 cursor-pointer'
|
|
}`}
|
|
>
|
|
{option.label}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<div className='space-y-4 sm:space-y-6'>
|
|
{/* 电影类型 - 显示两级选择器 */}
|
|
{type === 'movie' && (
|
|
<div className='space-y-3 sm:space-y-4'>
|
|
{/* 一级选择器 */}
|
|
<div className='flex flex-col sm:flex-row sm:items-center gap-2'>
|
|
<span className='text-xs sm:text-sm font-medium text-gray-600 dark:text-gray-400 min-w-[48px]'>
|
|
分类
|
|
</span>
|
|
<div className='overflow-x-auto'>
|
|
{renderCapsuleSelector(
|
|
moviePrimaryOptions,
|
|
primarySelection || moviePrimaryOptions[0].value,
|
|
onPrimaryChange,
|
|
true
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* 二级选择器 - 只在非"全部"时显示 */}
|
|
{primarySelection !== '全部' ? (
|
|
<div className='flex flex-col sm:flex-row sm:items-center gap-2'>
|
|
<span className='text-xs sm:text-sm font-medium text-gray-600 dark:text-gray-400 min-w-[48px]'>
|
|
地区
|
|
</span>
|
|
<div className='overflow-x-auto'>
|
|
{renderCapsuleSelector(
|
|
movieSecondaryOptions,
|
|
secondarySelection || movieSecondaryOptions[0].value,
|
|
onSecondaryChange,
|
|
false
|
|
)}
|
|
</div>
|
|
</div>
|
|
) : (
|
|
/* 多级选择器 - 只在选中"全部"时显示 */
|
|
<div className='flex flex-col sm:flex-row sm:items-center gap-2'>
|
|
<span className='text-xs sm:text-sm font-medium text-gray-600 dark:text-gray-400 min-w-[48px]'>
|
|
筛选
|
|
</span>
|
|
<div className='overflow-x-auto'>
|
|
<MultiLevelSelector
|
|
key={`${type}-${primarySelection}`}
|
|
onChange={handleMultiLevelChange}
|
|
contentType={type}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* 电视剧类型 - 显示两级选择器 */}
|
|
{type === 'tv' && (
|
|
<div className='space-y-3 sm:space-y-4'>
|
|
{/* 一级选择器 */}
|
|
<div className='flex flex-col sm:flex-row sm:items-center gap-2'>
|
|
<span className='text-xs sm:text-sm font-medium text-gray-600 dark:text-gray-400 min-w-[48px]'>
|
|
分类
|
|
</span>
|
|
<div className='overflow-x-auto'>
|
|
{renderCapsuleSelector(
|
|
tvPrimaryOptions,
|
|
primarySelection || tvPrimaryOptions[1].value,
|
|
onPrimaryChange,
|
|
true
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* 二级选择器 - 只在选中"最近热门"时显示,选中"全部"时显示多级选择器 */}
|
|
{(primarySelection || tvPrimaryOptions[1].value) === '最近热门' ? (
|
|
<div className='flex flex-col sm:flex-row sm:items-center gap-2'>
|
|
<span className='text-xs sm:text-sm font-medium text-gray-600 dark:text-gray-400 min-w-[48px]'>
|
|
类型
|
|
</span>
|
|
<div className='overflow-x-auto'>
|
|
{renderCapsuleSelector(
|
|
tvSecondaryOptions,
|
|
secondarySelection || tvSecondaryOptions[0].value,
|
|
onSecondaryChange,
|
|
false
|
|
)}
|
|
</div>
|
|
</div>
|
|
) : (primarySelection || tvPrimaryOptions[1].value) === '全部' ? (
|
|
/* 多级选择器 - 只在选中"全部"时显示 */
|
|
<div className='flex flex-col sm:flex-row sm:items-center gap-2'>
|
|
<span className='text-xs sm:text-sm font-medium text-gray-600 dark:text-gray-400 min-w-[48px]'>
|
|
筛选
|
|
</span>
|
|
<div className='overflow-x-auto'>
|
|
<MultiLevelSelector
|
|
key={`${type}-${primarySelection}`}
|
|
onChange={handleMultiLevelChange}
|
|
contentType={type}
|
|
/>
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
)}
|
|
|
|
{/* 动漫类型 - 显示一级选择器和多级选择器 */}
|
|
{type === 'anime' && (
|
|
<div className='space-y-3 sm:space-y-4'>
|
|
<div className='flex flex-col sm:flex-row sm:items-center gap-2'>
|
|
<span className='text-xs sm:text-sm font-medium text-gray-600 dark:text-gray-400 min-w-[48px]'>
|
|
分类
|
|
</span>
|
|
<div className='overflow-x-auto'>
|
|
{renderCapsuleSelector(
|
|
animePrimaryOptions,
|
|
primarySelection || animePrimaryOptions[0].value,
|
|
onPrimaryChange,
|
|
true
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* 筛选部分 - 根据一级选择器显示不同内容 */}
|
|
{(primarySelection || animePrimaryOptions[0].value) === '每日放送' ? (
|
|
// 每日放送分类下显示星期选择器
|
|
<div className='flex flex-col sm:flex-row sm:items-center gap-2'>
|
|
<span className='text-xs sm:text-sm font-medium text-gray-600 dark:text-gray-400 min-w-[48px]'>
|
|
星期
|
|
</span>
|
|
<div className='overflow-x-auto'>
|
|
<WeekdaySelector onWeekdayChange={onWeekdayChange} />
|
|
</div>
|
|
</div>
|
|
) : (
|
|
// 其他分类下显示原有的筛选功能
|
|
<div className='flex flex-col sm:flex-row sm:items-center gap-2'>
|
|
<span className='text-xs sm:text-sm font-medium text-gray-600 dark:text-gray-400 min-w-[48px]'>
|
|
筛选
|
|
</span>
|
|
<div className='overflow-x-auto'>
|
|
{(primarySelection || animePrimaryOptions[0].value) ===
|
|
'番剧' ? (
|
|
<MultiLevelSelector
|
|
key={`anime-tv-${primarySelection}`}
|
|
onChange={handleMultiLevelChange}
|
|
contentType='anime-tv'
|
|
/>
|
|
) : (
|
|
<MultiLevelSelector
|
|
key={`anime-movie-${primarySelection}`}
|
|
onChange={handleMultiLevelChange}
|
|
contentType='anime-movie'
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* 综艺类型 - 显示两级选择器 */}
|
|
{type === 'show' && (
|
|
<div className='space-y-3 sm:space-y-4'>
|
|
{/* 一级选择器 */}
|
|
<div className='flex flex-col sm:flex-row sm:items-center gap-2'>
|
|
<span className='text-xs sm:text-sm font-medium text-gray-600 dark:text-gray-400 min-w-[48px]'>
|
|
分类
|
|
</span>
|
|
<div className='overflow-x-auto'>
|
|
{renderCapsuleSelector(
|
|
showPrimaryOptions,
|
|
primarySelection || showPrimaryOptions[1].value,
|
|
onPrimaryChange,
|
|
true
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* 二级选择器 - 只在选中"最近热门"时显示,选中"全部"时显示多级选择器 */}
|
|
{(primarySelection || showPrimaryOptions[1].value) === '最近热门' ? (
|
|
<div className='flex flex-col sm:flex-row sm:items-center gap-2'>
|
|
<span className='text-xs sm:text-sm font-medium text-gray-600 dark:text-gray-400 min-w-[48px]'>
|
|
类型
|
|
</span>
|
|
<div className='overflow-x-auto'>
|
|
{renderCapsuleSelector(
|
|
showSecondaryOptions,
|
|
secondarySelection || showSecondaryOptions[0].value,
|
|
onSecondaryChange,
|
|
false
|
|
)}
|
|
</div>
|
|
</div>
|
|
) : (primarySelection || showPrimaryOptions[1].value) === '全部' ? (
|
|
/* 多级选择器 - 只在选中"全部"时显示 */
|
|
<div className='flex flex-col sm:flex-row sm:items-center gap-2'>
|
|
<span className='text-xs sm:text-sm font-medium text-gray-600 dark:text-gray-400 min-w-[48px]'>
|
|
筛选
|
|
</span>
|
|
<div className='overflow-x-auto'>
|
|
<MultiLevelSelector
|
|
key={`${type}-${primarySelection}`}
|
|
onChange={handleMultiLevelChange}
|
|
contentType={type}
|
|
/>
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default DoubanSelector;
|