修复VideoCard来源数量浮层左上角被裁切,并阻止长按同时弹出操作菜单
This commit is contained in:
+136
-111
@@ -986,9 +986,9 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(
|
||||
return false;
|
||||
}}
|
||||
>
|
||||
{/* 海报容器 */}
|
||||
{/* 海报容器:不在外层 overflow-hidden,避免裁切来源数量浮层 */}
|
||||
<div
|
||||
className={`relative overflow-hidden rounded-lg ${
|
||||
className={`relative rounded-lg ${
|
||||
origin === 'live'
|
||||
? 'ring-1 ring-gray-300/80 dark:ring-gray-600/80'
|
||||
: ''
|
||||
@@ -1007,119 +1007,124 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(
|
||||
return false;
|
||||
}}
|
||||
>
|
||||
{/* 骨架屏 */}
|
||||
{!isLoading && !isDirectPlaySource && (
|
||||
<ImagePlaceholder
|
||||
aspectRatio={
|
||||
orientation === 'horizontal' ? 'aspect-[3/2]' : 'aspect-[2/3]'
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{isDirectPlaySource ? (
|
||||
<div className='absolute inset-0 flex items-center justify-center bg-gray-200/80 dark:bg-gray-700/80'>
|
||||
<Link className='w-8 h-8 text-blue-500' />
|
||||
</div>
|
||||
) : isNetdiskSource(actualSource) &&
|
||||
!actualPoster &&
|
||||
displayPoster === netdiskPosterPlaceholder ? (
|
||||
<div className='absolute inset-0 flex flex-col items-center justify-center bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400'>
|
||||
<Cloud className='w-10 h-10 opacity-80' />
|
||||
</div>
|
||||
) : (
|
||||
<Image
|
||||
src={displayPoster}
|
||||
alt={actualTitle}
|
||||
fill
|
||||
className={
|
||||
origin === 'live'
|
||||
? 'object-contain'
|
||||
: orientation === 'horizontal'
|
||||
? 'object-cover object-center'
|
||||
: 'object-cover'
|
||||
}
|
||||
referrerPolicy='no-referrer'
|
||||
loading='lazy'
|
||||
onLoadingComplete={() => {
|
||||
setIsLoading(true);
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setShowImageViewer(true);
|
||||
}}
|
||||
onError={(e) => {
|
||||
const img = e.currentTarget as HTMLImageElement;
|
||||
const doubanFallbackPoster =
|
||||
getDoubanImageFallbackUrl(actualPoster);
|
||||
if (
|
||||
doubanFallbackPoster &&
|
||||
tryApplyDoubanImageFallback(img, actualPoster)
|
||||
) {
|
||||
setDisplayPoster(doubanFallbackPoster);
|
||||
return;
|
||||
{/* 仅图片层做圆角裁剪 */}
|
||||
<div className='absolute inset-0 overflow-hidden rounded-lg'>
|
||||
{/* 骨架屏 */}
|
||||
{!isLoading && !isDirectPlaySource && (
|
||||
<ImagePlaceholder
|
||||
aspectRatio={
|
||||
orientation === 'horizontal'
|
||||
? 'aspect-[3/2]'
|
||||
: 'aspect-[2/3]'
|
||||
}
|
||||
|
||||
const bangumiFallbackPoster =
|
||||
getBangumiImageFallbackUrl(actualPoster);
|
||||
if (
|
||||
bangumiFallbackPoster &&
|
||||
tryApplyBangumiImageFallback(img, actualPoster)
|
||||
) {
|
||||
setDisplayPoster(bangumiFallbackPoster);
|
||||
return;
|
||||
/>
|
||||
)}
|
||||
{isDirectPlaySource ? (
|
||||
<div className='absolute inset-0 flex items-center justify-center bg-gray-200/80 dark:bg-gray-700/80'>
|
||||
<Link className='w-8 h-8 text-blue-500' />
|
||||
</div>
|
||||
) : isNetdiskSource(actualSource) &&
|
||||
!actualPoster &&
|
||||
displayPoster === netdiskPosterPlaceholder ? (
|
||||
<div className='absolute inset-0 flex flex-col items-center justify-center bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400'>
|
||||
<Cloud className='w-10 h-10 opacity-80' />
|
||||
</div>
|
||||
) : (
|
||||
<Image
|
||||
src={displayPoster}
|
||||
alt={actualTitle}
|
||||
fill
|
||||
className={
|
||||
origin === 'live'
|
||||
? 'object-contain'
|
||||
: orientation === 'horizontal'
|
||||
? 'object-cover object-center'
|
||||
: 'object-cover'
|
||||
}
|
||||
referrerPolicy='no-referrer'
|
||||
loading='lazy'
|
||||
onLoadingComplete={() => {
|
||||
setIsLoading(true);
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setShowImageViewer(true);
|
||||
}}
|
||||
onError={(e) => {
|
||||
const img = e.currentTarget as HTMLImageElement;
|
||||
const doubanFallbackPoster =
|
||||
getDoubanImageFallbackUrl(actualPoster);
|
||||
if (
|
||||
doubanFallbackPoster &&
|
||||
tryApplyDoubanImageFallback(img, actualPoster)
|
||||
) {
|
||||
setDisplayPoster(doubanFallbackPoster);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
clearBangumiImageFallbackCacheIfFailed(img, actualPoster)
|
||||
) {
|
||||
setDisplayPoster(processedPoster);
|
||||
return;
|
||||
}
|
||||
const bangumiFallbackPoster =
|
||||
getBangumiImageFallbackUrl(actualPoster);
|
||||
if (
|
||||
bangumiFallbackPoster &&
|
||||
tryApplyBangumiImageFallback(img, actualPoster)
|
||||
) {
|
||||
setDisplayPoster(bangumiFallbackPoster);
|
||||
return;
|
||||
}
|
||||
|
||||
// 图片加载失败时的重试机制
|
||||
if (!img.dataset.retried) {
|
||||
img.dataset.retried = 'true';
|
||||
setTimeout(() => {
|
||||
if (
|
||||
clearBangumiImageFallbackCacheIfFailed(img, actualPoster)
|
||||
) {
|
||||
setDisplayPoster(processedPoster);
|
||||
img.src = processedPoster;
|
||||
}, 2000);
|
||||
return;
|
||||
}
|
||||
|
||||
// 图片加载失败时的重试机制
|
||||
if (!img.dataset.retried) {
|
||||
img.dataset.retried = 'true';
|
||||
setTimeout(() => {
|
||||
setDisplayPoster(processedPoster);
|
||||
img.src = processedPoster;
|
||||
}, 2000);
|
||||
}
|
||||
}}
|
||||
style={
|
||||
{
|
||||
// 禁用图片的默认长按效果
|
||||
WebkitUserSelect: 'none',
|
||||
userSelect: 'none',
|
||||
WebkitTouchCallout: 'none',
|
||||
pointerEvents: 'auto', // 改为auto以响应点击事件
|
||||
cursor: 'pointer', // 添加指针样式
|
||||
} as React.CSSProperties
|
||||
}
|
||||
}}
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}}
|
||||
onDragStart={(e) => {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 悬浮遮罩 */}
|
||||
<div
|
||||
className='absolute inset-0 bg-gradient-to-t from-black/80 via-black/20 to-transparent transition-opacity duration-300 ease-in-out opacity-0 group-hover:opacity-100'
|
||||
style={
|
||||
{
|
||||
// 禁用图片的默认长按效果
|
||||
WebkitUserSelect: 'none',
|
||||
userSelect: 'none',
|
||||
WebkitTouchCallout: 'none',
|
||||
pointerEvents: 'auto', // 改为auto以响应点击事件
|
||||
cursor: 'pointer', // 添加指针样式
|
||||
} as React.CSSProperties
|
||||
}
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}}
|
||||
onDragStart={(e) => {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 悬浮遮罩 */}
|
||||
<div
|
||||
className='absolute inset-0 bg-gradient-to-t from-black/80 via-black/20 to-transparent transition-opacity duration-300 ease-in-out opacity-0 group-hover:opacity-100'
|
||||
style={
|
||||
{
|
||||
WebkitUserSelect: 'none',
|
||||
userSelect: 'none',
|
||||
WebkitTouchCallout: 'none',
|
||||
} as React.CSSProperties
|
||||
}
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 播放按钮或上映倒计时 */}
|
||||
{isUpcoming && daysUntilRelease !== null ? (
|
||||
@@ -1511,7 +1516,8 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`absolute bottom-1 right-1 sm:bottom-2 sm:right-2 transition-all duration-300 ease-in-out delay-75 ${
|
||||
data-button='true'
|
||||
className={`absolute bottom-1 right-1 z-[55] sm:bottom-2 sm:right-2 transition-all duration-300 ease-in-out delay-75 ${
|
||||
from === 'search'
|
||||
? 'opacity-100'
|
||||
: 'opacity-0 sm:group-hover:opacity-100'
|
||||
@@ -1523,9 +1529,32 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(
|
||||
WebkitTouchCallout: 'none',
|
||||
} as React.CSSProperties
|
||||
}
|
||||
// 阻止冒泡到卡片:避免长按/右键同时弹出操作菜单
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
e.stopPropagation();
|
||||
}}
|
||||
onTouchStart={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
onTouchMove={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
onTouchEnd={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
onMouseDown={(e) => {
|
||||
// 右键/中键按下时也不要触发卡片手势
|
||||
if (e.button !== 0) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
}}
|
||||
onPointerDown={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<div
|
||||
@@ -1547,10 +1576,6 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(
|
||||
WebkitTouchCallout: 'none',
|
||||
} as React.CSSProperties
|
||||
}
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}}
|
||||
>
|
||||
{sourceCount}
|
||||
</div>
|
||||
@@ -1590,7 +1615,7 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(
|
||||
|
||||
return (
|
||||
<div
|
||||
className='absolute bottom-full mb-2 opacity-0 invisible group-hover/sources:opacity-100 group-hover/sources:visible transition-all duration-200 ease-out delay-100 pointer-events-none z-50 right-0 sm:right-0 -translate-x-0 sm:translate-x-0'
|
||||
className='absolute bottom-full right-0 z-[60] mb-2 -translate-x-0 opacity-0 invisible pointer-events-none transition-all duration-200 ease-out delay-100 group-hover/sources:opacity-100 group-hover/sources:visible sm:right-0 sm:translate-x-0'
|
||||
style={
|
||||
{
|
||||
WebkitUserSelect: 'none',
|
||||
@@ -1604,7 +1629,7 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className='bg-gray-800/90 backdrop-blur-sm text-white text-xs sm:text-xs rounded-lg shadow-xl border border-white/10 p-1.5 sm:p-2 min-w-[100px] sm:min-w-[120px] max-w-[140px] sm:max-w-[200px] overflow-hidden'
|
||||
className='relative rounded-lg border border-white/10 bg-gray-800/95 p-1.5 text-xs text-white shadow-xl backdrop-blur-sm sm:p-2 sm:text-xs min-w-[100px] max-w-[140px] sm:min-w-[120px] sm:max-w-[200px]'
|
||||
style={
|
||||
{
|
||||
WebkitUserSelect: 'none',
|
||||
@@ -1624,9 +1649,9 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(
|
||||
key={index}
|
||||
className='flex items-center gap-1 sm:gap-1.5'
|
||||
>
|
||||
<div className='w-0.5 h-0.5 sm:w-1 sm:h-1 bg-blue-400 rounded-full flex-shrink-0'></div>
|
||||
<div className='h-0.5 w-0.5 flex-shrink-0 rounded-full bg-blue-400 sm:h-1 sm:w-1'></div>
|
||||
<span
|
||||
className='truncate text-[10px] sm:text-xs leading-tight'
|
||||
className='truncate text-[10px] leading-tight sm:text-xs'
|
||||
title={sourceName}
|
||||
>
|
||||
{sourceName}
|
||||
@@ -1637,17 +1662,17 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(
|
||||
|
||||
{/* 显示更多提示 */}
|
||||
{hasMore && (
|
||||
<div className='mt-1 sm:mt-2 pt-1 sm:pt-1.5 border-t border-gray-700/50'>
|
||||
<div className='mt-1 border-t border-gray-700/50 pt-1 sm:mt-2 sm:pt-1.5'>
|
||||
<div className='flex items-center justify-center text-gray-400'>
|
||||
<span className='text-[10px] sm:text-xs font-medium'>
|
||||
<span className='text-[10px] font-medium sm:text-xs'>
|
||||
+{remainingCount} 播放源
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 小箭头 */}
|
||||
<div className='absolute top-full right-2 sm:right-3 w-0 h-0 border-l-[4px] border-r-[4px] border-t-[4px] sm:border-l-[6px] sm:border-r-[6px] sm:border-t-[6px] border-transparent border-t-gray-800/90'></div>
|
||||
{/* 小箭头:放在内容盒外侧,避免被裁切 */}
|
||||
<div className='absolute top-full right-2 h-0 w-0 border-l-[4px] border-r-[4px] border-t-[4px] border-transparent border-t-gray-800/95 sm:right-3 sm:border-l-[6px] sm:border-r-[6px] sm:border-t-[6px]'></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user