增加首页模块配置
This commit is contained in:
641
src/app/page.tsx
641
src/app/page.tsx
@@ -23,6 +23,14 @@ import HttpWarningDialog from '@/components/HttpWarningDialog';
|
|||||||
import BannerCarousel from '@/components/BannerCarousel';
|
import BannerCarousel from '@/components/BannerCarousel';
|
||||||
import AIChatPanel from '@/components/AIChatPanel';
|
import AIChatPanel from '@/components/AIChatPanel';
|
||||||
|
|
||||||
|
// 首页模块配置接口
|
||||||
|
interface HomeModule {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
enabled: boolean;
|
||||||
|
order: number;
|
||||||
|
}
|
||||||
|
|
||||||
function HomeClient() {
|
function HomeClient() {
|
||||||
// 移除了 activeTab 状态,收藏夹功能已移到 UserMenu
|
// 移除了 activeTab 状态,收藏夹功能已移到 UserMenu
|
||||||
const [hotMovies, setHotMovies] = useState<DoubanItem[]>([]);
|
const [hotMovies, setHotMovies] = useState<DoubanItem[]>([]);
|
||||||
@@ -36,12 +44,57 @@ function HomeClient() {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const { announcement } = useSite();
|
const { announcement } = useSite();
|
||||||
|
|
||||||
|
// 首页模块配置状态
|
||||||
|
const [homeModules, setHomeModules] = useState<HomeModule[]>([
|
||||||
|
{ id: 'hotMovies', name: '热门电影', enabled: true, order: 0 },
|
||||||
|
{ id: 'hotDuanju', name: '热播短剧', enabled: true, order: 1 },
|
||||||
|
{ id: 'bangumiCalendar', name: '新番放送', enabled: true, order: 2 },
|
||||||
|
{ id: 'hotTvShows', name: '热门剧集', enabled: true, order: 3 },
|
||||||
|
{ id: 'hotVarietyShows', name: '热门综艺', enabled: true, order: 4 },
|
||||||
|
{ id: 'upcomingContent', name: '即将上映', enabled: true, order: 5 },
|
||||||
|
]);
|
||||||
|
|
||||||
const [showAnnouncement, setShowAnnouncement] = useState(false);
|
const [showAnnouncement, setShowAnnouncement] = useState(false);
|
||||||
const [showHttpWarning, setShowHttpWarning] = useState(true);
|
const [showHttpWarning, setShowHttpWarning] = useState(true);
|
||||||
const [showAIChat, setShowAIChat] = useState(false);
|
const [showAIChat, setShowAIChat] = useState(false);
|
||||||
const [aiEnabled, setAiEnabled] = useState(false);
|
const [aiEnabled, setAiEnabled] = useState(false);
|
||||||
const [sourceSearchEnabled, setSourceSearchEnabled] = useState(true);
|
const [sourceSearchEnabled, setSourceSearchEnabled] = useState(true);
|
||||||
|
|
||||||
|
// 加载首页模块配置
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const savedHomeModules = localStorage.getItem('homeModules');
|
||||||
|
if (savedHomeModules) {
|
||||||
|
try {
|
||||||
|
setHomeModules(JSON.parse(savedHomeModules));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('解析首页模块配置失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 监听首页模块配置更新事件
|
||||||
|
useEffect(() => {
|
||||||
|
const handleHomeModulesUpdated = () => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const savedHomeModules = localStorage.getItem('homeModules');
|
||||||
|
if (savedHomeModules) {
|
||||||
|
try {
|
||||||
|
setHomeModules(JSON.parse(savedHomeModules));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('解析首页模块配置失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('homeModulesUpdated', handleHomeModulesUpdated);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('homeModulesUpdated', handleHomeModulesUpdated);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
// 检查AI功能是否启用
|
// 检查AI功能是否启用
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
@@ -185,6 +238,300 @@ function HomeClient() {
|
|||||||
localStorage.setItem('hasSeenAnnouncement', announcement); // 记录已查看弹窗
|
localStorage.setItem('hasSeenAnnouncement', announcement); // 记录已查看弹窗
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 渲染模块的函数
|
||||||
|
const renderModule = (moduleId: string) => {
|
||||||
|
switch (moduleId) {
|
||||||
|
case 'hotMovies':
|
||||||
|
return (
|
||||||
|
<section key="hotMovies" 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=movie'
|
||||||
|
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='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) => (
|
||||||
|
<div
|
||||||
|
key={movie.id}
|
||||||
|
className='min-w-[96px] w-24 sm:min-w-[180px] sm:w-44'
|
||||||
|
>
|
||||||
|
<VideoCard
|
||||||
|
id={movie.id}
|
||||||
|
poster={movie.poster}
|
||||||
|
title={movie.title}
|
||||||
|
year={movie.year}
|
||||||
|
rate={movie.rate}
|
||||||
|
type='movie'
|
||||||
|
from='douban'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</ScrollableRow>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'hotDuanju':
|
||||||
|
if (hotDuanju.length === 0) return null;
|
||||||
|
return (
|
||||||
|
<section key="hotDuanju" 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>
|
||||||
|
</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='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>
|
||||||
|
))
|
||||||
|
: hotDuanju.map((duanju) => (
|
||||||
|
<div
|
||||||
|
key={duanju.id + duanju.source}
|
||||||
|
className='min-w-[96px] w-24 sm:min-w-[180px] sm:w-44'
|
||||||
|
>
|
||||||
|
<VideoCard
|
||||||
|
id={duanju.id}
|
||||||
|
source={duanju.source}
|
||||||
|
poster={duanju.poster}
|
||||||
|
title={duanju.title}
|
||||||
|
year={duanju.year}
|
||||||
|
type='tv'
|
||||||
|
from='search'
|
||||||
|
source_name={duanju.source_name}
|
||||||
|
episodes={duanju.episodes?.length}
|
||||||
|
douban_id={duanju.douban_id}
|
||||||
|
cmsData={{
|
||||||
|
desc: duanju.desc,
|
||||||
|
episodes: duanju.episodes,
|
||||||
|
episodes_titles: duanju.episodes_titles,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</ScrollableRow>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'bangumiCalendar':
|
||||||
|
return (
|
||||||
|
<section key="bangumiCalendar" 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.filter((anime) => anime.images) || [];
|
||||||
|
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'hotTvShows':
|
||||||
|
return (
|
||||||
|
<section key="hotTvShows" 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=tv'
|
||||||
|
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='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((tvShow) => (
|
||||||
|
<div
|
||||||
|
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}
|
||||||
|
rate={tvShow.rate}
|
||||||
|
type='tv'
|
||||||
|
from='douban'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</ScrollableRow>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'hotVarietyShows':
|
||||||
|
return (
|
||||||
|
<section key="hotVarietyShows" 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=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'
|
||||||
|
>
|
||||||
|
查看更多
|
||||||
|
<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='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((varietyShow) => (
|
||||||
|
<div
|
||||||
|
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}
|
||||||
|
rate={varietyShow.rate}
|
||||||
|
type='tv'
|
||||||
|
from='douban'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</ScrollableRow>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'upcomingContent':
|
||||||
|
if (upcomingContent.length === 0) return null;
|
||||||
|
return (
|
||||||
|
<section key="upcomingContent" 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>
|
||||||
|
</div>
|
||||||
|
<ScrollableRow>
|
||||||
|
{upcomingContent.map((item) => (
|
||||||
|
<div
|
||||||
|
key={`${item.media_type}-${item.id}`}
|
||||||
|
className='min-w-[96px] w-24 sm:min-w-[180px] sm:w-44'
|
||||||
|
>
|
||||||
|
<VideoCard
|
||||||
|
title={item.title}
|
||||||
|
poster={getTMDBImageUrl(item.poster_path)}
|
||||||
|
year={item.release_date?.split('-')?.[0] || ''}
|
||||||
|
rate={
|
||||||
|
item.vote_average && item.vote_average > 0
|
||||||
|
? item.vote_average.toFixed(1)
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
type={item.media_type === 'tv' ? 'tv' : 'movie'}
|
||||||
|
from='douban'
|
||||||
|
releaseDate={item.release_date}
|
||||||
|
isUpcoming={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</ScrollableRow>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageLayout>
|
<PageLayout>
|
||||||
{/* TMDB 热门轮播图 */}
|
{/* TMDB 热门轮播图 */}
|
||||||
@@ -225,295 +572,11 @@ function HomeClient() {
|
|||||||
{/* 继续观看 */}
|
{/* 继续观看 */}
|
||||||
<ContinueWatching />
|
<ContinueWatching />
|
||||||
|
|
||||||
{/* 热门电影 */}
|
{/* 根据配置动态渲染首页模块 */}
|
||||||
<section className='mb-8'>
|
{homeModules
|
||||||
<div className='mb-4 flex items-center justify-between'>
|
.filter(module => module.enabled)
|
||||||
<h2 className='text-xl font-bold text-gray-800 dark:text-gray-200'>
|
.sort((a, b) => a.order - b.order)
|
||||||
热门电影
|
.map(module => renderModule(module.id))}
|
||||||
</h2>
|
|
||||||
<Link
|
|
||||||
href='/douban?type=movie'
|
|
||||||
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='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) => (
|
|
||||||
<div
|
|
||||||
key={movie.id}
|
|
||||||
className='min-w-[96px] w-24 sm:min-w-[180px] sm:w-44'
|
|
||||||
>
|
|
||||||
<VideoCard
|
|
||||||
id={movie.id}
|
|
||||||
poster={movie.poster}
|
|
||||||
title={movie.title}
|
|
||||||
year={movie.year}
|
|
||||||
rate={movie.rate}
|
|
||||||
type='movie'
|
|
||||||
from='douban'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</ScrollableRow>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* 热播短剧 */}
|
|
||||||
{hotDuanju.length > 0 && (
|
|
||||||
<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>
|
|
||||||
</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='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>
|
|
||||||
))
|
|
||||||
: hotDuanju.map((duanju) => (
|
|
||||||
<div
|
|
||||||
key={duanju.id + duanju.source}
|
|
||||||
className='min-w-[96px] w-24 sm:min-w-[180px] sm:w-44'
|
|
||||||
>
|
|
||||||
<VideoCard
|
|
||||||
id={duanju.id}
|
|
||||||
source={duanju.source}
|
|
||||||
poster={duanju.poster}
|
|
||||||
title={duanju.title}
|
|
||||||
year={duanju.year}
|
|
||||||
type='tv'
|
|
||||||
from='search'
|
|
||||||
source_name={duanju.source_name}
|
|
||||||
episodes={duanju.episodes?.length}
|
|
||||||
douban_id={duanju.douban_id}
|
|
||||||
cmsData={{
|
|
||||||
desc: duanju.desc,
|
|
||||||
episodes: duanju.episodes,
|
|
||||||
episodes_titles: duanju.episodes_titles,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</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.filter((anime) => anime.images) || [];
|
|
||||||
|
|
||||||
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'>
|
|
||||||
<h2 className='text-xl font-bold text-gray-800 dark:text-gray-200'>
|
|
||||||
热门剧集
|
|
||||||
</h2>
|
|
||||||
<Link
|
|
||||||
href='/douban?type=tv'
|
|
||||||
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='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((tvShow) => (
|
|
||||||
<div
|
|
||||||
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}
|
|
||||||
rate={tvShow.rate}
|
|
||||||
type='tv'
|
|
||||||
from='douban'
|
|
||||||
/>
|
|
||||||
</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=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'
|
|
||||||
>
|
|
||||||
查看更多
|
|
||||||
<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='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((varietyShow) => (
|
|
||||||
<div
|
|
||||||
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}
|
|
||||||
rate={varietyShow.rate}
|
|
||||||
type='tv'
|
|
||||||
from='douban'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</ScrollableRow>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* 即将上映/播出 (TMDB) */}
|
|
||||||
{upcomingContent.length > 0 && (
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
<ScrollableRow>
|
|
||||||
{upcomingContent.map((item) => (
|
|
||||||
<div
|
|
||||||
key={`${item.media_type}-${item.id}`}
|
|
||||||
className='min-w-[96px] w-24 sm:min-w-[180px] sm:w-44'
|
|
||||||
>
|
|
||||||
<VideoCard
|
|
||||||
title={item.title}
|
|
||||||
poster={getTMDBImageUrl(item.poster_path)}
|
|
||||||
year={item.release_date?.split('-')?.[0] || ''}
|
|
||||||
rate={
|
|
||||||
item.vote_average && item.vote_average > 0
|
|
||||||
? item.vote_average.toFixed(1)
|
|
||||||
: ''
|
|
||||||
}
|
|
||||||
type={item.media_type === 'tv' ? 'tv' : 'movie'}
|
|
||||||
from='douban'
|
|
||||||
releaseDate={item.release_date}
|
|
||||||
isUpcoming={true}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</ScrollableRow>
|
|
||||||
</section>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -31,14 +31,14 @@ export default function TopProgressBar() {
|
|||||||
const originalForward = router.forward;
|
const originalForward = router.forward;
|
||||||
|
|
||||||
// 拦截 router.push
|
// 拦截 router.push
|
||||||
router.push = function (...args: any[]) {
|
router.push = function (...args: Parameters<typeof originalPush>) {
|
||||||
isNavigatingRef.current = true;
|
isNavigatingRef.current = true;
|
||||||
NProgress.start();
|
NProgress.start();
|
||||||
return originalPush.apply(this, args);
|
return originalPush.apply(this, args);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 拦截 router.replace
|
// 拦截 router.replace
|
||||||
router.replace = function (...args: any[]) {
|
router.replace = function (...args: Parameters<typeof originalReplace>) {
|
||||||
isNavigatingRef.current = true;
|
isNavigatingRef.current = true;
|
||||||
NProgress.start();
|
NProgress.start();
|
||||||
return originalReplace.apply(this, args);
|
return originalReplace.apply(this, args);
|
||||||
|
|||||||
@@ -10,8 +10,13 @@ import {
|
|||||||
Copy,
|
Copy,
|
||||||
Download,
|
Download,
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
|
Eye,
|
||||||
|
EyeOff,
|
||||||
|
Home,
|
||||||
KeyRound,
|
KeyRound,
|
||||||
LogOut,
|
LogOut,
|
||||||
|
MoveDown,
|
||||||
|
MoveUp,
|
||||||
Rss,
|
Rss,
|
||||||
Settings,
|
Settings,
|
||||||
Shield,
|
Shield,
|
||||||
@@ -107,6 +112,26 @@ export const UserMenu: React.FC = () => {
|
|||||||
const [isUsageSectionOpen, setIsUsageSectionOpen] = useState(false);
|
const [isUsageSectionOpen, setIsUsageSectionOpen] = useState(false);
|
||||||
const [isBufferSectionOpen, setIsBufferSectionOpen] = useState(false);
|
const [isBufferSectionOpen, setIsBufferSectionOpen] = useState(false);
|
||||||
const [isDanmakuSectionOpen, setIsDanmakuSectionOpen] = useState(false);
|
const [isDanmakuSectionOpen, setIsDanmakuSectionOpen] = useState(false);
|
||||||
|
const [isHomepageSectionOpen, setIsHomepageSectionOpen] = useState(false);
|
||||||
|
|
||||||
|
// 首页模块配置
|
||||||
|
interface HomeModule {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
enabled: boolean;
|
||||||
|
order: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultHomeModules: HomeModule[] = [
|
||||||
|
{ id: 'hotMovies', name: '热门电影', enabled: true, order: 0 },
|
||||||
|
{ id: 'hotDuanju', name: '热播短剧', enabled: true, order: 1 },
|
||||||
|
{ id: 'bangumiCalendar', name: '新番放送', enabled: true, order: 2 },
|
||||||
|
{ id: 'hotTvShows', name: '热门剧集', enabled: true, order: 3 },
|
||||||
|
{ id: 'hotVarietyShows', name: '热门综艺', enabled: true, order: 4 },
|
||||||
|
{ id: 'upcomingContent', name: '即将上映', enabled: true, order: 5 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const [homeModules, setHomeModules] = useState<HomeModule[]>(defaultHomeModules);
|
||||||
|
|
||||||
// 豆瓣数据源选项
|
// 豆瓣数据源选项
|
||||||
const doubanDataSourceOptions = [
|
const doubanDataSourceOptions = [
|
||||||
@@ -343,6 +368,16 @@ export const UserMenu: React.FC = () => {
|
|||||||
if (savedNextEpisodePreCache !== null) {
|
if (savedNextEpisodePreCache !== null) {
|
||||||
setNextEpisodePreCache(savedNextEpisodePreCache === 'true');
|
setNextEpisodePreCache(savedNextEpisodePreCache === 'true');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 加载首页模块配置
|
||||||
|
const savedHomeModules = localStorage.getItem('homeModules');
|
||||||
|
if (savedHomeModules !== null) {
|
||||||
|
try {
|
||||||
|
setHomeModules(JSON.parse(savedHomeModules));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('解析首页模块配置失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -606,6 +641,53 @@ export const UserMenu: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 首页模块配置处理函数
|
||||||
|
const handleHomeModuleToggle = (id: string, enabled: boolean) => {
|
||||||
|
const updatedModules = homeModules.map(module =>
|
||||||
|
module.id === id ? { ...module, enabled } : module
|
||||||
|
);
|
||||||
|
setHomeModules(updatedModules);
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
localStorage.setItem('homeModules', JSON.stringify(updatedModules));
|
||||||
|
// 触发自定义事件通知首页刷新
|
||||||
|
window.dispatchEvent(new CustomEvent('homeModulesUpdated'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleHomeModuleMoveUp = (index: number) => {
|
||||||
|
if (index === 0) return;
|
||||||
|
const updatedModules = [...homeModules];
|
||||||
|
const temp = updatedModules[index];
|
||||||
|
updatedModules[index] = updatedModules[index - 1];
|
||||||
|
updatedModules[index - 1] = temp;
|
||||||
|
// 更新order
|
||||||
|
updatedModules.forEach((module, idx) => {
|
||||||
|
module.order = idx;
|
||||||
|
});
|
||||||
|
setHomeModules(updatedModules);
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
localStorage.setItem('homeModules', JSON.stringify(updatedModules));
|
||||||
|
window.dispatchEvent(new CustomEvent('homeModulesUpdated'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleHomeModuleMoveDown = (index: number) => {
|
||||||
|
if (index === homeModules.length - 1) return;
|
||||||
|
const updatedModules = [...homeModules];
|
||||||
|
const temp = updatedModules[index];
|
||||||
|
updatedModules[index] = updatedModules[index + 1];
|
||||||
|
updatedModules[index + 1] = temp;
|
||||||
|
// 更新order
|
||||||
|
updatedModules.forEach((module, idx) => {
|
||||||
|
module.order = idx;
|
||||||
|
});
|
||||||
|
setHomeModules(updatedModules);
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
localStorage.setItem('homeModules', JSON.stringify(updatedModules));
|
||||||
|
window.dispatchEvent(new CustomEvent('homeModulesUpdated'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 获取感谢信息
|
// 获取感谢信息
|
||||||
const getThanksInfo = (dataSource: string) => {
|
const getThanksInfo = (dataSource: string) => {
|
||||||
switch (dataSource) {
|
switch (dataSource) {
|
||||||
@@ -649,6 +731,7 @@ export const UserMenu: React.FC = () => {
|
|||||||
setDoubanImageProxyUrl(defaultDoubanImageProxyUrl);
|
setDoubanImageProxyUrl(defaultDoubanImageProxyUrl);
|
||||||
setBufferStrategy('medium');
|
setBufferStrategy('medium');
|
||||||
setNextEpisodePreCache(true);
|
setNextEpisodePreCache(true);
|
||||||
|
setHomeModules(defaultHomeModules);
|
||||||
|
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
localStorage.setItem('defaultAggregateSearch', JSON.stringify(true));
|
localStorage.setItem('defaultAggregateSearch', JSON.stringify(true));
|
||||||
@@ -663,6 +746,8 @@ export const UserMenu: React.FC = () => {
|
|||||||
localStorage.setItem('doubanImageProxyUrl', defaultDoubanImageProxyUrl);
|
localStorage.setItem('doubanImageProxyUrl', defaultDoubanImageProxyUrl);
|
||||||
localStorage.setItem('bufferStrategy', 'medium');
|
localStorage.setItem('bufferStrategy', 'medium');
|
||||||
localStorage.setItem('nextEpisodePreCache', 'true');
|
localStorage.setItem('nextEpisodePreCache', 'true');
|
||||||
|
localStorage.setItem('homeModules', JSON.stringify(defaultHomeModules));
|
||||||
|
window.dispatchEvent(new CustomEvent('homeModulesUpdated'));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1513,6 +1598,105 @@ export const UserMenu: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 首页设置 */}
|
||||||
|
<div className='border border-gray-200 dark:border-gray-700 rounded-lg overflow-visible'>
|
||||||
|
<button
|
||||||
|
onClick={() => setIsHomepageSectionOpen(!isHomepageSectionOpen)}
|
||||||
|
className='w-full px-3 py-2.5 md:px-4 md:py-3 bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-750 transition-colors flex items-center justify-between'
|
||||||
|
>
|
||||||
|
<h3 className='text-base font-semibold text-gray-800 dark:text-gray-200'>
|
||||||
|
首页设置
|
||||||
|
</h3>
|
||||||
|
{isHomepageSectionOpen ? (
|
||||||
|
<ChevronUp className='w-5 h-5 text-gray-600 dark:text-gray-400' />
|
||||||
|
) : (
|
||||||
|
<ChevronDown className='w-5 h-5 text-gray-600 dark:text-gray-400' />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
{isHomepageSectionOpen && (
|
||||||
|
<div className='p-3 md:p-4 space-y-4 md:space-y-6'>
|
||||||
|
<div>
|
||||||
|
<p className='text-xs text-gray-500 dark:text-gray-400 mb-3'>
|
||||||
|
配置首页模块的显示顺序和可见性
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 模块列表 */}
|
||||||
|
<div className='space-y-2'>
|
||||||
|
{homeModules.map((module, index) => (
|
||||||
|
<div
|
||||||
|
key={module.id}
|
||||||
|
className='flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700'
|
||||||
|
>
|
||||||
|
{/* 左侧:显示/隐藏开关 */}
|
||||||
|
<button
|
||||||
|
onClick={() => handleHomeModuleToggle(module.id, !module.enabled)}
|
||||||
|
className='flex-shrink-0'
|
||||||
|
title={module.enabled ? '点击隐藏' : '点击显示'}
|
||||||
|
>
|
||||||
|
{module.enabled ? (
|
||||||
|
<Eye className='w-5 h-5 text-green-600 dark:text-green-400' />
|
||||||
|
) : (
|
||||||
|
<EyeOff className='w-5 h-5 text-gray-400 dark:text-gray-500' />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* 中间:模块名称 */}
|
||||||
|
<div className='flex-1'>
|
||||||
|
<span className={`text-sm font-medium ${
|
||||||
|
module.enabled
|
||||||
|
? 'text-gray-900 dark:text-gray-100'
|
||||||
|
: 'text-gray-400 dark:text-gray-500'
|
||||||
|
}`}>
|
||||||
|
{module.name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 右侧:上下移动按钮 */}
|
||||||
|
<div className='flex gap-1'>
|
||||||
|
<button
|
||||||
|
onClick={() => handleHomeModuleMoveUp(index)}
|
||||||
|
disabled={index === 0}
|
||||||
|
className='p-1.5 rounded hover:bg-gray-200 dark:hover:bg-gray-700 disabled:opacity-30 disabled:cursor-not-allowed transition-colors'
|
||||||
|
title='上移'
|
||||||
|
>
|
||||||
|
<MoveUp className='w-4 h-4 text-gray-600 dark:text-gray-400' />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleHomeModuleMoveDown(index)}
|
||||||
|
disabled={index === homeModules.length - 1}
|
||||||
|
className='p-1.5 rounded hover:bg-gray-200 dark:hover:bg-gray-700 disabled:opacity-30 disabled:cursor-not-allowed transition-colors'
|
||||||
|
title='下移'
|
||||||
|
>
|
||||||
|
<MoveDown className='w-4 h-4 text-gray-600 dark:text-gray-400' />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 恢复默认按钮 */}
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setHomeModules(defaultHomeModules);
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
localStorage.setItem('homeModules', JSON.stringify(defaultHomeModules));
|
||||||
|
window.dispatchEvent(new CustomEvent('homeModulesUpdated'));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className='w-full px-4 py-2 bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-300 text-sm font-medium rounded-lg transition-colors'
|
||||||
|
>
|
||||||
|
恢复默认配置
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* 提示信息 */}
|
||||||
|
<div className='text-xs text-gray-500 dark:text-gray-400 p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg'>
|
||||||
|
<p>💡 提示:点击眼睛图标可显示/隐藏模块,使用箭头按钮调整模块顺序</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 底部说明 */}
|
{/* 底部说明 */}
|
||||||
|
|||||||
Reference in New Issue
Block a user