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);
}}
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}
aspectRatio={aspectRatio}
isMobile={isMobile}
/>
</div>
)}

View File

@ -182,12 +182,15 @@ export function H5MediaViewer({
const status = raw?.video_status;
const videoId = raw?.video_id as string | undefined;
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 ? (
<>
<video
ref={(el) => (videoRefs.current[idx] = el)}
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}
preload="metadata"
playsInline
@ -217,38 +220,11 @@ export function H5MediaViewer({
<Play className="w-8 h-8" />
</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 && (
<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="text-4xl"></div>
<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>
)}
{status !== 0 && status !== 2 && (
<span className="text-white/70 text-base">Pending</span>
)}
{/* 失败重试按钮改为全局固定渲染,移出 slide */}
</div>
)}
</div>
@ -298,8 +261,10 @@ export function H5MediaViewer({
adaptiveHeight
>
{imageUrls.map((url, idx) => (
<div key={`h5-image-${idx}`} data-alt="image-slide" className="relative w-full h-full">
<img src={url} alt="scene" className="w-full h-full object-contain" />
<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" style={{
maxHeight: 'calc(100vh - 20rem)',
}} />
</div>
))}
</Carousel>
@ -403,8 +368,8 @@ export function H5MediaViewer({
// 其他阶段:使用 Carousel
return (
<div ref={rootRef} data-alt="h5-media-viewer" className="w-[100vw] relative px-4">
{/* 左侧最终视频缩略图栏H5 */}
{taskObject?.final?.url && (
{/* 左侧最终视频缩略图栏H5 视频暂停时展示 */}
{taskObject?.final?.url && !isPlaying && (
<div className="absolute left-4 top-0 z-[60]" data-alt="final-sidebar-h5">
<div className="flex items-start">
{isFinalBarOpen && (
@ -426,8 +391,8 @@ export function H5MediaViewer({
{stage === 'final_video' && videoUrls.length > 0 && renderVideoSlides()}
{stage === 'video' && videoUrls.length > 0 && renderVideoSlides()}
{(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">
{stage === 'video' && (
<>
@ -456,20 +421,25 @@ export function H5MediaViewer({
await downloadAllVideos(all);
}}
/>
<GlassIconButton
data-alt="download-current-button"
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"
icon={Download}
size="sm"
aria-label="download-current"
onClick={async () => {
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]);
}
}}
/>
{(() => {
const status = (taskObject.videos?.data ?? [])[activeIndex]?.video_status;
return status === 1 ? (
<GlassIconButton
data-alt="download-current-button"
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"
icon={Download}
size="sm"
aria-label="download-current"
onClick={async () => {
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;
return status === 2 ? (
@ -518,7 +488,7 @@ export function H5MediaViewer({
)}
<style jsx global>{`
[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>
</div>
);

View File

@ -80,7 +80,7 @@ const H5TaskInfo: React.FC<H5TaskInfoProps> = ({
if (selectedView === 'final' && taskObject.final?.url) {
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
return `Shots ${Math.max(displayCurrent, 1)}/${Math.max(videosTotal, 1)}`
}

View File

@ -18,6 +18,8 @@ interface ThumbnailGridProps {
className: string;
selectedView?: 'final' | 'video' | null;
aspectRatio: AspectRatioValue;
cols: string;
isMobile: boolean;
}
/**
@ -31,7 +33,9 @@ export function ThumbnailGrid({
onRetryVideo,
className,
selectedView,
aspectRatio
aspectRatio,
cols,
isMobile
}: ThumbnailGridProps) {
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
@ -191,7 +195,7 @@ export function ThumbnailGrid({
key={`video-${urls}-${index}`}
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'}
${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)}
>
@ -251,12 +255,16 @@ export function ThumbnailGrid({
</div>
<div className='absolute bottom-0 left-0 right-0 p-2'>
<div className="inline-flex items-center px-2 py-1 rounded-full bg-green-500/20 backdrop-blur-sm">
<Video className="w-3 h-3 text-green-400 mr-1" />
<span className="text-xs text-green-400">{index + 1}</span>
{!isMobile && (
<div className='absolute bottom-0 left-0 right-0 p-2'>
<div className="inline-flex items-center px-2 py-1 rounded-full bg-green-500/20 backdrop-blur-sm">
<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 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>
@ -276,7 +284,7 @@ export function ThumbnailGrid({
key={sketch.uniqueId || `sketch-${sketch.url}-${index}`}
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'}
${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)}
>
@ -307,29 +315,33 @@ export function ThumbnailGrid({
/>
</div>
)}
<div className='absolute bottom-0 left-0 right-0 p-2'>
{/* 角色类型 */}
{sketch.type === 'role' && (
<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" />
<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" />
<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" />
<span className="text-xs text-cyan-400">{index + 1}</span>
</div>
)}
</div>
{!isMobile && (
<div className='absolute bottom-0 left-0 right-0 p-2'>
{/* 角色类型 */}
{sketch.type === 'role' && (
<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" />
<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" />
<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" />
<span className="text-xs text-cyan-400">{index + 1}</span>
</div>
)}
</div>
)}
{/* <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>
@ -344,7 +356,7 @@ export function ThumbnailGrid({
<div
ref={thumbnailsRef}
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
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}