diff --git a/sample/DisassemblyEditPreviewPage.html b/sample/DisassemblyEditPreviewPage.html new file mode 100644 index 0000000..0767e16 --- /dev/null +++ b/sample/DisassemblyEditPreviewPage.html @@ -0,0 +1,515 @@ +import React, { useState, useCallback, useRef } from 'react'; +import { + Cpu, + Settings, + Anchor, + ChevronUp, + ChevronDown, + Plus, + Trash2, + Image, + Layers, + FileText, + ChevronRight, + RotateCw, + Target, + Move +} from 'lucide-react'; + +/** + * Disassembly Edit/Preview Page + * 融合 DisassemblyPages(视觉展示)与 DisassemblyLinkedProductsPage(交互编辑) + * + * 编辑模式:左侧属性面板 + 点击选中导航组件 + 支持新增/删除/修改名称/代码/图片 + * 预览模式:与 DisassemblyPages v7.2.0 一致的工业草稿风格展示 + */ + +const styles = ` + @keyframes scanline { + 0% { transform: translateY(-100%); } + 100% { transform: translateY(100%); } + } + @keyframes shimmer { + 0% { transform: translateX(-100%); } + 100% { transform: translateX(300%); } + } + @keyframes node-pulse-kf { + 0% { box-shadow: 0 0 0 0px rgba(234, 179, 8, 0.5); } + 100% { box-shadow: 0 0 0 10px rgba(234, 179, 8, 0); } + } + .blueprint-grid { + background-color: #ffffff; + background-image: + linear-gradient(rgba(0, 0, 0, 0.05) 1px, transparent 1px), + linear-gradient(90deg, rgba(0, 0, 0, 0.05) 1px, transparent 1px); + background-size: 30px 30px; + } + .blueprint-grid::after { + content: ""; + position: absolute; + inset: 0; + background-image: + linear-gradient(rgba(0, 0, 0, 0.02) 1px, transparent 1px), + linear-gradient(90deg, rgba(0, 0, 0, 0.02) 1px, transparent 1px); + background-size: 10px 10px; + pointer-events: none; + } + .assembly-view { + filter: drop-shadow(0 15px 30px rgba(0,0,0,0.1)) contrast(1.05); + transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1); + } + .assembly-view:hover { + filter: drop-shadow(0 20px 40px rgba(0,0,0,0.15)) contrast(1.1); + } + .leader-line-svg { + transition: stroke 0.4s ease, stroke-width 0.4s ease; + } + .text-label-container { + transition: all 0.4s cubic-bezier(0.23, 1, 0.32, 1); + } + .scan-effect { + position: absolute; + top: 0; left: 0; right: 0; height: 100%; + background: linear-gradient(to bottom, transparent, rgba(0, 0, 0, 0.01), transparent); + animation: scanline 15s linear infinite; + pointer-events: none; + } + .node-selected { + animation: node-pulse-kf 2s infinite; + outline: 2px solid #eab308; + outline-offset: 4px; + } + .custom-scrollbar::-webkit-scrollbar { width: 4px; } + .custom-scrollbar::-webkit-scrollbar-thumb { background: #000; border-radius: 10px; } + .part-hover { transition: all 0.35s cubic-bezier(0.23, 1, 0.32, 1); } +`; + +const DEFAULT_PARTS = [ + { id: 'p1', name: '环境监控', code: 'ENV-X', img: 'https://images.unsplash.com/photo-1555680202-c86f0e12f086?auto=format&fit=crop&q=80&w=200' }, + { id: 'p2', name: '能源模组', code: 'BATT-V8', img: 'https://images.unsplash.com/photo-1619641259501-c88f28c6e355?auto=format&fit=crop&q=80&w=200' }, + { id: 'p3', name: '雷达阵列', code: 'LDR-07', img: 'https://images.unsplash.com/photo-1555680202-c86f0e12f086?auto=format&fit=crop&q=80&w=200' }, + { id: 'p4', name: '核心总成', code: 'CORE-MAX', img: 'https://images.unsplash.com/photo-1518770660439-4636190af475?auto=format&fit=crop&q=80&w=200' }, + { id: 'p5', name: '液压单元', code: 'HYD-02', img: 'https://images.unsplash.com/photo-1635350736475-c8cef4b21906?auto=format&fit=crop&q=80&w=200' }, + { id: 'p6', name: '散热单元', code: 'COOL-F2', img: 'https://images.unsplash.com/photo-1635350736475-c8cef4b21906?auto=format&fit=crop&q=80&w=200' }, + { id: 'p7', name: '存储阵列', code: 'DATA-2T', img: 'https://images.unsplash.com/photo-1544006659-f0b21f04cb1d?auto=format&fit=crop&q=80&w=200' }, +]; + +const DEFAULT_ASSEMBLY = 'https://images.unsplash.com/photo-1581092160562-40aa08e78837?auto=format&fit=crop&q=80&w=1600'; + +const ICON_SIZE = 80; +const CENTER_AXIS_Y = 180; +const OFFSET_Y = 40; + +// ─── 单个组件节点(共用于编辑 & 预览)───────────────────────────────────── +function PartNode({ part, index, isHovered, isSelected, isEditMode, onHover, onClick }) { + const isUp = index % 2 === 0; + const boxTop = isUp ? CENTER_AXIS_Y - OFFSET_Y : CENTER_AXIS_Y + OFFSET_Y; + const SVG_CENTER_X = 50; + const SVG_CENTER_Y = 40; + + const lineColor = isSelected + ? '#eab308' + : isHovered + ? '#000' + : '#f5f5f5'; + const lineWidth = isSelected ? '2.5' : isHovered ? '2.5' : '1'; + + return ( +
onHover(part.id)} + onMouseLeave={() => onHover(null)} + onClick={() => isEditMode && onClick(part.id)} + className={`group flex flex-col items-center relative flex-1 + ${isEditMode ? 'cursor-pointer' : 'cursor-default'} + ${isSelected ? 'z-30' : 'z-10'}`} + style={{ minWidth: 0 }} + > + {/* 垂直引导线 */} + + + + + + + {/* 图标节点 */} +
+ {part.name} + {(isHovered || isSelected) && ( +
+ )} + {isEditMode && ( +
+ +
+ )} +
+ + {/* 标签容器 */} +
+ {/* 接线端子 */} +
+ {/* 指示器 */} +
+ {isUp ? :
} +
+ CODE.0{index + 1} +
+ {!isUp ? :
} +
+ {/* 大字名称 */} +
+ {part.name} +
+
+
+ ); +} + +// ─── 主组件 ──────────────────────────────────────────────────────────────── +export default function App() { + const [isEditMode, setIsEditMode] = useState(true); + const [parts, setParts] = useState(DEFAULT_PARTS); + const [assemblyImg, setAssemblyImg] = useState(DEFAULT_ASSEMBLY); + const [hoveredId, setHoveredId] = useState(null); + const [selectedId, setSelectedId] = useState(null); + const [imgInputVal, setImgInputVal] = useState(''); + + const selectedPart = parts.find(p => p.id === selectedId) || null; + const idxOf = (id) => parts.findIndex(p => p.id === id); + + // ── 更新选中组件字段 ── + const updateField = useCallback((field, value) => { + if (!selectedId) return; + setParts(prev => prev.map(p => p.id === selectedId ? { ...p, [field]: value } : p)); + }, [selectedId]); + + // ── 新增组件 ── + const addPart = () => { + const newPart = { + id: 'p-' + Date.now(), + name: '新组件', + code: 'NEW-00', + img: 'https://images.unsplash.com/photo-1518770660439-4636190af475?auto=format&fit=crop&q=80&w=200', + }; + setParts(prev => [...prev, newPart]); + setSelectedId(newPart.id); + }; + + // ── 删除选中组件 ── + const deletePart = () => { + if (!selectedId) return; + setParts(prev => prev.filter(p => p.id !== selectedId)); + setSelectedId(null); + }; + + // ── 上移 / 下移 ── + const movePart = (dir) => { + const idx = idxOf(selectedId); + if (idx < 0) return; + const next = idx + dir; + if (next < 0 || next >= parts.length) return; + setParts(prev => { + const arr = [...prev]; + [arr[idx], arr[next]] = [arr[next], arr[idx]]; + return arr; + }); + }; + + // ── 切换模式 ── + const enterPreview = () => { + setIsEditMode(false); + setSelectedId(null); + }; + const enterEdit = () => setIsEditMode(true); + + return ( +
+ + + {/* ── 顶部标题栏 ────────────────────────────────────── */} +
+
+
+ +
+
+

