H5 适配竖屏

This commit is contained in:
北枳 2025-09-23 16:50:26 +08:00
parent 4b163db814
commit 7a7339e6e4
4 changed files with 83 additions and 99 deletions

View File

@ -540,9 +540,11 @@ Please process this video editing request.`;
setCurrentSketchIndex(index); setCurrentSketchIndex(index);
}} }}
onRetryVideo={handleRetryVideo} onRetryVideo={handleRetryVideo}
className={isDesktop ? 'auto-cols-[20%]' : (isTablet ? 'auto-cols-[25%]' : 'auto-cols-[40%]')} className={isDesktop ? 'auto-cols-[20%]' : (isTablet ? 'auto-cols-[25%]' : 'auto-cols-[33%]')}
cols={isDesktop ? '20%' : isTablet ? '25%' : '33%'}
selectedView={selectedView} selectedView={selectedView}
aspectRatio={aspectRatio} aspectRatio={aspectRatio}
isMobile={isMobile}
/> />
</div> </div>
)} )}

View File

@ -182,12 +182,15 @@ export function H5MediaViewer({
const status = raw?.video_status; const status = raw?.video_status;
const videoId = raw?.video_id as string | undefined; const videoId = raw?.video_id as string | undefined;
return ( return (
<div key={`h5-video-${idx}`} data-alt="video-slide" className="relative w-full h-full"> <div key={`h5-video-${idx}`} data-alt="video-slide" className="relative w-full h-full flex justify-center">
{hasUrl ? ( {hasUrl ? (
<> <>
<video <video
ref={(el) => (videoRefs.current[idx] = el)} ref={(el) => (videoRefs.current[idx] = el)}
className="w-full h-full object-contain [transform:translateZ(0)] [backface-visibility:hidden] [will-change:transform] bg-black" className="w-full h-full object-contain [transform:translateZ(0)] [backface-visibility:hidden] [will-change:transform] bg-black"
style={{
maxHeight: 'calc(100vh - 20rem)',
}}
src={url} src={url}
preload="metadata" preload="metadata"
playsInline playsInline
@ -217,38 +220,11 @@ export function H5MediaViewer({
<Play className="w-8 h-8" /> <Play className="w-8 h-8" />
</button> </button>
)} )}
{/* 顶部操作按钮 */}
<div data-alt="video-actions" className="absolute top-2 right-2 z-10 flex items-center gap-2">
{showGotoCutButton && (
<button
type="button"
onClick={onGotoCut}
data-alt="goto-cut-button"
className="w-9 h-9 rounded-full bg-black/50 text-white flex items-center justify-center active:scale-95"
aria-label={'Go to editing platform'}
>
<Scissors className="w-5 h-5" />
</button>
)}
{!!setVideoPreview && !!onOpenChat && hasUrl && (
<button
type="button"
onClick={() => {
setVideoPreview(url, videoId || '');
onOpenChat();
}}
data-alt="open-chat-button"
className="w-9 h-9 rounded-full bg-black/50 text-white flex items-center justify-center active:scale-95"
aria-label={'Open chat for editing'}
>
<MessageCircleMore className="w-5 h-5" />
</button>
)}
</div>
</> </>
) : ( ) : (
<div className="w-full aspect-auto min-h-[200px] flex items-center justify-center bg-black/10 relative" data-alt="video-status"> <div className="w-full aspect-auto min-h-[200px] flex items-center justify-center bg-black/10 relative" data-alt="video-status" style={{
maxHeight: 'calc(100vh - 20rem)',
}}>
{status === 0 && ( {status === 0 && (
<span className="text-blue-500 text-base">Generating...</span> <span className="text-blue-500 text-base">Generating...</span>
)} )}
@ -256,24 +232,11 @@ export function H5MediaViewer({
<div className="flex flex-col items-center justify-center gap-3"> <div className="flex flex-col items-center justify-center gap-3">
<div className="text-4xl"></div> <div className="text-4xl"></div>
<span className="text-red-500 text-base">Generate failed</span> <span className="text-red-500 text-base">Generate failed</span>
{onRetryVideo && videoId && (
<button
type="button"
onClick={() => onRetryVideo(videoId)}
data-alt="retry-button"
className="px-3 py-1.5 rounded-full bg-black/60 text-white text-sm inline-flex items-center gap-1 active:scale-95"
aria-label={'Retry'}
>
<RotateCcw className="w-4 h-4" />
Retry
</button>
)}
</div> </div>
)} )}
{status !== 0 && status !== 2 && ( {status !== 0 && status !== 2 && (
<span className="text-white/70 text-base">Pending</span> <span className="text-white/70 text-base">Pending</span>
)} )}
{/* 失败重试按钮改为全局固定渲染,移出 slide */}
</div> </div>
)} )}
</div> </div>
@ -298,8 +261,10 @@ export function H5MediaViewer({
adaptiveHeight adaptiveHeight
> >
{imageUrls.map((url, idx) => ( {imageUrls.map((url, idx) => (
<div key={`h5-image-${idx}`} data-alt="image-slide" className="relative w-full h-full"> <div key={`h5-image-${idx}`} data-alt="image-slide" className="relative w-full h-full flex justify-center">
<img src={url} alt="scene" className="w-full h-full object-contain" /> <img src={url} alt="scene" className="w-full h-full object-contain" style={{
maxHeight: 'calc(100vh - 20rem)',
}} />
</div> </div>
))} ))}
</Carousel> </Carousel>
@ -403,8 +368,8 @@ export function H5MediaViewer({
// 其他阶段:使用 Carousel // 其他阶段:使用 Carousel
return ( return (
<div ref={rootRef} data-alt="h5-media-viewer" className="w-[100vw] relative px-4"> <div ref={rootRef} data-alt="h5-media-viewer" className="w-[100vw] relative px-4">
{/* 左侧最终视频缩略图栏H5 */} {/* 左侧最终视频缩略图栏H5 视频暂停时展示 */}
{taskObject?.final?.url && ( {taskObject?.final?.url && !isPlaying && (
<div className="absolute left-4 top-0 z-[60]" data-alt="final-sidebar-h5"> <div className="absolute left-4 top-0 z-[60]" data-alt="final-sidebar-h5">
<div className="flex items-start"> <div className="flex items-start">
{isFinalBarOpen && ( {isFinalBarOpen && (
@ -426,8 +391,8 @@ export function H5MediaViewer({
{stage === 'final_video' && videoUrls.length > 0 && renderVideoSlides()} {stage === 'final_video' && videoUrls.length > 0 && renderVideoSlides()}
{stage === 'video' && videoUrls.length > 0 && renderVideoSlides()} {stage === 'video' && videoUrls.length > 0 && renderVideoSlides()}
{(stage === 'scene' || stage === 'character') && imageUrls.length > 0 && renderImageSlides()} {(stage === 'scene' || stage === 'character') && imageUrls.length > 0 && renderImageSlides()}
{/* 全局固定操作区(右上角) */} {/* 全局固定操作区(右上角)视频暂停时展示 */}
{(stage === 'video' || stage === 'final_video') && ( {(stage === 'video' || stage === 'final_video') && !isPlaying && (
<div data-alt="global-video-actions" className="absolute top-2 right-6 z-[60] flex items-center gap-2"> <div data-alt="global-video-actions" className="absolute top-2 right-6 z-[60] flex items-center gap-2">
{stage === 'video' && ( {stage === 'video' && (
<> <>
@ -456,20 +421,25 @@ export function H5MediaViewer({
await downloadAllVideos(all); await downloadAllVideos(all);
}} }}
/> />
<GlassIconButton {(() => {
data-alt="download-current-button" const status = (taskObject.videos?.data ?? [])[activeIndex]?.video_status;
className="w-8 h-8 bg-gradient-to-br from-amber-500/80 to-yellow-600/80 backdrop-blur-xl border border-amber-400/30 rounded-full flex items-center justify-center hover:from-amber-400/80 hover:to-yellow-500/80 transition-all" return status === 1 ? (
icon={Download} <GlassIconButton
size="sm" data-alt="download-current-button"
aria-label="download-current" className="w-8 h-8 bg-gradient-to-br from-amber-500/80 to-yellow-600/80 backdrop-blur-xl border border-amber-400/30 rounded-full flex items-center justify-center hover:from-amber-400/80 hover:to-yellow-500/80 transition-all"
onClick={async () => { icon={Download}
const current = (taskObject.videos?.data ?? [])[activeIndex] as any; size="sm"
const hasUrl = current && Array.isArray(current.urls) && current.urls.length > 0; aria-label="download-current"
if (hasUrl) { onClick={async () => {
await downloadVideo(current.urls[0]); const current = (taskObject.videos?.data ?? [])[activeIndex] as any;
} const hasUrl = current && Array.isArray(current.urls) && current.urls.length > 0;
}} if (hasUrl) {
/> await downloadVideo(current.urls[0]);
}
}}
/>
) : null;
})()}
{(() => { {(() => {
const status = (taskObject.videos?.data ?? [])[activeIndex]?.video_status; const status = (taskObject.videos?.data ?? [])[activeIndex]?.video_status;
return status === 2 ? ( return status === 2 ? (
@ -518,7 +488,7 @@ export function H5MediaViewer({
)} )}
<style jsx global>{` <style jsx global>{`
[data-alt='carousel-wrapper'] .slick-slide { display: block; } [data-alt='carousel-wrapper'] .slick-slide { display: block; }
.slick-list { width: 100%;height: 100%; } .slick-list { width: 100%;height: 100%;max-height: calc(100vh - 20rem); }
`}</style> `}</style>
</div> </div>
); );

View File

@ -80,7 +80,7 @@ const H5TaskInfo: React.FC<H5TaskInfoProps> = ({
if (selectedView === 'final' && taskObject.final?.url) { if (selectedView === 'final' && taskObject.final?.url) {
return 'Final 1/1' return 'Final 1/1'
} }
if (selectedView === 'video') { if (selectedView === 'video' && !['scene', 'character'].includes(taskObject.currentStage)) {
const videosTotal = taskObject.videos?.total_count || taskObject.videos?.data?.length || 0 const videosTotal = taskObject.videos?.total_count || taskObject.videos?.data?.length || 0
return `Shots ${Math.max(displayCurrent, 1)}/${Math.max(videosTotal, 1)}` return `Shots ${Math.max(displayCurrent, 1)}/${Math.max(videosTotal, 1)}`
} }

View File

@ -18,6 +18,8 @@ interface ThumbnailGridProps {
className: string; className: string;
selectedView?: 'final' | 'video' | null; selectedView?: 'final' | 'video' | null;
aspectRatio: AspectRatioValue; aspectRatio: AspectRatioValue;
cols: string;
isMobile: boolean;
} }
/** /**
@ -31,7 +33,9 @@ export function ThumbnailGrid({
onRetryVideo, onRetryVideo,
className, className,
selectedView, selectedView,
aspectRatio aspectRatio,
cols,
isMobile
}: ThumbnailGridProps) { }: ThumbnailGridProps) {
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null); const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
@ -191,7 +195,7 @@ export function ThumbnailGrid({
key={`video-${urls}-${index}`} key={`video-${urls}-${index}`}
className={`relative aspect-auto rounded-lg overflow-hidden className={`relative aspect-auto rounded-lg overflow-hidden
${(currentSketchIndex === index && !disabled && selectedView !== 'final') ? 'ring-2 ring-blue-500 z-10' : 'hover:ring-2 hover:ring-blue-500/50'} ${(currentSketchIndex === index && !disabled && selectedView !== 'final') ? 'ring-2 ring-blue-500 z-10' : 'hover:ring-2 hover:ring-blue-500/50'}
${aspectRatio === 'VIDEO_ASPECT_RATIO_LANDSCAPE' ? 'min-w-[210px]' : 'min-w-[70px]'} ${aspectRatio === 'VIDEO_ASPECT_RATIO_LANDSCAPE' ? `min-w-[${cols}]` : 'min-w-[70px]'}
`} `}
onClick={() => !isDragging && !disabled && onSketchSelect(index)} onClick={() => !isDragging && !disabled && onSketchSelect(index)}
> >
@ -251,12 +255,16 @@ export function ThumbnailGrid({
</div> </div>
<div className='absolute bottom-0 left-0 right-0 p-2'> {!isMobile && (
<div className="inline-flex items-center px-2 py-1 rounded-full bg-green-500/20 backdrop-blur-sm"> <div className='absolute bottom-0 left-0 right-0 p-2'>
<Video className="w-3 h-3 text-green-400 mr-1" /> <div className="inline-flex items-center px-2 py-1 rounded-full bg-green-500/20 backdrop-blur-sm">
<span className="text-xs text-green-400">{index + 1}</span> <Video className="w-3 h-3 text-green-400 mr-1" />
<span className="text-xs text-green-400">{index + 1}</span>
</div>
</div> </div>
</div> )}
{/* <div className="absolute bottom-0 left-0 right-0 p-2 bg-gradient-to-t from-black/60 to-transparent z-10"> {/* <div className="absolute bottom-0 left-0 right-0 p-2 bg-gradient-to-t from-black/60 to-transparent z-10">
<span className="text-xs text-white/90">Scene {index + 1}</span> <span className="text-xs text-white/90">Scene {index + 1}</span>
@ -276,7 +284,7 @@ export function ThumbnailGrid({
key={sketch.uniqueId || `sketch-${sketch.url}-${index}`} key={sketch.uniqueId || `sketch-${sketch.url}-${index}`}
className={`relative aspect-auto rounded-lg overflow-hidden className={`relative aspect-auto rounded-lg overflow-hidden
${currentSketchIndex === index ? 'ring-2 ring-blue-500 z-10' : 'hover:ring-2 hover:ring-blue-500/50'} ${currentSketchIndex === index ? 'ring-2 ring-blue-500 z-10' : 'hover:ring-2 hover:ring-blue-500/50'}
${aspectRatio === 'VIDEO_ASPECT_RATIO_LANDSCAPE' ? 'min-w-[210px]' : 'min-w-[70px]'} ${aspectRatio === 'VIDEO_ASPECT_RATIO_LANDSCAPE' ? `min-w-[${cols}]` : 'min-w-[70px]'}
`} `}
onClick={() => !isDragging && onSketchSelect(index)} onClick={() => !isDragging && onSketchSelect(index)}
> >
@ -307,29 +315,33 @@ export function ThumbnailGrid({
/> />
</div> </div>
)} )}
<div className='absolute bottom-0 left-0 right-0 p-2'>
{/* 角色类型 */} {!isMobile && (
{sketch.type === 'role' && ( <div className='absolute bottom-0 left-0 right-0 p-2'>
<div className="inline-flex items-center px-2 py-1 rounded-full bg-purple-500/20 backdrop-blur-sm"> {/* 角色类型 */}
<SquareUserRound className="w-3 h-3 text-purple-400 mr-1" /> {sketch.type === 'role' && (
<span className="text-xs text-purple-400">Role</span> <div className="inline-flex items-center px-2 py-1 rounded-full bg-purple-500/20 backdrop-blur-sm">
</div> <SquareUserRound className="w-3 h-3 text-purple-400 mr-1" />
)} <span className="text-xs text-purple-400">Role</span>
{/* 场景类型 */} </div>
{sketch.type === 'scene' && ( )}
<div className="inline-flex items-center px-2 py-1 rounded-full bg-purple-500/20 backdrop-blur-sm"> {/* 场景类型 */}
<MapPinHouse className="w-3 h-3 text-purple-400 mr-1" /> {sketch.type === 'scene' && (
<span className="text-xs text-purple-400">Scene</span> <div className="inline-flex items-center px-2 py-1 rounded-full bg-purple-500/20 backdrop-blur-sm">
</div> <MapPinHouse className="w-3 h-3 text-purple-400 mr-1" />
)} <span className="text-xs text-purple-400">Scene</span>
{/* 分镜类型 */} </div>
{(!sketch.type || sketch.type === 'shot_sketch') && ( )}
<div className="inline-flex items-center px-2 py-1 rounded-full bg-cyan-500/20 backdrop-blur-sm"> {/* 分镜类型 */}
<Clapperboard className="w-3 h-3 text-cyan-400 mr-1" /> {(!sketch.type || sketch.type === 'shot_sketch') && (
<span className="text-xs text-cyan-400">{index + 1}</span> <div className="inline-flex items-center px-2 py-1 rounded-full bg-cyan-500/20 backdrop-blur-sm">
</div> <Clapperboard className="w-3 h-3 text-cyan-400 mr-1" />
)} <span className="text-xs text-cyan-400">{index + 1}</span>
</div> </div>
)}
</div>
)}
{/* <div className="absolute bottom-0 left-0 right-0 p-2 bg-gradient-to-t from-black/60 to-transparent z-10"> {/* <div className="absolute bottom-0 left-0 right-0 p-2 bg-gradient-to-t from-black/60 to-transparent z-10">
<span className="text-xs text-white/90">{sketch.type === 'role' ? 'Role' : (sketch.type === 'scene' ? 'Scene' : 'Shot')} {index + 1}</span> <span className="text-xs text-white/90">{sketch.type === 'role' ? 'Role' : (sketch.type === 'scene' ? 'Scene' : 'Shot')} {index + 1}</span>
@ -344,7 +356,7 @@ export function ThumbnailGrid({
<div <div
ref={thumbnailsRef} ref={thumbnailsRef}
tabIndex={0} tabIndex={0}
className={`w-full h-full grid grid-flow-col gap-2 overflow-x-auto hide-scrollbar px-1 py-1 cursor-grab active:cursor-grabbing focus:outline-none select-none ${className} auto-cols-max`} className={`w-full h-full grid grid-flow-col gap-2 overflow-x-auto hide-scrollbar px-1 py-1 cursor-grab active:cursor-grabbing focus:outline-none select-none auto-cols-[${cols}] ${aspectRatio === 'VIDEO_ASPECT_RATIO_LANDSCAPE' ? '' : '!auto-cols-max'}`}
autoFocus autoFocus
onMouseDown={handleMouseDown} onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove} onMouseMove={handleMouseMove}