修复首页/api/favorites接口重复加载

This commit is contained in:
mtvpls
2025-12-04 23:14:28 +08:00
parent 179fc73c8f
commit d34c314636
2 changed files with 256 additions and 309 deletions

View File

@@ -4,7 +4,7 @@
import { ChevronRight } from 'lucide-react';
import Link from 'next/link';
import { Suspense, useEffect, useState } from 'react';
import { Suspense, useCallback, useEffect, useRef, useState } from 'react';
import {
BangumiCalendarData,
@@ -66,6 +66,7 @@ function HomeClient() {
};
const [favoriteItems, setFavoriteItems] = useState<FavoriteItem[]>([]);
const favoritesFetchedRef = useRef(false);
useEffect(() => {
const fetchRecommendData = async () => {
@@ -109,7 +110,7 @@ function HomeClient() {
}, []);
// 处理收藏数据更新的函数
const updateFavoriteItems = async (allFavorites: Record<string, any>) => {
const updateFavoriteItems = useCallback(async (allFavorites: Record<string, any>) => {
const allPlayRecords = await getAllPlayRecords();
// 根据保存时间排序(从近到远)
@@ -138,11 +139,19 @@ function HomeClient() {
} as FavoriteItem;
});
setFavoriteItems(sorted);
};
}, []);
// 当切换到收藏夹时加载收藏数据
// 当切换到收藏夹时加载收藏数据(使用 ref 防止重复加载)
useEffect(() => {
if (activeTab !== 'favorites') return;
if (activeTab !== 'favorites') {
favoritesFetchedRef.current = false;
return;
}
// 已经加载过就不再加载
if (favoritesFetchedRef.current) return;
favoritesFetchedRef.current = true;
const loadFavorites = async () => {
const allFavorites = await getAllFavorites();
@@ -150,8 +159,12 @@ function HomeClient() {
};
loadFavorites();
}, [activeTab, updateFavoriteItems]);
// 监听收藏更新事件(独立的 useEffect
useEffect(() => {
if (activeTab !== 'favorites') return;
// 监听收藏更新事件
const unsubscribe = subscribeToDataUpdates(
'favoritesUpdated',
(newFavorites: Record<string, any>) => {
@@ -160,7 +173,7 @@ function HomeClient() {
);
return unsubscribe;
}, [activeTab]);
}, [activeTab, updateFavoriteItems]);
const handleCloseAnnouncement = (announcement: string) => {
setShowAnnouncement(false);
@@ -248,26 +261,22 @@ function HomeClient() {
key={index}
className='min-w-[96px] w-24 sm:min-w-[180px] sm:w-44'
>
<div className='relative aspect-[2/3] w-full overflow-hidden rounded-lg bg-gray-200 animate-pulse dark:bg-gray-800'>
<div className='absolute inset-0 bg-gray-300 dark:bg-gray-700'></div>
</div>
<div className='mt-2 h-4 bg-gray-200 rounded animate-pulse dark:bg-gray-800'></div>
<div className='aspect-[2/3] bg-gray-200 dark:bg-gray-700 rounded-lg animate-pulse mb-2' />
<div className='h-4 bg-gray-200 dark:bg-gray-700 rounded animate-pulse w-3/4' />
</div>
))
: // 显示真实数据
hotMovies.map((movie, index) => (
: hotMovies.map((movie) => (
<div
key={index}
key={movie.id}
className='min-w-[96px] w-24 sm:min-w-[180px] sm:w-44'
>
<VideoCard
from='douban'
title={movie.title}
id={movie.id}
poster={movie.poster}
douban_id={Number(movie.id)}
rate={movie.rate}
title={movie.title}
year={movie.year}
type='movie'
from='douban'
/>
</div>
))}
@@ -290,113 +299,33 @@ function HomeClient() {
</div>
<ScrollableRow>
{loading
? // 加载状态显示灰色占位数据
Array.from({ length: 8 }).map((_, index) => (
? Array.from({ length: 8 }).map((_, index) => (
<div
key={index}
className='min-w-[96px] w-24 sm:min-w-[180px] sm:w-44'
>
<div className='relative aspect-[2/3] w-full overflow-hidden rounded-lg bg-gray-200 animate-pulse dark:bg-gray-800'>
<div className='absolute inset-0 bg-gray-300 dark:bg-gray-700'></div>
</div>
<div className='mt-2 h-4 bg-gray-200 rounded animate-pulse dark:bg-gray-800'></div>
<div className='aspect-[2/3] bg-gray-200 dark:bg-gray-700 rounded-lg animate-pulse mb-2' />
<div className='h-4 bg-gray-200 dark:bg-gray-700 rounded animate-pulse w-3/4' />
</div>
))
: // 显示真实数据
hotTvShows.map((show, index) => (
: hotTvShows.map((tvShow) => (
<div
key={index}
key={tvShow.id}
className='min-w-[96px] w-24 sm:min-w-[180px] sm:w-44'
>
<VideoCard
id={tvShow.id}
poster={tvShow.poster}
title={tvShow.title}
year={tvShow.year}
type='tv'
from='douban'
title={show.title}
poster={show.poster}
douban_id={Number(show.id)}
rate={show.rate}
year={show.year}
/>
</div>
))}
</ScrollableRow>
</section>
{/* 每日新番放送 */}
<section className='mb-8'>
<div className='mb-4 flex items-center justify-between'>
<h2 className='text-xl font-bold text-gray-800 dark:text-gray-200'>
</h2>
<Link
href='/douban?type=anime'
className='flex items-center text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200'
>
<ChevronRight className='w-4 h-4 ml-1' />
</Link>
</div>
<ScrollableRow>
{loading
? // 加载状态显示灰色占位数据
Array.from({ length: 8 }).map((_, index) => (
<div
key={index}
className='min-w-[96px] w-24 sm:min-w-[180px] sm:w-44'
>
<div className='relative aspect-[2/3] w-full overflow-hidden rounded-lg bg-gray-200 animate-pulse dark:bg-gray-800'>
<div className='absolute inset-0 bg-gray-300 dark:bg-gray-700'></div>
</div>
<div className='mt-2 h-4 bg-gray-200 rounded animate-pulse dark:bg-gray-800'></div>
</div>
))
: // 展示当前日期的番剧
(() => {
// 获取当前日期对应的星期
const today = new Date();
const weekdays = [
'Sun',
'Mon',
'Tue',
'Wed',
'Thu',
'Fri',
'Sat',
];
const currentWeekday = weekdays[today.getDay()];
// 找到当前星期对应的番剧数据
const todayAnimes =
bangumiCalendarData.find(
(item) => item.weekday.en === currentWeekday
)?.items || [];
return todayAnimes.map((anime, index) => (
<div
key={`${anime.id}-${index}`}
className='min-w-[96px] w-24 sm:min-w-[180px] sm:w-44'
>
<VideoCard
from='douban'
title={anime.name_cn || anime.name}
poster={
anime.images?.large ||
anime.images?.common ||
anime.images?.medium ||
anime.images?.small ||
anime.images?.grid ||
''
}
douban_id={anime.id}
rate={anime.rating?.score?.toFixed(1) || ''}
year={anime.air_date?.split('-')?.[0] || ''}
isBangumi={true}
/>
</div>
));
})()}
</ScrollableRow>
</section>
{/* 热门综艺 */}
<section className='mb-8'>
<div className='mb-4 flex items-center justify-between'>
@@ -404,7 +333,7 @@ function HomeClient() {
</h2>
<Link
href='/douban?type=show'
href='/douban?type=tv&category=show'
className='flex items-center text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200'
>
@@ -413,100 +342,87 @@ function HomeClient() {
</div>
<ScrollableRow>
{loading
? // 加载状态显示灰色占位数据
Array.from({ length: 8 }).map((_, index) => (
? Array.from({ length: 8 }).map((_, index) => (
<div
key={index}
className='min-w-[96px] w-24 sm:min-w-[180px] sm:w-44'
>
<div className='relative aspect-[2/3] w-full overflow-hidden rounded-lg bg-gray-200 animate-pulse dark:bg-gray-800'>
<div className='absolute inset-0 bg-gray-300 dark:bg-gray-700'></div>
</div>
<div className='mt-2 h-4 bg-gray-200 rounded animate-pulse dark:bg-gray-800'></div>
<div className='aspect-[2/3] bg-gray-200 dark:bg-gray-700 rounded-lg animate-pulse mb-2' />
<div className='h-4 bg-gray-200 dark:bg-gray-700 rounded animate-pulse w-3/4' />
</div>
))
: // 显示真实数据
hotVarietyShows.map((show, index) => (
: hotVarietyShows.map((varietyShow) => (
<div
key={index}
key={varietyShow.id}
className='min-w-[96px] w-24 sm:min-w-[180px] sm:w-44'
>
<VideoCard
id={varietyShow.id}
poster={varietyShow.poster}
title={varietyShow.title}
year={varietyShow.year}
type='tv'
from='douban'
title={show.title}
poster={show.poster}
douban_id={Number(show.id)}
rate={show.rate}
year={show.year}
/>
</div>
))}
</ScrollableRow>
</section>
{/* 番剧时间表 */}
{bangumiCalendarData.length > 0 && (
<section className='mb-8'>
<div className='mb-4'>
<h2 className='text-xl font-bold text-gray-800 dark:text-gray-200'>
</h2>
</div>
{bangumiCalendarData.map((day) => (
<div key={day.weekday.en} className='mb-6'>
<h3 className='text-sm text-gray-600 dark:text-gray-400 mb-3'>
{day.weekday.en}
</h3>
<ScrollableRow>
{day.items.map((item) => (
<div
key={item.id}
className='min-w-[96px] w-24 sm:min-w-[180px] sm:w-44'
>
<VideoCard
id={String(item.id)}
poster={item.images.large}
title={item.name}
year={item.air_date || ''}
type='tv'
from='douban'
/>
</div>
))}
</ScrollableRow>
</div>
))}
</section>
)}
</>
)}
</div>
</div>
{announcement && showAnnouncement && (
<div
className={`fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm dark:bg-black/70 p-4 transition-opacity duration-300 ${showAnnouncement ? '' : 'opacity-0 pointer-events-none'
}`}
onTouchStart={(e) => {
// 如果点击的是背景区域,阻止触摸事件冒泡,防止背景滚动
if (e.target === e.currentTarget) {
e.preventDefault();
}
}}
onTouchMove={(e) => {
// 如果触摸的是背景区域,阻止触摸移动,防止背景滚动
if (e.target === e.currentTarget) {
e.preventDefault();
e.stopPropagation();
}
}}
onTouchEnd={(e) => {
// 如果触摸的是背景区域,阻止触摸结束事件,防止背景滚动
if (e.target === e.currentTarget) {
e.preventDefault();
}
}}
style={{
touchAction: 'none', // 禁用所有触摸操作
}}
>
<div
className='w-full max-w-md rounded-xl bg-white p-6 shadow-xl dark:bg-gray-900 transform transition-all duration-300 hover:shadow-2xl'
onTouchMove={(e) => {
// 允许公告内容区域正常滚动,阻止事件冒泡到外层
e.stopPropagation();
}}
style={{
touchAction: 'auto', // 允许内容区域的正常触摸操作
}}
>
<div className='flex justify-between items-start mb-4'>
<h3 className='text-2xl font-bold tracking-tight text-gray-800 dark:text-white border-b border-green-500 pb-1'>
</h3>
<button
onClick={() => handleCloseAnnouncement(announcement)}
className='text-gray-400 hover:text-gray-500 dark:text-gray-500 dark:hover:text-white transition-colors'
aria-label='关闭'
></button>
</div>
<div className='mb-6'>
<div className='relative overflow-hidden rounded-lg mb-4 bg-green-50 dark:bg-green-900/20'>
<div className='absolute inset-y-0 left-0 w-1.5 bg-green-500 dark:bg-green-400'></div>
<p className='ml-4 text-gray-600 dark:text-gray-300 leading-relaxed'>
{announcement}
</p>
</div>
{/* 公告弹窗 */}
{showAnnouncement && (
<div className='fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4'>
<div className='bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-md w-full p-6'>
<h3 className='text-lg font-semibold text-gray-900 dark:text-gray-100 mb-3'>
</h3>
<div className='text-gray-700 dark:text-gray-300 mb-4 whitespace-pre-wrap'>
{announcement}
</div>
<button
onClick={() => handleCloseAnnouncement(announcement)}
className='w-full rounded-lg bg-gradient-to-r from-green-600 to-green-700 px-4 py-3 text-white font-medium shadow-md hover:shadow-lg hover:from-green-700 hover:to-green-800 dark:from-green-600 dark:to-green-700 dark:hover:from-green-700 dark:hover:to-green-800 transition-all duration-300 transform hover:-translate-y-0.5'
onClick={() => handleCloseAnnouncement(announcement || '')}
className='w-full px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors'
>
</button>
</div>
</div>