gbmake-payload/sample/DisassemblyEditPreviewPage....

516 lines
24 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
);
}