forked from 77media/video-flow
缩略图区域
This commit is contained in:
parent
1d5a98bb7d
commit
de9b28e935
@ -165,7 +165,7 @@
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-auto-rows: auto;
|
grid-auto-rows: auto;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: self-start;
|
||||||
}
|
}
|
||||||
.videoContainer-qteKNi {
|
.videoContainer-qteKNi {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@ -181,7 +181,7 @@
|
|||||||
object-position: center;
|
object-position: center;
|
||||||
background-color: #0003;
|
background-color: #0003;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
height: 100%;
|
/* height: 100%; */
|
||||||
}
|
}
|
||||||
.container-kIPoeH {
|
.container-kIPoeH {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|||||||
@ -448,8 +448,13 @@ Please process this video editing request.`;
|
|||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
>
|
>
|
||||||
{isDesktop ? (
|
{isDesktop ? (
|
||||||
<div className={`relative heroVideo-FIzuK1 ${['script'].includes(taskObject.currentStage) ? 'h-[calc(100vh-6rem)] w-[calc((100vh-6rem)/9*16)]' : 'h-[calc(100vh-6rem-200px)] w-[calc((100vh-6rem-200px)/9*16)]'}`} style={{ aspectRatio: "16 / 9" }} key={taskObject.currentStage+'_'+currentSketchIndex}>
|
<div className={`relative heroVideo-FIzuK1`}
|
||||||
|
style={{
|
||||||
|
height: taskObject.final.url ? 'calc(100vh - 8rem)' : 'calc(100vh - 12rem)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
<MediaViewer
|
<MediaViewer
|
||||||
|
key={taskObject.currentStage+'_'+currentSketchIndex}
|
||||||
taskObject={taskObject}
|
taskObject={taskObject}
|
||||||
scriptData={scriptData}
|
scriptData={scriptData}
|
||||||
currentSketchIndex={currentSketchIndex}
|
currentSketchIndex={currentSketchIndex}
|
||||||
@ -471,62 +476,103 @@ Please process this video editing request.`;
|
|||||||
onGotoCut={generateEditPlan}
|
onGotoCut={generateEditPlan}
|
||||||
isSmartChatBoxOpen={isSmartChatBoxOpen}
|
isSmartChatBoxOpen={isSmartChatBoxOpen}
|
||||||
onRetryVideo={(video_id) => handleRetryVideo(video_id)}
|
onRetryVideo={(video_id) => handleRetryVideo(video_id)}
|
||||||
|
aspectRatio={aspectRatio}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{['scene', 'character', 'video', 'final_video'].includes(taskObject.currentStage) && (
|
||||||
|
<div className={`h-14 absolute bottom-[0.5rem] left-[50%] translate-x-[-50%] z-[21]`}
|
||||||
|
style={{
|
||||||
|
maxWidth: 'calc(100% - 5.5rem)'
|
||||||
|
}}>
|
||||||
|
<ThumbnailGrid
|
||||||
|
isDisabledFocus={isEditModalOpen || isPauseWorkFlow || isFocusChatInput}
|
||||||
|
taskObject={taskObject}
|
||||||
|
currentSketchIndex={currentSketchIndex}
|
||||||
|
onSketchSelect={(index) => {
|
||||||
|
if (index === -1 && taskObject.final.url) {
|
||||||
|
// 点击最终视频
|
||||||
|
setSelectedView('final');
|
||||||
|
setCurrentSketchIndex(0);
|
||||||
|
} else {
|
||||||
|
// 点击普通视频
|
||||||
|
taskObject.final.url && setSelectedView('video');
|
||||||
|
setCurrentSketchIndex(index);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onRetryVideo={handleRetryVideo}
|
||||||
|
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>
|
||||||
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<H5MediaViewer
|
<div className="relative w-full">
|
||||||
taskObject={taskObject}
|
<H5MediaViewer
|
||||||
scriptData={scriptData}
|
taskObject={taskObject}
|
||||||
currentSketchIndex={currentSketchIndex}
|
scriptData={scriptData}
|
||||||
selectedView={selectedView}
|
currentSketchIndex={currentSketchIndex}
|
||||||
mode={mode}
|
selectedView={selectedView}
|
||||||
setIsPauseWorkFlow={setIsPauseWorkFlow}
|
mode={mode}
|
||||||
setAnyAttribute={setAnyAttribute}
|
setIsPauseWorkFlow={setIsPauseWorkFlow}
|
||||||
isPauseWorkFlow={isPauseWorkFlow}
|
setAnyAttribute={setAnyAttribute}
|
||||||
applyScript={applyScript}
|
isPauseWorkFlow={isPauseWorkFlow}
|
||||||
setCurrentSketchIndex={setCurrentSketchIndex}
|
applyScript={applyScript}
|
||||||
onOpenChat={() => setIsSmartChatBoxOpen(true)}
|
setCurrentSketchIndex={setCurrentSketchIndex}
|
||||||
setVideoPreview={(url, id) => {
|
onOpenChat={() => setIsSmartChatBoxOpen(true)}
|
||||||
setPreviewVideoUrl(url);
|
setVideoPreview={(url, id) => {
|
||||||
setPreviewVideoId(id);
|
setPreviewVideoUrl(url);
|
||||||
}}
|
setPreviewVideoId(id);
|
||||||
showGotoCutButton={showGotoCutButton || editingStatus !== 'idle'}
|
}}
|
||||||
onGotoCut={generateEditPlan}
|
showGotoCutButton={showGotoCutButton || editingStatus !== 'idle'}
|
||||||
isSmartChatBoxOpen={isSmartChatBoxOpen}
|
onGotoCut={generateEditPlan}
|
||||||
onRetryVideo={(video_id) => handleRetryVideo(video_id)}
|
isSmartChatBoxOpen={isSmartChatBoxOpen}
|
||||||
onSelectView={(view) => setSelectedView(view)}
|
onRetryVideo={(video_id) => handleRetryVideo(video_id)}
|
||||||
enableVideoEdit={true}
|
onSelectView={(view) => setSelectedView(view)}
|
||||||
onVideoEditDescriptionSubmit={handleVideoEditDescriptionSubmit}
|
enableVideoEdit={true}
|
||||||
projectId={episodeId}
|
onVideoEditDescriptionSubmit={handleVideoEditDescriptionSubmit}
|
||||||
/>
|
projectId={episodeId}
|
||||||
|
aspectRatio={aspectRatio === 'VIDEO_ASPECT_RATIO_LANDSCAPE' ? '16:9' : '9:16'}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{['scene', 'character', 'video', 'final_video'].includes(taskObject.currentStage) && (
|
||||||
|
<div className={`h-14 absolute left-[50%] translate-x-[-50%] z-[21] ${isMobile ? '' : 'bottom-[180px]'}`}
|
||||||
|
style={{
|
||||||
|
maxWidth: 'calc(100vw - 5.5rem)',
|
||||||
|
bottom: (aspectRatio === 'VIDEO_ASPECT_RATIO_LANDSCAPE') ? '-2.5rem' : '0.5rem'
|
||||||
|
}}>
|
||||||
|
<ThumbnailGrid
|
||||||
|
isDisabledFocus={isEditModalOpen || isPauseWorkFlow || isFocusChatInput}
|
||||||
|
taskObject={taskObject}
|
||||||
|
currentSketchIndex={currentSketchIndex}
|
||||||
|
onSketchSelect={(index) => {
|
||||||
|
if (index === -1) {
|
||||||
|
// 点击最终视频
|
||||||
|
setSelectedView('final');
|
||||||
|
setCurrentSketchIndex(0);
|
||||||
|
} else {
|
||||||
|
// 点击普通视频
|
||||||
|
setSelectedView('video');
|
||||||
|
setCurrentSketchIndex(index);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onRetryVideo={handleRetryVideo}
|
||||||
|
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>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{taskObject.currentStage !== 'script' && (
|
|
||||||
<div className={`h-[123px] ${!isDesktop ? '!w-full' : 'w-[calc((100vh-6rem-200px)/9*16)]'}`}>
|
|
||||||
<ThumbnailGrid
|
|
||||||
isDisabledFocus={isEditModalOpen || isPauseWorkFlow || isFocusChatInput}
|
|
||||||
taskObject={taskObject}
|
|
||||||
currentSketchIndex={currentSketchIndex}
|
|
||||||
onSketchSelect={(index) => {
|
|
||||||
if (index === -1) {
|
|
||||||
// 点击最终视频
|
|
||||||
setSelectedView('final');
|
|
||||||
setCurrentSketchIndex(0);
|
|
||||||
} else {
|
|
||||||
// 点击普通视频
|
|
||||||
setSelectedView('video');
|
|
||||||
setCurrentSketchIndex(index);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onRetryVideo={handleRetryVideo}
|
|
||||||
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>
|
|
||||||
)}
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -49,6 +49,8 @@ interface H5MediaViewerProps {
|
|||||||
onVideoEditDescriptionSubmit?: (editPoint: any, description: string) => void;
|
onVideoEditDescriptionSubmit?: (editPoint: any, description: string) => void;
|
||||||
/** 项目ID */
|
/** 项目ID */
|
||||||
projectId?: string;
|
projectId?: string;
|
||||||
|
/** 视频比例 */
|
||||||
|
aspectRatio?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -76,7 +78,8 @@ export function H5MediaViewer({
|
|||||||
onSelectView,
|
onSelectView,
|
||||||
enableVideoEdit,
|
enableVideoEdit,
|
||||||
onVideoEditDescriptionSubmit,
|
onVideoEditDescriptionSubmit,
|
||||||
projectId
|
projectId,
|
||||||
|
aspectRatio
|
||||||
}: H5MediaViewerProps) {
|
}: H5MediaViewerProps) {
|
||||||
const carouselRef = useRef<CarouselRef>(null);
|
const carouselRef = useRef<CarouselRef>(null);
|
||||||
const videoRefs = useRef<Array<HTMLVideoElement | null>>([]);
|
const videoRefs = useRef<Array<HTMLVideoElement | null>>([]);
|
||||||
@ -85,10 +88,34 @@ export function H5MediaViewer({
|
|||||||
const [isPlaying, setIsPlaying] = useState<boolean>(false);
|
const [isPlaying, setIsPlaying] = useState<boolean>(false);
|
||||||
const [isCatalogOpen, setIsCatalogOpen] = useState<boolean>(false);
|
const [isCatalogOpen, setIsCatalogOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
|
/** 解析形如 "16:9" 的比例字符串 */
|
||||||
|
const parseAspect = (input?: string): { w: number; h: number } => {
|
||||||
|
const parts = (typeof input === 'string' ? input.split(':') : []);
|
||||||
|
const w = Number(parts[0]);
|
||||||
|
const h = Number(parts[1]);
|
||||||
|
return {
|
||||||
|
w: Number.isFinite(w) && w > 0 ? w : 16,
|
||||||
|
h: Number.isFinite(h) && h > 0 ? h : 9
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 视频轮播容器高度:按 aspectRatio 计算(基于视口宽度) */
|
||||||
|
const videoWrapperHeight = useMemo(() => {
|
||||||
|
const { w, h } = parseAspect(aspectRatio);
|
||||||
|
return `calc(100vw * ${h} / ${w})`;
|
||||||
|
}, [aspectRatio]);
|
||||||
|
|
||||||
|
/** 图片轮播容器高度:默认 16:9 */
|
||||||
|
const imageWrapperHeight = useMemo(() => {
|
||||||
|
// return 'calc(100vw * 9 / 16)';
|
||||||
|
const { w, h } = parseAspect(aspectRatio);
|
||||||
|
return `calc(100vw * ${h} / ${w})`;
|
||||||
|
}, [aspectRatio]);
|
||||||
|
|
||||||
// 计算当前阶段类型
|
// 计算当前阶段类型
|
||||||
const stage = (selectedView === 'final' && taskObject.final?.url)
|
const stage = (selectedView === 'final' && taskObject.final?.url)
|
||||||
? 'final_video'
|
? 'final_video'
|
||||||
: (!['script', 'character', 'scene'].includes(taskObject.currentStage) ? 'video' : taskObject.currentStage);
|
: (!['init', 'script', 'character', 'scene'].includes(taskObject.currentStage) ? 'video' : taskObject.currentStage);
|
||||||
|
|
||||||
// 生成各阶段对应的 slides 数据
|
// 生成各阶段对应的 slides 数据
|
||||||
const videoUrls = useMemo(() => {
|
const videoUrls = useMemo(() => {
|
||||||
@ -103,16 +130,19 @@ export function H5MediaViewer({
|
|||||||
return [];
|
return [];
|
||||||
}, [stage, taskObject.final?.url, taskObject.videos?.data]);
|
}, [stage, taskObject.final?.url, taskObject.videos?.data]);
|
||||||
|
|
||||||
const imageUrls = useMemo(() => {
|
const imageItems = useMemo(() => {
|
||||||
if (stage === 'scene' || stage === 'character') {
|
if (stage === 'scene' || stage === 'character') {
|
||||||
const roles = (taskObject.roles?.data ?? []) as Array<any>;
|
const roles = (taskObject.roles?.data ?? []) as Array<any>;
|
||||||
const scenes = (taskObject.scenes?.data ?? []) as Array<any>;
|
const scenes = (taskObject.scenes?.data ?? []) as Array<any>;
|
||||||
console.log('h5-media-viewer:stage', stage);
|
console.log('h5-media-viewer:stage', stage);
|
||||||
console.log('h5-media-viewer:roles', roles);
|
console.log('h5-media-viewer:roles', roles);
|
||||||
console.log('h5-media-viewer:scenes', scenes);
|
console.log('h5-media-viewer:scenes', scenes);
|
||||||
return [...roles, ...scenes].map(item => item?.url).filter(Boolean) as string[];
|
return [...roles, ...scenes].map(item => ({
|
||||||
|
url: item?.url as string | undefined,
|
||||||
|
status: item?.status as number | undefined,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
return [];
|
return [] as Array<{ url?: string; status?: number }>;
|
||||||
}, [stage, taskObject.roles?.data, taskObject.scenes?.data]);
|
}, [stage, taskObject.roles?.data, taskObject.scenes?.data]);
|
||||||
|
|
||||||
// 占位,避免未使用警告
|
// 占位,避免未使用警告
|
||||||
@ -164,7 +194,7 @@ export function H5MediaViewer({
|
|||||||
// 渲染视频 slide
|
// 渲染视频 slide
|
||||||
const renderVideoSlides = () => (
|
const renderVideoSlides = () => (
|
||||||
<div data-alt="carousel-wrapper" className="relative w-full aspect-auto min-h-[200px] overflow-hidden rounded-lg" style={{
|
<div data-alt="carousel-wrapper" className="relative w-full aspect-auto min-h-[200px] overflow-hidden rounded-lg" style={{
|
||||||
height: 'calc(100vh - 20rem)',
|
height: videoWrapperHeight,
|
||||||
}}>
|
}}>
|
||||||
<Carousel
|
<Carousel
|
||||||
ref={carouselRef}
|
ref={carouselRef}
|
||||||
@ -190,7 +220,7 @@ export function H5MediaViewer({
|
|||||||
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={{
|
style={{
|
||||||
maxHeight: 'calc(100vh - 20rem)',
|
maxHeight: '100%',
|
||||||
}}
|
}}
|
||||||
src={url}
|
src={url}
|
||||||
preload="metadata"
|
preload="metadata"
|
||||||
@ -224,7 +254,7 @@ export function H5MediaViewer({
|
|||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="w-full aspect-auto min-h-[200px] flex items-center justify-center bg-black/10 relative" data-alt="video-status" style={{
|
<div className="w-full aspect-auto min-h-[200px] flex items-center justify-center bg-black/10 relative" data-alt="video-status" style={{
|
||||||
height: 'calc(100vh - 20rem)',
|
height: videoWrapperHeight,
|
||||||
}}>
|
}}>
|
||||||
{status === 0 && (
|
{status === 0 && (
|
||||||
<span className="text-blue-500 text-base">Generating...</span>
|
<span className="text-blue-500 text-base">Generating...</span>
|
||||||
@ -250,11 +280,11 @@ export function H5MediaViewer({
|
|||||||
// 渲染图片 slide
|
// 渲染图片 slide
|
||||||
const renderImageSlides = () => (
|
const renderImageSlides = () => (
|
||||||
<div data-alt="carousel-wrapper" className="relative w-full aspect-auto min-h-[200px] overflow-hidden rounded-lg" style={{
|
<div data-alt="carousel-wrapper" className="relative w-full aspect-auto min-h-[200px] overflow-hidden rounded-lg" style={{
|
||||||
height: 'calc(100vh - 20rem)',
|
height: imageWrapperHeight,
|
||||||
}}>
|
}}>
|
||||||
<Carousel
|
<Carousel
|
||||||
ref={carouselRef}
|
ref={carouselRef}
|
||||||
key={`h5-carousel-image-${stage}-${imageUrls.length}`}
|
key={`h5-carousel-image-${stage}-${imageItems.length}`}
|
||||||
arrows
|
arrows
|
||||||
dots={false}
|
dots={false}
|
||||||
infinite={false}
|
infinite={false}
|
||||||
@ -263,13 +293,34 @@ export function H5MediaViewer({
|
|||||||
slidesToShow={1}
|
slidesToShow={1}
|
||||||
adaptiveHeight
|
adaptiveHeight
|
||||||
>
|
>
|
||||||
{imageUrls.map((url, idx) => (
|
{imageItems.map((item, idx) => {
|
||||||
<div key={`h5-image-${idx}`} data-alt="image-slide" className="relative w-full h-full flex justify-center">
|
const status = item?.status;
|
||||||
<img src={url} alt="scene" className="w-full h-full object-contain" style={{
|
const url = item?.url;
|
||||||
maxHeight: 'calc(100vh - 20rem)',
|
const showImage = status === 1 && typeof url === 'string' && url.length > 0;
|
||||||
}} />
|
return (
|
||||||
</div>
|
<div key={`h5-image-${idx}`} data-alt="image-slide" className="relative w-full h-full flex justify-center">
|
||||||
))}
|
{showImage ? (
|
||||||
|
<img src={url} alt="scene" className="w-full h-full object-contain" style={{
|
||||||
|
maxHeight: '100%',
|
||||||
|
}} />
|
||||||
|
) : (
|
||||||
|
<div className="w-full aspect-auto min-h-[200px] flex items-center justify-center bg-black/10 relative" data-alt="image-status" style={{
|
||||||
|
height: imageWrapperHeight,
|
||||||
|
}}>
|
||||||
|
{status === 0 && (
|
||||||
|
<span className="text-blue-500 text-base">Generating...</span>
|
||||||
|
)}
|
||||||
|
{status === 2 && (
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</Carousel>
|
</Carousel>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -285,7 +336,10 @@ export function H5MediaViewer({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<div data-alt="script-content" className="w-full h-full">
|
<div data-alt="script-content" className="w-full overflow-auto"
|
||||||
|
style={{
|
||||||
|
height: 'calc(100vh - 10rem)'
|
||||||
|
}}>
|
||||||
{scriptData ? (
|
{scriptData ? (
|
||||||
<>
|
<>
|
||||||
<ScriptRenderer
|
<ScriptRenderer
|
||||||
@ -370,13 +424,16 @@ export function H5MediaViewer({
|
|||||||
|
|
||||||
// 其他阶段:使用 Carousel
|
// 其他阶段:使用 Carousel
|
||||||
return (
|
return (
|
||||||
<div ref={rootRef} data-alt="h5-media-viewer" className={`w-[100vw] relative ${stage === 'final_video' ? '' : 'px-4'}`}>
|
<div ref={rootRef} data-alt="h5-media-viewer" className={`relative`}
|
||||||
|
style={{
|
||||||
|
width: 'calc(100vw - 2rem)'
|
||||||
|
}}>
|
||||||
{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') && imageItems.length > 0 && renderImageSlides()}
|
||||||
{/* 全局固定操作区(右下角)视频暂停时展示 */}
|
{/* 全局固定操作区(右下角)视频暂停时展示 */}
|
||||||
{(stage === 'video' || stage === 'final_video') && !isPlaying && (
|
{(stage === 'video' || stage === 'final_video') && !isPlaying && (
|
||||||
<div data-alt="global-video-actions" className="absolute bottom-0 right-4 z-[60] flex flex-col items-center gap-2">
|
<div data-alt="global-video-actions" className="absolute top-0 right-4 z-[60] flex flex-col items-center gap-2">
|
||||||
{stage === 'video' && (
|
{stage === 'video' && (
|
||||||
<>
|
<>
|
||||||
<GlassIconButton
|
<GlassIconButton
|
||||||
@ -471,10 +528,13 @@ export function H5MediaViewer({
|
|||||||
)}
|
)}
|
||||||
<style jsx global>{`
|
<style jsx global>{`
|
||||||
[data-alt='carousel-wrapper'] .slick-slide { display: flex !important;justify-content: center; }
|
[data-alt='carousel-wrapper'] .slick-slide { display: flex !important;justify-content: center; }
|
||||||
.slick-slider { height: 100% !important;display: flex !important; }
|
.slick-slider { height: 100% !important; }
|
||||||
.ant-carousel { height: 100% !important; }
|
.ant-carousel { height: 100% !important; }
|
||||||
.slick-list { width: 100%;height: 100% !important;max-height: calc(100vh - 20rem); }
|
.slick-list { width: 100%;height: 100% !important;max-height: 100%; }
|
||||||
.slick-track { display: flex !important; align-items: center;height: 100% !important; }
|
.slick-track { display: flex !important; align-items: center;height: 100% !important; }
|
||||||
|
[data-alt='carousel-wrapper'] .slick-arrow { z-index: 70 !important; }
|
||||||
|
[data-alt='carousel-wrapper'] .slick-prev { left: 8px; }
|
||||||
|
[data-alt='carousel-wrapper'] .slick-next { right: 8px; }
|
||||||
`}</style>
|
`}</style>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -15,6 +15,8 @@ import { downloadVideo, downloadAllVideos, getFirstFrame } from '@/utils/tools';
|
|||||||
import { VideoEditOverlay } from './video-edit/VideoEditOverlay';
|
import { VideoEditOverlay } from './video-edit/VideoEditOverlay';
|
||||||
import { EditPoint as EditPointType } from './video-edit/types';
|
import { EditPoint as EditPointType } from './video-edit/types';
|
||||||
import { isVideoModificationEnabled } from '@/lib/server-config';
|
import { isVideoModificationEnabled } from '@/lib/server-config';
|
||||||
|
import error_image from '@/public/assets/error.webp';
|
||||||
|
import { AspectRatioValue } from '@/components/ChatInputBox/AspectRatioSelector';
|
||||||
|
|
||||||
interface MediaViewerProps {
|
interface MediaViewerProps {
|
||||||
taskObject: TaskObject;
|
taskObject: TaskObject;
|
||||||
@ -38,6 +40,7 @@ interface MediaViewerProps {
|
|||||||
enableVideoEdit?: boolean;
|
enableVideoEdit?: boolean;
|
||||||
onVideoEditDescriptionSubmit?: (editPoint: EditPointType, description: string) => void;
|
onVideoEditDescriptionSubmit?: (editPoint: EditPointType, description: string) => void;
|
||||||
projectId?: string;
|
projectId?: string;
|
||||||
|
aspectRatio: AspectRatioValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MediaViewer = React.memo(function MediaViewer({
|
export const MediaViewer = React.memo(function MediaViewer({
|
||||||
@ -61,7 +64,8 @@ export const MediaViewer = React.memo(function MediaViewer({
|
|||||||
onRetryVideo,
|
onRetryVideo,
|
||||||
enableVideoEdit = true,
|
enableVideoEdit = true,
|
||||||
onVideoEditDescriptionSubmit,
|
onVideoEditDescriptionSubmit,
|
||||||
projectId
|
projectId,
|
||||||
|
aspectRatio
|
||||||
}: MediaViewerProps) {
|
}: MediaViewerProps) {
|
||||||
const mainVideoRef = useRef<HTMLVideoElement>(null);
|
const mainVideoRef = useRef<HTMLVideoElement>(null);
|
||||||
const finalVideoRef = useRef<HTMLVideoElement>(null);
|
const finalVideoRef = useRef<HTMLVideoElement>(null);
|
||||||
@ -212,6 +216,9 @@ export const MediaViewer = React.memo(function MediaViewer({
|
|||||||
<video
|
<video
|
||||||
ref={finalVideoRef}
|
ref={finalVideoRef}
|
||||||
className="w-full h-full object-contain rounded-lg"
|
className="w-full h-full object-contain rounded-lg"
|
||||||
|
style={{
|
||||||
|
aspectRatio: aspectRatio === 'VIDEO_ASPECT_RATIO_LANDSCAPE' ? "16 / 9" : "9 / 16"
|
||||||
|
}}
|
||||||
src={taskObject.final.url}
|
src={taskObject.final.url}
|
||||||
autoPlay={isFinalVideoPlaying}
|
autoPlay={isFinalVideoPlaying}
|
||||||
loop
|
loop
|
||||||
@ -396,47 +403,14 @@ export const MediaViewer = React.memo(function MediaViewer({
|
|||||||
playing: boolean
|
playing: boolean
|
||||||
) => (
|
) => (
|
||||||
<div
|
<div
|
||||||
className="absolute left-0 right-0 bottom-2 z-[21] px-6"
|
className="absolute left-0 right-0 bottom-4 z-[21] px-2"
|
||||||
data-alt={isFinal ? 'final-controls' : 'video-controls'}
|
data-alt={isFinal ? 'final-controls' : 'video-controls'}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center justify-between">
|
||||||
{/* 播放/暂停 */}
|
{/* 播放/暂停 */}
|
||||||
<GlassIconButton icon={playing ? Pause : Play} onClick={onPlayToggle} size="sm" />
|
<GlassIconButton icon={playing ? Pause : Play} onClick={onPlayToggle} size="sm" />
|
||||||
|
|
||||||
{/* 静音,仅图标 */}
|
|
||||||
<GlassIconButton icon={isMuted ? VolumeX : Volume2} onClick={toggleMute} size="sm" />
|
|
||||||
|
|
||||||
{/* 进度条 */}
|
|
||||||
<div className="flex-1 flex items-center">
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min="0"
|
|
||||||
max="100"
|
|
||||||
step="0.1"
|
|
||||||
value={progressPercent}
|
|
||||||
onChange={(e) => seekTo(parseFloat(e.target.value))}
|
|
||||||
className="w-full h-1 bg-white/20 rounded-lg appearance-none cursor-pointer
|
|
||||||
[&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:h-3
|
|
||||||
[&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-white
|
|
||||||
[&::-webkit-slider-thumb]:cursor-pointer
|
|
||||||
[&::-moz-range-thumb]:w-3 [&::-moz-range-thumb]:h-3 [&::-moz-range-thumb]:rounded-full
|
|
||||||
[&::-moz-range-thumb]:bg-white [&::-moz-range-thumb]:cursor-pointer [&::-moz-range-thumb]:border-none"
|
|
||||||
style={{
|
|
||||||
background: `linear-gradient(to right, white 0%, white ${progressPercent}%, rgba(255,255,255,0.2) ${progressPercent}%, rgba(255,255,255,0.2) 100%)`
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 剩余时间 */}
|
|
||||||
<div className="text-white/80 text-sm w-14 text-right select-none" data-alt="time-remaining">
|
|
||||||
{formatRemaining(duration, currentTime)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 画中画 */}
|
|
||||||
<GlassIconButton icon={PictureInPicture2} onClick={requestPip} size="sm" />
|
|
||||||
|
|
||||||
{/* 全屏 */}
|
{/* 全屏 */}
|
||||||
<GlassIconButton icon={isFullscreen ? Minimize : Maximize} onClick={toggleFullscreen} size="sm" />
|
<GlassIconButton className="group-hover:block hidden animate-in duration-300" icon={isFullscreen ? Minimize : Maximize} onClick={toggleFullscreen} size="sm" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -456,11 +430,14 @@ export const MediaViewer = React.memo(function MediaViewer({
|
|||||||
<motion.div
|
<motion.div
|
||||||
className="absolute inset-0 overflow-hidden"
|
className="absolute inset-0 overflow-hidden"
|
||||||
initial={{ filter: "blur(0px)", scale: 1, opacity: 1 }}
|
initial={{ filter: "blur(0px)", scale: 1, opacity: 1 }}
|
||||||
animate={{ filter: "blur(20px)", scale: 1.1, opacity: 0.5 }}
|
animate={{ filter: "blur(20px)", scale: 1, opacity: 0.5 }}
|
||||||
transition={{ duration: 0.8, ease: "easeInOut" }}
|
transition={{ duration: 0.8, ease: "easeInOut" }}
|
||||||
>
|
>
|
||||||
<video
|
<video
|
||||||
className="w-full h-full rounded-lg object-contain object-center"
|
className="w-full h-full rounded-lg object-contain object-center"
|
||||||
|
style={{
|
||||||
|
aspectRatio: aspectRatio === 'VIDEO_ASPECT_RATIO_LANDSCAPE' ? "16 / 9" : "9 / 16"
|
||||||
|
}}
|
||||||
src={taskObject.final.url}
|
src={taskObject.final.url}
|
||||||
loop
|
loop
|
||||||
playsInline
|
playsInline
|
||||||
@ -484,8 +461,9 @@ export const MediaViewer = React.memo(function MediaViewer({
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* 编辑和剪辑按钮 */}
|
{/* 编辑和剪辑按钮 */}
|
||||||
<div className="absolute top-4 z-[21] flex items-center gap-2 transition-right duration-100" style={{
|
<div className="absolute top-2 right-2 z-[21] flex items-center gap-2 transition-right duration-100"
|
||||||
right: toosBtnRight
|
style={{
|
||||||
|
right: aspectRatio === 'VIDEO_ASPECT_RATIO_LANDSCAPE' ? toosBtnRight : ''
|
||||||
}}>
|
}}>
|
||||||
<Tooltip placement="top" title='Edit'>
|
<Tooltip placement="top" title='Edit'>
|
||||||
<GlassIconButton
|
<GlassIconButton
|
||||||
@ -535,6 +513,9 @@ export const MediaViewer = React.memo(function MediaViewer({
|
|||||||
className="relative w-full h-full rounded-lg group"
|
className="relative w-full h-full rounded-lg group"
|
||||||
key={`render-video-${urls}`}
|
key={`render-video-${urls}`}
|
||||||
ref={videoContentRef}
|
ref={videoContentRef}
|
||||||
|
style={{
|
||||||
|
width: taskObject.videos.data[currentSketchIndex].video_status !== 1 ? 'calc(100vw - 8rem)' : '100%'
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{/* 背景模糊的图片 */}
|
{/* 背景模糊的图片 */}
|
||||||
{taskObject.videos.data[currentSketchIndex].video_status !== 1 && (
|
{taskObject.videos.data[currentSketchIndex].video_status !== 1 && (
|
||||||
@ -580,6 +561,9 @@ export const MediaViewer = React.memo(function MediaViewer({
|
|||||||
ref={mainVideoRef}
|
ref={mainVideoRef}
|
||||||
key={taskObject.videos.data[currentSketchIndex].urls[0]}
|
key={taskObject.videos.data[currentSketchIndex].urls[0]}
|
||||||
className="w-full h-full rounded-lg object-contain object-center relative z-10"
|
className="w-full h-full rounded-lg object-contain object-center relative z-10"
|
||||||
|
style={{
|
||||||
|
aspectRatio: aspectRatio === 'VIDEO_ASPECT_RATIO_LANDSCAPE' ? "16 / 9" : "9 / 16"
|
||||||
|
}}
|
||||||
src={taskObject.videos.data[currentSketchIndex].urls[0]}
|
src={taskObject.videos.data[currentSketchIndex].urls[0]}
|
||||||
poster={getFirstFrame(taskObject.videos.data[currentSketchIndex].urls[0])}
|
poster={getFirstFrame(taskObject.videos.data[currentSketchIndex].urls[0])}
|
||||||
preload="none"
|
preload="none"
|
||||||
@ -612,8 +596,8 @@ export const MediaViewer = React.memo(function MediaViewer({
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* 跳转剪辑按钮 */}
|
{/* 跳转剪辑按钮 */}
|
||||||
<div className="absolute top-4 right-4 z-[21] flex items-center gap-2 transition-right duration-100" style={{
|
<div className="absolute top-2 right-2 z-[21] flex items-center gap-2 transition-right duration-100" style={{
|
||||||
right: toosBtnRight
|
right: aspectRatio === 'VIDEO_ASPECT_RATIO_LANDSCAPE' ? toosBtnRight : ''
|
||||||
}}>
|
}}>
|
||||||
{/* 视频编辑模式切换按钮 - 通过服务器配置控制显示 */}
|
{/* 视频编辑模式切换按钮 - 通过服务器配置控制显示 */}
|
||||||
{enableVideoEdit && showVideoModification && (
|
{enableVideoEdit && showVideoModification && (
|
||||||
@ -702,8 +686,11 @@ export const MediaViewer = React.memo(function MediaViewer({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="relative w-full h-full rounded-lg group"
|
className="relative w-full h-full rounded-lg group overflow-hidden"
|
||||||
key={`render-sketch-${currentSketch.url}`}
|
key={`render-sketch-${currentSketch.url}`}
|
||||||
|
style={{
|
||||||
|
width: currentSketch.status === 1 ? '100%' : 'calc(100vw - 8rem)'
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{/* 状态 */}
|
{/* 状态 */}
|
||||||
{currentSketch.status === 0 && (
|
{currentSketch.status === 0 && (
|
||||||
@ -715,8 +702,8 @@ export const MediaViewer = React.memo(function MediaViewer({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{currentSketch.status === 2 && (
|
{currentSketch.status === 2 && (
|
||||||
<div className="absolute inset-0 bg-red-500/5 flex items-center justify-center">
|
<div className="absolute inset-0 bg-[#fcb0ba1a] flex items-center justify-center">
|
||||||
<div className="text-2xl mb-4">⚠️</div>
|
<img src={error_image.src} alt="error" className="w-12 h-12" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* 只在生成过程中或没有分镜图片时使用ProgressiveReveal */}
|
{/* 只在生成过程中或没有分镜图片时使用ProgressiveReveal */}
|
||||||
@ -785,7 +772,10 @@ export const MediaViewer = React.memo(function MediaViewer({
|
|||||||
// 渲染剧本
|
// 渲染剧本
|
||||||
const renderScriptContent = () => {
|
const renderScriptContent = () => {
|
||||||
return (
|
return (
|
||||||
<div className="relative w-full h-full rounded-lg overflow-hidden p-2">
|
<div className="relative h-full rounded-lg overflow-hidden p-2"
|
||||||
|
style={{
|
||||||
|
width: 'calc(100vw - 8rem)'
|
||||||
|
}}>
|
||||||
{scriptData ? (
|
{scriptData ? (
|
||||||
<ScriptRenderer
|
<ScriptRenderer
|
||||||
data={scriptData}
|
data={scriptData}
|
||||||
|
|||||||
@ -220,23 +220,6 @@ export function TaskInfo({
|
|||||||
) : currentLoadingText}
|
) : currentLoadingText}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 主题 彩色标签tags */}
|
|
||||||
<div className="flex items-center justify-center gap-2">
|
|
||||||
{taskObject?.tags?.map((tag: string) => (
|
|
||||||
<div
|
|
||||||
key={tag}
|
|
||||||
data-alt="tag-item"
|
|
||||||
className="flex items-center gap-2 text-sm text-[#ececec] rounded-full px-3 py-1.5 bg-white/10 backdrop-blur-sm shadow-[0_4px_12px_rgba(0,0,0,0.2)]"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="w-2 h-2 rounded-full"
|
|
||||||
style={{ backgroundColor: tagColors[tag] }}
|
|
||||||
/>
|
|
||||||
{tag}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ScriptModal
|
<ScriptModal
|
||||||
isOpen={isScriptModalOpen}
|
isOpen={isScriptModalOpen}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
|
|||||||
@ -1,13 +1,11 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useRef, useEffect, useState, useCallback } from 'react';
|
import React, { useRef, useEffect, useState, useCallback } from 'react';
|
||||||
import { motion } from 'framer-motion';
|
import { CircleAlert, Film } from 'lucide-react';
|
||||||
import { Skeleton } from '@/components/ui/skeleton';
|
|
||||||
import { ProgressiveReveal, presets } from '@/components/ui/progressive-reveal';
|
|
||||||
import { Loader2, X, SquareUserRound, MapPinHouse, Clapperboard, Video, RotateCcw, CircleAlert, Film } from 'lucide-react';
|
|
||||||
import { TaskObject } from '@/api/DTO/movieEdit';
|
import { TaskObject } from '@/api/DTO/movieEdit';
|
||||||
import { getFirstFrame } from '@/utils/tools';
|
import { getFirstFrame } from '@/utils/tools';
|
||||||
import { AspectRatioValue } from '@/components/ChatInputBox/AspectRatioSelector';
|
import { AspectRatioValue } from '@/components/ChatInputBox/AspectRatioSelector';
|
||||||
|
import error_image from '@/public/assets/error.webp';
|
||||||
|
|
||||||
interface ThumbnailGridProps {
|
interface ThumbnailGridProps {
|
||||||
isDisabledFocus: boolean;
|
isDisabledFocus: boolean;
|
||||||
@ -93,37 +91,43 @@ export function ThumbnailGrid({
|
|||||||
return [];
|
return [];
|
||||||
}, [taskObject.currentStage, taskObject.videos.data, taskObject.roles.data, taskObject.scenes.data]);
|
}, [taskObject.currentStage, taskObject.videos.data, taskObject.roles.data, taskObject.scenes.data]);
|
||||||
|
|
||||||
// 使用 useRef 存储前一次的数据,避免触发重渲染
|
/** Store previous status snapshot for change detection */
|
||||||
const prevDataRef = useRef<any[]>([]);
|
const prevStatusRef = useRef<Array<number | undefined>>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const currentData = getCurrentData();
|
const currentData = getCurrentData();
|
||||||
if (currentData && currentData.length > 0) {
|
if (!currentData || currentData.length === 0) return;
|
||||||
const currentDataStr = JSON.stringify(currentData);
|
|
||||||
const prevDataStr = JSON.stringify(prevDataRef.current);
|
|
||||||
|
|
||||||
// 只有当数据真正发生变化时才进行处理
|
// Extract status fields only to detect meaningful changes
|
||||||
if (currentDataStr !== prevDataStr) {
|
const currentStatuses: Array<number | undefined> = currentData.map((item: any) => (
|
||||||
// 找到最新更新的数据项的索引
|
taskObject.currentStage === 'video' ? item?.video_status : item?.status
|
||||||
const changedIndex = currentData.findIndex((item, index) => {
|
));
|
||||||
// 检查是否是新增的数据
|
|
||||||
if (index >= prevDataRef.current.length) return true;
|
|
||||||
// 检查数据是否发生变化(包括状态变化)
|
|
||||||
return JSON.stringify(item) !== JSON.stringify(prevDataRef.current[index]);
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('changedIndex_thumbnail-grid', changedIndex, 'currentData:', currentData, 'prevData:', prevDataRef.current);
|
const prevStatuses = prevStatusRef.current;
|
||||||
|
|
||||||
// 如果找到变化的项,自动选择该项
|
// Find first changed or newly added index
|
||||||
if (changedIndex !== -1) {
|
let changedIndex = -1;
|
||||||
onSketchSelect(changedIndex);
|
for (let i = 0; i < currentStatuses.length; i += 1) {
|
||||||
}
|
if (i >= prevStatuses.length) {
|
||||||
|
changedIndex = i; // new item
|
||||||
// 更新前一次的数据快照
|
break;
|
||||||
prevDataRef.current = JSON.parse(JSON.stringify(currentData));
|
}
|
||||||
|
if (currentStatuses[i] !== prevStatuses[i]) {
|
||||||
|
changedIndex = i; // status changed
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [taskObject, getCurrentData, onSketchSelect]);
|
|
||||||
|
console.log('changedIndex_thumbnail-grid', changedIndex);
|
||||||
|
|
||||||
|
|
||||||
|
if (changedIndex !== -1) {
|
||||||
|
onSketchSelect(changedIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update snapshot
|
||||||
|
prevStatusRef.current = currentStatuses.slice();
|
||||||
|
}, [taskObject, getCurrentData]);
|
||||||
|
|
||||||
// 处理键盘左右键事件
|
// 处理键盘左右键事件
|
||||||
const handleKeyDown = useCallback((e: KeyboardEvent) => {
|
const handleKeyDown = useCallback((e: KeyboardEvent) => {
|
||||||
@ -192,49 +196,45 @@ export function ThumbnailGrid({
|
|||||||
{taskObject?.final?.url && (
|
{taskObject?.final?.url && (
|
||||||
<div
|
<div
|
||||||
key="video-final"
|
key="video-final"
|
||||||
className={`relative aspect-auto rounded-lg overflow-hidden
|
data-alt="final-thumbnail"
|
||||||
${selectedView === 'final' ? 'ring-2 ring-amber-500 z-10' : 'hover:ring-2 hover:ring-amber-500/50'}
|
className={`relative w-8 h-8 shrink-0 hover:brightness-50 rounded-full duration-300 transition-all ring-offset-surface-tertiary ring-white size-8
|
||||||
${aspectRatio === 'VIDEO_ASPECT_RATIO_LANDSCAPE' ? `min-w-[${cols}]` : 'min-w-[70px]'}
|
${selectedView === 'final' ? '!w-16 !h-10 ring-2 ring-white ring-offset-2 ring-offset-neutral-900 shadow-[0_0_0_3px_rgba(255,255,255,0.6)] z-10' : 'ring-1 ring-white/20 hover:ring-2 hover:ring-white/60'}
|
||||||
`}
|
`}
|
||||||
onClick={() => !isDragging && !disabled && onSketchSelect(-1)}
|
onClick={() => !isDragging && !disabled && onSketchSelect(-1)}
|
||||||
>
|
>
|
||||||
{/* 视频层 */}
|
<div className="rounded-full overflow-hidden w-full h-full">
|
||||||
<div className="relative w-full h-full transform hover:scale-105 transition-transform duration-500">
|
{/* 视频层 */}
|
||||||
<div
|
<div className="relative w-full h-full transform hover:scale-105 transition-transform duration-300">
|
||||||
className="w-full h-full relative"
|
<div
|
||||||
onMouseEnter={() => handleMouseEnter(-1)}
|
className="w-full h-full relative"
|
||||||
onMouseLeave={() => handleMouseLeave(-1)}
|
onMouseEnter={() => handleMouseEnter(-1)}
|
||||||
>
|
onMouseLeave={() => handleMouseLeave(-1)}
|
||||||
<img
|
>
|
||||||
className="w-full h-full object-contain"
|
<img
|
||||||
src={getFirstFrame(taskObject.final.url)}
|
className="w-full h-full object-cover"
|
||||||
draggable="false"
|
src={getFirstFrame(taskObject.final.url)}
|
||||||
alt="final video thumbnail"
|
draggable="false"
|
||||||
/>
|
alt="final video thumbnail"
|
||||||
{hoveredIndex === -1 && (
|
|
||||||
<video
|
|
||||||
className="absolute inset-0 w-full h-full object-contain"
|
|
||||||
src={taskObject.final.url}
|
|
||||||
autoPlay
|
|
||||||
muted
|
|
||||||
playsInline
|
|
||||||
loop
|
|
||||||
poster={getFirstFrame(taskObject.final.url)}
|
|
||||||
preload="none"
|
|
||||||
/>
|
/>
|
||||||
)}
|
{hoveredIndex === -1 && (
|
||||||
|
<video
|
||||||
|
className="absolute inset-0 w-full h-full object-cover"
|
||||||
|
src={taskObject.final.url}
|
||||||
|
autoPlay
|
||||||
|
muted
|
||||||
|
playsInline
|
||||||
|
loop
|
||||||
|
poster={getFirstFrame(taskObject.final.url)}
|
||||||
|
preload="none"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/* 最终视频徽标 */}
|
||||||
{/* 左上角三角标签 */}
|
<div className="absolute -top-1 -left-1 z-20">
|
||||||
<div className='absolute top-0 left-0 z-20'>
|
<div className="w-4 h-4 rounded-full bg-amber-500/60 flex items-center justify-center">
|
||||||
<div className="relative w-12 h-12">
|
<Film className="w-2.5 h-2.5 text-white" />
|
||||||
{/* 三角形背景 */}
|
|
||||||
<div className="absolute top-0 left-0 w-0 h-0 border-t-[3rem] border-t-amber-400/60 border-r-[3rem] border-r-transparent" />
|
|
||||||
{/* 图标 */}
|
|
||||||
<div className="absolute top-1 left-1 flex flex-col items-center gap-0.5">
|
|
||||||
<Film className="w-3.5 h-3.5 text-white" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -247,25 +247,27 @@ export function ThumbnailGrid({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={`video-${urls}-${index}`}
|
key={`video-${urls}-${index}`}
|
||||||
className={`relative aspect-auto rounded-lg overflow-hidden
|
data-alt={`video-thumbnail-${index+1}`}
|
||||||
${(currentSketchIndex === index && !disabled && selectedView !== 'final') ? 'ring-2 ring-blue-500 z-10' : 'hover:ring-2 hover:ring-blue-500/50'}
|
className={`relative overflow-hidden w-8 h-8 shrink-0 rounded-full duration-300 transition-all ring-offset-surface-tertiary ring-white size-8
|
||||||
${aspectRatio === 'VIDEO_ASPECT_RATIO_LANDSCAPE' ? `min-w-[${cols}]` : 'min-w-[70px]'}
|
${(currentSketchIndex === index && !disabled && selectedView !== 'final') ? '!w-16 !h-10 ring-2 ring-white ring-offset-2 ring-offset-neutral-900 shadow-[0_0_0_3px_rgba(255,255,255,0.6)] z-10' : 'ring-1 ring-white/20 hover:ring-2 hover:ring-white/60'}
|
||||||
`}
|
`}
|
||||||
onClick={() => !isDragging && !disabled && onSketchSelect(index)}
|
onClick={() => !isDragging && !disabled && onSketchSelect(index)}
|
||||||
>
|
>
|
||||||
|
|
||||||
{/* 视频层 */}
|
{/* 视频层 */}
|
||||||
<div className="relative w-full h-full transform hover:scale-105 transition-transform duration-500">
|
<div className="relative w-full h-full transform hover:scale-105 transition-transform duration-300">
|
||||||
{taskObject.videos.data[index].video_status === 0 && (
|
{taskObject.videos.data[index].video_status === 0 && (
|
||||||
<div className="absolute inset-0 bg-black/10 flex items-center justify-center z-20">
|
<div className="absolute inset-0 flex items-center justify-center z-20">
|
||||||
<div className="text-blue-500 text-xl font-bold flex items-center gap-2">
|
<div className="relative size-12">
|
||||||
<Loader2 className="w-10 h-10 animate-spin" />
|
<span className="absolute inset-0 rounded-full border-2 border-white/40 animate-ping" />
|
||||||
|
<span className="absolute inset-2 rounded-full border-2 border-white/20 animate-ping [animation-delay:150ms]" />
|
||||||
|
<span className="absolute inset-0 rounded-full bg-white/5" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{taskObject.videos.data[index].video_status === 2 && (
|
{taskObject.videos.data[index].video_status === 2 && (
|
||||||
<div className="absolute inset-0 bg-red-500/5 flex items-center justify-center z-20">
|
<div className="absolute inset-0 bg-red-500/10 flex items-center justify-center z-20">
|
||||||
<div className="text-2xl mb-4">⚠️</div>
|
<img src={error_image.src} alt="error" className="w-6 h-6" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -283,14 +285,14 @@ export function ThumbnailGrid({
|
|||||||
onMouseLeave={() => handleMouseLeave(index)}
|
onMouseLeave={() => handleMouseLeave(index)}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
className="w-full h-full object-contain"
|
className="w-full h-full object-cover"
|
||||||
src={getFirstFrame(taskObject.videos.data[index].urls[0])}
|
src={getFirstFrame(taskObject.videos.data[index].urls[0])}
|
||||||
draggable="false"
|
draggable="false"
|
||||||
alt="video thumbnail"
|
alt="video thumbnail"
|
||||||
/>
|
/>
|
||||||
{hoveredIndex === index && (
|
{hoveredIndex === index && (
|
||||||
<video
|
<video
|
||||||
className="absolute inset-0 w-full h-full object-contain"
|
className="absolute inset-0 w-full h-full object-cover"
|
||||||
src={taskObject.videos.data[index].urls[0]}
|
src={taskObject.videos.data[index].urls[0]}
|
||||||
autoPlay
|
autoPlay
|
||||||
muted
|
muted
|
||||||
@ -302,27 +304,10 @@ export function ThumbnailGrid({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="w-full h-full transform hover:scale-105 transition-transform duration-500">
|
<div className="w-full h-full" />
|
||||||
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!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 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>
|
|
||||||
</div> */}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@ -337,63 +322,40 @@ export function ThumbnailGrid({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={sketch.uniqueId || `sketch-${sketch.url}-${index}`}
|
key={sketch.uniqueId || `sketch-${sketch.url}-${index}`}
|
||||||
className={`relative aspect-auto rounded-lg overflow-hidden
|
data-alt={`sketch-thumbnail-${index+1}`}
|
||||||
${currentSketchIndex === index ? 'ring-2 ring-blue-500 z-10' : 'hover:ring-2 hover:ring-blue-500/50'}
|
className={`relative overflow-hidden w-8 h-8 shrink-0 hover:brightness-50 rounded-full duration-300 transition-all ring-offset-surface-tertiary ring-white size-8
|
||||||
${aspectRatio === 'VIDEO_ASPECT_RATIO_LANDSCAPE' ? `min-w-[${cols}]` : 'min-w-[70px]'}
|
${currentSketchIndex === index ? '!w-16 !h-10 ring-2 ring-white ring-offset-2 ring-offset-neutral-900 shadow-[0_0_0_3px_rgba(255,255,255,0.6)] z-10' : 'ring-1 ring-white/20 hover:ring-2 hover:ring-white/60'}
|
||||||
`}
|
`}
|
||||||
onClick={() => !isDragging && onSketchSelect(index)}
|
onClick={() => !isDragging && onSketchSelect(index)}
|
||||||
>
|
>
|
||||||
|
|
||||||
{/* 状态 */}
|
{/* 状态 */}
|
||||||
{sketch.status === 0 && (
|
{sketch.status === 0 && (
|
||||||
<div className="absolute inset-0 bg-black/10 flex items-center justify-center">
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
<div className="text-blue-500 text-xl font-bold flex items-center gap-2">
|
<div className="relative size-12">
|
||||||
<Loader2 className="w-10 h-10 animate-spin" />
|
<span className="absolute inset-0 rounded-full border-2 border-white/40 animate-ping" />
|
||||||
|
<span className="absolute inset-2 rounded-full border-2 border-white/20 animate-ping [animation-delay:150ms]" />
|
||||||
|
<span className="absolute inset-0 rounded-full bg-white/5" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{sketch.status === 2 && (
|
{sketch.status === 2 && (
|
||||||
<div className="absolute inset-0 bg-red-500/5 flex items-center justify-center z-20">
|
<div className="absolute inset-0 bg-red-500/10 flex items-center justify-center z-20">
|
||||||
<div className="text-2xl mb-4">⚠️</div>
|
<img src={error_image.src} alt="error" className="w-6 h-6" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* 只在生成过程中或没有分镜图片时使用ProgressiveReveal */}
|
{/* 只在生成过程中或没有分镜图片时使用ProgressiveReveal */}
|
||||||
{(sketch.status === 1) && (
|
{(sketch.status === 1) && (
|
||||||
<div className="w-full h-full transform hover:scale-105 transition-transform duration-500">
|
<div className="w-full h-full transform hover:scale-105 transition-transform duration-300">
|
||||||
<img
|
<img
|
||||||
className="w-full h-full object-contain select-none"
|
className="w-full h-full object-cover select-none"
|
||||||
src={sketch.url}
|
src={sketch.url}
|
||||||
draggable="false"
|
draggable="false"
|
||||||
alt={sketch.type ? String(sketch.type) : 'sketch'}
|
alt={sketch.type ? String(sketch.type) : 'sketch'}
|
||||||
/>
|
/>
|
||||||
</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">
|
{/* <div className="absolute bottom-0 left-0 right-0 p-2 bg-gradient-to-t from-black/60 to-transparent z-10">
|
||||||
@ -409,7 +371,8 @@ 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 auto-cols-[${cols}] ${aspectRatio === 'VIDEO_ASPECT_RATIO_LANDSCAPE' ? '' : '!auto-cols-min'}`}
|
data-alt="thumbnail-strip"
|
||||||
|
className={`w-full h-full grid grid-flow-col items-center gap-2 px-3 overflow-x-auto hide-scrollbar cursor-grab active:cursor-grabbing focus:outline-none select-none rounded-full ring-1 ring-white/10 shadow-inner backdrop-blur-md bg-white/10 auto-cols-[${cols}] !auto-cols-min border border-white/20`}
|
||||||
autoFocus
|
autoFocus
|
||||||
onMouseDown={handleMouseDown}
|
onMouseDown={handleMouseDown}
|
||||||
onMouseMove={handleMouseMove}
|
onMouseMove={handleMouseMove}
|
||||||
|
|||||||
BIN
public/assets/error.webp
Normal file
BIN
public/assets/error.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
Loading…
x
Reference in New Issue
Block a user