* refactor(editor): 提取行为树编辑器为独立包并重构编辑器架构 * feat(editor): 添加插件市场功能 * feat(editor): 重构插件市场以支持版本管理和ZIP打包 * feat(editor): 重构插件发布流程并修复React渲染警告 * fix(plugin): 修复插件发布和市场的路径不一致问题 * feat: 重构插件发布流程并添加插件删除功能 * fix(editor): 完善插件删除功能并修复多个关键问题 * fix(auth): 修复自动登录与手动登录的竞态条件问题 * feat(editor): 重构插件管理流程 * feat(editor): 支持 ZIP 文件直接发布插件 - 新增 PluginSourceParser 解析插件源 - 重构发布流程支持文件夹和 ZIP 两种方式 - 优化发布向导 UI * feat(editor): 插件市场支持多版本安装 - 插件解压到项目 plugins 目录 - 新增 Tauri 后端安装/卸载命令 - 支持选择任意版本安装 - 修复打包逻辑,保留完整 dist 目录结构 * feat(editor): 个人中心支持多版本管理 - 合并同一插件的不同版本 - 添加版本历史展开/折叠功能 - 禁止有待审核 PR 时更新插件 * fix(editor): 修复 InspectorRegistry 服务注册 - InspectorRegistry 实现 IService 接口 - 注册到 Core.services 供插件使用 * feat(behavior-tree-editor): 完善插件注册和文件操作 - 添加文件创建模板和操作处理器 - 实现右键菜单创建行为树功能 - 修复文件读取权限问题(使用 Tauri 命令) - 添加 BehaviorTreeEditorPanel 组件 - 修复 rollup 配置支持动态导入 * feat(plugin): 完善插件构建和发布流程 * fix(behavior-tree-editor): 完整恢复编辑器并修复 Toast 集成 * fix(behavior-tree-editor): 修复节点选中、连线跟随和文件加载问题并优化性能 * fix(behavior-tree-editor): 修复端口连接失败问题并优化连线样式 * refactor(behavior-tree-editor): 移除调试面板功能简化代码结构 * refactor(behavior-tree-editor): 清理冗余代码合并重复逻辑 * feat(behavior-tree-editor): 完善编辑器核心功能增强扩展性 * fix(lint): 修复ESLint错误确保CI通过 * refactor(behavior-tree-editor): 优化编辑器工具栏和编译器功能 * refactor(behavior-tree-editor): 清理技术债务,优化代码质量 * fix(editor-app): 修复字符串替换安全问题
479 lines
18 KiB
TypeScript
479 lines
18 KiB
TypeScript
import React from 'react';
|
|
import { Play, Pause, Square, SkipForward, Undo, Redo, ZoomIn, Save, FolderOpen, Download, Clipboard, Home } from 'lucide-react';
|
|
|
|
type ExecutionMode = 'idle' | 'running' | 'paused';
|
|
|
|
interface EditorToolbarProps {
|
|
executionMode: ExecutionMode;
|
|
canUndo: boolean;
|
|
canRedo: boolean;
|
|
hasUnsavedChanges?: boolean;
|
|
onPlay: () => void;
|
|
onPause: () => void;
|
|
onStop: () => void;
|
|
onStep: () => void;
|
|
onReset: () => void;
|
|
onUndo: () => void;
|
|
onRedo: () => void;
|
|
onResetView: () => void;
|
|
onSave?: () => void;
|
|
onOpen?: () => void;
|
|
onExport?: () => void;
|
|
onCopyToClipboard?: () => void;
|
|
onGoToRoot?: () => void;
|
|
}
|
|
|
|
export const EditorToolbar: React.FC<EditorToolbarProps> = ({
|
|
executionMode,
|
|
canUndo,
|
|
canRedo,
|
|
hasUnsavedChanges = false,
|
|
onPlay,
|
|
onPause,
|
|
onStop,
|
|
onStep,
|
|
onReset,
|
|
onUndo,
|
|
onRedo,
|
|
onResetView,
|
|
onSave,
|
|
onOpen,
|
|
onExport,
|
|
onCopyToClipboard,
|
|
onGoToRoot
|
|
}) => {
|
|
return (
|
|
<div style={{
|
|
position: 'absolute',
|
|
top: '12px',
|
|
left: '50%',
|
|
transform: 'translateX(-50%)',
|
|
display: 'flex',
|
|
gap: '6px',
|
|
backgroundColor: '#2a2a2a',
|
|
padding: '6px',
|
|
borderRadius: '8px',
|
|
boxShadow: '0 4px 12px rgba(0,0,0,0.5)',
|
|
border: '1px solid #3f3f3f',
|
|
zIndex: 100
|
|
}}>
|
|
{/* 文件操作组 */}
|
|
<div style={{
|
|
display: 'flex',
|
|
gap: '4px',
|
|
padding: '2px',
|
|
backgroundColor: '#1e1e1e',
|
|
borderRadius: '6px'
|
|
}}>
|
|
{onOpen && (
|
|
<button
|
|
onClick={onOpen}
|
|
style={{
|
|
padding: '6px 8px',
|
|
backgroundColor: '#3c3c3c',
|
|
border: 'none',
|
|
borderRadius: '4px',
|
|
color: '#ccc',
|
|
cursor: 'pointer',
|
|
fontSize: '13px',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
transition: 'all 0.15s'
|
|
}}
|
|
title="打开文件 (Ctrl+O)"
|
|
onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#4a4a4a'}
|
|
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = '#3c3c3c'}
|
|
>
|
|
<FolderOpen size={14} />
|
|
</button>
|
|
)}
|
|
|
|
{onSave && (
|
|
<button
|
|
onClick={onSave}
|
|
style={{
|
|
padding: '6px 8px',
|
|
backgroundColor: hasUnsavedChanges ? '#2563eb' : '#3c3c3c',
|
|
border: 'none',
|
|
borderRadius: '4px',
|
|
color: hasUnsavedChanges ? '#fff' : '#ccc',
|
|
cursor: 'pointer',
|
|
fontSize: '13px',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
transition: 'all 0.15s'
|
|
}}
|
|
title={`保存 (Ctrl+S)${hasUnsavedChanges ? ' - 有未保存的更改' : ''}`}
|
|
onMouseEnter={(e) => e.currentTarget.style.backgroundColor = hasUnsavedChanges ? '#1d4ed8' : '#4a4a4a'}
|
|
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = hasUnsavedChanges ? '#2563eb' : '#3c3c3c'}
|
|
>
|
|
<Save size={14} />
|
|
</button>
|
|
)}
|
|
|
|
{onExport && (
|
|
<button
|
|
onClick={onExport}
|
|
style={{
|
|
padding: '6px 8px',
|
|
backgroundColor: '#3c3c3c',
|
|
border: 'none',
|
|
borderRadius: '4px',
|
|
color: '#ccc',
|
|
cursor: 'pointer',
|
|
fontSize: '13px',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
transition: 'all 0.15s'
|
|
}}
|
|
title="导出运行时配置"
|
|
onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#4a4a4a'}
|
|
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = '#3c3c3c'}
|
|
>
|
|
<Download size={14} />
|
|
</button>
|
|
)}
|
|
|
|
{onCopyToClipboard && (
|
|
<button
|
|
onClick={onCopyToClipboard}
|
|
style={{
|
|
padding: '6px 8px',
|
|
backgroundColor: '#3c3c3c',
|
|
border: 'none',
|
|
borderRadius: '4px',
|
|
color: '#ccc',
|
|
cursor: 'pointer',
|
|
fontSize: '13px',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
transition: 'all 0.15s'
|
|
}}
|
|
title="复制JSON到剪贴板"
|
|
onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#4a4a4a'}
|
|
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = '#3c3c3c'}
|
|
>
|
|
<Clipboard size={14} />
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{/* 分隔符 */}
|
|
<div style={{
|
|
width: '1px',
|
|
backgroundColor: '#444',
|
|
margin: '2px 0'
|
|
}} />
|
|
|
|
{/* 执行控制组 */}
|
|
<div style={{
|
|
display: 'flex',
|
|
gap: '4px',
|
|
padding: '2px',
|
|
backgroundColor: '#1e1e1e',
|
|
borderRadius: '6px'
|
|
}}>
|
|
{/* 播放按钮 */}
|
|
<button
|
|
onClick={onPlay}
|
|
disabled={executionMode === 'running'}
|
|
style={{
|
|
padding: '6px 10px',
|
|
backgroundColor: executionMode === 'running' ? '#2a2a2a' : '#16a34a',
|
|
border: 'none',
|
|
borderRadius: '4px',
|
|
color: executionMode === 'running' ? '#666' : '#fff',
|
|
cursor: executionMode === 'running' ? 'not-allowed' : 'pointer',
|
|
fontSize: '13px',
|
|
fontWeight: 500,
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '4px',
|
|
transition: 'all 0.15s'
|
|
}}
|
|
title="运行 (Play)"
|
|
onMouseEnter={(e) => {
|
|
if (executionMode !== 'running') {
|
|
e.currentTarget.style.backgroundColor = '#15803d';
|
|
}
|
|
}}
|
|
onMouseLeave={(e) => {
|
|
if (executionMode !== 'running') {
|
|
e.currentTarget.style.backgroundColor = '#16a34a';
|
|
}
|
|
}}
|
|
>
|
|
<Play size={14} fill="currentColor" />
|
|
</button>
|
|
|
|
{/* 暂停按钮 */}
|
|
<button
|
|
onClick={onPause}
|
|
disabled={executionMode === 'idle'}
|
|
style={{
|
|
padding: '6px 10px',
|
|
backgroundColor: executionMode === 'idle' ? '#2a2a2a' : '#f59e0b',
|
|
border: 'none',
|
|
borderRadius: '4px',
|
|
color: executionMode === 'idle' ? '#666' : '#fff',
|
|
cursor: executionMode === 'idle' ? 'not-allowed' : 'pointer',
|
|
fontSize: '13px',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
transition: 'all 0.15s'
|
|
}}
|
|
title={executionMode === 'paused' ? '继续' : '暂停'}
|
|
onMouseEnter={(e) => {
|
|
if (executionMode !== 'idle') {
|
|
e.currentTarget.style.backgroundColor = '#d97706';
|
|
}
|
|
}}
|
|
onMouseLeave={(e) => {
|
|
if (executionMode !== 'idle') {
|
|
e.currentTarget.style.backgroundColor = '#f59e0b';
|
|
}
|
|
}}
|
|
>
|
|
{executionMode === 'paused' ? <Play size={14} fill="currentColor" /> : <Pause size={14} fill="currentColor" />}
|
|
</button>
|
|
|
|
{/* 停止按钮 */}
|
|
<button
|
|
onClick={onStop}
|
|
disabled={executionMode === 'idle'}
|
|
style={{
|
|
padding: '6px 10px',
|
|
backgroundColor: executionMode === 'idle' ? '#2a2a2a' : '#dc2626',
|
|
border: 'none',
|
|
borderRadius: '4px',
|
|
color: executionMode === 'idle' ? '#666' : '#fff',
|
|
cursor: executionMode === 'idle' ? 'not-allowed' : 'pointer',
|
|
fontSize: '13px',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
transition: 'all 0.15s'
|
|
}}
|
|
title="停止"
|
|
onMouseEnter={(e) => {
|
|
if (executionMode !== 'idle') {
|
|
e.currentTarget.style.backgroundColor = '#b91c1c';
|
|
}
|
|
}}
|
|
onMouseLeave={(e) => {
|
|
if (executionMode !== 'idle') {
|
|
e.currentTarget.style.backgroundColor = '#dc2626';
|
|
}
|
|
}}
|
|
>
|
|
<Square size={14} fill="currentColor" />
|
|
</button>
|
|
|
|
{/* 单步执行按钮 */}
|
|
<button
|
|
onClick={onStep}
|
|
disabled={executionMode !== 'idle' && executionMode !== 'paused'}
|
|
style={{
|
|
padding: '6px 10px',
|
|
backgroundColor: (executionMode !== 'idle' && executionMode !== 'paused') ? '#2a2a2a' : '#3b82f6',
|
|
border: 'none',
|
|
borderRadius: '4px',
|
|
color: (executionMode !== 'idle' && executionMode !== 'paused') ? '#666' : '#fff',
|
|
cursor: (executionMode !== 'idle' && executionMode !== 'paused') ? 'not-allowed' : 'pointer',
|
|
fontSize: '13px',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
transition: 'all 0.15s'
|
|
}}
|
|
title="单步执行"
|
|
onMouseEnter={(e) => {
|
|
if (executionMode === 'idle' || executionMode === 'paused') {
|
|
e.currentTarget.style.backgroundColor = '#2563eb';
|
|
}
|
|
}}
|
|
onMouseLeave={(e) => {
|
|
if (executionMode === 'idle' || executionMode === 'paused') {
|
|
e.currentTarget.style.backgroundColor = '#3b82f6';
|
|
}
|
|
}}
|
|
>
|
|
<SkipForward size={14} />
|
|
</button>
|
|
</div>
|
|
|
|
{/* 分隔符 */}
|
|
<div style={{
|
|
width: '1px',
|
|
backgroundColor: '#444',
|
|
margin: '2px 0'
|
|
}} />
|
|
|
|
{/* 视图控制 */}
|
|
<button
|
|
onClick={onResetView}
|
|
style={{
|
|
padding: '6px 10px',
|
|
backgroundColor: '#3c3c3c',
|
|
border: 'none',
|
|
borderRadius: '4px',
|
|
color: '#ccc',
|
|
cursor: 'pointer',
|
|
fontSize: '11px',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '4px',
|
|
transition: 'all 0.15s'
|
|
}}
|
|
title="重置视图 (滚轮缩放, Alt+拖动平移)"
|
|
onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#4a4a4a'}
|
|
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = '#3c3c3c'}
|
|
>
|
|
<ZoomIn size={13} />
|
|
<span>Reset View</span>
|
|
</button>
|
|
|
|
{/* 分隔符 */}
|
|
<div style={{
|
|
width: '1px',
|
|
backgroundColor: '#444',
|
|
margin: '2px 0'
|
|
}} />
|
|
|
|
{/* 历史控制组 */}
|
|
<div style={{
|
|
display: 'flex',
|
|
gap: '4px',
|
|
padding: '2px',
|
|
backgroundColor: '#1e1e1e',
|
|
borderRadius: '6px'
|
|
}}>
|
|
<button
|
|
onClick={onUndo}
|
|
disabled={!canUndo}
|
|
style={{
|
|
padding: '6px 8px',
|
|
backgroundColor: canUndo ? '#3c3c3c' : '#2a2a2a',
|
|
border: 'none',
|
|
borderRadius: '4px',
|
|
color: canUndo ? '#ccc' : '#666',
|
|
cursor: canUndo ? 'pointer' : 'not-allowed',
|
|
fontSize: '13px',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
transition: 'all 0.15s'
|
|
}}
|
|
title="撤销 (Ctrl+Z)"
|
|
onMouseEnter={(e) => {
|
|
if (canUndo) {
|
|
e.currentTarget.style.backgroundColor = '#4a4a4a';
|
|
}
|
|
}}
|
|
onMouseLeave={(e) => {
|
|
if (canUndo) {
|
|
e.currentTarget.style.backgroundColor = '#3c3c3c';
|
|
}
|
|
}}
|
|
>
|
|
<Undo size={14} />
|
|
</button>
|
|
|
|
<button
|
|
onClick={onRedo}
|
|
disabled={!canRedo}
|
|
style={{
|
|
padding: '6px 8px',
|
|
backgroundColor: canRedo ? '#3c3c3c' : '#2a2a2a',
|
|
border: 'none',
|
|
borderRadius: '4px',
|
|
color: canRedo ? '#ccc' : '#666',
|
|
cursor: canRedo ? 'pointer' : 'not-allowed',
|
|
fontSize: '13px',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
transition: 'all 0.15s'
|
|
}}
|
|
title="重做 (Ctrl+Shift+Z / Ctrl+Y)"
|
|
onMouseEnter={(e) => {
|
|
if (canRedo) {
|
|
e.currentTarget.style.backgroundColor = '#4a4a4a';
|
|
}
|
|
}}
|
|
onMouseLeave={(e) => {
|
|
if (canRedo) {
|
|
e.currentTarget.style.backgroundColor = '#3c3c3c';
|
|
}
|
|
}}
|
|
>
|
|
<Redo size={14} />
|
|
</button>
|
|
</div>
|
|
|
|
{/* 状态指示器 */}
|
|
<div style={{
|
|
padding: '6px 12px',
|
|
backgroundColor: '#1e1e1e',
|
|
borderRadius: '6px',
|
|
fontSize: '11px',
|
|
color: '#999',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '6px',
|
|
fontWeight: 500,
|
|
minWidth: '70px'
|
|
}}>
|
|
<span style={{
|
|
width: '6px',
|
|
height: '6px',
|
|
borderRadius: '50%',
|
|
backgroundColor:
|
|
executionMode === 'running' ? '#16a34a' :
|
|
executionMode === 'paused' ? '#f59e0b' : '#666',
|
|
boxShadow: executionMode !== 'idle' ? `0 0 8px ${
|
|
executionMode === 'running' ? '#16a34a' :
|
|
executionMode === 'paused' ? '#f59e0b' : 'transparent'
|
|
}` : 'none',
|
|
transition: 'all 0.2s'
|
|
}} />
|
|
<span style={{
|
|
color: executionMode === 'running' ? '#16a34a' :
|
|
executionMode === 'paused' ? '#f59e0b' : '#888'
|
|
}}>
|
|
{executionMode === 'idle' ? 'Idle' :
|
|
executionMode === 'running' ? 'Running' : 'Paused'}
|
|
</span>
|
|
</div>
|
|
|
|
{onGoToRoot && (
|
|
<>
|
|
<div style={{
|
|
width: '1px',
|
|
backgroundColor: '#444',
|
|
margin: '2px 0'
|
|
}} />
|
|
<button
|
|
onClick={onGoToRoot}
|
|
style={{
|
|
padding: '6px 10px',
|
|
backgroundColor: '#3c3c3c',
|
|
border: 'none',
|
|
borderRadius: '4px',
|
|
color: '#ccc',
|
|
cursor: 'pointer',
|
|
fontSize: '11px',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '4px',
|
|
transition: 'all 0.15s'
|
|
}}
|
|
title="回到根节点"
|
|
onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#4a4a4a'}
|
|
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = '#3c3c3c'}
|
|
>
|
|
<Home size={13} />
|
|
<span>Root</span>
|
|
</button>
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|