265 lines
7.7 KiB
TypeScript

/**
* 编辑点交互组件
* 实现脉冲动画效果和点击交互
*/
import React, { useCallback, useMemo } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Edit3, Check, X, Loader2 } from 'lucide-react';
import { EditPoint as EditPointType, EditPointStatus } from './types';
interface EditPointProps {
/** 编辑点数据 */
editPoint: EditPointType;
/** 是否被选中 */
isSelected: boolean;
/** 容器尺寸 */
containerSize: { width: number; height: number };
/** 点击事件处理 */
onClick: (editPoint: EditPointType) => void;
/** 删除事件处理 */
onDelete: (id: string) => void;
/** 编辑事件处理 */
onEdit: (id: string) => void;
/** 样式配置 */
style?: {
size?: number;
color?: string;
pulseColor?: string;
};
}
/**
* 编辑点组件
*/
export const EditPoint: React.FC<EditPointProps> = ({
editPoint,
isSelected,
containerSize,
onClick,
onDelete,
onEdit,
style = {}
}) => {
const {
size = 12,
color = '#3b82f6',
pulseColor = 'rgba(59, 130, 246, 0.3)'
} = style;
// 计算绝对位置
const absolutePosition = useMemo(() => ({
x: (editPoint.position.x / 100) * containerSize.width,
y: (editPoint.position.y / 100) * containerSize.height
}), [editPoint.position, containerSize]);
// 处理点击事件
const handleClick = useCallback((e: React.MouseEvent) => {
e.stopPropagation();
onClick(editPoint);
}, [onClick, editPoint]);
// 处理删除事件
const handleDelete = useCallback((e: React.MouseEvent) => {
e.stopPropagation();
onDelete(editPoint.id);
}, [onDelete, editPoint.id]);
// 处理编辑事件
const handleEdit = useCallback((e: React.MouseEvent) => {
e.stopPropagation();
onEdit(editPoint.id);
}, [onEdit, editPoint.id]);
// 根据状态获取颜色
const getStatusColor = useCallback(() => {
switch (editPoint.status) {
case EditPointStatus.PENDING:
return '#f59e0b'; // 黄色
case EditPointStatus.EDITED:
return '#10b981'; // 绿色
case EditPointStatus.PROCESSING:
return '#3b82f6'; // 蓝色
case EditPointStatus.COMPLETED:
return '#059669'; // 深绿色
case EditPointStatus.FAILED:
return '#ef4444'; // 红色
default:
return color;
}
}, [editPoint.status, color]);
// 根据状态获取图标
const getStatusIcon = useCallback(() => {
switch (editPoint.status) {
case EditPointStatus.PENDING:
return Edit3;
case EditPointStatus.EDITED:
return Check;
case EditPointStatus.PROCESSING:
return Loader2;
case EditPointStatus.COMPLETED:
return Check;
case EditPointStatus.FAILED:
return X;
default:
return Edit3;
}
}, [editPoint.status]);
const StatusIcon = getStatusIcon();
const statusColor = getStatusColor();
return (
<motion.div
className="absolute z-20 cursor-pointer"
data-edit-point="true"
style={{
left: absolutePosition.x - size / 2,
top: absolutePosition.y - size / 2,
}}
initial={{ scale: 0, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0, opacity: 0 }}
transition={{
type: "spring",
stiffness: 300,
damping: 25,
duration: 0.3
}}
onClick={handleClick}
>
{/* 脉冲动画背景 */}
<AnimatePresence>
{(editPoint.status === EditPointStatus.PENDING || isSelected) && (
<motion.div
className="absolute inset-0 rounded-full"
style={{
backgroundColor: pulseColor,
width: size * 3,
height: size * 3,
left: -size,
top: -size,
}}
initial={{ scale: 0, opacity: 0 }}
animate={{
scale: [1, 1.5, 1],
opacity: [0.6, 0.2, 0.6],
}}
exit={{ scale: 0, opacity: 0 }}
transition={{
duration: 2,
repeat: Infinity,
ease: "easeInOut"
}}
/>
)}
</AnimatePresence>
{/* 主编辑点 */}
<motion.div
className="relative rounded-full flex items-center justify-center shadow-lg backdrop-blur-sm"
style={{
width: size,
height: size,
backgroundColor: statusColor,
border: `2px solid rgba(255, 255, 255, 0.3)`,
}}
whileHover={{ scale: 1.2 }}
whileTap={{ scale: 0.9 }}
animate={editPoint.status === EditPointStatus.PROCESSING ? {
rotate: 360,
transition: { duration: 1, repeat: Infinity, ease: "linear" }
} : {}}
>
<StatusIcon
size={size * 0.5}
color="white"
className={editPoint.status === EditPointStatus.PROCESSING ? "animate-spin" : ""}
/>
</motion.div>
{/* 选中状态的操作按钮 */}
<AnimatePresence>
{isSelected && editPoint.status !== EditPointStatus.PROCESSING && (
<motion.div
className="absolute flex gap-1"
style={{
left: size + 8,
top: -4,
}}
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -10 }}
transition={{ duration: 0.2 }}
>
{/* 编辑按钮 */}
<motion.button
className="w-6 h-6 rounded-full bg-blue-500/80 backdrop-blur-sm flex items-center justify-center text-white hover:bg-blue-600/80 transition-colors"
onClick={handleEdit}
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
title="编辑描述"
>
<Edit3 size={12} />
</motion.button>
{/* 删除按钮 */}
<motion.button
className="w-6 h-6 rounded-full bg-red-500/80 backdrop-blur-sm flex items-center justify-center text-white hover:bg-red-600/80 transition-colors"
onClick={handleDelete}
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
title="删除编辑点"
>
<X size={12} />
</motion.button>
</motion.div>
)}
</AnimatePresence>
{/* 状态提示文本 */}
<AnimatePresence>
{isSelected && editPoint.description && (
<motion.div
className="absolute whitespace-nowrap text-xs text-white bg-black/60 backdrop-blur-sm rounded px-2 py-1 pointer-events-none"
style={{
left: size + 8,
top: size + 8,
maxWidth: 200,
}}
initial={{ opacity: 0, y: -5 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -5 }}
transition={{ duration: 0.2 }}
>
{editPoint.description.length > 50
? `${editPoint.description.substring(0, 50)}...`
: editPoint.description
}
</motion.div>
)}
</AnimatePresence>
{/* 时间戳显示 */}
<AnimatePresence>
{isSelected && (
<motion.div
className="absolute text-xs text-white/70 bg-black/40 backdrop-blur-sm rounded px-1 py-0.5 pointer-events-none"
style={{
left: -20,
top: size + 8,
}}
initial={{ opacity: 0, y: -5 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -5 }}
transition={{ duration: 0.2 }}
>
{Math.floor(editPoint.timestamp)}s
</motion.div>
)}
</AnimatePresence>
</motion.div>
);
};