forked from 77media/video-flow
275 lines
8.7 KiB
TypeScript
275 lines
8.7 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 && (
|
||
<>
|
||
{/* 连接线 */}
|
||
<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>
|
||
);
|
||
};
|