266 lines
8.7 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 编辑描述显示组件
* 显示已提交的编辑描述内容,带有优雅的连接线
*/
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>
);
};