修复VideoCard来源数量浮层左上角被裁切,并阻止长按同时弹出操作菜单

This commit is contained in:
mtvpls
2026-07-27 18:58:02 +08:00
parent c4be0e1886
commit 03a5ce4573
2 changed files with 142 additions and 119 deletions
+136 -111
View File
@@ -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>
);