refactor(editor): 提取行为树编辑器为独立包并重构编辑器架构 (#216)
* 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): 修复字符串替换安全问题
This commit is contained in:
@@ -1,15 +1,32 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { EditorPluginManager, IEditorPluginMetadata, EditorPluginCategory } from '@esengine/editor-core';
|
||||
import * as LucideIcons from 'lucide-react';
|
||||
import { Package, CheckCircle, XCircle, Search, Grid, List, ChevronDown, ChevronRight, X, RefreshCw } from 'lucide-react';
|
||||
import {
|
||||
Package,
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
Search,
|
||||
Grid,
|
||||
List,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
X,
|
||||
RefreshCw,
|
||||
ShoppingCart
|
||||
} from 'lucide-react';
|
||||
import { PluginMarketPanel } from './PluginMarketPanel';
|
||||
import { PluginMarketService } from '../services/PluginMarketService';
|
||||
import { GitHubService } from '../services/GitHubService';
|
||||
import '../styles/PluginManagerWindow.css';
|
||||
|
||||
interface PluginManagerWindowProps {
|
||||
pluginManager: EditorPluginManager;
|
||||
githubService: GitHubService;
|
||||
onClose: () => void;
|
||||
onRefresh?: () => Promise<void>;
|
||||
onOpen?: () => void;
|
||||
locale: string;
|
||||
projectPath: string | null;
|
||||
}
|
||||
|
||||
const categoryIcons: Record<EditorPluginCategory, string> = {
|
||||
@@ -20,7 +37,7 @@ const categoryIcons: Record<EditorPluginCategory, string> = {
|
||||
[EditorPluginCategory.ImportExport]: 'Package'
|
||||
};
|
||||
|
||||
export function PluginManagerWindow({ pluginManager, onClose, onRefresh, onOpen, locale }: PluginManagerWindowProps) {
|
||||
export function PluginManagerWindow({ pluginManager, githubService, onClose, onRefresh, onOpen, locale, projectPath }: PluginManagerWindowProps) {
|
||||
const t = (key: string) => {
|
||||
const translations: Record<string, Record<string, string>> = {
|
||||
zh: {
|
||||
@@ -43,7 +60,9 @@ export function PluginManagerWindow({ pluginManager, onClose, onRefresh, onOpen,
|
||||
categoryWindows: '窗口',
|
||||
categoryInspectors: '检查器',
|
||||
categorySystem: '系统',
|
||||
categoryImportExport: '导入/导出'
|
||||
categoryImportExport: '导入/导出',
|
||||
tabInstalled: '已安装',
|
||||
tabMarketplace: '插件市场'
|
||||
},
|
||||
en: {
|
||||
title: 'Plugin Manager',
|
||||
@@ -65,7 +84,9 @@ export function PluginManagerWindow({ pluginManager, onClose, onRefresh, onOpen,
|
||||
categoryWindows: 'Windows',
|
||||
categoryInspectors: 'Inspectors',
|
||||
categorySystem: 'System',
|
||||
categoryImportExport: 'Import/Export'
|
||||
categoryImportExport: 'Import/Export',
|
||||
tabInstalled: 'Installed',
|
||||
tabMarketplace: 'Marketplace'
|
||||
}
|
||||
};
|
||||
return translations[locale]?.[key] || translations.en?.[key] || key;
|
||||
@@ -81,6 +102,7 @@ export function PluginManagerWindow({ pluginManager, onClose, onRefresh, onOpen,
|
||||
};
|
||||
return t(categoryKeys[category]);
|
||||
};
|
||||
const [activeTab, setActiveTab] = useState<'installed' | 'marketplace'>('installed');
|
||||
const [plugins, setPlugins] = useState<IEditorPluginMetadata[]>([]);
|
||||
const [filter, setFilter] = useState('');
|
||||
const [viewMode, setViewMode] = useState<'grid' | 'list'>('list');
|
||||
@@ -89,6 +111,13 @@ export function PluginManagerWindow({ pluginManager, onClose, onRefresh, onOpen,
|
||||
);
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
|
||||
const marketService = useMemo(() => new PluginMarketService(pluginManager), [pluginManager]);
|
||||
|
||||
// 设置项目路径到 marketService
|
||||
useEffect(() => {
|
||||
marketService.setProjectPath(projectPath);
|
||||
}, [projectPath, marketService]);
|
||||
|
||||
const updatePluginList = () => {
|
||||
const allPlugins = pluginManager.getAllPluginMetadata();
|
||||
setPlugins(allPlugins);
|
||||
@@ -154,13 +183,16 @@ export function PluginManagerWindow({ pluginManager, onClose, onRefresh, onOpen,
|
||||
);
|
||||
});
|
||||
|
||||
const pluginsByCategory = filteredPlugins.reduce((acc, plugin) => {
|
||||
if (!acc[plugin.category]) {
|
||||
acc[plugin.category] = [];
|
||||
}
|
||||
acc[plugin.category].push(plugin);
|
||||
return acc;
|
||||
}, {} as Record<EditorPluginCategory, IEditorPluginMetadata[]>);
|
||||
const pluginsByCategory = filteredPlugins.reduce(
|
||||
(acc, plugin) => {
|
||||
if (!acc[plugin.category]) {
|
||||
acc[plugin.category] = [];
|
||||
}
|
||||
acc[plugin.category].push(plugin);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<EditorPluginCategory, IEditorPluginMetadata[]>
|
||||
);
|
||||
|
||||
const enabledCount = plugins.filter((p) => p.enabled).length;
|
||||
const disabledCount = plugins.filter((p) => !p.enabled).length;
|
||||
@@ -185,9 +217,7 @@ export function PluginManagerWindow({ pluginManager, onClose, onRefresh, onOpen,
|
||||
{plugin.enabled ? <CheckCircle size={18} /> : <XCircle size={18} />}
|
||||
</button>
|
||||
</div>
|
||||
{plugin.description && (
|
||||
<div className="plugin-card-description">{plugin.description}</div>
|
||||
)}
|
||||
{plugin.description && <div className="plugin-card-description">{plugin.description}</div>}
|
||||
<div className="plugin-card-footer">
|
||||
<span className="plugin-card-category">
|
||||
{(() => {
|
||||
@@ -218,9 +248,7 @@ export function PluginManagerWindow({ pluginManager, onClose, onRefresh, onOpen,
|
||||
{plugin.displayName}
|
||||
<span className="plugin-list-version">v{plugin.version}</span>
|
||||
</div>
|
||||
{plugin.description && (
|
||||
<div className="plugin-list-description">{plugin.description}</div>
|
||||
)}
|
||||
{plugin.description && <div className="plugin-list-description">{plugin.description}</div>}
|
||||
</div>
|
||||
<div className="plugin-list-status">
|
||||
{plugin.enabled ? (
|
||||
@@ -253,118 +281,157 @@ export function PluginManagerWindow({ pluginManager, onClose, onRefresh, onOpen,
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="plugin-toolbar">
|
||||
<div className="plugin-toolbar-left">
|
||||
<div className="plugin-search">
|
||||
<Search size={14} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder={t('searchPlaceholder')}
|
||||
value={filter}
|
||||
onChange={(e) => setFilter(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="plugin-toolbar-right">
|
||||
<div className="plugin-stats">
|
||||
<span className="stat-item enabled">
|
||||
<CheckCircle size={14} />
|
||||
{enabledCount} {t('enabled')}
|
||||
</span>
|
||||
<span className="stat-item disabled">
|
||||
<XCircle size={14} />
|
||||
{disabledCount} {t('disabled')}
|
||||
</span>
|
||||
</div>
|
||||
{onRefresh && (
|
||||
<button
|
||||
className="plugin-refresh-btn"
|
||||
onClick={handleRefresh}
|
||||
disabled={isRefreshing}
|
||||
title={t('refreshPluginList')}
|
||||
style={{
|
||||
padding: '6px 12px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '6px',
|
||||
backgroundColor: '#0e639c',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
color: '#fff',
|
||||
cursor: isRefreshing ? 'not-allowed' : 'pointer',
|
||||
fontSize: '12px',
|
||||
opacity: isRefreshing ? 0.6 : 1
|
||||
}}
|
||||
>
|
||||
<RefreshCw size={14} className={isRefreshing ? 'spinning' : ''} />
|
||||
{t('refresh')}
|
||||
</button>
|
||||
)}
|
||||
<div className="plugin-view-mode">
|
||||
<button
|
||||
className={viewMode === 'list' ? 'active' : ''}
|
||||
onClick={() => setViewMode('list')}
|
||||
title={t('listView')}
|
||||
>
|
||||
<List size={14} />
|
||||
</button>
|
||||
<button
|
||||
className={viewMode === 'grid' ? 'active' : ''}
|
||||
onClick={() => setViewMode('grid')}
|
||||
title={t('gridView')}
|
||||
>
|
||||
<Grid size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="plugin-manager-tabs">
|
||||
<button
|
||||
className={`plugin-manager-tab ${activeTab === 'installed' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('installed')}
|
||||
>
|
||||
<Package size={16} />
|
||||
{t('tabInstalled')}
|
||||
</button>
|
||||
<button
|
||||
className={`plugin-manager-tab ${activeTab === 'marketplace' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('marketplace')}
|
||||
>
|
||||
<ShoppingCart size={16} />
|
||||
{t('tabMarketplace')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="plugin-content">
|
||||
{plugins.length === 0 ? (
|
||||
<div className="plugin-empty">
|
||||
<Package size={48} />
|
||||
<p>{t('noPlugins')}</p>
|
||||
{activeTab === 'installed' && (
|
||||
<>
|
||||
<div className="plugin-toolbar">
|
||||
<div className="plugin-toolbar-left">
|
||||
<div className="plugin-search">
|
||||
<Search size={14} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder={t('searchPlaceholder')}
|
||||
value={filter}
|
||||
onChange={(e) => setFilter(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="plugin-toolbar-right">
|
||||
<div className="plugin-stats">
|
||||
<span className="stat-item enabled">
|
||||
<CheckCircle size={14} />
|
||||
{enabledCount} {t('enabled')}
|
||||
</span>
|
||||
<span className="stat-item disabled">
|
||||
<XCircle size={14} />
|
||||
{disabledCount} {t('disabled')}
|
||||
</span>
|
||||
</div>
|
||||
{onRefresh && (
|
||||
<button
|
||||
className="plugin-refresh-btn"
|
||||
onClick={handleRefresh}
|
||||
disabled={isRefreshing}
|
||||
title={t('refreshPluginList')}
|
||||
style={{
|
||||
padding: '6px 12px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '6px',
|
||||
backgroundColor: '#0e639c',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
color: '#fff',
|
||||
cursor: isRefreshing ? 'not-allowed' : 'pointer',
|
||||
fontSize: '12px',
|
||||
opacity: isRefreshing ? 0.6 : 1
|
||||
}}
|
||||
>
|
||||
<RefreshCw size={14} className={isRefreshing ? 'spinning' : ''} />
|
||||
{t('refresh')}
|
||||
</button>
|
||||
)}
|
||||
<div className="plugin-view-mode">
|
||||
<button
|
||||
className={viewMode === 'list' ? 'active' : ''}
|
||||
onClick={() => setViewMode('list')}
|
||||
title={t('listView')}
|
||||
>
|
||||
<List size={14} />
|
||||
</button>
|
||||
<button
|
||||
className={viewMode === 'grid' ? 'active' : ''}
|
||||
onClick={() => setViewMode('grid')}
|
||||
title={t('gridView')}
|
||||
>
|
||||
<Grid size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="plugin-categories">
|
||||
{Object.entries(pluginsByCategory).map(([category, categoryPlugins]) => {
|
||||
const cat = category as EditorPluginCategory;
|
||||
const isExpanded = expandedCategories.has(cat);
|
||||
|
||||
return (
|
||||
<div key={category} className="plugin-category">
|
||||
<div
|
||||
className="plugin-category-header"
|
||||
onClick={() => toggleCategory(cat)}
|
||||
>
|
||||
<button className="plugin-category-toggle">
|
||||
{isExpanded ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
|
||||
</button>
|
||||
<span className="plugin-category-icon">
|
||||
{(() => {
|
||||
const CategoryIcon = (LucideIcons as any)[categoryIcons[cat]];
|
||||
return CategoryIcon ? <CategoryIcon size={16} /> : null;
|
||||
})()}
|
||||
</span>
|
||||
<span className="plugin-category-name">{getCategoryName(cat)}</span>
|
||||
<span className="plugin-category-count">
|
||||
{categoryPlugins.length}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="plugin-content"
|
||||
style={{ display: activeTab === 'installed' ? 'block' : 'none' }}
|
||||
>
|
||||
{plugins.length === 0 ? (
|
||||
<div className="plugin-empty">
|
||||
<Package size={48} />
|
||||
<p>{t('noPlugins')}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="plugin-categories">
|
||||
{Object.entries(pluginsByCategory).map(([category, categoryPlugins]) => {
|
||||
const cat = category as EditorPluginCategory;
|
||||
const isExpanded = expandedCategories.has(cat);
|
||||
|
||||
{isExpanded && (
|
||||
<div className={`plugin-category-content ${viewMode}`}>
|
||||
{viewMode === 'grid'
|
||||
? categoryPlugins.map(renderPluginCard)
|
||||
: categoryPlugins.map(renderPluginList)}
|
||||
return (
|
||||
<div key={category} className="plugin-category">
|
||||
<div
|
||||
className="plugin-category-header"
|
||||
onClick={() => toggleCategory(cat)}
|
||||
>
|
||||
<button className="plugin-category-toggle">
|
||||
{isExpanded ? (
|
||||
<ChevronDown size={16} />
|
||||
) : (
|
||||
<ChevronRight size={16} />
|
||||
)}
|
||||
</button>
|
||||
<span className="plugin-category-icon">
|
||||
{(() => {
|
||||
const CategoryIcon = (LucideIcons as any)[
|
||||
categoryIcons[cat]
|
||||
];
|
||||
return CategoryIcon ? <CategoryIcon size={16} /> : null;
|
||||
})()}
|
||||
</span>
|
||||
<span className="plugin-category-name">{getCategoryName(cat)}</span>
|
||||
<span className="plugin-category-count">
|
||||
{categoryPlugins.length}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{isExpanded && (
|
||||
<div className={`plugin-category-content ${viewMode}`}>
|
||||
{viewMode === 'grid'
|
||||
? categoryPlugins.map(renderPluginCard)
|
||||
: categoryPlugins.map(renderPluginList)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{activeTab === 'marketplace' && (
|
||||
<PluginMarketPanel
|
||||
marketService={marketService}
|
||||
locale={locale}
|
||||
projectPath={projectPath}
|
||||
onReloadPlugins={onRefresh}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user