diff --git a/config.json b/config.json index 0984250..1f3d4fb 100644 --- a/config.json +++ b/config.json @@ -1,6 +1,11 @@ { "cache_time": 7200, "api_site": { + "dyttzy": { + "api": "http://caiji.dyttzyapi.com/api.php/provide/vod", + "name": "电影天堂资源", + "detail": "http://caiji.dyttzyapi.com" + }, "heimuer": { "api": "https://json.heimuer.xyz/api.php/provide/vod", "name": "黑木耳", @@ -10,11 +15,6 @@ "api": "https://cj.rycjapi.com/api.php/provide/vod", "name": "如意资源" }, - "dyttzy": { - "api": "http://caiji.dyttzyapi.com/api.php/provide/vod", - "name": "电影天堂资源", - "detail": "http://caiji.dyttzyapi.com" - }, "bfzy": { "api": "https://bfzyapi.com/api.php/provide/vod", "name": "暴风资源" diff --git a/src/app/douban/page.tsx b/src/app/douban/page.tsx index e4a9c86..d434cef 100644 --- a/src/app/douban/page.tsx +++ b/src/app/douban/page.tsx @@ -199,11 +199,10 @@ function DoubanPageClient() { doubanData.map((item, index) => (
diff --git a/src/app/page.tsx b/src/app/page.tsx index 390d16d..0ab4865 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -156,7 +156,7 @@ function HomeClient() {
{favoriteItems.map((item) => (
- +
))} {favoriteItems.length === 0 && ( @@ -207,11 +207,10 @@ function HomeClient() { className='min-w-[96px] w-24 sm:min-w-[180px] sm:w-44' >
@@ -254,11 +253,10 @@ function HomeClient() { className='min-w-[96px] w-24 sm:min-w-[180px] sm:w-44' > diff --git a/src/app/search/page.tsx b/src/app/search/page.tsx index 0fb84fe..90e2f71 100644 --- a/src/app/search/page.tsx +++ b/src/app/search/page.tsx @@ -201,15 +201,7 @@ function SearchPageClient() { ? aggregatedResults.map(([mapKey, group]) => { return (
- +
); }) @@ -225,7 +217,7 @@ function SearchPageClient() { episodes={item.episodes.length} source={item.source} source_name={item.source_name} - douban_id={item.douban_id} + douban_id={item.douban_id?.toString()} from='search' /> diff --git a/src/components/ContinueWatching.tsx b/src/components/ContinueWatching.tsx index 3e6b9d8..e367d89 100644 --- a/src/components/ContinueWatching.tsx +++ b/src/components/ContinueWatching.tsx @@ -120,6 +120,7 @@ export default function ContinueWatching({ className }: ContinueWatchingProps) { progress={getProgress(record)} episodes={record.total_episodes} currentEpisode={record.index} + from='playrecord' onDelete={() => setPlayRecords((prev) => prev.filter((r) => r.key !== record.key) diff --git a/src/components/VideoCard.tsx b/src/components/VideoCard.tsx index 99b5096..d81f523 100644 --- a/src/components/VideoCard.tsx +++ b/src/components/VideoCard.tsx @@ -4,37 +4,27 @@ import { useRouter } from 'next/navigation'; import React, { useEffect, useMemo, useState } from 'react'; import { deletePlayRecord, isFavorited, toggleFavorite } from '@/lib/db.client'; +import { SearchResult } from '@/lib/types'; import { ImagePlaceholder } from '@/components/ImagePlaceholder'; -// 聚合卡需要的基本字段,与搜索接口保持一致 -interface SearchResult { - id: string; - title: string; - poster: string; - source: string; - source_name: string; - douban_id?: number; - episodes: string[]; -} - interface VideoCardProps { - id: string; - source: string; - title: string; - poster: string; + id?: string; + source?: string; + title?: string; + poster?: string; episodes?: number; - source_name: string; + source_name?: string; progress?: number; year?: string; - from?: string; + from: 'playrecord' | 'favorite' | 'search' | 'douban'; currentEpisode?: number; - douban_id?: number; + douban_id?: string; onDelete?: () => void; - // 可选属性,根据存在与否决定卡片行为 - rate?: string; // 如果存在,按demo卡片处理 - items?: SearchResult[]; // 如果存在,按aggregate卡片处理 + // 可选属性 + rate?: string; // douban 卡片可能有评分 + items?: SearchResult[]; // search 卡片可能有聚合数据 } function CheckCircleCustom() { @@ -111,10 +101,8 @@ export default function VideoCard({ const [isDeleting, setIsDeleting] = useState(false); const router = useRouter(); - // 判断卡片类型 - const isDemo = !!rate; - const isAggregate = !!items && items.length > 0; - const isStandard = !isDemo && !isAggregate; + // 判断是否为聚合卡片(只有 search 类型可能有聚合) + const isAggregate = from === 'search' && !!items && items.length > 0; // 处理聚合卡片的逻辑 const aggregateData = useMemo(() => { @@ -159,33 +147,54 @@ export default function VideoCard({ } }); + // 统计出现次数最多的年份 + const yearCountMap = new Map(); + items.forEach((item) => { + if (item.year && item.year.trim()) { + const yearStr = item.year.trim(); + yearCountMap.set(yearStr, (yearCountMap.get(yearStr) || 0) + 1); + } + }); + + let mostFrequentYear: string | undefined; + let maxYearCount = 0; + yearCountMap.forEach((cnt, yr) => { + if (cnt > maxYearCount) { + maxYearCount = cnt; + mostFrequentYear = yr; + } + }); + return { first, mostFrequentDoubanId, mostFrequentEpisodes, + mostFrequentYear, }; }, [isAggregate, items]); // 根据卡片类型决定实际使用的数据 const actualTitle = - isAggregate && aggregateData ? aggregateData.first.title : title; + isAggregate && aggregateData ? aggregateData.first.title : title || ''; const actualPoster = - isAggregate && aggregateData ? aggregateData.first.poster : poster; + isAggregate && aggregateData ? aggregateData.first.poster : poster || ''; const actualSource = isAggregate && aggregateData ? aggregateData.first.source : source; const actualId = isAggregate && aggregateData ? aggregateData.first.id : id; const actualDoubanId = isAggregate && aggregateData - ? aggregateData.mostFrequentDoubanId + ? aggregateData.mostFrequentDoubanId?.toString() : douban_id; const actualEpisodes = isAggregate && aggregateData ? aggregateData.mostFrequentEpisodes : episodes; + const actualYear = + isAggregate && aggregateData ? aggregateData.mostFrequentYear : year; - // 检查初始收藏状态(仅标准卡片) + // 检查初始收藏状态(需要 source 和 id 的卡片类型) useEffect(() => { - if (!isStandard) return; + if (from === 'douban' || !actualSource || !actualId) return; (async () => { try { @@ -195,22 +204,22 @@ export default function VideoCard({ throw new Error('检查收藏状态失败'); } })(); - }, [isStandard, actualSource, actualId]); + }, [from, actualSource, actualId]); - // 切换收藏状态(仅标准卡片) + // 切换收藏状态 const handleToggleFavorite = async ( e: React.MouseEvent ) => { e.preventDefault(); e.stopPropagation(); - if (!isStandard) return; + if (from === 'douban' || !actualSource || !actualId) return; try { const newState = await toggleFavorite(actualSource, actualId, { title: actualTitle, - source_name, - year: year || '', + source_name: source_name || '', + year: actualYear || '', cover: actualPoster, total_episodes: actualEpisodes ?? 1, save_time: Date.now(), @@ -225,14 +234,14 @@ export default function VideoCard({ } }; - // 删除对应播放记录(仅标准卡片) + // 删除对应播放记录 const handleDeleteRecord = async ( e: React.MouseEvent ) => { e.preventDefault(); e.stopPropagation(); - if (!isStandard) return; + if (from !== 'playrecord' || !actualSource || !actualId) return; try { await deletePlayRecord(actualSource, actualId); @@ -244,13 +253,15 @@ export default function VideoCard({ // 点击处理逻辑 const handleClick = () => { - if (isDemo) { + if (from === 'douban') { + // douban 卡片使用 title 搜索 router.push(`/play?title=${encodeURIComponent(actualTitle.trim())}`); - } else { + } else if (actualSource && actualId) { + // 其他类型使用 source 和 id router.push( `/play?source=${actualSource}&id=${actualId}&title=${encodeURIComponent( actualTitle.trim() - )}${year ? `&year=${year}` : ''}` + )}${actualYear ? `&year=${actualYear}` : ''}` ); } }; @@ -262,20 +273,101 @@ export default function VideoCard({ handleClick(); }; - const hideCheckCircle = - from === 'favorites' || from === 'search' || !isStandard; - const alwaysShowHeart = from !== 'favorites' && isStandard; - const showHoverLayer = isStandard - ? alwaysShowHeart - ? 'opacity-50 group-hover:opacity-100' - : 'opacity-0 group-hover:opacity-100' - : 'opacity-0 group-hover:opacity-100'; + // 根据 from 类型决定显示逻辑 + const getDisplayConfig = () => { + switch (from) { + case 'playrecord': + return { + showSourceName: true, + showProgress: true, + showPlayButton: true, + playButtonAlwaysVisible: true, + playButtonOpacity: 'opacity-50 group-hover:opacity-100', + showHeart: true, + heartAlwaysVisible: true, + heartOpacity: 'opacity-100', + showCheckCircle: true, + checkCircleAlwaysVisible: true, + showDoubanLink: false, + showRating: false, + hoverLayerOpacity: 'opacity-50 group-hover:opacity-100', + }; + case 'favorite': + return { + showSourceName: true, + showProgress: false, + showPlayButton: true, + playButtonAlwaysVisible: false, + playButtonOpacity: 'opacity-70 group-hover:opacity-100', + showHeart: true, + heartAlwaysVisible: false, + heartOpacity: 'opacity-70 group-hover:opacity-100', + showCheckCircle: false, + checkCircleAlwaysVisible: false, + showDoubanLink: false, + showRating: false, + hoverLayerOpacity: 'opacity-0 group-hover:opacity-100', + }; + case 'search': + return { + showSourceName: true, + showProgress: false, + showPlayButton: true, + playButtonAlwaysVisible: true, + playButtonOpacity: 'opacity-50 group-hover:opacity-100', + showHeart: !isAggregate, // 聚合卡片不显示收藏 + heartAlwaysVisible: !isAggregate, + heartOpacity: 'opacity-50 group-hover:opacity-100', + showCheckCircle: false, + checkCircleAlwaysVisible: false, + showDoubanLink: !!actualDoubanId, + showRating: false, + hoverLayerOpacity: isAggregate + ? 'opacity-50 group-hover:opacity-100' + : 'opacity-50 group-hover:opacity-100', + }; + case 'douban': + return { + showSourceName: false, + showProgress: false, + showPlayButton: true, + playButtonAlwaysVisible: false, + playButtonOpacity: 'opacity-70 group-hover:opacity-100', + showHeart: false, + heartAlwaysVisible: false, + heartOpacity: '', + showCheckCircle: false, + checkCircleAlwaysVisible: false, + showDoubanLink: true, + showRating: !!rate, + hoverLayerOpacity: 'opacity-0 group-hover:opacity-100', + }; + default: + return { + showSourceName: true, + showProgress: false, + showPlayButton: true, + playButtonAlwaysVisible: false, + playButtonOpacity: 'opacity-70 group-hover:opacity-100', + showHeart: true, + heartAlwaysVisible: false, + heartOpacity: 'opacity-70 group-hover:opacity-100', + showCheckCircle: false, + checkCircleAlwaysVisible: false, + showDoubanLink: false, + showRating: false, + hoverLayerOpacity: 'opacity-0 group-hover:opacity-100', + }; + } + }; + + const config = getDisplayConfig(); return (
{/* 海报图片容器 */} @@ -297,29 +389,34 @@ export default function VideoCard({ referrerPolicy='no-referrer' priority={false} /> + {/* Hover 效果层 */}
{/* 播放按钮 */} -
-
setPlayHover(true)} - onMouseLeave={() => setPlayHover(false)} - > - + {config.showPlayButton && ( +
+
setPlayHover(true)} + onMouseLeave={() => setPlayHover(false)} + > + +
-
+ )} - {/* 右侧操作按钮组(仅标准卡片) */} - {isStandard && ( + {/* 右侧操作按钮组 */} + {(config.showHeart || config.showCheckCircle) && (
- {!hideCheckCircle && ( + {config.showCheckCircle && ( )} - - - + {config.showHeart && ( + + + + )}
)}
- {/* 评分徽章(如果有rate字段) */} - {rate && ( + + {/* 评分徽章(豆瓣卡片) */} + {config.showRating && rate && (
{rate} @@ -356,8 +454,8 @@ export default function VideoCard({
)} - {/* 继续观看 - 集数矩形展示框(标准卡片) */} - {isStandard && + {/* 继续观看 - 集数矩形展示框 */} + {(from === 'playrecord' || from === 'favorite') && actualEpisodes && actualEpisodes > 1 && currentEpisode && ( @@ -372,8 +470,8 @@ export default function VideoCard({
)} - {/* 搜索非聚合/聚合 - 集数圆形展示框 */} - {(isStandard || isAggregate) && + {/* 搜索页 - 集数圆形展示框 */} + {from === 'search' && actualEpisodes && actualEpisodes > 1 && !currentEpisode && ( @@ -385,30 +483,29 @@ export default function VideoCard({ )} {/* 豆瓣链接按钮 */} - {actualDoubanId && - (isDemo || (isStandard && from === 'search') || isAggregate) && ( - e.stopPropagation()} - className='absolute top-2 left-2 scale-90 group-hover:scale-100 opacity-0 group-hover:opacity-100 transition-all duration-300 cubic-bezier(0.4,0,0.2,1)' + {config.showDoubanLink && actualDoubanId && ( + e.stopPropagation()} + className='absolute top-2 left-2 scale-90 group-hover:scale-100 opacity-0 group-hover:opacity-100 transition-all duration-300 cubic-bezier(0.4,0,0.2,1)' + > +
-
- -
-
- )} + +
+ + )}
- {/* 播放进度条(仅标准卡片) */} - {isStandard && progress !== undefined && ( + {/* 播放进度条(仅播放记录卡片) */} + {config.showProgress && progress !== undefined && (
- {/* 来源信息(仅标准卡片) */} - {isStandard && actualSource && ( + {/* 来源信息 */} + {config.showSourceName && source_name && ( {source_name}