调整按钮位置

This commit is contained in:
北枳 2025-08-30 23:22:05 +08:00
parent cf79fae40e
commit fef2df1643
4 changed files with 137 additions and 146 deletions

View File

@ -8,13 +8,13 @@ import { MediaViewer } from "./work-flow/media-viewer";
import { ThumbnailGrid } from "./work-flow/thumbnail-grid";
import { useWorkflowData } from "./work-flow/use-workflow-data";
import { usePlaybackControls } from "./work-flow/use-playback-controls";
import { AlertCircle, RefreshCw, Pause, Play, ChevronLast, MessageSquareText } from "lucide-react";
import { AlertCircle, RefreshCw, Pause, Play, ChevronLast, ChevronsLeft, Bot, BriefcaseBusiness, Scissors } from "lucide-react";
import { motion } from "framer-motion";
import { GlassIconButton } from '@/components/ui/glass-icon-button';
import { SaveEditUseCase } from "@/app/service/usecase/SaveEditUseCase";
import { useSearchParams } from "next/navigation";
import SmartChatBox from "@/components/SmartChatBox/SmartChatBox";
import { Drawer } from 'antd';
import { Drawer, Tooltip } from 'antd';
const WorkFlow = React.memo(function WorkFlow() {
useEffect(() => {
@ -28,6 +28,7 @@ const WorkFlow = React.memo(function WorkFlow() {
const [previewVideoUrl, setPreviewVideoUrl] = React.useState<string | null>(null);
const [previewVideoId, setPreviewVideoId] = React.useState<string | null>(null);
const [isFocusChatInput, setIsFocusChatInput] = React.useState(false);
const [isHovered, setIsHovered] = React.useState(false);
const searchParams = useSearchParams();
const episodeId = searchParams.get('episodeId') || '';
@ -92,6 +93,7 @@ const WorkFlow = React.memo(function WorkFlow() {
isPauseWorkFlow={isPauseWorkFlow}
showGotoCutButton={showGotoCutButton}
onGotoCut={generateEditPlan}
setIsPauseWorkFlow={setIsPauseWorkFlow}
/>
</div>
</div>
@ -100,41 +102,10 @@ const WorkFlow = React.memo(function WorkFlow() {
className="videoContainer-qteKNi"
ref={containerRef}
>
{dataLoadError ? (
<motion.div
className="flex flex-col items-center justify-center w-full aspect-video rounded-lg bg-red-50 border-2 border-red-200"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
<motion.div
className="flex items-center gap-3 mb-4"
initial={{ scale: 0.8 }}
animate={{ scale: 1 }}
transition={{ duration: 0.3, delay: 0.2 }}
>
<AlertCircle className="w-8 h-8 text-red-500" />
<h3 className="text-lg font-medium text-red-800"></h3>
</motion.div>
<p className="text-red-600 text-center mb-6 max-w-md px-4">
{dataLoadError}
</p>
<motion.button
className="flex items-center gap-2 px-6 py-3 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors"
onClick={() => retryLoadData?.()}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<RefreshCw className="w-4 h-4" />
</motion.button>
</motion.div>
) : isLoading ? (
{isLoading ? (
<Skeleton className="w-full aspect-video rounded-lg" />
) : (
<div className={`heroVideo-FIzuK1 ${['final_video', '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 ${['final_video', '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}>
<MediaViewer
taskObject={taskObject}
scriptData={scriptData}
@ -152,6 +123,9 @@ const WorkFlow = React.memo(function WorkFlow() {
setPreviewVideoUrl(url);
setPreviewVideoId(id);
}}
showGotoCutButton={showGotoCutButton}
onGotoCut={generateEditPlan}
isSmartChatBoxOpen={isSmartChatBoxOpen}
/>
</div>
)}
@ -171,35 +145,18 @@ const WorkFlow = React.memo(function WorkFlow() {
</div>
</div>
{/* 暂停/播放按钮 */}
{
(taskObject.currentStage !== 'final_video') && (
<div className="absolute right-12 bottom-16 z-[49] flex gap-4">
<GlassIconButton
icon={isPauseWorkFlow ? Play : Pause}
size='md'
tooltip={isPauseWorkFlow ? "Play" : "Pause"}
onClick={() => setIsPauseWorkFlow(!isPauseWorkFlow)}
/>
{ !mode.includes('auto') && (
<GlassIconButton
icon={ChevronLast}
size='md'
tooltip="Next"
/>
)}
</div>
)
}
{/* 智能对话按钮 */}
<div className="absolute right-12 bottom-32 z-[49] flex gap-4">
<GlassIconButton
icon={MessageSquareText}
size='md'
tooltip={"Chat"}
onClick={() => setIsSmartChatBoxOpen(true)}
/>
<div
className="fixed right-[2rem] top-[4rem] z-[49]"
>
<Tooltip title="Open chat" placement="left">
<GlassIconButton
icon={Bot}
size='md'
onClick={() => setIsSmartChatBoxOpen(true)}
className="backdrop-blur-lg"
/>
</Tooltip>
</div>
{/* 智能对话弹窗 */}

View File

@ -2,7 +2,7 @@
import React, { useRef, useEffect, useState, SetStateAction, useMemo } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Edit3, Play, Pause, Volume2, VolumeX, Maximize, Minimize, Loader2, X } from 'lucide-react';
import { Edit3, Play, Pause, Volume2, VolumeX, Maximize, Minimize, Loader2, X, Scissors } from 'lucide-react';
import { ProgressiveReveal, presets } from '@/components/ui/progressive-reveal';
import { GlassIconButton } from '@/components/ui/glass-icon-button';
import { ScriptRenderer } from '@/components/script-renderer/ScriptRenderer';
@ -26,6 +26,9 @@ interface MediaViewerProps {
mode: string;
onOpenChat?: () => void;
setVideoPreview?: (url: string, id: string) => void;
showGotoCutButton?: boolean;
onGotoCut: () => void;
isSmartChatBoxOpen: boolean;
}
export const MediaViewer = React.memo(function MediaViewer({
@ -41,11 +44,14 @@ export const MediaViewer = React.memo(function MediaViewer({
applyScript,
mode,
onOpenChat,
setVideoPreview
setVideoPreview,
showGotoCutButton,
onGotoCut,
isSmartChatBoxOpen
}: MediaViewerProps) {
const mainVideoRef = useRef<HTMLVideoElement>(null);
const finalVideoRef = useRef<HTMLVideoElement>(null);
const videoContentRef = useRef<HTMLDivElement>(null);
// 音量控制状态
const [isMuted, setIsMuted] = useState(false);
const [volume, setVolume] = useState(0.8);
@ -55,6 +61,17 @@ export const MediaViewer = React.memo(function MediaViewer({
const [isFullscreen, setIsFullscreen] = useState(false);
const [finalVideoReady, setFinalVideoReady] = useState(false);
const [userHasInteracted, setUserHasInteracted] = useState(false);
const [toosBtnRight, setToodsBtnRight] = useState('1rem');
useEffect(() => {
if (isSmartChatBoxOpen) {
const videoContentWidth = videoContentRef.current?.clientWidth ?? 0;
const right = (window.innerWidth * 0.25) - ((window.innerWidth - videoContentWidth) / 2) + 32;
setToodsBtnRight(right + 'px');
} else {
setToodsBtnRight('1rem');
}
}, [isSmartChatBoxOpen])
// 音量控制函数
const toggleMute = () => {
@ -421,12 +438,13 @@ export const MediaViewer = React.memo(function MediaViewer({
};
// 渲染视频内容
const renderVideoContent = () => {
const renderVideoContent = (onGotoCut: () => void) => {
const urls = taskObject.videos.data[currentSketchIndex].urls ? taskObject.videos.data[currentSketchIndex].urls.join(',') : '';
return (
<div
className="relative w-full h-full rounded-lg group"
key={`render-video-${urls}`}
ref={videoContentRef}
>
{/* 背景模糊的图片 */}
{taskObject.videos.data[currentSketchIndex].video_status !== 1 && (
@ -479,22 +497,26 @@ export const MediaViewer = React.memo(function MediaViewer({
/>
</motion.div>
{/* 添加到chat去编辑 按钮 */}
<Tooltip title="Edit video with chat">
<Button
className="absolute top-4 left-4 z-[21] bg-white/10 backdrop-blur-sm border border-white/20 text-white"
onClick={() => {
{/* 跳转剪辑按钮 */}
<div className="absolute top-4 right-4 z-[21] flex items-center gap-2 transition-right duration-100" style={{
right: toosBtnRight
}}>
{/* 添加到chat去编辑 按钮 */}
<Tooltip placement="top" title="Edit video with chat">
<GlassIconButton icon={Video} size='sm' text="Edit with chat" onClick={() => {
const currentVideo = taskObject.videos.data[currentSketchIndex];
if (currentVideo && currentVideo.urls && currentVideo.urls.length > 0 && setVideoPreview) {
setVideoPreview(currentVideo.urls[0], currentVideo.video_id);
if (onOpenChat) onOpenChat();
}
}}
>
<Video className="w-4 h-4" />
<span className="text-xs">Edit with chat</span>
</Button>
</Tooltip>
}} />
</Tooltip>
{showGotoCutButton && (
<Tooltip placement="top" title='Go to AI-powered editing platform'>
<GlassIconButton icon={Scissors} size='sm' onClick={onGotoCut} />
</Tooltip>
)}
</div>
</>
)}
@ -672,7 +694,7 @@ export const MediaViewer = React.memo(function MediaViewer({
}
if (taskObject.currentStage === 'video') {
return renderVideoContent();
return renderVideoContent(onGotoCut);
}
if (taskObject.currentStage === 'script') {

View File

@ -8,7 +8,9 @@ import {
Heart,
Camera,
Film,
Scissors
Scissors,
Play,
Pause
} from 'lucide-react';
import { TaskObject } from '@/api/DTO/movieEdit';
import { GlassIconButton } from '@/components/ui/glass-icon-button';
@ -21,6 +23,7 @@ interface TaskInfoProps {
isPauseWorkFlow: boolean;
showGotoCutButton: boolean;
onGotoCut?: () => void;
setIsPauseWorkFlow: (isPauseWorkFlow: boolean) => void;
}
const stageIconMap = {
@ -46,7 +49,7 @@ const TAG_COLORS = ['#924eadcc', '#4c90a0', '#3b4a5a', '#957558'];
// const TAG_COLORS = ['#6bf5f9', '#92a6fc', '#ac71fd', '#c73dfe'];
// 阶段图标组件
const StageIcons = ({ currentStage, isExpanded, isPauseWorkFlow }: { currentStage: number, isExpanded: boolean, isPauseWorkFlow: boolean }) => {
const StageIcons = ({ currentStage, isExpanded, isPauseWorkFlow, setIsPauseWorkFlow }: { currentStage: number, isExpanded: boolean, isPauseWorkFlow: boolean, setIsPauseWorkFlow: (isPauseWorkFlow: boolean) => void }) => {
// 根据当前阶段重新排序图标
const orderedStages = useMemo(() => {
const stages = Object.entries(stageIconMap).map(([stage, data]) => ({
@ -58,69 +61,73 @@ const StageIcons = ({ currentStage, isExpanded, isPauseWorkFlow }: { currentStag
}, [currentStage]);
return (
<motion.div
className="relative flex items-center"
>
<AnimatePresence mode="popLayout">
{orderedStages.map((stage, index) => {
const isCurrentStage = stage.stage === currentStage;
const Icon = stage.icon;
// 只显示当前阶段或展开状态
if (!isExpanded && !isCurrentStage) return null;
return (
<motion.div
key={stage.stage}
className="relative"
initial={isExpanded ? {
opacity: 0,
x: -20,
scale: 0.5
} : {}}
animate={{
opacity: 1,
x: 0,
scale: 1,
transition: {
type: "spring",
stiffness: 300,
damping: 25,
delay: index * 0.1
}
}}
exit={{
opacity: 0,
x: 20,
scale: 0.5,
transition: { duration: 0.2 }
}}
style={{
marginLeft: index > 0 ? '8px' : '0px',
zIndex: isCurrentStage ? 2 : 1
}}
>
<Tooltip title={isPauseWorkFlow ? "Click to Play" : "Click to Pause"} placement="bottom">
<motion.div
className="relative flex items-center cursor-pointer"
onClick={() => setIsPauseWorkFlow(!isPauseWorkFlow)}
>
<AnimatePresence mode="popLayout">
{orderedStages.map((stage, index) => {
const isCurrentStage = stage.stage === currentStage;
const Icon = stage.icon;
// 只显示当前阶段或展开状态
if (!isExpanded && !isCurrentStage) return null;
return (
<motion.div
className={`relative rounded-full p-1 ${isCurrentStage ? 'bg-opacity-20' : 'bg-opacity-10'}`}
animate={(isCurrentStage && !isPauseWorkFlow) ? {
rotate: [0, 360],
scale: [1, 1.2, 1],
transition: {
rotate: { duration: 3, repeat: Infinity, ease: "linear" },
scale: { duration: 1.5, repeat: Infinity, ease: "easeInOut" }
}
key={stage.stage}
className="relative"
initial={isExpanded ? {
opacity: 0,
x: -20,
scale: 0.5
} : {}}
animate={{
opacity: 1,
x: 0,
scale: 1,
transition: {
type: "spring",
stiffness: 300,
damping: 25,
delay: index * 0.1
}
}}
exit={{
opacity: 0,
x: 20,
scale: 0.5,
transition: { duration: 0.2 }
}}
style={{
marginLeft: index > 0 ? '8px' : '0px',
zIndex: isCurrentStage ? 2 : 1
}}
>
<Icon
className="w-5 h-5"
style={{ color: stage.color }}
/>
<motion.div
className={`relative rounded-full p-1 ${isCurrentStage ? 'bg-opacity-20 cursor-pointer' : 'bg-opacity-10'}`}
animate={(isCurrentStage && !isPauseWorkFlow) ? {
rotate: [0, 360],
scale: [1, 1.2, 1],
transition: {
rotate: { duration: 3, repeat: Infinity, ease: "linear" },
scale: { duration: 1.5, repeat: Infinity, ease: "easeInOut" }
}
} : {}}
>
<Icon
className="w-5 h-5"
style={{ color: stage.color }}
/>
</motion.div>
</motion.div>
</motion.div>
);
})}
</AnimatePresence>
</motion.div>
);
})}
</AnimatePresence>
</motion.div>
</Tooltip>
);
};
@ -130,7 +137,8 @@ export function TaskInfo({
roles,
isPauseWorkFlow,
showGotoCutButton,
onGotoCut
onGotoCut,
setIsPauseWorkFlow
}: TaskInfoProps) {
const [isScriptModalOpen, setIsScriptModalOpen] = useState(false);
const [currentStage, setCurrentStage] = useState(0);
@ -284,7 +292,7 @@ export function TaskInfo({
onMouseEnter={() => setIsStageIconsExpanded(true)}
onMouseLeave={() => setIsStageIconsExpanded(false)}
>
<StageIcons currentStage={currentStage} isExpanded={isStageIconsExpanded} isPauseWorkFlow={isPauseWorkFlow} />
<StageIcons currentStage={currentStage} isExpanded={isStageIconsExpanded} isPauseWorkFlow={isPauseWorkFlow} setIsPauseWorkFlow={setIsPauseWorkFlow}/>
<motion.div
className="relative"
@ -402,12 +410,12 @@ export function TaskInfo({
} : {}}
/> */}
{/* 跳转剪辑按钮 */}
{/* //
{showGotoCutButton && (
<Tooltip placement="top" title='AI-powered editing platform'>
<GlassIconButton icon={Scissors} size='sm' onClick={onGotoCut} />
</Tooltip>
)}
)} */}
</motion.div>
)}
</>

View File

@ -13,6 +13,7 @@ interface GlassIconButtonProps {
size?: 'sm' | 'md' | 'lg';
className?: string;
[key: string]: any; // To allow spreading other props
text?: string;
}
const variantStyles = {
@ -37,12 +38,12 @@ const iconSizes = {
const MotionButton = motion.button;
export const GlassIconButton = forwardRef<HTMLButtonElement, GlassIconButtonProps>(
({ icon: Icon, tooltip, variant = 'secondary', size = 'md', className, ...props }, ref) => {
({ icon: Icon, tooltip, variant = 'secondary', size = 'md', className, text, ...props }, ref) => {
return (
<MotionButton
ref={ref}
className={cn(
'relative rounded-full backdrop-blur-md transition-colors shadow-lg border',
'relative rounded-full backdrop-blur-md transition-colors shadow-lg border flex items-center gap-2',
variantStyles[variant],
sizeStyles[size],
className
@ -60,6 +61,9 @@ export const GlassIconButton = forwardRef<HTMLButtonElement, GlassIconButtonProp
{...props}
>
<Icon className={cn('text-white', iconSizes[size])} />
{text && (
<span className="text-white text-xs">{text}</span>
)}
{tooltip && (
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-2 py-1 text-xs
bg-black/80 text-white rounded-md opacity-0 group-hover:opacity-100 transition-opacity