This commit is contained in:
海龙 2025-08-14 17:06:31 +08:00
commit 84a5e9ab58
5 changed files with 265 additions and 205 deletions

View File

@ -103,15 +103,13 @@ export function useWorkflowData() {
} = useScriptService(); } = useScriptService();
// 初始化剧本 // 初始化剧本
useUpdateEffect(() => { useUpdateEffect(() => {
if (currentStep !== '0') { console.log('开始初始化剧本', originalText,episodeId);
console.log('开始初始化剧本', originalText,episodeId); // TODO 为什么一开始没项目id
// TODO 为什么一开始没项目id originalText && initializeFromProject(episodeId, originalText).then(() => {
originalText && initializeFromProject(episodeId, originalText).then(() => { console.log('应用剧本');
console.log('应用剧本'); // 自动模式下 应用剧本;手动模式 需要点击 下一步 触发
// 自动模式下 应用剧本;手动模式 需要点击 下一步 触发 mode.includes('auto') && applyScript();
mode.includes('auto') && applyScript(); });
});
}
}, [originalText], {mode: 'none'}); }, [originalText], {mode: 'none'});
// 监听剧本加载完毕 // 监听剧本加载完毕
useEffect(() => { useEffect(() => {

View File

@ -101,7 +101,7 @@ const [pendingSwitchTabId, setPendingSwitchTabId] = useState<string | null>(null
if (activeTab === '0') { if (activeTab === '0') {
const scriptTabContent = scriptTabContentRef.current; const scriptTabContent = scriptTabContentRef.current;
if (scriptTabContent) { if (scriptTabContent) {
return scriptTabContent.checkUpdate(); return scriptTabContent.switchBefore(tabId);
} }
} else if (activeTab === '1') { } else if (activeTab === '1') {
const characterTabContent = characterTabContentRef.current; const characterTabContent = characterTabContentRef.current;
@ -183,6 +183,8 @@ const [pendingSwitchTabId, setPendingSwitchTabId] = useState<string | null>(null
setIsPauseWorkFlow={setIsPauseWorkFlow} setIsPauseWorkFlow={setIsPauseWorkFlow}
isPauseWorkFlow={isPauseWorkFlow} isPauseWorkFlow={isPauseWorkFlow}
originalText={originalText} originalText={originalText}
onApply={handleApply}
setActiveTab={setActiveTab}
/> />
); );
case '1': case '1':

View File

@ -41,6 +41,7 @@ export type PersonDetection = {
}; };
type Props = { type Props = {
scanState: 'idle' | 'scanning' | 'detected' | 'failed' | 'timeout';
backgroundImage?: string; backgroundImage?: string;
videoSrc?: string; videoSrc?: string;
detections: PersonDetection[]; detections: PersonDetection[];
@ -56,6 +57,7 @@ type Props = {
}; };
export const PersonDetectionScene: React.FC<Props> = ({ export const PersonDetectionScene: React.FC<Props> = ({
scanState,
backgroundImage, backgroundImage,
videoSrc, videoSrc,
detections, detections,
@ -201,210 +203,214 @@ export const PersonDetectionScene: React.FC<Props> = ({
/> />
)} )}
{/* 暗化层 */} {scanState !== 'idle' && (
<AnimatePresence> <>
{isShowScan && ( {/* 暗化层 */}
<motion.div <AnimatePresence>
initial={{ opacity: 0 }} {isShowScan && (
animate={{ opacity: 1 }} <motion.div
exit={{ opacity: 0 }} initial={{ opacity: 0 }}
className="absolute inset-0 bg-black/40 backdrop-blur-sm z-10" animate={{ opacity: 1 }}
/> exit={{ opacity: 0 }}
)} className="absolute inset-0 bg-black/40 backdrop-blur-sm z-10"
</AnimatePresence> />
)}
</AnimatePresence>
{/* 扫描状态提示 */} {/* 扫描状态提示 */}
<AnimatePresence> <AnimatePresence>
{isShowScan && ( {isShowScan && (
<motion.div <motion.div
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
exit={{ opacity: 0 }} exit={{ opacity: 0 }}
className="absolute top-1/2 left-1/2 z-50 flex flex-col items-center -translate-y-1/2 -translate-x-1/2" className="absolute top-1/2 left-1/2 z-50 flex flex-col items-center -translate-y-1/2 -translate-x-1/2"
> >
{/* 状态图标 */} {/* 状态图标 */}
<div className={`relative w-12 h-12 mb-2 ${isShowError ? 'scale-110' : ''}`}> <div className={`relative w-12 h-12 mb-2 ${isShowError ? 'scale-110' : ''}`}>
{!isShowError ? ( {!isShowError ? (
<> <>
<motion.div <motion.div
className="absolute inset-0 rounded-full" className="absolute inset-0 rounded-full"
style={{ style={{
border: '2px solid rgba(6, 182, 212, 0.5)', border: '2px solid rgba(6, 182, 212, 0.5)',
borderTopColor: 'rgb(6, 182, 212)', borderTopColor: 'rgb(6, 182, 212)',
}} }}
animate={{ rotate: 360 }} animate={{ rotate: 360 }}
transition={{ duration: 1.5, repeat: Infinity, ease: "linear" }} transition={{ duration: 1.5, repeat: Infinity, ease: "linear" }}
/> />
<motion.div <motion.div
className="absolute inset-2 rounded-full" className="absolute inset-2 rounded-full"
style={{ style={{
border: '2px solid rgba(6, 182, 212, 0.3)', border: '2px solid rgba(6, 182, 212, 0.3)',
borderRightColor: 'rgb(6, 182, 212)', borderRightColor: 'rgb(6, 182, 212)',
}} }}
animate={{ rotate: -360 }} animate={{ rotate: -360 }}
transition={{ duration: 2, repeat: Infinity, ease: "linear" }} transition={{ duration: 2, repeat: Infinity, ease: "linear" }}
/> />
<div className="absolute inset-4 bg-cyan-400 rounded-full" /> <div className="absolute inset-4 bg-cyan-400 rounded-full" />
</> </>
) : ( ) : (
<motion.div
initial={{ scale: 0.8 }}
animate={{
scale: [0.8, 1.1, 1],
borderColor: ['rgb(239, 68, 68)', 'rgb(239, 68, 68)', 'rgb(239, 68, 68)']
}}
transition={{ duration: 0.5 }}
className="w-full h-full rounded-full flex items-center justify-center"
>
<TriangleAlert className="w-12 h-12 text-red-500" />
</motion.div>
)}
</div>
{/* 状态文字 */}
<motion.div <motion.div
initial={{ scale: 0.8 }} initial={{ opacity: 0, scale: 0.9 }}
animate={{ animate={{ opacity: 1, scale: 1 }}
scale: [0.8, 1.1, 1], transition={{ delay: 0.1 }}
borderColor: ['rgb(239, 68, 68)', 'rgb(239, 68, 68)', 'rgb(239, 68, 68)']
}}
transition={{ duration: 0.5 }}
className="w-full h-full rounded-full flex items-center justify-center"
> >
<TriangleAlert className="w-12 h-12 text-red-500" /> <div className="flex items-center gap-2">
{!isShowError ? (
<span className="text-cyan-400 text-sm font-medium tracking-wider whitespace-nowrap">
Intelligenting portrait recognition
</span>
) : (
<span className="text-red-400 text-sm font-medium tracking-wider whitespace-nowrap">
{scanStatus === 'timeout' ? 'Timeout, please try again' : 'Failed, please try again'}
</span>
)}
</div>
</motion.div> </motion.div>
)} </motion.div>
</div> )}
</AnimatePresence>
{/* 状态文字 */} {/* 扫描动画效果 */}
<motion.div <AnimatePresence>
initial={{ opacity: 0, scale: 0.9 }} {isShowScan && scanState === 'scanning' && (
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.1 }}
>
<div className="flex items-center gap-2">
{!isShowError ? (
<span className="text-cyan-400 text-sm font-medium tracking-wider whitespace-nowrap">
Intelligenting portrait recognition
</span>
) : (
<span className="text-red-400 text-sm font-medium tracking-wider whitespace-nowrap">
{scanStatus === 'timeout' ? 'Timeout, please try again' : 'Failed, please try again'}
</span>
)}
</div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
{/* 扫描动画效果 */}
<AnimatePresence>
{isShowScan && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="absolute inset-0"
>
{/* 主扫描线 */}
<motion.div
className="absolute left-0 w-full h-[150px] z-20"
animate={scanControls}
initial={{ y: "-120%" }}
exit={{ opacity: 0 }}
style={{
background: `
linear-gradient(
180deg,
transparent 0%,
rgba(6, 182, 212, 0.1) 20%,
rgba(6, 182, 212, 0.3) 50%,
rgba(6, 182, 212, 0.1) 80%,
transparent 100%
)
`,
boxShadow: '0 0 30px rgba(6, 182, 212, 0.3)'
}}
>
{/* 扫描线中心光束 */}
<motion.div <motion.div
className="absolute top-1/2 left-0 w-full h-[2px]"
style={{
background: 'linear-gradient(90deg, transparent 0%, rgb(6, 182, 212) 50%, transparent 100%)',
boxShadow: '0 0 20px rgb(6, 182, 212)'
}}
/>
</motion.div>
{/* 扫描网格 */}
<div className="absolute inset-0 z-15">
<motion.div
className="w-full h-full"
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
exit={{ opacity: 0 }} exit={{ opacity: 0 }}
transition={{ duration: 0.5 }}
style={{
backgroundImage: `
linear-gradient(90deg, rgba(6, 182, 212, 0.1) 1px, transparent 1px),
linear-gradient(0deg, rgba(6, 182, 212, 0.1) 1px, transparent 1px)
`,
backgroundSize: '40px 40px'
}}
/>
<motion.div
className="absolute inset-0" className="absolute inset-0"
initial={{ opacity: 0 }}
animate={{ opacity: 0.5 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.5 }}
style={{
backgroundImage: `
linear-gradient(90deg, rgba(6, 182, 212, 0.05) 1px, transparent 1px),
linear-gradient(0deg, rgba(6, 182, 212, 0.05) 1px, transparent 1px)
`,
backgroundSize: '20px 20px'
}}
/>
</div>
{/* 四角扫描框 */}
<div className="absolute inset-4 z-30 pointer-events-none">
<motion.div
className="w-full h-full"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
> >
{/* 左上角 */} {/* 主扫描线 */}
<div className="absolute top-0 left-0 w-8 h-8 border-l-2 border-t-2 border-cyan-400/70" /> <motion.div
{/* 右上角 */} className="absolute left-0 w-full h-[150px] z-20"
<div className="absolute top-0 right-0 w-8 h-8 border-r-2 border-t-2 border-cyan-400/70" /> animate={scanControls}
{/* 左下角 */} initial={{ y: "-120%" }}
<div className="absolute bottom-0 left-0 w-8 h-8 border-l-2 border-b-2 border-cyan-400/70" /> exit={{ opacity: 0 }}
{/* 右下角 */} style={{
<div className="absolute bottom-0 right-0 w-8 h-8 border-r-2 border-b-2 border-cyan-400/70" /> background: `
</motion.div> linear-gradient(
</div> 180deg,
</motion.div> transparent 0%,
)} rgba(6, 182, 212, 0.1) 20%,
</AnimatePresence> rgba(6, 182, 212, 0.3) 50%,
rgba(6, 182, 212, 0.1) 80%,
transparent 100%
)
`,
boxShadow: '0 0 30px rgba(6, 182, 212, 0.3)'
}}
>
{/* 扫描线中心光束 */}
<motion.div
className="absolute top-1/2 left-0 w-full h-[2px]"
style={{
background: 'linear-gradient(90deg, transparent 0%, rgb(6, 182, 212) 50%, transparent 100%)',
boxShadow: '0 0 20px rgb(6, 182, 212)'
}}
/>
</motion.div>
{/* 人物识别框和浮签 */} {/* 扫描网格 */}
<AnimatePresence> <div className="absolute inset-0 z-15">
{detections.length === 0 && triggerSuccess && ( <motion.div
<div className="absolute inset-0 flex items-center justify-center bg-black/40 backdrop-blur-sm"> className="w-full h-full"
<span className="text-white text-sm">No portrait detected</span> initial={{ opacity: 0 }}
</div> animate={{ opacity: 1 }}
)} exit={{ opacity: 0 }}
{detections.map((person, index) => { transition={{ duration: 0.5 }}
return ( style={{
<div key={person.id} className="cursor-pointer" onClick={() => { backgroundImage: `
onPersonClick?.(person); linear-gradient(90deg, rgba(6, 182, 212, 0.1) 1px, transparent 1px),
}}> linear-gradient(0deg, rgba(6, 182, 212, 0.1) 1px, transparent 1px)
<PersonBox person={person} /> `,
<motion.div backgroundSize: '40px 40px'
className="absolute z-50 px-3 py-1 text-white text-xs bg-cyan-500/20 border border-cyan-400/30 rounded-md backdrop-blur-md whitespace-nowrap" }}
style={{ />
top: `${person.position.top}px`, <motion.div
left: `${person.position.left}px` className="absolute inset-0"
}} initial={{ opacity: 0 }}
initial={{ opacity: 0, y: -10 }} animate={{ opacity: 0.5 }}
animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0 }}
exit={{ opacity: 0, y: -10 }} transition={{ duration: 0.5 }}
> style={{
{person.name} backgroundImage: `
linear-gradient(90deg, rgba(6, 182, 212, 0.05) 1px, transparent 1px),
linear-gradient(0deg, rgba(6, 182, 212, 0.05) 1px, transparent 1px)
`,
backgroundSize: '20px 20px'
}}
/>
</div>
{/* 四角扫描框 */}
<div className="absolute inset-4 z-30 pointer-events-none">
<motion.div
className="w-full h-full"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
{/* 左上角 */}
<div className="absolute top-0 left-0 w-8 h-8 border-l-2 border-t-2 border-cyan-400/70" />
{/* 右上角 */}
<div className="absolute top-0 right-0 w-8 h-8 border-r-2 border-t-2 border-cyan-400/70" />
{/* 左下角 */}
<div className="absolute bottom-0 left-0 w-8 h-8 border-l-2 border-b-2 border-cyan-400/70" />
{/* 右下角 */}
<div className="absolute bottom-0 right-0 w-8 h-8 border-r-2 border-b-2 border-cyan-400/70" />
</motion.div>
</div>
</motion.div> </motion.div>
</div> )}
); </AnimatePresence>
})}
</AnimatePresence> {/* 人物识别框和浮签 */}
<AnimatePresence>
{detections.length === 0 && triggerSuccess && (
<div className="absolute inset-0 flex items-center justify-center bg-black/40 backdrop-blur-sm">
<span className="text-white text-sm">No portrait detected</span>
</div>
)}
{detections.map((person, index) => {
return (
<div key={person.id} className="cursor-pointer" onClick={() => {
onPersonClick?.(person);
}}>
<PersonBox person={person} />
<motion.div
className="absolute z-50 px-3 py-1 text-white text-xs bg-cyan-500/20 border border-cyan-400/30 rounded-md backdrop-blur-md whitespace-nowrap"
style={{
top: `${person.position.top}px`,
left: `${person.position.left}px`
}}
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
>
{person.name}
</motion.div>
</div>
);
})}
</AnimatePresence>
</>
)}
</div> </div>
); );
}; };

View File

@ -1,24 +1,29 @@
import React, { useState, useCallback, useEffect, SetStateAction, forwardRef } from 'react'; import React, { useState, useCallback, useEffect, SetStateAction, forwardRef } from 'react';
import { motion, AnimatePresence } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
import { FileText } from 'lucide-react'; import { FileText, Undo2, X, TriangleAlert } from 'lucide-react';
import { ScriptRenderer } from '@/components/script-renderer/ScriptRenderer'; import { ScriptRenderer } from '@/components/script-renderer/ScriptRenderer';
import { useEditData } from '@/components/pages/work-flow/use-edit-data'; import { useEditData } from '@/components/pages/work-flow/use-edit-data';
import FloatingGlassPanel from './FloatingGlassPanel';
interface ScriptTabContentProps { interface ScriptTabContentProps {
setIsPauseWorkFlow: (isPauseWorkFlow: boolean) => void; setIsPauseWorkFlow: (isPauseWorkFlow: boolean) => void;
isPauseWorkFlow: boolean; isPauseWorkFlow: boolean;
originalText?: string; originalText?: string;
onApply: () => void;
setActiveTab: (tabId: string) => void;
} }
export const ScriptTabContent = forwardRef< export const ScriptTabContent = forwardRef<
{ checkUpdate: () => boolean }, { switchBefore: (tabId: string) => boolean },
ScriptTabContentProps ScriptTabContentProps
>((props, ref) => { >((props, ref) => {
const { setIsPauseWorkFlow, isPauseWorkFlow, originalText } = props; const { setIsPauseWorkFlow, isPauseWorkFlow, originalText, onApply, setActiveTab } = props;
const { loading, scriptData, setAnyAttribute, applyScript } = useEditData('script', originalText); const { loading, scriptData, setAnyAttribute, applyScript } = useEditData('script', originalText);
const [isUpdate, setIsUpdate] = useState(false); const [isUpdate, setIsUpdate] = useState(false);
const [isRemindApplyUpdate, setIsRemindApplyUpdate] = useState(false);
const [nextToTabId, setNextToTabId] = useState<string>('');
useEffect(() => { useEffect(() => {
console.log('contentEditableRef---scriptTabContentIsChange', isUpdate); console.log('contentEditableRef---scriptTabContentIsChange', isUpdate);
@ -26,9 +31,24 @@ export const ScriptTabContent = forwardRef<
// 暴露方法给父组件 // 暴露方法给父组件
React.useImperativeHandle(ref, () => ({ React.useImperativeHandle(ref, () => ({
checkUpdate: () => isUpdate switchBefore: (tabId: string) => {
setNextToTabId(tabId);
console.log('switchBefore', isUpdate);
if (isUpdate) {
setIsRemindApplyUpdate(true);
}
return isUpdate;
},
})); }));
const handleApply = () => {
onApply();
}
const handleCancel = () => {
setIsRemindApplyUpdate(false);
setActiveTab(nextToTabId);
}
// 如果loading 显示loading状态 // 如果loading 显示loading状态
if (loading) { if (loading) {
return ( return (
@ -66,6 +86,39 @@ export const ScriptTabContent = forwardRef<
setIsUpdate={setIsUpdate} setIsUpdate={setIsUpdate}
/> />
</motion.div> </motion.div>
<FloatingGlassPanel
open={isRemindApplyUpdate}
width='500px'
clickMaskClose={false}
>
<div className="flex flex-col items-center gap-4 text-white py-4">
<div className="flex items-center gap-3">
<TriangleAlert className="w-6 h-6 text-yellow-400" />
<p className="text-lg font-medium"></p>
</div>
<div className="flex gap-3 mt-2">
<button
onClick={() => handleApply()}
data-alt="confirm-replace-button"
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-md transition-colors duration-200 flex items-center gap-2"
>
<Undo2 className="w-4 h-4" />
Apply
</button>
<button
onClick={() => handleCancel()}
data-alt="ignore-button"
className="px-4 py-2 bg-gray-600 hover:bg-gray-700 rounded-md transition-colors duration-200 flex items-center gap-2"
>
<X className="w-4 h-4" />
Cancel
</button>
</div>
</div>
</FloatingGlassPanel>
</div> </div>
); );
}); });

View File

@ -329,6 +329,7 @@ export function ShotTabContent({
<PersonDetectionScene <PersonDetectionScene
videoSrc={shotData[selectedIndex]?.videoUrl[0]} videoSrc={shotData[selectedIndex]?.videoUrl[0]}
detections={detections} detections={detections}
scanState={scanState}
triggerScan={scanState === 'scanning'} triggerScan={scanState === 'scanning'}
triggerSuccess={scanState === 'detected'} triggerSuccess={scanState === 'detected'}
onScanTimeout={handleScanTimeout} onScanTimeout={handleScanTimeout}