import React, { useState, useRef, useEffect, useCallback, memo, useMemo } from 'react';
import {
Settings,
Plus,
Trash2,
Cpu,
Box,
BookOpen,
Move,
Target,
Ruler,
Zap,
Palette,
Spline,
ChevronRight,
Maximize2,
Wind,
RotateCw,
FileText,
DollarSign,
Activity,
GitBranch,
Layers,
Share2,
Battery,
HardDrive
} from 'lucide-react';
/**
* 工业蓝图风格样式系统
*/
const customStyles = `
.blueprint-grid {
background-image:
linear-gradient(#ccc 1px, transparent 1px),
linear-gradient(90deg, #ccc 1px, transparent 1px);
background-size: 40px 40px;
}
.drag-active {
cursor: grabbing !important;
}
.selection-box {
position: absolute;
border: 1px dashed #000;
background-color: rgba(0, 0, 0, 0.05);
pointer-events: none;
z-index: 200;
}
@keyframes line-flow {
from { stroke-dashoffset: 40; }
to { stroke-dashoffset: 0; }
}
@keyframes dash-flow-circle {
from { stroke-dashoffset: 40; }
to { stroke-dashoffset: 0; }
}
.leader-line {
animation: line-flow linear infinite;
pointer-events: none;
stroke-linecap: round;
transition: opacity 0.6s ease-out;
}
.rotating-origin {
animation: dash-flow-circle linear infinite;
pointer-events: none;
}
.custom-scrollbar::-webkit-scrollbar {
width: 4px;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: #000;
border-radius: 10px;
}
.node-pulse {
animation: node-pulse-kf 2s infinite;
}
@keyframes node-pulse-kf {
0% { box-shadow: 0 0 0 0px rgba(234, 179, 8, 0.4); }
100% { box-shadow: 0 0 0 10px rgba(234, 179, 8, 0); }
}
.junction-node {
transition: opacity 0.6s ease-out, transform 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
`;
// 工业随机颜色池
const INDUSTRIAL_PALETTE = [
'#262626', '#ef4444', '#3b82f6', '#10b981', '#f59e0b', '#8b5cf6', '#06b6d4', '#64748b'
];
// 零件实体组件
const PartItem = memo(({ part, isSelected, showExploded, onMouseDown, isDraggingAny }) => {
const x = showExploded ? part.target.x : part.origin.x;
const y = showExploded ? part.target.y : part.origin.y;
const getIcon = () => {
const n = part.name.toLowerCase();
if (n.includes('处理器') || n.includes('cpu')) return ;
if (n.includes('电池') || n.includes('energy')) return ;
if (n.includes('存储') || n.includes('ssd')) return ;
if (n.includes('冷却') || n.includes('fan')) return ;
return ;
};
return (
onMouseDown(e, part.id, 'target')}
className={`absolute w-60 h-32 -translate-x-1/2 -translate-y-1/2 cursor-move pointer-events-auto z-40
${isSelected ? 'z-[45]' : ''}
${!isDraggingAny ? 'transition-all duration-700 cubic-bezier(0.25, 1, 0.5, 1)' : ''}`}
style={{ left: `${x}%`, top: `${y}%` }}
>
COST:
${part.price || '0.00'}
{getIcon()}
{part.name}
{part.description || 'SPECIFICATION_NOT_DEFINED'}
);
});
const INITIAL_GROUPS = [
{
id: 'g1',
title: '系统架构拓扑 - 多样性物料',
image: 'https://images.unsplash.com/photo-1581091226825-a6a2a5aee158?auto=format&fit=crop&q=80&w=1600',
parts: [
{ id: 'p1-1', name: '独立感应器', description: '环境监测模块\nIP67工业防护等级\n实时光敏反馈回路', price: '45.00', color: '#10b981', lineColor: '#059669', origin: { x: 20, y: 70 }, target: { x: 15, y: 35 }, waypoint: { x: 20, y: 55 }, pointRadius: 30, lineWidth: 2, lineType: 'straight', dashLength: 4, circleDash: 2, lineSpeed: 2.0, circleSpeed: 4.0 },
{ id: 'p1-2', name: '处理器 A (主)', description: '核心逻辑运算单元\n共享总线节点\n分布式指令架构', price: '299.00', color: '#ef4444', lineColor: '#b91c1c', origin: { x: 50, y: 50 }, target: { x: 35, y: 15 }, waypoint: { x: 45, y: 35 }, pointRadius: 40, lineWidth: 2.5, lineType: 'polyline', dashLength: 2, circleDash: 1.5, lineSpeed: 2.0, circleSpeed: 4.0 },
{ id: 'p1-3', name: '处理器 B (从)', description: '冗余热备模块\n自动继承父级配色\n共享起点布线逻辑', price: '299.00', color: '#ef4444', lineColor: '#b91c1c', origin: { x: 50, y: 50 }, target: { x: 65, y: 15 }, waypoint: { x: 55, y: 35 }, pointRadius: 40, lineWidth: 2.5, lineType: 'polyline', dashLength: 2, circleDash: 1.5, lineSpeed: 2.0, circleSpeed: 4.0 },
{ id: 'p1-4', name: '电池模组', description: '5000mAh 动力包\n共享能源转接点\n内置热管理网格', price: '120.00', color: '#3b82f6', lineColor: '#1d4ed8', origin: { x: 80, y: 60 }, target: { x: 75, y: 85 }, waypoint: { x: 85, y: 75 }, pointRadius: 35, lineWidth: 2, lineType: 'polyline', dashLength: 2, circleDash: 1.5, lineSpeed: 2.0, circleSpeed: 5.0 },
]
}
];
const App = () => {
const [isEditMode, setIsEditMode] = useState(true);
const [isExploded, setIsExploded] = useState(true);
const [topoVisible, setTopoVisible] = useState(true); // 核心:控制线条与节点的显示
const [groups, setGroups] = useState(INITIAL_GROUPS);
const [activeGroupIndex, setActiveGroupIndex] = useState(0);
const [selectedPartIds, setSelectedPartIds] = useState([INITIAL_GROUPS[0].parts[1].id]);
const [dragState, setDragState] = useState({ active: false, type: 'target', affectedIds: [] });
const [canvasSize, setCanvasSize] = useState({ width: 1, height: 1 });
const canvasRef = useRef(null);
const dragStartRef = useRef({ x: 0, y: 0 });
const initialPositionsRef = useRef({});
const currentGroup = groups[activeGroupIndex];
const lastSelectedPart = currentGroup.parts.find(p => p.id === selectedPartIds[selectedPartIds.length - 1]);
// 监听炸开状态,实现逻辑上的延迟显示
useEffect(() => {
if (isEditMode) {
setTopoVisible(true);
return;
}
if (isExploded) {
// 炸开:组件移动耗时 700ms,我们延迟 800ms 后显示线条
const timer = setTimeout(() => setTopoVisible(true), 800);
return () => clearTimeout(timer);
} else {
// 收回:线条立即消失
setTopoVisible(false);
}
}, [isExploded, isEditMode]);
const uniqueOrigins = useMemo(() => {
const seen = new Set();
return currentGroup.parts.filter(p => {
const key = `${Math.round(p.origin.x)}-${Math.round(p.origin.y)}`;
if (seen.has(key)) return false;
seen.add(key);
return true;
});
}, [currentGroup.parts]);
const uniqueWaypoints = useMemo(() => {
const seen = new Set();
return currentGroup.parts.filter(p => {
if (p.lineType !== 'polyline') return false;
const key = `${Math.round(p.waypoint.x)}-${Math.round(p.waypoint.y)}`;
if (seen.has(key)) return false;
seen.add(key);
return true;
});
}, [currentGroup.parts]);
useEffect(() => {
const updateSize = () => {
if (canvasRef.current) {
setCanvasSize({ width: canvasRef.current.offsetWidth, height: canvasRef.current.offsetHeight });
}
};
updateSize();
window.addEventListener('resize', updateSize);
return () => window.removeEventListener('resize', updateSize);
}, []);
const updatePart = (partId, field, value) => {
setGroups(prev => {
const n = [...prev];
n[activeGroupIndex] = {
...n[activeGroupIndex],
parts: n[activeGroupIndex].parts.map(p => p.id === partId ? { ...p, [field]: value } : p)
};
return n;
});
};
const bulkUpdate = (field, value) => {
setGroups(prev => {
const n = [...prev];
n[activeGroupIndex] = {
...n[activeGroupIndex],
parts: n[activeGroupIndex].parts.map(p => selectedPartIds.includes(p.id) ? { ...p, [field]: value } : p)
};
return n;
});
};
const handleBranch = () => {
if (!lastSelectedPart) return;
const newId = 'p-branch-' + Date.now();
const newPart = {
...lastSelectedPart,
id: newId,
name: `${lastSelectedPart.name} 分支`,
target: { x: lastSelectedPart.target.x + 10, y: lastSelectedPart.target.y + 10 },
color: lastSelectedPart.color,
description: `基于 ${lastSelectedPart.name} 的拓扑分支\n自动同步父级配色与节点\n规格支持独立定义`
};
setGroups(prev => {
const n = [...prev];
n[activeGroupIndex].parts = [...n[activeGroupIndex].parts, newPart];
return n;
});
setSelectedPartIds([newId]);
};
const handleMouseDown = (e, partId, dragType) => {
if (!isEditMode) return;
e.stopPropagation();
const targetPart = currentGroup.parts.find(p => p.id === partId);
let affectedIds = [partId];
if (dragType === 'origin' || dragType === 'waypoint') {
affectedIds = currentGroup.parts.filter(p =>
Math.round(p[dragType].x) === Math.round(targetPart[dragType].x) &&
Math.round(p[dragType].y) === Math.round(targetPart[dragType].y)
).map(p => p.id);
}
setSelectedPartIds(affectedIds);
setDragState({ active: true, type: dragType, affectedIds });
const rect = canvasRef.current.getBoundingClientRect();
dragStartRef.current = {
x: ((e.clientX - rect.left) / rect.width) * 100,
y: ((e.clientY - rect.top) / rect.height) * 100
};
const initPos = {};
currentGroup.parts.forEach(p => {
if (affectedIds.includes(p.id)) {
initPos[p.id] = { ...p[dragType] };
}
});
initialPositionsRef.current = initPos;
};
const handleMouseMove = useCallback((e) => {
if (!isEditMode || !dragState.active || !canvasRef.current) return;
const rect = canvasRef.current.getBoundingClientRect();
const currentX = ((e.clientX - rect.left) / rect.width) * 100;
const currentY = ((e.clientY - rect.top) / rect.height) * 100;
const dx = currentX - dragStartRef.current.x;
const dy = currentY - dragStartRef.current.y;
const coordToUpdate = dragState.type;
setGroups(prev => {
const n = [...prev];
const g = n[activeGroupIndex];
n[activeGroupIndex] = {
...g,
parts: g.parts.map(p => {
if (dragState.affectedIds.includes(p.id)) {
const initPos = initialPositionsRef.current[p.id];
return {
...p,
[coordToUpdate]: {
x: Math.round((initPos.x + dx) * 10) / 10,
y: Math.round((initPos.y + dy) * 10) / 10
}
};
}
return p;
})
};
return n;
});
}, [dragState, isEditMode, activeGroupIndex]);
const handleMouseUp = useCallback(() => {
setDragState({ active: false, type: 'target', affectedIds: [] });
}, []);
useEffect(() => {
if (dragState.active) {
window.addEventListener('mousemove', handleMouseMove);
window.addEventListener('mouseup', handleMouseUp);
}
return () => {
window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('mouseup', handleMouseUp);
};
}, [dragState.active, handleMouseMove, handleMouseUp]);
const getLeaderPath = (part) => {
if (part.lineType === 'polyline') {
return `M ${part.origin.x} ${part.origin.y} L ${part.waypoint.x} ${part.waypoint.y} L ${part.target.x} ${part.target.y}`;
}
return `M ${part.origin.x} ${part.origin.y} L ${part.target.x} ${part.target.y}`;
};
return (
Blueprint Topology
Visual_Sync v15.0
{isEditMode && (
)}
isEditMode && setSelectedPartIds([])}>
{/* 线条渲染层 - 使用逻辑状态 topoVisible 控制显示 */}
{/* 节点圆环渲染层 - 同步 topoVisible */}
{/* 物料卡片渲染层 */}
{currentGroup.parts.map(part => (
))}
{/* 编辑句柄 - 起点 */}
{isEditMode && uniqueOrigins.map(part => (
handleMouseDown(e, part.id, 'origin')}
className={`absolute -translate-x-1/2 -translate-y-1/2 cursor-move z-[55] rounded-full transition-all
${selectedPartIds.includes(part.id) ? 'bg-yellow-400/20 node-pulse' : 'hover:bg-black/10'}`}
style={{ left: `${part.origin.x}%`, top: `${part.origin.y}%`, width: `${part.pointRadius * 1.5}px`, height: `${part.pointRadius * 1.5}px` }}
/>
))}
{/* 编辑句柄 - 中继点 - 同步 topoVisible */}
{uniqueWaypoints.map(part => {
const isSelected = selectedPartIds.includes(part.id);
return (
handleMouseDown(e, part.id, 'waypoint')}
className={`absolute w-4 h-4 -translate-x-1/2 -translate-y-1/2 bg-white border-2 rounded-full cursor-move z-[70] shadow-md junction-node
${isSelected ? 'border-yellow-500 scale-125 bg-yellow-50 shadow-lg' : 'border-neutral-900'}`}
style={{
left: `${part.waypoint.x}%`,
top: `${part.waypoint.y}%`,
opacity: topoVisible ? 1 : 0,
pointerEvents: topoVisible ? 'auto' : 'none'
}}
>
{(isSelected && isEditMode) &&
Junction
}
);
})}
{!isEditMode && (
)}
);
};
export default App;