+ Disassembly_Editor +

+

+ EDIT_PREVIEW_v1.0.0 // VISUAL_ENHANCED +

+
+
+ + {/* 模式切换 */} +
+ + +
+ +
+
+ +
+ {isEditMode ? 'MODE: EDIT' : 'MODE: PREVIEW'} + +
+
+ +
+
+ + {/* ── 主体区域 ────────────────────────────────────── */} +
+ + {/* ── 左侧编辑面板(仅编辑模式)─────────────────── */} + {isEditMode && ( + + )} + + {/* ── 右侧主画布 ─────────────────────────────────── */} +
isEditMode && setSelectedId(null)} + > +
+
+ + {/* 中心工业总成图 */} +
+ {/* 角标 */} +
+
+
+
+ + Central Assembly System + + {/* 编辑模式标注 */} + {isEditMode && ( +
+
+ + 双击替换图片 + +
+ )} +
+ + {/* 底部组件导航 */} +
e.stopPropagation()} + > + {parts.map((part, index) => ( + setSelectedId(prev => prev === id ? null : id)} + /> + ))} +
+ + {/* 预览模式按钮 */} + {!isEditMode && ( +
+ +
+ )} +
+
+ + {/* ── 页脚 ───────────────────────────────────────── */} +
+
+
+ + Disassembly_EditPreview +
+ | + + {parts.length} NODE{parts.length !== 1 ? 'S' : ''} // {isEditMode ? 'EDIT' : 'PREVIEW'}_LOCK + +
+
+ v1.0.0 +
+
+
+ ); +} diff --git a/sample/DisassemblyLinkedProductsPage.html b/sample/DisassemblyLinkedProductsPage.html new file mode 100644 index 0000000..9497993 --- /dev/null +++ b/sample/DisassemblyLinkedProductsPage.html @@ -0,0 +1,568 @@ +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 && ( +