video-flow-b/components/pages/work-flow/editing-notification.tsx
2025-09-11 20:04:04 +08:00

228 lines
7.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { notification, Progress } from 'antd';
import { useEffect, useRef, useState, useMemo, useCallback } from 'react';
import { Scissors } from 'lucide-react';
import { motion } from 'framer-motion';
// 暗色玻璃风格样式
const darkGlassStyle = {
background: 'rgba(30, 32, 40, 0.95)',
backdropFilter: 'blur(10px)',
WebkitBackdropFilter: 'blur(10px)',
border: '1px solid rgba(255, 255, 255, 0.08)',
borderRadius: '8px',
boxShadow: '0 4px 16px rgba(0, 0, 0, 0.4)',
padding: '12px 16px',
};
const messageStyle = {
fontSize: '13px',
fontWeight: 500,
color: '#ffffff',
marginBottom: '6px',
display: 'flex',
alignItems: 'center',
gap: '6px',
};
const descriptionStyle = {
fontSize: '12px',
color: 'rgba(255, 255, 255, 0.65)',
marginBottom: '12px',
lineHeight: '1.5',
display: 'flex',
alignItems: 'center'
};
interface EditingNotificationProps {
/** 编辑是否完成 */
isCompleted?: boolean;
/** 初始描述文案 */
description?: string;
/** 编辑成功时的描述文案 */
successDescription?: string;
/** 编辑超时失败时的描述文案 */
timeoutDescription?: string;
/** key */
key?: string;
/** 完成时的回调 */
onComplete?: () => void;
/** 失败时的回调 */
onFail?: () => void;
/** 超时时间毫秒默认10分钟 */
timeout?: number;
/** 是否显示关闭按钮默认false */
showCloseIcon?: boolean;
/** 获取当前进度值的回调 */
onGetProgress?: (progress: number) => void;
/** 设置进度值的回调 */
onSetProgress?: (setProgressFn: (value: number) => void) => void;
}
/**
* 显示视频编辑进度通知
* @param props EditingNotificationProps
*/
export const showEditingNotification = (props: EditingNotificationProps) => {
console.log('🔔 showEditingNotification called:', { key: props.key, description: props.description, isCompleted: props.isCompleted });
const {
isCompleted = false,
description = 'The intelligent editing platform is currently editing the videos.',
successDescription = 'The editing is complete, and the updated video has been displayed on the page.',
timeoutDescription = 'The editing timed out, please try again.',
key,
onComplete,
onFail,
timeout = 8 * 60 * 1000, // 默认8分钟
showCloseIcon = false,
onGetProgress,
onSetProgress,
} = props;
const NotificationContent = () => {
const [progress, setProgressInternal] = useState(0);
const [status, setStatus] = useState<'active' | 'success' | 'exception'>('active');
const [currentDescription, setCurrentDescription] = useState(description);
const timerRef = useRef<NodeJS.Timeout>();
const startTimeRef = useRef(Date.now());
// 包装 setProgress 以支持外部回调
const setProgress = useCallback((value: number | ((prev: number) => number)) => {
const newValue = typeof value === 'function' ? value(progress) : value;
setProgressInternal(newValue);
onGetProgress?.(newValue);
}, [progress, onGetProgress]); // 恢复原来的依赖
// 监听外部设置进度值
useEffect(() => {
if (onSetProgress) {
onSetProgress(setProgress);
}
}, [setProgress]); // 只依赖 setProgress
// 重置进度条
const resetProgress = () => {
setProgress(0);
setStatus('active');
setCurrentDescription(description);
startTimeRef.current = Date.now();
};
// 将重置方法暴露给外部
if (props.key && typeof window !== 'undefined') {
(window as any)[`resetProgress_${props.key}`] = resetProgress;
}
const scissorsIcon = useMemo(() => (
<motion.div
style={{ display: 'inline-flex', marginRight: '8px' }}
animate={status === 'active' ? {
rotate: [0, 360],
scale: [1, 1.2, 1]
} : { rotate: 0, scale: 1 }}
transition={status === 'active' ? {
rotate: { duration: 3, repeat: Infinity, ease: "linear" },
scale: { duration: 1.5, repeat: Infinity, ease: "easeInOut" }
} : { duration: 0.3 }}
>
<Scissors className="w-5 h-5 text-[#f59e0b]" />
</motion.div>
), [status]);
// 处理进度更新
useEffect(() => {
const updateProgress = () => {
const elapsed = Date.now() - startTimeRef.current;
const timeLimit = timeout; // 使用传入的超时时间
if (isCompleted) {
// 如果完成了快速增加到100%
setProgress((prev: number) => {
const next = prev + (100 - prev) / 10;
if (next >= 99.9) {
setStatus('success');
setCurrentDescription(successDescription);
onComplete?.();
clearInterval(timerRef.current);
return 100;
}
return next;
});
} else if (elapsed >= timeLimit) {
// 超时失败,只调用一次 onFail
if (status !== 'exception') {
console.log('⏰ Timeout reached, calling onFail');
setStatus('exception');
setCurrentDescription(timeoutDescription);
onFail?.();
}
// 继续更新进度但不再重复调用 onFail
setProgress((prev: number) => {
const next = Math.min(prev + 0.2, 90); // 失败后继续缓慢增加但限制在90%以内
return next;
});
} else {
// 增加到90%
setProgress((prev: number) => {
const targetProgress = (elapsed / timeLimit) * 90;
const next = Math.min(prev + 2, targetProgress);
return next;
});
}
};
timerRef.current = setInterval(updateProgress, 100);
return () => clearInterval(timerRef.current);
}, [isCompleted, setProgress, successDescription, timeoutDescription, timeout]); // 恢复原来的依赖
return (
<div data-alt="editing-notification" style={{ minWidth: '300px' }}>
<h3 style={{
...descriptionStyle,
color: status === 'exception' ? '#ff4d4f' :
'rgba(255, 255, 255, 0.65)',
}}>
{scissorsIcon}
{currentDescription}
</h3>
<Progress
percent={Math.round(progress)}
status={status}
strokeColor={{
'0%': '#f59e0b',
'100%': '#a855f7',
}}
trailColor="rgba(255,255,255,0.08)"
size="small"
format={percent => (
<span style={{
color: status === 'exception' ? '#ff4d4f' :
status === 'success' ? '#a855f7' :
'rgba(255,255,255,0.85)',
fontSize: '12px',
fontWeight: 500
}}>
{`${percent}%`}
</span>
)}
className="transition-all duration-300 ease-in-out"
/>
</div>
);
};
notification.open({
key,
message: null,
description: <NotificationContent />,
duration: 0,
placement: 'topRight',
style: darkGlassStyle,
className: 'dark-glass-notification',
closeIcon: showCloseIcon ? undefined : null
});
// 返回key以便外部可以手动关闭通知
return key;
};