diff --git a/src/app/api/douban/categories/route.ts b/src/app/api/douban/categories/route.ts new file mode 100644 index 0000000..fb62227 --- /dev/null +++ b/src/app/api/douban/categories/route.ts @@ -0,0 +1,129 @@ +import { NextResponse } from 'next/server'; + +import { getCacheTime } from '@/lib/config'; +import { DoubanItem, DoubanResult } from '@/lib/types'; + +interface DoubanCategoryApiResponse { + total: number; + items: Array<{ + id: string; + title: string; + pic: { + large: string; + normal: string; + }; + rating: { + value: number; + }; + }>; +} + +async function fetchDoubanData( + url: string +): Promise { + // 添加超时控制 + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 10000); // 10秒超时 + + // 设置请求选项,包括信号和头部 + const fetchOptions = { + signal: controller.signal, + headers: { + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36', + Referer: 'https://movie.douban.com/', + Accept: 'application/json, text/plain, */*', + Origin: 'https://movie.douban.com', + }, + }; + + try { + // 尝试直接访问豆瓣API + const response = await fetch(url, fetchOptions); + clearTimeout(timeoutId); + + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + + return await response.json(); + } catch (error) { + clearTimeout(timeoutId); + throw error; + } +} + +export const runtime = 'edge'; + +export async function GET(request: Request) { + const { searchParams } = new URL(request.url); + + // 获取参数 + const kind = searchParams.get('kind') || 'movie'; + const category = searchParams.get('category'); + const type = searchParams.get('type'); + const pageLimit = parseInt(searchParams.get('limit') || '20'); + const pageStart = parseInt(searchParams.get('start') || '0'); + + // 验证参数 + if (!kind || !category || !type) { + return NextResponse.json( + { error: '缺少必要参数: kind 或 category 或 type' }, + { status: 400 } + ); + } + + if (!['tv', 'movie'].includes(kind)) { + return NextResponse.json( + { error: 'kind 参数必须是 tv 或 movie' }, + { status: 400 } + ); + } + + if (pageLimit < 1 || pageLimit > 100) { + return NextResponse.json( + { error: 'pageSize 必须在 1-100 之间' }, + { status: 400 } + ); + } + + if (pageStart < 0) { + return NextResponse.json( + { error: 'pageStart 不能小于 0' }, + { status: 400 } + ); + } + + const target = `https://m.douban.com/rexxar/api/v2/subject/recent_hot/${kind}?start=${pageStart}&limit=${pageLimit}&category=${category}&type=${type}`; + + try { + // 调用豆瓣 API + const doubanData = await fetchDoubanData(target); + + // 转换数据格式 + const list: DoubanItem[] = doubanData.items.map((item) => ({ + id: item.id, + title: item.title, + poster: item.pic?.normal || item.pic?.large || '', + rate: item.rating?.value ? item.rating.value.toFixed(1) : '', + })); + + const response: DoubanResult = { + code: 200, + message: '获取成功', + list: list, + }; + + const cacheTime = await getCacheTime(); + return NextResponse.json(response, { + headers: { + 'Cache-Control': `public, max-age=${cacheTime}`, + }, + }); + } catch (error) { + return NextResponse.json( + { error: '获取豆瓣数据失败', details: (error as Error).message }, + { status: 500 } + ); + } +} diff --git a/src/app/douban/page.tsx b/src/app/douban/page.tsx index 5a8a6b5..ee819da 100644 --- a/src/app/douban/page.tsx +++ b/src/app/douban/page.tsx @@ -1,37 +1,136 @@ +/* eslint-disable no-console,react-hooks/exhaustive-deps */ + 'use client'; import { useSearchParams } from 'next/navigation'; import { Suspense } from 'react'; -import { useEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; -import { getDoubanData } from '@/lib/douban.client'; +import { getDoubanCategories } from '@/lib/douban.client'; import { DoubanItem } from '@/lib/types'; import DoubanCardSkeleton from '@/components/DoubanCardSkeleton'; +import DoubanSelector from '@/components/DoubanSelector'; import PageLayout from '@/components/PageLayout'; import VideoCard from '@/components/VideoCard'; function DoubanPageClient() { const searchParams = useSearchParams(); const [doubanData, setDoubanData] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); const [currentPage, setCurrentPage] = useState(0); const [hasMore, setHasMore] = useState(true); const [isLoadingMore, setIsLoadingMore] = useState(false); + const [selectorsReady, setSelectorsReady] = useState(false); const observerRef = useRef(null); const loadingRef = useRef(null); + const debounceTimeoutRef = useRef(null); - const type = searchParams.get('type'); - const tag = searchParams.get('tag'); + const type = searchParams.get('type') || 'movie'; + + // 选择器状态 - 完全独立,不依赖URL参数 + const [primarySelection, setPrimarySelection] = useState(() => { + return type === 'movie' ? '热门' : ''; + }); + const [secondarySelection, setSecondarySelection] = useState(() => { + if (type === 'movie') return '全部'; + if (type === 'tv') return 'tv'; + if (type === 'show') return 'show'; + return '全部'; + }); + + // 初始化时标记选择器为准备好状态 + useEffect(() => { + // 短暂延迟确保初始状态设置完成 + const timer = setTimeout(() => { + setSelectorsReady(true); + }, 50); + + return () => clearTimeout(timer); + }, []); // 只在组件挂载时执行一次 + + // type变化时立即重置selectorsReady(最高优先级) + useEffect(() => { + setSelectorsReady(false); + setLoading(true); // 立即显示loading状态 + }, [type]); + + // 当type变化时重置选择器状态 + useEffect(() => { + // 批量更新选择器状态 + if (type === 'movie') { + setPrimarySelection('热门'); + setSecondarySelection('全部'); + } else if (type === 'tv') { + setPrimarySelection(''); + setSecondarySelection('tv'); + } else if (type === 'show') { + setPrimarySelection(''); + setSecondarySelection('show'); + } else { + setPrimarySelection(''); + setSecondarySelection('全部'); + } + + // 使用短暂延迟确保状态更新完成后标记选择器准备好 + const timer = setTimeout(() => { + setSelectorsReady(true); + }, 50); + + return () => clearTimeout(timer); + }, [type]); // 生成骨架屏数据 const skeletonData = Array.from({ length: 25 }, (_, index) => index); + // 生成API请求参数的辅助函数 + const getRequestParams = useCallback( + (pageStart: number) => { + // 当type为tv或show时,kind统一为'tv',category使用type本身 + if (type === 'tv' || type === 'show') { + return { + kind: 'tv' as const, + category: type, + type: secondarySelection, + pageLimit: 25, + pageStart, + }; + } + + // 电影类型保持原逻辑 + return { + kind: type as 'tv' | 'movie', + category: primarySelection, + type: secondarySelection, + pageLimit: 25, + pageStart, + }; + }, + [type, primarySelection, secondarySelection] + ); + + // 防抖的数据加载函数 + const loadInitialData = useCallback(async () => { + try { + setLoading(true); + const data = await getDoubanCategories(getRequestParams(0)); + + if (data.code === 200) { + setDoubanData(data.list); + setHasMore(data.list.length === 25); + setLoading(false); + } else { + throw new Error(data.message || '获取数据失败'); + } + } catch (err) { + console.error(err); + } + }, [type, primarySelection, secondarySelection, getRequestParams]); + + // 只在选择器准备好后才加载数据 useEffect(() => { - if (!type || !tag) { - setError('缺少必要参数: type 或 tag'); - setLoading(false); + // 只有在选择器准备好时才开始加载 + if (!selectorsReady) { return; } @@ -39,49 +138,42 @@ function DoubanPageClient() { setDoubanData([]); setCurrentPage(0); setHasMore(true); - setError(null); setIsLoadingMore(false); - // 立即加载第一页数据 - const loadInitialData = async () => { - try { - setLoading(true); - const data = await getDoubanData({ - type: type as 'tv' | 'movie', - tag, - pageSize: 25, - pageStart: 0, - }); + // 清除之前的防抖定时器 + if (debounceTimeoutRef.current) { + clearTimeout(debounceTimeoutRef.current); + } - if (data.code === 200) { - setDoubanData(data.list); - setHasMore(data.list.length === 25); - } else { - throw new Error(data.message || '获取数据失败'); - } - } catch (err) { - setError(err instanceof Error ? err.message : '获取豆瓣数据失败'); - } finally { - setLoading(false); + // 使用防抖机制加载数据,避免连续状态更新触发多次请求 + debounceTimeoutRef.current = setTimeout(() => { + loadInitialData(); + }, 100); // 100ms 防抖延迟 + + // 清理函数 + return () => { + if (debounceTimeoutRef.current) { + clearTimeout(debounceTimeoutRef.current); } }; - - loadInitialData(); - }, [type, tag]); + }, [ + selectorsReady, + type, + primarySelection, + secondarySelection, + loadInitialData, + ]); // 单独处理 currentPage 变化(加载更多) useEffect(() => { - if (currentPage > 0 && type && tag) { + if (currentPage > 0) { const fetchMoreData = async () => { try { setIsLoadingMore(true); - const data = await getDoubanData({ - type: type as 'tv' | 'movie', - tag, - pageSize: 25, - pageStart: currentPage * 25, - }); + const data = await getDoubanCategories( + getRequestParams(currentPage * 25) + ); if (data.code === 200) { setDoubanData((prev) => [...prev, ...data.list]); @@ -90,7 +182,7 @@ function DoubanPageClient() { throw new Error(data.message || '获取数据失败'); } } catch (err) { - setError(err instanceof Error ? err.message : '获取豆瓣数据失败'); + console.error(err); } finally { setIsLoadingMore(false); } @@ -98,7 +190,7 @@ function DoubanPageClient() { fetchMoreData(); } - }, [currentPage, type, tag]); + }, [currentPage, type, primarySelection, secondarySelection]); // 设置滚动监听 useEffect(() => { @@ -131,28 +223,35 @@ function DoubanPageClient() { }; }, [hasMore, isLoadingMore, loading]); + // 处理选择器变化 + const handlePrimaryChange = useCallback( + (value: string) => { + // 立即设置loading状态 + setLoading(true); + // 批量更新状态 - React 18会自动批处理这些更新 + setPrimarySelection(value); + // 电影类型时,重置二级选择器为第一个选项 + if (type === 'movie') { + setSecondarySelection('全部'); + } + }, + [type] + ); + + const handleSecondaryChange = useCallback((value: string) => { + // 立即设置loading状态 + setLoading(true); + setSecondarySelection(value); + }, []); + const getPageTitle = () => { - // 优先使用 URL 中的 title 参数 - const titleParam = searchParams.get('title'); - if (titleParam) { - return titleParam; - } - - // 如果 title 参数不存在,根据 type 和 tag 拼接 - if (!type || !tag) return '豆瓣内容'; - - const typeText = type === 'movie' ? '电影' : '电视剧'; - const tagText = tag === 'top250' ? 'Top250' : tag; - - return `${typeText} - ${tagText}`; + // 根据 type 生成标题 + return type === 'movie' ? '电影' : type === 'tv' ? '电视剧' : '综艺'; }; const getActivePath = () => { const params = new URLSearchParams(); if (type) params.set('type', type); - if (tag) params.set('tag', tag); - const titleParam = searchParams.get('title'); - if (titleParam) params.set('title', titleParam); const queryString = params.toString(); const activePath = `/douban${queryString ? `?${queryString}` : ''}`; @@ -162,81 +261,80 @@ function DoubanPageClient() { return (
- {/* 页面标题 */} -
-

- {getPageTitle()} -

-

来自豆瓣的精选内容

+ {/* 页面标题和选择器 */} +
+ {/* 页面标题 */} +
+

+ {getPageTitle()} +

+

+ 来自豆瓣的精选内容 +

+
+ + {/* 选择器组件 */} +
+ +
{/* 内容展示区域 */}
- {error ? ( -
-
-
加载失败
-
{error}
-
+ {/* 内容网格 */} +
+ {loading || !selectorsReady + ? // 显示骨架屏 + skeletonData.map((index) => ) + : // 显示实际数据 + doubanData.map((item, index) => ( +
+ +
+ ))} +
+ + {/* 加载更多指示器 */} + {hasMore && !loading && ( +
{ + if (el && el.offsetParent !== null) { + ( + loadingRef as React.MutableRefObject + ).current = el; + } + }} + className='flex justify-center mt-12 py-8' + > + {isLoadingMore && ( +
+
+ 加载中... +
+ )}
- ) : ( - <> - {/* 内容网格 */} -
- {loading - ? // 显示骨架屏 - skeletonData.map((index) => ( - - )) - : // 显示实际数据 - doubanData.map((item, index) => ( -
- -
- ))} -
+ )} - {/* 加载更多指示器 */} - {hasMore && !loading && ( -
{ - if (el && el.offsetParent !== null) { - ( - loadingRef as React.MutableRefObject - ).current = el; - } - }} - className='flex justify-center mt-12 py-8' - > - {isLoadingMore && ( -
-
- 加载中... -
- )} -
- )} + {/* 没有更多数据提示 */} + {!hasMore && doubanData.length > 0 && ( +
已加载全部内容
+ )} - {/* 没有更多数据提示 */} - {!hasMore && doubanData.length > 0 && ( -
- 已加载全部内容 -
- )} - - {/* 空状态 */} - {!loading && doubanData.length === 0 && !error && ( -
- 暂无相关内容 -
- )} - + {/* 空状态 */} + {!loading && doubanData.length === 0 && ( +
暂无相关内容
)}
diff --git a/src/app/page.tsx b/src/app/page.tsx index 2c71ee2..4f5613c 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -13,7 +13,7 @@ import { getAllPlayRecords, subscribeToDataUpdates, } from '@/lib/db.client'; -import { getDoubanData } from '@/lib/douban.client'; +import { getDoubanRecommends } from '@/lib/douban.client'; import { DoubanItem } from '@/lib/types'; import CapsuleSwitch from '@/components/CapsuleSwitch'; @@ -65,8 +65,8 @@ function HomeClient() { // 并行获取热门电影和热门剧集 const [moviesData, tvShowsData] = await Promise.all([ - getDoubanData({ type: 'movie', tag: '热门' }), - getDoubanData({ type: 'tv', tag: '热门' }), + getDoubanRecommends({ type: 'movie', tag: '热门' }), + getDoubanRecommends({ type: 'tv', tag: '热门' }), ]); if (moviesData.code === 200) { @@ -209,7 +209,7 @@ function HomeClient() { 热门电影 查看更多 @@ -255,7 +255,7 @@ function HomeClient() { 热门剧集 查看更多 diff --git a/src/components/CapsuleSwitch.tsx b/src/components/CapsuleSwitch.tsx index a5d136a..abaea15 100644 --- a/src/components/CapsuleSwitch.tsx +++ b/src/components/CapsuleSwitch.tsx @@ -1,4 +1,6 @@ -import React from 'react'; +/* eslint-disable react-hooks/exhaustive-deps */ + +import React, { useEffect, useRef, useState } from 'react'; interface CapsuleSwitchProps { options: { label: string; value: string }[]; @@ -13,25 +15,87 @@ const CapsuleSwitch: React.FC = ({ onChange, className, }) => { + const containerRef = useRef(null); + const buttonRefs = useRef<(HTMLButtonElement | null)[]>([]); + const [indicatorStyle, setIndicatorStyle] = useState<{ + left: number; + width: number; + }>({ left: 0, width: 0 }); + + const activeIndex = options.findIndex((opt) => opt.value === active); + + // 更新指示器位置 + const updateIndicatorPosition = () => { + if ( + activeIndex >= 0 && + buttonRefs.current[activeIndex] && + containerRef.current + ) { + 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, + }); + } + } + } + }; + + // 组件挂载时立即计算初始位置 + useEffect(() => { + const timeoutId = setTimeout(updateIndicatorPosition, 0); + return () => clearTimeout(timeoutId); + }, []); + + // 监听选中项变化 + useEffect(() => { + const timeoutId = setTimeout(updateIndicatorPosition, 0); + return () => clearTimeout(timeoutId); + }, [activeIndex]); + return (
- {options.map((opt) => ( - - ))} + {/* 滑动的白色背景指示器 */} + {indicatorStyle.width > 0 && ( +
+ )} + + {options.map((opt, index) => { + const isActive = active === opt.value; + return ( + + ); + })}
); }; diff --git a/src/components/DoubanSelector.tsx b/src/components/DoubanSelector.tsx new file mode 100644 index 0000000..1f0eabb --- /dev/null +++ b/src/components/DoubanSelector.tsx @@ -0,0 +1,330 @@ +/* eslint-disable react-hooks/exhaustive-deps */ + +'use client'; + +import React, { useEffect, useRef, useState } from 'react'; + +interface SelectorOption { + label: string; + value: string; +} + +interface DoubanSelectorProps { + type: 'movie' | 'tv' | 'show'; + primarySelection?: string; + secondarySelection?: string; + onPrimaryChange: (value: string) => void; + onSecondaryChange: (value: string) => void; +} + +const DoubanSelector: React.FC = ({ + type, + primarySelection, + secondarySelection, + onPrimaryChange, + onSecondaryChange, +}) => { + // 为不同的选择器创建独立的refs和状态 + const primaryContainerRef = useRef(null); + const primaryButtonRefs = useRef<(HTMLButtonElement | null)[]>([]); + const [primaryIndicatorStyle, setPrimaryIndicatorStyle] = useState<{ + left: number; + width: number; + }>({ left: 0, width: 0 }); + + const secondaryContainerRef = useRef(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: '冷门佳片' }, + ]; + + // 电影的二级选择器选项 + const movieSecondaryOptions: SelectorOption[] = [ + { label: '全部', value: '全部' }, + { label: '华语', value: '华语' }, + { label: '欧美', value: '欧美' }, + { label: '韩国', value: '韩国' }, + { label: '日本', value: '日本' }, + ]; + + // 电视剧选择器选项 + const tvOptions: 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 showOptions: SelectorOption[] = [ + { label: '综合', value: 'show' }, + { label: '国内', value: 'show_domestic' }, + { label: '国外', value: 'show_foreign' }, + ]; + + // 更新指示器位置的通用函数 + const updateIndicatorPosition = ( + activeIndex: number, + containerRef: React.RefObject, + 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 + ); + } + + // 副选择器初始位置 + let secondaryActiveIndex = -1; + if (type === 'movie') { + secondaryActiveIndex = movieSecondaryOptions.findIndex( + (opt) => + opt.value === (secondarySelection || movieSecondaryOptions[0].value) + ); + } else if (type === 'tv') { + secondaryActiveIndex = tvOptions.findIndex( + (opt) => opt.value === (secondarySelection || tvOptions[0].value) + ); + } else if (type === 'show') { + secondaryActiveIndex = showOptions.findIndex( + (opt) => opt.value === (secondarySelection || showOptions[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; + } + }, [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 = tvOptions.findIndex( + (opt) => opt.value === secondarySelection + ); + options = tvOptions; + } else if (type === 'show') { + activeIndex = showOptions.findIndex( + (opt) => opt.value === secondarySelection + ); + options = showOptions; + } + + 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 ( +
+ {/* 滑动的白色背景指示器 */} + {indicatorStyle.width > 0 && ( +
+ )} + + {options.map((option, index) => { + const isActive = activeValue === option.value; + return ( + + ); + })} +
+ ); + }; + + return ( +
+ {/* 电影类型 - 显示两级选择器 */} + {type === 'movie' && ( +
+ {/* 一级选择器 */} +
+ + 分类 + +
+ {renderCapsuleSelector( + moviePrimaryOptions, + primarySelection || moviePrimaryOptions[0].value, + onPrimaryChange, + true + )} +
+
+ + {/* 二级选择器 */} +
+ + 地区 + +
+ {renderCapsuleSelector( + movieSecondaryOptions, + secondarySelection || movieSecondaryOptions[0].value, + onSecondaryChange, + false + )} +
+
+
+ )} + + {/* 电视剧类型 - 只显示一级选择器 */} + {type === 'tv' && ( +
+ + 类型 + +
+ {renderCapsuleSelector( + tvOptions, + secondarySelection || tvOptions[0].value, + onSecondaryChange, + false + )} +
+
+ )} + + {/* 综艺类型 - 只显示一级选择器 */} + {type === 'show' && ( +
+ + 类型 + +
+ {renderCapsuleSelector( + showOptions, + secondarySelection || showOptions[0].value, + onSecondaryChange, + false + )} +
+
+ )} +
+ ); +}; + +export default DoubanSelector; diff --git a/src/components/MobileBottomNav.tsx b/src/components/MobileBottomNav.tsx index 8d39a49..4b2b3e9 100644 --- a/src/components/MobileBottomNav.tsx +++ b/src/components/MobileBottomNav.tsx @@ -1,17 +1,6 @@ 'use client'; -import { - Clover, - Film, - Home, - MessageCircleHeart, - MountainSnow, - Search, - Star, - Swords, - Tv, - VenetianMask, -} from 'lucide-react'; +import { Clover, Film, Home, Search, Tv } from 'lucide-react'; import Link from 'next/link'; import { usePathname } from 'next/navigation'; @@ -34,36 +23,22 @@ const MobileBottomNav = ({ activePath }: MobileBottomNavProps) => { { icon: Film, label: '电影', - href: '/douban?type=movie&tag=热门&title=热门电影', + href: '/douban?type=movie', }, { icon: Tv, label: '剧集', - href: '/douban?type=tv&tag=热门&title=热门剧集', - }, - { - icon: Star, - label: '高分', - href: '/douban?type=movie&tag=top250&title=豆瓣 Top250', + href: '/douban?type=tv', }, { icon: Clover, label: '综艺', - href: '/douban?type=tv&tag=综艺&title=综艺', + href: '/douban?type=show', }, - { icon: Swords, label: '美剧', href: '/douban?type=tv&tag=美剧' }, - { - icon: MessageCircleHeart, - label: '韩剧', - href: '/douban?type=tv&tag=韩剧', - }, - { icon: MountainSnow, label: '日剧', href: '/douban?type=tv&tag=日剧' }, - { icon: VenetianMask, label: '日漫', href: '/douban?type=tv&tag=日本动画' }, ]; const isActive = (href: string) => { const typeMatch = href.match(/type=([^&]+)/)?.[1]; - const tagMatch = href.match(/tag=([^&]+)/)?.[1]; // 解码URL以进行正确的比较 const decodedActive = decodeURIComponent(currentActive); @@ -72,14 +47,13 @@ const MobileBottomNav = ({ activePath }: MobileBottomNavProps) => { return ( decodedActive === decodedItemHref || (decodedActive.startsWith('/douban') && - decodedActive.includes(`type=${typeMatch}`) && - decodedActive.includes(`tag=${tagMatch}`)) + decodedActive.includes(`type=${typeMatch}`)) ); }; return (