516 lines
24 KiB
HTML
516 lines
24 KiB
HTML
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 (
|
||
<div
|
||
onMouseEnter={() => 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 }}
|
||
>
|
||
{/* 垂直引导线 */}
|
||
<svg
|
||
className="absolute top-0 left-1/2 -translate-x-1/2 w-[100px] h-[400px] overflow-visible pointer-events-none z-0"
|
||
viewBox="0 0 100 400"
|
||
>
|
||
<path
|
||
d={`M ${SVG_CENTER_X} ${SVG_CENTER_Y} L ${SVG_CENTER_X} ${boxTop}`}
|
||
fill="none"
|
||
stroke={lineColor}
|
||
strokeWidth={lineWidth}
|
||
strokeDasharray={isSelected || isHovered ? 'none' : '3,3'}
|
||
className="leader-line-svg"
|
||
/>
|
||
<circle cx={SVG_CENTER_X} cy={SVG_CENTER_Y} r="2.5"
|
||
fill={isSelected ? '#eab308' : isHovered ? '#000' : '#e0e0e0'} />
|
||
<circle cx={SVG_CENTER_X} cy={boxTop} r={isSelected || isHovered ? '4' : '2'}
|
||
fill={isSelected ? '#eab308' : isHovered ? '#000' : '#e0e0e0'} />
|
||
</svg>
|
||
|
||
{/* 图标节点 */}
|
||
<div
|
||
className={`relative flex items-center justify-center transition-all duration-500 z-10 part-hover rounded-sm
|
||
${isHovered || isSelected ? 'scale-110 -translate-y-2' : ''}
|
||
${isSelected ? 'node-selected' : ''}`}
|
||
style={{ width: `${ICON_SIZE}px`, height: `${ICON_SIZE}px` }}
|
||
>
|
||
<img src={part.img} className="w-16 h-16 object-contain relative z-10" alt={part.name} />
|
||
{(isHovered || isSelected) && (
|
||
<div className="absolute inset-0 bg-neutral-900/5 blur-2xl rounded-full scale-125" />
|
||
)}
|
||
{isEditMode && (
|
||
<div className="absolute -top-1 -right-1 w-4 h-4 bg-neutral-800 text-white flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity rounded-full z-20">
|
||
<Move size={9} />
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* 标签容器 */}
|
||
<div
|
||
className={`absolute flex flex-col items-center whitespace-nowrap text-label-container z-30
|
||
${isHovered || isSelected ? (isUp ? '-translate-y-1' : 'translate-y-1') : ''}`}
|
||
style={{ top: `${boxTop}px`, left: '50%', transform: 'translateX(-50%)' }}
|
||
>
|
||
{/* 接线端子 */}
|
||
<div className={`w-2.5 h-2.5 rounded-full border-2 border-white mb-3 transition-all
|
||
${isSelected ? 'bg-yellow-400 scale-150' : isHovered ? 'bg-neutral-900 scale-125' : 'bg-neutral-200'}`}
|
||
/>
|
||
{/* 指示器 */}
|
||
<div className="flex items-center gap-1.5 mb-1.5 opacity-60">
|
||
{isUp ? <ChevronUp size={8} /> : <div className="w-[8px]" />}
|
||
<div className={`text-[8px] font-black uppercase tracking-widest transition-colors
|
||
${isHovered || isSelected ? 'text-neutral-900' : 'text-neutral-300'}`}>
|
||
CODE.0{index + 1}
|
||
</div>
|
||
{!isUp ? <ChevronDown size={8} /> : <div className="w-[8px]" />}
|
||
</div>
|
||
{/* 大字名称 */}
|
||
<div className={`text-2xl font-black uppercase tracking-tighter transition-all duration-300
|
||
${isSelected ? 'text-yellow-500 scale-105' : isHovered ? 'text-neutral-900 scale-105' : 'text-neutral-400'}`}>
|
||
{part.name}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// ─── 主组件 ────────────────────────────────────────────────────────────────
|
||
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 (
|
||
<div className="h-screen w-screen bg-[#ffffff] text-neutral-900 font-mono flex flex-col overflow-hidden select-none">
|
||
<style>{styles}</style>
|
||
|
||
{/* ── 顶部标题栏 ────────────────────────────────────── */}
|
||
<header className="h-14 border-b-2 border-neutral-900 bg-white flex items-center justify-between px-8 z-50 shadow-sm">
|
||
<div className="flex items-center gap-4">
|
||
<div className="p-1.5 bg-neutral-900 rounded-sm">
|
||
<Cpu size={18} className="text-white" />
|
||
</div>
|
||
<div className="flex flex-col">
|
||
<h1 className="text-[13px] font-black uppercase tracking-[0.35em] leading-none text-neutral-900">
|
||
Disassembly_Editor
|
||
</h1>
|
||
<p className="text-[9px] text-neutral-400 mt-0.5 font-bold tracking-tighter">
|
||
EDIT_PREVIEW_v1.0.0 // VISUAL_ENHANCED
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 模式切换 */}
|
||
<div className="flex items-center bg-neutral-100 p-1 border-2 border-neutral-900 rounded-sm">
|
||
<button
|
||
onClick={enterEdit}
|
||
className={`px-5 py-1.5 text-[10px] font-black transition-all uppercase tracking-widest
|
||
${isEditMode ? 'bg-black text-white shadow-inner' : 'text-neutral-500 hover:text-black'}`}
|
||
>
|
||
编辑模式
|
||
</button>
|
||
<button
|
||
onClick={enterPreview}
|
||
className={`px-5 py-1.5 text-[10px] font-black transition-all uppercase tracking-widest
|
||
${!isEditMode ? 'bg-black text-white shadow-inner' : 'text-neutral-500 hover:text-black'}`}
|
||
>
|
||
预览展示
|
||
</button>
|
||
</div>
|
||
|
||
<div className="flex items-center gap-6 text-neutral-400">
|
||
<div className="flex flex-col items-end text-[9px] font-black uppercase tracking-widest">
|
||
<span className="flex items-center gap-2">
|
||
<div className="w-1.5 h-1.5 bg-neutral-900 rounded-full animate-pulse" />
|
||
{isEditMode ? 'MODE: EDIT' : 'MODE: PREVIEW'}
|
||
</span>
|
||
</div>
|
||
<div className="h-8 w-px bg-neutral-200" />
|
||
<Settings size={16} className="hover:text-neutral-900 cursor-pointer transition-colors" />
|
||
</div>
|
||
</header>
|
||
|
||
{/* ── 主体区域 ────────────────────────────────────── */}
|
||
<div className="flex-1 flex overflow-hidden">
|
||
|
||
{/* ── 左侧编辑面板(仅编辑模式)─────────────────── */}
|
||
{isEditMode && (
|
||
<aside className="w-72 bg-white border-r-2 border-neutral-900 z-40 flex flex-col overflow-y-auto custom-scrollbar shadow-xl">
|
||
|
||
{/* 中央图片设置 */}
|
||
<div className="p-5 border-b-2 border-neutral-100 space-y-3">
|
||
<h2 className="text-[10px] font-black uppercase tracking-widest text-neutral-500 flex items-center gap-2">
|
||
<Image size={12} /> 中央组装图
|
||
</h2>
|
||
<div className="relative w-full aspect-video border-2 border-neutral-200 overflow-hidden bg-neutral-50">
|
||
<img src={assemblyImg} className="w-full h-full object-cover filter grayscale" alt="Assembly" />
|
||
</div>
|
||
<input
|
||
type="text"
|
||
value={imgInputVal}
|
||
onChange={e => setImgInputVal(e.target.value)}
|
||
placeholder="粘贴图片 URL..."
|
||
className="w-full border-2 border-neutral-900 px-2 py-2 text-[10px] font-bold outline-none focus:bg-yellow-50 transition-colors"
|
||
/>
|
||
<button
|
||
onClick={() => { if (imgInputVal.trim()) { setAssemblyImg(imgInputVal.trim()); setImgInputVal(''); } }}
|
||
className="w-full py-2 bg-neutral-900 text-white text-[10px] font-black uppercase tracking-widest hover:bg-neutral-700 transition-colors flex items-center justify-center gap-2"
|
||
>
|
||
<RotateCw size={12} /> 替换图片
|
||
</button>
|
||
</div>
|
||
|
||
{/* 组件列表 */}
|
||
<div className="p-5 border-b-2 border-neutral-100 space-y-2">
|
||
<div className="flex items-center justify-between">
|
||
<h2 className="text-[10px] font-black uppercase tracking-widest text-neutral-500 flex items-center gap-2">
|
||
<Layers size={12} /> 组件列表
|
||
<span className="bg-neutral-100 border border-neutral-300 text-[9px] px-1.5 py-0.5 rounded font-bold">{parts.length}</span>
|
||
</h2>
|
||
<button
|
||
onClick={addPart}
|
||
className="flex items-center gap-1 px-2 py-1 bg-black text-white text-[9px] font-black uppercase hover:bg-neutral-700 transition-colors shadow-[2px_2px_0px_#ccc]"
|
||
>
|
||
<Plus size={10} /> 添加
|
||
</button>
|
||
</div>
|
||
<div className="space-y-1 max-h-36 overflow-y-auto custom-scrollbar">
|
||
{parts.map((p, i) => (
|
||
<button
|
||
key={p.id}
|
||
onClick={() => setSelectedId(p.id)}
|
||
className={`w-full flex items-center gap-2 px-3 py-2 border text-left transition-all
|
||
${selectedId === p.id
|
||
? 'border-yellow-400 bg-yellow-50 shadow-[2px_2px_0px_#000]'
|
||
: 'border-neutral-200 hover:border-neutral-400 hover:bg-neutral-50'}`}
|
||
>
|
||
<img src={p.img} className="w-7 h-7 object-contain bg-neutral-100 border border-neutral-200 flex-none" alt="" />
|
||
<div className="flex-1 min-w-0">
|
||
<div className="text-[10px] font-black uppercase truncate">{p.name}</div>
|
||
<div className="text-[8px] text-neutral-400 font-bold">{p.code} · #{i + 1}</div>
|
||
</div>
|
||
{selectedId === p.id && <div className="w-1.5 h-1.5 rounded-full bg-yellow-400 flex-none" />}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* 选中组件属性编辑 */}
|
||
{selectedPart ? (
|
||
<div className="flex-1 p-5 space-y-5">
|
||
<h2 className="text-[10px] font-black uppercase tracking-widest text-neutral-500 flex items-center justify-between">
|
||
<span className="flex items-center gap-2"><FileText size={12} /> 属性编辑</span>
|
||
<span className="text-[8px] bg-yellow-400 px-2 py-0.5 font-bold border border-yellow-500">{selectedPart.code}</span>
|
||
</h2>
|
||
|
||
<div className="space-y-4 p-4 border-2 border-neutral-900 bg-neutral-50 rounded-sm shadow-sm">
|
||
<div className="space-y-1.5">
|
||
<label className="text-[10px] font-bold text-neutral-400 uppercase tracking-tighter">组件名称</label>
|
||
<input
|
||
type="text"
|
||
value={selectedPart.name}
|
||
onChange={e => updateField('name', e.target.value)}
|
||
className="w-full border-2 border-neutral-900 px-2 py-2 text-xs font-bold outline-none focus:bg-white transition-colors"
|
||
/>
|
||
</div>
|
||
<div className="space-y-1.5">
|
||
<label className="text-[10px] font-bold text-neutral-400 uppercase tracking-tighter">编号代码</label>
|
||
<input
|
||
type="text"
|
||
value={selectedPart.code}
|
||
onChange={e => updateField('code', e.target.value)}
|
||
className="w-full border-2 border-neutral-900 px-2 py-2 text-xs font-bold outline-none focus:bg-white transition-colors"
|
||
/>
|
||
</div>
|
||
<div className="space-y-1.5">
|
||
<label className="text-[10px] font-bold text-neutral-400 uppercase tracking-tighter">图片 URL</label>
|
||
<input
|
||
type="text"
|
||
value={selectedPart.img}
|
||
onChange={e => updateField('img', e.target.value)}
|
||
className="w-full border-2 border-neutral-900 px-2 py-2 text-[10px] font-bold outline-none focus:bg-white transition-colors"
|
||
/>
|
||
<img src={selectedPart.img} alt="" className="w-full h-20 object-contain bg-neutral-100 border border-neutral-200 mt-1" />
|
||
</div>
|
||
</div>
|
||
|
||
{/* 排序 & 删除 */}
|
||
<div className="flex gap-2">
|
||
<button onClick={() => movePart(-1)} className="flex-1 py-2 border-2 border-neutral-900 text-[10px] font-black uppercase hover:bg-neutral-50 transition-all flex items-center justify-center gap-1 shadow-[3px_3px_0px_#ddd]">
|
||
<ChevronUp size={12} /> 上移
|
||
</button>
|
||
<button onClick={() => movePart(1)} className="flex-1 py-2 border-2 border-neutral-900 text-[10px] font-black uppercase hover:bg-neutral-50 transition-all flex items-center justify-center gap-1 shadow-[3px_3px_0px_#ddd]">
|
||
<ChevronDown size={12} /> 下移
|
||
</button>
|
||
</div>
|
||
<button
|
||
onClick={deletePart}
|
||
className="w-full py-2.5 bg-red-50 text-red-600 border-2 border-red-200 text-[10px] font-black hover:bg-red-600 hover:text-white transition-all flex items-center justify-center gap-2 uppercase tracking-tighter"
|
||
>
|
||
<Trash2 size={12} /> Delete_Node
|
||
</button>
|
||
</div>
|
||
) : (
|
||
<div className="flex-1 flex items-center justify-center p-8">
|
||
<div className="text-center text-neutral-300 text-[10px] font-bold uppercase italic border-2 border-dashed border-neutral-200 p-8 leading-relaxed">
|
||
<Target size={24} className="mx-auto mb-3 opacity-30" />
|
||
点击左侧列表或画布<br />节点以选中组件
|
||
</div>
|
||
</div>
|
||
)}
|
||
</aside>
|
||
)}
|
||
|
||
{/* ── 右侧主画布 ─────────────────────────────────── */}
|
||
<main
|
||
className="flex-1 relative flex flex-col items-center justify-center p-12 overflow-hidden"
|
||
onClick={() => isEditMode && setSelectedId(null)}
|
||
>
|
||
<div className="absolute inset-0 blueprint-grid" />
|
||
<div className="scan-effect" />
|
||
|
||
{/* 中心工业总成图 */}
|
||
<div className="relative w-full flex-1 max-w-4xl max-h-[35vh] flex items-center justify-center z-10">
|
||
{/* 角标 */}
|
||
<div className="absolute -inset-4 pointer-events-none opacity-40">
|
||
<div className="absolute top-0 left-0 w-12 h-12 border-t border-l border-neutral-600" />
|
||
<div className="absolute bottom-0 right-0 w-12 h-12 border-b border-r border-neutral-600" />
|
||
</div>
|
||
|
||
<img
|
||
src={assemblyImg}
|
||
className="max-w-full max-h-full object-contain assembly-view"
|
||
alt="Central Assembly System"
|
||
/>
|
||
|
||
{/* 编辑模式标注 */}
|
||
{isEditMode && (
|
||
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
|
||
<div className="border-2 border-dashed border-yellow-400/40 w-full h-full rounded-sm" />
|
||
<span className="absolute bottom-2 right-2 text-[8px] font-black text-yellow-500 uppercase bg-white/80 px-2 py-1 border border-yellow-300">
|
||
双击替换图片
|
||
</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* 底部组件导航 */}
|
||
<div
|
||
className="w-full max-w-[1300px] mt-12 flex justify-between z-20 pb-72 px-10"
|
||
onClick={e => e.stopPropagation()}
|
||
>
|
||
{parts.map((part, index) => (
|
||
<PartNode
|
||
key={part.id}
|
||
part={part}
|
||
index={index}
|
||
isHovered={hoveredId === part.id}
|
||
isSelected={selectedId === part.id}
|
||
isEditMode={isEditMode}
|
||
onHover={setHoveredId}
|
||
onClick={(id) => setSelectedId(prev => prev === id ? null : id)}
|
||
/>
|
||
))}
|
||
</div>
|
||
|
||
{/* 预览模式按钮 */}
|
||
{!isEditMode && (
|
||
<div className="absolute bottom-12 flex flex-col items-center z-[120]">
|
||
<button
|
||
onClick={enterEdit}
|
||
className="bg-black text-white px-14 py-5 font-black uppercase text-[10px] tracking-[0.4em] shadow-[18px_18px_0px_#ccc] hover:shadow-none hover:translate-x-2.5 hover:translate-y-2.5 transition-all flex items-center gap-5 group border-2 border-neutral-900"
|
||
>
|
||
ENTER_EDIT_MODE <ChevronRight size={18} className="group-hover:translate-x-2 transition-transform" />
|
||
</button>
|
||
</div>
|
||
)}
|
||
</main>
|
||
</div>
|
||
|
||
{/* ── 页脚 ───────────────────────────────────────── */}
|
||
<footer className="h-10 bg-white border-t-2 border-neutral-900 flex items-center justify-between px-8 text-[9px] font-bold text-neutral-300 uppercase tracking-[0.4em]">
|
||
<div className="flex gap-8 items-center">
|
||
<div className="flex items-center gap-2 text-neutral-900">
|
||
<Anchor size={12} className="opacity-20" />
|
||
<span className="tracking-widest opacity-70 italic">Disassembly_EditPreview</span>
|
||
</div>
|
||
<span className="opacity-20">|</span>
|
||
<span className="opacity-40 tracking-tighter">
|
||
{parts.length} NODE{parts.length !== 1 ? 'S' : ''} // {isEditMode ? 'EDIT' : 'PREVIEW'}_LOCK
|
||
</span>
|
||
</div>
|
||
<div className="flex gap-3 items-center">
|
||
<span className="text-neutral-900 font-black px-3 py-1 bg-neutral-50 border border-neutral-200">v1.0.0</span>
|
||
</div>
|
||
</footer>
|
||
</div>
|
||
);
|
||
}
|