优化素材库视频筛选加载
This commit is contained in:
parent
524024f7d6
commit
0f319f7b68
@ -140,6 +140,39 @@ describe('MaterialCard', () => {
|
||||
expect(screen.getByText('15天后可查看详情')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
test('视频卡片初始只加载封面,悬停时才挂载视频预览', () => {
|
||||
const { container } = render(
|
||||
<MaterialCard
|
||||
{...defaultProps}
|
||||
material={{
|
||||
...baseMaterial,
|
||||
materialType: '淘系-逛逛视频',
|
||||
videoCoverUrl: 'https://example.com/video-cover.jpg',
|
||||
videoPreviewClipUrl: 'https://example.com/video-preview.mp4',
|
||||
videoProcessingStatus: 'done',
|
||||
}}
|
||||
/>,
|
||||
)
|
||||
|
||||
const previewArea = container.querySelector('[data-material-link="true"]')
|
||||
expect(previewArea).not.toBeNull()
|
||||
expect(screen.getByAltText('SPU-001 视频封面')).toHaveAttribute(
|
||||
'src',
|
||||
'https://example.com/video-cover.jpg',
|
||||
)
|
||||
expect(screen.queryByLabelText('SPU-001 视频预览')).not.toBeInTheDocument()
|
||||
|
||||
fireEvent.mouseEnter(previewArea!)
|
||||
expect(screen.getByLabelText('SPU-001 视频预览')).toHaveAttribute(
|
||||
'src',
|
||||
'https://example.com/video-preview.mp4',
|
||||
)
|
||||
|
||||
fireEvent.mouseLeave(previewArea!)
|
||||
expect(screen.queryByLabelText('SPU-001 视频预览')).not.toBeInTheDocument()
|
||||
expect(screen.getByAltText('SPU-001 视频封面')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
test('15天保护期内仍可复制分享链接', () => {
|
||||
render(
|
||||
<MaterialCard
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import React, { useRef, useEffect, useState } from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import Image from 'next/image'
|
||||
import { format } from 'date-fns'
|
||||
import AudioVisualizer from '../AudioVisualizer'
|
||||
@ -148,8 +148,8 @@ export default function MaterialCard({
|
||||
onRetryTranscode,
|
||||
showToast,
|
||||
}: MaterialCardProps) {
|
||||
const videoRef = useRef<HTMLVideoElement>(null)
|
||||
const [posterLoadError, setPosterLoadError] = useState(false)
|
||||
const [shouldLoadVideoPreview, setShouldLoadVideoPreview] = useState(false)
|
||||
|
||||
// 判断素材类型
|
||||
const isVideoType = isVideoMaterialType(material.materialType)
|
||||
@ -173,30 +173,11 @@ export default function MaterialCard({
|
||||
reportTitle: material.reportTitle,
|
||||
})
|
||||
|
||||
// 检测封面图是否可加载
|
||||
// 素材切换时回到封面态;视频只在用户悬停时加载,避免列表首屏并发预载全部视频。
|
||||
useEffect(() => {
|
||||
if (isVideoType && posterUrl && typeof window !== 'undefined') {
|
||||
setPosterLoadError(false)
|
||||
const img = document.createElement('img')
|
||||
img.onload = () => {
|
||||
setPosterLoadError(false)
|
||||
}
|
||||
img.onerror = () => {
|
||||
console.error('[MaterialCard] 视频封面图预加载失败:', {
|
||||
materialId: material.id,
|
||||
productModel: materialDisplayTitle,
|
||||
videoCoverUrl: material.videoCoverUrl,
|
||||
previewUrl: material.previewUrl,
|
||||
posterUrl: posterUrl,
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
setPosterLoadError(true)
|
||||
}
|
||||
img.src = posterUrl
|
||||
} else {
|
||||
setPosterLoadError(false)
|
||||
}
|
||||
}, [isVideoType, posterUrl, material.id, materialDisplayTitle, material.videoCoverUrl, material.previewUrl])
|
||||
setPosterLoadError(false)
|
||||
setShouldLoadVideoPreview(false)
|
||||
}, [material.id, posterUrl])
|
||||
|
||||
// 音频URL(API已经处理过,直接使用)
|
||||
let audioUrl = ''
|
||||
@ -218,18 +199,13 @@ export default function MaterialCard({
|
||||
onClick(material)
|
||||
}
|
||||
|
||||
// 视频鼠标悬停播放控制
|
||||
// 视频鼠标悬停播放控制:离开后卸载 video,立即释放媒体连接。
|
||||
const handleVideoMouseEnter = () => {
|
||||
if (videoRef.current) {
|
||||
videoRef.current.play().catch(() => {})
|
||||
}
|
||||
setShouldLoadVideoPreview(true)
|
||||
}
|
||||
|
||||
const handleVideoMouseLeave = () => {
|
||||
if (videoRef.current) {
|
||||
videoRef.current.pause()
|
||||
videoRef.current.currentTime = 0
|
||||
}
|
||||
setShouldLoadVideoPreview(false)
|
||||
}
|
||||
|
||||
// 复制ID到剪贴板
|
||||
@ -376,28 +352,43 @@ export default function MaterialCard({
|
||||
{/* 视频类型 */}
|
||||
{isVideoType && videoPreviewUrl ? (
|
||||
<>
|
||||
<video
|
||||
ref={videoRef}
|
||||
src={videoPreviewUrl}
|
||||
poster={!posterLoadError && posterUrl ? posterUrl : undefined}
|
||||
className="h-full w-full object-cover"
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
preload="metadata"
|
||||
onError={(e) => {
|
||||
// 视频加载失败时的错误处理
|
||||
console.error('[MaterialCard] 视频加载失败:', {
|
||||
materialId: material.id,
|
||||
productModel: materialDisplayTitle,
|
||||
videoPreviewUrl: videoPreviewUrl,
|
||||
posterUrl: posterUrl,
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
}}
|
||||
/>
|
||||
{/* 视频封面加载失败时的占位符 */}
|
||||
{posterLoadError && (
|
||||
{shouldLoadVideoPreview ? (
|
||||
<video
|
||||
src={videoPreviewUrl}
|
||||
poster={!posterLoadError && posterUrl ? posterUrl : undefined}
|
||||
aria-label={`${materialDisplayTitle} 视频预览`}
|
||||
className="h-full w-full object-cover"
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
autoPlay
|
||||
preload="metadata"
|
||||
onError={() => {
|
||||
console.error('[MaterialCard] 视频加载失败:', {
|
||||
materialId: material.id,
|
||||
productModel: materialDisplayTitle,
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
}}
|
||||
/>
|
||||
) : !posterLoadError && posterUrl ? (
|
||||
<Image
|
||||
src={posterUrl}
|
||||
alt={`${materialDisplayTitle} 视频封面`}
|
||||
fill
|
||||
unoptimized
|
||||
className="object-cover"
|
||||
sizes="(max-width: 768px) 100vw, 300px"
|
||||
onError={() => {
|
||||
console.error('[MaterialCard] 视频封面图加载失败:', {
|
||||
materialId: material.id,
|
||||
productModel: materialDisplayTitle,
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
setPosterLoadError(true)
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className="absolute inset-0 w-full h-full flex items-center justify-center bg-[#eceef0] z-10">
|
||||
<span className="text-[#86898e] text-sm">封面加载失败</span>
|
||||
</div>
|
||||
|
||||
@ -233,12 +233,6 @@ export function rewriteStorageUrlForClient(url?: string | null, request?: NextRe
|
||||
const decodedPath = decodeURIComponent(parsed.pathname)
|
||||
// 手动构建URL字符串,避免toString()自动编码
|
||||
const decodedUrl = `${parsed.protocol}//${parsed.hostname}${parsed.port ? `:${parsed.port}` : ''}${decodedPath}${parsed.search || ''}${parsed.hash || ''}`
|
||||
console.log('[URL Rewrite] 解码内网MinIO路径:', {
|
||||
original: trimmed,
|
||||
originalPath: parsed.pathname,
|
||||
decodedPath,
|
||||
decodedUrl
|
||||
})
|
||||
return decodedUrl
|
||||
} catch {
|
||||
// 解码失败,保持原路径(可能部分编码)
|
||||
@ -278,17 +272,10 @@ export function rewriteStorageUrlForClient(url?: string | null, request?: NextRe
|
||||
if (isAlreadyEncoded) {
|
||||
// 路径已经编码过,先解码(MinIO期望未编码的路径)
|
||||
try {
|
||||
const originalPath = parsed.pathname
|
||||
const decodedPath = decodeURIComponent(parsed.pathname)
|
||||
// 手动构建URL字符串,避免toString()自动编码
|
||||
// 格式:protocol://hostname:port/pathname?search#hash
|
||||
const decodedUrl = `${parsed.protocol}//${parsed.hostname}${parsed.port ? `:${parsed.port}` : ''}${decodedPath}${parsed.search || ''}${parsed.hash || ''}`
|
||||
console.log('[URL Rewrite] 解码公网S3路径:', {
|
||||
original: trimmed,
|
||||
originalPath,
|
||||
decodedPath,
|
||||
decodedUrl
|
||||
})
|
||||
return decodedUrl
|
||||
} catch {
|
||||
// 解码失败,保持原路径
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user