mirror of
https://github.com/MoonTechLab/LunaTV.git
synced 2026-06-09 10:13:13 +08:00
Merge pull request #42 from OuOumm/main
refactor(components): 优化卡片组件样式和交互逻辑
This commit is contained in:
@@ -30,23 +30,19 @@ function SearchPageClient() {
|
|||||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
// 视图模式:聚合(agg) 或 全部(all),默认值由环境变量 NEXT_PUBLIC_AGGREGATE_SEARCH_RESULT 决定
|
// 视图模式:聚合(agg) 或 全部(all),默认值由环境变量 NEXT_PUBLIC_AGGREGATE_SEARCH_RESULT 决定
|
||||||
const [viewMode, setViewMode] = useState<'agg' | 'all'>(() => {
|
const [viewMode, setViewMode] = useState<'agg' | 'all'>(
|
||||||
const envVal = process.env.NEXT_PUBLIC_AGGREGATE_SEARCH_RESULT;
|
process.env.NEXT_PUBLIC_AGGREGATE_SEARCH_RESULT === 'false' ||
|
||||||
// 默认聚合('agg')。当显式设置为 'false' 或 '0' 时使用 'all'
|
process.env.NEXT_PUBLIC_AGGREGATE_SEARCH_RESULT === '0'
|
||||||
if (envVal === 'false' || envVal === '0') {
|
? 'all'
|
||||||
return 'all';
|
: 'agg'
|
||||||
}
|
);
|
||||||
return 'agg';
|
|
||||||
});
|
|
||||||
|
|
||||||
// 聚合后的结果(按标题和年份分组)
|
// 聚合后的结果(按标题和年份分组)
|
||||||
const aggregatedResults = useMemo(() => {
|
const aggregatedResults = useMemo(() => {
|
||||||
const map = new Map<string, SearchResult[]>();
|
const map = new Map<string, SearchResult[]>();
|
||||||
searchResults.forEach((item) => {
|
searchResults.forEach((item) => {
|
||||||
// 使用 title + year 作为键,若 year 不存在则使用 'unknown'
|
// 使用 title + year + id 作为键,若 year 不存在则使用 'unknown'
|
||||||
const key = `${item.title}-${item.year || 'unknown'}-${
|
const key = `${item.title}-${item.year || 'unknown'}-${item.id}`;
|
||||||
item.episodes.length === 1 ? 'movie' : 'tv'
|
|
||||||
}`;
|
|
||||||
const arr = map.get(key) || [];
|
const arr = map.get(key) || [];
|
||||||
arr.push(item);
|
arr.push(item);
|
||||||
map.set(key, arr);
|
map.set(key, arr);
|
||||||
@@ -55,16 +51,9 @@ function SearchPageClient() {
|
|||||||
}, [searchResults]);
|
}, [searchResults]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 自动聚焦搜索框:仅当 URL 中没有搜索参数时
|
// 无搜索参数时聚焦搜索框
|
||||||
if (!searchParams.get('q')) {
|
!searchParams.get('q') && searchInputRef.current?.focus();
|
||||||
searchInputRef.current?.focus();
|
getSearchHistory().then(setSearchHistory);
|
||||||
}
|
|
||||||
|
|
||||||
// 加载搜索历史
|
|
||||||
(async () => {
|
|
||||||
const history = await getSearchHistory();
|
|
||||||
setSearchHistory(history);
|
|
||||||
})();
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -172,13 +161,13 @@ function SearchPageClient() {
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
key={`search-results-${viewMode}`}
|
key={`search-results-${viewMode}`}
|
||||||
className='justify-start grid grid-cols-3 gap-x-2 gap-y-14 sm:gap-y-20 px-0 sm:px-2 sm:grid-cols-[repeat(auto-fill,_minmax(11rem,_1fr))] sm:gap-x-8 sm:px-4'
|
className='justify-start grid grid-cols-3 gap-x-2 gap-y-14 sm:gap-y-20 px-0 sm:px-2 sm:grid-cols-[repeat(auto-fill,_minmax(11rem,_1fr))] sm:gap-x-8'
|
||||||
>
|
>
|
||||||
{viewMode === 'agg'
|
{viewMode === 'agg'
|
||||||
? aggregatedResults.map((group) => {
|
? aggregatedResults.map((group) => {
|
||||||
const key = `${group[0].title}-${
|
const key = `${group[0].title}-${
|
||||||
group[0].year || 'unknown'
|
group[0].year || 'unknown'
|
||||||
}`;
|
}-${group[0].id}`;
|
||||||
return (
|
return (
|
||||||
<div key={`agg-${key}`} className='w-full'>
|
<div key={`agg-${key}`} className='w-full'>
|
||||||
<AggregateCard
|
<AggregateCard
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ function PlayCircleSolid({
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 与 `VideoCard` 基本一致,删除了集数徽标、来源标签、收藏等功能
|
* 与 `VideoCard` 基本一致,删除了来源标签、收藏等功能
|
||||||
* 点击播放按钮 -> 跳到第一个源播放
|
* 点击播放按钮 -> 跳到第一个源播放
|
||||||
* 点击卡片其他区域 -> 跳到聚合详情页 (/aggregate)
|
* 点击卡片其他区域 -> 跳到聚合详情页 (/aggregate)
|
||||||
*/
|
*/
|
||||||
@@ -161,9 +161,9 @@ const AggregateCard: React.FC<AggregateCardProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 集数矩形展示框 - 不透明,无hover效果 */}
|
{/* 集数矩形展示框 */}
|
||||||
{mostFrequentEpisodes && mostFrequentEpisodes > 1 && (
|
{mostFrequentEpisodes && mostFrequentEpisodes > 1 && (
|
||||||
<div className='absolute top-2 right-2 min-w-[1.875rem] h-5 sm:h-7 sm:min-w-[2.5rem] bg-green-500 dark:bg-green-600 rounded-md flex items-center justify-center px-2 shadow-md text-[0.55rem] sm:text-xs'>
|
<div className='absolute top-2 right-2 w-7 h-7 sm:w-7 sm:h-7 rounded-full bg-green-500/90 dark:bg-green-600/90 flex items-center justify-center shadow-md text-[0.55rem] sm:text-xs'>
|
||||||
<span className='text-white font-bold leading-none'>
|
<span className='text-white font-bold leading-none'>
|
||||||
{mostFrequentEpisodes}
|
{mostFrequentEpisodes}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Link as LinkIcon, Search } from 'lucide-react';
|
import { Link as LinkIcon, Search } from 'lucide-react';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
interface DemoCardProps {
|
interface DemoCardProps {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -45,6 +45,7 @@ function SearchCircle({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const DemoCard = ({ id, title, poster, rate, type }: DemoCardProps) => {
|
const DemoCard = ({ id, title, poster, rate, type }: DemoCardProps) => {
|
||||||
|
const [hover, setHover] = useState(false);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
@@ -57,7 +58,7 @@ const DemoCard = ({ id, title, poster, rate, type }: DemoCardProps) => {
|
|||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
>
|
>
|
||||||
{/* 海报图片区域 */}
|
{/* 海报图片区域 */}
|
||||||
<div className='relative aspect-[2/3] w-full overflow-hidden rounded-md transition-all duration-400 cubic-bezier(0.4, 0, 0.2, 1)'>
|
<div className='relative aspect-[2/3] w-full overflow-hidden rounded-md group-hover:scale-[1.02] transition-all duration-400 cubic-bezier(0.4, 0, 0.2, 1) safari-fix'>
|
||||||
<Image
|
<Image
|
||||||
src={poster}
|
src={poster}
|
||||||
alt={title}
|
alt={title}
|
||||||
@@ -77,12 +78,15 @@ const DemoCard = ({ id, title, poster, rate, type }: DemoCardProps) => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 悬浮层 - 搜索按钮 */}
|
{/* 悬浮层 - 搜索按钮 */}
|
||||||
<div
|
<div className='absolute inset-0 bg-gradient-to-t from-black/70 via-black/20 to-transparent opacity-0 group-hover:opacity-100 transition-all duration-300 cubic-bezier(0.4, 0, 0.2, 1) flex items-center justify-center'>
|
||||||
style={{ willChange: 'opacity' }}
|
<div
|
||||||
className='absolute inset-0 bg-gradient-to-t from-black/70 via-black/20 to-transparent opacity-0 group-hover:opacity-100 transition-all duration-300 cubic-bezier(0.4, 0, 0.2, 1) flex items-center justify-center'
|
onMouseEnter={() => setHover(true)}
|
||||||
>
|
onMouseLeave={() => setHover(false)}
|
||||||
<div className='transition-all duration-300 cubic-bezier(0.4, 0, 0.2, 1) scale-90 group-hover:scale-110 group-hover:rotate-12'>
|
className={`transition-all duration-300 cubic-bezier(0.4, 0, 0.2, 1) ${
|
||||||
<SearchCircle className='group-hover:fill-green-500' />
|
hover ? 'scale-110 rotate-12' : 'scale-90'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<SearchCircle fillColor={hover ? '#22c55e' : 'none'} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -111,12 +111,6 @@ export default function VideoCard({
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
// 如果是从收藏夹移除,立即更新UI
|
|
||||||
if (favorited && from === 'favorites') {
|
|
||||||
setIsDeleting(true);
|
|
||||||
onDelete?.();
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const newState = await toggleFavorite(source, id, {
|
const newState = await toggleFavorite(source, id, {
|
||||||
title,
|
title,
|
||||||
@@ -237,23 +231,23 @@ export default function VideoCard({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 集数矩形展示框 - 增加条件判断:仅当有多个集数且已播放时显示 */}
|
{/* 继续观看 - 集数矩形展示框 */}
|
||||||
{episodes && episodes > 1 && currentEpisode && (
|
{episodes && episodes > 1 && currentEpisode && (
|
||||||
<div className='absolute top-2 right-2 min-w-[1.875rem] h-5 sm:h-7 sm:min-w-[2.5rem] bg-green-500/90 dark:bg-green-600/90 rounded-md flex items-center justify-center px-2 shadow-md text-[0.55rem] sm:text-xs'>
|
<div className='absolute top-2 right-2 min-w-[1.875rem] h-5 sm:h-7 sm:min-w-[2.5rem] bg-green-500/90 dark:bg-green-600/90 rounded-md flex items-center justify-center px-2 shadow-md text-[0.55rem] sm:text-xs'>
|
||||||
<span className='text-white font-bold leading-none'>
|
<span className='text-white font-bold leading-none'>
|
||||||
{currentEpisode}
|
{currentEpisode}
|
||||||
|
<span className='mx-1 text-white/80'>/</span>
|
||||||
</span>
|
</span>
|
||||||
<span className='mx-1 text-white/80'>/</span>
|
|
||||||
<span className='text-white font-bold leading-none'>
|
<span className='text-white font-bold leading-none'>
|
||||||
{episodes}
|
{episodes}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 集数圆形展示框 - 无当前集数且总集数大于 1 时展示 */}
|
{/* 搜索非聚合 - 集数圆形展示框 */}
|
||||||
{episodes && episodes > 1 && !currentEpisode && (
|
{from === 'search' && (
|
||||||
<div className='absolute top-2 right-2 w-4 h-4 sm:w-7 sm:h-7 bg-green-500 rounded-full flex items-center justify-center'>
|
<div className='absolute top-2 right-2 w-4 h-4 sm:w-7 sm:h-7 rounded-full bg-green-500/90 dark:bg-green-600/90 flex items-center justify-center shadow-md text-[0.55rem] sm:text-xs'>
|
||||||
<span className='text-white text-[0.5rem] sm:text-xs font-bold'>
|
<span className='text-white font-bold leading-none'>
|
||||||
{episodes}
|
{episodes}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user