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}