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 && (