275 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';
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 && (
<>
{/* 连接线 */}
<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={statusColor}
strokeWidth={2}
fill="none"
strokeDasharray="4,4"
initial={{ pathLength: 0, opacity: 0 }}
animate={{
pathLength: 1,
opacity: 0.8,
strokeDashoffset: [0, -8]
}}
exit={{ pathLength: 0, opacity: 0 }}
transition={{
pathLength: { duration: 0.8, ease: "easeOut" },
opacity: { duration: 0.5 },
strokeDashoffset: {
duration: 2,
repeat: Infinity,
ease: "linear"
}
}}
/>
</motion.svg>
{/* 描述内容框 */}
<motion.div
className="absolute cursor-pointer group"
data-edit-description="true"
style={{
left: position.x,
top: position.y,
zIndex: 25,
maxWidth: '300px',
minWidth: '200px'
}}
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)}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
{/* 玻璃态背景 */}
<div className="relative bg-white/90 dark:bg-gray-800/90 backdrop-blur-md rounded-lg shadow-lg border border-white/20 dark:border-gray-700/30 overflow-hidden">
{/* 状态指示条 */}
<div
className="h-1 w-full"
style={{ backgroundColor: statusColor }}
/>
{/* 内容区域 */}
<div className="p-3">
{/* 状态标签 */}
{statusText && (
<div className="flex items-center gap-2 mb-2">
<div
className="w-2 h-2 rounded-full"
style={{ backgroundColor: statusColor }}
/>
<span
className="text-xs font-medium"
style={{ color: statusColor }}
>
{statusText}
</span>
</div>
)}
{/* 描述文本 */}
<div className="text-sm text-gray-800 dark:text-gray-200 leading-relaxed">
{editPoint.description}
</div>
{/* 时间戳 */}
<div className="text-xs text-gray-500 dark:text-gray-400 mt-2 flex items-center justify-between">
<span>
{Math.floor(editPoint.timestamp)}s
</span>
<span>
{new Date(editPoint.updatedAt).toLocaleTimeString()}
</span>
</div>
</div>
{/* 悬停时显示的操作按钮 */}
<motion.div
className="absolute top-2 right-2 flex gap-1 opacity-0 group-hover:opacity-100"
initial={{ opacity: 0 }}
animate={{ opacity: 0 }}
whileHover={{ opacity: 1 }}
transition={{ duration: 0.2 }}
>
{onEdit && (
<button
className="w-6 h-6 rounded-full bg-blue-500/80 hover:bg-blue-500 text-white text-xs flex items-center justify-center transition-colors"
onClick={(e) => {
e.stopPropagation();
onEdit(editPoint.id);
}}
title="编辑"
>
</button>
)}
{onDelete && (
<button
className="w-6 h-6 rounded-full bg-red-500/80 hover:bg-red-500 text-white text-xs flex items-center justify-center transition-colors"
onClick={(e) => {
e.stopPropagation();
onDelete(editPoint.id);
}}
title="删除"
>
🗑
</button>
)}
</motion.div>
{/* 装饰性光效 */}
<div className="absolute inset-0 bg-gradient-to-br from-white/10 to-transparent pointer-events-none" />
</div>
{/* 连接点指示器 */}
<div
className="absolute w-2 h-2 rounded-full border-2 border-white shadow-sm"
style={{
backgroundColor: statusColor,
left: connectionEnd.x - position.x - 4,
top: connectionEnd.y - position.y - 4,
}}
/>
</motion.div>
</>
)}
</AnimatePresence>
);
};