forked from 77media/video-flow
266 lines
8.7 KiB
TypeScript
266 lines
8.7 KiB
TypeScript
/**
|
||
* 编辑描述显示组件
|
||
* 显示已提交的编辑描述内容,带有优雅的连接线
|
||
*/
|
||
|
||
import React, { useMemo } from 'react';
|
||
import { motion, AnimatePresence } from 'framer-motion';
|
||
import { EditPoint as EditPointType, EditPointStatus } from './types';
|
||
import {
|
||
CONNECTION_STYLE,
|
||
calculateArrowGeometry,
|
||
calculateCurvePath,
|
||
getConnectionAnimationConfig
|
||
} from './connection-config';
|
||
|
||
interface EditDescriptionProps {
|
||
/** 编辑点数据 */
|
||
editPoint: EditPointType;
|
||
/** 容器尺寸 */
|
||
containerSize: { width: number; height: number };
|
||
/** 描述框位置 */
|
||
position: { x: number; y: number };
|
||
/** 连接线终点 */
|
||
connectionEnd: { x: number; y: number };
|
||
/** 点击事件处理 */
|
||
onClick?: (editPoint: EditPointType) => void;
|
||
/** 编辑事件处理 */
|
||
onEdit?: (id: string) => void;
|
||
/** 删除事件处理 */
|
||
onDelete?: (id: string) => void;
|
||
}
|
||
|
||
/**
|
||
* 编辑描述组件
|
||
*/
|
||
export const EditDescription: React.FC<EditDescriptionProps> = ({
|
||
editPoint,
|
||
containerSize,
|
||
position,
|
||
connectionEnd,
|
||
onClick,
|
||
onEdit,
|
||
onDelete
|
||
}) => {
|
||
// 计算编辑点的屏幕坐标
|
||
const editPointPosition = useMemo(() => ({
|
||
x: (editPoint.position.x / 100) * containerSize.width,
|
||
y: (editPoint.position.y / 100) * containerSize.height
|
||
}), [editPoint.position, containerSize]);
|
||
|
||
// 使用统一的连接线几何计算
|
||
const connectionGeometry = useMemo(() => {
|
||
const startPoint = { x: editPointPosition.x, y: editPointPosition.y };
|
||
const endPoint = { x: connectionEnd.x, y: connectionEnd.y };
|
||
|
||
// 使用统一的箭头几何计算
|
||
const arrowGeometry = calculateArrowGeometry(startPoint, endPoint);
|
||
|
||
// 使用统一的路径计算
|
||
const path = calculateCurvePath(startPoint, arrowGeometry.center, containerSize);
|
||
|
||
return {
|
||
path,
|
||
arrowPoints: arrowGeometry.points,
|
||
arrowTip: arrowGeometry.tip,
|
||
arrowBase: arrowGeometry.base,
|
||
arrowCenter: arrowGeometry.center
|
||
};
|
||
}, [editPointPosition, connectionEnd, containerSize]);
|
||
|
||
// 获取统一的动画配置
|
||
const animationConfig = useMemo(() =>
|
||
getConnectionAnimationConfig(true), // EditDescription总是使用动画
|
||
[]
|
||
);
|
||
|
||
// 获取状态颜色
|
||
const getStatusColor = () => {
|
||
switch (editPoint.status) {
|
||
case EditPointStatus.EDITED:
|
||
return '#10b981'; // 绿色
|
||
case EditPointStatus.PROCESSING:
|
||
return '#3b82f6'; // 蓝色
|
||
case EditPointStatus.COMPLETED:
|
||
return '#059669'; // 深绿色
|
||
case EditPointStatus.FAILED:
|
||
return '#ef4444'; // 红色
|
||
default:
|
||
return '#6b7280'; // 灰色
|
||
}
|
||
};
|
||
|
||
// 获取状态文本
|
||
const getStatusText = () => {
|
||
switch (editPoint.status) {
|
||
case EditPointStatus.EDITED:
|
||
return '已编辑';
|
||
case EditPointStatus.PROCESSING:
|
||
return '处理中';
|
||
case EditPointStatus.COMPLETED:
|
||
return '已完成';
|
||
case EditPointStatus.FAILED:
|
||
return '失败';
|
||
default:
|
||
return '';
|
||
}
|
||
};
|
||
|
||
const statusColor = getStatusColor();
|
||
const statusText = getStatusText();
|
||
|
||
return (
|
||
<AnimatePresence>
|
||
{editPoint.description && editPoint.status !== EditPointStatus.PENDING && (
|
||
<>
|
||
{/* 统一的虚线连接线 - 与EditConnection完全一致 */}
|
||
<motion.svg
|
||
className="absolute pointer-events-none"
|
||
style={{
|
||
zIndex: 5,
|
||
left: 0,
|
||
top: 0,
|
||
width: containerSize.width,
|
||
height: containerSize.height
|
||
}}
|
||
initial={{ opacity: 0 }}
|
||
animate={{ opacity: 1 }}
|
||
exit={{ opacity: 0 }}
|
||
transition={{ duration: 0.5 }}
|
||
>
|
||
{/* 统一的虚线连接线 - 与EditConnection完全一致 */}
|
||
<motion.path
|
||
d={connectionGeometry.path}
|
||
stroke={CONNECTION_STYLE.color}
|
||
strokeWidth={CONNECTION_STYLE.strokeWidth}
|
||
fill="none"
|
||
strokeDasharray={CONNECTION_STYLE.dashArray}
|
||
strokeLinecap="round"
|
||
strokeLinejoin="round"
|
||
initial={animationConfig.line?.initial}
|
||
animate={animationConfig.line?.animate}
|
||
exit={animationConfig.line?.initial}
|
||
transition={{
|
||
...animationConfig.line?.transition,
|
||
// 稍微延长显示状态的动画时间
|
||
pathLength: { duration: 0.8, ease: "easeOut" },
|
||
opacity: { duration: 0.5 }
|
||
}}
|
||
style={{
|
||
filter: CONNECTION_STYLE.dropShadow
|
||
}}
|
||
/>
|
||
|
||
{/* 几何精确的箭头 - 与连接线完美对齐 */}
|
||
<motion.polygon
|
||
points={connectionGeometry.arrowPoints.map(p => `${p.x},${p.y}`).join(' ')}
|
||
fill={CONNECTION_STYLE.color}
|
||
initial={animationConfig.arrow?.initial}
|
||
animate={animationConfig.arrow?.animate}
|
||
exit={animationConfig.arrow?.initial}
|
||
transition={animationConfig.arrow?.transition}
|
||
style={{
|
||
filter: CONNECTION_STYLE.dropShadow
|
||
}}
|
||
/>
|
||
</motion.svg>
|
||
|
||
{/* Consistent white text display matching EditInput component */}
|
||
<motion.div
|
||
className="absolute cursor-pointer group"
|
||
data-edit-description="true"
|
||
style={{
|
||
left: position.x,
|
||
top: position.y,
|
||
zIndex: 25,
|
||
}}
|
||
initial={{
|
||
opacity: 0,
|
||
scale: 0.8,
|
||
y: -10
|
||
}}
|
||
animate={{
|
||
opacity: 1,
|
||
scale: 1,
|
||
y: 0
|
||
}}
|
||
exit={{
|
||
opacity: 0,
|
||
scale: 0.8,
|
||
y: -10
|
||
}}
|
||
transition={{
|
||
type: "spring",
|
||
stiffness: 300,
|
||
damping: 25,
|
||
duration: 0.4
|
||
}}
|
||
onClick={() => onClick?.(editPoint)}
|
||
>
|
||
{/* White text display with exact same styling as EditInput */}
|
||
<div className="flex items-center">
|
||
<div
|
||
className="text-white font-bold text-lg tracking-wide uppercase"
|
||
style={{
|
||
textShadow: '2px 2px 4px rgba(0, 0, 0, 0.8)',
|
||
fontFamily: 'Arial, sans-serif',
|
||
letterSpacing: '0.1em'
|
||
}}
|
||
>
|
||
{editPoint.description}
|
||
</div>
|
||
|
||
{/* Interactive edit/delete buttons on hover */}
|
||
<div className="ml-3 opacity-0 group-hover:opacity-100 transition-opacity duration-200 flex items-center gap-2">
|
||
{onEdit && (
|
||
<motion.button
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
onEdit(editPoint.id);
|
||
}}
|
||
className="w-6 h-6 rounded-full bg-white/20 hover:bg-white/30 backdrop-blur-sm flex items-center justify-center text-white text-xs transition-colors"
|
||
whileHover={{ scale: 1.1 }}
|
||
whileTap={{ scale: 0.9 }}
|
||
title="Edit description"
|
||
>
|
||
✏️
|
||
</motion.button>
|
||
)}
|
||
{onDelete && (
|
||
<motion.button
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
onDelete(editPoint.id);
|
||
}}
|
||
className="w-6 h-6 rounded-full bg-red-500/20 hover:bg-red-500/30 backdrop-blur-sm flex items-center justify-center text-white text-xs transition-colors"
|
||
whileHover={{ scale: 1.1 }}
|
||
whileTap={{ scale: 0.9 }}
|
||
title="Delete edit point"
|
||
>
|
||
🗑️
|
||
</motion.button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Status indicator for processing states */}
|
||
{editPoint.status === EditPointStatus.PROCESSING && (
|
||
<div className="mt-1 flex items-center">
|
||
<div className="w-2 h-2 bg-blue-400 rounded-full animate-pulse mr-2"></div>
|
||
<span className="text-white/70 text-xs uppercase tracking-wide">Processing...</span>
|
||
</div>
|
||
)}
|
||
|
||
{editPoint.status === EditPointStatus.FAILED && (
|
||
<div className="mt-1 flex items-center">
|
||
<div className="w-2 h-2 bg-red-400 rounded-full mr-2"></div>
|
||
<span className="text-white/70 text-xs uppercase tracking-wide">Failed - Click to retry</span>
|
||
</div>
|
||
)}
|
||
</motion.div>
|
||
</>
|
||
)}
|
||
</AnimatePresence>
|
||
);
|
||
};
|