forked from 77media/video-flow
253 lines
8.3 KiB
TypeScript
253 lines
8.3 KiB
TypeScript
/**
|
||
* 编辑描述显示组件
|
||
* 显示已提交的编辑描述内容,带有优雅的连接线
|
||
*/
|
||
|
||
import React, { useMemo } from 'react';
|
||
import { motion, AnimatePresence } from 'framer-motion';
|
||
import { EditPoint as EditPointType, EditPointStatus } from './types';
|
||
|
||
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 connectionPath = useMemo(() => {
|
||
const startX = editPointPosition.x;
|
||
const startY = editPointPosition.y;
|
||
const endX = connectionEnd.x;
|
||
const endY = connectionEnd.y;
|
||
|
||
// 计算控制点,创建优雅的弧线
|
||
const deltaX = endX - startX;
|
||
const deltaY = endY - startY;
|
||
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
||
|
||
// 控制点偏移量,创建自然的弧线
|
||
const controlOffset = Math.min(distance * 0.3, 60);
|
||
const controlX = startX + deltaX * 0.5 + (deltaY > 0 ? -controlOffset : controlOffset);
|
||
const controlY = startY + deltaY * 0.5 - Math.abs(deltaX) * 0.2;
|
||
|
||
return `M ${startX} ${startY} Q ${controlX} ${controlY} ${endX} ${endY}`;
|
||
}, [editPointPosition, connectionEnd]);
|
||
|
||
// 获取状态颜色
|
||
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 && (
|
||
<>
|
||
{/* White dashed connection line to match reference image */}
|
||
<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 }}
|
||
>
|
||
<motion.path
|
||
d={connectionPath}
|
||
stroke="rgba(255, 255, 255, 0.9)"
|
||
strokeWidth={2}
|
||
fill="none"
|
||
strokeDasharray="8,4"
|
||
strokeLinecap="round"
|
||
initial={{ pathLength: 0, opacity: 0 }}
|
||
animate={{
|
||
pathLength: 1,
|
||
opacity: 1
|
||
}}
|
||
exit={{ pathLength: 0, opacity: 0 }}
|
||
transition={{
|
||
pathLength: { duration: 0.8, ease: "easeOut" },
|
||
opacity: { duration: 0.5 }
|
||
}}
|
||
style={{
|
||
filter: 'drop-shadow(2px 2px 4px rgba(0, 0, 0, 0.8))'
|
||
}}
|
||
/>
|
||
|
||
{/* Arrow head */}
|
||
<motion.polygon
|
||
points={`${connectionEnd.x},${connectionEnd.y} ${connectionEnd.x-8},${connectionEnd.y-4} ${connectionEnd.x-8},${connectionEnd.y+4}`}
|
||
fill="rgba(255, 255, 255, 0.9)"
|
||
initial={{ scale: 0, opacity: 0 }}
|
||
animate={{ scale: 1, opacity: 1 }}
|
||
exit={{ scale: 0, opacity: 0 }}
|
||
transition={{ delay: 0.4, duration: 0.3 }}
|
||
style={{
|
||
filter: 'drop-shadow(2px 2px 4px rgba(0, 0, 0, 0.8))'
|
||
}}
|
||
/>
|
||
</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>
|
||
);
|
||
};
|